初版功能完成
ci / Go checks (ubuntu-latest) (push) Has been cancelled
ci / Go checks (windows-latest) (push) Has been cancelled

This commit is contained in:
qsc
2026-08-29 13:12:17 +08:00
commit 142e5dc7d6
217 changed files with 21313 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
package clientwg
import (
"errors"
"log/slog"
"golang.zx2c4.com/wireguard/tun"
)
// Adapter supplies the one RemLink TUN device created by platform/windows.
type Adapter interface {
Device() tun.Device
}
// NewFromAdapter transfers ownership of the Adapter's live TUN session to an
// embedded wireguard-go Device.
func NewFromAdapter(adapter Adapter, logger *slog.Logger) (*Device, error) {
if adapter == nil {
return nil, errors.New("RemLink adapter must not be nil")
}
return NewDevice(adapter.Device(), logger)
}
+92
View File
@@ -0,0 +1,92 @@
package clientwg
import (
"encoding/hex"
"errors"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"time"
)
const keySize = 32
// Key is one raw 32-byte WireGuard private or public key.
type Key [keySize]byte
// Config is the one-Server-peer wireguard-go configuration used by v1 Nodes.
type Config struct {
PrivateKey Key
ServerPublicKey Key
ServerEndpoint string
OverlayAllowedIPs []netip.Prefix
ListenPort uint16
PersistentKeepalive time.Duration
}
func (c Config) uapi() (string, error) {
if err := c.validate(); err != nil {
return "", err
}
var builder strings.Builder
fmt.Fprintf(&builder, "private_key=%s\n", hex.EncodeToString(c.PrivateKey[:]))
fmt.Fprintf(&builder, "listen_port=%d\n", c.ListenPort)
builder.WriteString("replace_peers=true\n")
fmt.Fprintf(&builder, "public_key=%s\n", hex.EncodeToString(c.ServerPublicKey[:]))
fmt.Fprintf(&builder, "endpoint=%s\n", c.ServerEndpoint)
fmt.Fprintf(&builder, "persistent_keepalive_interval=%d\n", int(c.PersistentKeepalive/time.Second))
builder.WriteString("replace_allowed_ips=true\n")
for _, prefix := range c.OverlayAllowedIPs {
fmt.Fprintf(&builder, "allowed_ip=%s\n", prefix)
}
builder.WriteString("\n")
return builder.String(), nil
}
func (c Config) validate() error {
if zeroKey(c.PrivateKey) {
return errors.New("WireGuard private key must not be zero")
}
if zeroKey(c.ServerPublicKey) {
return errors.New("Server WireGuard public key must not be zero")
}
if c.PrivateKey == c.ServerPublicKey {
return errors.New("Node private key and Server public key must differ")
}
host, portText, err := net.SplitHostPort(c.ServerEndpoint)
if err != nil {
return fmt.Errorf("Server WireGuard endpoint must be host:port: %w", err)
}
if host == "" || portText == "" {
return errors.New("Server WireGuard endpoint must include a host and port")
}
port, err := strconv.ParseUint(portText, 10, 16)
if err != nil || port == 0 {
return errors.New("Server WireGuard endpoint port must be between 1 and 65535")
}
if len(c.OverlayAllowedIPs) != 1 {
return errors.New("v1 requires exactly one Overlay AllowedIP and one Server peer")
}
for _, prefix := range c.OverlayAllowedIPs {
if !prefix.IsValid() || !prefix.Addr().Is4() || prefix.Bits() == 0 {
return fmt.Errorf("Overlay AllowedIP must be IPv4: %s", prefix)
}
if prefix != prefix.Masked() {
return fmt.Errorf("Overlay AllowedIP must use its network address: %s", prefix)
}
}
if c.PersistentKeepalive < 0 || c.PersistentKeepalive > 65535*time.Second {
return errors.New("persistent keepalive must be between 0 and 65535 seconds")
}
if c.PersistentKeepalive%time.Second != 0 {
return errors.New("persistent keepalive must be a whole number of seconds")
}
return nil
}
func zeroKey(key Key) bool {
return key == Key{}
}
+83
View File
@@ -0,0 +1,83 @@
package clientwg
import (
"net/netip"
"strings"
"testing"
"time"
)
func TestConfigUAPIGolden(t *testing.T) {
t.Parallel()
privateKey := Key{1, 2, 3}
serverKey := Key{4, 5, 6}
config := Config{
PrivateKey: privateKey,
ServerPublicKey: serverKey,
ServerEndpoint: "203.0.113.10:51820",
OverlayAllowedIPs: []netip.Prefix{netip.MustParsePrefix("10.88.0.0/16")},
PersistentKeepalive: 25 * time.Second,
}
got, err := config.uapi()
if err != nil {
t.Fatalf("uapi() error = %v", err)
}
checks := []string{
"private_key=0102030000000000000000000000000000000000000000000000000000000000\n",
"listen_port=0\n",
"replace_peers=true\n",
"public_key=0405060000000000000000000000000000000000000000000000000000000000\n",
"endpoint=203.0.113.10:51820\n",
"persistent_keepalive_interval=25\n",
"replace_allowed_ips=true\n",
"allowed_ip=10.88.0.0/16\n",
}
for _, check := range checks {
if !strings.Contains(got, check) {
t.Fatalf("uapi() missing %q in:\n%s", check, got)
}
}
if !strings.HasSuffix(got, "\n\n") {
t.Fatalf("uapi() must terminate with a blank line: %q", got)
}
}
func TestConfigRejectsNonV1Values(t *testing.T) {
t.Parallel()
valid := Config{
PrivateKey: Key{1},
ServerPublicKey: Key{2},
ServerEndpoint: "203.0.113.10:51820",
OverlayAllowedIPs: []netip.Prefix{netip.MustParsePrefix("10.88.0.0/16")},
PersistentKeepalive: 25 * time.Second,
}
tests := []struct {
name string
mutate func(*Config)
}{
{name: "zero private key", mutate: func(c *Config) { c.PrivateKey = Key{} }},
{name: "zero public key", mutate: func(c *Config) { c.ServerPublicKey = Key{} }},
{name: "same keys", mutate: func(c *Config) { c.ServerPublicKey = c.PrivateKey }},
{name: "endpoint", mutate: func(c *Config) { c.ServerEndpoint = "203.0.113.10" }},
{name: "endpoint port", mutate: func(c *Config) { c.ServerEndpoint = "203.0.113.10:not-a-port" }},
{name: "no AllowedIPs", mutate: func(c *Config) { c.OverlayAllowedIPs = nil }},
{name: "multiple AllowedIPs", mutate: func(c *Config) {
c.OverlayAllowedIPs = []netip.Prefix{netip.MustParsePrefix("10.88.0.0/16"), netip.MustParsePrefix("192.168.13.0/24")}
}},
{name: "IPv6", mutate: func(c *Config) { c.OverlayAllowedIPs = []netip.Prefix{netip.MustParsePrefix("fd00::/64")} }},
{name: "Exit Node", mutate: func(c *Config) { c.OverlayAllowedIPs = []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")} }},
{name: "unmasked", mutate: func(c *Config) { c.OverlayAllowedIPs = []netip.Prefix{netip.MustParsePrefix("10.88.0.1/16")} }},
{name: "fractional keepalive", mutate: func(c *Config) { c.PersistentKeepalive = 1500 * time.Millisecond }},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
config := valid
test.mutate(&config)
if _, err := config.uapi(); err == nil {
t.Fatal("uapi() unexpectedly accepted invalid config")
}
})
}
}
+84
View File
@@ -0,0 +1,84 @@
package clientwg
import (
"errors"
"fmt"
"log/slog"
"sync"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
)
// Device owns the MuxTun, wireguard-go engine, and transport bind.
type Device struct {
tun *MuxTun
wireguard *device.Device
closeOnce sync.Once
}
// NewDevice transfers ownership of base to a new embedded wireguard-go Device.
func NewDevice(base tun.Device, logger *slog.Logger) (*Device, error) {
if base == nil {
return nil, errors.New("base TUN device must not be nil")
}
mux := NewMuxTun(base)
wireguard := device.NewDevice(mux, conn.NewDefaultBind(), wireGuardLogger(logger))
return &Device{tun: mux, wireguard: wireguard}, nil
}
// Configure atomically replaces the single Server peer through wireguard-go UAPI.
func (d *Device) Configure(config Config) error {
uapi, err := config.uapi()
if err != nil {
return err
}
if err := d.wireguard.IpcSet(uapi); err != nil {
return fmt.Errorf("configure embedded wireguard-go: %w", err)
}
return nil
}
// Up starts the embedded WireGuard device after configuration.
func (d *Device) Up() error {
if err := d.wireguard.Up(); err != nil {
return fmt.Errorf("bring embedded wireguard-go up: %w", err)
}
return nil
}
// UAPIState returns wireguard-go's current UAPI state for diagnostics.
func (d *Device) UAPIState() (string, error) {
return d.wireguard.IpcGet()
}
// MuxTun returns the single packet boundary used by Session transport.
func (d *Device) MuxTun() *MuxTun { return d.tun }
// Close is idempotent and stops wireguard-go, MuxTun, and the base Wintun.
func (d *Device) Close() {
if d == nil || d.wireguard == nil {
return
}
d.closeOnce.Do(func() {
d.wireguard.Close()
})
}
func wireGuardLogger(logger *slog.Logger) *device.Logger {
if logger == nil {
return &device.Logger{
Verbosef: device.DiscardLogf,
Errorf: device.DiscardLogf,
}
}
return &device.Logger{
Verbosef: func(format string, args ...any) {
logger.Debug(fmt.Sprintf(format, args...))
},
Errorf: func(format string, args ...any) {
logger.Error(fmt.Sprintf(format, args...))
},
}
}
+20
View File
@@ -0,0 +1,20 @@
package clientwg
import (
"encoding/base64"
"fmt"
)
// ParseKeyBase64 parses the standard WireGuard 32-byte base64 key format.
func ParseKeyBase64(value string) (Key, error) {
var key Key
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return key, fmt.Errorf("decode WireGuard key: %w", err)
}
if len(decoded) != keySize {
return key, fmt.Errorf("WireGuard key decoded length is %d, want %d", len(decoded), keySize)
}
copy(key[:], decoded)
return key, nil
}
+27
View File
@@ -0,0 +1,27 @@
package clientwg
import (
"encoding/base64"
"testing"
)
func TestParseKeyBase64(t *testing.T) {
t.Parallel()
raw := make([]byte, keySize)
for index := range raw {
raw[index] = byte(index + 1)
}
key, err := ParseKeyBase64(base64.StdEncoding.EncodeToString(raw))
if err != nil {
t.Fatalf("ParseKeyBase64() error = %v", err)
}
if key[0] != 1 || key[31] != 32 {
t.Fatalf("ParseKeyBase64() returned unexpected bytes: first=%d last=%d", key[0], key[31])
}
if _, err := ParseKeyBase64("not-base64"); err == nil {
t.Fatal("ParseKeyBase64() accepted invalid base64")
}
if _, err := ParseKeyBase64(base64.StdEncoding.EncodeToString([]byte{1, 2, 3})); err == nil {
t.Fatal("ParseKeyBase64() accepted a short key")
}
}
+121
View File
@@ -0,0 +1,121 @@
// Package clientwg embeds wireguard-go around the single RemLink Wintun.
package clientwg
import (
"errors"
"os"
"sync"
"sync/atomic"
"golang.zx2c4.com/wireguard/tun"
)
var _ tun.Device = (*MuxTun)(nil)
// MuxTun is the deliberately thin Phase 1 wrapper around the real Wintun.
// Later phases add Read-side CIDR classification without changing this surface.
type MuxTun struct {
base tun.Device
router atomic.Pointer[PacketMux]
writeMu sync.Mutex
closeOnce sync.Once
closeErr error
}
// NewMuxTun wraps a live TUN device without changing its semantics.
func NewMuxTun(base tun.Device) *MuxTun {
return &MuxTun{base: base}
}
func (m *MuxTun) File() *os.File {
return m.base.File()
}
func (m *MuxTun) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
router := m.router.Load()
if router == nil {
return m.base.Read(bufs, sizes, offset)
}
if len(bufs) == 0 || len(sizes) < len(bufs) || offset < 0 {
return 0, errors.New("invalid MuxTun read buffers")
}
for {
count, readErr := m.base.Read(bufs, sizes, offset)
if count < 0 || count > len(bufs) {
return 0, errors.New("base TUN returned invalid batch count")
}
overlayCount := 0
for index := 0; index < count; index++ {
size := sizes[index]
if size < 0 || offset+size > len(bufs[index]) {
return 0, errors.New("base TUN returned invalid packet size")
}
packet := bufs[index][offset : offset+size]
class, sink := router.route(packet)
switch class {
case PacketOverlay:
if overlayCount != index {
if offset+size > len(bufs[overlayCount]) {
return 0, errors.New("destination batch buffer is too small")
}
copy(bufs[overlayCount][offset:offset+size], packet)
}
sizes[overlayCount] = size
overlayCount++
case PacketRemote:
if sink != nil {
owned := append([]byte(nil), packet...)
if !sink.Enqueue(owned) {
router.recordDrop(DropEvent{Reason: DropRemoteQueueFull})
}
}
}
}
if overlayCount > 0 || readErr != nil {
return overlayCount, readErr
}
// A batch containing only Remote/dropped packets is consumed here. Read
// again instead of returning 0,nil to wireguard-go and spinning it.
}
}
func (m *MuxTun) Write(bufs [][]byte, offset int) (int, error) {
m.writeMu.Lock()
defer m.writeMu.Unlock()
return m.base.Write(bufs, offset)
}
// SetPacketMux enables or replaces Engineer outbound classification. A nil
// value restores Phase 1 passthrough behavior.
func (m *MuxTun) SetPacketMux(router *PacketMux) { m.router.Store(router) }
// InjectInbound serializes Site Session replies with wireguard-go writes.
func (m *MuxTun) InjectInbound(packet []byte) error {
m.writeMu.Lock()
defer m.writeMu.Unlock()
_, err := m.base.Write([][]byte{packet}, 0)
return err
}
func (m *MuxTun) MTU() (int, error) {
return m.base.MTU()
}
func (m *MuxTun) Name() (string, error) {
return m.base.Name()
}
func (m *MuxTun) Events() <-chan tun.Event {
return m.base.Events()
}
func (m *MuxTun) Close() error {
m.closeOnce.Do(func() {
m.closeErr = m.base.Close()
})
return m.closeErr
}
func (m *MuxTun) BatchSize() int {
return m.base.BatchSize()
}
+96
View File
@@ -0,0 +1,96 @@
package clientwg
import (
"errors"
"os"
"testing"
"golang.zx2c4.com/wireguard/tun"
)
func TestMuxTunProxiesBaseSemanticsAndClosesOnce(t *testing.T) {
t.Parallel()
base := newFakeTUN()
mux := NewMuxTun(base)
if name, err := mux.Name(); err != nil || name != "RemLink" {
t.Fatalf("Name() = (%q, %v)", name, err)
}
if mtu, err := mux.MTU(); err != nil || mtu != 1280 {
t.Fatalf("MTU() = (%d, %v)", mtu, err)
}
if mux.BatchSize() != 1 {
t.Fatalf("BatchSize() = %d, want 1", mux.BatchSize())
}
if mux.Events() != base.events {
t.Fatal("Events() did not return the base channel")
}
buffer := make([]byte, 32)
sizes := make([]int, 1)
n, err := mux.Read([][]byte{buffer}, sizes, 4)
if err != nil || n != 1 || sizes[0] != 3 || string(buffer[4:7]) != "out" {
t.Fatalf("Read() = n=%d sizes=%v data=%q err=%v", n, sizes, buffer[4:7], err)
}
n, err = mux.Write([][]byte{[]byte("xxxxin")}, 4)
if err != nil || n != 1 || string(base.written) != "in" {
t.Fatalf("Write() = n=%d written=%q err=%v", n, base.written, err)
}
if err := mux.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
if err := mux.Close(); err != nil {
t.Fatalf("second Close() error = %v", err)
}
if base.closeCalls != 1 {
t.Fatalf("base Close() calls = %d, want 1", base.closeCalls)
}
}
func TestMuxTunPreservesBaseErrors(t *testing.T) {
t.Parallel()
want := errors.New("read failed")
base := newFakeTUN()
base.readErr = want
_, err := NewMuxTun(base).Read([][]byte{make([]byte, 8)}, make([]int, 1), 0)
if !errors.Is(err, want) {
t.Fatalf("Read() error = %v, want %v", err, want)
}
}
type fakeTUN struct {
events chan tun.Event
written []byte
readErr error
closeCalls int
}
func newFakeTUN() *fakeTUN {
return &fakeTUN{events: make(chan tun.Event, 1)}
}
func (f *fakeTUN) File() *os.File { return nil }
func (f *fakeTUN) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
if f.readErr != nil {
return 0, f.readErr
}
sizes[0] = copy(bufs[0][offset:], []byte("out"))
return 1, nil
}
func (f *fakeTUN) Write(bufs [][]byte, offset int) (int, error) {
f.written = append(f.written[:0], bufs[0][offset:]...)
return len(bufs), nil
}
func (f *fakeTUN) MTU() (int, error) { return 1280, nil }
func (f *fakeTUN) Name() (string, error) { return "RemLink", nil }
func (f *fakeTUN) Events() <-chan tun.Event {
return f.events
}
func (f *fakeTUN) Close() error {
f.closeCalls++
return nil
}
func (f *fakeTUN) BatchSize() int { return 1 }
+134
View File
@@ -0,0 +1,134 @@
package clientwg
import (
"net/netip"
"sync/atomic"
)
// PacketClass is the only Engineer outbound routing decision.
type PacketClass uint8
const (
PacketDrop PacketClass = iota
PacketOverlay
PacketRemote
)
type DropReason string
const (
DropInvalidIPv4 DropReason = "INVALID_IPV4"
DropUnmanagedDestination DropReason = "UNMANAGED_DESTINATION"
DropRemoteQueueFull DropReason = "REMOTE_QUEUE_FULL"
)
type DropEvent struct {
Reason DropReason
Destination netip.Addr
}
type dropHandler struct{ callback func(DropEvent) }
// RemoteSink accepts an owned copy without performing network I/O in Read.
type RemoteSink interface {
Enqueue(packet []byte) bool
}
type routeSnapshot struct {
overlay netip.Prefix
remote []netip.Prefix
sink RemoteSink
}
// PacketMux classifies raw IPv4 packets using an atomically replaced snapshot.
type PacketMux struct {
routes atomic.Pointer[routeSnapshot]
onDrop atomic.Pointer[dropHandler]
overlayPackets atomic.Uint64
remotePackets atomic.Uint64
remoteBytes atomic.Uint64
droppedPackets atomic.Uint64
}
func NewPacketMux(overlay netip.Prefix, remote []netip.Prefix, sink RemoteSink) *PacketMux {
mux := &PacketMux{}
mux.SetRoutes(overlay, remote, sink)
return mux
}
// SetRoutes replaces the Active Session CIDRs as one consistent snapshot.
func (m *PacketMux) SetRoutes(overlay netip.Prefix, remote []netip.Prefix, sink RemoteSink) {
copyOfRemote := append([]netip.Prefix(nil), remote...)
m.routes.Store(&routeSnapshot{overlay: overlay.Masked(), remote: copyOfRemote, sink: sink})
}
// SetDropHandler installs an optional metadata-only callback for rate-limited
// logging. The callback never receives packet bytes.
func (m *PacketMux) SetDropHandler(callback func(DropEvent)) {
if callback == nil {
m.onDrop.Store(nil)
return
}
m.onDrop.Store(&dropHandler{callback: callback})
}
// Classify validates enough of IPv4 to safely read Destination Address.
func (m *PacketMux) Classify(packet []byte) PacketClass {
routes := m.routes.Load()
class, _, _ := classifySnapshot(routes, packet)
return class
}
func classifySnapshot(routes *routeSnapshot, packet []byte) (PacketClass, RemoteSink, DropEvent) {
if routes == nil || len(packet) < 20 || packet[0]>>4 != 4 {
return PacketDrop, nil, DropEvent{Reason: DropInvalidIPv4}
}
headerLength := int(packet[0]&0x0F) * 4
totalLength := int(packet[2])<<8 | int(packet[3])
if headerLength < 20 || headerLength > len(packet) || totalLength < headerLength || totalLength != len(packet) {
return PacketDrop, nil, DropEvent{Reason: DropInvalidIPv4}
}
destination := netip.AddrFrom4([4]byte{packet[16], packet[17], packet[18], packet[19]})
if routes.overlay.IsValid() && routes.overlay.Contains(destination) {
return PacketOverlay, nil, DropEvent{}
}
for _, prefix := range routes.remote {
if prefix.Contains(destination) {
return PacketRemote, routes.sink, DropEvent{}
}
}
return PacketDrop, nil, DropEvent{Reason: DropUnmanagedDestination, Destination: destination}
}
// Counters returns cumulative classification outcomes.
func (m *PacketMux) Counters() (overlay, remote, dropped uint64) {
return m.overlayPackets.Load(), m.remotePackets.Load(), m.droppedPackets.Load()
}
// RemoteCounters is the Engineer-view Upload source of truth: bytes and
// packets are counted when PacketMux intercepts them, before queueing or UDP
// host I/O, exactly as required by the Session statistics contract.
func (m *PacketMux) RemoteCounters() (bytes, packets uint64) {
return m.remoteBytes.Load(), m.remotePackets.Load()
}
func (m *PacketMux) route(packet []byte) (PacketClass, RemoteSink) {
class, sink, drop := classifySnapshot(m.routes.Load(), packet)
switch class {
case PacketOverlay:
m.overlayPackets.Add(1)
case PacketRemote:
m.remotePackets.Add(1)
m.remoteBytes.Add(uint64(len(packet)))
default:
m.recordDrop(drop)
}
return class, sink
}
func (m *PacketMux) recordDrop(event DropEvent) {
m.droppedPackets.Add(1)
if handler := m.onDrop.Load(); handler != nil {
handler.callback(event)
}
}
+189
View File
@@ -0,0 +1,189 @@
package clientwg
import (
"net/netip"
"os"
"testing"
"golang.zx2c4.com/wireguard/tun"
)
type collectingSink struct{ packets [][]byte }
func (s *collectingSink) Enqueue(packet []byte) bool {
s.packets = append(s.packets, packet)
return true
}
type rejectingSink struct{}
func (rejectingSink) Enqueue([]byte) bool { return false }
func TestPacketMuxClassifiesIPv4Destination(t *testing.T) {
mux := NewPacketMux(
netip.MustParsePrefix("10.88.0.0/16"),
[]netip.Prefix{netip.MustParsePrefix("192.168.13.0/24")},
nil,
)
for _, test := range []struct {
packet []byte
want PacketClass
}{
{ipv4Packet("10.88.0.3"), PacketOverlay},
{ipv4Packet("192.168.13.10"), PacketRemote},
{ipv4Packet("8.8.8.8"), PacketDrop},
{[]byte{0x60, 0, 0, 20}, PacketDrop},
{[]byte{0x45, 0, 0, 40}, PacketDrop},
} {
if got := mux.Classify(test.packet); got != test.want {
t.Errorf("Classify(%v) = %v, want %v", test.packet, got, test.want)
}
}
}
func TestMuxTunConsumesRemoteOnlyBatchUntilOverlay(t *testing.T) {
base := &sequenceTUN{
events: make(chan tun.Event),
batches: [][][]byte{
{ipv4Packet("192.168.13.10")},
{ipv4Packet("10.88.0.3")},
},
}
sink := &collectingSink{}
router := NewPacketMux(netip.MustParsePrefix("10.88.0.0/16"),
[]netip.Prefix{netip.MustParsePrefix("192.168.13.0/24")}, sink)
mux := NewMuxTun(base)
mux.SetPacketMux(router)
buffer := make([]byte, 128)
sizes := make([]int, 1)
count, err := mux.Read([][]byte{buffer}, sizes, 4)
if err != nil {
t.Fatal(err)
}
if count != 1 || base.reads != 2 || len(sink.packets) != 1 {
t.Fatalf("count=%d reads=%d remote=%d", count, base.reads, len(sink.packets))
}
if destination(buffer[4:4+sizes[0]]) != netip.MustParseAddr("10.88.0.3") {
t.Fatalf("returned packet destination = %s", destination(buffer[4:4+sizes[0]]))
}
if destination(sink.packets[0]) != netip.MustParseAddr("192.168.13.10") {
t.Fatalf("queued packet destination = %s", destination(sink.packets[0]))
}
base.batches[0][0][16] = 1
if destination(sink.packets[0]) != netip.MustParseAddr("192.168.13.10") {
t.Fatal("RemoteSink packet aliases the base TUN buffer")
}
}
func TestMuxTunCompactsMixedBatch(t *testing.T) {
base := &sequenceTUN{
events: make(chan tun.Event),
batches: [][][]byte{{
ipv4Packet("192.168.13.10"), ipv4Packet("10.88.0.4"), ipv4Packet("8.8.8.8"), ipv4Packet("10.88.0.5"),
}},
}
sink := &collectingSink{}
router := NewPacketMux(netip.MustParsePrefix("10.88.0.0/16"),
[]netip.Prefix{netip.MustParsePrefix("192.168.13.0/24")}, sink)
mux := NewMuxTun(base)
mux.SetPacketMux(router)
bufs := [][]byte{make([]byte, 64), make([]byte, 64), make([]byte, 64), make([]byte, 64)}
sizes := make([]int, len(bufs))
count, err := mux.Read(bufs, sizes, 0)
if err != nil {
t.Fatal(err)
}
if count != 2 || destination(bufs[0][:sizes[0]]).String() != "10.88.0.4" || destination(bufs[1][:sizes[1]]).String() != "10.88.0.5" {
t.Fatalf("compacted count=%d destinations=%s,%s", count, destination(bufs[0][:sizes[0]]), destination(bufs[1][:sizes[1]]))
}
overlay, remote, dropped := router.Counters()
if overlay != 2 || remote != 1 || dropped != 1 {
t.Fatalf("counters overlay=%d remote=%d dropped=%d", overlay, remote, dropped)
}
}
func TestPacketMuxDropCallbackContainsMetadataOnly(t *testing.T) {
mux := NewPacketMux(netip.MustParsePrefix("10.88.0.0/16"), nil, nil)
var events []DropEvent
mux.SetDropHandler(func(event DropEvent) { events = append(events, event) })
mux.route(ipv4Packet("8.8.8.8"))
mux.route([]byte{0x60})
if len(events) != 2 || events[0].Reason != DropUnmanagedDestination || events[0].Destination.String() != "8.8.8.8" || events[1].Reason != DropInvalidIPv4 {
t.Fatalf("drop events = %+v", events)
}
}
func TestPacketMuxCountsInterceptedUploadAndReportsQueueDrop(t *testing.T) {
base := &sequenceTUN{
events: make(chan tun.Event),
batches: [][][]byte{{ipv4Packet("192.168.13.10")}, {ipv4Packet("10.88.0.3")}},
}
router := NewPacketMux(netip.MustParsePrefix("10.88.0.0/16"),
[]netip.Prefix{netip.MustParsePrefix("192.168.13.0/24")}, rejectingSink{})
var drops []DropEvent
router.SetDropHandler(func(event DropEvent) { drops = append(drops, event) })
mux := NewMuxTun(base)
mux.SetPacketMux(router)
buffer := make([]byte, 64)
sizes := make([]int, 1)
if _, err := mux.Read([][]byte{buffer}, sizes, 0); err != nil {
t.Fatal(err)
}
bytes, packets := router.RemoteCounters()
_, remote, dropped := router.Counters()
if bytes != 20 || packets != 1 || remote != 1 || dropped != 1 {
t.Fatalf("Upload/drop counters bytes=%d packets=%d remote=%d dropped=%d", bytes, packets, remote, dropped)
}
if len(drops) != 1 || drops[0].Reason != DropRemoteQueueFull {
t.Fatalf("queue drop events = %+v", drops)
}
}
func TestPacketMuxRejectsTrailingBytesBeyondIPv4TotalLength(t *testing.T) {
packet := append(ipv4Packet("192.168.13.10"), 0)
mux := NewPacketMux(netip.MustParsePrefix("10.88.0.0/16"),
[]netip.Prefix{netip.MustParsePrefix("192.168.13.0/24")}, nil)
if got := mux.Classify(packet); got != PacketDrop {
t.Fatalf("Classify packet with trailing bytes = %v, want PacketDrop", got)
}
}
func ipv4Packet(destinationText string) []byte {
packet := make([]byte, 20)
packet[0] = 0x45
packet[2] = 0
packet[3] = 20
packet[12] = 10
packet[13] = 88
packet[14] = 0
packet[15] = 2
destination := netip.MustParseAddr(destinationText).As4()
copy(packet[16:20], destination[:])
return packet
}
func destination(packet []byte) netip.Addr {
return netip.AddrFrom4([4]byte{packet[16], packet[17], packet[18], packet[19]})
}
type sequenceTUN struct {
events chan tun.Event
batches [][][]byte
reads int
}
func (t *sequenceTUN) File() *os.File { return nil }
func (t *sequenceTUN) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
batch := t.batches[t.reads]
t.reads++
for index, packet := range batch {
sizes[index] = copy(bufs[index][offset:], packet)
}
return len(batch), nil
}
func (t *sequenceTUN) Write(bufs [][]byte, offset int) (int, error) { return len(bufs), nil }
func (t *sequenceTUN) MTU() (int, error) { return 1280, nil }
func (t *sequenceTUN) Name() (string, error) { return "RemLink", nil }
func (t *sequenceTUN) Events() <-chan tun.Event { return t.events }
func (t *sequenceTUN) Close() error { return nil }
func (t *sequenceTUN) BatchSize() int { return 4 }