Extract the Central networking functionality from wild-cloud/api into a standalone service. Wild Central manages DNS (dnsmasq), gateway (HAProxy), firewall (nftables), VPN (WireGuard), TLS (certbot), security (CrowdSec), and DDNS — all the network appliance concerns. All Cloud-specific code (instances, clusters, nodes, apps, backups, operations, kubectl/talosctl tooling) has been removed. The API struct and route registration contain only Central endpoints. Tests updated to match the new API signature. Builds and all tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
306 lines
7.1 KiB
Go
306 lines
7.1 KiB
Go
package wireguard
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func newTestManager(t *testing.T) *Manager {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
return NewManager(dir, filepath.Join(dir, "wg0.conf"))
|
|
}
|
|
|
|
// --- Config tests ---
|
|
|
|
func TestGetConfig_Defaults(t *testing.T) {
|
|
m := newTestManager(t)
|
|
cfg, err := m.GetConfig()
|
|
if err != nil {
|
|
t.Fatalf("GetConfig: %v", err)
|
|
}
|
|
if cfg.ListenPort != 51820 {
|
|
t.Errorf("default ListenPort = %d, want 51820", cfg.ListenPort)
|
|
}
|
|
if cfg.Address != "10.8.0.1/24" {
|
|
t.Errorf("default Address = %q, want 10.8.0.1/24", cfg.Address)
|
|
}
|
|
if cfg.Enabled {
|
|
t.Error("default Enabled should be false")
|
|
}
|
|
}
|
|
|
|
func TestSaveAndGetConfig(t *testing.T) {
|
|
m := newTestManager(t)
|
|
cfg := &Config{
|
|
Enabled: true,
|
|
ListenPort: 51820,
|
|
Address: "10.100.0.1/24",
|
|
Endpoint: "vpn.example.com:51820",
|
|
DNS: "10.100.0.1",
|
|
}
|
|
if err := m.SaveConfig(cfg); err != nil {
|
|
t.Fatalf("SaveConfig: %v", err)
|
|
}
|
|
got, err := m.GetConfig()
|
|
if err != nil {
|
|
t.Fatalf("GetConfig: %v", err)
|
|
}
|
|
if got.Address != cfg.Address {
|
|
t.Errorf("Address = %q, want %q", got.Address, cfg.Address)
|
|
}
|
|
if got.Endpoint != cfg.Endpoint {
|
|
t.Errorf("Endpoint = %q, want %q", got.Endpoint, cfg.Endpoint)
|
|
}
|
|
if got.DNS != cfg.DNS {
|
|
t.Errorf("DNS = %q, want %q", got.DNS, cfg.DNS)
|
|
}
|
|
}
|
|
|
|
// --- Peer management tests ---
|
|
|
|
func writeTestSecrets(t *testing.T, m *Manager) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(m.vpnDir(), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content := "privateKey: fakeprivkey123\npublicKey: fakepubkey456\n"
|
|
if err := os.WriteFile(m.secretsFilePath(), []byte(content), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestListPeers_Empty(t *testing.T) {
|
|
m := newTestManager(t)
|
|
peers, err := m.ListPeers()
|
|
if err != nil {
|
|
t.Fatalf("ListPeers: %v", err)
|
|
}
|
|
if len(peers) != 0 {
|
|
t.Errorf("expected 0 peers, got %d", len(peers))
|
|
}
|
|
}
|
|
|
|
func TestSaveAndGetPeer(t *testing.T) {
|
|
m := newTestManager(t)
|
|
p := &Peer{
|
|
ID: "test-id-1",
|
|
Name: "my-phone",
|
|
PublicKey: "pubkey1",
|
|
PrivateKey: "privkey1",
|
|
AllowedIPs: "10.8.0.2/32",
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
if err := m.savePeer(p); err != nil {
|
|
t.Fatalf("savePeer: %v", err)
|
|
}
|
|
|
|
got, err := m.GetPeer("test-id-1")
|
|
if err != nil {
|
|
t.Fatalf("GetPeer: %v", err)
|
|
}
|
|
if got.Name != "my-phone" {
|
|
t.Errorf("Name = %q, want %q", got.Name, "my-phone")
|
|
}
|
|
if got.AllowedIPs != "10.8.0.2/32" {
|
|
t.Errorf("AllowedIPs = %q, want 10.8.0.2/32", got.AllowedIPs)
|
|
}
|
|
}
|
|
|
|
func TestDeletePeer(t *testing.T) {
|
|
m := newTestManager(t)
|
|
p := &Peer{ID: "del-me", Name: "temp", PublicKey: "k", PrivateKey: "k", AllowedIPs: "10.8.0.2/32", CreatedAt: time.Now()}
|
|
if err := m.savePeer(p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.DeletePeer("del-me"); err != nil {
|
|
t.Fatalf("DeletePeer: %v", err)
|
|
}
|
|
if _, err := m.GetPeer("del-me"); err == nil {
|
|
t.Error("expected error getting deleted peer, got nil")
|
|
}
|
|
}
|
|
|
|
func TestDeletePeer_NotFound(t *testing.T) {
|
|
m := newTestManager(t)
|
|
if err := m.DeletePeer("nope"); err == nil {
|
|
t.Error("expected error for missing peer")
|
|
}
|
|
}
|
|
|
|
// --- IP assignment tests ---
|
|
|
|
func TestNextAvailableIP_FirstPeer(t *testing.T) {
|
|
m := newTestManager(t)
|
|
// Server is 10.8.0.1/24; first peer should get 10.8.0.2.
|
|
ip, err := m.nextAvailableIP("10.8.0.1/24")
|
|
if err != nil {
|
|
t.Fatalf("nextAvailableIP: %v", err)
|
|
}
|
|
if ip != "10.8.0.2" {
|
|
t.Errorf("first peer IP = %q, want 10.8.0.2", ip)
|
|
}
|
|
}
|
|
|
|
func TestNextAvailableIP_SkipsTakenIPs(t *testing.T) {
|
|
m := newTestManager(t)
|
|
// Pre-populate .2 and .3
|
|
for _, pair := range []struct{ id, ip string }{
|
|
{"p1", "10.8.0.2/32"},
|
|
{"p2", "10.8.0.3/32"},
|
|
} {
|
|
if err := m.savePeer(&Peer{ID: pair.id, AllowedIPs: pair.ip, CreatedAt: time.Now()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
ip, err := m.nextAvailableIP("10.8.0.1/24")
|
|
if err != nil {
|
|
t.Fatalf("nextAvailableIP: %v", err)
|
|
}
|
|
if ip != "10.8.0.4" {
|
|
t.Errorf("next IP = %q, want 10.8.0.4", ip)
|
|
}
|
|
}
|
|
|
|
func TestNextAvailableIP_InvalidCIDR(t *testing.T) {
|
|
m := newTestManager(t)
|
|
if _, err := m.nextAvailableIP("not-a-cidr"); err == nil {
|
|
t.Error("expected error for invalid CIDR")
|
|
}
|
|
}
|
|
|
|
// --- GetPublicKey ---
|
|
|
|
func TestGetPublicKey_NoSecrets(t *testing.T) {
|
|
m := newTestManager(t)
|
|
if key := m.GetPublicKey(); key != "" {
|
|
t.Errorf("expected empty key, got %q", key)
|
|
}
|
|
}
|
|
|
|
func TestGetPublicKey_WithSecrets(t *testing.T) {
|
|
m := newTestManager(t)
|
|
writeTestSecrets(t, m)
|
|
if key := m.GetPublicKey(); key != "fakepubkey456" {
|
|
t.Errorf("public key = %q, want fakepubkey456", key)
|
|
}
|
|
}
|
|
|
|
// --- Peer config text ---
|
|
|
|
func TestGeneratePeerConfigText(t *testing.T) {
|
|
m := newTestManager(t)
|
|
writeTestSecrets(t, m)
|
|
|
|
if err := m.SaveConfig(&Config{
|
|
Enabled: true,
|
|
ListenPort: 51820,
|
|
Address: "10.8.0.1/24",
|
|
Endpoint: "vpn.example.com",
|
|
DNS: "10.8.0.1",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
peer := &Peer{
|
|
ID: "peer-1",
|
|
Name: "laptop",
|
|
PublicKey: "clientpub",
|
|
PrivateKey: "clientpriv",
|
|
AllowedIPs: "10.8.0.2/32",
|
|
CreatedAt: time.Now(),
|
|
}
|
|
if err := m.savePeer(peer); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
text, err := m.GeneratePeerConfigText("peer-1")
|
|
if err != nil {
|
|
t.Fatalf("GeneratePeerConfigText: %v", err)
|
|
}
|
|
|
|
checks := []string{
|
|
"[Interface]",
|
|
"PrivateKey = clientpriv",
|
|
"Address = 10.8.0.2/32",
|
|
"DNS = 10.8.0.1",
|
|
"[Peer]",
|
|
"PublicKey = fakepubkey456",
|
|
"Endpoint = vpn.example.com:51820",
|
|
"AllowedIPs =",
|
|
"PersistentKeepalive = 25",
|
|
}
|
|
for _, want := range checks {
|
|
if !strings.Contains(text, want) {
|
|
t.Errorf("peer config missing %q\nFull config:\n%s", want, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGeneratePeerConfigText_NoSecrets(t *testing.T) {
|
|
m := newTestManager(t)
|
|
_ = m.SaveConfig(&Config{Enabled: true, ListenPort: 51820, Address: "10.8.0.1/24"})
|
|
_ = m.savePeer(&Peer{ID: "p1", AllowedIPs: "10.8.0.2/32", CreatedAt: time.Now()})
|
|
if _, err := m.GeneratePeerConfigText("p1"); err == nil {
|
|
t.Error("expected error when secrets not generated")
|
|
}
|
|
}
|
|
|
|
// --- renderWgConfig ---
|
|
|
|
func TestRenderWgConfig(t *testing.T) {
|
|
m := newTestManager(t)
|
|
cfg := &Config{ListenPort: 51820, Address: "10.8.0.1/24"}
|
|
srv := &secrets{PrivateKey: "servpriv", PublicKey: "servpub"}
|
|
peers := []*Peer{
|
|
{Name: "alice", PublicKey: "alicepub", AllowedIPs: "10.8.0.2/32"},
|
|
}
|
|
out := m.renderWgConfig(cfg, srv, peers)
|
|
|
|
checks := []string{
|
|
"[Interface]",
|
|
"PrivateKey = servpriv",
|
|
"Address = 10.8.0.1/24",
|
|
"ListenPort = 51820",
|
|
"[Peer]",
|
|
"# alice",
|
|
"PublicKey = alicepub",
|
|
"AllowedIPs = 10.8.0.2/32",
|
|
}
|
|
for _, want := range checks {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("wg config missing %q\nFull:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- IP helpers ---
|
|
|
|
func TestCloneIP(t *testing.T) {
|
|
ip := net.ParseIP("10.8.0.1").To4()
|
|
clone := cloneIP(ip)
|
|
clone[3] = 99
|
|
if ip[3] == 99 {
|
|
t.Error("cloneIP should not share underlying array")
|
|
}
|
|
}
|
|
|
|
func TestIncrementIP(t *testing.T) {
|
|
ip := net.ParseIP("10.8.0.1").To4()
|
|
incrementIP(ip)
|
|
if ip.String() != "10.8.0.2" {
|
|
t.Errorf("incrementIP: got %s, want 10.8.0.2", ip)
|
|
}
|
|
}
|
|
|
|
func TestBroadcastAddr(t *testing.T) {
|
|
_, ipNet, _ := net.ParseCIDR("10.8.0.0/24")
|
|
if got := broadcastAddr(ipNet); got != "10.8.0.255" {
|
|
t.Errorf("broadcastAddr = %q, want 10.8.0.255", got)
|
|
}
|
|
}
|