// SPDX-License-Identifier: MIT package config import ( "path/filepath" "testing" ) func TestDefaultIsValid(t *testing.T) { if err := Default().Validate(); err != nil { t.Fatalf("default config invalid: %v", err) } } func TestParseCIDR(t *testing.T) { tests := []struct { in string want string bad bool }{ {"192.168.1.0/24", "192.168.1.0/24", false}, {"1.2.3.4", "1.2.3.4/32", false}, {"2001:db8::/32", "2001:db8::/32", false}, {"::1", "::1/128", false}, {"10.0.0.0/8", "10.0.0.0/8", false}, {"not-an-ip", "", true}, {"", "", true}, } for _, tt := range tests { got, err := ParseCIDR(tt.in) if tt.bad { if err == nil { t.Errorf("ParseCIDR(%q): expected error", tt.in) } continue } if err != nil { t.Errorf("ParseCIDR(%q): %v", tt.in, err) continue } if got != tt.want { t.Errorf("ParseCIDR(%q) = %q, want %q", tt.in, got, tt.want) } } } func TestAddRemoveExtraCIDR(t *testing.T) { c := Default() changed, err := c.AddExtraCIDR("203.0.113.7") if err != nil || !changed { t.Fatalf("AddExtraCIDR: changed=%v err=%v", changed, err) } changed, err = c.AddExtraCIDR("203.0.113.7/32") if err != nil || changed { t.Fatalf("duplicate should be a no-op: changed=%v err=%v", changed, err) } if _, err := c.AddExtraCIDR("0.0.0.0/0"); err == nil { t.Fatal("expected refusal to bypass default route") } changed, err = c.RemoveExtraCIDR("203.0.113.7") if err != nil || !changed { t.Fatalf("RemoveExtraCIDR: changed=%v err=%v", changed, err) } } func TestValidateRejectsBadValues(t *testing.T) { cases := map[string]func(*Config){ "zero mark": func(c *Config) { c.Marks.InboundMark = 0 }, "reserved table": func(c *Config) { c.Routing.TableV4 = 254 }, "bad priority": func(c *Config) { c.Routing.PriorityV4 = 70000 }, "bad cidr": func(c *Config) { c.Bypass.ExtraCIDRs = []string{"nope"} }, "relative state": func(c *Config) { c.State.Path = "state.json" }, "empty type": func(c *Config) { c.Engine.Type = "" }, "bad service": func(c *Config) { c.Engine.ServiceName = "bad name!" }, "bad wan iface": func(c *Config) { c.Network.WANInterface = "bad iface" }, "bad gateway": func(c *Config) { c.Network.WANGateway = "nope" }, "relative engine": func(c *Config) { c.Engine.ConfigPath = "rel.json" }, } for name, mutate := range cases { c := Default() mutate(c) if err := c.Validate(); err == nil { t.Errorf("case %q: expected validation error", name) } } } func TestSaveLoadRoundTrip(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "prx", "config.json") c := Default() if changed, _ := c.AddExtraCIDR("198.51.100.0/24"); !changed { t.Fatal("expected change") } if err := Save(path, c); err != nil { t.Fatalf("Save: %v", err) } got, err := Load(path) if err != nil { t.Fatalf("Load: %v", err) } if len(got.Bypass.ExtraCIDRs) != 1 || got.Bypass.ExtraCIDRs[0] != "198.51.100.0/24" { t.Fatalf("unexpected extra cidrs: %+v", got.Bypass.ExtraCIDRs) } if got.Marks.InboundMark != DefaultMark { t.Fatalf("mark not preserved: %x", got.Marks.InboundMark) } } func TestLoadMissingReturnsDefault(t *testing.T) { got, err := Load(filepath.Join(t.TempDir(), "absent.json")) if err != nil { t.Fatalf("Load: %v", err) } if got.Engine.Type != "sing-box" { t.Fatalf("expected default engine, got %q", got.Engine.Type) } } func TestAddDomainValidatesAndDedupes(t *testing.T) { c := Default() if _, err := c.AddDomain("not a domain"); err == nil { t.Fatal("expected error for invalid domain") } if _, err := c.AddDomain(""); err == nil { t.Fatal("expected error for empty domain") } changed, err := c.AddDomain("Example.com") if err != nil || !changed { t.Fatalf("AddDomain: changed=%v err=%v", changed, err) } if changed, _ := c.AddDomain("example.com"); changed { t.Fatal("duplicate domain should be a no-op (case-insensitive)") } if changed, _ := c.AddDomain("*.wild.example.com"); !changed { t.Fatal("wildcard domain should be accepted") } if !c.RemoveDomain("EXAMPLE.COM") { t.Fatal("RemoveDomain should be case-insensitive") } } func TestValidDomain(t *testing.T) { valid := []string{"example.com", "a.b.c.example.org", "*.example.com", "xn--bcher-kva.example"} for _, d := range valid { if !ValidDomain(d) { t.Errorf("ValidDomain(%q) = false, want true", d) } } invalid := []string{"", "no_dots", "trailing-.com", "-leading.com", "sp ace.com", "*.", "a..b.com"} for _, d := range invalid { if ValidDomain(d) { t.Errorf("ValidDomain(%q) = true, want false", d) } } } func TestHealthProbeValidation(t *testing.T) { ok := Default() if err := ok.Validate(); err != nil { t.Fatalf("default probe rejected: %v", err) } disabled := Default() disabled.Health.Probe = "" if err := disabled.Validate(); err != nil { t.Fatalf("empty probe should disable the check: %v", err) } for _, bad := range []string{"nope", ":443", "1.1.1.1:0", "1.1.1.1:99999"} { c := Default() c.Health.Probe = bad if err := c.Validate(); err == nil { t.Errorf("Health.Probe %q: expected validation error", bad) } } } func TestCheckTableIDRejectsReserved(t *testing.T) { for _, id := range []int{0, 253, 254, 255, -1, MaxTableID + 1} { if err := CheckTableID("table", id); err == nil { t.Errorf("CheckTableID(%d): expected error", id) } } if err := CheckTableID("table", 100); err != nil { t.Errorf("CheckTableID(100): unexpected error: %v", err) } } func TestWANGateway6Validation(t *testing.T) { c := Default() c.Network.WANGateway6 = "2001:db8::1" if err := c.Validate(); err != nil { t.Fatalf("valid IPv6 gateway rejected: %v", err) } c.Network.WANGateway6 = "192.168.1.1" if err := c.Validate(); err == nil { t.Fatal("IPv4 address in wan_gateway6 should be rejected") } }