البدء مع URnetwork SDK
يأخذ هذا الدليل المطوِّر من مشروع فارغ إلى أول اتصال: احصل على رمز JWT للحساب، وثبّت طبقة الربط الخاصة بمنصتك، وارفع جهازاً، وتأكّد من تدفق الحركة. وSDK وحدةُ Go واحدة، github.com/urnetwork/sdk، معروضة عبر ثلاث طبقات ربط: روابط gomobile لنظامي Android وApple، وبناء WebAssembly للغة JavaScript، ومكتبات c-shared مع واجهة C ABI منتقاة لكل ما عداها. ولمعرفة البنية خلف كل طبقة ربط، اقرأ جولة SDK.
تستخدم URnetwork أجهزة مخارج يشغّلها الأعضاء. ولا يتلقى المزوّدون عناوين IP المصدرية لمستخدميك على المسارات المُرحَّلة. كما تشفّر الأجهزة المبنية على SDK حركة البيانات حتى المزوّد افتراضياً. اقرأ كيف تعمل URnetwork للاطلاع على النموذج كاملاً.
ما تحتاج إليه
الطبقات الثلاث كلها واجهةٌ للنواة نفسها التي بُنيت عليها تطبيقات URnetwork الرسمية: الجهاز (محرك الاتصال)، وعميل API، ونموذج إعداد فضاء الشبكة (network space) — أي نشرة المنصة التي تخاطبها، وبأي نقاط نهاية وأعلام — ومتحكّمات العرض (view controllers)، وهي كائنات بلا واجهة تحمل الحالة والأحداث للشاشات الشائعة، تربط بها واجهتك أو تتجاهلها. وواجهة API الخاصة بالمشغّل خلف ذلك موثّقة في /docs/api. أنت تبني تطبيقك أنت على منصة URnetwork المستضافة: فواجهة API والمرحّلات والمزوّدون هم الخدمة الحية، ومستخدموك يجلبون حسابات أو ينشئونها.
الحدود الدنيا للمنصات، لكل طبقة ربط:
- Android — مستوى API 24 فأحدث. وحزمة Java هي
com.bringyour.sdk. - iOS / macOS — iOS 16.0 وmacOS 13.5.
- JavaScript — للمتصفح وحده.
- cgo — Ubuntu 22.04+ (glibc 2.35+) أو Windows 10+، على amd64 وarm64.
- البناء من المصدر — Go 1.26+.
ترخيص SDK هو MPL-2.0 (Mozilla Public License): اربطها بحرية داخل تطبيقات مغلقة المصدر. فالحقوق المتروكة على مستوى الملف لا تُلزم إلا بمشاركة التغييرات على ملفات SDK نفسها.
تسجيل الدخول
كل مقتطف أدناه يأخذ byJwt: وهو رمز JWT للحساب، أي رمز تسجيل دخول موقّع تُصدره واجهة API في URnetwork عند مصادقة حساب. ولا يوجد نظام مفاتيح API منفصل.
يشغّل تطبيقك مسار تسجيل دخول مرة واحدة عبر عميل API في SDK. يُبلغ authLogin عن طرق المصادقة المتاحة لمعرِّف ما؛ ويُتمّه authLoginWithPassword (ويُنهي authVerify رمزاً مرسلاً بالبريد أو بالرسائل القصيرة)، ومصادقةُ المحفظة وnetworkCreate مدخلان موازيان. ومصادقة المحفظة بالتوقيع وحده: ترسل wallet_address وwallet_message وwallet_signature، ولا ترسل مفتاحاً أبداً، ولا يطلب أي سطح في URnetwork المفتاح الخاص لمحفظة ولا عبارتها التذكيرية. أما networkCreate بلا أي طريقة مصادقة فهو مسار الحساب الفوري: يسكّ حساباً دائماً ويعيد seedphrase، وهي عبارة الاستعادة الخاصة بشبكة URnetwork لذلك الحساب، تُولَّد على الخادم وتُسلَّم مرة واحدة بالضبط. اعرضها على المستخدم حينها، وإلا فلا مسار استعادة للحساب.
وكل مسار يعيد by_jwt. احفظه، ونادِ setByJwt، ومرّره إلى مُنشئ الجهاز. ولا تكتب أي كود تجديد؛ فمدير الرموز في Api يدوّر رمز JWT. وعامِل إخفاق المصادقة القاطع بوصفه «شغّل تسجيل الدخول من جديد».
والحساب هو أيضاً وحدة الفوترة: فالحساب الذي يحمل الجهازُ رمزَ JWT الخاص به هو الحساب الذي تُقاس خطته. المجانية حصة بيانات يومية، وPro حصة شهرية كبيرة؛ والأرقام الحالية على ur.io/products. واقصد حساباً واحداً لكل مستخدم نهائي، وهو ما يجعله مسارُ الحساب الفوري بلا احتكاك، بدل حساب واحد مضمَّن يجمع استخدام كل مستخدم وسلوكه في فاعل واحد. وحين تنفد بيانات حساب، يتوقف النقل حتى تتجدد الحصة، فقل «نفدت البيانات» في تجربة المستخدم لديك، لا خطأ شبكة عاماً.
وتملك SDK أيضاً مسار الخطة المدفوعة، لأن العميل يجب ألا يسمّي سعره بنفسه أبداً: سجّل نيّة بـcreateSolanaPaymentIntent (مع reference من createPaymentReference، وplan بقيمة "monthly" أو "yearly")، وخذ amountUsd من النتيجة، ومرّرها إلى buildSolanaPaymentUrl. وفي واجهة C ABI استدعاءُ النيّة، أما بانِي الرابط فليس فيها بعد.
التثبيت
المخرجات المبنية مسبقاً موجودة بوصفها أصول إصدار على github.com/urnetwork/build. والإصدارات مؤرَّخة بالتاريخ، vYYYY.M.D- (مثل v2026.7.22-999364023)، لا بترقيم SemVer: ثبّت إصداراً بعينه وارتقِ عن قصد، لأن الرقم يقول متى قُطع الإصدار، لا ما إذا كانت واجهة API قد تحركت.
Android. نزّل URnetworkSdk-.aar (مع -sources.jar للتنقل في بيئة التطوير) وضعهما في مجلد تمسحه وحدةُ Gradle لديك أصلاً، مثل:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}iOS / macOS. نزّل URnetworkSdk.xcframework.zip، وفُكَّ ضغطه، وأشِر إليه من حزمة Swift محلية بوصفه binaryTarget:
// Package.swift
targets: [
.binaryTarget(name: "URnetworkSdk", path: "URnetworkSdk.xcframework")
]وكل الأنواع مسبوقة بـSdk (SdkDeviceLocal وSdkNetworkSpace وغيرها). ويحمل xcframework الشرائح ios/arm64 وiossimulator/arm64 وmacos/arm64 وmacos/amd64؛ ولا توجد شريحة محاكي iOS لمعالجات Intel.
JavaScript. حزمة npm هي @urnetwork/sdk-js، وهي مُحمِّل للمتصفح وحده يجلب wasm_exec.js من Go إضافة إلى ملف wasm الخاص بـSDK ويُنشئهما في الصفحة. ثبّت وسم nightly؛ فهو مقطوع من SDK الحالية على مخطط الإصدارات المؤرَّخ نفسه ويشحن معه ملف wasm. أما latest فمتأخر بأشهر ولا يحمل wasm إطلاقاً، فتتركك npm install المجرَّدة بمُحمِّل لا شيء ليحمّله.
npm install @urnetwork/sdk-js@nightly # then pin the version it resolved toوحجم ملف wasm نحو 43 MB، أي نواة Go كاملة، فهو لن يصغر، لكنه يُضغط جيداً: قدّمه أصلاً ساكناً (بضغط gzip أو brotli، مع تخزين مؤقت طويل) وحمّله بتكاسل، فقط حين يبلغ المستخدم سطحَ اتصال. ولا تدع أداةَ تجميع تُضمّنه أو تحوّله أبداً، واشحن دائماً sdk.wasm وwasm_exec.js من البناء نفسه؛ فالغراء مقترن بواجهة ABI مع سلسلة أدوات Go التي جمّعت wasm، وبناءُ SDK يشترط تطابق الاثنين بايتاً ببايت.
cgo. مكتبات c-shared، وهي الطبقة نفسها التي بُنيت عليها تطبيقات URnetwork المشحونة على Linux وWindows، ومعها ترويستان منتقاتان:
libURnetworkSdk.so— لنظام Linux.URnetworkSdk.dll(+urnetwork_sdk.def) — لنظام Windows.urnetwork_sdk.h— واجهة C ABI المجرّدة.urnetwork_sdk.hpp— غلاف C++17 بنمط RAII من ترويسات فقط فوقها، يتطلب nlohmann/json في مسار التضمين لديك.
وكل ما له واجهة استدعاء دوال خارجية بلغة C (مثل Rust bindgen وPython ctypes وC# P/Invoke) يتوافق مع واجهة ABI هذه القائمة على المقابض وJSON؛ وترويسة C++ هي التسهيل اللغوي الوحيد. ومع MSVC، ولّد مكتبة الاستيراد من ملف .def أولاً:
lib /def:urnetwork_sdk.def /machine:x64 /out:URnetworkSdk.libمن المصدر. تحتاج إلى Go 1.26+، وللمنصات المحمولة نفّذ make init أولاً: فهو يثبّت نسخة gomobile نفسها التي تُقطع بها الروابط، ويثبّت أداة checksec التي يشغّلها هدف Android. ويحتاج build_android أيضاً إلى ضبط ANDROID_NDK_HOME، لأنه يجرّد .comment بأداة llvm-objcopy من NDK. ويغلّف sdk/build-android.sh وsdk/build-ios.sh الاثنين. ومن sdk/build:
make init # pinned gomobile + checksec; run before the mobile targets
make build_android # AAR
make build_apple # xcframework (build_ios is an alias)
make build_js # wasm + loader
make build_linux # c-shared .so, cross-compiled with zig
make build_windows # c-shared .dll; not in `all` — the shipped one builds in a VMويشترط build_android قائمةَ التخطي في gomobile، فالرمز المفقود يعني عدم تطابق إصدارات لا إسقاطَ ربط صامتاً؛ وgomobile لا يربط شرائح البنى (فالقوائم تعبر بوصفها SdkStringList وSdkIdList وغيرها) ولا يربط context.Context.
التقييمات الأمنية الخارجية، وحدودها، مغطّاة في نموذج التهديد.
إذن النفق
موافقةُ نظام التشغيل، والنفق نفسه، ملكٌ لتطبيقك أنت. وتبدأ SDK عند طبقة الحزم:
- على Android، خدمةُ
VpnServiceملكك: تطبيقك يعلنها، ويحصل على موافقة VPN بـVpnService.prepare()، ثم يناديestablish(). وتتسلّم SDK الأمر عند واصف الملف ذاك. - وعلى منصات Apple، القسمة اشتراطٌ من Apple لا من SDK: فأنفاق الحزم تعمل داخل NetworkExtension منفصل بحدود ذاكرة ضيقة، ولذلك يملك موفّر النفق (tunnel provider) الجهازَ بينما ترتبط به عمليةُ تطبيقك عن بُعد.
- وفي المتصفح، لا تستطيع صفحة أن تملك واجهة شبكة، فلا إذن يُطلب، ولا شيء في طبقة JavaScript ينفق حركةَ الصفحة نفسها. وكلا نموذجَي الجهاز في JavaScript يقود بدلاً من ذلك جهازاً على المنصة. ويغطي امتداد URnetwork تبويبات المتصفح؛ ويغطي تطبيق أصلي الجهازَ كله.
ولمعرفة ما تسجّله URnetwork عن الاتصالات، اقرأ نموذج التهديد.
الاتصال
يبدأ الجهاز الجديد على افتراضيات الشبكة. والجلسة بين العميل والمزوّد مختومة جاهزةً من الصندوق: يُشحن setPerformanceProfile وPostQuantumEncryption فيه مفعّل، وهو العَلَم الذي يقف خلف عنصر التحكم «تشفير ما بعد الكم» (Post Quantum Encryption) في التطبيقات، وكل إصدارات المزوّد الحالية تفعّل الجانب المجيب. وما دام العَلَم مفعّلاً، يعمل العميل بالإغلاق عند الفشل: فهو لا يحمل بيانات التطبيقات في العلن، ويتخطّى المزوّد الذي يتعذّر ختم جلسة معه بدلاً من استخدامه من دون ختم. أوقف العَلَم، وتعود الحركة قادرةً على سلوك المسار القياسي. وAllowDirect متوقف افتراضياً؛ وهو إعداد السرعة الذي تختاره بنفسك، ويزيل القفزة المُخفية للهوية ويسلّم ذلك المزوّدَ عنوان IP الحقيقي لمستخدمك، وتعرضه التطبيقات مقلوباً باسم «الإخفاء القوي للهوية» (Strong Anonymization)، وملفات الأجهزة المستضافة تُجبره على الإيقاف. أما الحماية من التسريب فعليك أن توصّلها بنفسك: مفتاح الإيقاف (kill switch) هو العنصر الأولي setRouteLocal، والجهاز يبدأ والتوجيه المحلي مسموح، وsetRouteLocal(false) يجعل الحركة تتوقف بدل أن ترتد حين يسقط النفق. ولا تضمّن SDK أي تحليلات ولا تبليغ أعطال؛ واتصالاتها الوحيدة هي نقاط نهاية المنصة والمرحّلات والمزوّدون الذين يستخدمهم الجهاز.
Android
ترتيب الإقلاع: أنشئ NetworkSpaceManager موجَّهاً إلى تخزين خاص بالتطبيق (فالمجلد يحمل الحالة المحلية، ومنها الاعتمادات)، ثم أنشئ فضاء الشبكة، ثم اضبط رمز JWT للحساب على عميل API الخاص به. والمفتاح اسمُ مضيف مع اسم بيئة؛ والإنتاج هو ("ur.network", "main")، وupdateNetworkSpace هو ما يُنشئ فضاءً، بينما getNetworkSpace لا يقرأ إلا فضاءً موجوداً، فيعيد null على تثبيت جديد. وتُشتق العناوين من المفتاح (https://api. وwss://connect.؛ والبيئة غير main تسبق اسم الخدمة ببادئة)، لكن الاشتقاق يفضّل migrationHostName، الذي يضبطه الإنتاج على bringyour.com: فالتطبيقات المشحونة تصل إلى api.bringyour.com، وapi.ur.network لا يُحلّ. ثم أنشئ الجهاز وسلّمه واصفَ ملف النفق:
import com.bringyour.sdk.Sdk
val manager = Sdk.newNetworkSpaceManager(context.filesDir.absolutePath)
val key = Sdk.newNetworkSpaceKey("ur.network", "main")
val networkSpace = manager.updateNetworkSpace(key) { it.migrationHostName = "bringyour.com" }
networkSpace.api.setByJwt(byJwt)
val device = Sdk.newDeviceLocalWithMemoryTarget(
networkSpace,
byJwt,
deviceDescription, // free-form, shown in the account's device list
deviceSpec, // e.g. Build.MODEL
appVersion,
Sdk.newId(), // instanceId; persist and reuse per install
/* enableRpc */ false,
keyMaterial, // persisted DeviceLocalKeyMaterial, or null for ephemeral
memoryTargetByteCount,
)
// inside your VpnService, after establish():
val detachedFd = pfd.detachFd() // ParcelFileDescriptor -> raw fd
val ioLoop = Sdk.newIoLoop(device, detachedFd) { /* done callback */ }يتملّك newIoLoop واصفَ الملف المفصول ويضخّ الحزم في الاتجاهين حتى تغلقه. ولا تلمس واصف الملف من Java بعد فصله.
ثلاثة من وسائط المُنشئ تستحق العناية. instanceId: ولّد واحداً بـSdk.newId() عند أول تشغيل، واحفظه، وأعد استخدامه طوال عمر التثبيت (جهاز حيّ واحد لكل عملية)؛ فمعرّف جديد مع كل إطلاق يضيف مدخلاً شبحياً إلى قائمة أجهزة الحساب. وkeyMaterial: لا بأس بـnull لعميل محض، لكن إن كان الجهاز سيزوّد سعةً يوماً، فاحفظ getKeyMaterial() في التخزين الآمن للمنصة. فهي هوية الجهاز بوصفه مزوّداً، وفقدانها يصفّر تاريخ موثوقية المزوّد. وmemoryTargetByteCount: ميزانية بايتات يقيس عليها الجهاز أحجام مخازنه المؤقتة وإيقاع جمع المهملات؛ اختر رقماً يناسب السقف الحقيقي لعمليتك (انظر الجولة). وDeviceLocal داخل العملية: فإن ماتت عمليتك مات النفق معها، فشغّل VpnService بوصفها خدمة أمامية، وأعد إنشاء الجهاز عند إعادة التشغيل بالمعرّف instanceId نفسه وبمادة المفاتيح نفسها.
iOS / macOS
يملك NEPacketTunnelProvider كائنَ SdkDeviceLocal وتدفقَ الحزم، بينما ترتبط به عمليةُ التطبيق بوصفها جهازاً بعيداً عبر الاسترجاع المحلي (loopback):
// in the packet tunnel provider (owns the device):
var err: NSError?
let device = SdkNewDeviceLocalWithMemoryTarget(
networkSpace, byJwt, deviceDescription, deviceSpec, appVersion,
instanceId, /* enableRpc */ true, keyMaterial, memoryTarget, &err)
// in the app process (attaches to it):
let remote = SdkNewDeviceRemoteWithDefaults(networkSpace, byJwt, instanceId, &err)
try remote?.setRpcServer(clientPem, serverCertPem: serverCertPem, hostPort: hostPort)يسكّ التطبيق مادةَ المفاتيح وملفات PEM الخاصة بـRPC (عبر SdkGenerateDeviceRpcKeyMaterial) ويسلّمها إلى الامتداد في إعداد الموفّر NETunnelProviderProtocol؛ ويقرأ الامتداد من هناك rpc_server_pem وrpc_client_pem وrpc_listen_hostport. ولا توجد مجموعة تطبيقات (app group) على هذا المسار. انظر الجولة لمعرفة سبب وجود القسمة وكيف تعمل إعادة الاتصال.
JavaScript
import { URNetwork } from "@urnetwork/sdk-js";
const sdk = await URNetwork.init({
wasmUrl: "/wasm/sdk.wasm",
wasmExecUrl: "/wasm/wasm_exec.js",
});يسجّل wasm صادراته على window، فشغّل نسخة وحدة واحدة لكل صفحة. وinit عديم الأثر عند التكرار داخل الوحدة (فالنداء الثاني يعيد النسخة نفسها)، لكن نسختين من المُحمِّل — حزمتين مثلاً أو إطارين يتشاركان مجالاً واحداً — تتسابقان على المتغيرات العامة نفسها. هيّئه مرة واحدة في مفرد على مستوى الوحدة، لا داخل دورة حياة مكوّن أبداً.
نموذجان للجهاز:
sdk.createProxyDevice(...)— عميل خفيف لعناوين بروكسي مستضافة، حين يكون كل ما تحتاج إليه هو «رابط بروكسي يخرج عبر URnetwork».sdk.createPlatformDeviceRemote({...})— كائنDeviceRemoteكامل يتكلم device-RPC مع جهاز مستضاف عبر websocket، لواجهة اتصال حقيقية (المواقع والمستمعون والإحصاءات):
const device = sdk.createPlatformDeviceRemote({
apiUrl: "api.bringyour.com",
platformUrl: "connect.bringyour.com",
byJwt,
proxyUrl, // from the platform's proxy config endpoint
signedProxyId, // HMAC auth token, not the JWT — see the tour
});
const unsub = device.addConnectLocationChangeListener((loc) => {
console.log("location:", loc?.name);
});
device.setConnectLocation({ bestAvailable: true });ويرمي createPlatformDeviceRemote استثناءً إن كان ملف wasm المحمَّل أقدم من ربط DeviceRemote؛ وذلك عَرَضُ وسم npm قديم.
cgo
عقد ABI باختصار:
- الكائنات مقابض
uint64_tمبهمة. ويحرّرurnet_release(h)المقبض من دون إيقاف الكائن، فنادِ أولاً*_close/*_stopالخاص به حيث يوجد. - سلاسل
char*المعادة ملكٌ للمستدعي؛ حرّرها بـurnet_free_string. - تعبر البياناتُ المهيكلة الحدَّ بوصفها سلاسل JSON بترميز UTF-8؛ والمعرّفات سلاسل UUID، والأوقات بالمللي ثانية منذ حقبة Unix (و
0تعني لا شيء). - تُطلَق ردود النداء على خيوط عشوائية تديرها Go؛ فانقلها إلى خيطك أنت. وسلاسلها ومخازنها المؤقتة لا تعيش إلا مدة النداء، والمقابض التي تسلّمها لك عليك أنت أن تحرّرها.
- الاستدعاءات القابلة للإخفاق تأخذ
char** out_error؛ وعند الإخفاق يُضبط على رسالة تحرّرها بـurnet_free_string. ومرّرNULLلتجاهل النص.
وتحمل الترويسة انقساماً واحداً بحسب المنصة: فـurnet_new_io_loop، وهي مضخّة واصف الملف التي يستخدمها تطبيق Linux، تقع داخل #if !defined(_WIN32) وتغيب عن ملف .def الخاص بـWindows. وعلى Windows تحرّك الحزم بـurnet_device_local_send_packet وurnet_device_local_add_receive_packet.
ويسكن مثال عامل من طرف إلى طرف في cgo/smoke؛ ويبني make smoke_hpp في sdk/cgo اختبارَ الدخان (smoke test) لغلاف C++ مقابل بناء مضيف ثم يشغّله.
تأكّد من أنها تعمل
ارفع الجهاز وافحص المخرج: أي فحص من نوع «ما عنوان IP الخاص بي» عبر النفق ينبغي أن يُبلغ الآن عن عنوان مزوّد، لا عن عنوان الجهاز. والحالة نفسها مرئية في الكود وفي الحساب: فالمستمعون يُطلقون مع تغيّر الاتصال (ويسجّل مقتطف JavaScript أعلاه كل تغيّر موقع لحظة وقوعه)، ويظهر الجهاز في قائمة أجهزة الحساب تحت وصف deviceDescription الذي مرّرته. وعلى واجهة C ABI، يُبلغ urnet_live_handle_count() عن المقابض الحية؛ فتحقّق في اختبارات التسريب من أنه يعود إلى خط الأساس.
إذا أخفق شيء
الدعم هو القنوات المفتوحة: المشكلات على المستودعات (github.com/urnetwork)، وملاحظات المنتج على feedback.ur.io، والبلاغات الأمنية إلى [email protected] (وسياسة الإفصاح على ur.io/vdp). ولا توجد اليوم فئة دعم مدفوعة لـSDK. وهناك توقفان لهما تفسير سهل: توقف النقل على نفق يعمل يعني عادةً حساباً نفدت بياناته، ومُحمِّل JavaScript الذي لا شيء ليحمّله هو وسم npm الموسوم latest. وتغطي الجولة أنماط الإخفاق التي ينبغي أن تعرفها قبل أن تشحن.
ماذا بعد
- خُذ جولة SDK. البنية خلف كل طبقة ربط: انقسامات العمليات، والخيوط، والمصادقة، وإعادة الاتصال، وتحديد حجم هدف الذاكرة.
- أخبر مستخدميك بما ينضمّون إليه. إن كان تطبيقك يوجّه الحركة عبر URnetwork، فـالنظرة العامة هي الوصف المعتمد للمسار ولما يستطيع كل طرف رؤيته، ونموذج التهديد هو السجل الكامل خلفه.
- انظر التطبيقات المكتملة لترى التجربة التي سيحصل عليها مستخدموك: Android وiOS وmacOS وWindows وLinux والمتصفح.