package vpn import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) func TestParseIPRangeAndFirstAvailable(t *testing.T) { start, end, err := parseIPRange("192.168.133.2-192.168.133.254") if err != nil { t.Fatalf("unexpected error parsing range: %v", err) } if start.String() != "192.168.133.2" || end.String() != "192.168.133.254" { t.Fatalf("unexpected parsed range: %s - %s", start, end) } used := map[string]bool{ "192.168.133.2": true, "192.168.133.3": true, } first := firstAvailableIPFromRange("192.168.133.2-192.168.133.254", used) if first != "192.168.133.4" { t.Fatalf("expected 192.168.133.4, got %s", first) } } func TestSyncSecretsInheritanceAndOverrides(t *testing.T) { tmpDir, err := os.MkdirTemp("", "ppp-test-*") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) mgr := &PPPServerManager{ baseDir: tmpDir, pppDir: tmpDir, profiles: make(map[string]*PPPProfile), secrets: make(map[string]*PPPSecret), } mgr.initDefaults() // Add custom profile with range mgr.profiles["p2p"] = &PPPProfile{ ID: "p2p", Name: "p2p", LocalAddress: "192.168.133.1", RemoteAddress: "192.168.133.2-192.168.133.254", } // Add profile with single static IP mgr.profiles["static-prof"] = &PPPProfile{ ID: "static-prof", Name: "static-prof", LocalAddress: "10.200.1.1", RemoteAddress: "10.200.1.50", } // Secret 1: explicit override on secret mgr.secrets["s1"] = &PPPSecret{ ID: "s1", Name: "user_explicit", Password: "pass1", Profile: "p2p", RemoteAddress: "192.168.133.99", } // Secret 2: inherits from range profile mgr.secrets["s2"] = &PPPSecret{ ID: "s2", Name: "user_range_inherit", Password: "pass2", Profile: "p2p", } // Secret 3: inherits from single static IP profile mgr.secrets["s3"] = &PPPSecret{ ID: "s3", Name: "user_static_inherit", Password: "pass3", Profile: "static-prof", } // Secret 4: default profile range inherit mgr.secrets["s4"] = &PPPSecret{ ID: "s4", Name: "user_default_inherit", Password: "pass4", Profile: "default", } if err := mgr.syncSecretsToSystemLocked(); err != nil { t.Fatalf("sync error: %v", err) } data, err := os.ReadFile(filepath.Join(tmpDir, "chap-secrets")) if err != nil { t.Skip("skipping chap-secrets read if no root permission") } content := string(data) // Verify user_explicit got 192.168.133.99 if !strings.Contains(content, "\"user_explicit\"\t*\t\"pass1\"\t192.168.133.99") { t.Errorf("user_explicit expected 192.168.133.99 in chap-secrets, got:\n%s", content) } // Verify user_static_inherit got 10.200.1.50 if !strings.Contains(content, "\"user_static_inherit\"\t*\t\"pass3\"\t10.200.1.50") { t.Errorf("user_static_inherit expected 10.200.1.50 in chap-secrets, got:\n%s", content) } // Verify user_range_inherit got first available from p2p (192.168.133.2) if !strings.Contains(content, "\"user_range_inherit\"\t*\t\"pass2\"\t192.168.133.2") { t.Errorf("user_range_inherit expected 192.168.133.2 in chap-secrets, got:\n%s", content) } } func TestSyncSecretsReportsWriteFailure(t *testing.T) { m := &PPPServerManager{pppDir: filepath.Join(t.TempDir(), "missing"), profiles: map[string]*PPPProfile{}, secrets: map[string]*PPPSecret{}} if err := m.syncSecretsToSystemLocked(); err == nil { t.Fatal("expected write error") } } func TestSecretPersistenceFailureDoesNotChangeState(t *testing.T) { root := t.TempDir() bad := filepath.Join(root, "not-dir") if err := os.WriteFile(bad, []byte("x"), 0600); err != nil { t.Fatal(err) } original := &PPPSecret{ID: "s", Name: "alice", Password: "p", Disabled: false} mgr := &PPPServerManager{baseDir: bad, pppDir: root, secrets: map[string]*PPPSecret{"s": original}, profiles: map[string]*PPPProfile{}} if err := mgr.SetSecretState("s", true); err == nil { t.Fatal("expected persistence error") } if mgr.secrets["s"].Disabled { t.Fatal("disabled state changed despite save failure") } if err := mgr.DeleteSecret("s"); err == nil { t.Fatal("expected deletion persistence error") } if _, ok := mgr.secrets["s"]; !ok { t.Fatal("secret removed despite save failure") } } func TestSecretApplyFailureRestoresStoredState(t *testing.T) { root := t.TempDir() ppp := filepath.Join(root, "ppp") if err := os.Mkdir(ppp, 0700); err != nil { t.Fatal(err) } mgr := &PPPServerManager{baseDir: filepath.Join(root, "config"), pppDir: ppp, secrets: map[string]*PPPSecret{}, profiles: map[string]*PPPProfile{}} secret := &PPPSecret{ID: "s", Name: "alice", Password: "p"} if err := mgr.SaveSecret(secret); err != nil { t.Fatal(err) } mgr.pppDir = filepath.Join(root, "missing") if err := mgr.SetSecretState("s", true); err == nil { t.Fatal("expected apply failure") } if mgr.secrets["s"].Disabled { t.Fatal("memory not restored") } data, err := os.ReadFile(filepath.Join(mgr.baseDir, "ppp_secrets.json")) if err != nil { t.Fatal(err) } var saved []PPPSecret if err = json.Unmarshal(data, &saved); err != nil { t.Fatal(err) } if len(saved) != 1 || saved[0].Disabled { t.Fatalf("saved state not restored: %+v", saved) } info, err := os.Stat(filepath.Join(mgr.baseDir, "ppp_secrets.json")) if err != nil || info.Mode().Perm() != 0600 { t.Fatalf("permissions: %v %v", info, err) } } func TestServerSaveFailureDoesNotStartOrChangeState(t *testing.T) { root := t.TempDir() bad := filepath.Join(root, "file") if err := os.WriteFile(bad, []byte("x"), 0600); err != nil { t.Fatal(err) } mgr := &PPPServerManager{baseDir: bad} for _, test := range []struct { name string run func() error enabled func() bool }{ {"PPPoE", func() error { return mgr.SetPPPoEServer(PPPoEServerConfig{Enabled: true}) }, func() bool { return mgr.pppoeServer.Enabled }}, {"PPTP", func() error { return mgr.SetPPTPServer(PPTPServerConfig{Enabled: true}) }, func() bool { return mgr.pptpServer.Enabled }}, {"L2TP", func() error { return mgr.SetL2TPServer(L2TPServerConfig{Enabled: true}) }, func() bool { return mgr.l2tpServer.Enabled }}, {"IPSec", func() error { return mgr.SetIPSecServer(IPSecServerConfig{Enabled: true}) }, func() bool { return mgr.ipsecServer.Enabled }}, } { t.Run(test.name, func(t *testing.T) { if err := test.run(); err == nil { t.Fatal("expected save error") } if test.enabled() { t.Fatal("state not restored") } }) } } func TestIsolatedPPPDaemonFailuresAreReported(t *testing.T) { if os.Getenv("DEBTIK_TEST_ISOLATED_PPP") != "1" { t.Skip("requires isolated mounts and process command fakes") } bin := t.TempDir() t.Setenv("PATH", bin) systemctl := "#!/bin/sh\ncase \"$1\" in stop) exit 0 ;; is-active) echo inactive; exit 3 ;; *) echo daemon-start-failed >&2; exit 1 ;; esac\n" if err := os.WriteFile(filepath.Join(bin, "systemctl"), []byte(systemctl), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(bin, "pkill"), []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil { t.Fatal(err) } m := &PPPServerManager{baseDir: t.TempDir(), profiles: map[string]*PPPProfile{}, secrets: map[string]*PPPSecret{}, pppoePIDFile: filepath.Join(t.TempDir(), "pppoe.pid")} m.initDefaults() if err := m.SetPPTPServer(PPTPServerConfig{Enabled: true}); err == nil || !strings.Contains(err.Error(), "restarting pptpd") { t.Fatalf("PPTP failure ignored: %v", err) } if err := m.SetL2TPServer(L2TPServerConfig{Enabled: true}); err == nil || !strings.Contains(err.Error(), "starting xl2tpd") { t.Fatalf("L2TP failure ignored: %v", err) } }