# How Virtual NICs Are Wired in the OpenFlux gVisor Stack: Dual-Interface Architecture Explained

> Understand how OpenFlux wires virtual NICs in its gVisor stack. Discover the dual-interface architecture for efficient network traffic routing within userspace.

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

---

**OpenFlux provisions two virtual NICs—a Tunnel NIC (ID 1) handling internal subnet traffic via a channel endpoint, and an Internet NIC (ID 2) forwarding external packets through raw sockets—to route traffic between the application and the physical network while running a complete TCP/IP stack in userspace.**

The OpenFlux project by p1neappleXpress implements a userspace networking layer using gVisor's `netstack` to isolate application TCP/IP processing from the host kernel. Understanding how virtual NICs are wired within this architecture is essential for comprehending how the system distinguishes between internal virtual network traffic and external internet-bound packets.

## The Dual-NIC Architecture

OpenFlux constructs a split-brain networking topology using two distinct virtual network interfaces that serve complementary purposes:

### Tunnel NIC (NIC ID 1)

The **Tunnel NIC** uses `tcpip.NICID(1)` and binds to a `*TunnelLinkEndpoint`, which implements the gVisor `stack.LinkEndpoint` interface. This interface carries packets between the application and the gVisor stack. In client mode, it represents the internal `10.10.10.0/24` subnet, while in exit-node mode, it handles traffic destined for that internal subnet. The Tunnel NIC serves as the primary attachment point for application-layer connections within the virtual network.

### Internet NIC (NIC ID 2)

The **Internet NIC** uses `tcpip.NICID(2)` and attaches to a `*RawSocketEndpoint`, which abstracts raw socket operations on the host. Created exclusively for exit nodes, this NIC enables the gVisor stack to emit raw IP packets directly onto the host's physical interface, bypassing the kernel's TCP stack to avoid NAT complications. This interface bridges the userspace stack to the external network.

## Wiring the gVisor Stack in tunnel/tunnel.go

The virtual NIC wiring logic resides in [`tunnel/tunnel.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/tunnel.go), where the gVisor stack is initialized and configured based on the node's operational mode.

### Stack Initialization

The gVisor stack is instantiated using `stack.New()` with configured TCP buffer sizes. According to the source code, this occurs at lines 58–64. Immediately following, the Tunnel NIC is created and attached to the `TunnelLinkEndpoint` at lines 72–75:

```go
// Simplified excerpt from tunnel/tunnel.go
s := stack.New(opts)
// ... buffer configuration ...
s.CreateNIC(1, tunnelLinkEndpoint)  // Tunnel NIC creation

```

### Exit Node Configuration

When operating as an exit node (`isExitNode == true`), the `setupExitNode` function executes to wire the Internet NIC. A `RawSocketEndpoint` is constructed via `NewRawSocketEndpoint` (lines 95–100), followed by the creation of NIC ID 2 attached to this raw socket endpoint (lines 106–108).

IP addressing follows at lines 112–122, assigning `10.10.10.x` to the Tunnel NIC and the host-detected egress IP to the Internet NIC. Routing tables are then configured with forwarding enabled:

- **Default route**: All IPv4 traffic (`header.IPv4EmptySubnet`) routes through the Internet NIC (lines 123–127)
- **Internal route**: The `10.10.10.0/24` subnet routes through the Tunnel NIC (lines 129–136)

### Client Configuration

For non-exit nodes, the `setupClient` function simplifies the configuration. The system assigns the static address `10.10.10.2` to the Tunnel NIC and establishes a default route pointing exclusively to this interface (lines 39–53), eliminating the need for raw socket access or external routing.

## TunnelLinkEndpoint Implementation

The `TunnelLinkEndpoint`, defined in [`tunnel/endpoint.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/endpoint.go), bridges the gVisor stack to OpenFlux's transport layer. This structure implements the `stack.LinkEndpoint` interface required by gVisor.

Outbound packets from the stack are captured via `onOutgoingPacket`, which the `TCPTunnel` registers to forward over the underlying transport layer (such as WireGuard or TCP). Inbound packets injected by the transport layer are delivered to the gVisor stack using `dispatcher.DeliverNetworkPacket`. The implementation spans lines 27–45, handling the bidirectional flow between the virtual NIC and the encrypted tunnel transport.

## RawSocketEndpoint for Cross-Platform Raw Sockets

The **Internet NIC** relies on `RawSocketEndpoint` to perform raw socket reads and writes on the host operating system. This abstraction plugs directly into the gVisor stack as a link endpoint, allowing emission of raw IP packets that the host kernel forwards to the physical network.

Platform-specific implementations reside in:
- [`tunnel/rawsocket_linux.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/rawsocket_linux.go) for Linux systems
- [`tunnel/rawsocket_windows.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/rawsocket_windows.go) for Windows
- [`tunnel/rawsocket_darwin.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/rawsocket_darwin.go) for macOS

These files provide the OS-level machinery necessary for the exit node to interact with the physical network interface without kernel TCP/IP processing interference.

## Practical Usage Example

The following example demonstrates creating a TCP tunnel with the dual-NIC architecture initialized:

```go
// Create a tunnel as an exit node (enables both NICs)
t := tunnel.NewTCPTunnel(myTransport, true)

// Dial operates through the gVisor stack via the Tunnel NIC
conn, err := t.DialTCP("example.com:443")
if err != nil {
    log.Fatalf("dial failed: %v", err)
}
defer conn.Close()

// Listen accepts connections on the virtual 10.10.10.0/24 subnet
ln, err := t.ListenTCP(8080)
if err != nil {
    log.Fatal(err)
}
for {
    c, _ := ln.Accept()
    go handleConn(c) // Connection backed by gVisor stack
}

```

The `NewTCPTunnel` call instantiates the gVisor stack and wires both virtual NICs as described above, while `DialTCP` and `ListenTCP` operate entirely within the userspace network stack.

## Summary

- OpenFlux utilizes **two virtual NICs** (Tunnel NIC at ID 1 and Internet NIC at ID 2) to separate internal and external traffic flows.
- **Tunnel NIC** connects to a `TunnelLinkEndpoint` for internal `10.10.10.0/24` subnet communication between the application and gVisor stack.
- **Internet NIC** connects to a `RawSocketEndpoint` for exit nodes to transmit raw IP packets to the physical network interface.
- Wiring occurs in [`tunnel/tunnel.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/tunnel.go), with stack initialization at lines 58–64, Tunnel NIC attachment at lines 72–75, and exit-node specific Internet NIC creation at lines 95–108.
- **Exit nodes** configure forwarding rules to route external traffic through NIC 2 and internal traffic through NIC 1, while **clients** use only the Tunnel NIC with a default route.

## Frequently Asked Questions

### How are the virtual NICs created in the OpenFlux gVisor stack?

The virtual NICs are created in [`tunnel/tunnel.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/tunnel.go) using gVisor's `stack.CreateNIC()` method. The Tunnel NIC (ID 1) is always created and attached to a `TunnelLinkEndpoint`, while the Internet NIC (ID 2) is conditionally created for exit nodes at lines 106–108 and attached to a `RawSocketEndpoint`.

### What distinguishes the Tunnel NIC from the Internet NIC?

The **Tunnel NIC** handles traffic within the virtual `10.10.10.0/24` network using a channel-based endpoint for communication between the application and gVisor stack. The **Internet NIC** only exists on exit nodes and uses raw sockets to bypass the host kernel's TCP stack, allowing the userspace stack to send packets directly to the physical network interface.

### How does traffic routing differ between exit nodes and clients?

Exit nodes configure **IP forwarding** between the two NICs: packets destined for `0.0.0.0/0` (all IPv4 traffic) route through the Internet NIC (ID 2), while packets for `10.10.10.0/24` route through the Tunnel NIC (ID 1). Clients only configure the Tunnel NIC with address `10.10.10.2` and a default route pointing to it, as they do not require direct external network access through raw sockets.

### Which operating systems support the raw socket implementation for the Internet NIC?

The raw socket abstraction is implemented in platform-specific files: [`tunnel/rawsocket_linux.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/rawsocket_linux.go) for Linux, [`tunnel/rawsocket_windows.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/rawsocket_windows.go) for Windows, and [`tunnel/rawsocket_darwin.go`](https://github.com/p1neappleXpress/OpenFlux/blob/main/tunnel/rawsocket_darwin.go) for macOS. This allows exit nodes to operate across these three platforms while maintaining consistent behavior for external packet transmission.