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>
This commit is contained in:
1 parent
c7c2ae8f8d
commit
2259580adf
16 files changed
+1359
-1
No files matched your search
@@ -0,0 +1,220 @@
|
||||
// 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})
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package wg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func freeUDP(t *testing.T) int {
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pc.Close()
|
||||
return pc.LocalAddr().(*net.UDPAddr).Port
|
||||
}
|
||||
|
||||
func TestTunnelCarriesTCPAndUDP(t *testing.T) {
|
||||
ka, _ := GenerateKey()
|
||||
kb, _ := GenerateKey()
|
||||
pa, _ := PublicKey(ka)
|
||||
pb, _ := PublicKey(kb)
|
||||
portA := freeUDP(t)
|
||||
|
||||
a, err := New(ka, netip.MustParseAddr("100.89.0.1"), portA) // the "edge": has a fixed port
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer a.Close()
|
||||
b, err := New(kb, netip.MustParseAddr("100.89.0.2"), 0) // the "site": behind NAT, dials out
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer b.Close()
|
||||
if err := a.SetPeers([]Peer{{PublicKey: pb, AllowedIP: "100.89.0.2"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := b.SetPeers([]Peer{{PublicKey: pa, AllowedIP: "100.89.0.1", Endpoint: net.JoinHostPort("127.0.0.1", strconv.Itoa(portA))}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// TCP: site listens inside the tunnel, edge dials it
|
||||
ln, err := b.ListenTCP(9111)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() { defer c.Close(); io.Copy(c, c) }()
|
||||
}
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
c, err := a.DialContext(ctx, "tcp", "100.89.0.2:9111")
|
||||
if err != nil {
|
||||
t.Fatalf("dial through tunnel: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
c.Write([]byte("hello"))
|
||||
buf := make([]byte, 5)
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := io.ReadFull(c, buf); err != nil || string(buf) != "hello" {
|
||||
t.Fatalf("echo: %q %v", buf, err)
|
||||
}
|
||||
|
||||
// UDP both ways
|
||||
pc, err := b.ListenUDP(9112)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pc.Close()
|
||||
go func() {
|
||||
p := make([]byte, 2048)
|
||||
for {
|
||||
n, from, err := pc.ReadFrom(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pc.WriteTo(append([]byte("re:"), p[:n]...), from)
|
||||
}
|
||||
}()
|
||||
u, err := a.DialContext(ctx, "udp", "100.89.0.2:9112")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer u.Close()
|
||||
u.Write([]byte("ping"))
|
||||
u.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
rb := make([]byte, 64)
|
||||
n, err := u.Read(rb)
|
||||
if err != nil || string(rb[:n]) != "re:ping" {
|
||||
t.Fatalf("udp: %q %v", rb[:n], err)
|
||||
}
|
||||
|
||||
// the site shows a recent handshake with the edge
|
||||
if hs := b.Handshakes(); time.Since(hs[pa]) > time.Minute {
|
||||
t.Errorf("no handshake recorded: %v", hs)
|
||||
}
|
||||
|
||||
// a second peer can be added and removed without disturbing the first
|
||||
kc, _ := GenerateKey()
|
||||
pcKey, _ := PublicKey(kc)
|
||||
if err := a.SetPeers([]Peer{{PublicKey: pb, AllowedIP: "100.89.0.2"}, {PublicKey: pcKey, AllowedIP: "100.89.0.3"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Write([]byte("again"))
|
||||
buf2 := make([]byte, 5)
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := io.ReadFull(c, buf2); err != nil || string(buf2) != "again" {
|
||||
t.Fatalf("existing connection broke when a peer was added: %q %v", buf2, err)
|
||||
}
|
||||
if err := a.SetPeers([]Peer{{PublicKey: pb, AllowedIP: "100.89.0.2"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user