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.
175 lines
4.2 KiB
Go
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()
|
|
}
|
|
}
|