Reverse zones made easy: suggested, filled, in the right view, PTRs kept in step
Missing reverse zones are suggested for the private views' A and AAAA records (/24 for private IPv4, /32 for a public address, /48 for IPv6) and created filled with PTR records (jarl zone suggest-reverse, the Zones page). zone create --fill and zone fill-ptr add the PTRs a reverse zone lacks; a reverse zone created without a view goes to the private view using its addresses. Per zone, auto_ptr keeps the PTR records of A and AAAA records in step on every change (database version 12). Zone commands take a network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a3b054cc86
commit
aa4b004a14
27 files changed
+1173
-38
No files matched your search
+136
-19
@@ -28,6 +28,9 @@ type expander struct {
|
||||
p *auth.Principal
|
||||
now time.Time
|
||||
warnings []string
|
||||
// exposures classifies the views as public or private (nil in tests:
|
||||
// every view private).
|
||||
exposures func(*model.State) []validate.Exposure
|
||||
}
|
||||
|
||||
func (x *expander) ids(n int) ([]int64, error) {
|
||||
@@ -137,7 +140,7 @@ func (x *expander) expand(req Request) ([]model.Op, error) {
|
||||
return nil, err
|
||||
}
|
||||
switch req.Op {
|
||||
case OpRecordCreate, OpRecordUpdate, OpRecordDelete, OpRRSetReplace, OpRecordDisable, OpRecordEnable:
|
||||
case OpRecordCreate, OpRecordUpdate, OpRecordDelete, OpRRSetReplace, OpRecordDisable, OpRecordEnable, OpZoneFillPTR:
|
||||
// Changes to linked zones reach the other zones of the link.
|
||||
return x.linkSync(ops)
|
||||
}
|
||||
@@ -152,6 +155,8 @@ func (x *expander) expandOne(req Request) ([]model.Op, error) {
|
||||
return x.zoneUnlink(req)
|
||||
case OpZoneCreate:
|
||||
return x.zoneCreate(req)
|
||||
case OpZoneFillPTR:
|
||||
return x.zoneFillPTR(req)
|
||||
case OpZoneUpdate, OpZoneDelete:
|
||||
z := x.st.Zones[req.ID]
|
||||
if z == nil {
|
||||
@@ -376,6 +381,7 @@ func (x *expander) zoneCreate(req Request) ([]model.Op, error) {
|
||||
return nil, errf(KindBadRequest, "%v", err)
|
||||
}
|
||||
in.Type = &zt
|
||||
viewGiven := in.View != nil || in.ViewID != nil
|
||||
if in.View != nil {
|
||||
if *in.View != "" {
|
||||
v := x.st.ViewByName(*in.View)
|
||||
@@ -386,6 +392,39 @@ func (x *expander) zoneCreate(req Request) ([]model.Op, error) {
|
||||
}
|
||||
in.View = nil
|
||||
}
|
||||
if !viewGiven && in.LinkWith == "" && in.CloneFrom == 0 && dnsname.IsReverse(name) {
|
||||
// A reverse zone belongs in the private view whose records use
|
||||
// its addresses (docs/reverse.md).
|
||||
v, err := reverseViewFor(x.st, x.publicViews(), name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v != nil {
|
||||
in.ViewID = &v.ID
|
||||
}
|
||||
}
|
||||
fill := in.Fill
|
||||
in.Fill = false
|
||||
if fill {
|
||||
if zt != model.ZonePrimary {
|
||||
return nil, errf(KindBadRequest, "fill only applies to primary zones")
|
||||
}
|
||||
if _, ok := dnsname.PrefixFromReverse(name); !ok {
|
||||
return nil, errf(KindBadRequest, "fill only applies to reverse zones of whole octets or nibbles (like 10.0.10.0/24)")
|
||||
}
|
||||
vid := int64(0)
|
||||
if in.ViewID != nil {
|
||||
vid = *in.ViewID
|
||||
}
|
||||
fills := PTRFills(x.st, vid, name, nil)
|
||||
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)))
|
||||
}
|
||||
x.warnings = append(x.warnings, fillWarnings(fills)...)
|
||||
for _, f := range fills {
|
||||
in.Records = append(in.Records, RecordInput{Name: model.Ptr(f.Name), Type: model.Ptr("PTR"), Data: model.Ptr(f.Target)})
|
||||
}
|
||||
}
|
||||
nameservers := in.Nameservers
|
||||
records := in.Records
|
||||
cloneFrom := in.CloneFrom
|
||||
@@ -638,7 +677,11 @@ func addrOf(p *rr.Parsed) (netip.Addr, bool) {
|
||||
}
|
||||
|
||||
// ptrOps returns operations creating the PTR record for an address record.
|
||||
func (x *expander) ptrCreate(z *model.Zone, owner string, p *rr.Parsed) ([]model.Op, error) {
|
||||
//
|
||||
// auto is set when the zone's auto_ptr asked rather than the request: a
|
||||
// missing reverse zone isn't worth a warning then (most addresses have
|
||||
// none), and an address whose PTR record names another host keeps it.
|
||||
func (x *expander) ptrCreate(z *model.Zone, owner string, p *rr.Parsed, auto bool) ([]model.Op, error) {
|
||||
addr, ok := addrOf(p)
|
||||
if !ok {
|
||||
return nil, errf(KindBadRequest, "ptr only applies to A and AAAA records")
|
||||
@@ -651,13 +694,13 @@ func (x *expander) ptrCreate(z *model.Zone, owner string, p *rr.Parsed) ([]model
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
more, err := x.ptrCreateIn(rz, ptrName, owner)
|
||||
more, err := x.ptrCreateIn(rz, ptrName, owner, auto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops = append(ops, more...)
|
||||
}
|
||||
if !found {
|
||||
if !found && !auto {
|
||||
x.warnings = append(x.warnings, fmt.Sprintf("no reverse zone for %s in this view, so no PTR record was added (create one: jarl zone create %s/%d)", addr, addr, addr.BitLen()))
|
||||
}
|
||||
return ops, nil
|
||||
@@ -678,7 +721,8 @@ func ptrViews(st *model.State, z *model.Zone) []int64 {
|
||||
|
||||
// ptrCreateIn returns operations creating the PTR record ptrName -> owner
|
||||
// in reverse zone rz (none when it is there; enabling it when disabled).
|
||||
func (x *expander) ptrCreateIn(rz *model.Zone, ptrName, owner string) ([]model.Op, error) {
|
||||
func (x *expander) ptrCreateIn(rz *model.Zone, ptrName, owner string, auto bool) ([]model.Op, error) {
|
||||
var other string
|
||||
for _, r := range x.st.ZoneRecords(rz.ID) {
|
||||
if r.Name == ptrName && r.Type == "PTR" {
|
||||
if strings.EqualFold(r.Data, owner) {
|
||||
@@ -688,9 +732,18 @@ func (x *expander) ptrCreateIn(rz *model.Zone, ptrName, owner string) ([]model.O
|
||||
}
|
||||
return nil, nil // already there
|
||||
}
|
||||
x.warnings = append(x.warnings, fmt.Sprintf("%s already has a PTR record (%s); adding another one", ptrName, r.Data))
|
||||
if !r.Disabled {
|
||||
other = r.Data
|
||||
}
|
||||
}
|
||||
}
|
||||
if other != "" {
|
||||
if auto {
|
||||
// One name per address: the one it has stays.
|
||||
return nil, nil
|
||||
}
|
||||
x.warnings = append(x.warnings, fmt.Sprintf("%s already has a PTR record (%s); adding another one", ptrName, other))
|
||||
}
|
||||
ids, err := x.ids(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -778,7 +831,7 @@ func (x *expander) recordCreate(req Request) ([]model.Op, error) {
|
||||
return nil, err
|
||||
}
|
||||
x.parseWarnings(owner, p)
|
||||
ptr := in.PTR
|
||||
ptr, auto := in.PTR, !in.PTR && autoPTR(z, owner, p)
|
||||
in.ZoneID, in.Zone, in.View, in.PTR = z.ID, "", "", false
|
||||
in.Name, in.Type, in.Data = &owner, &p.Type, &p.Data
|
||||
ids, err := x.ids(1)
|
||||
@@ -807,8 +860,8 @@ func (x *expander) recordCreate(req Request) ([]model.Op, error) {
|
||||
// local on a zone that isn't linked: its records are local anyway.
|
||||
in.Local = nil
|
||||
ops := []model.Op{newOp(OpRecordCreate, ids[0], 0, in, summary)}
|
||||
if ptr {
|
||||
more, err := x.ptrCreate(z, owner, p)
|
||||
if ptr || auto && (in.Disabled == nil || !*in.Disabled) {
|
||||
more, err := x.ptrCreate(z, owner, p, auto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -939,13 +992,43 @@ func (x *expander) recordUpdate(req Request) ([]model.Op, error) {
|
||||
if in.Disabled != nil {
|
||||
disabled = *in.Disabled
|
||||
}
|
||||
moved := p.Data != r.Data || owner != r.Name
|
||||
moved := p.Data != r.Data || owner != r.Name || p.Type != r.Type
|
||||
if !ptr && z.AutoPTR {
|
||||
// The zone keeps PTR records in step (docs/reverse.md): as if
|
||||
// asked, for whichever of the old and new record is an address.
|
||||
old, err := rr.Parse(z.Name, r.Name, r.Type, r.Data)
|
||||
oldAuto := err == nil && autoPTR(z, r.Name, old)
|
||||
newAuto := autoPTR(z, owner, p)
|
||||
switch {
|
||||
case moved:
|
||||
if oldAuto {
|
||||
ops = append(ops, x.ptrDelete(z, r.Name, old)...)
|
||||
}
|
||||
if newAuto && !disabled {
|
||||
more, err := x.ptrCreate(z, owner, p, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops = append(ops, more...)
|
||||
}
|
||||
case toggled && newAuto && disabled:
|
||||
ops = append(ops, x.ptrSetDisabled(z, owner, p, true)...)
|
||||
case toggled && newAuto:
|
||||
// Enables the PTR record, or adds it when it is missing.
|
||||
more, err := x.ptrCreate(z, owner, p, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops = append(ops, more...)
|
||||
}
|
||||
return ops, nil
|
||||
}
|
||||
if ptr && moved {
|
||||
if old, err := rr.Parse(z.Name, r.Name, r.Type, r.Data); err == nil {
|
||||
ops = append(ops, x.ptrDelete(z, r.Name, old)...)
|
||||
}
|
||||
if !disabled {
|
||||
more, err := x.ptrCreate(z, owner, p)
|
||||
more, err := x.ptrCreate(z, owner, p, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -961,7 +1044,7 @@ func (x *expander) recordUpdate(req Request) ([]model.Op, error) {
|
||||
if ptr && !moved && !toggled && !disabled {
|
||||
// Nothing else changes: make sure the PTR record is there (the
|
||||
// reverse zone may be newer than the record).
|
||||
more, err := x.ptrCreate(z, owner, p)
|
||||
more, err := x.ptrCreate(z, owner, p, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -988,11 +1071,9 @@ func (x *expander) recordDelete(req Request) ([]model.Op, error) {
|
||||
base = r.Version
|
||||
}
|
||||
ops := []model.Op{newOp(OpRecordDelete, r.ID, base, nil, fmt.Sprintf("delete %s %s %s", r.Name, r.Type, r.Data))}
|
||||
if in.PTR {
|
||||
z := x.st.Zones[r.ZoneID]
|
||||
if p, err := rr.Parse(z.Name, r.Name, r.Type, r.Data); err == nil {
|
||||
ops = append(ops, x.ptrDelete(z, r.Name, p)...)
|
||||
}
|
||||
z := x.st.Zones[r.ZoneID]
|
||||
if p, err := rr.Parse(z.Name, r.Name, r.Type, r.Data); err == nil && (in.PTR || autoPTR(z, r.Name, p)) {
|
||||
ops = append(ops, x.ptrDelete(z, r.Name, p)...)
|
||||
}
|
||||
return ops, nil
|
||||
}
|
||||
@@ -1032,8 +1113,19 @@ func (x *expander) recordDelete(req Request) ([]model.Op, error) {
|
||||
in.ZoneID, in.Zone, in.View, in.PTR = z.ID, "", "", false
|
||||
in.Name, in.Type = &owner, &typ
|
||||
ops := []model.Op{newOp(OpRecordDelete, 0, 0, in, "delete "+what)}
|
||||
if ptr && parsed != nil {
|
||||
switch {
|
||||
case ptr && parsed != nil:
|
||||
ops = append(ops, x.ptrDelete(z, owner, parsed)...)
|
||||
case z.AutoPTR:
|
||||
// Every matching record's PTR record goes too.
|
||||
for _, r := range x.st.RRset(z.ID, owner, typ) {
|
||||
if parsed != nil && r.Data != parsed.Data {
|
||||
continue
|
||||
}
|
||||
if p, err := rr.Parse(z.Name, r.Name, r.Type, r.Data); err == nil && autoPTR(z, r.Name, p) {
|
||||
ops = append(ops, x.ptrDelete(z, r.Name, p)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ops, nil
|
||||
}
|
||||
@@ -1169,7 +1261,32 @@ func (x *expander) rrsetReplace(req Request) ([]model.Op, error) {
|
||||
if len(canon) == 0 {
|
||||
summary = fmt.Sprintf("delete all %s records of %s%s", typ, owner, where)
|
||||
}
|
||||
return append(pre, newOp(OpRRSetReplace, 0, 0, in, summary)), nil
|
||||
ops := append(pre, newOp(OpRRSetReplace, 0, 0, in, summary))
|
||||
if z.AutoPTR && (typ == "A" || typ == "AAAA") && !strings.HasPrefix(owner, "*.") {
|
||||
// PTR records follow the addresses that go and come.
|
||||
old := map[string]bool{}
|
||||
for _, r := range x.st.RRset(z.ID, owner, typ) {
|
||||
old[r.Data] = true
|
||||
if !slices.Contains(canon, r.Data) {
|
||||
if p, err := rr.Parse(z.Name, r.Name, r.Type, r.Data); err == nil {
|
||||
ops = append(ops, x.ptrDelete(z, r.Name, p)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, d := range canon {
|
||||
if old[d] {
|
||||
continue
|
||||
}
|
||||
if _, p, err := parseRecord(z, owner, typ, d); err == nil {
|
||||
more, err := x.ptrCreate(z, owner, p, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops = append(ops, more...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ops, nil
|
||||
}
|
||||
|
||||
func (x *expander) viewCreate(req Request) ([]model.Op, error) {
|
||||
|
||||
Reference in new issue
Block a user