This commit is contained in:
dingfeng.wong
2025-07-25 18:01:53 +08:00
parent 7a67b9687c
commit 9f0133a5c9
5 changed files with 732 additions and 0 deletions
+268
View File
@@ -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 <PEER_PUBLIC_KEY> \
endpoint <PEER_IP>:<PORT> \
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
+178
View File
@@ -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)
}
+171
View File
@@ -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")
}
+24
View File
@@ -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."
+91
View File
@@ -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 <interface1> [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 <private-key-file>", tunDevice.Name())
logger.Verbosef(" sudo wg set %s peer <peer-public-key> endpoint <peer-endpoint> allowed-ips <allowed-ips>", tunDevice.Name())
logger.Verbosef(" sudo ip addr add <your-vpn-ip>/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()
}