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>
221 lines
5.8 KiB
Go
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})
|
|
}
|