Go backend that drives kernel WireGuard over netlink (wireguard-go as the fallback), nftables NAT with MSS clamping, forwarding and buffer sysctls, SQLite for peers, users, sessions, traffic history and the audit log. React console: dashboard with live rates and usage history, peer management with QR codes and .conf downloads, disconnect, session reset, key rotation, expiry, client-supplied keys, settings, users with admin and viewer roles, two-factor authentication with recovery codes, audit log. Docker image on Alpine with compose files for bridged and host networking, CI and GHCR publish workflows, performance notes.
175 lines
4.1 KiB
Go
175 lines
4.1 KiB
Go
package wg
|
|
|
|
import (
|
|
"context"
|
|
"math/rand/v2"
|
|
"net"
|
|
"net/netip"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Mock is an in-memory data plane. It needs no privileges, so it is what the
|
|
// tests use and what `WGX_BACKEND=mock` gives a developer working on the UI.
|
|
// With Simulate on, peers randomly handshake, move traffic and go quiet so
|
|
// the dashboard has something to show.
|
|
type Mock struct {
|
|
mu sync.Mutex
|
|
name string
|
|
cfg DeviceConfig
|
|
peers map[Key]*mockPeer
|
|
up bool
|
|
Simulate bool
|
|
stop chan struct{}
|
|
}
|
|
|
|
type mockPeer struct {
|
|
cfg PeerConfig
|
|
endpoint *net.UDPAddr
|
|
handshake time.Time
|
|
rx, tx int64
|
|
active bool
|
|
}
|
|
|
|
// NewMock returns an empty mock backend for the named interface.
|
|
func NewMock(name string, simulate bool) *Mock {
|
|
return &Mock{name: name, peers: map[Key]*mockPeer{}, Simulate: simulate}
|
|
}
|
|
|
|
func (m *Mock) Kind() string { return "mock" }
|
|
|
|
func (m *Mock) Up(ctx context.Context, cfg DeviceConfig, addrs []netip.Prefix, mtu int) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.cfg = cfg
|
|
m.up = true
|
|
if m.Simulate && m.stop == nil {
|
|
m.stop = make(chan struct{})
|
|
go m.simulate(m.stop)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Mock) Down(ctx context.Context) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.up = false
|
|
if m.stop != nil {
|
|
close(m.stop)
|
|
m.stop = nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Mock) Device(ctx context.Context) (*DeviceState, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
st := &DeviceState{Name: m.name, PublicKey: m.cfg.PrivateKey.PublicKey(), ListenPort: m.cfg.ListenPort}
|
|
keys := make([]Key, 0, len(m.peers))
|
|
for k := range m.peers {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
|
|
for _, k := range keys {
|
|
p := m.peers[k]
|
|
st.Peers = append(st.Peers, PeerState{
|
|
PublicKey: k,
|
|
Endpoint: p.endpoint,
|
|
LastHandshake: p.handshake,
|
|
ReceiveBytes: p.rx,
|
|
TransmitBytes: p.tx,
|
|
AllowedIPs: append([]netip.Prefix(nil), p.cfg.AllowedIPs...),
|
|
PersistentKeepalive: p.cfg.PersistentKeepalive,
|
|
})
|
|
}
|
|
return st, nil
|
|
}
|
|
|
|
func (m *Mock) SetPeer(ctx context.Context, p PeerConfig) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if existing, ok := m.peers[p.PublicKey]; ok {
|
|
existing.cfg = p
|
|
return nil
|
|
}
|
|
// Half of new peers start out busy so a fresh mock install has
|
|
// something moving on the dashboard straight away.
|
|
m.peers[p.PublicKey] = &mockPeer{cfg: p, active: m.Simulate && rand.IntN(2) == 0}
|
|
return nil
|
|
}
|
|
|
|
func (m *Mock) RemovePeer(ctx context.Context, pub Key) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.peers, pub)
|
|
return nil
|
|
}
|
|
|
|
func (m *Mock) ReplacePeers(ctx context.Context, peers []PeerConfig) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
next := map[Key]*mockPeer{}
|
|
for _, p := range peers {
|
|
if existing, ok := m.peers[p.PublicKey]; ok {
|
|
existing.cfg = p
|
|
next[p.PublicKey] = existing
|
|
} else {
|
|
next[p.PublicKey] = &mockPeer{cfg: p}
|
|
}
|
|
}
|
|
m.peers = next
|
|
return nil
|
|
}
|
|
|
|
func (m *Mock) SetMTU(ctx context.Context, mtu int) error { return nil }
|
|
|
|
// Touch fakes a handshake and some traffic for a peer. Tests use it to make
|
|
// a peer look connected without waiting on the simulator.
|
|
func (m *Mock) Touch(pub Key, rx, tx int64, endpoint string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
p, ok := m.peers[pub]
|
|
if !ok {
|
|
return
|
|
}
|
|
p.handshake = time.Now()
|
|
p.rx += rx
|
|
p.tx += tx
|
|
if endpoint != "" {
|
|
if ap, err := netip.ParseAddrPort(endpoint); err == nil {
|
|
p.endpoint = net.UDPAddrFromAddrPort(ap)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Mock) simulate(stop chan struct{}) {
|
|
t := time.NewTicker(2 * time.Second)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case <-t.C:
|
|
}
|
|
m.mu.Lock()
|
|
for _, p := range m.peers {
|
|
// Peers flip between active and idle a few times an hour.
|
|
if rand.IntN(60) == 0 {
|
|
p.active = !p.active
|
|
}
|
|
if p.active {
|
|
if p.endpoint == nil {
|
|
p.endpoint = &net.UDPAddr{IP: net.IPv4(203, 0, 113, byte(1+rand.IntN(250))), Port: 30000 + rand.IntN(30000)}
|
|
}
|
|
if time.Since(p.handshake) > time.Duration(90+rand.IntN(40))*time.Second {
|
|
p.handshake = time.Now()
|
|
}
|
|
p.rx += int64(rand.IntN(400_000))
|
|
p.tx += int64(rand.IntN(3_000_000))
|
|
}
|
|
}
|
|
m.mu.Unlock()
|
|
}
|
|
}
|