New reverse zone: pick from the networks your records use, sized around them
The Zones page's New reverse zone lists every network the records use without a reverse zone, in every view, also where nothing is suggested (suggestions off, dismissed, public views), marked. Picking one fills in the network and view; size buttons (/48 /56 /64 /128, /16 /24 /32) re-mask it around a record's address, so a /48 becomes the records' own /64. Fill stays on. jarl zone suggest-reverse --all [--view V]; GET /reverse-zones/suggestions?all=1&view=. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1719ce362
commit
304c89dba4
12 files changed
+228
-31
No files matched your search
+54
-10
@@ -261,7 +261,10 @@ type ReverseSuggestion struct {
|
||||
// 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"`
|
||||
Group string `json:"group"`
|
||||
// Hidden says why the suggestion isn't made unasked (with All only):
|
||||
// "public view", "suggestions off" or "dismissed".
|
||||
Hidden string `json:"hidden,omitempty"`
|
||||
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"
|
||||
@@ -285,6 +288,22 @@ const (
|
||||
// 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 {
|
||||
return ReverseCandidates(st, exposures, local, ReverseOptions{})
|
||||
}
|
||||
|
||||
// ReverseOptions widen or narrow ReverseCandidates.
|
||||
type ReverseOptions struct {
|
||||
// All includes what isn't suggested unasked, marked Hidden: public
|
||||
// views, views with suggestions off, dismissed networks.
|
||||
All bool
|
||||
// View narrows it to one view (by name; "" all).
|
||||
View string
|
||||
}
|
||||
|
||||
// ReverseCandidates is ReverseSuggestions with options: with All, every
|
||||
// network the views' records use that no reverse zone in their view
|
||||
// covers, for creating one by hand where suggestions are off.
|
||||
func ReverseCandidates(st *model.State, exposures []validate.Exposure, local []netip.Addr, opt ReverseOptions) []ReverseSuggestion {
|
||||
public := map[int64]bool{}
|
||||
for _, e := range exposures {
|
||||
public[e.ViewID] = e.Public
|
||||
@@ -292,16 +311,31 @@ func ReverseSuggestions(st *model.State, exposures []validate.Exposure, local []
|
||||
type scope struct {
|
||||
id int64
|
||||
dismissed []string
|
||||
hidden string
|
||||
}
|
||||
var scopes []scope
|
||||
if len(st.Views) > 0 {
|
||||
for _, v := range st.OrderedViews() {
|
||||
if !public[v.ID] && !v.ReverseSuggestionsOff {
|
||||
scopes = append(scopes, scope{v.ID, v.DismissedReverse})
|
||||
if opt.View != "" && !strings.EqualFold(v.Name, opt.View) {
|
||||
continue
|
||||
}
|
||||
hidden := ""
|
||||
switch {
|
||||
case public[v.ID]:
|
||||
hidden = "public view"
|
||||
case v.ReverseSuggestionsOff:
|
||||
hidden = "suggestions off"
|
||||
}
|
||||
if hidden == "" || opt.All {
|
||||
scopes = append(scopes, scope{v.ID, v.DismissedReverse, hidden})
|
||||
}
|
||||
}
|
||||
} else if !public[0] {
|
||||
scopes = append(scopes, scope{0, st.Settings.DismissedReverse})
|
||||
} else if !public[0] || opt.All {
|
||||
hidden := ""
|
||||
if public[0] {
|
||||
hidden = "public server"
|
||||
}
|
||||
scopes = append(scopes, scope{0, st.Settings.DismissedReverse, hidden})
|
||||
}
|
||||
out := []ReverseSuggestion{}
|
||||
for _, sc := range scopes {
|
||||
@@ -315,15 +349,24 @@ func ReverseSuggestions(st *model.State, exposures []validate.Exposure, local []
|
||||
continue
|
||||
}
|
||||
p, group, why := suggestedPrefix(a, local)
|
||||
if seen[p] || slices.Contains(sc.dismissed, p.String()) {
|
||||
hidden := sc.hidden
|
||||
if seen[p] {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(sc.dismissed, p.String()) {
|
||||
if !opt.All {
|
||||
continue
|
||||
}
|
||||
if hidden == "" {
|
||||
hidden = "dismissed"
|
||||
}
|
||||
}
|
||||
seen[p] = true
|
||||
name, err := dnsname.ReverseZone(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s := ReverseSuggestion{ViewID: sc.id, View: viewName(st, sc.id), Prefix: p.String(), Zone: strings.TrimSuffix(name, "."), Group: group, Why: why, Samples: []string{}}
|
||||
s := ReverseSuggestion{ViewID: sc.id, View: viewName(st, sc.id), Prefix: p.String(), Zone: strings.TrimSuffix(name, "."), Group: group, Hidden: hidden, Why: why, Samples: []string{}}
|
||||
fills := PTRFills(st, sc.id, name, nil)
|
||||
s.Fill = len(fills)
|
||||
for i, f := range fills {
|
||||
@@ -365,13 +408,14 @@ func suggestedPrefix(a netip.Addr, local []netip.Addr) (netip.Prefix, string, st
|
||||
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 {
|
||||
// ReverseSuggestions lists the reverse zones the views lack (opt.All:
|
||||
// also where suggestions aren't made unasked).
|
||||
func (e *Engine) ReverseSuggestions(st *model.State, opt ReverseOptions) []ReverseSuggestion {
|
||||
var local []netip.Addr
|
||||
if e.cfg.LocalAddrs != nil {
|
||||
local = e.cfg.LocalAddrs()
|
||||
}
|
||||
return ReverseSuggestions(st, e.Exposures(st), local)
|
||||
return ReverseCandidates(st, e.Exposures(st), local, opt)
|
||||
}
|
||||
|
||||
// reverseDismiss expands reverse.dismiss and reverse.undismiss.
|
||||
|
||||
@@ -235,3 +235,30 @@ func TestChildZoneTakesRecords(t *testing.T) {
|
||||
t.Error("the delegation left the parent")
|
||||
}
|
||||
}
|
||||
|
||||
// With All, the networks no suggestion is made for unasked are listed too,
|
||||
// marked: views with suggestions off, dismissed ones, public views.
|
||||
func TestReverseCandidatesAll(t *testing.T) {
|
||||
env := linkedEnv(t)
|
||||
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("example.com"), View: model.Ptr("external"), LinkWith: "internal"}))
|
||||
env.submit(env.admin, NewRequest(OpRRSetReplace, 0, RRSetInput{Zone: "example.com", View: "internal", Name: "nas", Type: "AAAA", Data: []string{"2a02:8013:7f3d:10::5"}, Local: model.Ptr(true)}))
|
||||
env.submit(env.admin, NewRequest(OpReverseDismiss, 0, DismissInput{View: "internal", Prefix: "192.0.2.10/32"}))
|
||||
internal := env.state().ViewByName("internal")
|
||||
env.submit(env.admin, NewRequest(OpViewUpdate, internal.ID, ViewInput{ReverseSuggestions: model.Ptr(false)}))
|
||||
list := func(opt ReverseOptions) []string {
|
||||
var out []string
|
||||
for _, s := range ReverseCandidates(env.state(), env.e.Exposures(env.state()), nil, opt) {
|
||||
out = append(out, s.View+" "+s.Prefix+" "+s.Hidden)
|
||||
}
|
||||
return out
|
||||
}
|
||||
want(t, "unasked", list(ReverseOptions{}))
|
||||
want(t, "internal, all", list(ReverseOptions{All: true, View: "internal"}),
|
||||
"internal 192.0.2.10/32 suggestions off", "internal 192.0.2.25/32 suggestions off", "internal 2a02:8013:7f3d:10::5/128 suggestions off")
|
||||
got := list(ReverseOptions{All: true, View: "external"})
|
||||
want(t, "external, all", got, "external 192.0.2.10/32 public view", "external 192.0.2.25/32 public view")
|
||||
|
||||
env.submit(env.admin, NewRequest(OpViewUpdate, internal.ID, ViewInput{ReverseSuggestions: model.Ptr(true)}))
|
||||
want(t, "dismissed", list(ReverseOptions{All: true, View: "internal"}),
|
||||
"internal 192.0.2.10/32 dismissed", "internal 192.0.2.25/32 ", "internal 2a02:8013:7f3d:10::5/128 ")
|
||||
}
|
||||
Reference in new issue
Block a user