Preview — /opt/splice-src/coordinator/main.go

/opt/splice-src/coordinator/main.go

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net"
	"net/http"
	"os/exec"
	"strings"
	"sync"
	"time"
)

type RegisterRequest struct {
	JoinKey    string `json:"join_key"`
	Name       string `json:"name"`
	PublicKey  string `json:"public_key"`
	ListenPort int    `json:"listen_port"`
	Profile    string `json:"profile"`
	Advertise  string `json:"advertise"`
	Observed   string `json:"observed"`
}
type Peer struct {
	Name       string   `json:"name"`
	PublicKey  string   `json:"public_key"`
	OverlayIP  string   `json:"overlay_ip"`
	Endpoints  []string `json:"endpoints"`
	AllowedIPs []string `json:"allowed_ips"`
}
type HubInfo struct {
	PublicKey  string `json:"public_key"`
	Endpoint   string `json:"endpoint"`
	AllowedIPs string `json:"allowed_ips"`
}
type RegisterResponse struct {
	OverlayIP string   `json:"overlay_ip"`
	Network   string   `json:"network"`
	Hub       *HubInfo `json:"hub,omitempty"`
	Error     string   `json:"error,omitempty"`
}
type MeshSnapshot struct {
	Self          Peer     `json:"self"`
	Peers         []Peer   `json:"peers"`
	Hub           *HubInfo `json:"hub,omitempty"`
	HubAllowedIPs []string `json:"hub_allowed_ips"`
	Epoch         int64    `json:"epoch"`
	Error         string   `json:"error,omitempty"`
}

type node struct {
	req       RegisterRequest
	overlay   net.IP
	advertise *net.IPNet
	endpoint  string
	lastSeen  time.Time
}
type network struct {
	key      string
	cidr     *net.IPNet
	nextHost uint32
	nodes    map[string]*node
	epoch    int64
}

var (
	mu          sync.Mutex
	networks    = map[string]*network{}
	nextNet     = 0
	reap        = 90 * time.Second
	hubIface    string
	hubEndpoint string
	hubPub      string
)

func getOrCreateNetwork(key string) *network {
	if n, ok := networks[key]; ok {
		return n
	}
	if nextNet >= 64 {
		return nil
	}
	cidr := &net.IPNet{IP: net.IPv4(100, byte(64+nextNet), 0, 0).To4(), Mask: net.CIDRMask(16, 32)}
	nextNet++
	n := &network{key: key, cidr: cidr, nextHost: 2, nodes: map[string]*node{}}
	networks[key] = n
	return n
}

func (n *network) ipFor(off uint32) net.IP {
	ip := make(net.IP, 4)
	copy(ip, n.cidr.IP.To4())
	ip[2] = byte(off >> 8)
	ip[3] = byte(off & 0xff)
	return ip
}

func overlap(a, b *net.IPNet) bool { return a.Contains(b.IP) || b.Contains(a.IP) }

func writeJSON(w http.ResponseWriter, code int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	_ = json.NewEncoder(w).Encode(v)
}

func hubInfoFor(n *network) *HubInfo {
	if hubPub == "" {
		return nil
	}
	return &HubInfo{PublicKey: hubPub, Endpoint: hubEndpoint, AllowedIPs: n.cidr.String()}
}
func hubAddPeer(pub, ipcidr string) {
	if hubPub == "" {
		return
	}
	if err := exec.Command("wg", "set", hubIface, "peer", pub, "allowed-ips", ipcidr).Run(); err != nil {
		log.Printf("hub add peer: %v", err)
	}
}
func hubDelPeer(pub string) {
	if hubPub == "" {
		return
	}
	_ = exec.Command("wg", "set", hubIface, "peer", pub, "remove").Run()
}

func peerOf(nd *node) Peer {
	allowed := []string{nd.overlay.String() + "/32"}
	if nd.advertise != nil {
		allowed = append(allowed, nd.advertise.String())
	}
	var eps []string
	if nd.endpoint != "" {
		eps = []string{nd.endpoint}
	}
	return Peer{Name: nd.req.Name, PublicKey: nd.req.PublicKey, OverlayIP: nd.overlay.String() + "/32", Endpoints: eps, AllowedIPs: allowed}
}

func handleRegister(w http.ResponseWriter, r *http.Request) {
	var req RegisterRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, 400, RegisterResponse{Error: "bad request"})
		return
	}
	if strings.TrimSpace(req.JoinKey) == "" || strings.TrimSpace(req.PublicKey) == "" {
		writeJSON(w, 401, RegisterResponse{Error: "join_key and public_key are required"})
		return
	}
	var adv *net.IPNet
	if req.Advertise != "" {
		_, cidr, err := net.ParseCIDR(req.Advertise)
		if err != nil {
			writeJSON(w, 400, RegisterResponse{Error: "advertise is not a valid CIDR"})
			return
		}
		adv = cidr
	}
	mu.Lock()
	defer mu.Unlock()
	n := getOrCreateNetwork(req.JoinKey)
	if n == nil {
		writeJSON(w, 503, RegisterResponse{Error: "coordinator at network capacity"})
		return
	}
	if adv != nil {
		if overlap(adv, n.cidr) {
			writeJSON(w, 409, RegisterResponse{Error: fmt.Sprintf("advertised %s overlaps overlay range %s", req.Advertise, n.cidr)})
			return
		}
		for _, other := range n.nodes {
			if other.req.PublicKey == req.PublicKey || other.advertise == nil {
				continue
			}
			if overlap(adv, other.advertise) {
				writeJSON(w, 409, RegisterResponse{Error: fmt.Sprintf("can't bridge: %s already in use by %q - renumber one side, or drop -advertise", req.Advertise, other.req.Name)})
				return
			}
		}
	}
	endpoint := req.Observed
	if endpoint == "" {
		if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil && req.ListenPort > 0 {
			endpoint = net.JoinHostPort(host, fmt.Sprintf("%d", req.ListenPort))
		}
	}
	nd, exists := n.nodes[req.PublicKey]
	if !exists {
		nd = &node{overlay: n.ipFor(n.nextHost)}
		n.nextHost++
		n.nodes[req.PublicKey] = nd
	}
	nd.req, nd.advertise, nd.endpoint, nd.lastSeen = req, adv, endpoint, time.Now()
	n.epoch = time.Now().Unix()
	hubAddPeer(req.PublicKey, nd.overlay.String()+"/32")
	disp := n.key
	if len(disp) > 8 {
		disp = disp[:8]
	}
	log.Printf("join: net %s (%s) <- %s -> %s", disp, n.cidr, req.Name, nd.overlay)
	writeJSON(w, 200, RegisterResponse{OverlayIP: nd.overlay.String() + "/32", Network: n.cidr.String(), Hub: hubInfoFor(n)})
}

func handlePoll(w http.ResponseWriter, r *http.Request) {
	key := r.URL.Query().Get("key")
	pub := r.URL.Query().Get("pubkey")
	mu.Lock()
	defer mu.Unlock()
	n, ok := networks[key]
	if !ok {
		writeJSON(w, 404, MeshSnapshot{Error: "unknown network"})
		return
	}
	self, ok := n.nodes[pub]
	if !ok {
		writeJSON(w, 404, MeshSnapshot{Error: "not registered"})
		return
	}
	self.lastSeen = time.Now()
	hubAddPeer(self.req.PublicKey, self.overlay.String()+"/32")
	snap := MeshSnapshot{Self: peerOf(self), Hub: hubInfoFor(n), HubAllowedIPs: []string{n.cidr.String()}, Epoch: n.epoch}
	for _, nd := range n.nodes {
		if nd.req.PublicKey == pub {
			continue
		}
		snap.Peers = append(snap.Peers, peerOf(nd))
	}
	writeJSON(w, 200, snap)
}

func reaper() {
	for range time.Tick(30 * time.Second) {
		mu.Lock()
		for _, n := range networks {
			for pk, nd := range n.nodes {
				if time.Since(nd.lastSeen) > reap {
					hubDelPeer(pk)
					delete(n.nodes, pk)
					n.epoch = time.Now().Unix()
				}
			}
		}
		mu.Unlock()
	}
}

func main() {
	listen := flag.String("listen", "0.0.0.0:7777", "listen address")
	hi := flag.String("hub-iface", "wgsplice", "hub WireGuard interface")
	he := flag.String("hub-endpoint", "", "hub public endpoint host:port")
	flag.Parse()
	hubIface = *hi
	hubEndpoint = *he
	if out, err := exec.Command("wg", "show", hubIface, "public-key").Output(); err == nil {
		hubPub = strings.TrimSpace(string(out))
		log.Printf("hub %s pubkey=%s endpoint=%s", hubIface, hubPub, hubEndpoint)
	} else {
		log.Printf("WARNING: hub %q unavailable (%v); relay disabled", hubIface, err)
	}
	http.HandleFunc("/register", handleRegister)
	http.HandleFunc("/poll", handlePoll)
	http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) })
	go reaper()
	log.Printf("splice-coordinator listening on %s", *listen)
	log.Fatal(http.ListenAndServe(*listen, nil))
}