How OpenFlux Turns an Application Connection into Packets
OpenFlux’s client-side question is concrete: when an application asks for a TCP connection through a local proxy, how does that request become traffic the selected carrier can move? This chapter follows that path through the SOCKS5 front end, the userspace TCP/IP stack, and the link between networking and transport. The payoff is a precise boundary: the carrier does not receive a browser’s high-level request, but a packet representation of a TCP flow.
The application asks for a TCP connection
A SOCKS5 proxy is an intermediary that accepts a connection from an application and makes a second connection on the application’s behalf. OpenFlux implements the part of SOCKS5 needed for a simple TCP CONNECT. It does not ask the application to understand OpenFlux’s carriers; it asks only for the usual proxy interaction.
The exchange begins with a small negotiation. The application identifies itself as a SOCKS5 client and offers authentication methods. OpenFlux accepts the no-auth method. The application then sends a CONNECT request containing a destination address and port. The address may be an IPv4 literal or a domain name. OpenFlux parses that request, constructs a conventional host:port target, and asks its tunnel object to dial it.
That sequence is easy to mistake for ordinary operating-system networking. The SOCKS5 server does not call the host’s normal net.Dial and then copy bytes over a regular socket. Its dialer is the OpenFlux tunnel. When the tunnel receives the target, it resolves the address, requires an IPv4 result, and creates the connection inside its own network stack. The application still sees a proxy response and a byte stream; the tunnel now owns the network conversation behind that stream.
The distinction matters for both capability and failure. A domain request can fail during name resolution. An IPv6-only result is rejected by the tunnel even though the application may have expected a normal TCP dial. A request using UDP association or BIND is not merely slower or less tested; it is outside the implemented SOCKS5 contract. The local proxy therefore offers a useful but deliberately narrow entry point: no authentication, TCP CONNECT, IPv4 literals or names that resolve to IPv4, and bidirectional byte copying after the connection succeeds.
Once the tunnel dials successfully, the SOCKS5 server returns a success response to the application. Two copy loops then move bytes in opposite directions. Data from the application is written to the tunnel connection; data arriving from the tunnel is written back to the application. The proxy does not reinterpret HTTP, TLS, or an application protocol after CONNECT. It treats the established stream as bytes and lets the network stack below it preserve TCP behavior.
That is why a browser is a useful example. The browser can speak HTTP or HTTPS to its destination, but OpenFlux only needs to understand the browser’s request to connect to a host and port. The details of the web protocol remain inside the TCP stream. A different carrier can replace the mechanism that moves the packets without requiring a different browser integration.
There is a useful difference between the proxy’s success response and the destination’s success. The SOCKS5 server can acknowledge the request only after its tunnel dialer has created the userspace connection; that tells the application that the proxy path accepted the connection, not that an HTTP server has returned a page. From then on, the proxy copies bytes and waits for the network stack and remote endpoint to make progress. If the destination closes, the copy loops eventually end. If the carrier stalls, the application experiences a stalled TCP stream rather than receiving a carrier-specific explanation. The interface boundary simplifies the application’s view while making lower-layer diagnosis more important.
The tunnel replaces an ordinary socket path
The next boundary is the userspace stack. OpenFlux creates a gVisor IPv4/TCP stack and attaches a custom link endpoint to it. In client mode, the stack has a tunnel-side network interface with a private address in the 10.10.10.0/24 range and a route that sends destination traffic to that interface. The address is not a second physical network card. It is part of the model that lets the userspace stack originate and receive IP traffic independently of the host’s ordinary socket table.
When the tunnel creates a TCP endpoint for the requested destination, the stack performs the work normally associated with a TCP connection: it tracks sequence numbers, acknowledgements, retransmission state, and the connection’s lifecycle. The application-facing side is represented through a Go network connection adapted from the stack. The stack-facing side is an IP packet path. This conversion is the reason the carrier can remain unaware of the application protocol while still carrying a complete TCP flow.
The custom link endpoint is the handoff point. When the userspace stack emits a packet, the endpoint copies the packet bytes and invokes the transport’s Send operation. When bytes arrive from the transport, the tunnel injects them into the same endpoint, which delivers them to the stack as an inbound IPv4 packet. The endpoint does not decide whether the packet came from Yandex, MAX, or another future adapter. It only connects the stack’s packet output to the transport boundary and the transport’s packet input back to the stack.
The diagram shows two different kinds of state crossing the boundary. The application and SOCKS5 proxy deal with a stream and a proxy handshake. The userspace stack deals with IP and TCP state. The carrier deals with a service session and a byte payload. A packet that reaches the carrier is therefore not the same object as the application’s original write, even if the payload ultimately contains those bytes. The intermediate TCP headers, addresses, sequence state, and packet timing are part of the network layer that OpenFlux is carrying.
That choice gives OpenFlux a consistent place to handle transport semantics. The TCP stack can acknowledge data, retransmit when a packet is lost, and decide when a connection is established or closed without asking Yandex or MAX to implement TCP themselves. The carrier only needs to move packets well enough for the two stack instances to maintain their state. This is a stronger separation than handing a raw application stream to a relay, because it preserves the network behavior that applications already expect. It is also more expensive: every active flow consumes userspace TCP state, packet buffers, and work in the stack, while the carrier must tolerate the resulting packet rate and ordering.
The custom endpoint is intentionally small, but it is not passive bookkeeping. It copies packet data so the stack and transport do not accidentally share mutable buffers, records packet movement for diagnostics, and exposes a fixed link shape to the gVisor stack. Its job is to preserve the boundary: packet emission becomes a transport send, and transport reception becomes packet injection. Decisions about destination resolution, TCP state, and carrier protocol remain outside that endpoint. Keeping those decisions separate makes the boundary understandable and gives later adapters a stable target.
This design also explains why the tunnel can serve more than one entry surface. A local SOCKS5 connection can be adapted into the userspace stack through DialTCP. An iOS packet-tunnel extension can hand raw IPv4 packets to a related transport bridge. In both cases the selected carrier can receive packet bytes, but the entry-side ownership is different. The SOCKS5 path begins after an application has already chosen TCP. A system packet path begins with the device’s captured IP traffic and must decide which protocols it can handle.
The packet boundary is not a promise of arbitrary packet forwarding. The common desktop stack is configured for IPv4 and TCP. The dialer rejects IPv6 destinations, and the SOCKS5 server does not implement UDP association. The transport abstraction may be able to carry a byte slice of any shape, but the layers that create and consume those slices determine which traffic can be represented correctly. A carrier that accepts more bytes cannot compensate for a client stack that never creates the required protocol state.
The boundary also affects addressing. A domain name in the SOCKS5 request is resolved before the tunnel creates its IPv4 endpoint. The resulting address and port become the destination of the userspace TCP connection. This means name resolution is part of the client-side path and can influence whether a request works, even before any packet reaches the carrier. The iOS bridge later adds a DNS-over-TLS resolver because its mobile environment has a different risk around local answers; the basic SOCKS5 path still illustrates the general rule that address selection belongs to an earlier layer than packet carriage.
The same layering explains why protocol fallback is not automatic. If an application begins with QUIC over UDP, the SOCKS5 CONNECT path has no UDP association to carry it. If an application prefers IPv6 and has no usable IPv4 address, the dialer cannot silently translate that preference into an IPv4 connection. If a packet is too large for a path or a queue cannot accept it, the resulting behavior depends on the stack and adapter rather than on the SOCKS5 handshake. OpenFlux can expose a familiar front door while still requiring the application’s traffic to fit the tunnel’s narrower internal model.
For a reader debugging a connection, the order of questions should follow this order of ownership. First verify that the application completed the proxy negotiation and requested a supported command. Then check whether the target name produced a usable IPv4 address and whether the userspace stack created the flow. Only after those steps does it make sense to investigate whether packets left the link endpoint and whether the selected transport delivered them. This is not a command recipe; it is the causal order implied by OpenFlux’s architecture.
The return path completes the connection
An outbound packet is only useful if the response can be associated with the same userspace flow. When a response arrives through the selected transport, the tunnel injects its bytes into the link endpoint. The gVisor stack examines the IPv4 and TCP headers, matches the packet to the connection it owns, updates its TCP state, acknowledges or reassembles data as necessary, and delivers application bytes through the adapted connection. The SOCKS5 copy loop then writes those bytes back to the application.
This return path is what makes the carrier a carrier rather than an application proxy. The carrier does not have to parse an HTTP response or understand a TLS handshake. It has to deliver the packet bytes to the right endpoint. The userspace stack owns the interpretation of the TCP state on the client side, just as the exit-side stack owns the corresponding state at the other end. Keeping those responsibilities separate is what allows OpenFlux to reuse the tunnel core across different service-shaped transports.
It also gives failures a more useful shape. If the SOCKS5 negotiation is malformed, the failure is at the application-entry boundary. If the request resolves only to IPv6, the tunnel’s addressing constraint is responsible. If the stack emits packets but the transport queue is full, the carrier adapter or wrapper path is implicated. If packets cross the carrier but the exit host sends resets or cannot forward them, the failure is at the exit boundary. A single “proxy failed” message would conceal these distinctions; the layered path gives an investigator places to look.
The practical conclusion is modest but important. OpenFlux turns a familiar TCP proxy request into an IPv4/TCP flow owned by a userspace stack, sends the resulting packet representation across a replaceable transport boundary, and reconstructs the response on the way back. That architecture explains both its flexibility and its limits. The next question is what happens when the packet path reaches the exit node, where userspace networking meets raw sockets, host routes, and kernel behavior that the carrier cannot control.