# OpenFlux Exit Node Routing Model: Dual-NIC gVisor Stack Implementation

> Discover the OpenFlux exit node routing model: a dual-NIC gVisor stack implementation for split routing and isolated internet egress. Learn how OpenFlux manages traffic.

- Repository: [p1neappleXpress/OpenFlux](https://github.com/p1neappleXpress/OpenFlux)
- Tags: architecture
- Published: 2026-09-13

---

**The OpenFlux exit node implements a split routing model using a gVisor userspace network stack with two virtual NICs—one for the tunnel subnet (10.10.10.0/24) and one bound to raw sockets for internet egress—forwarding all non-local IPv4 traffic through the physical network interface while maintaining isolated internal routing.**

The OpenFlux exit node acts as a bridge between the encrypted tunnel mesh and the public internet, requiring precise control over packet forwarding. According to the `p1neappleXpress/OpenFlux` source code, the routing model leverages **gVisor**'s TCP/IP stack to create a software-defined router that separates internal tunnel traffic from external egress. This architecture enables the exit node to intercept packets from connected clients and forward them to arbitrary internet destinations without kernel-level network modifications.

## Dual-NIC Architecture Overview

The exit node instantiates a virtual network stack with two distinct network interface controllers (NICs) that partition traffic into internal and external paths.

- **NIC 1 (Tunnel NIC)**: Handles the internal tunnel network at `10.10.10.0/24`. It is backed by a `TunnelLinkEndpoint` that sends and receives packets through the encrypted transport layer. This NIC is created in `NewTCPTunnel()` via `t.gvisorStack.CreateNIC(tunnelNIC, tunnelEP)` in [`main/tunnel/tunnel.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/main/tunnel/tunnel.go).

- **NIC 2 (Internet NIC)**: Represents the physical egress interface. It is backed by a `RawSocketEndpoint` that forwards packets directly to the host's network interface using raw sockets. This is instantiated in `setupExitNode()` via `NewRawSocketEndpoint(tcpip.NICID(2))`.

When the `--exit-node` flag is parsed in [`main/main.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/main/main.go), the application initializes this dual-NIC configuration to enable layer-3 routing between the tunnel and the internet.

## Route Table Configuration in setupExitNode

The `setupExitNode()` function in [`main/tunnel/tunnel.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/main/tunnel/tunnel.go) configures the routing table with two critical static routes and enables global forwarding.

First, the function enables forwarding for all IPv4 traffic across all NICs:

```go
t.gvisorStack.SetForwardingDefaultAndAllNICs(ipv4.ProtocolNumber, true)

```

This ensures the gVisor stack acts as a router, forwarding packets between interfaces when no local socket owns the destination.

Next, it installs the **default route** (0.0.0.0/0) pointing to the internet NIC (ID 2):

```go
t.gvisorStack.AddRoute(tcpip.Route{
    Destination: header.IPv4EmptySubnet,
    NIC:         internetNIC,
})

```

Then it adds a specific route for the **tunnel subnet** (10.10.10.0/24) pointing to the tunnel NIC (ID 1):

```go
tunnelSubnet := tcpip.AddressWithPrefix{
    Address:   tcpip.AddrFrom4([4]byte{10, 10, 10, 0}),
    PrefixLen: 24,
}.Subnet()
t.gvisorStack.AddRoute(tcpip.Route{
    Destination: tunnelSubnet,
    NIC:         tunnelNIC,
})

```

With this configuration, any packet destined for the internal tunnel network stays within the virtual stack, while all other IPv4 traffic is handed to the raw socket for physical transmission.

## Inbound and Outbound Packet Flow

The routing model creates a bidirectional data path between the tunnel clients and external hosts.

**Outbound flow**: When a connected client sends a packet to an external IP, it arrives at the exit node through the `TunnelLinkEndpoint` on NIC 1. The gVisor stack consults its route table, matches the destination against the default route (`0.0.0.0/0`), and forwards the packet to NIC 2. The `RawSocketEndpoint` then transmits the packet onto the physical network using the host's internet interface.

**Inbound flow**: Responses from external servers arrive at the host's physical interface and are captured by the raw socket. The `RawSocketEndpoint` injects these packets into NIC 2 of the gVisor stack. The stack performs reverse routing, matches the destination against the tunnel subnet route, and forwards the packet to NIC 1, where it is sent back through the encrypted tunnel to the originating client.

The exit node also rewrites the source address of outbound packets using the detected or overridden egress IP (`localIPOverride`) to ensure return traffic is correctly routed back through the tunnel.

## Key Implementation Files and Functions

The routing model is distributed across several files in the repository:

- **[`main/main.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/main/main.go)**: Parses the `--exit-node` flag and initializes the transport and tunnel layers. It calls `tunnel.NewTCPTunnel(trans, true)` where the second argument enables exit node mode.
- **[`main/tunnel/tunnel.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/main/tunnel/tunnel.go)**: Contains `NewTCPTunnel()` for NIC creation and `setupExitNode()` for route configuration. It also handles connection logic in `DialTCP()`, which selects NIC 2 when `isExitNode` is true:
  ```go
  if t.isExitNode {
      nic = tcpip.NICID(2)
  }
  ```

- **[`main/tunnel/endpoint.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/main/tunnel/endpoint.go)**: Defines `TunnelLinkEndpoint`, the in-process endpoint that bridges the gVisor stack with the encrypted transport.
- **`main/tunnel/rawsocket_*.go`**: Platform-specific implementations (e.g., Linux and Windows) of the raw socket endpoint that actually push packets onto the physical network.
- **[`main/transport/encrypted.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/main/transport/encrypted.go)**: Manages the encrypted transport layer; the `exitNode` boolean ensures the client and exit ends use opposite directional encryption keys.

## Deploying an OpenFlux Exit Node

To activate the exit node routing model, compile and run OpenFlux with the appropriate flags:

```bash

# Start OpenFlux as an exit node on a VPS

./openflux --exit-node --local-ip 203.0.113.5

```

From application code, you can leverage the routing stack programmatically:

```go
// Create tunnel in exit node mode
tunnel := tunnel.NewTCPTunnel(trans, true) // true enables exit node routing

// Connect to an external service through the exit node
conn, err := tunnel.DialTCP("google.com:443")
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

// Use the connection normally
fmt.Fprintf(conn, "GET / HTTP/1.0\r\nHost: google.com\r\n\r\n")
io.Copy(os.Stdout, conn)

```

When `DialTCP` is called on an exit node, it automatically selects NIC 2 (the internet-facing raw socket), ensuring traffic egresses through the physical network rather than looping back into the tunnel.

## Summary

- The OpenFlux exit node uses a **gVisor userspace network stack** to implement virtual routing without modifying host kernel networking.
- Two virtual NICs partition traffic: **NIC 1** (`10.10.10.0/24`) for the encrypted tunnel and **NIC 2** (raw socket) for internet egress.
- **Route configuration** in `setupExitNode()` sets the default route to the internet NIC and a specific route to the tunnel NIC, with global forwarding enabled via `SetForwardingDefaultAndAllNICs`.
- **Bidirectional flow** allows tunnel clients to reach arbitrary internet destinations while responses are correctly routed back through the encrypted tunnel.
- The `--exit-node` CLI flag activates this routing model, creating a software-defined exit gateway.

## Frequently Asked Questions

### How does the OpenFlux exit node route packets to the internet without a TUN device?

The exit node uses **gVisor's netstack** to create a pure userspace routing layer. Instead of a kernel TUN device, it binds a `RawSocketEndpoint` to the physical network interface. This endpoint captures outgoing packets from the gVisor stack and transmits them directly via raw sockets, while incoming packets are injected back into the stack for routing to tunnel clients.

### Why does the exit node use two separate NICs instead of one?

The **dual-NIC architecture** provides strict traffic isolation between the internal tunnel network (`10.10.10.0/24`) and the external internet. NIC 1 handles only mesh-tunnel traffic through the `TunnelLinkEndpoint`, while NIC 2 handles only raw internet traffic. This separation allows the gVisor stack to make routing decisions based on destination IP, forwarding tunnel-bound packets internally and internet-bound packets externally.

### What happens if the exit node cannot determine its public IP address?

If automatic public IP detection fails, the exit node relies on the `--local-ip` flag or the `localIPOverride` configuration set via `SetLocalIP`. The `setupExitNode()` function registers this address on NIC 2 using `AddProtocolAddress`. If no override is provided and detection fails, the node cannot properly source NAT outbound traffic, causing return packets to be unroutable.

### Is the OpenFlux exit node routing model compatible with IPv6?

The current implementation focuses on **IPv4** routing, as evidenced by the use of `ipv4.ProtocolNumber` in `SetForwardingDefaultAndAllNICs` and the `header.IPv4EmptySubnet` constant for the default route. While gVisor supports IPv6, the OpenFlux exit node would require additional route configurations using `ipv6.ProtocolNumber` and appropriate subnet definitions to support dual-stack operation.