Files
ihasvpn/internal/wg/mock.go
T
jcoffey-dev 02e7993c87 Rename the project to ihasvpn
WGX shares its name with several other WireGuard tools, so the project
becomes ihasvpn, alongside ihasmail.

- Module github.com/Coffey-Labs/ihasvpn, command cmd/ihasvpn, image
  ghcr.io/coffey-labs/ihasvpn.
- Environment variables move from WGX_* to IHASVPN_*. The default database
  is ihasvpn.db, the nftables table is `ihasvpn`, metrics are ihasvpn_*, and
  the session cookie and theme key are renamed, so existing sessions end.
- The mark is the ihasmail cat peeking over the edge of a shield, drawn as
  a vector. docs/brand/generate.py builds the mark, mono mark, wordmarks,
  social card, favicons and app icons from that one drawing.
- The console takes ihasmail's palette: the ihasmail.org teal-navy for dark,
  its contrast-checked light tiers with the site's light accent, received
  traffic in the cat's orange and sent in teal. The wordmark weight and
  font stack follow ihasmail.org.
- Detail values wrap at spaces before breaking inside an address, so an
  IPv6 tunnel address no longer splits mid-number.
- The README history note about the earlier WGX installer is gone with the
  name it explained. Screenshots retaken.
2026-09-12 23:48:36 -07:00

175 lines
4.2 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 `IHASVPN_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()
}
}