Compare commits
13 Commits
0e4482a086
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c353e76058 | |||
| bc84b69ebc | |||
| 557f266b3c | |||
| 9f0133a5c9 | |||
| 7a67b9687c | |||
| f333402bd9 | |||
| c92064f1ce | |||
| 264889f0bb | |||
| 256bcbd70d | |||
| 1571e0fbae | |||
| 842888ac5c | |||
| 9e7529c3d2 | |||
| 436f7fdc16 |
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Automatically load the Nix flake development environment
|
||||
use flake
|
||||
|
||||
# Add project-specific environment variables
|
||||
export PROJECT_ROOT="$(pwd)"
|
||||
export GOPATH="$PROJECT_ROOT/.go"
|
||||
export GOBIN="$GOPATH/bin"
|
||||
export GOCACHE="$PROJECT_ROOT/.gocache"
|
||||
|
||||
# Add Go bin to PATH
|
||||
PATH_add "$GOBIN"
|
||||
|
||||
# Load any additional .env file if present
|
||||
dotenv_if_exists
|
||||
+49
-1
@@ -1 +1,49 @@
|
||||
wireguard-go
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Development environment artifacts
|
||||
.go/
|
||||
.gocache/
|
||||
.direnv/
|
||||
result
|
||||
result-*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Air live reload
|
||||
.air.toml
|
||||
tmp/
|
||||
|
||||
# Local environment variables
|
||||
.env
|
||||
.env.local
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
build-tags:
|
||||
- integration
|
||||
|
||||
output:
|
||||
format: colored-line-number
|
||||
print-issued-lines: true
|
||||
print-linter-name: true
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
check-blank: true
|
||||
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
gofmt:
|
||||
simplify: true
|
||||
|
||||
goimports:
|
||||
local-prefixes: golang.zx2c4.com/wireguard
|
||||
|
||||
golint:
|
||||
min-confidence: 0.8
|
||||
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable-all: true
|
||||
|
||||
ineffassign:
|
||||
check-exported: false
|
||||
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
nakedret:
|
||||
max-func-lines: 30
|
||||
|
||||
prealloc:
|
||||
simple: true
|
||||
range-loops: true
|
||||
for-loops: false
|
||||
|
||||
unparam:
|
||||
check-exported: false
|
||||
|
||||
unused:
|
||||
check-exported: false
|
||||
|
||||
whitespace:
|
||||
multi-if: false
|
||||
multi-func: false
|
||||
|
||||
wsl:
|
||||
strict-append: true
|
||||
allow-assign-and-call: true
|
||||
allow-multiline-assign: true
|
||||
allow-cuddle-declarations: false
|
||||
allow-trailing-comment: false
|
||||
force-case-trailing-whitespace: 0
|
||||
|
||||
linters:
|
||||
enable:
|
||||
# Default linters
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unused
|
||||
|
||||
# Additional recommended linters
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- cyclop
|
||||
- dupl
|
||||
- durationcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goheader
|
||||
- goimports
|
||||
- gomnd
|
||||
- gomoddirectives
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- grouper
|
||||
- importas
|
||||
- maintidx
|
||||
- makezero
|
||||
- misspell
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- stylecheck
|
||||
- tenv
|
||||
- testpackage
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
disable:
|
||||
- gochecknoglobals # Too restrictive for this codebase
|
||||
- goerr113 # Error wrapping style is project-specific
|
||||
- godox # TODO comments are fine
|
||||
- lll # Line length is handled by formatter
|
||||
- paralleltest # Not all tests need to be parallel
|
||||
- wrapcheck # Error wrapping style is project-specific
|
||||
- varnamelen # Variable naming style is project-specific
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on tests files
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- funlen
|
||||
- goconst
|
||||
- gocognit
|
||||
- scopelint
|
||||
- lll
|
||||
|
||||
# Exclude known false positives
|
||||
- text: "weak cryptographic primitive"
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
# Ignore certain GoDoc issues
|
||||
- text: "should have a package comment"
|
||||
linters:
|
||||
- golint
|
||||
- stylecheck
|
||||
|
||||
# Maximum issues count per one linter. Set to 0 to disable
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text. Set to 0 to disable
|
||||
max-same-issues: 0
|
||||
|
||||
# Show only new issues created after git revision `REV`
|
||||
new: false
|
||||
|
||||
# Fix issues automatically when possible
|
||||
fix: false
|
||||
+268
@@ -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
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package conn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MultiPathBind implements Bind interface and sends/receives packets through multiple network paths
|
||||
type MultiPathBind struct {
|
||||
mu sync.RWMutex
|
||||
binds []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,
|
||||
}
|
||||
}
|
||||
|
||||
// Open puts all binds into listening state and collects receive functions from all binds
|
||||
func (mpb *MultiPathBind) Open(port uint16) (fns []ReceiveFunc, actualPort uint16, err error) {
|
||||
mpb.mu.Lock()
|
||||
defer mpb.mu.Unlock()
|
||||
|
||||
// Open first bind to get the actual port
|
||||
var firstBindFns []ReceiveFunc
|
||||
firstBindFns, actualPort, err = mpb.binds[0].Open(port)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to open bind 0: %w", err)
|
||||
}
|
||||
|
||||
// Collect receive functions from the first bind
|
||||
fns = append(fns, firstBindFns...)
|
||||
|
||||
// Open additional binds on the same port and collect their receive functions
|
||||
for i, bind := range mpb.binds[1:] {
|
||||
var bindFns []ReceiveFunc
|
||||
var bindPort uint16
|
||||
bindFns, bindPort, err = bind.Open(actualPort)
|
||||
if err != nil {
|
||||
// If any bind fails, close already opened binds
|
||||
mpb.binds[0].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, err)
|
||||
}
|
||||
|
||||
// Verify all binds use the same port
|
||||
if bindPort != actualPort {
|
||||
mpb.binds[0].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)
|
||||
}
|
||||
|
||||
// Collect receive functions from this bind
|
||||
fns = append(fns, bindFns...)
|
||||
}
|
||||
|
||||
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 first bind to parse endpoints
|
||||
func (mpb *MultiPathBind) ParseEndpoint(s string) (Endpoint, error) {
|
||||
mpb.mu.RLock()
|
||||
defer mpb.mu.RUnlock()
|
||||
return mpb.binds[0].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)
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
# WireGuard Go Development Environment
|
||||
|
||||
This repository includes a comprehensive Nix flake development environment with all the tools needed for efficient Go development.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- [Nix](https://nixos.org/download.html) with flakes enabled
|
||||
- [direnv](https://direnv.net/) (optional but recommended)
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Clone and enter the repository:**
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
cd wireguard-go
|
||||
```
|
||||
|
||||
2. **Option A: Using direnv (Recommended)**
|
||||
```bash
|
||||
direnv allow
|
||||
```
|
||||
This will automatically load the development environment when you enter the directory.
|
||||
|
||||
3. **Option B: Manual activation**
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
## 🔧 Included Tools
|
||||
|
||||
### Core Go Tools
|
||||
- **Go 1.23.1** - Matching the project's go.mod
|
||||
- **gopls** - Official Go Language Server for LSP support
|
||||
|
||||
### Code Quality
|
||||
- **golangci-lint** - Comprehensive linter with 30+ linters enabled
|
||||
- **staticcheck** - Advanced static analysis
|
||||
- **gosec** - Security vulnerability scanner
|
||||
- **govulncheck** - Official Go vulnerability scanner
|
||||
- **gofumpt** - Stricter version of gofmt
|
||||
|
||||
### Development Tools
|
||||
- **delve** - Go debugger
|
||||
- **air** - Live reload for development
|
||||
- **gotests** - Automatic test generation
|
||||
- **gomodifytags** - Struct tag manipulation
|
||||
- **impl** - Interface implementation generator
|
||||
- **gotestsum** - Enhanced test output
|
||||
|
||||
### System Tools
|
||||
- **wireguard-tools** - WireGuard utilities
|
||||
- **iproute2** - Network configuration tools
|
||||
- **iptables** - Firewall utilities
|
||||
|
||||
## 🎯 Quick Commands
|
||||
|
||||
### Development Workflow
|
||||
```bash
|
||||
# Install/update dependencies
|
||||
go mod tidy
|
||||
|
||||
# Run comprehensive linting
|
||||
golangci-lint run
|
||||
|
||||
# Check for security vulnerabilities
|
||||
govulncheck ./...
|
||||
|
||||
# Run tests with coverage
|
||||
go test -race -coverprofile=coverage.out ./...
|
||||
|
||||
# Generate tests for a package
|
||||
gotests -all -w ./device
|
||||
|
||||
# Start live reload development server
|
||||
air
|
||||
|
||||
# Format code with stricter rules
|
||||
gofumpt -w .
|
||||
```
|
||||
|
||||
### Building and Testing
|
||||
```bash
|
||||
# Build the project
|
||||
go build .
|
||||
|
||||
# Run all tests
|
||||
go test ./...
|
||||
|
||||
# Run tests with race detection
|
||||
go test -race ./...
|
||||
|
||||
# Benchmark tests
|
||||
go test -bench=. ./...
|
||||
|
||||
# Generate coverage report
|
||||
go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out
|
||||
```
|
||||
|
||||
### Debugging
|
||||
```bash
|
||||
# Start delve debugger
|
||||
dlv debug
|
||||
|
||||
# Debug a specific test
|
||||
dlv test ./device
|
||||
```
|
||||
|
||||
## 📝 Editor Integration
|
||||
|
||||
### VS Code
|
||||
A `.vscode/settings.json` is included with optimized settings for Go development:
|
||||
- Automatic formatting with gofumpt
|
||||
- Integrated linting with golangci-lint
|
||||
- Proper LSP configuration
|
||||
- Optimized file watching and exclusions
|
||||
|
||||
### Other Editors
|
||||
For vim/neovim, emacs, or other editors that support LSP:
|
||||
- Use `gopls` as the language server
|
||||
- Point formatters to use `gofumpt` instead of `gofmt`
|
||||
- Configure linting to use `golangci-lint`
|
||||
|
||||
## 🔍 Code Quality Configuration
|
||||
|
||||
### Linting
|
||||
The included `.golangci.yml` enables 30+ linters with sensible defaults:
|
||||
- Security checks (gosec, G-prefixed rules)
|
||||
- Performance optimizations (prealloc, ineffassign)
|
||||
- Style consistency (gofumpt, goimports)
|
||||
- Bug prevention (errcheck, staticcheck)
|
||||
|
||||
### Pre-commit Hooks (Optional)
|
||||
Consider setting up pre-commit hooks:
|
||||
```bash
|
||||
# Create .git/hooks/pre-commit
|
||||
#!/bin/bash
|
||||
set -e
|
||||
golangci-lint run
|
||||
go test ./...
|
||||
govulncheck ./...
|
||||
```
|
||||
|
||||
## 🌍 Environment Variables
|
||||
|
||||
The flake automatically sets up:
|
||||
- `GOPATH="$PWD/.go"`
|
||||
- `GOBIN="$PWD/.go/bin"`
|
||||
- `GOCACHE="$PWD/.gocache"`
|
||||
- `GO111MODULE=on`
|
||||
- `CGO_ENABLED=1`
|
||||
- `WG_COLOR_MODE=always`
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
# All tests
|
||||
go test ./...
|
||||
|
||||
# With race detection
|
||||
go test -race ./...
|
||||
|
||||
# Verbose output
|
||||
go test -v ./...
|
||||
|
||||
# Specific package
|
||||
go test ./device
|
||||
|
||||
# With coverage
|
||||
go test -coverprofile=coverage.out ./...
|
||||
```
|
||||
|
||||
### Test Generation
|
||||
```bash
|
||||
# Generate tests for all functions in a package
|
||||
gotests -all -w ./device
|
||||
|
||||
# Generate tests for specific functions
|
||||
gotests -only FunctionName -w ./device
|
||||
```
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
### Vulnerability Scanning
|
||||
```bash
|
||||
# Scan for known vulnerabilities
|
||||
govulncheck ./...
|
||||
|
||||
# Security-focused linting
|
||||
gosec ./...
|
||||
```
|
||||
|
||||
### WireGuard-Specific Security
|
||||
The environment includes networking tools for testing:
|
||||
- WireGuard tools for protocol testing
|
||||
- Network namespace utilities
|
||||
- Traffic analysis tools
|
||||
|
||||
## 📦 Building Packages
|
||||
|
||||
### Development Build
|
||||
```bash
|
||||
go build .
|
||||
```
|
||||
|
||||
### Optimized Build
|
||||
```bash
|
||||
go build -ldflags="-w -s" .
|
||||
```
|
||||
|
||||
### Using Nix to Build
|
||||
```bash
|
||||
# Build using the included Nix package
|
||||
nix build
|
||||
|
||||
# The binary will be in ./result/bin/
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"command not found" errors**
|
||||
- Ensure you're in the flake environment: `nix develop`
|
||||
- Or allow direnv: `direnv allow`
|
||||
|
||||
2. **Go module issues**
|
||||
- Clean module cache: `go clean -modcache`
|
||||
- Verify modules: `go mod verify`
|
||||
|
||||
3. **LSP not working**
|
||||
- Restart your editor
|
||||
- Check gopls is available: `which gopls`
|
||||
- Verify Go version: `go version`
|
||||
|
||||
### Performance Tips
|
||||
- Use `.gocache` for faster builds (already configured)
|
||||
- Exclude build artifacts from file watchers
|
||||
- Use `gotestsum` for faster test feedback
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- [Go Documentation](https://golang.org/doc/)
|
||||
- [WireGuard Protocol](https://www.wireguard.com/protocol/)
|
||||
- [golangci-lint Documentation](https://golangci-lint.run/)
|
||||
- [Delve Debugger](https://github.com/go-delve/delve)
|
||||
+35
-12
@@ -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 {
|
||||
@@ -262,6 +254,37 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
|
||||
*parent.parent.parentBit = child
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package device
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"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
|
||||
|
||||
// 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
@@ -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()
|
||||
|
||||
@@ -15,6 +15,7 @@ const (
|
||||
NoisePublicKeySize = 32
|
||||
NoisePrivateKeySize = 32
|
||||
NoisePresharedKeySize = 32
|
||||
Poly1305TagSize = 16 // Size of Poly1305 authentication tag
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
+5
-23
@@ -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
@@ -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
@@ -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" {
|
||||
|
||||
Executable
+24
@@ -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."
|
||||
@@ -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()
|
||||
}
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1753432016,
|
||||
"narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6027c30c8e9810896b92429f0092f624f7b1aace",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
{
|
||||
description = "WireGuard Go development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
# Go version matching go.mod
|
||||
go = pkgs.go_1_23;
|
||||
|
||||
# Additional Go tools for development
|
||||
goTools = with pkgs; [
|
||||
# Core Go toolchain
|
||||
go
|
||||
|
||||
# Language Server Protocol
|
||||
gopls
|
||||
|
||||
# Formatting and imports
|
||||
# gofmt
|
||||
# goimports
|
||||
gofumpt # Stricter gofmt
|
||||
|
||||
# Linting and static analysis
|
||||
golangci-lint
|
||||
gosec # Security checker
|
||||
ineffassign
|
||||
|
||||
# Debugging
|
||||
delve
|
||||
|
||||
# Code generation and refactoring
|
||||
gotests # Generate tests
|
||||
gomodifytags # Modify struct tags
|
||||
impl # Generate interface implementations
|
||||
govulncheck # Vulnerability scanner
|
||||
|
||||
# Build and development tools
|
||||
air # Live reload
|
||||
gotools # Various tools (guru, gorename, etc.)
|
||||
|
||||
# Testing and benchmarking
|
||||
gotestsum # Pretty test output
|
||||
|
||||
# Documentation
|
||||
# godoc
|
||||
];
|
||||
|
||||
# System tools
|
||||
systemTools = with pkgs; [
|
||||
git
|
||||
gnumake
|
||||
direnv
|
||||
nix-direnv
|
||||
|
||||
# Networking tools (useful for WireGuard development)
|
||||
iproute2
|
||||
iptables
|
||||
wireguard-tools
|
||||
|
||||
# Text processing
|
||||
jq
|
||||
yq-go
|
||||
|
||||
# Shell and utilities
|
||||
fish
|
||||
ripgrep
|
||||
fd
|
||||
tree
|
||||
];
|
||||
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = goTools ++ systemTools;
|
||||
|
||||
shellHook = ''
|
||||
echo "🚀 WireGuard Go development environment loaded!"
|
||||
echo "📦 Go version: $(go version)"
|
||||
echo "🔧 Available tools:"
|
||||
echo " • LSP: gopls"
|
||||
echo " • Linting: golangci-lint, staticcheck, gosec"
|
||||
echo " • Formatting: gofmt, goimports, gofumpt"
|
||||
echo " • Debugging: delve (dlv)"
|
||||
echo " • Testing: gotests, gotestsum"
|
||||
echo " • Security: govulncheck"
|
||||
echo " • Live reload: air"
|
||||
echo ""
|
||||
echo "💡 Quick commands:"
|
||||
echo " • go mod tidy # Clean dependencies"
|
||||
echo " • golangci-lint run # Run all linters"
|
||||
echo " • govulncheck ./... # Check for vulnerabilities"
|
||||
echo " • gotests -all # Generate tests"
|
||||
echo " • air # Live reload server"
|
||||
echo ""
|
||||
|
||||
# Set up Go environment
|
||||
export GOPATH="$PWD/.go"
|
||||
export GOBIN="$GOPATH/bin"
|
||||
export PATH="$GOBIN:$PATH"
|
||||
|
||||
# Create necessary directories
|
||||
mkdir -p "$GOPATH/bin"
|
||||
|
||||
# Set Go build cache to project directory
|
||||
export GOCACHE="$PWD/.gocache"
|
||||
mkdir -p "$GOCACHE"
|
||||
|
||||
# Ensure proper Go module mode
|
||||
export GO111MODULE=on
|
||||
|
||||
# Development-friendly settings
|
||||
export GOTOOLCHAIN="go1.23.1"
|
||||
export CGO_ENABLED=1
|
||||
|
||||
# WireGuard specific
|
||||
export WG_COLOR_MODE=always
|
||||
'';
|
||||
|
||||
# Environment variables for tools
|
||||
CGO_ENABLED = "1";
|
||||
GO111MODULE = "on";
|
||||
GOFLAGS = "-buildvcs=false"; # Disable VCS stamping for reproducible builds
|
||||
};
|
||||
|
||||
# Optional: Create a package for the WireGuard binary
|
||||
packages.default = pkgs.buildGoModule {
|
||||
pname = "wireguard-go";
|
||||
version = "0.0.0-dev";
|
||||
src = ./.;
|
||||
vendorHash = null; # Let Nix handle dependencies
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Userspace Go implementation of WireGuard";
|
||||
homepage = "https://www.wireguard.com/";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux ++ platforms.darwin;
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
const Version = "0.0.20230223"
|
||||
const Version = "0.0.20250522"
|
||||
|
||||
Reference in New Issue
Block a user