package engine import ( "os" "path/filepath" "strings" "testing" "jarl/internal/model" "jarl/internal/validate" ) func hasProblem(ps []validate.Problem, code string) bool { for _, p := range ps { if p.Code == code { return true } } return false } func describeProblems(e *Error) string { var out []string for _, p := range e.Problems { out = append(out, p.Message) } return strings.Join(out, "; ") } func optionReq(name, value string) Request { return NewRequest(OpOptionCreate, 0, OptionInput{Scope: model.Ptr(model.ScopeOptions), Name: &name, Value: &value}) } func TestAddressRanges(t *testing.T) { env := newEnv(t) setupZone(env) env.submit(env.admin, NewRequest(OpACLCreate, 0, ACLInput{Name: model.Ptr("dhcp"), Elements: &[]string{"!10.0.10.150 - 10.0.10.160", "10.0.10.101-10.0.10.199", "2001:db8::100-2001:db8::1ff"}}), optionReq("allow-query", "10.0.20.1 - 10.0.20.6; localhost"), ) st := env.state() if a := st.ACLByName("dhcp"); strings.Join(a.Elements, " ") != "!10.0.10.150-10.0.10.160 10.0.10.101-10.0.10.199 2001:db8::100-2001:db8::1ff" { t.Errorf("stored elements = %q", a.Elements) } if o := st.FindOption(model.ScopeOptions, 0, "allow-query"); o == nil || o.Value != "{ 10.0.20.1-10.0.20.6; localhost; }" { t.Errorf("stored option = %+v", o) } conf, _ := os.ReadFile(filepath.Join(env.dir, "etc/named.conf")) c := string(conf) for _, want := range []string{ "acl \"dhcp\" {\n\t// !10.0.10.150-10.0.10.160\n\t!10.0.10.150/31;\n\t!10.0.10.152/29;\n\t!10.0.10.160;\n\t// 10.0.10.101-10.0.10.199\n\t10.0.10.101;\n", "\t10.0.10.192/29;\n\t// 2001:db8::100-2001:db8::1ff\n\t2001:db8::100/120;\n};\n", "\tallow-query {\n\t\t// 10.0.20.1-10.0.20.6\n\t\t10.0.20.1;\n\t\t10.0.20.2/31;\n\t\t10.0.20.4/31;\n\t\t10.0.20.6;\n\t\tlocalhost;\n\t};\n", } { if !strings.Contains(c, want) { t.Errorf("named.conf lacks %q:\n%s", want, c) } } for _, l := range strings.Split(c, "\n") { if code, _, _ := strings.Cut(l, "//"); strings.Contains(code, "-10.0.") || strings.Contains(code, "-2001:") { t.Errorf("a range reached named.conf outside a comment: %q", l) } } // Mistakes are caught when they're entered. for _, tc := range []struct { req Request want string }{ {NewRequest(OpACLCreate, 0, ACLInput{Name: model.Ptr("bad"), Elements: &[]string{"10.0.10.199-10.0.10.101"}}), "start is after the end"}, {NewRequest(OpACLCreate, 0, ACLInput{Name: model.Ptr("bad"), Elements: &[]string{"10.0.10.101-199"}}), "not a full IP address"}, {optionReq("forwarders", "{ 192.0.2.1-192.0.2.9; }"), "only work in address match lists"}, {optionReq("allow-recursion", "{ 10.0.0.9-10.0.0.1; }"), "start is after the end"}, {NewRequest(OpACLCreate, 0, ACLInput{Name: model.Ptr("10.0.0.1-10.0.0.9"), Elements: &[]string{"10.0.0.1"}}), "looks like an address range"}, } { if e := env.submitErr(env.admin, tc.req); !strings.Contains(e.Error(), tc.want) && !strings.Contains(describeProblems(e), tc.want) { t.Errorf("%s: %v %s, want %q", tc.req.Op, e, describeProblems(e), tc.want) } } // Validation sees what a range covers: everything is "any". res := env.submit(env.admin, optionReq("allow-recursion", "0.0.0.0-255.255.255.255")) if !hasProblem(res.Problems, "security_open_resolver") { t.Errorf("no open resolver warning for 0.0.0.0-255.255.255.255: %+v", res.Problems) } // IPv6 ranges in the IPv4-mapped space get a warning. res = env.submit(env.admin, NewRequest(OpACLCreate, 0, ACLInput{Name: model.Ptr("mapped"), Elements: &[]string{"::ffff:10.0.0.1-::ffff:10.0.0.9"}})) if !hasProblem(res.Problems, "range_mapped4") { t.Errorf("no range_mapped4 warning: %+v", res.Problems) } } func TestClientGroupRanges(t *testing.T) { env := newEnv(t) setupZone(env) _, srv := newListServer(t, "||ads.example^\n") env.submit(env.admin, NewRequest(OpACLCreate, 0, ACLInput{Name: model.Ptr("tablets"), Elements: &[]string{"10.0.0.50-10.0.0.51"}}), ) env.submit(env.admin, blocklist("ads", srv.URL+"/a.txt"), clientGroup("kids", "off", "tablets", "10.0.0.20 - 10.0.0.23")) if g := env.state().SortedClientGroups(); len(g) != 1 || strings.Join(g[0].Members, " ") != "tablets 10.0.0.20-10.0.0.23" { t.Fatalf("groups = %+v", g) } b, err := os.ReadFile(filepath.Join(env.dir, "zones/_default/off.gate.jarl.invalid.rpz")) if err != nil { t.Fatal(err) } for _, want := range []string{"31.50.0.0.10.rpz-client-ip", "30.20.0.0.10.rpz-client-ip"} { if !strings.Contains(string(b), want) { t.Errorf("off gate lacks %s:\n%s", want, b) } } }