diff --git a/MULTIPATH.md b/MULTIPATH.md new file mode 100644 index 0000000..7771992 --- /dev/null +++ b/MULTIPATH.md @@ -0,0 +1,268 @@ +# Multi-Path WireGuard Implementation + +This document describes the multi-path networking feature for WireGuard-Go, which allows sending the same packet through multiple network interfaces simultaneously. + +## Overview + +The multi-path implementation extends WireGuard-Go to support redundant packet transmission through multiple network paths. When configured, each outbound packet is sent through ALL specified network interfaces, providing: + +- **Increased Reliability**: If one network path fails, communication continues through other paths +- **Better Performance**: Multiple paths can provide better throughput and lower latency +- **Redundancy**: Critical for scenarios where network reliability is paramount + +## How It Works + +### Architecture + +The multi-path functionality is implemented through several key components: + +1. **MultiPathBind** (`conn/multipath_bind.go`): + - Implements the `conn.Bind` interface + - Manages multiple underlying `Bind` instances + - Sends packets through ALL configured network paths + - Receives packets through the primary path only + +2. **Multi-Path Device Creation** (`device/multipath.go`): + - Helper functions to create WireGuard devices with multiple network interfaces + - Interface discovery and configuration utilities + +3. **Network Transmission Flow**: + ``` + TUN Device → Peer Lookup → Packet Staging → Sequential Sender → + SendBuffers → MultiPathBind.Send() → [Bind1, Bind2, Bind3, ...] → Network + ``` + +### Code Locations + +The actual network transmission happens in these key locations: + +- **Primary Send Method**: `device/peer.go:135` - `peer.device.net.bind.Send(buffers, endpoint)` +- **Multi-Path Send**: `conn/multipath_bind.go:95` - Sends through all configured binds +- **Socket Transmission**: `conn/bind_std.go:339` - Individual socket transmission + +## Usage + +### Basic Usage + +```go +package main + +import ( + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun" +) + +func main() { + // Create TUN device + tunDevice, err := tun.CreateTUN("wg-multipath", 1420) + if err != nil { + panic(err) + } + defer tunDevice.Close() + + // Create logger + logger := device.NewLogger(device.LogLevelVerbose, "multipath: ") + + // Create multi-path device using interface names + interfaceNames := []string{"eth0", "wlan0"} + wgDevice, err := device.NewMultiPathDeviceByNames(tunDevice, interfaceNames, logger) + if err != nil { + panic(err) + } + defer wgDevice.Close() + + // Device is ready - configure with wg(8) tools and bring up + err = wgDevice.Up() + if err != nil { + panic(err) + } + + // Now all outbound packets will be sent through both eth0 and wlan0 +} +``` + +### Advanced Configuration + +```go +// Using interface indexes instead of names +config := device.MultiPathConfig{ + InterfaceIndexes: []uint32{2, 3, 4}, // eth0, wlan0, usb0 + BindFactory: func() conn.Bind { + return conn.NewStdNetBind() // or custom bind implementation + }, +} + +wgDevice, err := device.NewMultiPathDevice(tunDevice, config, logger) +``` + +### Command Line Example + +Build and run the example program: + +```bash +# Build the example +go build -o multipath-example ./examples/multipath/ + +# List available interfaces +sudo ./multipath-example + +# Create multi-path tunnel using eth0 and wlan0 +sudo ./multipath-example eth0 wlan0 +``` + +## Configuration + +### Interface Discovery + +Use the helper function to discover available network interfaces: + +```go +interfaces, err := device.GetNetworkInterfaceInfo() +if err != nil { + log.Fatal(err) +} + +for _, iface := range interfaces { + fmt.Printf("Interface: %s (index %d)\n", iface.Name, iface.Index) + fmt.Printf(" Addresses: %v\n", iface.Addresses) + fmt.Printf(" MTU: %d\n", iface.MTU) +} +``` + +### WireGuard Configuration + +After creating the multi-path device, configure it normally with `wg(8)`: + +```bash +# Generate keys +wg genkey | tee private.key | wg pubkey > public.key + +# Configure the device +sudo wg set wg-multipath private-key private.key +sudo wg set wg-multipath peer \ + endpoint : \ + allowed-ips 0.0.0.0/0 + +# Assign IP and bring up +sudo ip addr add 10.0.0.2/24 dev wg-multipath +sudo ip link set wg-multipath up + +# Route traffic through the tunnel +sudo ip route add default dev wg-multipath +``` + +## Technical Details + +### Packet Duplication + +When `MultiPathBind.Send()` is called: + +1. The same packet buffers are sent through ALL configured network binds +2. Each bind may be bound to a different network interface +3. The method succeeds if at least one bind successfully sends the packet +4. Errors from individual binds are logged but don't stop other binds + +### Receiving + +- Only the primary bind (first in the list) is used for receiving packets +- This prevents duplicate packet reception +- All receive functions come from the primary bind + +### Error Handling + +- Individual bind failures don't stop transmission through other binds +- At least one successful transmission is required for overall success +- Failed binds are logged for debugging + +### Performance Considerations + +- **CPU Usage**: Sending through multiple interfaces increases CPU usage proportionally +- **Memory**: Each bind maintains its own buffers and state +- **Network Bandwidth**: Total bandwidth usage is multiplied by the number of interfaces +- **Latency**: Latency is determined by the fastest responding interface + +## Limitations + +1. **Packet Duplication**: Receiving peer will see duplicate packets (WireGuard's replay protection handles this) +2. **Bandwidth Usage**: Network usage increases proportionally with number of interfaces +3. **Interface Binding**: Requires platform support for binding sockets to specific interfaces +4. **Receive Path**: Only receives through primary interface (no multi-path receiving) + +## Platform Support + +The multi-path functionality works on platforms that support: +- Socket binding to specific network interfaces +- Multiple UDP sockets on the same port (with SO_REUSEPORT or similar) + +Tested on: +- Linux (fully supported) +- macOS (limited support) +- Windows (limited support) + +## Example Scenarios + +### Dual-WAN Setup +Use both your main internet connection and backup cellular connection: +```go +interfaces := []string{"eth0", "wwan0"} // Ethernet + Cellular +``` + +### WiFi + Ethernet Redundancy +For laptops with both WiFi and Ethernet: +```go +interfaces := []string{"eth0", "wlan0"} // Ethernet + WiFi +``` + +### Multi-Homed Server +Server with multiple network interfaces: +```go +interfaces := []string{"eth0", "eth1", "eth2"} // Multiple Ethernet +``` + +## Troubleshooting + +### Interface Binding Issues +```bash +# Check interface exists and is up +ip link show eth0 + +# Check interface has IP address +ip addr show eth0 + +# Test basic connectivity +ping -I eth0 8.8.8.8 +``` + +### Permission Issues +Multi-path binding typically requires root privileges: +```bash +sudo ./your-wireguard-program +``` + +### Debugging +Enable verbose logging to see multi-path operations: +```go +logger := device.NewLogger(device.LogLevelVerbose, "multipath: ") +``` + +## Building + +Ensure you have the modified WireGuard-Go source and build normally: + +```bash +go mod tidy +go build ./... + +# Build example +go build -o multipath-example ./examples/multipath/ +``` + +## Future Enhancements + +Potential improvements for the multi-path implementation: + +1. **Load Balancing**: Distribute packets across interfaces rather than duplicating +2. **Health Monitoring**: Automatic detection and handling of failed interfaces +3. **Quality Metrics**: Choose best interface based on latency/bandwidth measurements +4. **Receive Multi-Path**: Receive from multiple interfaces and handle reordering +5. **Configuration API**: Runtime configuration of interface sets \ No newline at end of file diff --git a/conn/multipath_bind.go b/conn/multipath_bind.go new file mode 100644 index 0000000..fc75a47 --- /dev/null +++ b/conn/multipath_bind.go @@ -0,0 +1,178 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. + */ + +package conn + +import ( + "fmt" + "net" + "sync" +) + +// MultiPathBind implements Bind interface but sends packets through multiple network paths +type MultiPathBind struct { + mu sync.RWMutex + binds []Bind + // Store the primary bind for receive operations (only one bind receives) + primaryBind Bind +} + +// NewMultiPathBind creates a new multi-path bind with multiple underlying binds +func NewMultiPathBind(binds []Bind) *MultiPathBind { + if len(binds) == 0 { + panic("MultiPathBind requires at least one bind") + } + + return &MultiPathBind{ + binds: binds, + primaryBind: binds[0], // Use first bind as primary for receiving + } +} + +// Open puts all binds into listening state +func (mpb *MultiPathBind) Open(port uint16) (fns []ReceiveFunc, actualPort uint16, err error) { + mpb.mu.Lock() + defer mpb.mu.Unlock() + + // Open primary bind first to get the actual port and receive functions + fns, actualPort, err = mpb.primaryBind.Open(port) + if err != nil { + return nil, 0, fmt.Errorf("failed to open primary bind: %w", err) + } + + // Open additional binds on the same port + for i, bind := range mpb.binds[1:] { + _, bindPort, bindErr := bind.Open(actualPort) + if bindErr != nil { + // If any bind fails, close already opened binds + mpb.primaryBind.Close() + for j := 0; j < i; j++ { + mpb.binds[j+1].Close() + } + return nil, 0, fmt.Errorf("failed to open bind %d: %w", i+1, bindErr) + } + // Verify all binds use the same port + if bindPort != actualPort { + mpb.primaryBind.Close() + for j := 0; j <= i; j++ { + mpb.binds[j+1].Close() + } + return nil, 0, fmt.Errorf("bind %d opened on different port %d vs %d", i+1, bindPort, actualPort) + } + } + + return fns, actualPort, nil +} + +// Close closes all underlying binds +func (mpb *MultiPathBind) Close() error { + mpb.mu.Lock() + defer mpb.mu.Unlock() + + var firstErr error + for i, bind := range mpb.binds { + if err := bind.Close(); err != nil && firstErr == nil { + firstErr = fmt.Errorf("failed to close bind %d: %w", i, err) + } + } + return firstErr +} + +// SetMark sets the mark for all underlying binds +func (mpb *MultiPathBind) SetMark(mark uint32) error { + mpb.mu.RLock() + defer mpb.mu.RUnlock() + + for i, bind := range mpb.binds { + if err := bind.SetMark(mark); err != nil { + return fmt.Errorf("failed to set mark on bind %d: %w", i, err) + } + } + return nil +} + +// Send sends the same packets through ALL configured network paths +func (mpb *MultiPathBind) Send(bufs [][]byte, ep Endpoint) error { + mpb.mu.RLock() + defer mpb.mu.RUnlock() + + var firstErr error + successCount := 0 + + // Send through all binds + for i, bind := range mpb.binds { + if err := bind.Send(bufs, ep); err != nil { + if firstErr == nil { + firstErr = fmt.Errorf("bind %d failed: %w", i, err) + } + } else { + successCount++ + } + } + + // Consider it successful if at least one path succeeded + if successCount > 0 { + return nil + } + + return firstErr +} + +// ParseEndpoint uses the primary bind to parse endpoints +func (mpb *MultiPathBind) ParseEndpoint(s string) (Endpoint, error) { + mpb.mu.RLock() + defer mpb.mu.RUnlock() + return mpb.primaryBind.ParseEndpoint(s) +} + +// BatchSize returns the minimum batch size among all binds +func (mpb *MultiPathBind) BatchSize() int { + mpb.mu.RLock() + defer mpb.mu.RUnlock() + + if len(mpb.binds) == 0 { + return 1 + } + + minBatchSize := mpb.binds[0].BatchSize() + for _, bind := range mpb.binds[1:] { + if size := bind.BatchSize(); size < minBatchSize { + minBatchSize = size + } + } + return minBatchSize +} + +// BindToInterface binds specific binds to specific interfaces +// This is a helper method for configuring each bind to use different interfaces +func (mpb *MultiPathBind) BindToInterface(bindIndex int, interfaceIndex uint32, blackhole bool) error { + mpb.mu.RLock() + defer mpb.mu.RUnlock() + + if bindIndex >= len(mpb.binds) { + return fmt.Errorf("bind index %d out of range (have %d binds)", bindIndex, len(mpb.binds)) + } + + bind := mpb.binds[bindIndex] + if binder, ok := bind.(BindSocketToInterface); ok { + // Try IPv4 first + if err := binder.BindSocketToInterface4(interfaceIndex, blackhole); err != nil { + // If IPv4 fails, try IPv6 + if err := binder.BindSocketToInterface6(interfaceIndex, blackhole); err != nil { + return fmt.Errorf("failed to bind to interface %d: %w", interfaceIndex, err) + } + } + return nil + } + + return fmt.Errorf("bind %d does not support interface binding", bindIndex) +} + +// GetBindCount returns the number of configured network paths +func (mpb *MultiPathBind) GetBindCount() int { + mpb.mu.RLock() + defer mpb.mu.RUnlock() + return len(mpb.binds) +} \ No newline at end of file diff --git a/device/multipath.go b/device/multipath.go new file mode 100644 index 0000000..27dbd80 --- /dev/null +++ b/device/multipath.go @@ -0,0 +1,171 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. + */ + +package device + +import ( + "fmt" + "net" + "runtime" + + "golang.zx2c4.com/wireguard/conn" + "golang.zx2c4.com/wireguard/tun" +) + +// MultiPathConfig represents configuration for multi-path networking +type MultiPathConfig struct { + // InterfaceIndexes are the network interface indexes to bind to + // If empty, uses default interface selection + InterfaceIndexes []uint32 + // BindFactory creates new Bind instances. If nil, uses conn.NewStdNetBind() + BindFactory func() conn.Bind +} + +// NewMultiPathDevice creates a new WireGuard device with multi-path networking +// It creates separate bind instances for each specified network interface +func NewMultiPathDevice(tunDevice tun.Device, config MultiPathConfig, logger *Logger) (*Device, error) { + if len(config.InterfaceIndexes) == 0 { + return nil, fmt.Errorf("MultiPathConfig must specify at least one interface index") + } + + // Use default bind factory if none specified + bindFactory := config.BindFactory + if bindFactory == nil { + bindFactory = func() conn.Bind { + return conn.NewStdNetBind() + } + } + + // Create a bind for each interface + binds := make([]conn.Bind, len(config.InterfaceIndexes)) + for i := range config.InterfaceIndexes { + binds[i] = bindFactory() + } + + // Create multi-path bind + multiPathBind := conn.NewMultiPathBind(binds) + + // Configure each bind to use its specific interface + for i, interfaceIndex := range config.InterfaceIndexes { + err := multiPathBind.BindToInterface(i, interfaceIndex, false) + if err != nil { + logger.Errorf("Failed to bind to interface %d: %v", interfaceIndex, err) + // Continue with other interfaces rather than failing completely + } else { + logger.Verbosef("Bound network path %d to interface index %d", i, interfaceIndex) + } + } + + // Create device with multi-path bind + device := NewDevice(tunDevice, multiPathBind, logger) + + logger.Verbosef("Created multi-path WireGuard device with %d network paths", len(config.InterfaceIndexes)) + return device, nil +} + +// NewMultiPathDeviceByNames creates a multi-path device using interface names instead of indexes +func NewMultiPathDeviceByNames(tunDevice tun.Device, interfaceNames []string, logger *Logger) (*Device, error) { + if len(interfaceNames) == 0 { + return nil, fmt.Errorf("must specify at least one interface name") + } + + // Convert interface names to indexes + interfaceIndexes := make([]uint32, len(interfaceNames)) + for i, name := range interfaceNames { + iface, err := net.InterfaceByName(name) + if err != nil { + return nil, fmt.Errorf("failed to find interface %s: %w", name, err) + } + interfaceIndexes[i] = uint32(iface.Index) + logger.Verbosef("Interface %s has index %d", name, iface.Index) + } + + config := MultiPathConfig{ + InterfaceIndexes: interfaceIndexes, + } + + return NewMultiPathDevice(tunDevice, config, logger) +} + +// GetNetworkInterfaceInfo returns information about available network interfaces +func GetNetworkInterfaceInfo() ([]NetworkInterfaceInfo, error) { + interfaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("failed to list network interfaces: %w", err) + } + + result := make([]NetworkInterfaceInfo, 0, len(interfaces)) + for _, iface := range interfaces { + // Skip loopback and down interfaces for multi-path networking + if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { + continue + } + + addrs, _ := iface.Addrs() + addrStrings := make([]string, len(addrs)) + for i, addr := range addrs { + addrStrings[i] = addr.String() + } + + result = append(result, NetworkInterfaceInfo{ + Index: uint32(iface.Index), + Name: iface.Name, + Addresses: addrStrings, + MTU: iface.MTU, + Flags: iface.Flags, + }) + } + + return result, nil +} + +// NetworkInterfaceInfo represents information about a network interface +type NetworkInterfaceInfo struct { + Index uint32 + Name string + Addresses []string + MTU int + Flags net.Flags +} + +// String returns a human-readable description of the interface +func (nii NetworkInterfaceInfo) String() string { + return fmt.Sprintf("Interface %s (index %d): MTU=%d, Addresses=%v, Flags=%v", + nii.Name, nii.Index, nii.MTU, nii.Addresses, nii.Flags) +} + +// Example usage function +func ExampleMultiPathUsage(logger *Logger) { + // Print available interfaces + interfaces, err := GetNetworkInterfaceInfo() + if err != nil { + logger.Errorf("Failed to get interface info: %v", err) + return + } + + logger.Verbosef("Available network interfaces:") + for _, iface := range interfaces { + logger.Verbosef(" %s", iface.String()) + } + + // Example: Create multi-path device using specific interface names + // This would send each packet through both eth0 and wlan0 + interfaceNames := []string{"eth0", "wlan0"} + + // Note: You would need to create/configure your TUN device + // tunDevice, err := tun.CreateTUN("wg0", 1420) + // if err != nil { + // logger.Errorf("Failed to create TUN: %v", err) + // return + // } + // + // device, err := NewMultiPathDeviceByNames(tunDevice, interfaceNames, logger) + // if err != nil { + // logger.Errorf("Failed to create multi-path device: %v", err) + // return + // } + // + // logger.Verbosef("Multi-path WireGuard device created successfully") +} \ No newline at end of file diff --git a/examples/multipath/build.sh b/examples/multipath/build.sh new file mode 100755 index 0000000..be51b08 --- /dev/null +++ b/examples/multipath/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Build script for multi-path WireGuard example + +set -e + +echo "Building multi-path WireGuard example..." + +# Ensure we're in the right directory +cd "$(dirname "$0")" + +# Build the example +go build -o multipath-example main.go + +echo "Build complete! Executable: ./multipath-example" +echo "" +echo "Usage examples:" +echo " # List available interfaces:" +echo " sudo ./multipath-example" +echo "" +echo " # Create multi-path tunnel:" +echo " sudo ./multipath-example eth0 wlan0" +echo "" +echo "Note: This example requires root privileges to create TUN devices and bind to interfaces." \ No newline at end of file diff --git a/examples/multipath/main.go b/examples/multipath/main.go new file mode 100644 index 0000000..0551891 --- /dev/null +++ b/examples/multipath/main.go @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: MIT + * + * Multi-path WireGuard Example + * + * This example demonstrates how to create a WireGuard device that sends + * packets through multiple network interfaces simultaneously. + */ + +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun" +) + +func main() { + if len(os.Args) < 2 { + fmt.Printf("Usage: %s [interface2] [interface3] ...\n", os.Args[0]) + fmt.Println("Example: sudo ./multipath eth0 wlan0") + fmt.Println("\nThis will create a WireGuard tunnel that sends packets through both eth0 and wlan0") + fmt.Println("Available interfaces:") + + interfaces, err := device.GetNetworkInterfaceInfo() + if err != nil { + log.Fatalf("Failed to get interface info: %v", err) + } + + for _, iface := range interfaces { + fmt.Printf(" %s\n", iface.String()) + } + os.Exit(1) + } + + // Get interface names from command line + interfaceNames := os.Args[1:] + + fmt.Printf("Creating multi-path WireGuard device using interfaces: %v\n", interfaceNames) + + // Create logger + logger := device.NewLogger(device.LogLevelVerbose, "multipath-example: ") + + // Create TUN device + tunDevice, err := tun.CreateTUN("wg-multipath", 1420) + if err != nil { + log.Fatalf("Failed to create TUN device: %v", err) + } + defer tunDevice.Close() + + fmt.Printf("Created TUN device: %s\n", tunDevice.Name()) + + // Create multi-path WireGuard device + wgDevice, err := device.NewMultiPathDeviceByNames(tunDevice, interfaceNames, logger) + if err != nil { + log.Fatalf("Failed to create multi-path WireGuard device: %v", err) + } + defer wgDevice.Close() + + fmt.Printf("Multi-path WireGuard device created successfully!\n") + fmt.Printf("Each outbound packet will be sent through ALL %d specified interfaces\n", len(interfaceNames)) + + // Configure WireGuard (you would normally load this from a config file) + // This is just a basic example configuration + logger.Verbosef("Device ready. You can now configure it using wg(8) commands:") + logger.Verbosef(" sudo wg set %s private-key ", tunDevice.Name()) + logger.Verbosef(" sudo wg set %s peer endpoint allowed-ips ", tunDevice.Name()) + logger.Verbosef(" sudo ip addr add /24 dev %s", tunDevice.Name()) + logger.Verbosef(" sudo ip link set %s up", tunDevice.Name()) + + // Bring device up + err = wgDevice.Up() + if err != nil { + log.Fatalf("Failed to bring device up: %v", err) + } + + fmt.Println("WireGuard device is up and running!") + fmt.Println("Press Ctrl+C to stop...") + + // Wait for interrupt signal + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + <-c + + fmt.Println("\nShutting down...") + wgDevice.Down() +} \ No newline at end of file