Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a93373687e | ||
|
|
6cd3f6ef4e | ||
|
|
d18fe18ccd |
No files matched your search
@@ -42,6 +42,26 @@ All notable changes to Jarl are documented here. The format follows
|
||||
|
||||
### Changed
|
||||
|
||||
- A reverse zone's missing PTR records count only its own view's A and
|
||||
AAAA records. An address only another view's records use (an internal
|
||||
host, seen from the vps view's copy of `10.0.10.in-addr.arpa`) is no
|
||||
longer reported as missing; it shows apart, as `Other views` in `jarl
|
||||
zone show`, `ptr_other_views` in the API and a note on the zone's page.
|
||||
Filling (`--fill`, `jarl zone fill-ptr`, the page's button) names those
|
||||
from the other view only when asked, with `--other-views`
|
||||
(`fill_other_views`, `?other_views=1`, a checkbox in the New reverse
|
||||
zone dialog), so one view's host names don't leak into another. When
|
||||
there is nothing else to fill, Jarl says how many there are and how to
|
||||
add them.
|
||||
|
||||
- Records, zones and validation findings sort numeric labels by value, so
|
||||
`2.10.0.10.in-addr.arpa` comes before `101.10.0.10.in-addr.arpa` in the
|
||||
web UI, the CLI and the rendered zone files. Names still group by label
|
||||
from the right (subdomains stay under their parent); other labels keep
|
||||
the old byte order (`host10` before `host2`). Zone files are rewritten
|
||||
in the new order the next time they change; named doesn't care about
|
||||
record order.
|
||||
|
||||
- `cname_chain` no longer warns about one CNAME pointing at another (a
|
||||
service name at a host alias is common, and named answers the chain at
|
||||
once). A chain of three or more CNAMEs gets an info note that shows
|
||||
|
||||
@@ -203,11 +203,13 @@ review step.
|
||||
## Status
|
||||
|
||||
Version 0.6.0 is the current release, and 0.6.1 is in testing
|
||||
(0.6.1-rc.1): it gets certificates onto your other hosts (`jarl cert
|
||||
(0.6.1-rc.2): it gets certificates onto your other hosts (`jarl cert
|
||||
get`: Jarl obtains and renews them, a host downloads them and runs a
|
||||
hook, a daily timer keeps them current), exports certificates, fixes
|
||||
PTR records and overrides in linked zones, and makes one-view tokens
|
||||
(such as ACME tokens for the external view) work with linked zones.
|
||||
Reverse zones list addresses in numeric order and no longer report
|
||||
another view's hosts as missing PTR records.
|
||||
0.6.0 makes reverse zones easy (suggested from your records,
|
||||
created in the right view and filled, PTR records kept in step), rebuilds
|
||||
comparing a zone's views, lets you explore the statistics down to each
|
||||
|
||||
+27
-7
@@ -101,6 +101,9 @@ func zoneCmd() *cobra.Command {
|
||||
if z.PTRMissing > 0 {
|
||||
fmt.Fprintf(w, "Missing PTRs:\t%d (addresses the view's records use here; add them: jarl zone fill-ptr %s)\n", z.PTRMissing, args[0])
|
||||
}
|
||||
if z.PTROtherViews > 0 {
|
||||
fmt.Fprintf(w, "Other views:\t%d addresses here only other views' records use, without a PTR record (to name them from there: jarl zone fill-ptr %s --other-views)\n", z.PTROtherViews, args[0])
|
||||
}
|
||||
}
|
||||
if len(z.LinkedWith) > 0 {
|
||||
fmt.Fprintf(w, "Linked with:\t%s (shared records are kept in step; 'jarl record list' shows which are local)\n", strings.Join(z.LinkedWith, ", "))
|
||||
@@ -119,7 +122,7 @@ func zoneCmd() *cobra.Command {
|
||||
var ns, records, primaries, forwarders, opts []string
|
||||
var contact, comment, cloneFrom, file, inView, forward, linkWith string
|
||||
var ttl uint32
|
||||
var fill bool
|
||||
var fill, fillOther bool
|
||||
create := &cobra.Command{
|
||||
Use: "create NAME",
|
||||
Short: "Create a zone",
|
||||
@@ -128,7 +131,8 @@ create its reverse zone: a /32 (or /128) gives a zone for that one address,
|
||||
and a network between octet boundaries, like a /22, gets one zone per /24
|
||||
(created together). Without --view, a reverse zone goes to the private
|
||||
view whose records use its addresses; --fill adds a PTR record for each
|
||||
of them (docs/reverse.md).
|
||||
of them, and --other-views also for the addresses only other views'
|
||||
records use, named from there (docs/reverse.md).
|
||||
|
||||
jarl zone create example.com --ns ns1.example.com. --record "ns1 A 192.0.2.1"
|
||||
jarl zone create 192.168.1.0/24
|
||||
@@ -169,9 +173,12 @@ of them (docs/reverse.md).
|
||||
if linkWith != "" {
|
||||
body["link_with"] = linkWith
|
||||
}
|
||||
if fill {
|
||||
if fill || fillOther {
|
||||
body["fill"] = true
|
||||
}
|
||||
if fillOther {
|
||||
body["fill_other_views"] = true
|
||||
}
|
||||
var recs []map[string]any
|
||||
for _, r := range records {
|
||||
f := strings.Fields(r)
|
||||
@@ -245,6 +252,7 @@ of them (docs/reverse.md).
|
||||
create.Flags().StringVar(&linkWith, "link-with", "", "link with the zone of the same name in this view: copy its shared records and keep them in step (docs/views.md#linked-zones)")
|
||||
create.Flags().StringVar(&comment, "comment", "", "comment")
|
||||
create.Flags().BoolVar(&fill, "fill", false, "reverse zones: add PTR records for the view's A and AAAA records in the network")
|
||||
create.Flags().BoolVar(&fillOther, "other-views", false, "reverse zones: --fill, also naming the addresses only other views' records use")
|
||||
|
||||
var refresh, retry, expire, minimum, serial uint32
|
||||
var mname, policy, moveView string
|
||||
@@ -1168,15 +1176,21 @@ func terminalWidth() int {
|
||||
|
||||
func zoneFillPTRCmd() *cobra.Command {
|
||||
var view string
|
||||
var otherViews bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "fill-ptr ZONE [--view VIEW]",
|
||||
Use: "fill-ptr ZONE [--view VIEW] [--other-views]",
|
||||
Short: "Add the PTR records a reverse zone lacks for its view's A and AAAA records",
|
||||
Long: `Adds a PTR record to a reverse zone for every address in its network that
|
||||
the view's A and AAAA records use and that has none yet. An address with
|
||||
several names gets the mail or name server's name, else a host's rather
|
||||
than the domain's, else the shortest (docs/reverse.md).`,
|
||||
than the domain's, else the shortest (docs/reverse.md).
|
||||
|
||||
--other-views also adds the addresses in the network that only other
|
||||
views' records use, naming them from there. That publishes those views'
|
||||
host names in this one, so it is never done unasked.`,
|
||||
Example: ` jarl zone fill-ptr 10.0.10.0/24
|
||||
jarl zone fill-ptr 10.0.10.in-addr.arpa --view internal`,
|
||||
jarl zone fill-ptr 10.0.10.in-addr.arpa --view internal
|
||||
jarl zone fill-ptr 10.0.10.0/24 --view vps --other-views`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newClient()
|
||||
@@ -1184,6 +1198,9 @@ than the domain's, else the shortest (docs/reverse.md).`,
|
||||
return err
|
||||
}
|
||||
path, qv := zonePath(args[0], view)
|
||||
if otherViews {
|
||||
qv.Set("other_views", "1")
|
||||
}
|
||||
resp, err := c.Mutate(ctx(), "POST", path+"/fill-ptr", qv, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1192,6 +1209,7 @@ than the domain's, else the shortest (docs/reverse.md).`,
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&view, "view", "", "view of the zone")
|
||||
cmd.Flags().BoolVar(&otherViews, "other-views", false, "also name the addresses only other views' records use")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1231,7 +1249,9 @@ again). A view can stop suggestions: jarl view update V
|
||||
--all lists every network the views' records use without a reverse zone,
|
||||
also where nothing is suggested unasked (a view with suggestions off, a
|
||||
dismissed network, a public view), to create one by hand; --view narrows
|
||||
the list to one view. Change the size as you need: a /48 suggested for
|
||||
the list to one view and adds the networks only other views' records use
|
||||
(hidden "records in VIEW"): create one with --fill --other-views to name
|
||||
them from there. Change the size as you need: a /48 suggested for
|
||||
IPv6 can be created as a /56 or /64 instead.`,
|
||||
Example: ` jarl zone suggest-reverse
|
||||
jarl zone suggest-reverse --create
|
||||
|
||||
+5
-3
@@ -191,6 +191,7 @@ ZoneInput:
|
||||
{
|
||||
"name": "adbraun.lol", // or a CIDR like "192.168.1.0/24" for a reverse zone
|
||||
"fill": true, // reverse zones: PTR records for the view's A/AAAA records there
|
||||
"fill_other_views": true, // with fill: also for addresses only other views' records use
|
||||
"view": "internal", // or "view_id"; moving a zone = PATCH with another view
|
||||
"type": "primary", // secondary, stub, static-stub, forward, mirror, hint, redirect, in-view
|
||||
"in_view": "internal", // type in-view only
|
||||
@@ -212,8 +213,9 @@ ZoneInput:
|
||||
|
||||
A reverse zone created without a view goes to the private view whose
|
||||
records use its addresses. Zones come back with `prefix` (reverse zones:
|
||||
the network) and `ptr_missing` (addresses in it that the view's A and AAAA
|
||||
records use, without a PTR record).
|
||||
the network), `ptr_missing` (addresses in it that the view's A and AAAA
|
||||
records use, without a PTR record) and `ptr_other_views` (addresses in it
|
||||
without a PTR record that only other views' records use; not missing).
|
||||
|
||||
`/serials` and `/delegation` send DNS queries from the server, and only
|
||||
when called ([operations.md](operations.md#checking-name-servers-and-the-delegation)).
|
||||
@@ -372,7 +374,7 @@ and of any type, in view order (`[{"id": 289, "view": "external", "type":
|
||||
|--------|------|------|
|
||||
| POST | `/zones/{zone}/link` | `{"with": ["external"]}` (default: every view with that zone). Names and types that are the same everywhere become shared, the rest stay local |
|
||||
| POST | `/zones/{zone}/unlink` | Takes the zone out of its link; its records stay, all local |
|
||||
| POST | `/zones/{zone}/fill-ptr` | Adds the PTR records a reverse zone lacks for its view's A and AAAA records ([reverse.md](reverse.md)) |
|
||||
| POST | `/zones/{zone}/fill-ptr` | Adds the PTR records a reverse zone lacks for its view's A and AAAA records; `?other_views=1` also for the addresses only other views' records use, named from there ([reverse.md](reverse.md)) |
|
||||
| GET | `/reverse-zones/suggestions` | The reverse zones the private views lack (`?all=1`: also the ones not suggested unasked, with `hidden`: public view, suggestions off, dismissed; `?view=`: one view): `view_id`, `view`, `prefix`, `zone`, `group` (`yours`, or `public`: only if the address is yours), `why`, `fill` (PTR records it would get), `samples` |
|
||||
| POST | `/reverse-zones/dismiss` | `{"view": "internal", "prefix": "185.199.108.153/32"}` (or `"prefixes": [...]` for several): don't suggest it again (`/undismiss` brings it back). A view's `reverse_suggestions: false` (PATCH `/views/{id}`) stops its suggestions |
|
||||
| GET | `/zones/{zone}/compare` | The zone compared with the same zone in other views, from its own view ([views.md](views.md#comparing-views)): `?with=a,b` (default: all), `?name=www&type=A` (one name, the same records included), `?all=1`. Returns `base`, `views`, `linked`, `summary` (per other view: `different`, `only_base`, `only_other`, `same`), `different` and `only` (`views`, `rows`) rows (name, `fqdn`, type, per view `records`, `data`, `ids`, `ttl`, `linked`), `same`, `same_rows`. A zone named without its view is compared from the first view that has it |
|
||||
|
||||
@@ -179,6 +179,7 @@ jarl zone create 10.0.8.0/22 # four zones, 8-11.0.10.i
|
||||
jarl zone create 10.0.10.100/32 # a zone for just that address
|
||||
jarl zone create 10.0.10.0/24 --fill # in the view whose records use it, with their PTRs
|
||||
jarl zone fill-ptr 10.0.10.0/24 # add the PTRs it lacks (zone commands take a network too)
|
||||
jarl zone fill-ptr 10.0.10.0/24 --view vps --other-views # also name addresses only other views' records use
|
||||
jarl zone suggest-reverse [--create] # reverse zones your private views lack (--create: yours)
|
||||
jarl zone suggest-reverse --dismiss 185.199.108.153/32,185.199.109.153/32 --view internal # not yours: don't suggest them again
|
||||
jarl zone suggest-reverse --all [--view V] # every network without a reverse zone, also the hidden ones
|
||||
|
||||
+19
-5
@@ -97,10 +97,19 @@ jarl zone create 2a02:8013:7f3d:10::/64 --view internal --fill
|
||||
the dialog's View), the list also has the addresses only other views'
|
||||
records use, marked "records in external": say a VPS's AAAA record that
|
||||
only the external view has. Fill names those from the other view's
|
||||
record, when the zone's own view has none for the address, and says so.
|
||||
Keeping PTR records in step (below) stays within a zone's own view and
|
||||
its linked views: a later change to that external record doesn't reach
|
||||
the internal reverse zone.
|
||||
record only when asked: `--other-views` (with `--fill` or `jarl zone
|
||||
fill-ptr`), or the dialog's **Also for addresses only other views'
|
||||
records use**, which is ticked for you when you pick a "records in"
|
||||
network. Otherwise one view's host names would turn up in another's
|
||||
reverse zone, which is what views are for keeping apart. Jarl says which
|
||||
names came from another view. Keeping PTR records in step (below) stays
|
||||
within a zone's own view and its linked views: a later change to that
|
||||
external record doesn't reach the internal reverse zone.
|
||||
|
||||
```sh
|
||||
jarl zone create 2600:3c01::f03c:93ff:fe2b:1/128 --view internal --fill --other-views
|
||||
jarl zone fill-ptr 10.0.10.0/24 --view vps --other-views
|
||||
```
|
||||
|
||||
## Creating and filling
|
||||
|
||||
@@ -135,7 +144,12 @@ Jarl says which name it chose, and which it didn't, so you can change it.
|
||||
A reverse zone's page shows the addresses that still lack a PTR record and
|
||||
the name each would get (the first 25), with a button to add them. `jarl
|
||||
zone show` shows how many as `Missing PTRs`; the API lists them in
|
||||
`ptr_missing_list`.
|
||||
`ptr_missing_list`. Only the zone's own view's records count: an address
|
||||
that only another view's records use isn't missing (say internal's
|
||||
`claude.adbraun.lol` and the vps view's copy of `10.0.10.in-addr.arpa`).
|
||||
Those are shown apart, as `Other views` in `jarl zone show`
|
||||
(`ptr_other_views` in the API) and a quiet note with a **Name them from
|
||||
there** button on the page; filling never adds them unasked.
|
||||
|
||||
## Keeping PTR records in step
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ type Zone struct {
|
||||
// PTRMissingList are the first 25 of them: the PTR records adding
|
||||
// them would create.
|
||||
PTRMissingList []PTRFill `json:"ptr_missing_list,omitempty"`
|
||||
// PTROtherViews counts the addresses in it, without a PTR record,
|
||||
// that only other views' records use: not missing, but fill-ptr with
|
||||
// other_views=1 can name them from there.
|
||||
PTROtherViews int `json:"ptr_other_views,omitempty"`
|
||||
// Pending is set when listing with a changeset: add, change or delete.
|
||||
Pending string `json:"pending,omitempty"`
|
||||
}
|
||||
|
||||
@@ -471,8 +471,8 @@ func (x *expander) zoneCreate(req Request) ([]model.Op, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
fill := in.Fill
|
||||
in.Fill = false
|
||||
fill, fillOther := in.Fill, in.FillOtherViews
|
||||
in.Fill, in.FillOtherViews = false, false
|
||||
if fill {
|
||||
if zt != model.ZonePrimary {
|
||||
return nil, errf(KindBadRequest, "fill only applies to primary zones")
|
||||
@@ -484,10 +484,15 @@ func (x *expander) zoneCreate(req Request) ([]model.Op, error) {
|
||||
if in.ViewID != nil {
|
||||
vid = *in.ViewID
|
||||
}
|
||||
fills := PTRFills(x.st, vid, name, moved)
|
||||
fills := PTRFills(x.st, vid, name, moved, fillOther)
|
||||
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)))
|
||||
}
|
||||
if !fillOther {
|
||||
if h := otherViewsHint(x.st, vid, name, moved, len(fills), "to name them from there, fill it with other views (jarl zone fill-ptr ZONE --other-views)"); h != "" {
|
||||
x.warnings = append(x.warnings, h)
|
||||
}
|
||||
}
|
||||
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)})
|
||||
|
||||
@@ -163,8 +163,11 @@ type ZoneInput struct {
|
||||
// step (docs/reverse.md).
|
||||
AutoPTR *bool `json:"auto_ptr,omitempty"`
|
||||
// Fill (create only, reverse zones) adds a PTR record for every
|
||||
// address of the view's A and AAAA records in the zone's range.
|
||||
Fill bool `json:"fill,omitempty"`
|
||||
// address of the view's A and AAAA records in the zone's range;
|
||||
// FillOtherViews also for the addresses only other views' records
|
||||
// use, named from there.
|
||||
Fill bool `json:"fill,omitempty"`
|
||||
FillOtherViews bool `json:"fill_other_views,omitempty"`
|
||||
|
||||
// Create only: name servers for the apex NS records (default: the
|
||||
// configured default name servers), additional records, and a zone to
|
||||
@@ -224,6 +227,13 @@ type LinkInput struct {
|
||||
With []string `json:"with,omitempty"`
|
||||
}
|
||||
|
||||
// FillPTRInput is zone.fill_ptr: OtherViews also fills the addresses only
|
||||
// other views' records use, naming them from there.
|
||||
type FillPTRInput struct {
|
||||
ZoneID int64 `json:"zone_id,omitempty"`
|
||||
OtherViews bool `json:"other_views,omitempty"`
|
||||
}
|
||||
|
||||
// RRSetInput replaces all records of one name and type.
|
||||
type RRSetInput struct {
|
||||
ZoneID int64 `json:"zone_id,omitempty"`
|
||||
|
||||
+35
-10
@@ -87,11 +87,12 @@ type PTRFill struct {
|
||||
// PTRFills returns the PTR records a reverse zone named zoneName in a view
|
||||
// lacks for the addresses of the view's A and AAAA records: one per
|
||||
// address, naming its mail or name server host when it is one, otherwise
|
||||
// a host rather than the domain itself, then the shortest name. An
|
||||
// address no record of the view uses is named from another view's
|
||||
// records (fillAddresses), marked From. existing are the zone's records
|
||||
// (none for a new zone).
|
||||
func PTRFills(st *model.State, viewID int64, zoneName string, existing []*model.Record) []PTRFill {
|
||||
// a host rather than the domain itself, then the shortest name. With
|
||||
// otherViews, an address no record of the view uses is named from another
|
||||
// view's records (fillAddresses), marked From; that is only on request,
|
||||
// since it publishes one view's host names in another. existing are the
|
||||
// zone's records (none for a new zone).
|
||||
func PTRFills(st *model.State, viewID int64, zoneName string, existing []*model.Record, otherViews bool) []PTRFill {
|
||||
prefix, ok := dnsname.PrefixFromReverse(zoneName)
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -105,7 +106,11 @@ func PTRFills(st *model.State, viewID int64, zoneName string, existing []*model.
|
||||
servers := serverNames(st, viewID)
|
||||
byAddr := map[netip.Addr][]addrUse{}
|
||||
var order []netip.Addr
|
||||
for _, u := range fillAddresses(st, viewID) {
|
||||
addrs := forwardAddresses(st, viewID)
|
||||
if otherViews {
|
||||
addrs = fillAddresses(st, viewID)
|
||||
}
|
||||
for _, u := range addrs {
|
||||
if !prefix.Contains(u.addr) {
|
||||
continue
|
||||
}
|
||||
@@ -195,9 +200,23 @@ func displayAll(names []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// otherViewsHint says how many addresses only other views' records use in
|
||||
// a reverse zone, and how to add them; "" when there are none.
|
||||
func otherViewsHint(st *model.State, viewID int64, zoneName string, existing []*model.Record, own int, how string) string {
|
||||
n := len(PTRFills(st, viewID, zoneName, existing, true)) - own
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
what := "addresses"
|
||||
if n == 1 {
|
||||
what = "address"
|
||||
}
|
||||
return fmt.Sprintf("%d %s in %s only other views' records use; %s", n, what, dnsname.Display(zoneName), how)
|
||||
}
|
||||
|
||||
// zoneFillPTR expands zone.fill_ptr: the PTR records a reverse zone lacks.
|
||||
func (x *expander) zoneFillPTR(req Request) ([]model.Op, error) {
|
||||
var in LinkInput
|
||||
var in FillPTRInput
|
||||
if err := decode(req.Data, &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,9 +234,15 @@ func (x *expander) zoneFillPTR(req Request) ([]model.Op, error) {
|
||||
if _, ok := dnsname.PrefixFromReverse(z.Name); !ok {
|
||||
return nil, errf(KindBadRequest, "zone %s isn't a reverse zone of whole octets or nibbles (like 10.0.10.in-addr.arpa)", zoneDesc(x.st, z))
|
||||
}
|
||||
fills := PTRFills(x.st, z.ViewID, z.Name, x.st.ZoneRecords(z.ID))
|
||||
fills := PTRFills(x.st, z.ViewID, z.Name, x.st.ZoneRecords(z.ID), in.OtherViews)
|
||||
if len(fills) == 0 {
|
||||
return nil, errf(KindBadRequest, "nothing to add: every address of %s's A and AAAA records in %s has a PTR record", viewOrServer(x.st, z.ViewID), zoneDesc(x.st, z))
|
||||
msg := fmt.Sprintf("nothing to add: every address of %s's A and AAAA records in %s has a PTR record", viewOrServer(x.st, z.ViewID), zoneDesc(x.st, z))
|
||||
if !in.OtherViews {
|
||||
if h := otherViewsHint(x.st, z.ViewID, z.Name, x.st.ZoneRecords(z.ID), 0, "to name them from there, fill with other views (--other-views)"); h != "" {
|
||||
msg += "; " + h
|
||||
}
|
||||
}
|
||||
return nil, errf(KindBadRequest, "%s", msg)
|
||||
}
|
||||
x.warnings = append(x.warnings, fillWarnings(fills)...)
|
||||
ids, err := x.ids(len(fills))
|
||||
@@ -415,7 +440,7 @@ func ReverseCandidates(st *model.State, exposures []validate.Exposure, local []n
|
||||
continue
|
||||
}
|
||||
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)
|
||||
fills := PTRFills(st, sc.id, name, nil, opt.All && opt.View != "")
|
||||
s.Fill = len(fills)
|
||||
for i, f := range fills {
|
||||
if i == 3 {
|
||||
|
||||
@@ -69,11 +69,11 @@ func TestReverseFillAndSuggest(t *testing.T) {
|
||||
env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("fdab:abab:abab::/48")}))
|
||||
st := env.state()
|
||||
z6 := st.FindZone("b.a.b.a.b.a.b.a.b.a.d.f.ip6.arpa.", st.ViewByName("internal").ID)
|
||||
env.submit(env.admin, NewRequest(OpZoneFillPTR, z6.ID, LinkInput{}))
|
||||
env.submit(env.admin, NewRequest(OpZoneFillPTR, z6.ID, FillPTRInput{}))
|
||||
if p := env.ptrs("internal", z6.Name); len(p) != 1 || !strings.HasSuffix(p[0], " nas.example.com.") {
|
||||
t.Errorf("ip6 fill: %q", p)
|
||||
}
|
||||
if e := env.submitErr(env.admin, NewRequest(OpZoneFillPTR, z6.ID, LinkInput{})); !strings.Contains(e.Message, "nothing to add") {
|
||||
if e := env.submitErr(env.admin, NewRequest(OpZoneFillPTR, z6.ID, FillPTRInput{})); !strings.Contains(e.Message, "nothing to add") {
|
||||
t.Errorf("second fill: %v", e)
|
||||
}
|
||||
|
||||
@@ -288,15 +288,38 @@ func TestReverseFromOtherView(t *testing.T) {
|
||||
t.Errorf("suggested unasked: %+v", s)
|
||||
}
|
||||
}
|
||||
res := env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("2600:3c01::f03c:93ff:fe2b:1/128"), View: model.Ptr("internal"), Fill: true}))
|
||||
st := env.state()
|
||||
var zone string
|
||||
for _, z := range st.ZonesInView(st.ViewByName("internal").ID) {
|
||||
if strings.HasSuffix(z.Name, ".ip6.arpa.") {
|
||||
zone = z.Name
|
||||
for _, s := range ReverseCandidates(env.state(), env.e.Exposures(env.state()), nil, ReverseOptions{All: true, View: "internal"}) {
|
||||
if strings.HasPrefix(s.Prefix, "2600:") && (s.Fill != 1 || len(s.Samples) != 1) {
|
||||
t.Errorf("candidate counts the other view's address: %+v", s)
|
||||
}
|
||||
}
|
||||
if p := env.ptrs("internal", zone); len(p) != 1 || !strings.HasSuffix(p[0], " vps.example.com.") {
|
||||
// Filling doesn't take names from other views unasked: it would
|
||||
// publish one view's host names in another. It says how to.
|
||||
res := env.submit(env.admin, NewRequest(OpZoneCreate, 0, ZoneInput{Name: model.Ptr("2600:3c01::f03c:93ff:fe2b:1/128"), View: model.Ptr("internal"), Fill: true}))
|
||||
st := env.state()
|
||||
var z *model.Zone
|
||||
for _, o := range st.ZonesInView(st.ViewByName("internal").ID) {
|
||||
if strings.HasSuffix(o.Name, ".ip6.arpa.") {
|
||||
z = o
|
||||
}
|
||||
}
|
||||
if p := env.ptrs("internal", z.Name); len(p) != 0 {
|
||||
t.Errorf("filled from another view unasked: %q", p)
|
||||
}
|
||||
if !slices.ContainsFunc(res.Warnings, func(w string) bool {
|
||||
return strings.Contains(w, "1 address in") && strings.Contains(w, "--other-views")
|
||||
}) {
|
||||
t.Errorf("warnings %q", res.Warnings)
|
||||
}
|
||||
// Not missing: no record of the zone's own view uses the address.
|
||||
if f := PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID), false); len(f) != 0 {
|
||||
t.Errorf("missing %+v", f)
|
||||
}
|
||||
if e := env.submitErr(env.admin, NewRequest(OpZoneFillPTR, z.ID, FillPTRInput{})); !strings.Contains(e.Message, "nothing to add") || !strings.Contains(e.Message, "--other-views") {
|
||||
t.Errorf("fill-ptr: %v", e)
|
||||
}
|
||||
res = env.submit(env.admin, NewRequest(OpZoneFillPTR, z.ID, FillPTRInput{OtherViews: true}))
|
||||
if p := env.ptrs("internal", z.Name); len(p) != 1 || !strings.HasSuffix(p[0], " vps.example.com.") {
|
||||
t.Errorf("filled %q", p)
|
||||
}
|
||||
if !slices.ContainsFunc(res.Warnings, func(w string) bool { return strings.Contains(w, "only view external has a record for it") }) {
|
||||
|
||||
@@ -204,7 +204,7 @@ func (s *State) SortedBlockRules() []*BlockRule {
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
a, b := out[i], out[j]
|
||||
if a.Name != b.Name {
|
||||
return CanonicalLess(a.Name+".", b.Name+".")
|
||||
return NameLess(a.Name+".", b.Name+".")
|
||||
}
|
||||
if a.Exact != b.Exact {
|
||||
return !a.Exact
|
||||
|
||||
+41
-10
@@ -524,14 +524,14 @@ func (s *State) SortedZones() []*Zone {
|
||||
if pi, pj := pos[out[i].ViewID], pos[out[j].ViewID]; pi != pj {
|
||||
return pi < pj
|
||||
}
|
||||
return CanonicalLess(out[i].Name, out[j].Name)
|
||||
return NameLess(out[i].Name, out[j].Name)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// SortZones sorts zones by name in DNS canonical order.
|
||||
// SortZones sorts zones by name (see NameLess).
|
||||
func SortZones(zs []*Zone) {
|
||||
sort.Slice(zs, func(i, j int) bool { return CanonicalLess(zs[i].Name, zs[j].Name) })
|
||||
sort.Slice(zs, func(i, j int) bool { return NameLess(zs[i].Name, zs[j].Name) })
|
||||
}
|
||||
|
||||
// FindZone finds a zone by name within a view (viewID 0 for no view).
|
||||
@@ -646,7 +646,7 @@ func typeOrder(t string) string {
|
||||
}
|
||||
|
||||
// SortRecords sorts records in zone-file order: apex first, then names in
|
||||
// DNS canonical order (so subdomains group together), then by type and
|
||||
// NameLess order (so subdomains group together), then by type and
|
||||
// data.
|
||||
func SortRecords(rs []*Record, z *Zone) {
|
||||
apex := ""
|
||||
@@ -662,7 +662,7 @@ func SortRecords(rs []*Record, z *Zone) {
|
||||
if b.Name == apex {
|
||||
return false
|
||||
}
|
||||
return CanonicalLess(a.Name, b.Name)
|
||||
return NameLess(a.Name, b.Name)
|
||||
}
|
||||
if a.Type != b.Type {
|
||||
return typeOrder(a.Type) < typeOrder(b.Type)
|
||||
@@ -674,19 +674,50 @@ func SortRecords(rs []*Record, z *Zone) {
|
||||
})
|
||||
}
|
||||
|
||||
// CanonicalLess compares names in DNS canonical order (RFC 4034 section
|
||||
// 6.1): by label from the right.
|
||||
func CanonicalLess(a, b string) bool {
|
||||
// NameLess compares names label by label from the right, like DNS
|
||||
// canonical order (RFC 4034 section 6.1), except that two all-digit
|
||||
// labels compare as numbers, so 2.10.0.10.in-addr.arpa comes before
|
||||
// 101.10.0.10.in-addr.arpa. It orders names for people and zone files;
|
||||
// BIND does its own canonical ordering where DNSSEC needs it.
|
||||
func NameLess(a, b string) bool {
|
||||
la := strings.Split(strings.TrimSuffix(a, "."), ".")
|
||||
lb := strings.Split(strings.TrimSuffix(b, "."), ".")
|
||||
for i, j := len(la)-1, len(lb)-1; i >= 0 && j >= 0; i, j = i-1, j-1 {
|
||||
if la[i] != lb[j] {
|
||||
return la[i] < lb[j]
|
||||
if c := compareLabel(la[i], lb[j]); c != 0 {
|
||||
return c < 0
|
||||
}
|
||||
}
|
||||
return len(la) < len(lb)
|
||||
}
|
||||
|
||||
func compareLabel(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
if allDigits(a) && allDigits(b) {
|
||||
na, nb := strings.TrimLeft(a, "0"), strings.TrimLeft(b, "0")
|
||||
if len(na) != len(nb) {
|
||||
return len(na) - len(nb)
|
||||
}
|
||||
if c := strings.Compare(na, nb); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func allDigits(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RRSetKey identifies an RRset.
|
||||
type RRSetKey struct {
|
||||
Name string
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNameLess(t *testing.T) {
|
||||
want := []string{
|
||||
"10.0.10.in-addr.arpa.",
|
||||
"1.10.0.10.in-addr.arpa.",
|
||||
"02.10.0.10.in-addr.arpa.",
|
||||
"2.10.0.10.in-addr.arpa.",
|
||||
"10.10.0.10.in-addr.arpa.",
|
||||
"101.10.0.10.in-addr.arpa.",
|
||||
"x.101.10.0.10.in-addr.arpa.",
|
||||
"254.10.0.10.in-addr.arpa.",
|
||||
"a.10.0.10.in-addr.arpa.",
|
||||
}
|
||||
got := slices.Clone(want)
|
||||
slices.Reverse(got)
|
||||
slices.SortFunc(got, func(a, b string) int {
|
||||
if NameLess(a, b) {
|
||||
return -1
|
||||
}
|
||||
if NameLess(b, a) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("got %v\nwant %v", got, want)
|
||||
}
|
||||
|
||||
// Non-numeric labels keep canonical (byte) order.
|
||||
if !NameLess("host10.example.", "host2.example.") {
|
||||
t.Error("host10 should sort before host2")
|
||||
}
|
||||
if !NameLess("example.", "www.example.") {
|
||||
t.Error("parent should sort before child")
|
||||
}
|
||||
}
|
||||
@@ -93,3 +93,27 @@ func TestPTRMissingList(t *testing.T) {
|
||||
t.Errorf("ptr_missing %v, list %v", z["ptr_missing"], z["ptr_missing_list"])
|
||||
}
|
||||
}
|
||||
|
||||
// An address only another view's records use isn't missing from a
|
||||
// reverse zone: it is counted apart, and filled only when asked.
|
||||
func TestPTROtherViews(t *testing.T) {
|
||||
e := newAPIEnv(t)
|
||||
e.do("POST", "/views", e.token, map[string]any{"name": "internal", "match_clients": []string{"10.0.0.0/8"}}, 201)
|
||||
e.do("POST", "/views", e.token, map[string]any{"name": "vps"}, 201)
|
||||
e.do("POST", "/zones", e.token, map[string]any{"name": "adbraun.lol", "view": "internal", "nameservers": []string{"ns1.example.net."},
|
||||
"records": []map[string]any{{"name": "claude", "type": "A", "data": "10.0.10.101"}}}, 201)
|
||||
for _, v := range []string{"internal", "vps"} {
|
||||
e.do("POST", "/zones", e.token, map[string]any{"name": "10.0.10.0/24", "view": v, "nameservers": []string{"ns1.example.net."}}, 201)
|
||||
}
|
||||
if z := e.do("GET", "/zones/10.0.10.in-addr.arpa?view=internal", e.token, nil, 200); z["ptr_missing"] != float64(1) || z["ptr_other_views"] != nil {
|
||||
t.Errorf("internal: ptr_missing %v, ptr_other_views %v", z["ptr_missing"], z["ptr_other_views"])
|
||||
}
|
||||
if z := e.do("GET", "/zones/10.0.10.in-addr.arpa?view=vps", e.token, nil, 200); z["ptr_missing"] != nil || z["ptr_other_views"] != float64(1) {
|
||||
t.Errorf("vps: ptr_missing %v, ptr_other_views %v", z["ptr_missing"], z["ptr_other_views"])
|
||||
}
|
||||
e.do("POST", "/zones/10.0.10.in-addr.arpa/fill-ptr?view=vps", e.token, nil, 400)
|
||||
e.do("POST", "/zones/10.0.10.in-addr.arpa/fill-ptr?view=vps&other_views=1", e.token, nil, 201)
|
||||
if z := e.do("GET", "/zones/10.0.10.in-addr.arpa?view=vps", e.token, nil, 200); z["ptr_other_views"] != nil {
|
||||
t.Errorf("vps after filling: ptr_other_views %v", z["ptr_other_views"])
|
||||
}
|
||||
}
|
||||
@@ -637,7 +637,7 @@ func (s *Server) search(w http.ResponseWriter, r *http.Request, p *auth.Principa
|
||||
}
|
||||
sort.Slice(recs, func(i, j int) bool {
|
||||
if recs[i].Name != recs[j].Name {
|
||||
return model.CanonicalLess(recs[i].Name, recs[j].Name)
|
||||
return model.NameLess(recs[i].Name, recs[j].Name)
|
||||
}
|
||||
return recs[i].ID < recs[j].ID
|
||||
})
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
)
|
||||
|
||||
// zoneFillPTR: POST /zones/{zone}/fill-ptr adds the PTR records a reverse
|
||||
// zone lacks for its view's A and AAAA records (docs/reverse.md).
|
||||
// zone lacks for its view's A and AAAA records; ?other_views=1 also for
|
||||
// the addresses only other views' records use (docs/reverse.md).
|
||||
func (s *Server) zoneFillPTR(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
st, err := s.state(r)
|
||||
if err != nil {
|
||||
@@ -18,7 +19,7 @@ func (s *Server) zoneFillPTR(w http.ResponseWriter, r *http.Request, p *auth.Pri
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.mutate(w, r, p, "", []engine.Request{engine.NewRequest(engine.OpZoneFillPTR, z.ID, engine.LinkInput{ZoneID: z.ID})}, nil)
|
||||
return s.mutate(w, r, p, "", []engine.Request{engine.NewRequest(engine.OpZoneFillPTR, z.ID, engine.FillPTRInput{ZoneID: z.ID, OtherViews: queryBool(r, "other_views")})}, nil)
|
||||
}
|
||||
|
||||
// reverseSuggestions: GET /reverse-zones/suggestions lists the reverse
|
||||
|
||||
@@ -215,7 +215,7 @@ func clientNames(st *model.State, want map[netip.Addr]bool, readable func(*model
|
||||
}
|
||||
slices.SortFunc(recs, func(a, b *model.Record) int {
|
||||
if a.Name != b.Name {
|
||||
if model.CanonicalLess(a.Name, b.Name) {
|
||||
if model.NameLess(a.Name, b.Name) {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
|
||||
@@ -41,8 +41,11 @@ func (s *Server) presentZone(st *model.State, z *model.Zone, counts map[int64]re
|
||||
if pf, ok := dnsname.PrefixFromReverse(z.Name); ok {
|
||||
out.Prefix = pf.String()
|
||||
if z.Type == model.ZonePrimary {
|
||||
fills := engine.PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID))
|
||||
fills := engine.PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID), false)
|
||||
out.PTRMissing = len(fills)
|
||||
if z.ViewID != 0 {
|
||||
out.PTROtherViews = len(engine.PTRFills(st, z.ViewID, z.Name, st.ZoneRecords(z.ID), true)) - len(fills)
|
||||
}
|
||||
for _, f := range fills[:min(len(fills), 25)] {
|
||||
out.PTRMissingList = append(out.PTRMissingList, api.PTRFill(f))
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ func (c *checker) checkZoneRecords(z *model.Zone) {
|
||||
for n := range ix.types {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Slice(names, func(i, j int) bool { return model.CanonicalLess(names[i], names[j]) })
|
||||
sort.Slice(names, func(i, j int) bool { return model.NameLess(names[i], names[j]) })
|
||||
|
||||
for _, name := range names {
|
||||
types := ix.types[name]
|
||||
|
||||
@@ -189,7 +189,7 @@ func sortProblems(ps []Problem) {
|
||||
return a.Zone < b.Zone
|
||||
}
|
||||
if a.Name != b.Name {
|
||||
return model.CanonicalLess(a.Name, b.Name)
|
||||
return model.NameLess(a.Name, b.Name)
|
||||
}
|
||||
return a.Message < b.Message
|
||||
})
|
||||
|
||||
@@ -72,7 +72,10 @@ export async function render(el, params, query) {
|
||||
h("span", { class: "mono" }, f.address), " → ", h("span", { class: "mono" }, display(f.target)),
|
||||
f.from ? h("span", { class: "muted" }, " (from view " + f.from + ")") : null,
|
||||
f.others && f.others.length ? h("span", { class: "muted" }, " (also " + f.others.map(display).join(", ") + ")") : null)),
|
||||
zone.ptr_missing > (zone.ptr_missing_list || []).length ? h("li", { class: "muted" }, "and " + (zone.ptr_missing - zone.ptr_missing_list.length) + " more") : null)) : null;
|
||||
zone.ptr_missing > (zone.ptr_missing_list || []).length ? h("li", { class: "muted" }, "and " + (zone.ptr_missing - zone.ptr_missing_list.length) + " more") : null))
|
||||
: zone.ptr_other_views && can("zones", true) ? h("div", { class: "muted small" },
|
||||
plural(zone.ptr_other_views, "address", "addresses") + " in " + zone.prefix + " only other views' records use, without a PTR record here. ",
|
||||
h("button", { class: "small", title: "Adds PTR records naming them from the other views' records, publishing those names in view " + zone.view, onclick: () => fillPTR(zone, true) }, "Name them from there")) : null;
|
||||
const tabBar = h("div", { class: "tabs" }, tabs.map(([k, label]) => h("a", { href: "#/zones/" + id + "/" + k, class: k === tab ? "active" : "" }, label)));
|
||||
// The header and tabs show at once. Below them, switching tabs keeps the
|
||||
// previous tab's content (faded) until the new one is loaded, rather
|
||||
@@ -951,9 +954,9 @@ async function toggleZone(zone) {
|
||||
}
|
||||
|
||||
// fillPTR adds the PTR records a reverse zone lacks (docs/reverse.md).
|
||||
async function fillPTR(zone) {
|
||||
async function fillPTR(zone, otherViews) {
|
||||
try {
|
||||
const r = await change("POST", "/zones/" + zone.id + "/fill-ptr");
|
||||
const r = await change("POST", "/zones/" + zone.id + "/fill-ptr", undefined, otherViews ? { query: { other_views: "1" } } : {});
|
||||
toast(describeResult(r));
|
||||
(r.warnings || []).forEach((w) => toast(w, "warn", 10000));
|
||||
rerender();
|
||||
|
||||
@@ -103,7 +103,10 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
cloneSel.addEventListener("change", syncLink);
|
||||
const comment = input();
|
||||
const fill = checkbox("Add a PTR record for each address the view's A and AAAA records use in this network", true);
|
||||
const fillRow = field("PTR records", fill, "Names already in your zones, so the reverse zone answers from the start. Leave it off to add them yourself.");
|
||||
// Off unless asked (or a network only other views use is picked): it
|
||||
// publishes one view's host names in another.
|
||||
const fillOther = checkbox("Also for addresses only other views' records use, named from there", false);
|
||||
const fillRow = field("PTR records", h("div", null, fill, views.length ? fillOther : null), "Names already in your zones, so the reverse zone answers from the start. Leave it off to add them yourself.");
|
||||
const updateClones = () => {
|
||||
// Records can only be copied into a single zone.
|
||||
const n = (kind === "reverse" ? reverseZoneFor(name.value).name || "-" : name.value.trim().toLowerCase().replace(/\.$/, "")) + ".";
|
||||
@@ -206,6 +209,7 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
name.value = c.prefix;
|
||||
sizeFrom = c.samples.length ? c.samples[0].split(" ")[0] : null;
|
||||
if (views.length && c.view) view.value = c.view;
|
||||
if ((c.hidden || "").startsWith("records in ")) { fill.input.checked = true; fillOther.input.checked = true; }
|
||||
changed();
|
||||
} },
|
||||
h("span", { class: "mono" }, c.prefix),
|
||||
@@ -273,7 +277,10 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
if (view.value) data.view = view.value;
|
||||
const opts = [];
|
||||
const list = (ta) => lines(ta.value).map((l) => l.replace(/;$/, "") + ";").join(" ");
|
||||
if (t === "primary" && kind === "reverse" && fill.input.checked) data.fill = true;
|
||||
if (t === "primary" && kind === "reverse" && fill.input.checked) {
|
||||
data.fill = true;
|
||||
if (fillOther.input.checked) data.fill_other_views = true;
|
||||
}
|
||||
if (t === "primary") {
|
||||
data.nameservers = lines(ns.value);
|
||||
if (contact.value.trim()) data.soa = { rname: contact.value.trim() };
|
||||
|
||||
+15
-1
@@ -703,6 +703,20 @@ run "fill-ptr" 0 $J zone fill-ptr fdab:99::/48
|
||||
expect_dig "filled later: IPv6 PTR" "nas.revtest.test." -x fdab:99:0:10::10
|
||||
run "fill-ptr again: nothing to add (exit 6)" 6 $J zone fill-ptr fdab:99::/48
|
||||
contains "nothing to add"
|
||||
# The same network in another view: internal's hosts aren't missing there,
|
||||
# and are named from internal only when asked.
|
||||
run "the network in external too, filled" 0 $J zone create 10.99.1.0/24 --view external --fill
|
||||
contains "only other views' records use"
|
||||
run "nothing missing there" 0 $J zone show 1.99.10.in-addr.arpa --view external
|
||||
not_contains "Missing PTRs"
|
||||
contains "^Other views: *2 addresses"
|
||||
run "fill-ptr there: nothing of its own (exit 6)" 6 $J zone fill-ptr 10.99.1.0/24 --view external
|
||||
contains "--other-views"
|
||||
run "fill-ptr --other-views" 0 $J zone fill-ptr 10.99.1.0/24 --view external --other-views
|
||||
contains "only view internal has a record for it"
|
||||
run "now named there" 0 $J record list 1.99.10.in-addr.arpa --view external
|
||||
contains "pi.revtest.test"
|
||||
run "drop the external copy" 0 $J zone delete 1.99.10.in-addr.arpa --view external --yes
|
||||
run "keep PTRs in step" 0 $J zone update revtest.test --view internal --auto-ptr
|
||||
run "zone show says so" 0 $J zone show revtest.test --view internal
|
||||
contains "^PTR records: *kept in step"
|
||||
@@ -736,7 +750,7 @@ run "and back on" 0 $J view update internal --reverse-suggestions
|
||||
run "an AAAA only in external" 0 $J record add adbraun.test vps AAAA 2600:3c01::f03c:93ff:fe2b:1 --view external
|
||||
run "offered for internal" 0 $J zone suggest-reverse --all --view internal
|
||||
contains "2600:3c01::f03c:93ff:fe2b:1/128 .*records in external"
|
||||
run "created in internal, filled from external's record" 0 $J zone create 2600:3c01::f03c:93ff:fe2b:1/128 --view internal --fill
|
||||
run "created in internal, filled from external's record" 0 $J zone create 2600:3c01::f03c:93ff:fe2b:1/128 --view internal --fill --other-views
|
||||
expect_dig "internal answers its PTR" "vps.adbraun.test." -x 2600:3c01::f03c:93ff:fe2b:1
|
||||
# A zone created inside another takes the parent's records under it.
|
||||
run "a /16 reverse zone" 0 $J zone create 10.88.0.0/16 --view internal
|
||||
|
||||
Reference in new issue
Block a user