The Exit Node’s Split Between Userspace and the Host
OpenFlux’s exit node is where a carried packet becomes Internet traffic, and it is also where the project’s clean userspace model meets an operating system that still controls interfaces, routes, and kernel-generated responses. The focus here is the exit-side boundary: why raw sockets and elevated privilege are needed, how addresses and checksums change as packets cross it, and why a tunnel can fail even when its application-level carrier is connected. Understanding that split makes it possible to separate a transport problem from a host-networking problem.
Two NICs, two owners of the packet path
The exit node gives the gVisor stack two network interfaces with different jobs. One is the tunnel-side interface. It receives packets arriving from the selected transport and represents the private network shared with the client. The other is backed by a raw socket and represents the public Internet side. Routes inside the userspace stack connect the private tunnel subnet to the Internet-facing interface, so a packet destined for an external host can be forwarded without becoming an ordinary kernel-owned TCP connection.
The phrase “raw socket” needs a practical definition. A normal TCP socket asks the host kernel to create and manage the connection. OpenFlux instead works with IP packets produced by its userspace TCP stack. The Linux raw-socket endpoint can send a complete IPv4 packet with its header included and can receive raw TCP packets from the host networking layer. Because the process is operating below the usual socket abstraction, creating those sockets requires elevated capability. The privilege is not a property of Yandex or MAX; it follows from the exit node’s choice to own TCP state in userspace while still putting packets onto the host’s network.
On the outbound path, the userspace stack emits a packet whose source belongs to the private tunnel model and whose destination is the external server. Before sending it through the host’s raw socket, the endpoint changes the source address to the selected public egress address. It then recalculates the IPv4 header checksum and the TCP checksum, because both checksums cover the addresses that just changed. The packet is sent toward its original destination, but it now looks to the public network like traffic originating from the exit host’s egress address.
The inbound path reverses that transformation. The raw endpoint watches for TCP packets addressed to the chosen egress address and limits attention to ports it has seen in active outbound flows. It copies the packet, changes the destination back to the client-side tunnel address, repairs the checksums, and hands the result back through the transport. The client injects the packet into its own userspace stack, where the TCP state and application stream are reconstructed. The exit node is therefore not simply forwarding an Ethernet frame; it is translating packet ownership between a private userspace flow and a public host address.
Some state is needed to keep that translation from accepting unrelated traffic. The raw endpoint records ports associated with outgoing SYN packets and tracks the sequence information expected when a connection begins. It removes ports when a flow ends with FIN or RST. These checks are not a general firewall, but they show why the endpoint must understand enough TCP structure to associate an inbound packet with a flow that the tunnel actually created. The carrier delivers bytes; the exit boundary decides whether those bytes belong to a valid network path.
Consider the beginning of one connection. The client-side stack emits a SYN toward the destination, and the exit-side stack forwards the packet through the raw endpoint. The endpoint records the local source port before rewriting the source address. When the destination answers with a SYN-ACK, the raw receiver checks that the destination is the selected public address and that the port belongs to an active flow. It also checks the expected relationship between the reply’s acknowledgement and the recorded SYN. Only then does it rewrite the destination back to the tunnel address and send the packet through the carrier. The client’s stack can now complete the handshake it believes it started. The service carrying the bytes never has to know why that port and sequence state matter; the exit boundary does.
The checksum step is similarly easy to understate. An IPv4 checksum covers the IP header, while the TCP checksum includes a pseudo-header containing source and destination addresses. Changing an address without recalculating the corresponding checksum produces a packet that may be discarded even though every higher-level component appears to be working. OpenFlux’s endpoint therefore sits at a protocol boundary where small bookkeeping errors become connection failures. That is one reason the exit path is more than a relay between two byte queues.
The two interfaces also explain why an exit node is not interchangeable with a client node. A client needs an entry path and a route into the tunnel. An exit node needs a public egress address, raw packet capability, forwarding configuration, and a way to prevent the host from treating userspace-owned flows as invalid. Swapping the carrier leaves these requirements in place. The carrier determines how packets arrive; it does not supply the exit node’s network identity or privilege.
Why the host can undo a correct userspace flow
A TCP connection normally has a kernel socket behind it. The kernel knows which process owns the socket, which local port is allocated, and what to do when an incoming packet arrives for that connection. OpenFlux’s exit-side TCP state lives in gVisor instead. From the host kernel’s perspective, an outbound packet can describe a connection for which no ordinary local socket exists.
The repository identifies the resulting failure as kernel-generated TCP resets. When the remote side replies, the host may decide that the packet belongs to no connection it owns and produce a reset. That reset can reach the peer and tear down the flow that gVisor was trying to maintain. The carrier may be healthy, the packet may have crossed the transport, and the userspace stack may be ready to process the reply; the host can still cancel the connection at the edge.
This is an important causal distinction. A transport carries a packet from the client to the exit node. The exit node’s raw endpoint puts that packet into the host network. The host kernel then applies its own rules and may emit traffic that is not generated by OpenFlux’s userspace stack. The failure is not solved by reconnecting the application-level carrier, because the carrier is only one participant in the path. It is a conflict between two owners of the same network conversation: gVisor believes it owns the TCP state, while the host kernel has not been told to stop reacting to those packets.
The project’s documented response is to suppress those resets, preferably only when they originate from the dedicated egress address used by the tunnel. A scoped rule preserves the host’s normal behavior for other services and closed ports. A host-wide rule is simpler to describe but changes the meaning of unrelated network events: a port that would normally reject a connection may instead appear silent. On a machine running several services, that difference affects diagnostics, scanning results, monitoring, and the behavior of applications that are not part of OpenFlux.
The scope of the workaround is therefore part of the design. A dedicated alias address gives the host a way to distinguish tunnel egress from other traffic. A separate network namespace or container can go further by isolating the rule and the raw-socket process from the host’s main services. The repository notes that matching by process owner is not a reliable substitute for address or namespace isolation in this case, because the problematic resets are generated by the kernel without an owning userspace socket. The general lesson is broader than this project: when userspace takes ownership of a protocol, host-level recovery mechanisms can still act on packets unless the boundary is deliberately isolated.
Isolation has a cost as well as a benefit. A dedicated address or namespace adds configuration that must be kept consistent with the userspace stack, route table, and monitoring. It may also change how operators collect logs or observe flows. A host-wide rule is easier to apply but creates a larger blast radius and can hide ordinary network behavior. The right choice depends on whether the machine is single-purpose and disposable or carries services whose failure and visibility matter. OpenFlux’s documentation prefers the narrower boundary because the convenience of a broad rule does not remove the consequences for the rest of the host.
This is also why “root required” is an incomplete operational description. Privilege permits the raw-socket operations, but it does not supply a correct egress address, a route to the destination, a compatible kernel policy, or a safe isolation boundary. Those are separate conditions. An administrator can satisfy the privilege check and still obtain a tunnel that fails at the first SYN, or one that works while quietly changing how unrelated ports behave. The mechanism and the deployment policy have to be evaluated together.
Diagnose the boundary before blaming the carrier
A layered failure should be diagnosed in the order that ownership changes. If the exit process cannot create its raw sockets, the problem is privilege or host capability, not the selected transport. If the userspace stack emits packets but the host sends resets, the problem is kernel interaction or firewall policy. If packets leave the host but replies never return, address selection, routing, upstream filtering, or the destination may be involved. Only after the host can send and receive the expected packets does a carrier-specific queue, reconnect, or framing problem become the leading explanation.
The observable symptoms differ, although they can look similar to an application. A missing raw-socket capability can prevent the exit-side Internet interface from being created at all. A route or egress-address error can allow the tunnel to report a carrier connection while no destination flow succeeds. An RST problem may produce connections that start and then close immediately. A carrier queue problem may delay or drop packets before they reach the exit node. OpenFlux’s shared byte interface cannot distinguish these causes by itself; the surrounding counters, logs, and host observations must be interpreted with the packet path in mind.
There are also limits to what the repository establishes. The Linux raw-socket implementation is one platform-specific path, and the source does not provide an independent deployment study showing that every suggested firewall or namespace arrangement is safe for every host. The documented explanation is a strong guide to the intended architecture, not a universal operational guarantee. A reader should test an exit node on an owned machine or controlled network and account for the effect on unrelated services before treating a rule as routine configuration.
The exit node completes OpenFlux’s common packet path, but it does not make the path simple. Its userspace stack provides the TCP semantics; its raw endpoint translates addresses and checksums; its host supplies the public network; and its firewall or namespace policy determines whether those owners interfere. Once that boundary is understood, the transport interface can be evaluated more honestly: it promises a place to send and receive packets, while each adapter must still decide how to manage its own service session, queues, and failures.