Compare commits

...

13 Commits

Author SHA1 Message Date
Your Name c353e76058 add 2025-07-27 22:55:43 +08:00
Your Name bc84b69ebc add 2025-07-27 22:55:38 +08:00
Your Name 557f266b3c test 2025-07-27 22:45:13 +08:00
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
21 changed files with 1717 additions and 119 deletions
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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
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" "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() { func init() {
controlFns = append(controlFns, controlFns = append(controlFns,
@@ -60,6 +89,17 @@ func init() {
// Attempt to enable UDP_GRO // Attempt to enable UDP_GRO
func(network, address string, c syscall.RawConn) error { 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) { c.Control(func(fd uintptr) {
_ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1) _ = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
}) })
+184
View File
@@ -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
View File
@@ -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
View File
@@ -223,19 +223,11 @@ func (table *AllowedIPs) EntriesForPeer(peer *Peer, cb func(prefix netip.Prefix)
} }
} }
func (table *AllowedIPs) RemoveByPeer(peer *Peer) { func (node *trieEntry) remove() {
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)
node.removeFromPeerEntries() node.removeFromPeerEntries()
node.peer = nil node.peer = nil
if node.child[0] != nil && node.child[1] != nil { if node.child[0] != nil && node.child[1] != nil {
continue return
} }
bit := 0 bit := 0
if node.child[0] == nil { if node.child[0] == nil {
@@ -248,12 +240,12 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
*node.parent.parentBit = child *node.parent.parentBit = child
if node.child[0] != nil || node.child[1] != nil || node.parent.parentBitType > 1 { if node.child[0] != nil || node.child[1] != nil || node.parent.parentBitType > 1 {
node.zeroizePointers() 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))) 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 { if parent.peer != nil {
node.zeroizePointers() node.zeroizePointers()
continue return
} }
child = parent.child[node.parent.parentBitType^1] child = parent.child[node.parent.parentBitType^1]
if child != nil { if child != nil {
@@ -263,6 +255,37 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
node.zeroizePointers() node.zeroizePointers()
parent.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) { 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) 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) { assertEQ := func(peer *Peer, a, b, c, d byte) {
p := allowedIPs.Lookup([]byte{a, b, c, d}) p := allowedIPs.Lookup([]byte{a, b, c, d})
if p != peer { if p != peer {
@@ -176,6 +180,21 @@ func TestTrieIPv4(t *testing.T) {
allowedIPs.RemoveByPeer(a) allowedIPs.RemoveByPeer(a)
assertNEQ(a, 192, 168, 0, 1) 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: /* 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) 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) { assertEQ := func(peer *Peer, a, b, c, d uint32) {
var addr []byte var addr []byte
addr = append(addr, expand(a)...) 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(d, 0x26075300, 0x60006b00, 0, 0xc05f0543, 128)
insert(c, 0x26075300, 0x60006b00, 0, 0, 64) insert(c, 0x26075300, 0x60006b00, 0, 0, 64)
insert(e, 0, 0, 0, 0, 0) 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, 0, 0)
assertEQ(h, 0x24046800, 0x40040800, 0x10101010, 0x10101010) assertEQ(h, 0x24046800, 0x40040800, 0x10101010, 0x10101010)
assertEQ(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef) 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)
} }
+169
View File
@@ -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
View File
@@ -6,6 +6,7 @@
package device package device
import ( import (
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"sync" "sync"
@@ -13,7 +14,6 @@ import (
"golang.org/x/crypto/blake2s" "golang.org/x/crypto/blake2s"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
"golang.zx2c4.com/wireguard/tai64n" "golang.zx2c4.com/wireguard/tai64n"
) )
@@ -64,7 +64,7 @@ const (
MessageResponseSize = 92 // size of response message MessageResponseSize = 92 // size of response message
MessageCookieReplySize = 64 // size of cookie reply message MessageCookieReplySize = 64 // size of cookie reply message
MessageTransportHeaderSize = 16 // size of data preceding content in transport 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 MessageKeepaliveSize = MessageTransportSize // size of keepalive
MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message MessageHandshakeSize = MessageInitiationSize // size of largest handshake related message
) )
@@ -85,8 +85,8 @@ type MessageInitiation struct {
Type uint32 Type uint32
Sender uint32 Sender uint32
Ephemeral NoisePublicKey Ephemeral NoisePublicKey
Static [NoisePublicKeySize + poly1305.TagSize]byte Static [NoisePublicKeySize + Poly1305TagSize]byte
Timestamp [tai64n.TimestampSize + poly1305.TagSize]byte Timestamp [tai64n.TimestampSize + Poly1305TagSize]byte
MAC1 [blake2s.Size128]byte MAC1 [blake2s.Size128]byte
MAC2 [blake2s.Size128]byte MAC2 [blake2s.Size128]byte
} }
@@ -96,7 +96,7 @@ type MessageResponse struct {
Sender uint32 Sender uint32
Receiver uint32 Receiver uint32
Ephemeral NoisePublicKey Ephemeral NoisePublicKey
Empty [poly1305.TagSize]byte Empty [Poly1305TagSize]byte
MAC1 [blake2s.Size128]byte MAC1 [blake2s.Size128]byte
MAC2 [blake2s.Size128]byte MAC2 [blake2s.Size128]byte
} }
@@ -112,7 +112,99 @@ type MessageCookieReply struct {
Type uint32 Type uint32
Receiver uint32 Receiver uint32
Nonce [chacha20poly1305.NonceSizeX]byte 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 { type Handshake struct {
@@ -522,27 +614,13 @@ func (peer *Peer) BeginSymmetricSession() error {
handshake.mutex.Lock() handshake.mutex.Lock()
defer handshake.mutex.Unlock() defer handshake.mutex.Unlock()
// derive keys // determine initiator role
var isInitiator bool var isInitiator bool
var sendKey [chacha20poly1305.KeySize]byte
var recvKey [chacha20poly1305.KeySize]byte
if handshake.state == handshakeResponseConsumed { if handshake.state == handshakeResponseConsumed {
KDF2(
&sendKey,
&recvKey,
handshake.chainKey[:],
nil,
)
isInitiator = true isInitiator = true
} else if handshake.state == handshakeResponseCreated { } else if handshake.state == handshakeResponseCreated {
KDF2(
&recvKey,
&sendKey,
handshake.chainKey[:],
nil,
)
isInitiator = false isInitiator = false
} else { } else {
return fmt.Errorf("invalid state for keypair derivation: %v", handshake.state) return fmt.Errorf("invalid state for keypair derivation: %v", handshake.state)
@@ -551,18 +629,15 @@ func (peer *Peer) BeginSymmetricSession() error {
// zero handshake // zero handshake
setZero(handshake.chainKey[:]) 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[:]) setZero(handshake.localEphemeral[:])
peer.handshake.state = handshakeZeroed peer.handshake.state = handshakeZeroed
// create AEAD instances // create keypair without encryption
keypair := new(Keypair) keypair := new(Keypair)
keypair.send, _ = chacha20poly1305.New(sendKey[:]) keypair.send = nil // no encryption
keypair.receive, _ = chacha20poly1305.New(recvKey[:]) keypair.receive = nil // no decryption
setZero(sendKey[:])
setZero(recvKey[:])
keypair.created = time.Now() keypair.created = time.Now()
keypair.replayFilter.Reset() keypair.replayFilter.Reset()
+1
View File
@@ -15,6 +15,7 @@ const (
NoisePublicKeySize = 32 NoisePublicKeySize = 32
NoisePrivateKeySize = 32 NoisePrivateKeySize = 32
NoisePresharedKeySize = 32 NoisePresharedKeySize = 32
Poly1305TagSize = 16 // Size of Poly1305 authentication tag
) )
type ( type (
+5 -23
View File
@@ -6,14 +6,12 @@
package device package device
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"net" "net"
"sync" "sync"
"time" "time"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
"golang.org/x/net/ipv6" "golang.org/x/net/ipv6"
"golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/conn"
@@ -237,8 +235,6 @@ func (device *Device) RoutineReceiveIncoming(maxBatchSize int, recv conn.Receive
} }
func (device *Device) RoutineDecryption(id int) { func (device *Device) RoutineDecryption(id int) {
var nonce [chacha20poly1305.NonceSize]byte
defer device.log.Verbosef("Routine: decryption worker %d - stopped", id) defer device.log.Verbosef("Routine: decryption worker %d - stopped", id)
device.log.Verbosef("Routine: decryption worker %d - started", 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] counter := elem.packet[MessageTransportOffsetCounter:MessageTransportOffsetContent]
content := elem.packet[MessageTransportOffsetContent:] content := elem.packet[MessageTransportOffsetContent:]
// decrypt and release to consumer // pass through content without decryption
var err error
elem.counter = binary.LittleEndian.Uint64(counter) elem.counter = binary.LittleEndian.Uint64(counter)
// copy counter to nonce elem.packet = content
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
}
} }
elemsContainer.Unlock() elemsContainer.Unlock()
} }
@@ -287,8 +272,7 @@ func (device *Device) RoutineHandshake(id int) {
// unmarshal packet // unmarshal packet
var reply MessageCookieReply var reply MessageCookieReply
reader := bytes.NewReader(elem.packet) err := reply.unmarshal(elem.packet)
err := binary.Read(reader, binary.LittleEndian, &reply)
if err != nil { if err != nil {
device.log.Verbosef("Failed to decode cookie reply") device.log.Verbosef("Failed to decode cookie reply")
goto skip goto skip
@@ -353,8 +337,7 @@ func (device *Device) RoutineHandshake(id int) {
// unmarshal // unmarshal
var msg MessageInitiation var msg MessageInitiation
reader := bytes.NewReader(elem.packet) err := msg.unmarshal(elem.packet)
err := binary.Read(reader, binary.LittleEndian, &msg)
if err != nil { if err != nil {
device.log.Errorf("Failed to decode initiation message") device.log.Errorf("Failed to decode initiation message")
goto skip goto skip
@@ -386,8 +369,7 @@ func (device *Device) RoutineHandshake(id int) {
// unmarshal // unmarshal
var msg MessageResponse var msg MessageResponse
reader := bytes.NewReader(elem.packet) err := msg.unmarshal(elem.packet)
err := binary.Read(reader, binary.LittleEndian, &msg)
if err != nil { if err != nil {
device.log.Errorf("Failed to decode response message") device.log.Errorf("Failed to decode response message")
goto skip goto skip
+12 -31
View File
@@ -6,7 +6,6 @@
package device package device
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"net" "net"
@@ -14,7 +13,6 @@ import (
"sync" "sync"
"time" "time"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
"golang.org/x/net/ipv6" "golang.org/x/net/ipv6"
"golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/conn"
@@ -124,10 +122,8 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
return err return err
} }
var buf [MessageInitiationSize]byte packet := make([]byte, MessageInitiationSize)
writer := bytes.NewBuffer(buf[:0]) _ = msg.marshal(packet)
binary.Write(writer, binary.LittleEndian, msg)
packet := writer.Bytes()
peer.cookieGenerator.AddMacs(packet) peer.cookieGenerator.AddMacs(packet)
peer.timersAnyAuthenticatedPacketTraversal() peer.timersAnyAuthenticatedPacketTraversal()
@@ -155,10 +151,8 @@ func (peer *Peer) SendHandshakeResponse() error {
return err return err
} }
var buf [MessageResponseSize]byte packet := make([]byte, MessageResponseSize)
writer := bytes.NewBuffer(buf[:0]) _ = response.marshal(packet)
binary.Write(writer, binary.LittleEndian, response)
packet := writer.Bytes()
peer.cookieGenerator.AddMacs(packet) peer.cookieGenerator.AddMacs(packet)
err = peer.BeginSymmetricSession() err = peer.BeginSymmetricSession()
@@ -189,11 +183,11 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement)
return err return err
} }
var buf [MessageCookieReplySize]byte packet := make([]byte, MessageCookieReplySize)
writer := bytes.NewBuffer(buf[:0]) _ = reply.marshal(packet)
binary.Write(writer, binary.LittleEndian, reply)
// TODO: allocation could be avoided // TODO: allocation could be avoided
device.net.bind.Send([][]byte{writer.Bytes()}, initiatingElem.endpoint) device.net.bind.Send([][]byte{packet}, initiatingElem.endpoint)
return nil return nil
} }
@@ -436,15 +430,12 @@ func calculatePaddingSize(packetSize, mtu int) int {
return paddedSize - lastUnit 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) * and marks them for sequential consumption (by releasing the mutex)
* *
* Obs. One instance per core * Obs. One instance per core
*/ */
func (device *Device) RoutineEncryption(id int) { 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) defer device.log.Verbosef("Routine: encryption worker %d - stopped", id)
device.log.Verbosef("Routine: encryption worker %d - started", 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.PutUint32(fieldReceiver, elem.keypair.remoteIndex)
binary.LittleEndian.PutUint64(fieldNonce, elem.nonce) binary.LittleEndian.PutUint64(fieldNonce, elem.nonce)
// pad content to multiple of 16 // append content directly to header without encryption
paddingSize := calculatePaddingSize(len(elem.packet), int(device.tun.mtu.Load())) copy(elem.buffer[MessageTransportHeaderSize:], elem.packet)
elem.packet = append(elem.packet, paddingZeros[:paddingSize]...) elem.packet = elem.buffer[:MessageTransportHeaderSize+len(elem.packet)]
// encrypt content and release to consumer
binary.LittleEndian.PutUint64(nonce[4:], elem.nonce)
elem.packet = elem.keypair.send.Seal(
header,
nonce[:],
elem.packet,
nil,
)
} }
elemsContainer.Unlock() 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) device.allowedips.RemoveByPeer(peer.Peer)
case "allowed_ip": 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) prefix, err := netip.ParsePrefix(value)
if err != nil { if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to set allowed ip: %w", err) 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 { if peer.dummy {
return nil return nil
} }
if add {
device.allowedips.Insert(prefix, peer.Peer) device.allowedips.Insert(prefix, peer.Peer)
} else {
device.allowedips.Remove(prefix, peer.Peer)
}
case "protocol_version": case "protocol_version":
if value != "1" { 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()
}
Generated
+61
View File
@@ -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
}
+147
View File
@@ -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;
};
};
});
}
+1 -1
View File
@@ -64,7 +64,7 @@ func (rw *RWCancel) ReadyRead() bool {
func (rw *RWCancel) ReadyWrite() bool { func (rw *RWCancel) ReadyWrite() bool {
closeFd := int32(rw.closingReader.Fd()) 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 var err error
for { for {
_, err = unix.Poll(pollFds, -1) _, err = unix.Poll(pollFds, -1)
+1 -1
View File
@@ -1,3 +1,3 @@
package main package main
const Version = "0.0.20230223" const Version = "0.0.20250522"