Reverse zone suggestions: yours and public, dismiss, per-view switch; child zones take their records
Suggestions split into networks that are yours (private IPv4 /24, ULA /48, an IPv6 /48 this server has an address in), which Create all creates, and public addresses (/32 or /128) to create only if they are yours or dismiss. A view can turn suggestions off, which also keeps it out of choosing a reverse zone's view (database version 13). A zone created inside another in the same view takes the parent's records under it; a delegation and its glue stay, a linked parent keeps them with a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa4b004a14
commit
b24c730b97
21 files changed
+543
-107
No files matched your search
@@ -25,7 +25,7 @@ func DiffOps(from, to *model.State) []model.Op {
|
||||
s := to.Settings
|
||||
add(OpSettingsUpdate, 0, 0, SettingsInput{DefaultTTL: &s.DefaultTTL, Nameservers: model.Ptr(append([]string{}, s.Nameservers...)), RName: &s.RName,
|
||||
Refresh: &s.Refresh, Retry: &s.Retry, Expire: &s.Expire, NegativeTTL: &s.NegativeTTL, SerialPolicy: &s.SerialPolicy,
|
||||
Exposure: &s.Exposure, Acknowledged: model.Ptr(slices.Clone(s.Acknowledged))}, "restore settings")
|
||||
Exposure: &s.Exposure, Acknowledged: model.Ptr(slices.Clone(s.Acknowledged)), DismissedReverse: model.Ptr(slices.Clone(s.DismissedReverse))}, "restore settings")
|
||||
}
|
||||
|
||||
for _, k := range sortedKeys(to.Keys) {
|
||||
@@ -50,7 +50,8 @@ func DiffOps(from, to *model.State) []model.Op {
|
||||
n := to.Views[k]
|
||||
in := ViewInput{Name: &n.Name, MatchClients: model.Ptr(append([]string{}, n.MatchClients...)),
|
||||
MatchDestinations: model.Ptr(append([]string{}, n.MatchDestinations...)), Comment: &n.Comment, Position: model.Ptr(n.Position),
|
||||
Exposure: model.Ptr(n.Exposure), Acknowledged: model.Ptr(slices.Clone(n.Acknowledged))}
|
||||
Exposure: model.Ptr(n.Exposure), Acknowledged: model.Ptr(slices.Clone(n.Acknowledged)),
|
||||
ReverseSuggestions: model.Ptr(!n.ReverseSuggestionsOff), DismissedReverse: model.Ptr(slices.Clone(n.DismissedReverse))}
|
||||
if n.MatchRecursiveOnly != nil {
|
||||
in.MatchRecursiveOnly = model.Some(*n.MatchRecursiveOnly)
|
||||
} else {
|
||||
|
||||
@@ -157,6 +157,8 @@ func (x *expander) expandOne(req Request) ([]model.Op, error) {
|
||||
return x.zoneCreate(req)
|
||||
case OpZoneFillPTR:
|
||||
return x.zoneFillPTR(req)
|
||||
case OpReverseDismiss, OpReverseUndismiss:
|
||||
return x.reverseDismiss(req, req.Op == OpReverseDismiss)
|
||||
case OpZoneUpdate, OpZoneDelete:
|
||||
z := x.st.Zones[req.ID]
|
||||
if z == nil {
|
||||
@@ -403,6 +405,41 @@ func (x *expander) zoneCreate(req Request) ([]model.Op, error) {
|
||||
in.ViewID = &v.ID
|
||||
}
|
||||
}
|
||||
var moveOps []model.Op
|
||||
var moved []*model.Record
|
||||
if zt == model.ZonePrimary {
|
||||
// The records a parent zone in the view has under the new zone
|
||||
// move into it: named answers those names from the new zone, so
|
||||
// left behind they would never be served again.
|
||||
vid := int64(0)
|
||||
if in.ViewID != nil {
|
||||
vid = *in.ViewID
|
||||
}
|
||||
if parent := x.parentZone(name, vid); parent != nil {
|
||||
moved = x.recordsUnder(parent, name)
|
||||
switch {
|
||||
case len(moved) == 0:
|
||||
case parent.LinkGroup != 0:
|
||||
x.warnings = append(x.warnings, fmt.Sprintf("zone %s is linked, so its %d records under %s stay there (hidden by the new zone): move them by hand", zoneDesc(x.st, parent), len(moved), dnsname.Display(name)))
|
||||
moved = nil
|
||||
default:
|
||||
for _, r := range moved {
|
||||
ri := RecordInput{Name: model.Ptr(r.Name), Type: model.Ptr(r.Type), Data: model.Ptr(r.Data)}
|
||||
if r.TTL != nil {
|
||||
ri.TTL = model.Some(*r.TTL)
|
||||
}
|
||||
if r.Comment != "" {
|
||||
ri.Comment = model.Ptr(r.Comment)
|
||||
}
|
||||
if r.Disabled {
|
||||
ri.Disabled = model.Ptr(true)
|
||||
}
|
||||
in.Records = append(in.Records, ri)
|
||||
moveOps = append(moveOps, newOp(OpRecordDelete, r.ID, r.Version, nil, fmt.Sprintf("move %s %s %s from zone %s into the new zone", r.Name, r.Type, r.Data, zoneDesc(x.st, parent))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fill := in.Fill
|
||||
in.Fill = false
|
||||
if fill {
|
||||
@@ -416,7 +453,7 @@ func (x *expander) zoneCreate(req Request) ([]model.Op, error) {
|
||||
if in.ViewID != nil {
|
||||
vid = *in.ViewID
|
||||
}
|
||||
fills := PTRFills(x.st, vid, name, nil)
|
||||
fills := PTRFills(x.st, vid, name, moved)
|
||||
if len(fills) == 0 {
|
||||
x.warnings = append(x.warnings, fmt.Sprintf("no A or AAAA record of %s has an address in %s, so there was nothing to fill it with", viewOrServer(x.st, vid), dnsname.Display(name)))
|
||||
}
|
||||
@@ -642,7 +679,45 @@ func (x *expander) zoneCreate(req Request) ([]model.Op, error) {
|
||||
r.in.ZoneID = ids[0]
|
||||
ops = append(ops, newOp(OpRecordCreate, ids[1+i], 0, r.in, r.summary))
|
||||
}
|
||||
return ops, nil
|
||||
return append(ops, moveOps...), nil
|
||||
}
|
||||
|
||||
// parentZone returns the most specific primary zone in a view that
|
||||
// contains name (not name itself).
|
||||
func (x *expander) parentZone(name string, viewID int64) *model.Zone {
|
||||
var best *model.Zone
|
||||
for _, z := range x.st.ZonesInView(viewID) {
|
||||
if z.Type == model.ZonePrimary && dnsname.IsStrictSubdomain(name, z.Name) && (best == nil || len(z.Name) > len(best.Name)) {
|
||||
best = z
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// recordsUnder returns a zone's records at or below name, except a
|
||||
// delegation's NS and DS records and its glue, which belong to the parent.
|
||||
func (x *expander) recordsUnder(z *model.Zone, name string) []*model.Record {
|
||||
all := x.st.ZoneRecords(z.ID)
|
||||
var ns []string
|
||||
for _, r := range all {
|
||||
if r.Name == name && r.Type == "NS" {
|
||||
ns = append(ns, strings.ToLower(r.Data))
|
||||
}
|
||||
}
|
||||
var out []*model.Record
|
||||
for _, r := range all {
|
||||
if !dnsname.IsSubdomain(r.Name, name) {
|
||||
continue
|
||||
}
|
||||
if r.Name == name && (r.Type == "NS" || r.Type == "DS") {
|
||||
continue
|
||||
}
|
||||
if (r.Type == "A" || r.Type == "AAAA") && slices.Contains(ns, strings.ToLower(r.Name)) {
|
||||
continue // glue
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func deref(s *string) string {
|
||||
|
||||
@@ -62,8 +62,20 @@ const (
|
||||
// Filling a reverse zone with the PTR records of its view's A and
|
||||
// AAAA records (docs/reverse.md) becomes record.create operations.
|
||||
OpZoneFillPTR = "zone.fill_ptr"
|
||||
// Dismissing a reverse zone suggestion, and taking that back, become
|
||||
// view.update or settings.update operations.
|
||||
OpReverseDismiss = "reverse.dismiss"
|
||||
OpReverseUndismiss = "reverse.undismiss"
|
||||
)
|
||||
|
||||
// DismissInput dismisses a reverse zone suggestion: a network in a view
|
||||
// (by ID or name), or the server's (neither given).
|
||||
type DismissInput struct {
|
||||
ViewID int64 `json:"view_id,omitempty"`
|
||||
View string `json:"view,omitempty"`
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
// AckInput acknowledges (or un-acknowledges) an exposure finding of a view
|
||||
// (by ID or name), or of the server (neither given).
|
||||
type AckInput struct {
|
||||
@@ -246,6 +258,11 @@ type ViewInput struct {
|
||||
// Exposure is "auto" (or ""), "public" or "private" (docs/exposure.md).
|
||||
Exposure *string `json:"exposure,omitempty"`
|
||||
Acknowledged *[]model.Acknowledgement `json:"acknowledged,omitempty"`
|
||||
// ReverseSuggestions turns the reverse zone suggestions for the
|
||||
// view's addresses on or off (docs/reverse.md).
|
||||
ReverseSuggestions *bool `json:"reverse_suggestions,omitempty"`
|
||||
// DismissedReverse are the networks whose suggestion was dismissed.
|
||||
DismissedReverse *[]string `json:"dismissed_reverse,omitempty"`
|
||||
}
|
||||
|
||||
// ReorderInput sets the order of views.
|
||||
@@ -292,6 +309,9 @@ type SettingsInput struct {
|
||||
// Exposure and Acknowledged are the server's (docs/exposure.md).
|
||||
Exposure *string `json:"exposure,omitempty"`
|
||||
Acknowledged *[]model.Acknowledgement `json:"acknowledged,omitempty"`
|
||||
// DismissedReverse are dismissed reverse zone suggestions (without
|
||||
// views).
|
||||
DismissedReverse *[]string `json:"dismissed_reverse,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteInput carries options for delete operations.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -808,6 +809,12 @@ func setViewFields(v *model.View, in *ViewInput) error {
|
||||
if in.Acknowledged != nil {
|
||||
v.Acknowledged = cleanAcks(*in.Acknowledged)
|
||||
}
|
||||
if in.ReverseSuggestions != nil {
|
||||
v.ReverseSuggestionsOff = !*in.ReverseSuggestions
|
||||
}
|
||||
if in.DismissedReverse != nil {
|
||||
v.DismissedReverse = slices.Clone(*in.DismissedReverse)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1404,6 +1411,9 @@ func applySettings(st *model.State, op model.Op, c *applyCtx) error {
|
||||
if in.Acknowledged != nil {
|
||||
s.Acknowledged = cleanAcks(*in.Acknowledged)
|
||||
}
|
||||
if in.DismissedReverse != nil {
|
||||
s.DismissedReverse = slices.Clone(*in.DismissedReverse)
|
||||
}
|
||||
s.Version++
|
||||
return nil
|
||||
}
|
||||
+121
-36
@@ -219,7 +219,8 @@ func (x *expander) publicViews() map[int64]bool {
|
||||
|
||||
// reverseViewFor chooses the view of a new reverse zone given without one:
|
||||
// the private view whose records use addresses in its range, or the only
|
||||
// private view. "" with no error leaves the choice to the usual rules.
|
||||
// private view; views with reverse suggestions off don't count. nil with
|
||||
// no error leaves the choice to the usual rules.
|
||||
func reverseViewFor(st *model.State, public map[int64]bool, zoneName string) (*model.View, error) {
|
||||
prefix, ok := dnsname.PrefixFromReverse(zoneName)
|
||||
if !ok || len(st.Views) == 0 {
|
||||
@@ -227,7 +228,7 @@ func reverseViewFor(st *model.State, public map[int64]bool, zoneName string) (*m
|
||||
}
|
||||
var private, using []*model.View
|
||||
for _, v := range st.OrderedViews() {
|
||||
if public[v.ID] {
|
||||
if public[v.ID] || v.ReverseSuggestionsOff {
|
||||
continue
|
||||
}
|
||||
private = append(private, v)
|
||||
@@ -253,62 +254,68 @@ func reverseViewFor(st *model.State, public map[int64]bool, zoneName string) (*m
|
||||
// ReverseSuggestion is a reverse zone a view lacks for addresses its
|
||||
// records use.
|
||||
type ReverseSuggestion struct {
|
||||
ViewID int64 `json:"view_id"`
|
||||
View string `json:"view,omitempty"`
|
||||
Prefix string `json:"prefix"`
|
||||
Zone string `json:"zone"`
|
||||
ViewID int64 `json:"view_id"`
|
||||
View string `json:"view,omitempty"`
|
||||
Prefix string `json:"prefix"`
|
||||
Zone string `json:"zone"`
|
||||
// Group is "yours" for networks that are yours for sure (private
|
||||
// ranges, IPv6 prefixes this server has an address in), "public" for
|
||||
// a public address that may be someone else's (a web host's).
|
||||
Group string `json:"group"`
|
||||
Why string `json:"why"`
|
||||
Fill int `json:"fill"` // PTR records creating it with fill adds
|
||||
Samples []string `json:"samples"` // a few of them, "address name"
|
||||
}
|
||||
|
||||
// Suggestion groups.
|
||||
const (
|
||||
SuggestYours = "yours"
|
||||
SuggestPublic = "public"
|
||||
)
|
||||
|
||||
// ReverseSuggestions returns the reverse zones the private views (or the
|
||||
// server, without views) lack for their A and AAAA records' addresses:
|
||||
// the /24 of a private IPv4 address, a /32 for a public one (the /24
|
||||
// belongs to someone else, and a zone for it here would hide the
|
||||
// neighbours' names from your clients), the /48 of an IPv6 address.
|
||||
// Public views get none: the internet asks the addresses' owners.
|
||||
func ReverseSuggestions(st *model.State, exposures []validate.Exposure) []ReverseSuggestion {
|
||||
//
|
||||
// - yours: the /24 of a private IPv4 address, the /48 of a ULA or of a
|
||||
// public IPv6 prefix this server has an address in (local);
|
||||
// - public: a /32 (or /128) for any other public address. It may be a
|
||||
// web host's; the rest of its /24 surely belongs to others, and a
|
||||
// zone for it here would hide their names from your clients.
|
||||
//
|
||||
// Public views, views with suggestions turned off and dismissed networks
|
||||
// get none: the internet asks the addresses' owners.
|
||||
func ReverseSuggestions(st *model.State, exposures []validate.Exposure, local []netip.Addr) []ReverseSuggestion {
|
||||
public := map[int64]bool{}
|
||||
for _, e := range exposures {
|
||||
public[e.ViewID] = e.Public
|
||||
}
|
||||
viewIDs := []int64{0}
|
||||
type scope struct {
|
||||
id int64
|
||||
dismissed []string
|
||||
}
|
||||
var scopes []scope
|
||||
if len(st.Views) > 0 {
|
||||
viewIDs = viewIDs[:0]
|
||||
for _, v := range st.OrderedViews() {
|
||||
if !public[v.ID] {
|
||||
viewIDs = append(viewIDs, v.ID)
|
||||
if !public[v.ID] && !v.ReverseSuggestionsOff {
|
||||
scopes = append(scopes, scope{v.ID, v.DismissedReverse})
|
||||
}
|
||||
}
|
||||
} else if public[0] {
|
||||
return []ReverseSuggestion{}
|
||||
} else if !public[0] {
|
||||
scopes = append(scopes, scope{0, st.Settings.DismissedReverse})
|
||||
}
|
||||
out := []ReverseSuggestion{}
|
||||
for _, vid := range viewIDs {
|
||||
for _, sc := range scopes {
|
||||
seen := map[netip.Prefix]bool{}
|
||||
for _, u := range forwardAddresses(st, vid) {
|
||||
for _, u := range forwardAddresses(st, sc.id) {
|
||||
a := u.addr
|
||||
if a.IsLoopback() || a.IsLinkLocalUnicast() || a.IsUnspecified() || a.IsMulticast() {
|
||||
continue
|
||||
}
|
||||
if rz, _ := reverseZoneFor(st, vid, a); rz != nil {
|
||||
if rz, _ := reverseZoneFor(st, sc.id, a); rz != nil {
|
||||
continue
|
||||
}
|
||||
var p netip.Prefix
|
||||
why := ""
|
||||
switch {
|
||||
case a.Is4() && (a.IsPrivate() || cgnat.Contains(a)):
|
||||
p, why = netip.PrefixFrom(a, 24).Masked(), "private network"
|
||||
case a.Is4():
|
||||
p, why = netip.PrefixFrom(a, 32), "a public address: its own zone, since the rest of its /24 belongs to others"
|
||||
default:
|
||||
p, why = netip.PrefixFrom(a, 48).Masked(), "the /48 of your IPv6 addresses (one zone for all their subnets)"
|
||||
if a.IsPrivate() {
|
||||
why = "your ULA /48 (one zone for all its subnets)"
|
||||
}
|
||||
}
|
||||
if seen[p] {
|
||||
p, group, why := suggestedPrefix(a, local)
|
||||
if seen[p] || slices.Contains(sc.dismissed, p.String()) {
|
||||
continue
|
||||
}
|
||||
seen[p] = true
|
||||
@@ -316,8 +323,8 @@ func ReverseSuggestions(st *model.State, exposures []validate.Exposure) []Revers
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s := ReverseSuggestion{ViewID: vid, View: viewName(st, vid), Prefix: p.String(), Zone: strings.TrimSuffix(name, "."), Why: why, Samples: []string{}}
|
||||
fills := PTRFills(st, vid, name, nil)
|
||||
s := ReverseSuggestion{ViewID: sc.id, View: viewName(st, sc.id), Prefix: p.String(), Zone: strings.TrimSuffix(name, "."), Group: group, Why: why, Samples: []string{}}
|
||||
fills := PTRFills(st, sc.id, name, nil)
|
||||
s.Fill = len(fills)
|
||||
for i, f := range fills {
|
||||
if i == 3 {
|
||||
@@ -329,6 +336,9 @@ func ReverseSuggestions(st *model.State, exposures []validate.Exposure) []Revers
|
||||
}
|
||||
}
|
||||
slices.SortStableFunc(out, func(a, b ReverseSuggestion) int {
|
||||
if a.Group != b.Group {
|
||||
return strings.Compare(b.Group, a.Group) // yours first
|
||||
}
|
||||
if a.View != b.View {
|
||||
return strings.Compare(a.View, b.View)
|
||||
}
|
||||
@@ -337,6 +347,81 @@ func ReverseSuggestions(st *model.State, exposures []validate.Exposure) []Revers
|
||||
return out
|
||||
}
|
||||
|
||||
// suggestedPrefix returns the reverse zone network for an address, its
|
||||
// group and why.
|
||||
func suggestedPrefix(a netip.Addr, local []netip.Addr) (netip.Prefix, string, string) {
|
||||
switch {
|
||||
case a.Is4() && (a.IsPrivate() || cgnat.Contains(a)):
|
||||
return netip.PrefixFrom(a, 24).Masked(), SuggestYours, "private network"
|
||||
case a.Is4():
|
||||
return netip.PrefixFrom(a, 32), SuggestPublic, "a public address: create it only if it is yours (not a web host's); the rest of its /24 belongs to others"
|
||||
case a.IsPrivate():
|
||||
return netip.PrefixFrom(a, 48).Masked(), SuggestYours, "your ULA /48 (one zone for all its subnets)"
|
||||
}
|
||||
p := netip.PrefixFrom(a, 48).Masked()
|
||||
if slices.ContainsFunc(local, func(l netip.Addr) bool { return p.Contains(l.Unmap()) }) {
|
||||
return p, SuggestYours, "your IPv6 /48 (this server has an address in it): one zone for all its subnets"
|
||||
}
|
||||
return netip.PrefixFrom(a, 128), SuggestPublic, "a public address outside this server's networks: create it only if it is yours"
|
||||
}
|
||||
|
||||
// ReverseSuggestions lists the reverse zones the private views lack.
|
||||
func (e *Engine) ReverseSuggestions(st *model.State) []ReverseSuggestion {
|
||||
var local []netip.Addr
|
||||
if e.cfg.LocalAddrs != nil {
|
||||
local = e.cfg.LocalAddrs()
|
||||
}
|
||||
return ReverseSuggestions(st, e.Exposures(st), local)
|
||||
}
|
||||
|
||||
// reverseDismiss expands reverse.dismiss and reverse.undismiss.
|
||||
func (x *expander) reverseDismiss(req Request, dismiss bool) ([]model.Op, error) {
|
||||
var in DismissInput
|
||||
if err := decode(req.Data, &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := netip.ParsePrefix(strings.TrimSpace(in.Prefix))
|
||||
if err != nil {
|
||||
return nil, errf(KindBadRequest, "prefix: %v", err)
|
||||
}
|
||||
p = p.Masked()
|
||||
var v *model.View
|
||||
switch {
|
||||
case in.ViewID != 0:
|
||||
v = x.st.Views[in.ViewID]
|
||||
case in.View != "":
|
||||
v = x.st.ViewByName(in.View)
|
||||
}
|
||||
if (in.ViewID != 0 || in.View != "") && v == nil {
|
||||
return nil, errf(KindNotFound, "no view %s", firstNonEmptyStr(in.View, fmt.Sprint(in.ViewID)))
|
||||
}
|
||||
if v == nil && len(x.st.Views) > 0 {
|
||||
return nil, errf(KindBadRequest, "give the view of the suggestion")
|
||||
}
|
||||
list, where := x.st.Settings.DismissedReverse, "the server"
|
||||
if v != nil {
|
||||
list, where = v.DismissedReverse, "view "+v.Name
|
||||
}
|
||||
out := slices.DeleteFunc(slices.Clone(list), func(s string) bool { return s == p.String() })
|
||||
summary := "dismiss the reverse zone suggestion for " + p.String() + " in " + where
|
||||
if dismiss {
|
||||
out = append(out, p.String())
|
||||
slices.Sort(out)
|
||||
} else {
|
||||
if len(out) == len(list) {
|
||||
return nil, errf(KindConflict, "%s isn't dismissed in %s", p, where)
|
||||
}
|
||||
summary = "suggest a reverse zone for " + p.String() + " in " + where + " again"
|
||||
}
|
||||
if out == nil {
|
||||
out = []string{}
|
||||
}
|
||||
if v != nil {
|
||||
return []model.Op{newOp(OpViewUpdate, v.ID, v.Version, ViewInput{DismissedReverse: &out}, summary)}, nil
|
||||
}
|
||||
return []model.Op{newOp(OpSettingsUpdate, 0, 0, SettingsInput{DismissedReverse: &out}, summary)}, nil
|
||||
}
|
||||
|
||||
var cgnat = netip.MustParsePrefix("100.64.0.0/10")
|
||||
|
||||
// autoPTR reports whether a record's PTR is kept in step by its zone's
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -40,16 +41,16 @@ func TestReverseFillAndSuggest(t *testing.T) {
|
||||
recIn("internal", "*.dev", "A", "10.0.10.99"),
|
||||
recIn("internal", "lo", "A", "127.0.0.1"))
|
||||
|
||||
sugg := ReverseSuggestions(env.state(), env.e.Exposures(env.state()))
|
||||
sugg := ReverseSuggestions(env.state(), env.e.Exposures(env.state()), nil)
|
||||
var got []string
|
||||
for _, s := range sugg {
|
||||
got = append(got, s.View+" "+s.Prefix+" "+s.Zone)
|
||||
got = append(got, s.Group+" "+s.View+" "+s.Prefix+" "+s.Zone)
|
||||
}
|
||||
want(t, "suggestions", got,
|
||||
"internal 10.0.10.0/24 10.0.10.in-addr.arpa",
|
||||
"internal 192.0.2.10/32 10.2.0.192.in-addr.arpa",
|
||||
"internal 192.0.2.25/32 25.2.0.192.in-addr.arpa",
|
||||
"internal fdab:abab:abab::/48 b.a.b.a.b.a.b.a.b.a.d.f.ip6.arpa")
|
||||
"yours internal 10.0.10.0/24 10.0.10.in-addr.arpa",
|
||||
"yours internal fdab:abab:abab::/48 b.a.b.a.b.a.b.a.b.a.d.f.ip6.arpa",
|
||||
"public internal 192.0.2.10/32 10.2.0.192.in-addr.arpa",
|
||||
"public internal 192.0.2.25/32 25.2.0.192.in-addr.arpa")
|
||||
if sugg[0].Fill != 1 || sugg[0].Samples[0] != "10.0.10.20 files.example.com" {
|
||||
t.Errorf("10.0.10.0/24: %+v", sugg[0])
|
||||
}
|
||||
@@ -77,7 +78,7 @@ func TestReverseFillAndSuggest(t *testing.T) {
|
||||
}
|
||||
|
||||
got = got[:0]
|
||||
for _, s := range ReverseSuggestions(env.state(), env.e.Exposures(env.state())) {
|
||||
for _, s := range ReverseSuggestions(env.state(), env.e.Exposures(env.state()), nil) {
|
||||
got = append(got, s.Prefix)
|
||||
}
|
||||
want(t, "suggestions left", got, "192.0.2.10/32")
|
||||
@@ -136,3 +137,101 @@ func TestAutoPTR(t *testing.T) {
|
||||
env.submit(env.admin, recIn("internal", "quiet", "A", "10.0.10.50"))
|
||||
want(t, "off", env.ptrs("internal", rz))
|
||||
}
|
||||
|
||||
func suggested(env *testEnv, local ...netip.Addr) []string {
|
||||
var out []string
|
||||
for _, s := range ReverseSuggestions(env.state(), env.e.Exposures(env.state()), local) {
|
||||
out = append(out, s.Group+" "+s.View+" "+s.Prefix)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Public IPv6: the /48 only when this server has an address in it, else
|
||||
// the address alone, as a public suggestion. Dismissed networks and views
|
||||
// with suggestions off get none.
|
||||
func TestReverseSuggestionGroups(t *testing.T) {
|
||||
env := linkedEnv(t)
|
||||
env.submit(env.admin,
|
||||
recIn("internal", "home", "AAAA", "2a02:8013:7f3d:10::5"),
|
||||
recIn("internal", "gh", "AAAA", "2606:50c0:8000::153"))
|
||||
want(t, "no local addresses", suggested(env),
|
||||
"public internal 192.0.2.10/32", "public internal 192.0.2.25/32",
|
||||
"public internal 2606:50c0:8000::153/128", "public internal 2a02:8013:7f3d:10::5/128")
|
||||
want(t, "this server is in 2a02:8013:7f3d::/48", suggested(env, netip.MustParseAddr("2a02:8013:7f3d:10:1266:6aff:fef9:f5b1")),
|
||||
"yours internal 2a02:8013:7f3d::/48",
|
||||
"public internal 192.0.2.10/32", "public internal 192.0.2.25/32", "public internal 2606:50c0:8000::153/128")
|
||||
|
||||
env.submit(env.admin, NewRequest(OpReverseDismiss, 0, DismissInput{View: "internal", Prefix: "2606:50c0:8000::153/128"}))
|
||||
if got := suggested(env); slices.Contains(got, "public internal 2606:50c0:8000::153/128") {
|
||||
t.Errorf("dismissed still suggested: %q", got)
|
||||
}
|
||||
if e := env.submitErr(env.admin, NewRequest(OpReverseUndismiss, 0, DismissInput{View: "internal", Prefix: "192.0.2.10/32"})); e.Kind != KindConflict {
|
||||
t.Errorf("undismiss of one not dismissed: %v", e)
|
||||
}
|
||||
env.submit(env.admin, NewRequest(OpReverseUndismiss, 0, DismissInput{View: "internal", Prefix: "2606:50c0:8000::153/128"}))
|
||||
if got := suggested(env); !slices.Contains(got, "public internal 2606:50c0:8000::153/128") {
|
||||
t.Errorf("undismissed not suggested: %q", got)
|
||||
}
|
||||
|
||||
// A second private view using the same address (a VPS's view): with
|
||||
// its suggestions off it gets none, and doesn't count when choosing
|
||||
// the view of a new reverse zone.
|
||||
env.submit(env.admin, NewRequest(OpViewCreate, 0, ViewInput{Name: model.Ptr("vps"), MatchClients: &[]string{"198.51.100.7"}}))
|
||||
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("vps"), LinkWith: "internal"}))
|
||||
if got := suggested(env); !slices.Contains(got, "public vps 192.0.2.25/32") {
|
||||
t.Errorf("vps view: %q", got)
|
||||
}
|
||||
if e := env.submitErr(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("192.0.2.25/32")})); !strings.Contains(e.Message, "views internal and vps") {
|
||||
t.Errorf("two views: %v", e)
|
||||
}
|
||||
vps := env.state().ViewByName("vps")
|
||||
env.submit(env.admin, NewRequest(OpViewUpdate, vps.ID, ViewInput{ReverseSuggestions: model.Ptr(false)}))
|
||||
if got := suggested(env); slices.ContainsFunc(got, func(s string) bool { return strings.Contains(s, " vps ") }) {
|
||||
t.Errorf("vps view with suggestions off: %q", got)
|
||||
}
|
||||
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("192.0.2.25/32"), Fill: true}))
|
||||
want(t, "went to internal", env.ptrs("internal", "25.2.0.192.in-addr.arpa."), "25.2.0.192.in-addr.arpa. mail.example.com.")
|
||||
}
|
||||
|
||||
// A zone created inside another takes the parent's records under it.
|
||||
func TestChildZoneTakesRecords(t *testing.T) {
|
||||
env := linkedEnv(t)
|
||||
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("10.0.0.0/8"), View: model.Ptr("internal")}))
|
||||
ptr := func(name, data string) Request {
|
||||
return NewRequest(OpRecordCreate, 0, RecordInput{Zone: "10.in-addr.arpa", View: "internal", Name: &name, Type: model.Ptr("PTR"), Data: &data})
|
||||
}
|
||||
env.submit(env.admin, ptr("20.10.0.10.in-addr.arpa.", "printer.example.com."), ptr("5.20.0.10.in-addr.arpa.", "other.example.com."))
|
||||
env.submit(env.admin, recIn("internal", "nas", "A", "10.0.10.30"))
|
||||
res := env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("10.0.10.0/24"), View: model.Ptr("internal"), Fill: true}))
|
||||
want(t, "/24", env.ptrs("internal", "10.0.10.in-addr.arpa."),
|
||||
"20.10.0.10.in-addr.arpa. printer.example.com.", "30.10.0.10.in-addr.arpa. nas.example.com.")
|
||||
want(t, "/8 keeps the rest", env.ptrs("internal", "10.in-addr.arpa."), "5.20.0.10.in-addr.arpa. other.example.com.")
|
||||
for _, p := range res.Problems {
|
||||
if p.Code == "shadowed_by_zone" {
|
||||
t.Errorf("still hidden records: %v", p)
|
||||
}
|
||||
}
|
||||
|
||||
// A forward child: its delegation and glue stay in the parent.
|
||||
env.submit(env.admin,
|
||||
recIn("internal", "lab", "NS", "ns.lab.example.com."),
|
||||
recIn("internal", "ns.lab", "A", "10.0.30.1"),
|
||||
recIn("internal", "www.lab", "A", "10.0.30.2"))
|
||||
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("lab.example.com"), View: model.Ptr("internal")}))
|
||||
st := env.state()
|
||||
lab := st.FindZone("lab.example.com.", st.ViewByName("internal").ID)
|
||||
var labRecs []string
|
||||
for _, r := range st.ZoneRecords(lab.ID) {
|
||||
if r.Type != "NS" {
|
||||
labRecs = append(labRecs, r.Name+" "+r.Type)
|
||||
}
|
||||
}
|
||||
want(t, "lab", labRecs, "www.lab.example.com. A")
|
||||
v := env.view("internal") // without NS records
|
||||
if !slices.Contains(v, "ns.lab A 10.0.30.1 L") || slices.ContainsFunc(v, func(s string) bool { return strings.HasPrefix(s, "www.lab ") }) {
|
||||
t.Errorf("parent after: %q", v)
|
||||
}
|
||||
if env.recordID("internal", "lab", "NS") == 0 {
|
||||
t.Error("the delegation left the parent")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user