Using 2 nics not working

This commit is contained in:
Owen
2025-11-05 21:46:29 -08:00
parent 348cac66c8
commit 2c8755f346
11 changed files with 1234 additions and 43 deletions

217
netstack2/README.md Normal file
View File

@@ -0,0 +1,217 @@
# Netstack2 TCP/UDP Proxying
This package provides transparent TCP and UDP connection proxying through WireGuard netstack, inspired by the tun2socks project.
## Overview
The netstack implementation now supports terminating TCP and UDP connections directly in the netstack layer and transparently proxying them to their actual destination targets. This is useful when you want to intercept and forward traffic that enters through a WireGuard tunnel.
## ⚠️ Important: Dual-Interface Architecture
**WARNING**: Installing TCP/UDP handlers on the same interface used by WireGuard can cause packet handling conflicts, as WireGuard already manipulates packets at the transport layer.
**Recommended Approach**: Use `EnableProxyOnSubnet()` to create a **secondary NIC** (Network Interface Card) within the netstack that is dedicated to TCP/UDP proxying. This approach:
1. **Isolates proxying from WireGuard**: WireGuard operates on NIC 1, proxying on NIC 2
2. **Uses route-based steering**: Specific subnets are routed to the proxy NIC via routing table entries
3. **Avoids conflicts**: Each NIC has its own packet handling pipeline
### Architecture Comparison
#### ❌ Single Interface (Not Recommended)
```
Client → WireGuard Tunnel → NIC 1 (with TCP/UDP handlers) → Conflicts!
Both WireGuard and handlers process same packets
```
#### ✅ Dual Interface (Recommended)
```
Client → WireGuard Tunnel → NIC 1 (WireGuard traffic, no handlers)
Routing Table
NIC 2 (TCP/UDP proxy for specific subnets)
Direct connection to targets
```
## Key Differences from tun2socks
While tun2socks proxies connections to an upstream SOCKS proxy, newt's implementation directly connects to the actual target addresses. This is because newt has direct network access to the targets.
## Architecture
### TCP Handling
1. **TCP Forwarder**: Installed on the netstack to intercept incoming TCP connections
2. **Connection Establishment**: Performs the TCP three-way handshake with the client through netstack
3. **Target Connection**: Establishes a direct TCP connection to the actual target
4. **Bidirectional Proxy**: Copies data bidirectionally between the netstack connection and the target connection
5. **Half-Close Support**: Properly handles TCP half-close semantics for graceful shutdown
### UDP Handling
1. **UDP Forwarder**: Installed on the netstack to intercept incoming UDP packets
2. **Connection Creation**: Creates a UDP endpoint in netstack for the client
3. **Target Connection**: Establishes a direct UDP connection to the actual target
4. **Packet Forwarding**: Forwards UDP packets bidirectionally with timeout handling
5. **Session Timeout**: UDP sessions timeout after 60 seconds of inactivity
## Usage
### ✅ Recommended: Subnet-Based Proxying (Dual-Interface)
This is the **recommended approach** to avoid conflicts with WireGuard:
```go
import "github.com/fosrl/newt/netstack2"
// Create netstack normally (no proxying on main interface)
tun, tnet, err := netstack2.CreateNetTUN(localAddresses, dnsServers, mtu)
// Define which subnets should be proxied
// These could be specific services or networks you want to intercept
proxySubnets := []netip.Prefix{
netip.MustParsePrefix("192.168.1.0/24"), // Internal network
netip.MustParsePrefix("10.20.0.0/16"), // Service network
}
// Enable proxying on a secondary NIC for these subnets only
err = tnet.EnableProxyOnSubnet(proxySubnets, true, true) // TCP=true, UDP=true
if err != nil {
log.Fatalf("Failed to enable proxy on subnet: %v", err)
}
// Now:
// - Traffic to 192.168.1.0/24 and 10.20.0.0/16 → Proxied via NIC 2
// - All other traffic → Handled normally by WireGuard on NIC 1
```
### Option 2: Enable During Creation (Single-Interface - Use with Caution)
**⚠️ May conflict with WireGuard packet handling!**
```go
// Enable proxying on the main interface
tun, tnet, err := netstack2.CreateNetTUNWithOptions(
localAddresses,
dnsServers,
mtu,
netstack2.NetTunOptions{
EnableTCPProxy: true,
EnableUDPProxy: true,
},
)
// All TCP/UDP traffic will be intercepted - may conflict with WireGuard
```
### Option 3: Enable After Creation (Single-Interface - Use with Caution)
**⚠️ May conflict with WireGuard packet handling!**
```go
// Create netstack normally
tun, tnet, err := netstack2.CreateNetTUN(localAddresses, dnsServers, mtu)
// Enable TCP proxying later on main interface
if err := tnet.EnableTCPProxy(); err != nil {
log.Fatalf("Failed to enable TCP proxy: %v", err)
}
// Enable UDP proxying later on main interface
if err := tnet.EnableUDPProxy(); err != nil {
log.Fatalf("Failed to enable UDP proxy: %v", err)
}
```
### Option 4: Backward Compatible (No Proxying)
```go
// Use the standard CreateNetTUN - no proxying enabled
tun, tnet, err := netstack2.CreateNetTUN(localAddresses, dnsServers, mtu)
// Connections will use standard netstack dial methods
```
## Configuration Parameters
### TCP Settings
- **TCP Connect Timeout**: 5 seconds for establishing connections to targets
- **TCP Keepalive Idle**: 60 seconds before first keepalive probe
- **TCP Keepalive Interval**: 30 seconds between keepalive probes
- **TCP Keepalive Count**: 9 probes before giving up
- **TCP Half-Close Timeout**: 60 seconds for graceful shutdown
### UDP Settings
- **UDP Session Timeout**: 60 seconds of inactivity before closing session
- **Max Packet Size**: 65535 bytes (standard UDP maximum)
## Performance Considerations
1. **Buffer Sizes**: 32KB buffers for TCP, 64KB for UDP
2. **Goroutines**: Each connection spawns 2 goroutines for bidirectional copying
3. **Memory**: Buffer allocations are reused where possible
4. **Socket Options**: Optimized TCP send/receive buffer sizes from stack defaults
## Example: WireGuard Integration
```go
func (s *WireGuardService) createNetstack() error {
// Create netstack WITHOUT proxying on the main interface
s.tun, s.tnet, err = netstack2.CreateNetTUN(
[]netip.Addr{tunnelIP},
s.dns,
s.mtu,
)
if err != nil {
return err
}
// Define subnets that should be proxied
// These are typically the target services you want to intercept
proxySubnets := []netip.Prefix{
netip.MustParsePrefix("192.168.100.0/24"), // Service subnet 1
netip.MustParsePrefix("10.50.0.0/16"), // Service subnet 2
}
// Enable proxying on a secondary NIC for specific subnets
// This avoids conflicts with WireGuard's packet handling
err = s.tnet.EnableProxyOnSubnet(proxySubnets, true, true)
if err != nil {
return fmt.Errorf("failed to enable proxy: %v", err)
}
// Now:
// - WireGuard handles encryption/decryption on NIC 1
// - Traffic to proxySubnets is routed to NIC 2 for TCP/UDP proxying
// - All other traffic goes through normal WireGuard path
return nil
}
```
## Debugging
When proxying is enabled:
- Failed TCP connections will result in RST packets being sent back to the client
- Failed UDP connections will silently drop packets (standard UDP behavior)
- Connection timeouts follow standard TCP/UDP semantics
## Limitations
1. **No Filtering**: All connections are proxied, no filtering capability
2. **Direct Routing**: Assumes direct network access to all target addresses
3. **No NAT Traversal**: Does not handle complex NAT scenarios
4. **Memory Usage**: Each active connection uses ~64KB of buffer space
## Future Enhancements
Potential improvements:
- Connection filtering/allow-listing
- Per-connection rate limiting
- Connection statistics and monitoring
- Dynamic timeout configuration
- Connection pooling for frequently accessed targets

301
netstack2/handlers.go Normal file
View File

@@ -0,0 +1,301 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/
package netstack2
import (
"context"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/fosrl/newt/logger"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"gvisor.dev/gvisor/pkg/waiter"
)
const (
// defaultWndSize if set to zero, the default
// receive window buffer size is used instead.
defaultWndSize = 0
// maxConnAttempts specifies the maximum number
// of in-flight tcp connection attempts.
maxConnAttempts = 2 << 10
// tcpKeepaliveCount is the maximum number of
// TCP keep-alive probes to send before giving up
// and killing the connection if no response is
// obtained from the other end.
tcpKeepaliveCount = 9
// tcpKeepaliveIdle specifies the time a connection
// must remain idle before the first TCP keepalive
// packet is sent. Once this time is reached,
// tcpKeepaliveInterval option is used instead.
tcpKeepaliveIdle = 60 * time.Second
// tcpKeepaliveInterval specifies the interval
// time between sending TCP keepalive packets.
tcpKeepaliveInterval = 30 * time.Second
// tcpConnectTimeout is the default timeout for TCP handshakes.
tcpConnectTimeout = 5 * time.Second
// tcpWaitTimeout implements a TCP half-close timeout.
tcpWaitTimeout = 60 * time.Second
// udpSessionTimeout is the default timeout for UDP sessions.
udpSessionTimeout = 60 * time.Second
// Buffer size for copying data
bufferSize = 32 * 1024
)
// TCPHandler handles TCP connections from netstack
type TCPHandler struct {
stack *stack.Stack
}
// UDPHandler handles UDP connections from netstack
type UDPHandler struct {
stack *stack.Stack
}
// NewTCPHandler creates a new TCP handler
func NewTCPHandler(s *stack.Stack) *TCPHandler {
return &TCPHandler{stack: s}
}
// NewUDPHandler creates a new UDP handler
func NewUDPHandler(s *stack.Stack) *UDPHandler {
return &UDPHandler{stack: s}
}
// InstallTCPHandler installs the TCP forwarder on the stack
func (h *TCPHandler) InstallTCPHandler() error {
tcpForwarder := tcp.NewForwarder(h.stack, defaultWndSize, maxConnAttempts, func(r *tcp.ForwarderRequest) {
var (
wq waiter.Queue
ep tcpip.Endpoint
err tcpip.Error
id = r.ID()
)
// Perform a TCP three-way handshake
ep, err = r.CreateEndpoint(&wq)
if err != nil {
// RST: prevent potential half-open TCP connection leak
r.Complete(true)
return
}
defer r.Complete(false)
// Set socket options
setTCPSocketOptions(h.stack, ep)
// Create TCP connection from netstack endpoint
netstackConn := gonet.NewTCPConn(&wq, ep)
// Handle the connection in a goroutine
go h.handleTCPConn(netstackConn, id)
})
h.stack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
return nil
}
// handleTCPConn handles a TCP connection by proxying it to the actual target
func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.TransportEndpointID) {
defer netstackConn.Close()
// Extract source and target address from the connection ID
srcIP := id.RemoteAddress.String()
srcPort := id.RemotePort
dstIP := id.LocalAddress.String()
dstPort := id.LocalPort
logger.Info("TCP Forwarder: Handling connection %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort)
targetAddr := fmt.Sprintf("%s:%d", dstIP, dstPort)
// Create context with timeout for connection establishment
ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout)
defer cancel()
// Dial the actual target using standard net package
var d net.Dialer
targetConn, err := d.DialContext(ctx, "tcp", targetAddr)
if err != nil {
logger.Info("TCP Forwarder: Failed to connect to %s: %v", targetAddr, err)
// Connection failed, netstack will handle RST
return
}
defer targetConn.Close()
logger.Info("TCP Forwarder: Successfully connected to %s, starting bidirectional copy", targetAddr)
// Bidirectional copy between netstack and target
pipeTCP(netstackConn, targetConn)
}
// pipeTCP copies data bidirectionally between two connections
func pipeTCP(origin, remote net.Conn) {
wg := sync.WaitGroup{}
wg.Add(2)
go unidirectionalStreamTCP(remote, origin, "origin->remote", &wg)
go unidirectionalStreamTCP(origin, remote, "remote->origin", &wg)
wg.Wait()
}
// unidirectionalStreamTCP copies data in one direction
func unidirectionalStreamTCP(dst, src net.Conn, dir string, wg *sync.WaitGroup) {
defer wg.Done()
buf := make([]byte, bufferSize)
_, _ = io.CopyBuffer(dst, src, buf)
// Do the upload/download side TCP half-close
if cr, ok := src.(interface{ CloseRead() error }); ok {
cr.CloseRead()
}
if cw, ok := dst.(interface{ CloseWrite() error }); ok {
cw.CloseWrite()
}
// Set TCP half-close timeout
dst.SetReadDeadline(time.Now().Add(tcpWaitTimeout))
}
// setTCPSocketOptions sets TCP socket options for better performance
func setTCPSocketOptions(s *stack.Stack, ep tcpip.Endpoint) {
// TCP keepalive options
ep.SocketOptions().SetKeepAlive(true)
idle := tcpip.KeepaliveIdleOption(tcpKeepaliveIdle)
ep.SetSockOpt(&idle)
interval := tcpip.KeepaliveIntervalOption(tcpKeepaliveInterval)
ep.SetSockOpt(&interval)
ep.SetSockOptInt(tcpip.KeepaliveCountOption, tcpKeepaliveCount)
// TCP send/recv buffer size
var ss tcpip.TCPSendBufferSizeRangeOption
if err := s.TransportProtocolOption(tcp.ProtocolNumber, &ss); err == nil {
ep.SocketOptions().SetSendBufferSize(int64(ss.Default), false)
}
var rs tcpip.TCPReceiveBufferSizeRangeOption
if err := s.TransportProtocolOption(tcp.ProtocolNumber, &rs); err == nil {
ep.SocketOptions().SetReceiveBufferSize(int64(rs.Default), false)
}
}
// InstallUDPHandler installs the UDP forwarder on the stack
func (h *UDPHandler) InstallUDPHandler() error {
udpForwarder := udp.NewForwarder(h.stack, func(r *udp.ForwarderRequest) {
var (
wq waiter.Queue
id = r.ID()
)
ep, err := r.CreateEndpoint(&wq)
if err != nil {
return
}
// Create UDP connection from netstack endpoint
netstackConn := gonet.NewUDPConn(&wq, ep)
// Handle the connection in a goroutine
go h.handleUDPConn(netstackConn, id)
})
h.stack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
return nil
}
// handleUDPConn handles a UDP connection by proxying it to the actual target
func (h *UDPHandler) handleUDPConn(netstackConn *gonet.UDPConn, id stack.TransportEndpointID) {
defer netstackConn.Close()
// Extract source and target address from the connection ID
srcIP := id.RemoteAddress.String()
srcPort := id.RemotePort
dstIP := id.LocalAddress.String()
dstPort := id.LocalPort
logger.Info("UDP Forwarder: Handling connection %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort)
targetAddr := fmt.Sprintf("%s:%d", dstIP, dstPort)
// Resolve target address
remoteUDPAddr, err := net.ResolveUDPAddr("udp", targetAddr)
if err != nil {
logger.Info("UDP Forwarder: Failed to resolve %s: %v", targetAddr, err)
return
}
// Create UDP connection to target
targetConn, err := net.DialUDP("udp", nil, remoteUDPAddr)
if err != nil {
logger.Info("UDP Forwarder: Failed to dial %s: %v", targetAddr, err)
return
}
defer targetConn.Close()
logger.Info("UDP Forwarder: Successfully connected to %s, starting bidirectional copy", targetAddr)
// Bidirectional copy between netstack and target
pipeUDP(netstackConn, targetConn, remoteUDPAddr, udpSessionTimeout)
}
// pipeUDP copies UDP packets bidirectionally
func pipeUDP(origin, remote net.PacketConn, to net.Addr, timeout time.Duration) {
wg := sync.WaitGroup{}
wg.Add(2)
go unidirectionalPacketStream(remote, origin, to, "origin->remote", &wg, timeout)
go unidirectionalPacketStream(origin, remote, nil, "remote->origin", &wg, timeout)
wg.Wait()
}
// unidirectionalPacketStream copies packets in one direction
func unidirectionalPacketStream(dst, src net.PacketConn, to net.Addr, dir string, wg *sync.WaitGroup, timeout time.Duration) {
defer wg.Done()
_ = copyPacketData(dst, src, to, timeout)
}
// copyPacketData copies UDP packet data with timeout
func copyPacketData(dst, src net.PacketConn, to net.Addr, timeout time.Duration) error {
buf := make([]byte, 65535) // Max UDP packet size
for {
src.SetReadDeadline(time.Now().Add(timeout))
n, _, err := src.ReadFrom(buf)
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return nil // ignore I/O timeout
} else if err == io.EOF {
return nil // ignore EOF
} else if err != nil {
return err
}
if _, err = dst.WriteTo(buf[:n], to); err != nil {
return err
}
dst.SetReadDeadline(time.Now().Add(timeout))
}
}

View File

@@ -22,6 +22,7 @@ import (
"syscall"
"time"
"github.com/fosrl/newt/logger"
"golang.zx2c4.com/wireguard/tun"
"golang.org/x/net/dns/dnsmessage"
@@ -40,42 +41,79 @@ import (
)
type netTun struct {
ep *channel.Endpoint
stack *stack.Stack
events chan tun.Event
notifyHandle *channel.NotificationHandle
incomingPacket chan *buffer.View
mtu int
dnsServers []netip.Addr
hasV4, hasV6 bool
ep *channel.Endpoint
proxyEp *channel.Endpoint // Separate endpoint for promiscuous mode
stack *stack.Stack
events chan tun.Event
notifyHandle *channel.NotificationHandle
proxyNotifyHandle *channel.NotificationHandle // Notify handle for proxy endpoint
incomingPacket chan *buffer.View
mtu int
dnsServers []netip.Addr
hasV4, hasV6 bool
tcpHandler *TCPHandler
udpHandler *UDPHandler
}
type Net netTun
// NetTunOptions contains options for creating a NetTUN device
type NetTunOptions struct {
EnableTCPProxy bool
EnableUDPProxy bool
}
// CreateNetTUN creates a new TUN device with netstack without proxying
func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error) {
opts := stack.Options{
return CreateNetTUNWithOptions(localAddresses, dnsServers, mtu, NetTunOptions{
EnableTCPProxy: true,
EnableUDPProxy: true,
})
}
// CreateNetTUNWithOptions creates a new TUN device with netstack and optional TCP/UDP proxying
func CreateNetTUNWithOptions(localAddresses, dnsServers []netip.Addr, mtu int, options NetTunOptions) (tun.Device, *Net, error) {
stackOpts := stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4},
HandleLocal: true,
}
dev := &netTun{
ep: channel.New(1024, uint32(mtu), ""),
stack: stack.New(opts),
proxyEp: channel.New(1024, uint32(mtu), ""),
stack: stack.New(stackOpts),
events: make(chan tun.Event, 10),
incomingPacket: make(chan *buffer.View),
dnsServers: dnsServers,
mtu: mtu,
}
sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default
if options.EnableTCPProxy {
dev.tcpHandler = NewTCPHandler(dev.stack)
if err := dev.tcpHandler.InstallTCPHandler(); err != nil {
return nil, nil, fmt.Errorf("failed to install TCP handler: %v", err)
}
}
if options.EnableUDPProxy {
dev.udpHandler = NewUDPHandler(dev.stack)
if err := dev.udpHandler.InstallUDPHandler(); err != nil {
return nil, nil, fmt.Errorf("failed to install UDP handler: %v", err)
}
}
sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is enabled by default
tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt)
if tcpipErr != nil {
return nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr)
}
// Create NIC 1 (main interface, no promiscuous mode)
dev.notifyHandle = dev.ep.AddNotify(dev)
tcpipErr = dev.stack.CreateNIC(1, dev.ep)
if tcpipErr != nil {
return nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr)
}
for _, ip := range localAddresses {
var protoNumber tcpip.NetworkProtocolNumber
if ip.Is4() {
@@ -98,10 +136,92 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device,
}
}
if dev.hasV4 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1})
// dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1})
// add 100.90.129.0/24
proxySubnet := netip.MustParsePrefix("100.90.129.0/24")
proxyTcpipSubnet, err := tcpip.NewSubnet(
tcpip.AddrFromSlice(proxySubnet.Addr().AsSlice()),
tcpip.MaskFromBytes(proxySubnet.Addr().AsSlice()),
)
if err != nil {
return nil, nil, fmt.Errorf("failed to create proxy subnet: %v", err)
}
dev.stack.AddRoute(tcpip.Route{Destination: proxyTcpipSubnet, NIC: 1})
}
if dev.hasV6 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1})
// if dev.hasV6 {
// // dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1})
// }
// Add specific route for proxy network (10.20.20.0/24) to NIC 2
if options.EnableTCPProxy || options.EnableUDPProxy {
dev.proxyNotifyHandle = dev.proxyEp.AddNotify(dev)
tcpipErr = dev.stack.CreateNIC(2, dev.proxyEp)
if tcpipErr != nil {
return nil, nil, fmt.Errorf("CreateNIC 2 (proxy): %v", tcpipErr)
}
// Enable promiscuous mode ONLY on NIC 2
if tcpipErr := dev.stack.SetPromiscuousMode(2, true); tcpipErr != nil {
return nil, nil, fmt.Errorf("SetPromiscuousMode on NIC 2: %v", tcpipErr)
}
// Enable spoofing ONLY on NIC 2
if tcpipErr := dev.stack.SetSpoofing(2, true); tcpipErr != nil {
return nil, nil, fmt.Errorf("SetSpoofing on NIC 2: %v", tcpipErr)
}
// Add the proxy network address (10.20.20.1/24) to NIC 2
// This allows the stack to accept connections to any IP in this range when in promiscuous mode
// Similar to how tun2socks adds 10.0.0.1/8 for multicast support
// The PEB: CanBePrimaryEndpoint is CRITICAL - it allows the stack to build routes
// and accept connections to any IP in this range when in promiscuous+spoofing mode
proxyAddr := netip.MustParseAddr("10.20.20.1")
protoAddr := tcpip.ProtocolAddress{
Protocol: ipv4.ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: tcpip.AddrFromSlice(proxyAddr.AsSlice()),
PrefixLen: 24, // /24 network
},
}
tcpipErr = dev.stack.AddProtocolAddress(2, protoAddr, stack.AddressProperties{
PEB: stack.CanBePrimaryEndpoint, // Allow this to be used as primary endpoint
})
if tcpipErr != nil {
return nil, nil, fmt.Errorf("AddProtocolAddress for proxy NIC: %v", tcpipErr)
}
proxySubnet := netip.MustParsePrefix("10.20.20.0/24")
proxyTcpipSubnet, err := tcpip.NewSubnet(
tcpip.AddrFromSlice(proxySubnet.Addr().AsSlice()),
tcpip.MaskFromBytes(net.CIDRMask(24, 32)),
)
if err != nil {
return nil, nil, fmt.Errorf("failed to create proxy subnet: %v", err)
}
dev.stack.AddRoute(tcpip.Route{
Destination: proxyTcpipSubnet,
NIC: 2,
})
}
// print the stack routes table and interfaces for debugging
logger.Info("Stack configuration:")
// Print NICs
nics := dev.stack.NICInfo()
for nicID, nicInfo := range nics {
logger.Info("NIC %d: %s (MTU: %d)", nicID, nicInfo.Name, nicInfo.MTU)
for _, addr := range nicInfo.ProtocolAddresses {
logger.Info(" Address: %s", addr.AddressWithPrefix)
}
}
// Print routing table
routes := dev.stack.GetRouteTable()
logger.Info("Routing table (%d routes):", len(routes))
for i, route := range routes {
logger.Info(" Route %d: %s -> NIC %d", i, route.Destination, route.NIC)
}
dev.events <- tun.EventUp
@@ -142,11 +262,44 @@ func (tun *netTun) Write(buf [][]byte, offset int) (int, error) {
}
pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)})
// Determine which NIC to inject the packet into based on destination IP
targetEp := tun.ep // Default to NIC 1
switch packet[0] >> 4 {
case 4:
tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb)
// Parse IPv4 header to check destination
if len(packet) >= header.IPv4MinimumSize {
ipv4Header := header.IPv4(packet)
dstIP := ipv4Header.DestinationAddress()
// Check if destination is in the proxy range (10.20.20.0/24)
// If so, inject into proxyEp (NIC 2) which has promiscuous mode
if tun.proxyEp != nil {
dstBytes := dstIP.As4()
// Check for 10.20.20.x
if dstBytes[0] == 10 && dstBytes[1] == 20 && dstBytes[2] == 20 {
targetEp = tun.proxyEp
// Log what protocol this is
proto := "unknown"
if len(packet) > header.IPv4MinimumSize {
switch ipv4Header.Protocol() {
case uint8(header.TCPProtocolNumber):
proto = "TCP"
case uint8(header.UDPProtocolNumber):
proto = "UDP"
case uint8(header.ICMPv4ProtocolNumber):
proto = "ICMP"
}
}
logger.Info("Routing %s packet to NIC 2 (proxy): dst=%s", proto, dstIP)
}
}
}
targetEp.InjectInbound(header.IPv4ProtocolNumber, pkb)
case 6:
tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb)
// For IPv6, always use NIC 1 for now
targetEp.InjectInbound(header.IPv6ProtocolNumber, pkb)
default:
return 0, syscall.EAFNOSUPPORT
}
@@ -154,20 +307,117 @@ func (tun *netTun) Write(buf [][]byte, offset int) (int, error) {
return len(buf), nil
}
// logPacketDetails parses and logs packet information
func logPacketDetails(pkt *stack.PacketBuffer, nicID int) {
netProto := pkt.NetworkProtocolNumber
var srcIP, dstIP string
var protocol string
var srcPort, dstPort uint16
// Parse network layer
switch netProto {
case header.IPv4ProtocolNumber:
if pkt.NetworkHeader().View().Size() >= header.IPv4MinimumSize {
ipv4 := header.IPv4(pkt.NetworkHeader().Slice())
srcIP = ipv4.SourceAddress().String()
dstIP = ipv4.DestinationAddress().String()
// Parse transport layer
switch ipv4.Protocol() {
case uint8(header.TCPProtocolNumber):
protocol = "TCP"
if pkt.TransportHeader().View().Size() >= header.TCPMinimumSize {
tcp := header.TCP(pkt.TransportHeader().Slice())
srcPort = tcp.SourcePort()
dstPort = tcp.DestinationPort()
}
case uint8(header.UDPProtocolNumber):
protocol = "UDP"
if pkt.TransportHeader().View().Size() >= header.UDPMinimumSize {
udp := header.UDP(pkt.TransportHeader().Slice())
srcPort = udp.SourcePort()
dstPort = udp.DestinationPort()
}
case uint8(header.ICMPv4ProtocolNumber):
protocol = "ICMPv4"
default:
protocol = fmt.Sprintf("Proto-%d", ipv4.Protocol())
}
}
case header.IPv6ProtocolNumber:
if pkt.NetworkHeader().View().Size() >= header.IPv6MinimumSize {
ipv6 := header.IPv6(pkt.NetworkHeader().Slice())
srcIP = ipv6.SourceAddress().String()
dstIP = ipv6.DestinationAddress().String()
// Parse transport layer
switch ipv6.TransportProtocol() {
case header.TCPProtocolNumber:
protocol = "TCP"
if pkt.TransportHeader().View().Size() >= header.TCPMinimumSize {
tcp := header.TCP(pkt.TransportHeader().Slice())
srcPort = tcp.SourcePort()
dstPort = tcp.DestinationPort()
}
case header.UDPProtocolNumber:
protocol = "UDP"
if pkt.TransportHeader().View().Size() >= header.UDPMinimumSize {
udp := header.UDP(pkt.TransportHeader().Slice())
srcPort = udp.SourcePort()
dstPort = udp.DestinationPort()
}
case header.ICMPv6ProtocolNumber:
protocol = "ICMPv6"
default:
protocol = fmt.Sprintf("Proto-%d", ipv6.TransportProtocol())
}
}
default:
protocol = fmt.Sprintf("Unknown-NetProto-%d", netProto)
}
packetSize := pkt.Size()
if srcPort > 0 && dstPort > 0 {
logger.Info("NIC %d packet: %s %s:%d -> %s:%d (size: %d bytes)",
nicID, protocol, srcIP, srcPort, dstIP, dstPort, packetSize)
} else {
logger.Info("NIC %d packet: %s %s -> %s (size: %d bytes)",
nicID, protocol, srcIP, dstIP, packetSize)
}
}
func (tun *netTun) WriteNotify() {
// Handle notifications from main endpoint (NIC 1)
pkt := tun.ep.Read()
if pkt == nil {
if pkt != nil {
view := pkt.ToView()
pkt.DecRef()
tun.incomingPacket <- view
return
}
view := pkt.ToView()
pkt.DecRef()
tun.incomingPacket <- view
// Handle notifications from proxy endpoint (NIC 2) if it exists
if tun.proxyEp != nil {
pkt = tun.proxyEp.Read()
if pkt != nil {
view := pkt.ToView()
pkt.DecRef()
tun.incomingPacket <- view
}
}
}
func (tun *netTun) Close() error {
tun.stack.RemoveNIC(1)
// Clean up proxy NIC if it exists
if tun.proxyEp != nil {
tun.stack.RemoveNIC(2)
tun.proxyEp.RemoveNotify(tun.proxyNotifyHandle)
tun.proxyEp.Close()
}
tun.stack.Close()
tun.ep.RemoveNotify(tun.notifyHandle)
tun.ep.Close()