package main import ( "net/http" "net/url" "regexp" "strings" "testing" ) func withDomain(t *testing.T) Settings { t.Helper() s := mustParse(t) obj(obj(s, "router"), "dns")["domain"] = "home.example.org" obj(obj(s, "router"), "dns")["forwarders"] = []any{"1.1.1.1"} return s } func TestDNSRecordsRoundTrip(t *testing.T) { s := withDomain(t) err := s.applyDNS([]DNSRecord{ {Name: "files", Addresses: "192.168.168.20, fd00::20"}, {Name: "photos.example.org", Addresses: "192.168.168.50"}, {Name: "*.k8s.stinnesbeck.com", Addresses: "192.168.168.254"}, {Name: "git.stinnesbeck.com.", Addresses: "10.43.162.50"}, // a trailing dot is fine {}, // the add row }, []DNSRecord{{Name: "tdf.stinnesbeck.com", Addresses: "10.1.0.254"}}) if err != nil { t.Fatal(err) } // The wildcard is an override of the name; hosts stay hosts. if _, ok := s.dnsMap("overrides")["k8s.stinnesbeck.com"]; !ok { t.Fatalf("overrides = %v", s.dnsMap("overrides")) } if _, ok := s.dnsMap("hosts")["files"]; !ok || len(s.dnsMap("hosts")) != 3 { t.Errorf("hosts = %v", s.dnsMap("hosts")) } rows := s.dnsRecords() var names []string for _, r := range rows { names = append(names, r.Name) } if strings.Join(names, " ") != "files git.stinnesbeck.com photos.example.org *.k8s.stinnesbeck.com" { t.Errorf("rows = %v", names) } if r := rows[0]; r.Addresses != "192.168.168.20, fd00::20" { t.Errorf("files = %+v", r) } if z := s.forwardZones(); len(z) != 1 || z[0].Name != "tdf.stinnesbeck.com" { t.Errorf("zones = %+v", z) } // The rest of dns survives, and removing everything removes the keys. if s.domain() != "home.example.org" || str(s, "router", "dns", "forwarders") == "" && s["router"].(map[string]any)["dns"].(map[string]any)["forwarders"] == nil { t.Error("domain or forwarders lost") } for i := range rows { rows[i].Remove = true } if err := s.applyDNS(rows, nil); err != nil { t.Fatal(err) } if len(s.dnsRecords()) != 0 || len(s.forwardZones()) != 0 || s.domain() == "" { t.Errorf("after removing: %v %v %q", s.dnsRecords(), s.forwardZones(), s.domain()) } } func TestDNSRecordsAreChecked(t *testing.T) { for name, c := range map[string]struct { rec DNSRecord want string }{ "bad name": {DNSRecord{Name: "no good", Addresses: "10.0.0.1"}, "rec[0].name"}, "wildcard mid": {DNSRecord{Name: "a.*.example.org", Addresses: "10.0.0.1"}, "rec[0].name"}, "no address": {DNSRecord{Name: "files", Addresses: ""}, "rec[0].addresses"}, "bad address": {DNSRecord{Name: "files", Addresses: "10.0.0.999"}, "rec[0].addresses"}, "prefix": {DNSRecord{Name: "files", Addresses: "10.0.0.1/24"}, "rec[0].addresses"}, // The sample has a DHCP reservation called nas, which publishes it. "reservation": {DNSRecord{Name: "nas", Addresses: "10.0.0.1"}, "rec[0].name"}, } { s := withDomain(t) err := s.applyDNS([]DNSRecord{c.rec}, nil) if fe, ok := err.(FormErrors); !ok || fe[c.want] == "" { t.Errorf("%s: %v", name, err) } } // Two names that are the same once placed in the domain. s := withDomain(t) if err := s.applyDNS([]DNSRecord{{Name: "web", Addresses: "10.0.0.1"}, {Name: "web.home.example.org", Addresses: "10.0.0.2"}}, nil); err == nil { t.Error("web and web.home.example.org were both accepted") } // A short name needs a domain; a name cannot be there twice. s = mustParse(t) err := s.applyDNS([]DNSRecord{{Name: "web", Addresses: "10.0.0.1"}}, nil) if fe, ok := err.(FormErrors); !ok || !strings.Contains(fe["rec[0].name"], "local domain") { t.Errorf("short name without a domain: %v", err) } if err := s.applyDNS([]DNSRecord{{Name: "a.example.org", Addresses: "10.0.0.1"}, {Name: "a.example.org", Addresses: "10.0.0.2"}}, nil); err == nil { t.Error("a name twice was accepted") } // A wildcard and a plain name of the same name are two different things. if err := s.applyDNS([]DNSRecord{{Name: "a.example.org", Addresses: "10.0.0.1"}, {Name: "*.a.example.org", Addresses: "10.0.0.2"}}, nil); err != nil { t.Errorf("a name and its wildcard: %v", err) } if err := s.applyDNS(nil, []DNSRecord{{Name: "*.branch.test", Addresses: "10.0.0.1"}}); err == nil { t.Error("a wildcard forward zone was accepted") } } func TestDNSPage(t *testing.T) { a, sys := newTestApp(t) h := a.routes() cookie := do(h, "POST", "/login", url.Values{"password": {"hunter2hunter2"}}, nil).Result().Cookies()[0] body := do(h, "GET", "/dns", nil, cookie).Body.String() if !strings.Contains(body, "Forward zones") || !strings.Contains(body, "DHCP reservation") { t.Fatalf("page:\n%s", body) } token := regexp.MustCompile(`name="csrf" value="([^"]+)"`).FindStringSubmatch(body)[1] form := url.Values{"csrf": {token}, "recs": {"2"}, "zones": {"0"}, "rec.0.name": {"*.k8s.stinnesbeck.com"}, "rec.0.addresses": {"192.168.168.254"}, "rec.1.name": {"web"}, "rec.1.addresses": {"192.168.168.9"}} // No local domain in the sample: the short name is refused, nothing is saved. if rec := do(h, "POST", "/dns", form, cookie); rec.Code != http.StatusUnprocessableEntity { t.Fatalf("short name without domain: %d", rec.Code) } form.Set("rec.1.name", "web.stinnesbeck.com") if rec := do(h, "POST", "/dns", form, cookie); rec.Code != http.StatusSeeOther { t.Fatalf("save: %d\n%s", rec.Code, rec.Body) } s, _ := loadSettings(sys.settingsPath()) if _, ok := s.dnsMap("overrides")["k8s.stinnesbeck.com"]; !ok { t.Errorf("settings: %v", s["router"]) } if body := do(h, "GET", "/dns", nil, cookie).Body.String(); !strings.Contains(body, `value="*.k8s.stinnesbeck.com"`) { t.Error("the wildcard is not shown again") } // Saving the network form must keep them. net := do(h, "GET", "/network", nil, cookie).Body.String() nt := regexp.MustCompile(`name="csrf" value="([^"]+)"`).FindStringSubmatch(net)[1] nf := url.Values{"csrf": {nt}, "hostName": {"gw"}, "wan.interface": {"eth1"}, "wan.type": {"dhcp"}, "lans": {"1"}, "lan.0.name": {"main"}, "lan.0.interface": {"eth2"}, "lan.0.address": {"192.168.10.1"}, "lan.0.dhcp": {"on"}, "lan.0.poolStart": {"192.168.10.100"}, "lan.0.poolEnd": {"192.168.10.199"}} do(h, "POST", "/network", nf, cookie) s, _ = loadSettings(sys.settingsPath()) if _, ok := s.dnsMap("overrides")["k8s.stinnesbeck.com"]; !ok { t.Error("the network form dropped the DNS records") } }