ACLs, views' match lists, options with address match lists and blocking client groups take ranges such as 10.0.10.101-10.0.10.199 or 2001:db8::100-2001:db8::1ff (negated, or with spaces around the dash, too). Jarl stores the range as written and writes the fewest prefixes that cover exactly that range to named.conf, with the range as a comment. New package internal/iprange; namedconf gets an AMRange element (a range used to parse as an ACL name), ExpandRanges and statement comments. Mistakes are refused when entered, validation sees what ranges cover, and IPv6 ranges in the IPv4-mapped space get a warning. The web UI shows what a range becomes while typing (GET /catalog/ranges). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
4.4 KiB
Go
114 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|