قصة البحث والتطوير في 3 أجزاء. الجزء 2 - التنموي.
هناك العديد من الزان - المزيد من الفوائد.
في الجزء الأول من المقالة ، تعرفنا على بعض الأدوات اللازمة لتنظيم الأنفاق العكسية ، ونظرنا في مزاياها وعيوبها ، ودرسنا آلية مُضاعِف Yamux ووصفنا المتطلبات الأساسية لوحدة Powerhell المنشأة حديثًا. لقد حان الوقت للبدء في تطوير وحدة بوويرشيل العميل للتنفيذ
الجاهزة لنفق
RSocksTun العكسي.
بادئ ذي بدء ، نحن بحاجة إلى أن نفهم في أي وضع ستعمل وحدتنا. من الواضح ، بالنسبة للنقل الأولي للبيانات ، سنحتاج إلى استخدام آلية مقبس النوافذ وإمكانيات .Net لتدفق القراءة والكتابة إلى المقابس. ولكن ، من ناحية أخرى ، ل نظرًا لأن الوحدة النمطية لدينا يجب أن تخدم عدة تدفقات yamux في نفس الوقت ، فلا ينبغي لجميع عمليات الإدخال / الإخراج أن تمنع تنفيذ برنامجنا تمامًا. يشير هذا إلى استنتاج مفاده أن الوحدة النمطية لدينا يجب أن تستخدم تعدد مؤشرات البرامج وتنفيذ عمليات القراءة والكتابة مع خادم yamux ، وكذلك عمليات القراءة والكتابة إلى الخوادم الوجهة في تدفقات البرامج المختلفة. حسنًا ، بالطبع ، من الضروري توفير آلية للتفاعل بين التدفقات المتوازية. لحسن الحظ ، يوفر بوويرشيل فرصًا واسعة لإطلاق وإدارة تدفقات البرنامج.
الخوارزمية العامة للعمل
وبالتالي ، يجب أن تكون الخوارزمية العامة لعملائنا مثل هذا:
- تأسيس اتصال SSL بالخادم ؛
- تسجيل الدخول بكلمة مرور حتى يتمكن الخادم من التمييز بيننا وبين ضابط الأمن ؛
- انتظر حزمة yamux لتثبيت دفق جديد ، والاستجابة بشكل دوري لطلبات الخادم keepalive ؛
- بدء دفق برنامج socksScript جديد (يجب عدم الخلط بينه وبين دفق) بمجرد وصول حزمة yamux لتثبيت دفق جديد. داخل socksScript ، تنفيذ عمل socks5 الخادم ؛
- عند وصول حزمة تحتوي على بيانات من yamux - فهم من رأس 12 بايت أي من التدفقات المقصود بها البيانات ، وكذلك حجمها ، وقراءة البيانات من خادم yamux ونقل البيانات المستلمة إلى الدفق مع رقم الدفق المقابل ؛
- مراقبة دورية لتوافر البيانات المعدة لخادم yamux في كل من سكربتات الجوارب قيد التشغيل. إذا كان هناك مثل هذه البيانات ، أضف رأس البايت 12 بايت الموافق لها وأرسلها إلى خادم yamux ؛
- عند وصول حزمة yamux لإغلاق الدفق ، قم بنقل الإشارة إلى الدفق المقابل لإنهاء الدفق وفصله ، وبعد ذلك ، أكمل الدفق نفسه ؛
لذلك ، من الضروري في عميلنا تنفيذ 3 تدفقات برنامج على الأقل:
- الرئيسية ، التي ستنشئ الاتصال ، قم بتسجيل الدخول إلى خادم yamux ، واستقبال البيانات منه ، ومعالجة رؤوس yamux وإرسال البيانات الأولية بالفعل إلى تدفقات البرنامج الأخرى ؛
- تيارات مع خوادم الجوارب. قد يكون هناك عدة - واحد لكل تيار. أنها تنفذ وظيفة socks5. سوف تتفاعل هذه التدفقات مع نقاط الوجهة على الشبكة الداخلية ؛
- التدفق العكسي. يتلقى البيانات من تدفقات الجوارب ، ويضيف رؤوس yamux إليهم ويرسلها إلى خادم yamux ؛
وبالطبع ، نحن بحاجة إلى توفير التفاعل بين كل هذه التدفقات.
لا نحتاج فقط إلى توفير مثل هذا التفاعل ، ولكن أيضًا للحصول على راحة تدفق المدخلات والمخرجات (على غرار المقابس). سيكون أنسب آلية لاستخدام أنابيب البرنامج. في Windows ، يتم تسجيل الأنابيب عندما يكون لكل أنبوب اسم خاص به ومجهول - يتم تحديد كل أنبوب بواسطة معالجه. من أجل السرية ، بالطبع ، سوف نستخدم أنابيب مجهولة المصدر. (بعد كل شيء ، لا نريد حساب الوحدة الخاصة بنا باستخدام أنابيب مسجلة في النظام - نعم؟). وبالتالي ، بين التدفقات الرئيسية / العكسية وتدفقات الجوارب ، سيكون التفاعل من خلال أنابيب مجهولة المصدر ، مما يدعم التدفق غير المتزامن I / O. بين التدفقات الرئيسية وتدفقات العودة ، سيتم الاتصال من خلال آلية الكائن المشترك (المتغيرات المشتركة المتزامنة) (المزيد حول ماهية هذه المتغيرات وكيفية التعايش معها يمكنك قراءتها
هنا ).
يجب تخزين المعلومات حول تشغيل تدفقات الجوارب في بنية البيانات المقابلة. عند إنشاء خيط جوارب في هذا الهيكل ، يجب أن نكتب:
- رقم جلسة yamux: $ ymxstream؛
- 4 متغيرات للعمل مع الأنابيب (القنوات): $ cipipe ، $ copipe ، $ sipipe ، $ sopipe. نظرًا لأن القنوات المجهولة تعمل إما داخل الشبكة أو في الخارج ، فنحن نحتاج إلى قناتين مجهولي الهوية لكل قناة من الجوارب ، يجب أن يكون لكل منهما طرفان (pipestream) (الخادم والعميل) ؛
- نتيجة استدعاء الدفق هي $ AsyncJobResult؛
- تيار معالج - Psobj $. من خلاله سنغلق الدفق ونصدر الموارد ؛
- نتيجة القراءة غير المتزامنة من توجيه مجهول بواسطة الدفق العكسي ($ readjob). يتم استخدام هذا المتغير في دفق yamuxScript العكسي للقراءة غير المتزامنة من توجيه الإخراج المقابل؛
- العازلة لقراءة البيانات لكل تيار الجوارب ؛
التيار الرئيسي
لذلك ، من وجهة نظر معالجة البيانات ، تم بناء عمل برنامجنا على النحو التالي:
- يقوم جانب الخادم (rsockstun - الذي تم تنفيذه على Golang) برفع خادم ssl وينتظر الاتصالات من العميل ؛
- عند تلقي اتصال من العميل ، يقوم الخادم بفحص كلمة المرور ، وإذا كان صحيحًا ، يقوم بإنشاء اتصال yamux ، ويزيد منفذ الجوارب وينتظر الاتصالات من عملاء الجوارب (وكلاءنا ، متصفحنا ، إلخ) ، وتبادل حزم keepalive بشكل دوري مع عملائنا. إذا كانت كلمة المرور غير صحيحة - تتم إعادة توجيه إلى الصفحة التي حددناها عند تثبيت الخادم (هذه صفحة "قانونية" للمسؤول اليقظ لأمن المعلومات) ؛
- عند تلقي اتصال من عميل الجوارب ، يرسل الخادم حزمة yamux إلى عميلنا لإنشاء دفق جديد (YMX SYN) ؛
الحصول على رأس Yamux وتحليلهتقوم الوحدة الخاصة بنا أولاً بإنشاء اتصال SSL بالخادم وتسجيل الدخول باستخدام كلمة مرور:
$tcpConnection = New-Object System.Net.Sockets.TcpClient($server, $port) $tcpStream = New-Object System.Net.Security.SslStream($tcpConnection.GetStream(),$false,({$True} -as [Net.Security.RemoteCertificateValidationCallback])) $tcpStream.AuthenticateAsClient('127.0.0.1')
بعد ذلك ، ينتظر البرنامج النصي رأس yamux من 12 بايت ويقوم بتوزيعه.
هناك فارق بسيط ... كما تظهر الممارسة ، ببساطة قراءة 12 بايت من المقبس:
$num = $tcpStream.Read($tmpbuffer,0,12)
لا يكفي ، حيث يمكن إكمال عملية القراءة بعد وصول جزء فقط من وحدات البايت الضرورية. لذلك ، نحتاج إلى انتظار 12 بايت في الحلقة:
do { try { $num = $tcpStream.Read($tmpbuffer,0,12) } catch {} $tnum += $num $ymxbuffer += $tmpbuffer[0..($num-1)] }while ($tnum -lt 12 -and $tcpConnection.Connected)
بعد اكتمال الحلقة ، يجب علينا تحليل رأس 12 بايت الموجود في المتغير $ ymxbuffer لنوعه والأعلام المحددة وفقًا لمواصفات Yamux.
يمكن أن يكون رأس Yamux من عدة أنواع:
- ymx syn - تثبيت دفق جديد ؛
- ymx fin-stream إكمال؛
- بيانات ymx - تمثل معلومات حول البيانات (ما حجمها وما هو الدفق المقصود منها) ؛
- ymx ping - رسالة keepalive ؛
- ymx win update - تأكيد نقل جزء من البيانات ؛
أي شيء لا يلائم الأنواع المدرجة من رؤوس yamux يعتبر حالة استثنائية. هناك 10 استثناءات من هذا القبيل ، ونعتقد أن هناك خطأ ما هنا ونكمل عمل وحدتنا.
(بالإضافة إلى محو جميع ملفاتنا ، امسح القرص ، وقم بتغيير الاسم الأخير ، وقم بجواز سفر جديد ، ثم اترك البلد ، وما إلى ذلك وفقًا للقائمة ...)خلق موضوع جديد الجوارببعد تلقي حزمة yamux لإنشاء دفق جديد ، ينشئ عميلنا أنبوبين مجهولين للخادم ($ sipipe ، $ sopipe) ، للداخل / للخارج ، على التوالي ، ينشئ أنابيب عميل ($ cipipe ، $ copipe) بناءً عليها:
$sipipe = new-object System.IO.Pipes.AnonymousPipeServerStream(1) $sopipe = new-object System.IO.Pipes.AnonymousPipeServerStream(2,1) $sipipe_clHandle = $sipipe.GetClientHandleAsString() $sopipe_clHandle = $sopipe.GetClientHandleAsString() $cipipe = new-object System.IO.Pipes.AnonymousPipeClientStream(1,$sopipe_clHandle) $copipe = new-object System.IO.Pipes.AnonymousPipeClientStream(2,$sipipe_clHandle)
ينشئ مساحة تشغيل لتيار الجوارب ، ويضبط المتغيرات المشتركة للتفاعل مع هذا التدفق (StopFlag) ، ويقوم بتشغيل scriptblock SocksScript ، الذي ينفذ وظيفة خادم الجوارب في دفق منفصل:
$state = [PSCustomObject]@{"StreamID"=$ymxstream;"inputStream"=$cipipe;"outputStream"=$copipe} $PS = [PowerShell]::Create() $socksrunspace = [runspacefactory]::CreateRunspace() $socksrunspace.Open() $socksrunspace.SessionStateProxy.SetVariable("StopFlag",$StopFlag) $PS.Runspace = $socksrunspace $PS.AddScript($socksScript).AddArgument($state) | Out-Null [System.IAsyncResult]$AsyncJobResult = $null $StopFlag[$ymxstream] = 0 $AsyncJobResult = $PS.BeginInvoke()
تتم كتابة المتغيرات التي تم إنشاؤها في بنية ArrayList خاصة - تناظرية قاموس في بيثون
[System.Collections.ArrayList]$streams = @{}
تحدث الإضافة من خلال طريقة الإضافة المدمجة:
$streams.add(@{ymxId=$ymxstream;cinputStream=$cipipe;sinputStream=$sipipe;coutputStream=$copipe;soutputStream=$sopipe;asyncobj=$AsyncJobResult;psobj=$PS;readjob=$null;readbuffer=$readbuffer}) | out-null
Yamux معالجة البياناتعند استلام البيانات الموجهة لأي دفق من الجوارب من خادم yamux ، يجب أن نحدد رقم دفق yamux (رقم دفق الجوارب الذي تهدف إليه هذه البيانات) وعدد بايتات البيانات من رأس yamux المكون من 12 بايت:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0) $ymxcount = [bitconverter]::ToInt32($buffer[11..8],0)
بعد ذلك ، من دفق ArrayList على طول حقل ymxId ، نحصل على معالجات الخرج الخارجي المقابل لتيار الجوارب هذا:
if ($streams.Count -gt 1){$streamind = $streams.ymxId.IndexOf($ymxstream)} else {$streamind = 0} $outStream = $streams[$streamind].soutputStream
بعد ذلك ، نقرأ البيانات من مأخذ التوصيل ، ونتذكر أننا بحاجة إلى قراءة عدد معين من البايتات من خلال الحلقة:
$databuffer = $null $tnum = 0 do { if ($buffer.length -le ($ymxcount-$tnum)) { $num = $tcpStream.Read($buffer,0,$buffer.Length) }else { $num = $tcpStream.Read($buffer,0,($ymxcount-$tnum)) } $tnum += $num $databuffer += $buffer[0..($num-1)] }while ($tnum -lt $ymxcount -and $tcpConnection.Connected)
وكتابة البيانات المستلمة إلى الأنبوب المقابل:
$num = $tcpStream.Read($buffer,0,$ymxcount) $outStream.Write($buffer,0,$ymxcount)
Yamux FIN Processing - End Streamعندما نتلقى حزمة من خادم yamix تشير إلى إغلاق الدفق ، نحصل أولاً على رقم دفق yamux من رأس 12 بايت:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0)
ثم ، من خلال متغير مشترك (أو بالأحرى ، مجموعة من العلامات ، حيث يكون الفهرس هو رقم تدفق yamux) ، فإننا نشير إلى مؤشر ترابط الجوارب لإكمال:
if ($streams.Count -gt 1){$streamind = $streams.ymxId.IndexOf($ymxstream)} else {$streamind = 0} if ($StopFlag[$ymxstream] -eq 0){ write-host "stopflag is 0. Setting to 1" $StopFlag[$ymxstream] = 1 }
بعد ضبط العلم ، قبل قتل مجرى الجوارب ، تحتاج إلى الانتظار فترة زمنية معينة لتيار الجوارب لمعالجة هذا العلم. 200 مللي ثانية كافية لهذا:
start-sleep -milliseconds 200 #wait for thread check flag
ثم أغلق جميع المواسير المرتبطة بهذا الدفق ، وأغلقي مساحة Runspace المقابلة وقم بقتل كائن Powershell لتوفير موارد مجانية:
$streams[$streamind].cinputStream.close() $streams[$streamind].coutputStream.close() $streams[$streamind].sinputStream.close() $streams[$streamind].soutputStream.close() $streams[$streamind].psobj.Runspace.close() $streams[$streamind].psobj.Dispose() $streams[$streamind].readbuffer.clear()
بعد إغلاق مجموعة الجوارب ، نحتاج إلى إزالة العنصر المقابل من تدفقات ArrayList:
$streams.RemoveAt($streamind)
وفي النهاية ، نحتاج إلى إجبار أداة تجميع مجمعي البيانات المهملة .Net على تحرير الموارد التي يستخدمها مؤشر الترابط. خلاف ذلك ، سيستهلك البرنامج النصي لدينا حوالي 100-200 ميغابايت من الذاكرة ، والتي يمكن أن تسترعي انتباه مستخدم ذي خبرة وتآكل ، لكننا لسنا بحاجة إلى ذلك:
[System.GC]::Collect()#clear garbage to minimize memory usage
Yamux Script - التدفق العكسي
كما ذكر أعلاه ، تتم معالجة البيانات الواردة من تدفقات الجوارب من خلال دفق yamuxScript منفصل ، يبدأ من البداية (بعد اتصال ناجح بالخادم). تتمثل مهمتها في الاستقصاء الدوري لأنابيب إخراج تدفقات الجوارب الموجودة في تدفقات ArrayList $:
foreach ($stream in $state.streams){ ... }
وإذا كانت هناك بيانات بها ، فقم بإرسالها إلى خادم yamux ، حيث سبق أن قدمت رأس yamux المقابل من 12 بايت والذي يحتوي على رقم جلسة yamux وعدد وحدات بايت البيانات:
if ($stream.readjob -eq $null){ $stream.readjob = $stream.sinputStream.ReadAsync($stream.readbuffer,0,1024) }elseif ( $stream.readjob.IsCompleted ){ #if read asyncjob completed - generate yamux header $outbuf = [byte[]](0x00,0x00,0x00,0x00)+ [bitconverter]::getbytes([int32]$stream.ymxId)[3..0]+ [bitconverter]::getbytes([int32]$stream.readjob.Result)[3..0] $state.tcpstream.Write($outbuf,0,12) #write raw data from socks thread to yamux $state.tcpstream.Write($stream.readbuffer,0,$stream.readjob.Result) $state.tcpstream.flush() #create new readasync job $stream.readjob = $stream.sinputStream.ReadAsync($stream.readbuffer,0,1024) }else{ #write-host "Not readed" }
يراقب YamuxScript أيضًا العلامة في صفيف StopFlag $ المشترك لكل من مؤشرات ترابط socksScript التي يتم تنفيذها. يمكن تعيين هذه العلامة على 2 إذا كان الخادم البعيد الذي يعمل socksScript يعمل مع قطع الاتصال. في هذه الحالة ، يجب الإبلاغ عن المعلومات إلى عميل الجوارب. السلسلة على النحو التالي: يجب على yamuxScript إبلاغ خادم yamux بالفصل بحيث يشير بدوره إلى عميل الجوارب.
if ($StopFlag[$stream.ymxId] -eq 2){ $stream.ymxId | out-file -Append c:\work\log.txt $outbuf = [byte[]](0x00,0x01,0x00,0x04)+ [bitconverter]::getbytes([int32]$stream.ymxId)[3..0]+ [byte[]](0x00,0x00,0x00,0x00) $state.tcpstream.Write($outbuf,0,12) $state.tcpstream.flush() }
تحديث نافذة Yamux
بالإضافة إلى ذلك ، يجب على yamuxScript مراقبة عدد البايتات الواردة من خادم yamux وإرسال رسالة YUX WinUpdate بشكل دوري. هذه الآلية في Yamux مسؤولة عن مراقبة وتغيير حجم إطار ما يسمى (على غرار بروتوكول TCP) - عدد بايتات البيانات التي يمكن إرسالها دون إقرار بذلك. بشكل افتراضي ، حجم الإطار 256 كيلو بايت. هذا يعني أنه عند إرسال أو استقبال ملفات أو بيانات أكبر من هذا الحجم ، نحتاج إلى إرسال حزمة تحديث windpw إلى خادم yamux. للتحكم في مقدار البيانات المستلمة من خادم yamux ، تم تقديم صفيف مشترك $ RcvBytes خاص ، حيث يقوم الدفق الرئيسي من خلال زيادة القيمة الحالية بتسجيل عدد البايتات الواردة من الخادم لكل قطار. إذا تم تجاوز الحد المعين ، يجب على yamuxScript إرسال حزمة إلى خادم WinUpdate وإعادة ضبط العداد:
if ($RcvBytes[$stream.ymxId] -ge 256144){ #out win update ymx packet with 256K size $outbuf = [byte[]](0x00,0x01,0x00,0x00)+ [bitconverter]::getbytes([int32]$stream.ymxId)[3..0]+ (0x00,0x04,0x00,0x00) $state.tcpstream.Write($outbuf,0,12) $RcvBytes[$stream.ymxId] = 0 }
تيارات الجوارب
الآن دعنا ننتقل مباشرة إلى socksScript نفسه.
تذكر أنه يتم استدعاء socksScript بشكل غير متزامن:
$state = [PSCustomObject]@{"StreamID"=$ymxstream;"inputStream"=$cipipe;"outputStream"=$copipe} $PS = [PowerShell]::Create() .... $AsyncJobResult = $PS.BeginInvoke()
وفي وقت الاتصال ، كانت البيانات التالية موجودة في متغير الحالة $ الذي تم نقله إلى الدفق:
- $ state.streamId - رقم جلسة yamux ؛
- $ state.inputStream - قراءة الأنبوب ؛
- $ state.oututStream - كتابة الأنابيب ؛
تأتي البيانات في الأنابيب في شكل خام بدون رؤوس yamux ، أي بالشكل الذي جاءوا به من عميل الجوارب.
بداخل الجوارب ، أولاً وقبل كل شيء ، نحتاج إلى تحديد إصدار الجوارب والتأكد من أنها 5:
$state.inputStream.Read($buffer,0,2) | Out-Null $socksVer=$buffer[0] if ($socksVer -eq 5){ ... }
حسنًا ، ننفذ تمامًا كما هو مطبق في البرنامج النصي Invoke-SocksProxy. الفرق الوحيد سيكون ذلك بدلاً من المكالمات
$AsyncJobResult.AsyncWaitHandle.WaitOne(); $AsyncJobResult2.AsyncWaitHandle.WaitOne();
من الضروري مراقبة اتصال tcp وعلامة الإنهاء المقابلة في مصفوفة $ StopFlag في وضع دوري ، وإلا فلن نتمكن من التعرف على وضع نهاية الاتصال من جانب عميل الجوارب وخادم ymux:
while ($StopFlag[$state.StreamID] -eq 0 -and $tmpServ.Connected ){ start-sleep -Milliseconds 50 }
في حالة انتهاء الاتصال على جانب tcp للخادم الذي نتصل به ، قمنا بتعيين هذه العلامة على 2 ، مما سيجبر yamuxscript على التعرف على هذا وإرسال حزمة FIN ymx المقابلة إلى خادم yamux:
if ($tmpServ.Connected){ $tmpServ.close() }else{ $StopFlag[$state.StreamID] = 2 }
يجب علينا أيضًا تعيين هذه العلامة إذا تعذر على socksScript الاتصال بالخادم الوجهة:
if($tmpServ.Connected){ ... } else{ $buffer[1]=4 $state.outputStream.Write($buffer,0,2) $StopFlag[$state.StreamID] = 2 }
خاتمة للجزء الثاني
في سياق بحثنا الترميز ، تمكنا من إنشاء عميل بوويرشيل لخادم RsocksTun لدينا مع القدرة على:
- اتصالات SSL
- إذن على الخادم ؛
- العمل مع خادم yamux بدعم لأصوات keepalive ؛
- وضع متعدد الخيوط للعملية ؛
- دعم لنقل الملفات الكبيرة ؛
خارج المقالة ، كان هناك تطبيق لوظائف الاتصال عبر خادم وكيل والتفويض عليه ، وكذلك تحويل البرنامج النصي لدينا إلى إصدار مضمّن ، يمكن تشغيله من سطر الأوامر. سيكون في الجزء الثالث.
هذا كل شيء لهذا اليوم. كما يقولون - اشترك ، على سبيل المثال ، اترك تعليقات (خاصة فيما يتعلق بأفكارك حول تحسين الكود وإضافة الوظيفة).