Developers

Erste Schritte mit dem URnetwork SDK

13 Min. LesezeitView as markdown ↗

Dieser Guide bringt einen Entwickler vom leeren Projekt zur ersten Verbindung: ein Konto-JWT besorgen, das Binding für deine Plattform installieren, ein Device hochbringen und prüfen, dass Datenverkehr fließt. Das SDK ist ein Go-Modul, github.com/urnetwork/sdk, zugänglich über drei Binding-Schichten: gomobile-Bindings für Android und Apple, ein WebAssembly-Build für JavaScript und c-shared-Bibliotheken mit einer kuratierten C-ABI für alles andere. Für die Architektur hinter jedem Binding lies die SDK-Tour.

URnetwork nutzt von Mitgliedern betriebene Exit-Geräte. Provider erhalten die Quell-IPs deiner Nutzer auf Relay-Pfaden nicht. Auf dem SDK gebaute Devices verschlüsseln den Datenverkehr zum Provider zudem standardmäßig. Lies So funktioniert URnetwork für das vollständige Modell.

Was du brauchst

Alle drei Schichten liegen vor demselben Kern, auf dem die offiziellen URnetwork-Apps gebaut sind: das Device (die Verbindungs-Engine), der API-Client, das Network-Space-Konfigurationsmodell (mit welchem Plattform-Deployment du sprichst, mit welchen Endpunkten und Flags) und die View-Controller, headless Zustand-plus-Events-Objekte für die üblichen Bildschirme, an die du deine UI binden oder die du ignorieren kannst. Die Operator-API dahinter ist unter /docs/api dokumentiert. Du baust deine eigene App auf URnetworks gehosteter Plattform: API, Relays und Provider sind der Live-Dienst, und deine Nutzer bringen oder erstellen Konten.

Plattform-Untergrenzen, pro Binding:

  • Android — API-Level 24 oder neuer. Das Java-Paket ist com.bringyour.sdk.
  • iOS / macOS — iOS 16.0 und macOS 13.5.
  • JavaScript — nur Browser.
  • cgo — Ubuntu 22.04+ (glibc 2.35+) oder Windows 10+, amd64 und arm64.
  • Bauen aus dem Quellcode — Go 1.26+.

Das SDK ist MPL-2.0 (Mozilla Public License): Verlinke es frei in Closed-Source-Apps. Das Copyleft auf Dateiebene verpflichtet nur dazu, Änderungen an den eigenen Dateien des SDK zu teilen.

Anmelden

Jeder Schnipsel unten nimmt ein byJwt: das Konto-JWT, ein signiertes Login-Token, das die URnetwork-API ausstellt, wenn sich ein Konto authentifiziert. Ein separates API-Key-System gibt es nicht.

Deine App fährt einmal einen Login-Flow durch den API-Client des SDK. authLogin meldet die Auth-Methoden eines Identifiers; authLoginWithPassword schließt ihn ab (authVerify beendet einen per E-Mail oder SMS geschickten Code), und Wallet-Auth und networkCreate sind parallele Einstiegspunkte. Wallet-Auth ist signature-only: Du sendest wallet_address, wallet_message und wallet_signature, nie einen Schlüssel, und keine URnetwork-Fläche fragt nach dem privaten Schlüssel oder der Mnemonic einer Wallet. networkCreate ganz ohne Auth-Methode ist der Sofort-Konto-Pfad (instant account): Er prägt ein dauerhaftes Konto und gibt eine seedphrase zurück, URnetworks eigene Wiederherstellungsphrase für dieses Konto, serverseitig erzeugt und genau einmal zurückgereicht. Zeig sie dem Nutzer dann, sonst hat das Konto keinen Wiederherstellungsweg.

Jeder Pfad gibt by_jwt zurück. Persistiere es, ruf setByJwt auf und übergib es dem Device-Konstruktor. Refresh-Code schreibst du keinen; der Token-Manager der Api rotiert das JWT. Behandle einen harten Auth-Fehler als „Login erneut ausführen“.

Das Konto ist auch die Abrechnungseinheit: Das Konto, dessen JWT das Device hält, ist das, dessen Tarif gemessen wird. Gratis ist ein tägliches Datenkontingent, Pro ein großes monatliches; aktuelle Zahlen auf ur.io/products. Plane ein Konto pro Endnutzer — was der Sofort-Konto-Pfad reibungslos macht — statt eines eingebetteten Kontos, das Nutzung und Verhalten aller Nutzer in einem einzigen Akteur sammelt. Läuft einem Konto das Datenvolumen aus, stockt der Transfer, bis sich das Kontingent auffrischt — sag in deiner UX also „kein Datenvolumen mehr“, nicht ein generischer Netzwerkfehler.

Das SDK besitzt auch den Bezahl-Tarif-Flow, denn der Client darf nie seinen eigenen Preis nennen: Registriere einen Intent mit createSolanaPaymentIntent (reference aus createPaymentReference, plan "monthly" oder "yearly"), nimm amountUsd aus dem Ergebnis und übergib es an buildSolanaPaymentUrl. Die C-ABI hat den Intent-Aufruf, noch nicht den URL-Builder.

Installieren

Vorgebaute Artefakte sind Release-Assets auf github.com/urnetwork/build. Versionen sind datumsbasiert, vYYYY.M.D- (z. B. v2026.7.22-999364023), kein SemVer: Pinne ein Release und aktualisiere bewusst, denn die Version sagt, wann sie geschnitten wurde, nicht, ob sich die API bewegt hat.

Android. Lade URnetworkSdk-.aar herunter (plus -sources.jar für die IDE-Navigation) und leg beide in ein Verzeichnis, das dein Gradle-Modul schon scannt, z. B.:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.aar'])
}

iOS / macOS. Lade URnetworkSdk.xcframework.zip herunter, entpacke es und referenziere es aus einem lokalen Swift-Package als binaryTarget:

// Package.swift
targets: [
    .binaryTarget(name: "URnetworkSdk", path: "URnetworkSdk.xcframework")
]

Alle Typen tragen das Präfix Sdk (SdkDeviceLocal, SdkNetworkSpace, ...). Das xcframework trägt ios/arm64, iossimulator/arm64, macos/arm64 und macos/amd64; eine Intel-Slice für den iOS-Simulator gibt es nicht.

JavaScript. Das npm-Paket ist @urnetwork/sdk-js, ein Nur-Browser-Loader, der Gos wasm_exec.js plus das SDK-wasm lädt und beide in der Seite instanziiert. Installiere den Tag nightly; er wird nach demselben datierten Versionsschema aus dem aktuellen SDK geschnitten und liefert das wasm mit. latest liegt Monate zurück und trägt gar kein wasm — ein schlichtes npm install hinterlässt dir also einen Loader ohne etwas zu laden.

npm install @urnetwork/sdk-js@nightly   # then pin the version it resolved to

Das wasm ist rund 43 MB, der gesamte Go-Kern, es wird also nicht schrumpfen, aber es komprimiert gut: Liefer es als statisches Asset aus (gzip oder brotli, hart gecacht) und lade es lazy, erst wenn der Nutzer eine Verbindungs-Oberfläche erreicht. Lass nie einen Bundler es inlinen oder transformieren, und liefere sdk.wasm und wasm_exec.js immer aus demselben Build; der Glue-Code ist ABI-gepaart mit der Go-Toolchain, die das wasm kompiliert hat, und der Build des SDK erzwingt, dass die beiden Byte für Byte übereinstimmen.

cgo. C-shared-Bibliotheken, dieselbe Schicht, auf der die ausgelieferten URnetwork-Apps für Linux und Windows gebaut sind, mit zwei kuratierten Headern daneben:

  • libURnetworkSdk.so — Linux.
  • URnetworkSdk.dll (+ urnetwork_sdk.def) — Windows.
  • urnetwork_sdk.h — die schlichte C-ABI.
  • urnetwork_sdk.hpp — ein header-only C++17-RAII-Wrapper darüber, der nlohmann/json auf deinem Include-Pfad verlangt.

Alles mit einem C-Foreign-Function-Interface (Rust bindgen, Python ctypes, C# P/Invoke) bildet sich auf diese Handles-und-JSON-ABI ab; der C++-Header ist die eine Sprach-Annehmlichkeit. Mit MSVC erzeugst du zuerst die Importbibliothek aus der .def-Datei:

lib /def:urnetwork_sdk.def /machine:x64 /out:URnetworkSdk.lib

Aus dem Quellcode. Du brauchst Go 1.26+ und für Mobile zuerst make init: Es pinnt exakt das gomobile, mit dem die Bindings geschnitten werden, und installiert das checksec, das das Android-Target ausführt. build_android braucht zusätzlich ein gesetztes ANDROID_NDK_HOME, denn es strippt .comment mit dem llvm-objcopy des NDK. sdk/build-android.sh und sdk/build-ios.sh wickeln beides ein. Aus 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 gatet auf gomobiles Skip-Liste — ein fehlendes Symbol ist also ein Versionskonflikt, kein stiller Binding-Ausfall; gomobile bindet keine Slices von Structs (Listen kreuzen als SdkStringList, SdkIdList, ...) und kein context.Context.

Externe Sicherheitsprüfungen, und ihre Grenzen, behandelt das Bedrohungsmodell.

Die Tunnel-Berechtigung

Die Betriebssystem-Zustimmung, und der Tunnel selbst, gehören deiner App. Das SDK beginnt an der Paketschicht:

  • Auf Android ist der VpnService deiner: Deine App deklariert ihn, holt die VPN-Zustimmung mit VpnService.prepare() ein und ruft establish() auf. Das SDK übernimmt an diesem File-Descriptor.
  • Auf Apple-Plattformen ist die Aufteilung Apples Anforderung, nicht die des SDK: Paket-Tunnel laufen in einer separaten NetworkExtension mit engen Speichergrenzen — der Tunnel-Provider besitzt also das Device, während sich dein App-Prozess aus der Ferne daran anhängt.
  • In einem Browser kann eine Seite kein Netzwerk-Interface besitzen, es gibt also keine Berechtigung zu erfragen, und nichts in der JavaScript-Schicht tunnelt den eigenen Datenverkehr der Seite. Beide JavaScript-Device-Modelle steuern stattdessen ein Device auf der Plattform. Die URnetwork-Erweiterung deckt Browser-Tabs ab; eine native App deckt die ganze Maschine ab.

Was URnetwork über Verbindungen speichert, steht im Bedrohungsmodell.

Verbinden

Ein neues Device startet auf den Netzwerk-Standardwerten. Die Client-Provider-Sitzung ist ab Werk versiegelt: setPerformanceProfile liefert PostQuantumEncryption eingeschaltet aus, das Flag hinter dem App-Schalter „Post-Quanten-Verschlüsselung“ (Post Quantum Encryption), und jeder aktuelle Provider-Build aktiviert die Responder-Seite. Mit eingeschaltetem Flag arbeitet der Client fail-closed: Er trägt keine Anwendungsdaten im Klartext und überspringt einen Provider, mit dem er nicht versiegeln kann, statt ihn unversiegelt zu nutzen. Schalt es aus, und der Datenverkehr kann wieder den Standardpfad nehmen. AllowDirect ist standardmäßig aus; es ist die Opt-in-Geschwindigkeitseinstellung, die den anonymisierenden Hop entfernt und diesem Provider die echte IP deines Nutzers übergibt; die Apps zeigen sie invertiert als „Starke Anonymisierung“ (Strong Anonymization), und gehostete Device-Profile erzwingen sie aus. Leak-Schutz verdrahtest du selbst: Der Kill Switch ist das Primitiv setRouteLocal, ein Device startet mit erlaubtem lokalem Routing, und setRouteLocal(false) lässt Datenverkehr stoppen statt zurückfallen, wenn der Tunnel unten ist. Das SDK bettet keine Analytics und kein Crash-Reporting ein; seine einzigen Verbindungen sind die Plattform-Endpunkte und die Relays und Provider, die das Device nutzt.

Android

Bootstrap-Reihenfolge: Erstelle einen NetworkSpaceManager, der auf app-privaten Speicher zeigt (das Verzeichnis hält lokalen Zustand, einschließlich Zugangsdaten), erstelle den Network Space, dann setz das Konto-JWT auf seinem API-Client. Der Schlüssel ist ein Hostname plus ein Umgebungsname; Produktion ist ("ur.network", "main"), und updateNetworkSpace ist, was einen erstellt, während getNetworkSpace nur einen existierenden Space zurückliest — auf einer frischen Installation gibt es also null zurück. URLs leiten sich aus dem Schlüssel ab (https://api., wss://connect.; eine Nicht-main-Umgebung stellt dem Dienst ein Präfix voran), aber die Ableitung bevorzugt migrationHostName, das die Produktion auf bringyour.com setzt: Die ausgelieferten Apps erreichen api.bringyour.com, und api.ur.network löst nicht auf. Dann erstelle das Device und übergib ihm den File-Descriptor des Tunnels:

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 übernimmt den Besitz des detachten fd und pumpt Pakete in beide Richtungen, bis du es schließt. Fass den fd nach dem Detachen nicht wieder aus Java an.

Drei Konstruktor-Argumente verdienen Sorgfalt. instanceId: Erzeuge einmal beim ersten Start ein Sdk.newId(), persistiere es und verwende es für die Lebensdauer der Installation wieder (ein lebendes Device pro Prozess); eine frische ID bei jedem Start fügt der Geräteliste des Kontos einen Phantom-Eintrag hinzu. keyMaterial: null ist fein für einen reinen Client, aber wenn das Device je Kapazität bereitstellen wird, persistiere getKeyMaterial() im sicheren Speicher der Plattform. Es ist die Provider-Identität des Device, und sie zu verlieren setzt die Zuverlässigkeits-Historie des Providers zurück. memoryTargetByteCount: ein Byte-Budget, an dem das Device seine Puffer und sein GC-Pacing bemisst; wähl eines, das zur echten Obergrenze deines Prozesses passt (siehe die Tour). DeviceLocal ist in-process: Stirbt dein Prozess, stirbt der Tunnel mit ihm — fahr den VpnService also als Foreground-Service und erstelle das Device beim Neustart mit derselben instanceId und demselben Key-Material neu.

iOS / macOS

Der NEPacketTunnelProvider besitzt das SdkDeviceLocal und den Paketfluss, während sich der App-Prozess als Remote-Device über Loopback daran anhängt:

// 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)

Die App prägt das Key-Material und die RPC-PEMs (SdkGenerateDeviceRpcKeyMaterial) und übergibt sie der Extension in der Provider-Konfiguration des NETunnelProviderProtocol; die Extension liest rpc_server_pem, rpc_client_pem und rpc_listen_hostport von dort. Eine App Group gibt es auf diesem Pfad nicht. Siehe die Tour dafür, warum die Aufteilung existiert und wie die Wiederverbindung funktioniert.

JavaScript

import { URNetwork } from "@urnetwork/sdk-js";

const sdk = await URNetwork.init({
  wasmUrl: "/wasm/sdk.wasm",
  wasmExecUrl: "/wasm/wasm_exec.js",
});

Das wasm registriert seine Exporte auf window, fahr also eine Modul-Instanz pro Seite. init ist innerhalb eines Moduls idempotent (ein zweiter Aufruf gibt dieselbe Instanz zurück), aber zwei Kopien des Loaders — etwa zwei Bundles oder zwei Frames, die sich einen Realm teilen — rangeln um dieselben Globals. Initialisiere einmal in einem Singleton auf Modulebene, nie in einem Komponenten-Lifecycle.

Zwei Device-Modelle:

  • sdk.createProxyDevice(...) — ein leichtgewichtiger Client gehosteter Proxy-URLs, für den Fall, dass du nur „eine Proxy-URL, die über URnetwork austritt“ brauchst.
  • sdk.createPlatformDeviceRemote({...}) — ein volles DeviceRemote, das über einen WebSocket Device-RPC mit einem gehosteten Device spricht, für eine echte Verbindungs-UI (Standorte, Listener, Statistiken):
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 wirft, wenn das geladene wasm älter ist als das DeviceRemote-Binding; das ist das Symptom eines alten npm-Tags.

cgo

Der ABI-Vertrag, in Kürze:

  • Objekte sind opake uint64_t-Handles. urnet_release(h) gibt das Handle frei, ohne das Objekt zu stoppen — ruf also zuerst sein *_close/*_stop auf, wo eines existiert.
  • Zurückgegebene char*-Strings gehören dem Aufrufer; gib sie mit urnet_free_string frei.
  • Strukturierte Daten kreuzen die Grenze als UTF-8-JSON-Strings; IDs sind UUID-Strings, Zeiten sind Unix-Epoch-Millisekunden (0 = keine).
  • Callbacks feuern auf beliebigen Go-verwalteten Threads; marshalle auf deinen eigenen Thread. Ihre Strings und Puffer leben nur für den Aufruf, und Handles, die sie dir übergeben, gibst du selbst frei.
  • Fehlbare Aufrufe nehmen ein char** out_error; im Fehlerfall wird es auf eine Meldung gesetzt, die du mit urnet_free_string freigibst. Übergib NULL, um den Text zu ignorieren.

Der Header trägt eine Plattform-Aufteilung: urnet_new_io_loop, die fd-Pumpe, die die Linux-App nutzt, sitzt in #if !defined(_WIN32) und fehlt in der Windows-.def. Auf Windows bewegst du Pakete mit urnet_device_local_send_packet und urnet_device_local_add_receive_packet.

Ein funktionierendes Ende-zu-Ende-Beispiel liegt unter cgo/smoke; make smoke_hpp in sdk/cgo baut den C++-Wrapper-Smoke-Test gegen einen Host-Build und führt ihn aus.

Prüfen, ob es funktioniert

Bring das Device hoch und prüf den Exit: Jeder „Wie ist meine IP“-Check durch den Tunnel sollte jetzt die Adresse eines Providers melden, nicht die der Maschine. Derselbe Zustand ist im Code und im Konto sichtbar: Listener feuern, wenn sich die Verbindung ändert (der JavaScript-Schnipsel oben loggt jeden Standortwechsel, sobald er eintrifft), und das Device erscheint in der Geräteliste des Kontos unter der deviceDescription, die du übergeben hast. Auf der C-ABI meldet urnet_live_handle_count() lebende Handles; assertiere in Leak-Tests, dass er zur Baseline zurückkehrt.

Wenn etwas schiefgeht

Support sind die offenen Kanäle: Issues auf den Repos (github.com/urnetwork), Produkt-Feedback auf feedback.ur.io und Sicherheitsmeldungen an [email protected] (Offenlegungsrichtlinie auf ur.io/vdp). Einen bezahlten SDK-Support-Tarif gibt es heute nicht. Zwei Aussetzer haben einfache Erklärungen: Ein Transfer-Stillstand auf einem funktionierenden Tunnel ist meist ein Konto ohne Datenvolumen, und ein JavaScript-Loader ohne etwas zu laden ist der npm-Tag latest. Die Tour behandelt die Fehlermodi, die du kennen solltest, bevor du auslieferst.

Wie es weitergeht

  • Mach die SDK-Tour. Die Architektur hinter jedem Binding: Prozess-Aufteilungen, Threading, Auth, Wiederverbindung und die Bemessung des Memory-Targets.
  • Sag deinen Nutzern, worauf sie sich einlassen. Wenn deine App Datenverkehr durch URnetwork leitet, ist der Überblick die maßgebliche Darstellung des Pfads und dessen, was jede Partei sehen kann, und das Bedrohungsmodell die vollständige Akte dahinter.
  • Sieh dir die fertigen Apps an, für das Erlebnis, das deine Nutzer bekommen: Android, iOS, macOS, Windows, Linux und den Browser.