Compare commits

...

10 Commits

Author SHA1 Message Date
dingfeng.wong 9f0133a5c9 add 2025-07-25 18:01:53 +08:00
dingfeng.wong 7a67b9687c remove encryption 2025-07-25 17:56:07 +08:00
Jason A. Donenfeld f333402bd9 version: bump snapshot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-22 01:45:02 +02:00
Jason A. Donenfeld c92064f1ce conn: don't enable GRO on Linux < 5.12
Kernels below 5.12 are missing this:

    commit 98184612aca0a9ee42b8eb0262a49900ee9eef0d
    Author: Norman Maurer <norman_maurer@apple.com>
    Date:   Thu Apr 1 08:59:17 2021

        net: udp: Add support for getsockopt(..., ..., UDP_GRO, ..., ...);

        Support for UDP_GRO was added in the past but the implementation for
        getsockopt was missed which did lead to an error when we tried to
        retrieve the setting for UDP_GRO. This patch adds the missing switch
        case for UDP_GRO

        Fixes: e20cf8d3f1f7 ("udp: implement GRO for plain UDP sockets.")
        Signed-off-by: Norman Maurer <norman_maurer@apple.com>
        Reviewed-by: David Ahern <dsahern@kernel.org>
        Signed-off-by: David S. Miller <davem@davemloft.net>

That means we can't set the option and then read it back later. Given
how buggy UDP_GRO is in general on odd kernels, just disable it on older
kernels all together.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-22 01:43:39 +02:00
Alexander Yastrebov 264889f0bb device: optimize message encoding
Optimize message encoding by eliminating binary.Write (which internally
uses reflection) in favour of hand-rolled encoding.

This is companion to 9e7529c3d2.

Synthetic benchmark:

    var packetSink []byte
    func BenchmarkMessageInitiationMarshal(b *testing.B) {
        var msg MessageInitiation
        b.Run("binary.Write", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                var buf [MessageInitiationSize]byte
                writer := bytes.NewBuffer(buf[:0])
                _ = binary.Write(writer, binary.LittleEndian, msg)
                packetSink = writer.Bytes()
            }
        })
        b.Run("binary.Encode", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                packet := make([]byte, MessageInitiationSize)
                _, _ = binary.Encode(packet, binary.LittleEndian, msg)
                packetSink = packet
            }
        })
        b.Run("marshal", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                packet := make([]byte, MessageInitiationSize)
                _ = msg.marshal(packet)
                packetSink = packet
            }
        })
    }

Results:
                                             │      -      │
                                             │   sec/op    │
    MessageInitiationMarshal/binary.Write-8    1.337µ ± 0%
    MessageInitiationMarshal/binary.Encode-8   1.242µ ± 0%
    MessageInitiationMarshal/marshal-8         53.05n ± 1%

                                             │     -      │
                                             │    B/op    │
    MessageInitiationMarshal/binary.Write-8    368.0 ± 0%
    MessageInitiationMarshal/binary.Encode-8   160.0 ± 0%
    MessageInitiationMarshal/marshal-8         160.0 ± 0%

                                             │     -      │
                                             │ allocs/op  │
    MessageInitiationMarshal/binary.Write-8    3.000 ± 0%
    MessageInitiationMarshal/binary.Encode-8   1.000 ± 0%
    MessageInitiationMarshal/marshal-8         1.000 ± 0%

Signed-off-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-21 00:09:36 +02:00
Jason A. Donenfeld 256bcbd70d device: add support for removing allowedips individually
This pairs with the recent change in wireguard-tools.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-20 23:03:06 +02:00
Jason A. Donenfeld 1571e0fbae version: bump snapshot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-15 16:54:03 +02:00
Jason A. Donenfeld 842888ac5c device: make unmarshall length checks exact
This is already enforced in receive.go, but if these unmarshallers are
to have error return values anyway, make them as explicit as possible.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-15 16:48:14 +02:00
Alexander Yastrebov 9e7529c3d2 device: reduce RoutineHandshake allocations
Reduce allocations by eliminating byte reader, hand-rolled decoding and
reusing message structs.

Synthetic benchmark:

    var msgSink MessageInitiation
    func BenchmarkMessageInitiationUnmarshal(b *testing.B) {
        packet := make([]byte, MessageInitiationSize)
        reader := bytes.NewReader(packet)
        err := binary.Read(reader, binary.LittleEndian, &msgSink)
        if err != nil {
            b.Fatal(err)
        }
        b.Run("binary.Read", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                reader := bytes.NewReader(packet)
                _ = binary.Read(reader, binary.LittleEndian, &msgSink)
            }
        })
        b.Run("unmarshal", func(b *testing.B) {
            b.ReportAllocs()
            for range b.N {
                _ = msgSink.unmarshal(packet)
            }
        })
    }

Results:
                                         │      -      │
                                         │   sec/op    │
MessageInitiationUnmarshal/binary.Read-8   1.508µ ± 2%
MessageInitiationUnmarshal/unmarshal-8     12.66n ± 2%

                                         │      -       │
                                         │     B/op     │
MessageInitiationUnmarshal/binary.Read-8   208.0 ± 0%
MessageInitiationUnmarshal/unmarshal-8     0.000 ± 0%

                                         │      -       │
                                         │  allocs/op   │
MessageInitiationUnmarshal/binary.Read-8   2.000 ± 0%
MessageInitiationUnmarshal/unmarshal-8     0.000 ± 0%

Signed-off-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-15 16:42:06 +02:00
Kurnia D Win 436f7fdc16 rwcancel: fix wrong poll event flag on ReadyWrite
It should be POLLIN because closeFd is read-only file.

Signed-off-by: Kurnia D Win <kurnia.d.win@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2025-05-05 15:10:08 +02:00
15 changed files with 1020 additions and 118 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
+40
View File
@@ -13,6 +13,35 @@ import (
"golang.org/x/sys/unix"
)
// Taken from go/src/internal/syscall/unix/kernel_version_linux.go
func kernelVersion() (major, minor int) {
var uname unix.Utsname
if err := unix.Uname(&uname); err != nil {
return
}
var (
values [2]int
value, vi int
)
for _, c := range uname.Release {
if '0' <= c && c <= '9' {
value = (value * 10) + int(c-'0')
} else {
// Note that we're assuming N.N.N here.
// If we see anything else, we are likely to mis-parse it.
values[vi] = value
vi++
if vi >= len(values) {
break
}
value = 0
}
}
return values[0], values[1]
}
func init() {
controlFns = append(controlFns,
@@ -60,6 +89,17 @@ func init() {
// Attempt to enable UDP_GRO
func(network, address string, c syscall.RawConn) error {
// Kernels below 5.12 are missing 98184612aca0 ("net:
// udp: Add support for getsockopt(..., ..., UDP_GRO,
// ..., ...);"), which means we can't read this back
// later. We could pipe the return value through to
// the rest of the code, but UDP_GRO is kind of buggy
// anyway, so just gate this here.
major, minor := kernelVersion()
if major < 5 || (major == 5 && minor < 12) {
return nil
}
c.Control(func(fd uintptr) {
_ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
})
+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)
}
+35 -12
View File
@@ -223,19 +223,11 @@ func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix)
}
}
func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
table.mutex.Lock()
defer table.mutex.Unlock()
var next *list.Element
for elem := peer.trieEntries.Front(); elem != nil; elem = next {
next = elem.Next()
node := elem.Value.(*trieEntry)
func (node *trieEntry) remove() {
node.removeFromPeerEntries()
node.peer = nil
if node.child[0] != nil && node.child[1] != nil {
continue
return
}
bit := 0
if node.child[0] == nil {
@@ -248,12 +240,12 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
*node.parent.parentBit = child
if node.child[0] != nil || node.child[1] != nil || node.parent.parentBitType > 1 {
node.zeroizePointers()
continue
return
}
parent := (*trieEntry)(unsafe.Pointer(uintptr(unsafe.Pointer(node.parent.parentBit)) - unsafe.Offsetof(node.child) - unsafe.Sizeof(node.child[0])*uintptr(node.parent.parentBitType)))
if parent.peer != nil {
node.zeroizePointers()
continue
return
}
child = parent.child[node.parent.parentBitType^1]
if child != nil {
@@ -263,6 +255,37 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
node.zeroizePointers()
parent.zeroizePointers()
}
func (table *AllowedIPs) Remove(prefix netip.Prefix, peer *Peer) {
table.mutex.Lock()
defer table.mutex.Unlock()
var node *trieEntry
var exact bool
if prefix.Addr().Is6() {
ip := prefix.Addr().As16()
node, exact = table.IPv6.nodePlacement(ip[:], uint8(prefix.Bits()))
} else if prefix.Addr().Is4() {
ip := prefix.Addr().As4()
node, exact = table.IPv4.nodePlacement(ip[:], uint8(prefix.Bits()))
} else {
panic(errors.New("removing unknown address type"))
}
if !exact || node == nil || peer != node.peer {
return
}
node.remove()
}
func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
table.mutex.Lock()
defer table.mutex.Unlock()
var next *list.Element
for elem := peer.trieEntries.Front(); elem != nil; elem = next {
next = elem.Next()
elem.Value.(*trieEntry).remove()
}
}
func (table *AllowedIPs) Insert(prefix netip.Prefix, peer *Peer) {
+57
View File
@@ -101,6 +101,10 @@ func TestTrieIPv4(t *testing.T) {
allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom4([4]byte{a, b, c, d}), int(cidr)), peer)
}
remove := func(peer *Peer, a, b, c, d byte, cidr uint8) {
allowedIPs.Remove(netip.PrefixFrom(netip.AddrFrom4([4]byte{a, b, c, d}), int(cidr)), peer)
}
assertEQ := func(peer *Peer, a, b, c, d byte) {
p := allowedIPs.Lookup([]byte{a, b, c, d})
if p != peer {
@@ -176,6 +180,21 @@ func TestTrieIPv4(t *testing.T) {
allowedIPs.RemoveByPeer(a)
assertNEQ(a, 192, 168, 0, 1)
insert(a, 1, 0, 0, 0, 32)
insert(a, 192, 0, 0, 0, 24)
assertEQ(a, 1, 0, 0, 0)
assertEQ(a, 192, 0, 0, 1)
remove(a, 192, 0, 0, 0, 32)
assertEQ(a, 192, 0, 0, 1)
remove(nil, 192, 0, 0, 0, 24)
assertEQ(a, 192, 0, 0, 1)
remove(b, 192, 0, 0, 0, 24)
assertEQ(a, 192, 0, 0, 1)
remove(a, 192, 0, 0, 0, 24)
assertNEQ(a, 192, 0, 0, 1)
remove(a, 1, 0, 0, 0, 32)
assertNEQ(a, 1, 0, 0, 0)
}
/* Test ported from kernel implementation:
@@ -211,6 +230,15 @@ func TestTrieIPv6(t *testing.T) {
allowedIPs.Insert(netip.PrefixFrom(netip.AddrFrom16(*(*[16]byte)(addr)), int(cidr)), peer)
}
remove := func(peer *Peer, a, b, c, d uint32, cidr uint8) {
var addr []byte
addr = append(addr, expand(a)...)
addr = append(addr, expand(b)...)
addr = append(addr, expand(c)...)
addr = append(addr, expand(d)...)
allowedIPs.Remove(netip.PrefixFrom(netip.AddrFrom16(*(*[16]byte)(addr)), int(cidr)), peer)
}
assertEQ := func(peer *Peer, a, b, c, d uint32) {
var addr []byte
addr = append(addr, expand(a)...)
@@ -223,6 +251,18 @@ func TestTrieIPv6(t *testing.T) {
}
}
assertNEQ := func(peer *Peer, a, b, c, d uint32) {
var addr []byte
addr = append(addr, expand(a)...)
addr = append(addr, expand(b)...)
addr = append(addr, expand(c)...)
addr = append(addr, expand(d)...)
p := allowedIPs.Lookup(addr)
if p == peer {
t.Error("Assert NEQ failed")
}
}
insert(d, 0x26075300, 0x60006b00, 0, 0xc05f0543, 128)
insert(c, 0x26075300, 0x60006b00, 0, 0, 64)
insert(e, 0, 0, 0, 0, 0)
@@ -244,4 +284,21 @@ func TestTrieIPv6(t *testing.T) {
assertEQ(h, 0x24046800, 0x40040800, 0, 0)
assertEQ(h, 0x24046800, 0x40040800, 0x10101010, 0x10101010)
assertEQ(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef)
insert(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128)
insert(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98)
assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef)
assertEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010)
remove(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 96)
assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef)
remove(nil, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128)
assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef)
remove(b, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128)
assertEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef)
remove(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128)
assertNEQ(a, 0x24446801, 0x40e40800, 0xdeaebeef, 0xdefbeef)
remove(b, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98)
assertEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010)
remove(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98)
assertNEQ(a, 0x24446800, 0xf0e40800, 0xeeaebeef, 0x10101010)
}
+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")
}
+103 -28
View File
@@ -6,6 +6,7 @@
package device
import (
"encoding/binary"
"errors"
"fmt"
"sync"
@@ -13,7 +14,6 @@ import (
"golang.org/x/crypto/blake2s"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
"golang.zx2c4.com/wireguard/tai64n"
)
@@ -64,7 +64,7 @@ const (
MessageResponseSize = 92 // size of response message
MessageCookieReplySize = 64 // size of cookie reply message
MessageTransportHeaderSize = 16 // size of data preceding content in transport message
MessageTransportSize = MessageTransportHeaderSize + poly1305.TagSize // size of empty transport
MessageTransportSize = MessageTransportHeaderSize // size of empty transport (no encryption tag)
MessageKeepaliveSize = MessageTransportSize // size of keepalive
MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message
)
@@ -85,8 +85,8 @@ type MessageInitiation struct {
Type uint32
Sender uint32
Ephemeral NoisePublicKey
Static [NoisePublicKeySize + poly1305.TagSize]byte
Timestamp [tai64n.TimestampSize + poly1305.TagSize]byte
Static [NoisePublicKeySize + Poly1305TagSize]byte
Timestamp [tai64n.TimestampSize + Poly1305TagSize]byte
MAC1 [blake2s.Size128]byte
MAC2 [blake2s.Size128]byte
}
@@ -96,7 +96,7 @@ type MessageResponse struct {
Sender uint32
Receiver uint32
Ephemeral NoisePublicKey
Empty [poly1305.TagSize]byte
Empty [Poly1305TagSize]byte
MAC1 [blake2s.Size128]byte
MAC2 [blake2s.Size128]byte
}
@@ -112,7 +112,99 @@ type MessageCookieReply struct {
Type uint32
Receiver uint32
Nonce [chacha20poly1305.NonceSizeX]byte
Cookie [blake2s.Size128 + poly1305.TagSize]byte
Cookie [blake2s.Size128 + Poly1305TagSize]byte
}
var errMessageLengthMismatch = errors.New("message length mismatch")
func (msg *MessageInitiation) unmarshal(b []byte) error {
if len(b) != MessageInitiationSize {
return errMessageLengthMismatch
}
msg.Type = binary.LittleEndian.Uint32(b)
msg.Sender = binary.LittleEndian.Uint32(b[4:])
copy(msg.Ephemeral[:], b[8:])
copy(msg.Static[:], b[8+len(msg.Ephemeral):])
copy(msg.Timestamp[:], b[8+len(msg.Ephemeral)+len(msg.Static):])
copy(msg.MAC1[:], b[8+len(msg.Ephemeral)+len(msg.Static)+len(msg.Timestamp):])
copy(msg.MAC2[:], b[8+len(msg.Ephemeral)+len(msg.Static)+len(msg.Timestamp)+len(msg.MAC1):])
return nil
}
func (msg *MessageInitiation) marshal(b []byte) error {
if len(b) != MessageInitiationSize {
return errMessageLengthMismatch
}
binary.LittleEndian.PutUint32(b, msg.Type)
binary.LittleEndian.PutUint32(b[4:], msg.Sender)
copy(b[8:], msg.Ephemeral[:])
copy(b[8+len(msg.Ephemeral):], msg.Static[:])
copy(b[8+len(msg.Ephemeral)+len(msg.Static):], msg.Timestamp[:])
copy(b[8+len(msg.Ephemeral)+len(msg.Static)+len(msg.Timestamp):], msg.MAC1[:])
copy(b[8+len(msg.Ephemeral)+len(msg.Static)+len(msg.Timestamp)+len(msg.MAC1):], msg.MAC2[:])
return nil
}
func (msg *MessageResponse) unmarshal(b []byte) error {
if len(b) != MessageResponseSize {
return errMessageLengthMismatch
}
msg.Type = binary.LittleEndian.Uint32(b)
msg.Sender = binary.LittleEndian.Uint32(b[4:])
msg.Receiver = binary.LittleEndian.Uint32(b[8:])
copy(msg.Ephemeral[:], b[12:])
copy(msg.Empty[:], b[12+len(msg.Ephemeral):])
copy(msg.MAC1[:], b[12+len(msg.Ephemeral)+len(msg.Empty):])
copy(msg.MAC2[:], b[12+len(msg.Ephemeral)+len(msg.Empty)+len(msg.MAC1):])
return nil
}
func (msg *MessageResponse) marshal(b []byte) error {
if len(b) != MessageResponseSize {
return errMessageLengthMismatch
}
binary.LittleEndian.PutUint32(b, msg.Type)
binary.LittleEndian.PutUint32(b[4:], msg.Sender)
binary.LittleEndian.PutUint32(b[8:], msg.Receiver)
copy(b[12:], msg.Ephemeral[:])
copy(b[12+len(msg.Ephemeral):], msg.Empty[:])
copy(b[12+len(msg.Ephemeral)+len(msg.Empty):], msg.MAC1[:])
copy(b[12+len(msg.Ephemeral)+len(msg.Empty)+len(msg.MAC1):], msg.MAC2[:])
return nil
}
func (msg *MessageCookieReply) unmarshal(b []byte) error {
if len(b) != MessageCookieReplySize {
return errMessageLengthMismatch
}
msg.Type = binary.LittleEndian.Uint32(b)
msg.Receiver = binary.LittleEndian.Uint32(b[4:])
copy(msg.Nonce[:], b[8:])
copy(msg.Cookie[:], b[8+len(msg.Nonce):])
return nil
}
func (msg *MessageCookieReply) marshal(b []byte) error {
if len(b) != MessageCookieReplySize {
return errMessageLengthMismatch
}
binary.LittleEndian.PutUint32(b, msg.Type)
binary.LittleEndian.PutUint32(b[4:], msg.Receiver)
copy(b[8:], msg.Nonce[:])
copy(b[8+len(msg.Nonce):], msg.Cookie[:])
return nil
}
type Handshake struct {
@@ -522,27 +614,13 @@ func (peer *Peer) BeginSymmetricSession() error {
handshake.mutex.Lock()
defer handshake.mutex.Unlock()
// derive keys
// determine initiator role
var isInitiator bool
var sendKey [chacha20poly1305.KeySize]byte
var recvKey [chacha20poly1305.KeySize]byte
if handshake.state == handshakeResponseConsumed {
KDF2(
&sendKey,
&recvKey,
handshake.chainKey[:],
nil,
)
isInitiator = true
} else if handshake.state == handshakeResponseCreated {
KDF2(
&recvKey,
&sendKey,
handshake.chainKey[:],
nil,
)
isInitiator = false
} else {
return fmt.Errorf("invalid state for keypair derivation: %v", handshake.state)
@@ -551,18 +629,15 @@ func (peer *Peer) BeginSymmetricSession() error {
// zero handshake
setZero(handshake.chainKey[:])
setZero(handshake.hash[:]) // Doesn't necessarily need to be zeroed. Could be used for something interesting down the line.
setZero(handshake.hash[:])
setZero(handshake.localEphemeral[:])
peer.handshake.state = handshakeZeroed
// create AEAD instances
// create keypair without encryption
keypair := new(Keypair)
keypair.send, _ = chacha20poly1305.New(sendKey[:])
keypair.receive, _ = chacha20poly1305.New(recvKey[:])
setZero(sendKey[:])
setZero(recvKey[:])
keypair.send = nil // no encryption
keypair.receive = nil // no decryption
keypair.created = time.Now()
keypair.replayFilter.Reset()
+1
View File
@@ -15,6 +15,7 @@ const (
NoisePublicKeySize = 32
NoisePrivateKeySize = 32
NoisePresharedKeySize = 32
Poly1305TagSize = 16 // Size of Poly1305 authentication tag
)
type (
+5 -23
View File
@@ -6,14 +6,12 @@
package device
import (
"bytes"
"encoding/binary"
"errors"
"net"
"sync"
"time"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"golang.zx2c4.com/wireguard/conn"
@@ -237,8 +235,6 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
}
func (device *Device) RoutineDecryption(id int) {
var nonce [chacha20poly1305.NonceSize]byte
defer device.log.Verbosef("Routine: decryption worker %d - stopped", id)
device.log.Verbosef("Routine: decryption worker %d - started", id)
@@ -248,20 +244,9 @@ func (device *Device) RoutineDecryption(id int) {
counter := elem.packet[MessageTransportOffsetCounter:MessageTransportOffsetContent]
content := elem.packet[MessageTransportOffsetContent:]
// decrypt and release to consumer
var err error
// pass through content without decryption
elem.counter = binary.LittleEndian.Uint64(counter)
// copy counter to nonce
binary.LittleEndian.PutUint64(nonce[0x4:0xc], elem.counter)
elem.packet, err = elem.keypair.receive.Open(
content[:0],
nonce[:],
content,
nil,
)
if err != nil {
elem.packet = nil
}
elem.packet = content
}
elemsContainer.Unlock()
}
@@ -287,8 +272,7 @@ func (device *Device) RoutineHandshake(id int) {
// unmarshal packet
var reply MessageCookieReply
reader := bytes.NewReader(elem.packet)
err := binary.Read(reader, binary.LittleEndian, &reply)
err := reply.unmarshal(elem.packet)
if err != nil {
device.log.Verbosef("Failed to decode cookie reply")
goto skip
@@ -353,8 +337,7 @@ func (device *Device) RoutineHandshake(id int) {
// unmarshal
var msg MessageInitiation
reader := bytes.NewReader(elem.packet)
err := binary.Read(reader, binary.LittleEndian, &msg)
err := msg.unmarshal(elem.packet)
if err != nil {
device.log.Errorf("Failed to decode initiation message")
goto skip
@@ -386,8 +369,7 @@ func (device *Device) RoutineHandshake(id int) {
// unmarshal
var msg MessageResponse
reader := bytes.NewReader(elem.packet)
err := binary.Read(reader, binary.LittleEndian, &msg)
err := msg.unmarshal(elem.packet)
if err != nil {
device.log.Errorf("Failed to decode response message")
goto skip
+12 -31
View File
@@ -6,7 +6,6 @@
package device
import (
"bytes"
"encoding/binary"
"errors"
"net"
@@ -14,7 +13,6 @@ import (
"sync"
"time"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"golang.zx2c4.com/wireguard/conn"
@@ -124,10 +122,8 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
return err
}
var buf [MessageInitiationSize]byte
writer := bytes.NewBuffer(buf[:0])
binary.Write(writer, binary.LittleEndian, msg)
packet := writer.Bytes()
packet := make([]byte, MessageInitiationSize)
_ = msg.marshal(packet)
peer.cookieGenerator.AddMacs(packet)
peer.timersAnyAuthenticatedPacketTraversal()
@@ -155,10 +151,8 @@ func (peer *Peer) SendHandshakeResponse() error {
return err
}
var buf [MessageResponseSize]byte
writer := bytes.NewBuffer(buf[:0])
binary.Write(writer, binary.LittleEndian, response)
packet := writer.Bytes()
packet := make([]byte, MessageResponseSize)
_ = response.marshal(packet)
peer.cookieGenerator.AddMacs(packet)
err = peer.BeginSymmetricSession()
@@ -189,11 +183,11 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement)
return err
}
var buf [MessageCookieReplySize]byte
writer := bytes.NewBuffer(buf[:0])
binary.Write(writer, binary.LittleEndian, reply)
packet := make([]byte, MessageCookieReplySize)
_ = reply.marshal(packet)
// TODO: allocation could be avoided
device.net.bind.Send([][]byte{writer.Bytes()}, initiatingElem.endpoint)
device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint)
return nil
}
@@ -436,15 +430,12 @@ func calculatePaddingSize(packetSize, mtu int) int {
return paddedSize - lastUnit
}
/* Encrypts the elements in the queue
/* Processes the elements in the queue without encryption
* and marks them for sequential consumption (by releasing the mutex)
*
* Obs. One instance per core
*/
func (device *Device) RoutineEncryption(id int) {
var paddingZeros [PaddingMultiple]byte
var nonce [chacha20poly1305.NonceSize]byte
defer device.log.Verbosef("Routine: encryption worker %d - stopped", id)
device.log.Verbosef("Routine: encryption worker %d - started", id)
@@ -461,19 +452,9 @@ func (device *Device) RoutineEncryption(id int) {
binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex)
binary.LittleEndian.PutUint64(fieldNonce, elem.nonce)
// pad content to multiple of 16
paddingSize := calculatePaddingSize(len(elem.packet), int(device.tun.mtu.Load()))
elem.packet = append(elem.packet, paddingZeros[:paddingSize]...)
// encrypt content and release to consumer
binary.LittleEndian.PutUint64(nonce[4:], elem.nonce)
elem.packet = elem.keypair.send.Seal(
header,
nonce[:],
elem.packet,
nil,
)
// append content directly to header without encryption
copy(elem.buffer[MessageTransportHeaderSize:], elem.packet)
elem.packet = elem.buffer[:MessageTransportHeaderSize+len(elem.packet)]
}
elemsContainer.Unlock()
}
+12 -1
View File
@@ -371,7 +371,14 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error
device.allowedips.RemoveByPeer(peer.Peer)
case "allowed_ip":
device.log.Verbosef("%v - UAPI: Adding allowedip", peer.Peer)
add := true
verb := "Adding"
if len(value) > 0 && value[0] == '-' {
add = false
verb = "Removing"
value = value[1:]
}
device.log.Verbosef("%v - UAPI: %s allowedip", peer.Peer, verb)
prefix, err := netip.ParsePrefix(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to set allowed ip: %w", err)
@@ -379,7 +386,11 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error
if peer.dummy {
return nil
}
if add {
device.allowedips.Insert(prefix, peer.Peer)
} else {
device.allowedips.Remove(prefix, peer.Peer)
}
case "protocol_version":
if value != "1" {
+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()
}
+1 -1
View File
@@ -64,7 +64,7 @@ func (rw *RWCancel) ReadyRead() bool {
func (rw *RWCancel) ReadyWrite() bool {
closeFd := int32(rw.closingReader.Fd())
pollFds := []unix.PollFd{{Fd: int32(rw.fd), Events: unix.POLLOUT}, {Fd: closeFd, Events: unix.POLLOUT}}
pollFds := []unix.PollFd{{Fd: int32(rw.fd), Events: unix.POLLOUT}, {Fd: closeFd, Events: unix.POLLIN}}
var err error
for {
_, err = unix.Poll(pollFds, -1)
+1 -1
View File
@@ -1,3 +1,3 @@
package main
const Version = "0.0.20230223"
const Version = "0.0.20250522"