Files
VeillaxandClaude Sonnet 5.5 2259580adf Add the shared packages for the sharks stack
model, tunnel, wg, authtoken, netguard, docker and dotenv, split out of the
original single repository so Hammerhead, Blacktip and Bamboo can live in
their own repos and import them. README extended; LICENSE unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 02:41:44 -06:00

221 lines
5.8 KiB
Go

// Package wg wraps wireguard-go with a userspace network stack (gVisor netstack),
// the same approach Newt uses: no root, no kernel interface, works on Windows.
//
// A Device has one tunnel address. Connections made with DialContext and
// listeners made with ListenTCP/ListenUDP live entirely inside the tunnel.
package wg
import (
"context"
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net"
"net/netip"
"sort"
"strconv"
"strings"
"sync"
"time"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun/netstack"
)
const (
// MTU is conservative so the tunnel works over typical consumer links.
MTU = 1280
// Keepalive keeps NAT mappings open for peers behind NAT.
Keepalive = 25
)
// GenerateKey returns a new base64-encoded private key.
func GenerateKey() (string, error) {
k, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(k.Bytes()), nil
}
// PublicKey derives the base64 public key from a base64 private key.
func PublicKey(priv string) (string, error) {
raw, err := base64.StdEncoding.DecodeString(priv)
if err != nil || len(raw) != 32 {
return "", errors.New("invalid WireGuard private key")
}
k, err := ecdh.X25519().NewPrivateKey(raw)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(k.PublicKey().Bytes()), nil
}
func toHex(b64 string) (string, error) {
raw, err := base64.StdEncoding.DecodeString(b64)
if err != nil || len(raw) != 32 {
return "", errors.New("invalid WireGuard key")
}
return hex.EncodeToString(raw), nil
}
// Peer is one remote end of the tunnel.
type Peer struct {
PublicKey string // base64
AllowedIP string // the peer's tunnel address, e.g. 100.89.0.7
Endpoint string // host:port, or "" when the peer connects to us
}
func (p Peer) sig() string { return p.PublicKey + "|" + p.AllowedIP + "|" + p.Endpoint }
type Device struct {
dev *device.Device
net *netstack.Net
ip netip.Addr
mu sync.Mutex
peers map[string]Peer // by public key
}
// New brings up a tunnel endpoint at addr. listenPort 0 picks a random UDP port.
func New(privateKey string, addr netip.Addr, listenPort int) (*Device, error) {
privHex, err := toHex(privateKey)
if err != nil {
return nil, err
}
tdev, tnet, err := netstack.CreateNetTUN([]netip.Addr{addr}, nil, MTU)
if err != nil {
return nil, err
}
dev := device.NewDevice(tdev, conn.NewDefaultBind(), device.NewLogger(device.LogLevelError, "wireguard: "))
cfg := "private_key=" + privHex + "\n"
if listenPort > 0 {
cfg += "listen_port=" + strconv.Itoa(listenPort) + "\n"
}
if err := dev.IpcSet(cfg); err != nil {
dev.Close()
return nil, fmt.Errorf("configure wireguard: %w", err)
}
if err := dev.Up(); err != nil {
dev.Close()
return nil, err
}
return &Device{dev: dev, net: tnet, ip: addr, peers: map[string]Peer{}}, nil
}
func (d *Device) Addr() netip.Addr { return d.ip }
func (d *Device) Close() { d.dev.Close() }
// resolve turns host:port into the ip:port form wireguard-go requires.
func resolve(endpoint string) (string, error) {
if endpoint == "" {
return "", nil
}
a, err := net.ResolveUDPAddr("udp", endpoint)
if err != nil {
return "", err
}
return a.String(), nil
}
// SetPeers makes the device's peer list match want. Peers that are unchanged
// keep their sessions, so adding one site never interrupts the others.
// Peers whose endpoint can't be resolved are skipped (and returned in err).
func (d *Device) SetPeers(want []Peer) error {
d.mu.Lock()
defer d.mu.Unlock()
var cfg strings.Builder
var bad []string
next := map[string]Peer{}
for _, p := range want {
ep, err := resolve(p.Endpoint)
if err != nil {
bad = append(bad, fmt.Sprintf("%s: %v", p.Endpoint, err))
if old, ok := d.peers[p.PublicKey]; ok {
next[p.PublicKey] = old // keep what works
}
continue
}
p.Endpoint = ep
next[p.PublicKey] = p
}
for key := range d.peers {
if _, keep := next[key]; !keep {
h, _ := toHex(key)
cfg.WriteString("public_key=" + h + "\nremove=true\n")
}
}
keys := make([]string, 0, len(next))
for k := range next {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
p := next[key]
if old, ok := d.peers[key]; ok && old.sig() == p.sig() {
continue
}
h, err := toHex(p.PublicKey)
if err != nil {
continue
}
cfg.WriteString("public_key=" + h + "\nreplace_allowed_ips=true\nallowed_ip=" + p.AllowedIP + "/32\n")
if p.Endpoint != "" {
cfg.WriteString("endpoint=" + p.Endpoint + "\npersistent_keepalive_interval=" + strconv.Itoa(Keepalive) + "\n")
}
}
if cfg.Len() > 0 {
if err := d.dev.IpcSet(cfg.String()); err != nil {
return err
}
}
d.peers = next
if len(bad) > 0 {
return errors.New(strings.Join(bad, "; "))
}
return nil
}
// Handshakes returns the time of the last completed handshake per peer public key.
func (d *Device) Handshakes() map[string]time.Time {
out := map[string]time.Time{}
state, err := d.dev.IpcGet()
if err != nil {
return out
}
var cur string
for _, line := range strings.Split(state, "\n") {
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
switch k {
case "public_key":
raw, _ := hex.DecodeString(v)
cur = base64.StdEncoding.EncodeToString(raw)
case "last_handshake_time_sec":
if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > 0 && cur != "" {
out[cur] = time.Unix(n, 0)
}
}
}
return out
}
func (d *Device) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
return d.net.DialContext(ctx, network, addr)
}
func (d *Device) ListenTCP(port int) (net.Listener, error) {
return d.net.ListenTCP(&net.TCPAddr{Port: port})
}
func (d *Device) ListenUDP(port int) (net.PacketConn, error) {
return d.net.ListenUDP(&net.UDPAddr{Port: port})
}