Developers

جولة في URnetwork SDK

14 دقائق قراءةView as markdown ↗

هذه هي الغوصة العميقة خلف البدء: كيف تُركَّب كل طبقة ربط، وما تشحنه من افتراضيات، والحوافُّ الحادّة التي نعرفها. وكل شيء يعمل مقابل منصة URnetwork المستضافة. أنت تجلب الاعتمادات، لا البنية التحتية. ونموذج الكائنات (الجهاز، وAPI، ومتحكّمات العرض) في /docs/overview و/docs/api.

نواة Go واحدة تنفّذ العميل كله: النقل، والعقود، ومسار الحزم. وكل مخرَج (AAR، وxcframework، وwasm، ومكتبة c-shared) ربطٌ لتلك النواة نفسها، ولهذا يتطابق السلوك عبر المنصات، ولهذا أيضاً تكبر المخرجات (فكل واحد يحمل وقت تشغيل Go). والنواة تحمل قواعد المنتج أيضاً، لا الحزم وحدها. وSolana Pay يسكن هنا (CreatePaymentReference وBuildSolanaPaymentUrl) لأنه لم يكن كذلك يوماً: فقد سكّ تطبيقُ الويب مرجعَ الدفع بصيغة uuid ست عشرية حيث يشترط Solana Pay مفتاحاً عاماً بصيغة base58 من 32 بايت، فكان يمكن لعميل أن يدفع ولا يُطابَق أبداً بحسابه، وثبّت Android المبلغَ وعنوانَ التاجر برمجياً. وإعادةُ تنفيذ قاعدة تتبعها التطبيقات أصلاً هي صنف الخلل الذي وُجدت هذه الطبقة لمنعه.

المنصات المحمولة

DeviceLocal، وقسمة عملية التطبيق على Apple

DeviceLocal هو الشيء الحقيقي، أي محرك العميل العامل: فهو يملك النقل، وحالةَ العقود (وهي محاسبة URnetwork للبايتات التي يجوز لحساب أن يحرّكها)، ومسارَ الحزم. أما DeviceRemote فعميلٌ لكائن DeviceLocal يسكن في عملية أخرى أو على المنصة، بسطح الواجهة نفسه: أي جهاز تحكم عن بُعد بالجهاز الحقيقي. وعلى Android تستضيف عمليةٌ واحدة الواجهةَ وVpnService معاً، فيمسك التطبيق بـDeviceLocal مباشرة.

وتشغّل Apple معالجة حزم VPN في عملية NetworkExtension معزولة خاصة بها (وكل شبكة VPN على iOS وmacOS مقسومة هكذا)، فينقسم SDK معها:

  • امتداد NEPacketTunnelProvider يملك SdkDeviceLocal وينادي عليه setRpcServer(serverPem, clientCertPem, hostPort) لبدء المستمع.
  • وعملية التطبيق تُنشئ SdkDeviceRemote وترتبط عبر اتصال RPC للجهاز بـmTLS على الاسترجاع المحلي: أي الصورة المرآتية setRpcServer(clientPem, serverCertPem, hostPort)، وكل جانب يسمّي هويته هو أولاً وشهادة النِّد ثانياً. وRPC الجهاز هو بروتوكول SDK الخاص: استدعاءات دوال إضافة إلى تدفقات اشتراك فوق net/rpc في Go، لا gRPC. وTLS المتبادل على الاسترجاع المحلي ليس مراسم شكلية. فـlocalhost يستطيع بلوغه كل عملية محلية وكل مستخدم، ولا يستطيع الارتباط إلا من يملك ملفات PEM المطابقة.
  • والتطبيق هو الذي يسكّ مادة المفاتيح، لا الامتداد. فـSdkGenerateDeviceRpcKeyMaterial() يعيد زوجَي مفاتيح جديدين موقَّعين ذاتياً للخادم وللعميل في كل جلسة VPN؛ ويضع التطبيق ملفَّي PEM وقيمة host:port في إعداد الموفّر في NETunnelProviderProtocol، وهكذا يحصل عليها الامتداد حين يبدأ النفق. ولا تدخل في الأمر أي حاوية مجموعة تطبيقات.
  • وراقب RemoteChangeListener: ففي أول مرة تصير فيها remoteConnected صحيحة تكون تلك المادة قد أُثبتت، فاحفظها بوصفها آخر نسخة معروفة السلامة وأعد تطبيقها في الإطلاق التالي. وهذا هو ما يتيح للتطبيق أن يرتبط بامتداد يعمل أصلاً.

وإعادات البناء أمر روتيني: فالامتداد يعيد بناء جهازه كلما أُعيد تشغيل النفق، إما لأن المستخدم بدّل VPN، أو لأن نظام التشغيل أعاد تشغيل الامتداد، أو لأن iOS قتله لتجاوزه حدَّ ذاكرة الامتدادات. فوصّل إعادة الارتباط تلك (السكّ، وبدء النفق، وsetRpcServer، وإعادة تطبيق حالة واجهتك) من اليوم الأول. وكل ما تستطيع فعله على DeviceLocal تستطيع فعله على DeviceRemote؛ فالبعيد يوكّل الاستدعاءات ويعيد تشغيل الاشتراكات عبر عمليات إعادة الاتصال. ومستويان: إعادة اتصال بسيطة (الجهاز نفسه، وقد سقطت وصلة RPC ثم عادت) تعيد إقامة الاشتراكات عنك؛ وإعادة إنشاء (نسخة جهاز جديدة تحتك) تعني تشغيل ذلك التسلسل من جديد. وفخّ واحد: DeviceRecreatedListener يُطلق عند تغيّر جيل الجهاز، ولا يختم جيلاً إلا مسارُ RPC المستضاف على المنصة، فعلى الاسترجاع المحلي قُد إعادة الارتباط من دورة حياة نفقك أنت ومن RemoteChangeListener.

IoLoop ‏(Android وحده)

Sdk.newIoLoop(device, detachedFd) هي مضخّة الحزم:

  • مرّر واصف ملف مفصولاً وغير حاجب ‏(ParcelFileDescriptor.detachFd()). وبعد ذلك الاستدعاء، تملك Go واصف الملف وستغلقه؛ فلا تغلّفه ولا تغلقه من Java مرة أخرى أبداً. فالمالكان يعنيان إغلاقاً مزدوجاً: وأرقام واصفات الملفات يُعاد تدويرها فوراً، فإغلاقٌ شارد من Java قد يدوس أي واصف لا صلة له أخذ الرقم بعده، وفسادٌ بعيد عن سببه.
  • والحلقة تضخّ في الاتجاهين (tun←الجهاز والجهاز←tun) داخل Go، وهذا يتجنب عبور JNI ونسخ المخازن المؤقتة لكل حزمة. ولهذا تأخذ الواجهة واصف ملف خاماً بدل كشف دوال قراءة وكتابة.
  • أغلق IoLoop (لا واصف الملف) لتتوقف؛ ويُطلق ردُّ نداء الإتمام حين تخرج الحلقة. وإن أُطلق من دون أن تطلب، لأن واصف tun بلغ EOF أو خطأً أو لأن الجهاز أُغلق، فعامله بوصفه «النفق ذهب»: أنهِ جلسة VpnService، ثم أعد الإقامة أو أظهر انقطاع الاتصال. ورد النداء يصل على خيط من خيوط SDK، وGo تغلق واصف الملف وهي خارجة؛ فلا تلمسه من المعالج أبداً.

ولا تستخدم Apple كائن IoLoop: فموفّر النفق يحرّك الحزم عبر packetFlow، وهي الواجهة الوحيدة التي يعرضها NetworkExtension. وكل شبكة VPN على iOS تدفع ثمن ذلك العبور، وقراءاتها الدفعية توزّع الكلفة.

أوضاع المشاركة، ولماذا تهمّ مادة المفاتيح

SetProvideMode يتحكم في ما إذا كان الجهاز يعرض سعةً على الشبكة. والافتراضي هو الإيقاف: فالجهاز المُنشأ حديثاً وضعُ مشاركته «none» ولا يعرض شيئاً حتى تضبط وضعاً، فتضمين SDK لا يشارك أبداً نطاق مستخدميك بصمت. ووضعان يفعلان شيئاً اليوم: public، وnetwork، وهو يقصر المشاركة على الأجهزة الأخرى في حساب URnetwork نفسه. وتحمل قائمة البروتوكول قيماً أكثر، لكن المنصة تحسم كل نِدّ خارج شبكتك إلى public، فالخيار الحقيقي هو: إيقاف، أو أجهزتي أنا، أو أي أحد.

والمشاركة هي حيث تهمّ مثابرة مادة المفاتيح: فكائن DeviceLocalKeyMaterial الذي تمرّره عند الإنشاء هو هوية الجهاز بوصفه مزوّداً، أي بذرةُ مفتاح عميل إضافة إلى شهادة TLS المشاركة ومفتاحها. فاحفظها في التخزين الآمن للمنصة (Keystore وKeychain) ومرّر المادة نفسها كل إطلاق، وإلا رأت الشبكةُ مزوّداً جديداً تماماً في كل مرة وفقدت تاريخ الموثوقية الذي يفضّله الاختيار. والمادة العابرة (null) لا بأس بها للعملاء المحض.

وإن عرضت المشاركة، فاذكر المقايضة: حركة الغرباء تخرج من عنوان IP للمستخدم، وموضع المزوّد يرى عناوين IP للوجهات وأسماء SNI في TLS (كما ترى شركة الإنترنت)، وإن كان لا يرى افتراضياً عنوان IP الحقيقي للمستخدم الأصلي. وسلامة المزوّد مهندَسة في المحرك. فطبقة ip_security المفتوحة المصدر تفحص منفذ خروج المزوّد نفسه وتُسقط الحركة من فئة DMCA ومن فئة CFAA قبل أن تغادر. والحكم حزمةٌ مُسقطة، بلا تسجيل وجهة ولا نطاق ولا محتويات في أي مكان. وتطابقُ توقيع BitTorrent يُصدر أيضاً علامة إساءة إلى المشغّل لا تحمل إلا معرّف جهاز النِّد وقيمة نعم/لا، ولا يشحن المشغّل لها أي معالج اليوم؛ أما إسقاطات المشفَّر المبهم فصامتة. ويشارك المزوّدون في بروتوكول UR؛ ويوثّق ur.xyz المكافآت.

ملاحظات وقت التشغيل

  • يُضبَط إيقاع جمع المهملات لكل نظام تشغيل تلقائياً: معامل إيقاع مقداره 10 على iOS (فحدود ذاكرة الامتدادات وحشية) مقابل 50 على Android و100 في ما عداهما. ولستَ أنت من يضبطه.
  • وmemoryTargetByteCount مقبض مختلف: ميزانية بايتات يقسمها الجهاز ليحدّد أحجام مخازنه المؤقتة، بنسبة dns 2 : client 14 : provider 4، مع إسناد حصة المزوّد لزوج العميل ما دامت المشاركة متوقفة (والافتراضي 20 MB). وهي ليست سقفاً ولا إعداداً لجمع المهملات: فحدّ البصمة الليّن على مستوى العملية هو SetMemoryLimit المنفصل، والقتل القاطع هو حدّ الامتدادات في نظام التشغيل. اضبط الهدف تحته؛ وأبقِ الامتداد خالياً من المنطق تقريباً.
  • وربطُ gomobile محروس في البناء، والآلية مهمة: فـgobind يحذف بصمت كل ما لا يستطيع ربطه، ولا يترك إلا تعليق // skipped في المصادر المولَّدة، ولذلك يبحث البناء في تلك المصادر ويخفق عند أي حذف ليس على قائمة سماح صريحة. والتخطيات المسموحة داخلية (حمولات gob في RPC، وسطح البروكسي/المنصة، وأشكال uint64/[][]byte التي لا يستطيع gomobile التعبير عنها): مقصودة، لا انحرافاً.

JavaScript

لماذا لا يوجد DeviceLocal في wasm

لا تستطيع صفحةُ متصفح أن تملك واجهة tun، فكائن DeviceLocal في wasm لن يكون لديه ما يضخّه. ولذلك تشحن طبقة JS نصفَي جانب العميل وحدهما: سطح الواجهة، ومتحكّمات العرض، وDeviceRemote، وهو عميل جهاز كامل يسكن جهازه في مكان آخر. فالصفحة تمسك بلوحة التحكم (حالة الاتصال، والمواقع، والإحصاءات، والحساب) بينما يستهلك مسارَ الحركة أيُّ شيء يستطيع استخدام البروكسي المستضاف. ومن هنا المصنعان: createProxyDevice هو النموذج الرفيع، يحلّ عناوين البروكسي المستضافة ويترك الحركة لأي شيء يستهلكها (إعداد بروكسي في امتداد، أو وكيل fetch)؛ وcreatePlatformDeviceRemote هو النموذج السميك، أي DeviceRemote حقيقي بسطح المستمعين ومتحكّمات العرض كاملاً فوق جهاز تستضيفه المنصة نيابة عنك.

المصادقة بـsignedProxyId

يفتح createPlatformDeviceRemote مقبس websocket لـRPC الجهاز على wss:///device-rpc. وذلك الـwebsocket لا يصادِق برمز JWT للحساب (byJwt، وهو الرمز الحامل من تسجيل الدخول في URnetwork؛ و«by» بقيّة من BringYour). بل يصادِق بـsignedProxyId، وهو auth_token الذي تعيده نقطة النهاية /network/auth-client في المنصة إلى جانب عنوان البروكسي. وذلك فصلُ نطاق مقصود: فمعرّف البروكسي الموقَّع يفوّض مقبس websocket واحداً بالضبط ولا يحمل أي سلطة على الحساب، فلا يركب الرمزُ الواسع مقبسَ مستوى البيانات أبداً. عامِل الزوج (proxyUrl وsignedProxyId) اعتماداً واحداً. اطلبهما ومرّرهما وجدّدهما معاً، وأعد طلب الزوج حين يرفض البروكسي المقبس بدل تخزين قطعة منه مؤقتاً.

نمط المستمعين ومتحكّمات العرض

كل اشتراك يتبع الشكل نفسه: add*ChangeListener(fn) يعيد دالة إلغاء اشتراك. أمسكها ونادِها عند الهدم. وفي React، أعِدها:

useEffect(() => {
  const unsub = device.addConnectChangeListener(setConnectEnabled);
  return unsub;
}, [device]);

وهذا ينجو من التأثيرات المستدعاة مرتين في وضع React الصارم (إضافة، وإلغاء اشتراك، وإضافة)، فيبقى اشتراك حيّ واحد بالضبط. وسطحُ متحكّمات العرض (الاتصال، والمواقع، والأجهزة، والعقود، وإجراءات الحجب) مربوط في wasm أيضاً، معلَّقاً على الجهاز، كما في device.openConnectViewController()، فيعيد تطبيقُ ويب استخدام منطق العرض نفسه الذي تستخدمه تطبيقات الهاتف بدل اشتقاق الحالة من مستمعين خام.

كيف يستخدمه ur.io

سطح /app على ur.io هو المستهلك المرجعي: فملف wasm محمَّل بتكاسل ولا يُحمَّل إلا حين يبلغ المستخدم سطحَ اتصال، فلا تدفع صفحاتُ الهبوط كلفة الـ43 MB تقريباً أبداً، وهناك DeviceRemote واحد لكل تبويب. والصادرات عامة على مستوى الصفحة، فـinit يقبع خلف مفرد.

ويحزم امتدادُ المتصفح ملف wasm نفسه لكنه لا يُنشئه اليوم: فهو يقود الاتصالية عبر بروكسي المنصة إضافة إلى واجهات البروكسي في المتصفح نفسه، ويستهلك المدخل الآخر لحزمة npm، وهو @urnetwork/sdk-js/react، أي خطاطيف واجهة قائمة على fetch المجرَّد وأنواع مولَّدة لا تحتاج إلى wasm. وإن كنت لا تريد إلا سطح REST، فاستورد ذلك ولا تنادِ init أبداً: فالمُحمِّل يحلّ عنوان wasm وقت التشغيل بدل حلّه عبر new URL(..., import.meta.url) ساكن، فلا تُصدره أدوات التجميع لمن لا يريده.

cgo

قسمة الخدمة الخفية

تطبيقا URnetwork المشحونان على Linux وWindows مبنيان على هذه الطبقة، وكلاهما يستخدم نمط العمليتين نفسه، وهو الشكل الذي تنسخه لأي تكامل cgo يمسّ جهاز tun. فـSDK يشحن المحرك وواجهة C؛ أما الخدمة الخفية فهي لك، ومعك ذانك التطبيقان المُصدَران مرجعَين مفتوحَي المصدر:

  • عملية جذر/خدمة (وحدة systemd، أو خدمة Windows) تشغّل DeviceLocal وتملك واجهة tun.
  • وعملية الواجهة بلا امتيازات تشغّل DeviceRemote وترتبط عبر RPC الجهاز بـmTLS على الاسترجاع المحلي؛ وعنوان SDK الافتراضي هو 127.0.0.1:12025.
  • وملفات PEM الخاصة بـmTLS تُسلَّم إلى الواجهة عبر قناة يفوّضها نظام التشغيل نفسه: مقبس unix مفحوص بـSO_PEERCRED على Linux (يُفحص عند القبول، قبل قراءة أي إطار)، وأنبوب مُسمّى على Windows. وتلك المصافحة، لا منفذ TCP، هي حدّ التفويض الحقيقي؛ وmTLS على الاسترجاع المحلي لا يفعل إلا إبعاد المستخدمين المحليين الآخرين عن المنفذ.

دلالات غلاف RAII ‏(urnetwork_sdk.hpp)

  • تُغلَّف المقابض في أنواع مالكة يستدعي مدمّرها urnet_release. لكن التحرير ليس إغلاقاً ولا إيقافاً: فإسقاط آخر غلاف لا يوقف جهازاً ولا يغلق اتصالاً. نادِ *_close / *_stop (أو .close() / .stop() في الغلاف) أولاً، ثم دع الغلاف يحرّر. فعدة مقابض قد تشير إلى كائن حيّ واحد، ولذلك يجب ألا يقتل خروجٌ من نطاق جلسةً حيّة بصمت أبداً.
  • وتعود الاشتراكات بصيغة urnet::Sub؛ ومدمّرها يلغي الاشتراك.
  • وتظهر الأخطاء بصيغة استثناءات urnet::Error تحمل نصّ out_error.

وقاعدة صارمة واحدة، وهي عكس ما يتوقعه الناس:

sub = device.addConnectChangeListener([&](bool enabled) { /* ... */ });
sub.close();   // returns immediately; a callback may still be running
// do NOT free what that lambda captured here

قوائم المستمعين تعمل بالنسخ عند الكتابة، فإلغاء الاشتراك يزيل المدخل ويعود. وذلك مقصود (فمن المسموح لرد نداء أن يزيل نفسه من داخل نفسه)، لكنه يعني أن رد نداء قد يظل يعمل على خيط آخر بعد اختفاء Sub. وتدميرُ الحالة التي التقطها ذلك المستمع هو الانهيار الحقيقي؛ فأبقِها حيّةً بعد الهدم بـshared_ptr أو بعَلَم يفحصه رد النداء.

ولانضباط التسريب، خذ لقطة من urnet_live_handle_count() قبل السيناريو، ونفّذ إنشاءً/استخداماً/إغلاقاً/تحريراً، وتحقّق من أنه يعود إلى خط الأساس. وهذا بالضبط ما يفعله cgo/smoke، وهو النمط الذي تنسخه.

ما المربوط فعلاً

واجهة C ABI مولَّدة من سطح Go، والمولِّد يكتب cgo/coverage_report.txt: كل رمز مُصدَّر، ولكل رمز لم يعبر، السبب (سياقات Go، ودواخل net.Conn، ومعاملات الدوال، واستدعاءات ملكية المجمّعات، وأنواع gob في RPC). اقرأ ذلك الملف، لا مصدر Go، للإجابة عن «هل هذا قابل للاستدعاء من C؟». والتصدير الجديد من Go لا يبلغ الترويسة إلا حين يعمل المولِّد، ولذلك تُشحن الترويسة بجانب المكتبة التي قُطعت معها. ثبّتهما معاً.

توافق البروتوكول على السلك

بروتوكول RPC الجهاز على السلك مثبَّت الإصدار (DeviceRpcVersion، وهو 1 حالياً) ومُنفَّذ من قِبل المحلي في كل مزامنة. وهو عمداً ليس إصدار الإصدارة: فالنصفان المستضافان يُنشران مستقلَّين، فربطهما سيرفض كل متصفح بعد نشر الخادم. وعدم التطابق لا يرمي استثناءً. فالبعيد يبقى غير متزامن ويعيد المحاولة، وهذا يبدو مطابقاً لـ«الخدمة الخفية لا تعمل». ميّز بينهما بـGetRemoteConnected() إضافة إلى GetSyncError(): فخطأ مزامنة فارغ يعني أنه غير قابل للوصول بعد، بينما "device rpc version mismatch: ..." أو "device instance mismatch: ..." رفضٌ لن تصلحه إعادة الاتصال أبداً. اشحن الخدمة الخفية والواجهة من إصدارة SDK نفسها.

أمور عابرة للطبقات

إعدادات تحمل سياسة

هذه الأعلام تحمل قرارات منتج، لا ضبطاً، وافتراضياتها هي وضعية الخصوصية التي يحصل عليها مستخدموك. وما يشحنه جهاز خارج الصندوق:

الإعدادالافتراضيالأثرالكلفة الرئيسة
SetPerformanceProfilenil (تلقائي)نافذتا الجودة والسرعة تعملان جنباً إلى جنب؛ والحركة تخرج عبر عدة مزوّدين في آن واحد (عادةً 3–8)، مع انتماء لكل موقعتثبيت WindowType يضيّق إلى نافذة واحدة
AllowDirectمتوقفيُبقي قفزة الإخفاء، فلا يرى أي مزوّد عنوان IP الحقيقي للمستخدمتفعيله: معدل نقل أعلى، والمزوّد يرى عنوان IP الحقيقي للمستخدم
PostQuantumEncryptionمفعّليختم جلسة العميل↔المزوّد، فيُرحّل المشغّل بايتات لا يستطيع قراءتهايُتخطّى المزوّد الذي يتعذّر ختم جلسة معه، ولا يُستخدم بلا ختم
SetRouteLocalمسموحترتدّ الحركة إلى المسار المحلي حين يسقط النفقالمنع هو مفتاح الإيقاف: تتوقف الحركة بدلاً من ذلك
SetProvideModenoneالجهاز لا يعرض أي سعة على الشبكةالمشاركة العامة أو الشبكية تُخرج حركة الآخرين من عنوان IP للمستخدم

تحفّظات ترافق الجدول:

  • AllowDirect هو إعداد السرعة، والعنوان الذي يكشفه هو بالضبط القفزة التي يزيلها. وتعرضه التطبيقات مقلوباً باسم «الإخفاء القوي للهوية»، مفعّلاً افتراضياً، وهو مُجبَر على الإيقاف في ملفات الأجهزة المستضافة مهما ضبط المستدعي. فلا تقدّمه لمستخدميك أبداً بوصفه سرعة مجانية؛ قدّمه بوصفه المقايضة التي هو عليها.
  • وPostQuantumEncryption هو جلسة العميل↔المزوّد من طرف إلى طرف التي تشحنها التطبيقات باسم «تشفير ما بعد الكم»، مفعّلة افتراضياً منذ الإطلاق وجاهزة من جانب المزوّد: فكل إصدارات المزوّد الحالية تفعّل الجانب المجيب. وما دام مفعّلاً، يعمل العميل بالإغلاق عند الفشل: فهو لا يحمل بيانات التطبيقات في العلن، ويتخطّى المزوّد الذي يتعذّر ختم جلسة معه بدل أن يستخدمه من دون ختم. فالكلفة في التوفّر، لا في السرّية. أوقف العَلَم، وتعود الحركة قادرةً على سلوك المسار القياسي.
  • وSetRouteLocal هو العنصر الأولي لمفتاح الإيقاف («السماح بالحركة المحلية»). أوقفه، وأعطِ مستخدميك المفتاح الموجود في كل تطبيق URnetwork، فتتوقف الحركة بدل أن ترتد حين يسقط النفق.

عمى المزوّد عن هوية مستخدمك غير مشروط؛ وعمى المشغّل عن المحتوى كذلك، افتراضياً. خاصيتان، كلتاهما صحيحة في ملف خارج الصندوق، وهو ما يجعل من الدقة قولَ إن لا طرف واحد يجمع بين هوية مستخدمك ونشاطه. وقلبُ أي من العَلَمين يُفرّط بالخاصية المقابلة، لا يكسبها.

والأوضاع الثلاثة التي يختارها هذان العَلَمان، مذكورةً من أجل المستخدمين الذين توجّههم:

الوضعما يراه المشغّلما يراه المزوّدكيف تحصل عليه
مُرحَّل مختوماتصال الحساب/المصدر، والاقتران بالمزوّد، ونص مشفّر مع التوقيت/الحجمحركة الوجهات ومعرّف جهاز/عقد، وليس عنوان IP الحقيقي للمستخدمالافتراضي: PostQuantumEncryption مفعّل، وAllowDirect متوقف
مُرحَّل قياسياتصال الحساب/المصدر، والاقتران بالمزوّد، والوجهات الداخلية وبايتات الحزمحركة الوجهات ومعرّف جهاز/عقد، وليس عنوان IP الحقيقي للمستخدمبإيقاف PostQuantumEncryption فقط
مباشردور أقل في الترحيلعنوان IP الحقيقي للمستخدم وحركة الوجهاتيُفعَّل يدوياً: AllowDirect مفعّل (مُجبَر على الإيقاف في الملفات المستضافة)

والافتراضي يشحن الصفَّ الأول أصلاً بلا ضبط شيء. والثاني يقتضي إيقاف PostQuantumEncryption: فما دام مفعّلاً، يتخطّى العميل المزوّد الذي يتعذّر ختم جلسة معه بدل أن يهبط إلى ذلك الصف. ولا توجد واجهة لكل اتصال تُبلغ عن الصف الذي انتهت إليه جلسةٌ بعينها. ويُخضِع نموذج التهديد هذه الصفوف لخصوم مسمّين، ويصرّح بموضع إخفاق كل صف.

قواعد الخيوط

  • الكائنات غير العرضية (DeviceLocal وDeviceRemote وApi وفضاءات الشبكة) آمنة للتزامن. نادِها من أي خيط.
  • ومتحكّمات العرض أحادية الخيط ما لم يوثّق أحدها غير ذلك. قُد كلاً منها من خيط واحد (عادةً خيط واجهتك). وJavaScript في المتصفح يفي بذلك مجاناً؛ أما على Android وApple وcgo فهو يعضّ، لأن ردود النداء تصل على خيوط تديرها Go.
  • وردود النداء تُطلق على خيوط عشوائية يديرها SDK. فانقلها إلى خيط واجهتك قبل أن تلمس الواجهة، ولا تحجب داخل واحد منها أبداً: فـDeviceRemote يُسلسل ردود ندائه عبر قناة واحدة ذات مخزن مؤقت، فالمستمع البطيء يضغط عكسياً على كل اشتراك عليها.

أحجام المخرجات

خطّط ميزانيات التنزيل والتعبئة حول هذه:

المخرَجالحجم (مضغوطاً حيث يُذكر)
Android AARنحو 37 MB
Apple xcframework ‏(zip)نحو 116 MB
JS wasmنحو 43.5 MB
Linux c-shared ‏(zip)نحو 25 MB
Windows c-shared ‏(zip)نحو 21 MB

استقرار واجهة API

الإصدارات مؤرَّخة بالتاريخ (vYYYY.M.D-)؛ ولا يوجد عقد SemVer بعد، وحزمة npm في مرحلة بيتا وتُنشر ليلياً. عامِل الأسطح الثلاثة كلها بوصفها متحركة حتى إصدار 1.0: ثبّت إصدارات بعينها، واقرأ ملاحظات الإصدار عند كل قفزة، وأبقِ المخرجات المتزاوجة (الخدمة الخفية والواجهة، والامتداد والتطبيق) على إصدارة واحدة.