package wireguard import ( "encoding/json" "os" "strconv" "strings" "testing" "vpn-platform/backend/internal/model" "vpn-platform/backend/internal/service" "vpn-platform/backend/internal/store" ) func TestBuildPeersMigratesLegacySingleClient(t *testing.T) { cfg := map[string]interface{}{ "address": "10.8.0.1/24", "client_address": "10.8.0.2/32", "allowed_ips": "0.0.0.0/0, ::/0", } peers, secrets, err := buildPeers(cfg, map[string]interface{}{}) if err != nil { t.Fatalf("buildPeers: %v", err) } if len(peers) != 1 || peers[0].ID != "client-1" || !peers[0].Managed { t.Fatalf("legacy peer migration = %#v", peers) } if !isWireGuardKey(peers[0].PublicKey) || !isWireGuardKey(secrets["client-1"].PrivateKey) || !isWireGuardKey(secrets["client-1"].PresharedKey) { t.Fatal("generated peer keys are not WireGuard keys") } cfg["server_public_key"] = "server-public-key" cfg["listen_port"] = 51820 client, err := generateClientConfig(cfg, peers, secrets, "client-1") if err != nil { t.Fatalf("generateClientConfig: %v", err) } if !strings.Contains(client, "Address = 10.8.0.2/32") || !strings.Contains(client, "Endpoint = your-server-ip:51820") { t.Fatalf("unexpected client config:\n%s", client) } } func TestBuildPeersKeepsExternalPeerPrivateKeyOutOfPlatform(t *testing.T) { _, externalPublic, err := genKey() if err != nil { t.Fatal(err) } cfg := map[string]interface{}{ "allowed_ips": "10.0.0.0/8", "peers": []interface{}{map[string]interface{}{ "id": "branch-office", "name": "Branch office", "public_key": externalPublic, "client_address": "10.8.0.10/32", "enabled": true, }}, } peers, secrets, err := buildPeers(cfg, map[string]interface{}{}) if err != nil { t.Fatalf("buildPeers: %v", err) } if peers[0].Managed || secrets["branch-office"].PrivateKey != "" { t.Fatalf("external peer must not gain a platform-held private key: %#v %#v", peers[0], secrets["branch-office"]) } if _, err := generateClientConfig(cfg, peers, secrets, "branch-office"); err == nil { t.Fatal("external peer configuration should not be downloadable") } } func TestValidateRejectsUnsafeNATAndInvalidPeerAddress(t *testing.T) { s := New() // enable_nat without egress:若本机无默认路由则失败;有默认路由则自动填 egress_interface。 if cfg, err := s.Validate(map[string]interface{}{"address": "10.8.0.1/24", "enable_nat": true}); err == nil { if service.Str(cfg, "egress_interface") == "" { t.Fatal("NAT auto-detect succeeded but egress_interface is empty") } } _, err := s.Validate(map[string]interface{}{ "address": "10.8.0.1/24", "peers": []interface{}{map[string]interface{}{"id": "bad", "client_address": "not-a-cidr"}}, }) if err == nil { t.Fatal("invalid peer address was accepted") } } func TestValidateWireGuardAuthMode(t *testing.T) { for _, mode := range []string{"", "password"} { if _, err := New().Validate(map[string]interface{}{"address": "10.8.0.1/24", "auth_mode": mode}); err != nil { t.Fatalf("legacy/password mode %q rejected: %v", mode, err) } } for _, mode := range []string{"oidc", "unknown"} { if _, err := New().Validate(map[string]interface{}{"address": "10.8.0.1/24", "auth_mode": mode}); err == nil { t.Fatalf("unsupported authentication mode %q accepted", mode) } } } func TestValidateLatencyProbeTarget(t *testing.T) { for _, target := range []string{"10.88.0.1:22", "[2001:db8::1]:443", ""} { if _, err := New().Validate(map[string]interface{}{"address": "10.8.0.1/24", "latency_probe": target}); err != nil { t.Fatalf("valid target %q rejected: %v", target, err) } } for _, target := range []string{"example.com:443", "10.88.0.1:0", "10.88.0.1:65536", "invalid"} { if _, err := New().Validate(map[string]interface{}{"address": "10.8.0.1/24", "latency_probe": target}); err == nil { t.Fatalf("invalid target %q accepted", target) } } } func TestNATOnlyAppliesToWireGuardSubnet(t *testing.T) { conf := renderServerConfig(map[string]interface{}{ "address": "10.88.0.1/24", "listen_port": 51820, "enable_nat": true, "egress_interface": "eth0", }, "test-key", nil, nil) if !strings.Contains(conf, "POSTROUTING -s 10.88.0.0/24 -o eth0 -j MASQUERADE") { t.Fatal("NAT must be limited to the tunnel subnet") } if strings.Count(conf, "POSTROUTING -s 10.88.0.0/24") != 2 { t.Fatal("PostUp and PostDown must use the same subnet") } } func TestInterfaceNameFitsLinuxLimit(t *testing.T) { name := interfaceName(&model.Tunnel{ID: ^uint(0)}) if len(name) > 15 { t.Fatalf("interface name %q exceeds IFNAMSIZ-1", name) } } func TestCreatePersistsOnlyPublicPeerData(t *testing.T) { dataDir := t.TempDir() t.Setenv("VPN_DATA_DIR", dataDir) if err := store.Init(dataDir); err != nil { t.Fatalf("store.Init: %v", err) } tunnel := &model.Tunnel{ Name: "wg-test", Protocol: "wireguard", Config: `{"address":"10.60.0.1/24","client_address":"10.60.0.2/32"}`, } if err := store.DB.Create(tunnel).Error; err != nil { t.Fatal(err) } if err := New().Create(tunnel); err != nil { t.Fatalf("Create: %v", err) } if strings.Contains(tunnel.Config, "private_key") || strings.Contains(tunnel.Config, "preshared_key") { t.Fatalf("private material leaked into Config: %s", tunnel.Config) } var config map[string]interface{} if err := json.Unmarshal([]byte(tunnel.Config), &config); err != nil { t.Fatal(err) } if config["interface_name"] != interfaceName(tunnel) || int(config["listen_port"].(float64)) < defaultPort { t.Fatalf("missing interface/allocated port: %#v", config) } conf, err := New().GenerateClientConfig(tunnel) if err != nil || !strings.Contains(conf, "PrivateKey = ") { t.Fatalf("client configuration unavailable: %v\n%s", err, conf) } if _, err := os.Stat(serviceConfigPath(tunnel)); err != nil { t.Fatalf("server config not written: %v", err) } } func serviceConfigPath(tunnel *model.Tunnel) string { return os.Getenv("VPN_DATA_DIR") + "/instances/" + strconv.Itoa(int(tunnel.ID)) + "-" + tunnel.Protocol + "/" + interfaceName(tunnel) + ".conf" }