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
+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
|
||||
|
||||
Reference in new issue
Block a user