You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add an OS-level transparent IPv4/TCP gateway to native Rings nodes. Selected host TCP flows are captured by a virtual network interface, reconstructed by one shared userspace TCP data plane, mapped to the existing Rings Onion TCP stream abstraction, and connected to the public target by an advertised TCP exit.
This issue is broader than #665. That issue covers a controlled WebView/HTTP gateway; this issue owns OS-level packet capture, transparent TCP routing, route ownership, underlay exclusion, cleanup, and gateway health.
Current product decisions
These decisions supersede the earlier daemon-service proposal and closed PR #685:
rings run remains the foreground, composable process primitive.
Rings does not wrap or generate systemd, launchd, Windows Service Control Manager, or other service-manager configuration.
No rings daemon ... lifecycle commands are added by this issue. Users may choose any external supervisor.
All gateway core code, platform bindings, Unix configuration, and cleanup leases live in one crates/gateway crate.
Linux, macOS, and Windows native targets provide client + server + gateway capability.
WASM remains client-only and must not compile, link, or export gateway/server code.
iOS NetworkExtension and Android VpnService integration are out of scope for this iteration.
Swift and Kotlin/JNA desktop FFI examples are in scope and align with the existing Python C FFI example. They do not imply iOS/Android VPN support.
Existing foundation
The native HTTP CONNECT ingress already resolves one Onion route per target and calls NativeOnionCircuitHandle::open_tcp_stream.
The Onion TCP runtime already implements target opening, ordered bidirectional byte transfer, half-close handling, exit policy, and resource accounting.
Public target admission already rejects private, loopback, link-local, and other ineligible targets.
StopSource / StopToken provide cooperative native shutdown.
The C ABI and Python example already cover provider creation, listening, JSON-RPC requests, signer callbacks, returned-string ownership, and destruction.
Architecture
One native gateway crate
The implementation is intentionally one shared data plane with small platform effect boundaries. The current source layout is:
Platform code is an effect boundary. The shared server, flow, TCP, admission, backpressure, and Onion mapping logic contains no Linux/macOS/Windows system API calls.
Core contracts
PacketIo: receive and inject complete IP packets. It is packet-semantic rather than fd-specific.
GatewayPlan: declarative addresses, MTU, include/exclude routes, routing mode, and explicit DNS policy.
TunnelControl: reconcile stale state, install exclusions, establish the packet device and capture routes, then consume a linear cleanup lease on teardown.
UnderlayPolicy: keep Rings overlay, bootstrap, ICE, and control traffic outside capture routes before packet admission.
OnionStreamConnector: bind one admitted immutable target to one Onion TCP stream without exposing Rings internals to the gateway crate.
The Onion byte pump accepts a generic AsyncRead + AsyncWrite stream rather than only tokio::net::TcpStream, so the userspace TCP stack reuses the existing Onion transfer, half-close, timeout, and accounting logic.
Underlay ordering for signaling and ICE
Native connectPeerViaHttp bootstrap uses the same underlay policy as ICE instead of a second routing mechanism. Before the first JSON-RPC request, Rings parses and resolves the HTTP endpoint, admits every IPv4 result, and pins the request client to exactly those addresses. Redirects are rejected because their destination has not crossed the admission boundary. Named endpoints require explicit DNS bypass; literal IPv4 endpoints remain usable with DNS block.
The transport handshake, not a periodic poll, owns first-use candidate admission:
Parse every literal remote ICE candidate IP from the offer or answer SDP before pair nomination.
Await the installed UnderlayCandidateAdmission policy.
Apply set_remote_description only after all required host-route exclusions succeed.
Publish the candidate projection only after the remote description succeeds.
Policy registration takes an asynchronous write lock while each remote-description application holds a read lock. Therefore, once gateway registration returns, all older handshakes have either published their candidates or failed, and every newer handshake observes the gateway policy.
The gateway serializes route mutations through one gate and keeps every admitted signaling or ICE target monotonic for the lifetime of one tunnel lease. A lagging topology snapshot may add candidates but cannot delete a candidate between route admission and ICE nomination. The gate is cleared only after successful tunnel teardown; if cleanup fails, the fail-safe policy remains installed with the failed lease.
mDNS hostnames are not claimed to be literal candidate-IP evidence. The IPv4 milestone relies on numeric server-reflexive/relay candidates plus fixed ICE-server and explicitly configured bypass targets; named/private host-candidate behavior must not be described as leak-free full-VPN support.
Gateway lifecycle
Stopped -> Starting -> Active -> Stopping -> Stopped
| ^
v |
Degraded
|
v
Failed -> Stopping -> Stopped
Packet admission is allowed only in Active, after underlay exclusions and capture resources are installed. Gateway health is observable independently from process health.
Flow lifecycle
Captured -> TargetBound -> RouteBuilding -> Opening -> Established
|
v
HalfClosed
|
v
Closed | Failed
The original destination is part of the flow identity. It cannot change after capture or target admission. Terminal flows release their bounded-flow capacity exactly once.
Safety invariants
A captured flow never opens a direct public connection from the client node.
If no valid route or compatible exit exists, the flow fails closed with no silent direct fallback.
A flow target is immutable for the lifetime of its Onion stream.
Underlay exclusions are established before captured packets are admitted; an HTTP signaling endpoint is admitted before its first request, and a newly received literal ICE IP is admitted before WebRTC applies its remote SDP.
Gateway failure remains observable independently from the rings run process.
Stop, restart by an external supervisor, startup failure, and stale-state reconciliation are idempotent and leave routes/DNS recoverable.
UDP, DNS, IPv4, and IPv6 behavior is explicit; an IPv4/TCP milestone is never described as a leak-free full VPN.
Existing exit target policy remains authoritative at both route admission and exit connection.
If runtime processing and shutdown both fail, diagnostics retain both failures; shutdown still attempts every flow, packet flush, and final lifecycle transition.
Platform bindings
Unix privilege boundary
gateway-config-unix is a separately launched foreground resource helper; it never invokes privilege elevation or a service manager.
The helper canonicalizes the socket and durable-ledger parents before mutation. Each direct parent must belong to the helper effective UID; every ancestor must belong to that UID or root and must reject group/other writes.
The socket is mode 0600 and may be chowned to one node UID. Kernel peer credentials must match that UID before any request is processed.
One authenticated connection owns establishment, descriptor transfer, bypass reconciliation, and teardown. Disconnect or malformed control traffic triggers cleanup.
Linux
Use /dev/net/tun for the IPv4/TCP gateway.
Keep CAP_NET_ADMIN out of the network-facing Rings process.
Use the narrow foreground helper and an SCM_RIGHTS packet fd for interface/routes/lease operations only.
Install overlay/bootstrap/ICE/control bypass routes before capture routes.
Reconcile stale route-ledger state on startup.
Do not install, generate, or manage systemd services.
macOS
Use the built-in utun packet interface for the CLI/native gateway.
Keep route configuration and cleanup in crates/gateway/src/bindings/unix.
Preserve foreground rings run semantics.
Do not install or manage launchd agents.
Signed NetworkExtension/App Store packaging is not part of this iteration.
Windows
Use Wintun for a packet interface with the same PacketIo semantics.
Keep route/interface mutation and cleanup behind a recoverable Windows lease.
Discover the DLL from an explicit config/env path, the executable directory, or the normal loader path and report failure explicitly.
Prove behavior with a Windows runtime smoke test, not cross-compilation alone.
Implementation roadmap and status
Status snapshot: 2026-08-29, isolated sibling worktree on branch codex/tun-gateway-684, rebased onto origin/master at d6775941 after PR #696 moved the workspace to v0.19 and removed Subring/SNARK. The implementation is published for review in PR #697 at current head 72a2ef0e; checked items mean implemented and validated, not merged. The rebase retained the upstream removals and reapplied only the gateway changes.
Freeze requirements and acceptance evidence
Gateway-first scope, foreground process model, desktop targets, WASM boundary, mobile non-goals, and FFI examples are recorded here.
Make DNS policy deterministic without system resolver APIs
Both policies require an explicit IPv4 resolver list. block installs more-specific capture routes and drops captured UDP plus TCP/53; bypass installs baseline-gateway host routes. Exact capture/bypass conflicts fail validation, and omitted resolvers are explicitly outside the guarantee.
Implement Linux TUN integration and helper protocol
Current-head Linux CI passes the privileged TUN/route smoke, real helper-process/SCM_RIGHTS boundary, captured TCP over a two-hop Onion public exit, and kernel TCP through TUN plus the two-hop Onion exit.
Implement macOS utun integration and helper protocol
Current-head macOS CI passes the privileged utun smoke and the real helper-process/SCM_RIGHTS normal-teardown and disconnect-cleanup test.
Implement Windows Wintun integration
Current-head Windows CI downloads and verifies the signed Wintun runtime, then passes the privileged Wintun capture, bypass, cleanup, and stale-ledger reconciliation test.
Integrate with rings-node
Optional foreground rings run task, cooperative stop, typed config, status endpoint, Onion connector, pre-request HTTP signaling admission/pinning, and synchronous ICE admission gate.
Enforce WASM client-only output
Target cfg, dependency-tree rejection, strict wasm32 clippy/build jobs, and no gateway/server export.
Add Swift and Kotlin desktop FFI examples
Swift dynamic C ABI wrapper and Kotlin/JNA wrapper align with Python ownership, callbacks, provider lifecycle, and two-provider flow. Deterministic fake-ABI tests cover wrapper semantics; both loaders also resolve and call the built Rust dynamic library without duplicating wallet cryptography.
Close native runtime evidence and remote CI
Current-head native jobs pass on Linux TUN, macOS utun, and Windows Wintun, including normal cleanup and stale/disconnect recovery.
Linux also passes captured TCP and kernel TCP through a two-hop Rings Onion public exit.
Python, Swift, and Kotlin/JNA FFI examples pass against the real Rust library; WASM remains client-only.
rings-gateway: 61 library tests passed, 1 privileged controller test ignored by the ordinary suite; gateway-config-unix: 1 test passed. A regression forces runtime and shutdown packet-I/O failures together and verifies both errors are preserved while local flow/TCP ownership and lifecycle still reach inactive. The ignored Unix helper process integration was then executed explicitly with privileges and passed.
rings-transport native WebRTC: 98 tests passed, including a real offer test proving policy rejection precedes remote-description application.
rings-node: 292 tests passed, 1 public three-node/TUN integration ignored by the ordinary suite; 2 CLI tests passed. One preceding full run observed a non-deterministic existing E2E frame-sequence failure; the exact test and the complete suite both passed on rerun. The ignored Linux integration was executed explicitly with privileges and passed.
Strict native Clippy passed for gateway/transport/core/node; dummy transport compilation and Clippy passed.
wasm32 strict Clippy passed for core and node browser builds after the signaling change; CI rejects rings-gateway, route_manager, or tun-rs in the browser dependency graph.
Linux gateway strict Clippy and Linux node test-target cargo zigbuild passed.
Windows gateway all-target strict Clippy passed on the installed MSVC target; iOS and Android cross-checks preserve the explicit unsupported-gateway boundary. A full Windows node cross-check from macOS is not runtime evidence and is locally limited by the absence of Windows SDK C headers; the Windows runner job is authoritative.
Python FFI: 15 tests passed against the real Rust library, including provider/offer/E2E integration. Swift passed deterministic callback/lifetime tests plus real Rust dylib load, all-symbol resolution, and an exported call. Kotlin/JNA passed the same two layers with task rerun forced and an architecture-matched Rust dylib/JVM.
Nightly rustfmt, Taplo, Cargo deny, and git diff --check passed for the rebased snapshot. actionlint passes the modified qaci.yml; the full-repository invocation remains red only on pre-existing shellcheck findings in unchanged auto-release.yml.
The dated in-memory data-plane benchmark is recorded in crates/gateway/BENCHMARKS.md with explicit omissions; it does not measure TUN, WebRTC, Onion cryptography, or public throughput.
The privileged platform smoke captures 1.1.1.0/24, installs a baseline-gateway /32 bypass for 1.1.1.1, proves actual TCP/HTTP reachability to the bypass target, confirms another address in the captured prefix arrives on TUN/utun/Wintun, then verifies normal teardown and stale-ledger reconciliation. It passed on privileged aarch64 Linux in an isolated OrbStack environment. That run exposed and fixed Linux ENXIO cleanup after the kernel auto-removes interface-bound routes. The stronger Linux kernel-TCP test also passed through TUN, the shared smoltcp data plane, a two-hop Rings Onion route, and the public TCP exit; it exposed and fixed explicit dropping of native IPv6 control frames in the IPv4-only milestone. A later DNS audit closed a split-route leak: both policies now require explicit IPv4 resolvers, block installs capture /32 routes, bypass installs baseline-gateway /32 routes, and exact capture/bypass conflicts fail validation. The updated privileged Linux smoke and kernel-TCP chain both passed again. A separate privileged process test then launched the real gateway-config-unix binary and passed kernel peer authentication, SCM_RIGHTS TUN transfer, live bypass replacement, explicit teardown, and client-disconnect cleanup. Native HTTP seed/signaling bootstrap now resolves and admits its target before the first request, pins DNS to the admitted IPv4 set, and rejects redirects to unadmitted destinations; focused resolver, admission-policy, pinned-request, and redirect tests pass. After the shutdown-error aggregation change, the ignored in-memory captured-TCP/two-hop-Onion/public-exit test was rerun and passed. Current-head remote CI now passes the Linux TUN/helper/kernel-TCP chain, macOS utun/helper chain, Windows Wintun runtime smoke, Python/Swift/Kotlin FFI jobs, WASM client-only jobs, dependency policy, Miri, strict Clippy/rustfmt, release builds, sanitizers, and CodeQL.
Acceptance criteria
A real captured IPv4 TCP client flow reaches a public test server only through a multi-hop Rings Onion route and TCP exit.
Removing the exit or breaking route construction fails the captured flow without direct egress.
Rings overlay/bootstrap/ICE/control traffic remains reachable while capture routes are owned.
TCP data, EOF, half-close, reset/error, backpressure, retransmission, timeout, MTU, and connection limits are covered.
Linux TUN, macOS utun, and Windows Wintun each pass a native runtime integration smoke test.
Stop and crash reconciliation do not leave a route that indefinitely blackholes the host.
RPC/inspection distinguishes process health from interface state, routing mode, exit availability, flow count, and active/degraded/failed gateway health.
Existing CLI shapes and non-gateway behavior remain compatible.
WASM builds remain client-only and contain no gateway/server dependency or exports.
Python, Swift, and Kotlin examples exercise equivalent C ABI ownership and provider lifecycle behavior.
Documentation and benchmarks do not present partial interception as a complete VPN.
Explicit non-goals for this iteration
iOS NetworkExtension integration.
Android VpnService integration.
UDP over Onion circuits.
IPv6 capture or egress.
Private/LAN target access through public exits.
Raw source-IP or end-to-end TCP packet identity preservation.
systemd, launchd, Windows SCM, or other service-manager wrappers.
Summary
Add an OS-level transparent IPv4/TCP gateway to native Rings nodes. Selected host TCP flows are captured by a virtual network interface, reconstructed by one shared userspace TCP data plane, mapped to the existing Rings Onion TCP stream abstraction, and connected to the public target by an advertised TCP exit.
This issue is broader than #665. That issue covers a controlled WebView/HTTP gateway; this issue owns OS-level packet capture, transparent TCP routing, route ownership, underlay exclusion, cleanup, and gateway health.
Current product decisions
These decisions supersede the earlier daemon-service proposal and closed PR #685:
rings runremains the foreground, composable process primitive.rings daemon ...lifecycle commands are added by this issue. Users may choose any external supervisor.crates/gatewaycrate.Existing foundation
NativeOnionCircuitHandle::open_tcp_stream.StopSource/StopTokenprovide cooperative native shutdown.Architecture
One native gateway crate
The implementation is intentionally one shared data plane with small platform effect boundaries. The current source layout is:
Platform code is an effect boundary. The shared server, flow, TCP, admission, backpressure, and Onion mapping logic contains no Linux/macOS/Windows system API calls.
Core contracts
PacketIo: receive and inject complete IP packets. It is packet-semantic rather than fd-specific.GatewayPlan: declarative addresses, MTU, include/exclude routes, routing mode, and explicit DNS policy.TunnelControl: reconcile stale state, install exclusions, establish the packet device and capture routes, then consume a linear cleanup lease on teardown.UnderlayPolicy: keep Rings overlay, bootstrap, ICE, and control traffic outside capture routes before packet admission.OnionStreamConnector: bind one admitted immutable target to one Onion TCP stream without exposing Rings internals to the gateway crate.The Onion byte pump accepts a generic
AsyncRead + AsyncWritestream rather than onlytokio::net::TcpStream, so the userspace TCP stack reuses the existing Onion transfer, half-close, timeout, and accounting logic.Underlay ordering for signaling and ICE
Native
connectPeerViaHttpbootstrap uses the same underlay policy as ICE instead of a second routing mechanism. Before the first JSON-RPC request, Rings parses and resolves the HTTP endpoint, admits every IPv4 result, and pins the request client to exactly those addresses. Redirects are rejected because their destination has not crossed the admission boundary. Named endpoints require explicit DNS bypass; literal IPv4 endpoints remain usable with DNS block.The transport handshake, not a periodic poll, owns first-use candidate admission:
UnderlayCandidateAdmissionpolicy.set_remote_descriptiononly after all required host-route exclusions succeed.Policy registration takes an asynchronous write lock while each remote-description application holds a read lock. Therefore, once gateway registration returns, all older handshakes have either published their candidates or failed, and every newer handshake observes the gateway policy.
The gateway serializes route mutations through one gate and keeps every admitted signaling or ICE target monotonic for the lifetime of one tunnel lease. A lagging topology snapshot may add candidates but cannot delete a candidate between route admission and ICE nomination. The gate is cleared only after successful tunnel teardown; if cleanup fails, the fail-safe policy remains installed with the failed lease.
mDNS hostnames are not claimed to be literal candidate-IP evidence. The IPv4 milestone relies on numeric server-reflexive/relay candidates plus fixed ICE-server and explicitly configured bypass targets; named/private host-candidate behavior must not be described as leak-free full-VPN support.
Gateway lifecycle
Packet admission is allowed only in
Active, after underlay exclusions and capture resources are installed. Gateway health is observable independently from process health.Flow lifecycle
The original destination is part of the flow identity. It cannot change after capture or target admission. Terminal flows release their bounded-flow capacity exactly once.
Safety invariants
rings runprocess.Platform bindings
Unix privilege boundary
gateway-config-unixis a separately launched foreground resource helper; it never invokes privilege elevation or a service manager.Linux
/dev/net/tunfor the IPv4/TCP gateway.CAP_NET_ADMINout of the network-facing Rings process.SCM_RIGHTSpacket fd for interface/routes/lease operations only.macOS
utunpacket interface for the CLI/native gateway.crates/gateway/src/bindings/unix.rings runsemantics.Windows
PacketIosemantics.Implementation roadmap and status
Status snapshot: 2026-08-29, isolated sibling worktree on branch
codex/tun-gateway-684, rebased ontoorigin/masteratd6775941after PR #696 moved the workspace to v0.19 and removed Subring/SNARK. The implementation is published for review in PR #697 at current head72a2ef0e; checked items mean implemented and validated, not merged. The rebase retained the upstream removals and reapplied only the gateway changes.rings-gatewayand its modeldef1capture, more-specific exclusions, write-ahead durable ledger, reverse cleanup, stale reconciliation.blockinstalls more-specific capture routes and drops captured UDP plus TCP/53;bypassinstalls baseline-gateway host routes. Exact capture/bypass conflicts fail validation, and omitted resolvers are explicitly outside the guarantee.rings-noderings runtask, cooperative stop, typed config, status endpoint, Onion connector, pre-request HTTP signaling admission/pinning, and synchronous ICE admission gate.Validation evidence currently available
Local non-privileged evidence from the worktree:
rings-gateway: 61 library tests passed, 1 privileged controller test ignored by the ordinary suite;gateway-config-unix: 1 test passed. A regression forces runtime and shutdown packet-I/O failures together and verifies both errors are preserved while local flow/TCP ownership and lifecycle still reach inactive. The ignored Unix helper process integration was then executed explicitly with privileges and passed.rings-transportnative WebRTC: 98 tests passed, including a real offer test proving policy rejection precedes remote-description application.rings-core: 489 tests passed, 1 pre-existing performance probe ignored. The lower count is expected after upstream PR Remove unused Subring and Nova SNARK features for v0.19.0 #696 removed Subring.rings-node: 292 tests passed, 1 public three-node/TUN integration ignored by the ordinary suite; 2 CLI tests passed. One preceding full run observed a non-deterministic existing E2E frame-sequence failure; the exact test and the complete suite both passed on rerun. The ignored Linux integration was executed explicitly with privileges and passed.rings-gateway,route_manager, ortun-rsin the browser dependency graph.cargo zigbuildpassed.git diff --checkpassed for the rebased snapshot.actionlintpasses the modifiedqaci.yml; the full-repository invocation remains red only on pre-existing shellcheck findings in unchangedauto-release.yml.crates/gateway/BENCHMARKS.mdwith explicit omissions; it does not measure TUN, WebRTC, Onion cryptography, or public throughput.The privileged platform smoke captures
1.1.1.0/24, installs a baseline-gateway/32bypass for1.1.1.1, proves actual TCP/HTTP reachability to the bypass target, confirms another address in the captured prefix arrives on TUN/utun/Wintun, then verifies normal teardown and stale-ledger reconciliation. It passed on privileged aarch64 Linux in an isolated OrbStack environment. That run exposed and fixed LinuxENXIOcleanup after the kernel auto-removes interface-bound routes. The stronger Linux kernel-TCP test also passed through TUN, the shared smoltcp data plane, a two-hop Rings Onion route, and the public TCP exit; it exposed and fixed explicit dropping of native IPv6 control frames in the IPv4-only milestone. A later DNS audit closed a split-route leak: both policies now require explicit IPv4 resolvers,blockinstalls capture/32routes,bypassinstalls baseline-gateway/32routes, and exact capture/bypass conflicts fail validation. The updated privileged Linux smoke and kernel-TCP chain both passed again. A separate privileged process test then launched the realgateway-config-unixbinary and passed kernel peer authentication,SCM_RIGHTSTUN transfer, live bypass replacement, explicit teardown, and client-disconnect cleanup. Native HTTP seed/signaling bootstrap now resolves and admits its target before the first request, pins DNS to the admitted IPv4 set, and rejects redirects to unadmitted destinations; focused resolver, admission-policy, pinned-request, and redirect tests pass. After the shutdown-error aggregation change, the ignored in-memory captured-TCP/two-hop-Onion/public-exit test was rerun and passed. Current-head remote CI now passes the Linux TUN/helper/kernel-TCP chain, macOS utun/helper chain, Windows Wintun runtime smoke, Python/Swift/Kotlin FFI jobs, WASM client-only jobs, dependency policy, Miri, strict Clippy/rustfmt, release builds, sanitizers, and CodeQL.Acceptance criteria
Explicit non-goals for this iteration