Reverse zones: name hosts from other views' records; search and bulk dismiss for long lists
A reverse zone's fill (and fill-ptr, the missing-PTR counts) names an address no record of its own view uses from another view's record, so an internal reverse zone can give the PTR of a host only external has an AAAA for; New reverse zone with a view chosen lists those addresses, marked. Automatic suggestions stay per view. For hundreds of addresses: the New reverse zone list has a search; on the Zones page the public addresses fold away, have a search, show 25 at a time and Dismiss these N dismisses everything a search found in one change (reverse.dismiss takes prefixes; --dismiss takes several). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
304c89dba4
commit
bfab83da6e
11 files changed
+239
-41
No files matched your search
+7
-1
@@ -41,7 +41,13 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
without a reverse zone, in every view, also where nothing is suggested
|
without a reverse zone, in every view, also where nothing is suggested
|
||||||
(suggestions off, dismissed, public views); pick one, change its size
|
(suggestions off, dismissed, public views); pick one, change its size
|
||||||
(/48, /56, /64, /128 for IPv6, around the records' address) and create
|
(/48, /56, /64, /128 for IPv6, around the records' address) and create
|
||||||
it filled. `jarl zone suggest-reverse --all [--view V]`.
|
it filled. With a view chosen, it also lists the addresses only other
|
||||||
|
views' records use, and fill names them from there (a VPS's AAAA in
|
||||||
|
external, its PTR in internal). `jarl zone suggest-reverse --all
|
||||||
|
[--view V]`. Both lists have a search, so hundreds of addresses stay
|
||||||
|
manageable: the public addresses fold away, show 25 at a time, and
|
||||||
|
**Dismiss these N** dismisses everything a search found in one change
|
||||||
|
(`--dismiss` takes several networks).
|
||||||
- Comparing one name across views: `jarl zone diff adbraun.lol nas [A]`,
|
- Comparing one name across views: `jarl zone diff adbraun.lol nas [A]`,
|
||||||
and a **Compare** button on each record.
|
and a **Compare** button on each record.
|
||||||
|
|
||||||
|
|||||||
+9
-8
@@ -1209,7 +1209,8 @@ type reverseSuggestion struct {
|
|||||||
|
|
||||||
func zoneSuggestReverseCmd() *cobra.Command {
|
func zoneSuggestReverseCmd() *cobra.Command {
|
||||||
var create, all bool
|
var create, all bool
|
||||||
var dismiss, undismiss, view string
|
var dismiss, undismiss []string
|
||||||
|
var view string
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "suggest-reverse [--create] [--dismiss NETWORK --view V]",
|
Use: "suggest-reverse [--create] [--dismiss NETWORK --view V]",
|
||||||
Short: "The reverse zones your private views lack for their records' addresses",
|
Short: "The reverse zones your private views lack for their records' addresses",
|
||||||
@@ -1244,12 +1245,12 @@ IPv6 can be created as a /56 or /64 instead.`,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if dismiss != "" || undismiss != "" {
|
if len(dismiss) > 0 || len(undismiss) > 0 {
|
||||||
path, prefix := "/reverse-zones/dismiss", dismiss
|
path, prefixes := "/reverse-zones/dismiss", dismiss
|
||||||
if undismiss != "" {
|
if len(undismiss) > 0 {
|
||||||
path, prefix = "/reverse-zones/undismiss", undismiss
|
path, prefixes = "/reverse-zones/undismiss", undismiss
|
||||||
}
|
}
|
||||||
resp, err := c.Mutate(ctx(), "POST", path, nil, map[string]any{"view": view, "prefix": prefix})
|
resp, err := c.Mutate(ctx(), "POST", path, nil, map[string]any{"view": view, "prefixes": prefixes})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1315,8 +1316,8 @@ IPv6 can be created as a /56 or /64 instead.`,
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().BoolVar(&create, "create", false, "create the ones that are yours, filled with PTR records")
|
cmd.Flags().BoolVar(&create, "create", false, "create the ones that are yours, filled with PTR records")
|
||||||
cmd.Flags().StringVar(&dismiss, "dismiss", "", "hide the suggestion for this network")
|
cmd.Flags().StringSliceVar(&dismiss, "dismiss", nil, "hide the suggestions for these networks (comma separated or repeated)")
|
||||||
cmd.Flags().StringVar(&undismiss, "undismiss", "", "show a dismissed suggestion again")
|
cmd.Flags().StringSliceVar(&undismiss, "undismiss", nil, "show dismissed suggestions again")
|
||||||
cmd.Flags().StringVar(&view, "view", "", "only this view; with --dismiss/--undismiss: the suggestion's view")
|
cmd.Flags().StringVar(&view, "view", "", "only this view; with --dismiss/--undismiss: the suggestion's view")
|
||||||
cmd.Flags().BoolVar(&all, "all", false, "also networks not suggested unasked (suggestions off, dismissed, public views)")
|
cmd.Flags().BoolVar(&all, "all", false, "also networks not suggested unasked (suggestions off, dismissed, public views)")
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
+1
-1
@@ -362,7 +362,7 @@ and `linked_with` (the other views).
|
|||||||
| POST | `/zones/{zone}/unlink` | Takes the zone out of its link; its records stay, all 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 ([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` |
|
| 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"}`: don't suggest it again (`/undismiss` brings it back). A view's `reverse_suggestions: false` (PATCH `/views/{id}`) stops its suggestions |
|
| 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 |
|
| 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 |
|
||||||
|
|
||||||
`POST /zones` takes `"link_with": "VIEW"` to create a zone linked with
|
`POST /zones` takes `"link_with": "VIEW"` to create a zone linked with
|
||||||
|
|||||||
+1
-1
@@ -180,7 +180,7 @@ jarl zone create 10.0.10.100/32 # a zone for just that ad
|
|||||||
jarl zone create 10.0.10.0/24 --fill # in the view whose records use it, with their PTRs
|
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 # add the PTRs it lacks (zone commands take a network too)
|
||||||
jarl zone suggest-reverse [--create] # reverse zones your private views lack (--create: yours)
|
jarl zone suggest-reverse [--create] # reverse zones your private views lack (--create: yours)
|
||||||
jarl zone suggest-reverse --dismiss 185.199.108.153/32 --view internal # not yours: don't suggest it again
|
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
|
jarl zone suggest-reverse --all [--view V] # every network without a reverse zone, also the hidden ones
|
||||||
jarl view update vps --reverse-suggestions=false # no suggestions for this view
|
jarl view update vps --reverse-suggestions=false # no suggestions for this view
|
||||||
jarl zone update adbraun.lol --auto-ptr # keep PTRs in step with the A/AAAA records (docs/reverse.md)
|
jarl zone update adbraun.lol --auto-ptr # keep PTRs in step with the A/AAAA records (docs/reverse.md)
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ answer NXDOMAIN to your clients for all of their addresses. Create the
|
|||||||
ones that are yours one by one, and **Dismiss** the others: Jarl
|
ones that are yours one by one, and **Dismiss** the others: Jarl
|
||||||
remembers, per view (`--undismiss` brings one back).
|
remembers, per view (`--undismiss` brings one back).
|
||||||
|
|
||||||
|
There can be hundreds of public addresses (records pointing at a web
|
||||||
|
host or a CDN), so the group is folded away when it is long. Opened, it
|
||||||
|
has a search and shows 25 at a time; **Dismiss these N** dismisses every
|
||||||
|
address the search found, in one change: search `104.16.` and dismiss
|
||||||
|
the CDN's addresses together. From the CLI, `--dismiss` takes several
|
||||||
|
networks (comma separated or repeated). The New reverse zone list has a
|
||||||
|
search too.
|
||||||
|
|
||||||
Loopback and link-local addresses, wildcards and disabled records are left
|
Loopback and link-local addresses, wildcards and disabled records are left
|
||||||
out.
|
out.
|
||||||
|
|
||||||
@@ -85,6 +93,15 @@ jarl zone suggest-reverse --all --view vps # every network,
|
|||||||
jarl zone create 2a02:8013:7f3d:10::/64 --view internal --fill
|
jarl zone create 2a02:8013:7f3d:10::/64 --view internal --fill
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Records in another view.** With a view chosen (`--view internal`, or
|
||||||
|
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.
|
||||||
|
|
||||||
## Creating and filling
|
## Creating and filling
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ const (
|
|||||||
type DismissInput struct {
|
type DismissInput struct {
|
||||||
ViewID int64 `json:"view_id,omitempty"`
|
ViewID int64 `json:"view_id,omitempty"`
|
||||||
View string `json:"view,omitempty"`
|
View string `json:"view,omitempty"`
|
||||||
Prefix string `json:"prefix"`
|
Prefix string `json:"prefix,omitempty"`
|
||||||
|
// Prefixes dismisses (or brings back) several at once.
|
||||||
|
Prefixes []string `json:"prefixes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AckInput acknowledges (or un-acknowledges) an exposure finding of a view
|
// AckInput acknowledges (or un-acknowledges) an exposure finding of a view
|
||||||
|
|||||||
+79
-16
@@ -21,6 +21,7 @@ type addrUse struct {
|
|||||||
addr netip.Addr
|
addr netip.Addr
|
||||||
name string // the owner name, absolute
|
name string // the owner name, absolute
|
||||||
apex bool
|
apex bool
|
||||||
|
view string // another view's record (fillAddresses), else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// forwardAddresses returns the addresses of the enabled A and AAAA
|
// forwardAddresses returns the addresses of the enabled A and AAAA
|
||||||
@@ -46,19 +47,51 @@ func forwardAddresses(st *model.State, viewID int64) []addrUse {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fillAddresses returns a view's addresses (forwardAddresses), then those
|
||||||
|
// of the other views' records that none of its own records use, marked
|
||||||
|
// with their view: a reverse zone in the internal view can name a host
|
||||||
|
// only the external view has a record for.
|
||||||
|
func fillAddresses(st *model.State, viewID int64) []addrUse {
|
||||||
|
out := forwardAddresses(st, viewID)
|
||||||
|
if len(st.Views) == 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
own := map[netip.Addr]bool{}
|
||||||
|
for _, u := range out {
|
||||||
|
own[u.addr] = true
|
||||||
|
}
|
||||||
|
for _, v := range st.OrderedViews() {
|
||||||
|
if v.ID == viewID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, u := range forwardAddresses(st, v.ID) {
|
||||||
|
if !own[u.addr] {
|
||||||
|
u.view = v.Name
|
||||||
|
out = append(out, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// PTRFill is a PTR record filling a reverse zone would add.
|
// PTRFill is a PTR record filling a reverse zone would add.
|
||||||
type PTRFill struct {
|
type PTRFill struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Name string `json:"name"` // the PTR owner name
|
Name string `json:"name"` // the PTR owner name
|
||||||
Target string `json:"target"` // the host name it gives
|
Target string `json:"target"` // the host name it gives
|
||||||
Others []string `json:"others,omitempty"` // other names with this address
|
Others []string `json:"others,omitempty"` // other names with this address
|
||||||
|
// From is the view whose record names the host, when the zone's own
|
||||||
|
// view has none for the address.
|
||||||
|
From string `json:"from,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PTRFills returns the PTR records a reverse zone named zoneName in a view
|
// 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
|
// 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
|
// address, naming its mail or name server host when it is one, otherwise
|
||||||
// a host rather than the domain itself, then the shortest name.
|
// a host rather than the domain itself, then the shortest name. An
|
||||||
// existing are the zone's records (none for a new zone).
|
// 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 {
|
func PTRFills(st *model.State, viewID int64, zoneName string, existing []*model.Record) []PTRFill {
|
||||||
prefix, ok := dnsname.PrefixFromReverse(zoneName)
|
prefix, ok := dnsname.PrefixFromReverse(zoneName)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -73,7 +106,7 @@ func PTRFills(st *model.State, viewID int64, zoneName string, existing []*model.
|
|||||||
servers := serverNames(st, viewID)
|
servers := serverNames(st, viewID)
|
||||||
byAddr := map[netip.Addr][]addrUse{}
|
byAddr := map[netip.Addr][]addrUse{}
|
||||||
var order []netip.Addr
|
var order []netip.Addr
|
||||||
for _, u := range forwardAddresses(st, viewID) {
|
for _, u := range fillAddresses(st, viewID) {
|
||||||
if !prefix.Contains(u.addr) {
|
if !prefix.Contains(u.addr) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -110,9 +143,11 @@ func PTRFills(st *model.State, viewID int64, zoneName string, existing []*model.
|
|||||||
}
|
}
|
||||||
return strings.Compare(x.name, y.name)
|
return strings.Compare(x.name, y.name)
|
||||||
})
|
})
|
||||||
f := PTRFill{Address: a.String(), Name: owner, Target: uses[0].name}
|
f := PTRFill{Address: a.String(), Name: owner, Target: uses[0].name, From: uses[0].view}
|
||||||
for _, u := range uses[1:] {
|
for _, u := range uses[1:] {
|
||||||
f.Others = append(f.Others, u.name)
|
if u.name != f.Target {
|
||||||
|
f.Others = append(f.Others, u.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out = append(out, f)
|
out = append(out, f)
|
||||||
}
|
}
|
||||||
@@ -143,6 +178,9 @@ func serverNames(st *model.State, viewID int64) map[string]bool {
|
|||||||
func fillWarnings(fills []PTRFill) []string {
|
func fillWarnings(fills []PTRFill) []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, f := range fills {
|
for _, f := range fills {
|
||||||
|
if f.From != "" {
|
||||||
|
out = append(out, fmt.Sprintf("%s: only view %s has a record for it, so its PTR record gives %s from there", f.Address, f.From, dnsname.Display(f.Target)))
|
||||||
|
}
|
||||||
if len(f.Others) > 0 {
|
if len(f.Others) > 0 {
|
||||||
out = append(out, fmt.Sprintf("%s has several names: its PTR record gives %s (not %s); change it if you prefer another", f.Address, dnsname.Display(f.Target), strings.Join(displayAll(f.Others), ", ")))
|
out = append(out, fmt.Sprintf("%s has several names: its PTR record gives %s (not %s); change it if you prefer another", f.Address, dnsname.Display(f.Target), strings.Join(displayAll(f.Others), ", ")))
|
||||||
}
|
}
|
||||||
@@ -296,7 +334,9 @@ type ReverseOptions struct {
|
|||||||
// All includes what isn't suggested unasked, marked Hidden: public
|
// All includes what isn't suggested unasked, marked Hidden: public
|
||||||
// views, views with suggestions off, dismissed networks.
|
// views, views with suggestions off, dismissed networks.
|
||||||
All bool
|
All bool
|
||||||
// View narrows it to one view (by name; "" all).
|
// View narrows it to one view (by name; "" all). With All, the view
|
||||||
|
// also gets the addresses only other views' records use, marked
|
||||||
|
// Hidden "records in VIEW": a reverse zone there can name them.
|
||||||
View string
|
View string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +380,11 @@ func ReverseCandidates(st *model.State, exposures []validate.Exposure, local []n
|
|||||||
out := []ReverseSuggestion{}
|
out := []ReverseSuggestion{}
|
||||||
for _, sc := range scopes {
|
for _, sc := range scopes {
|
||||||
seen := map[netip.Prefix]bool{}
|
seen := map[netip.Prefix]bool{}
|
||||||
for _, u := range forwardAddresses(st, sc.id) {
|
addrs := forwardAddresses(st, sc.id)
|
||||||
|
if opt.All && opt.View != "" {
|
||||||
|
addrs = fillAddresses(st, sc.id)
|
||||||
|
}
|
||||||
|
for _, u := range addrs {
|
||||||
a := u.addr
|
a := u.addr
|
||||||
if a.IsLoopback() || a.IsLinkLocalUnicast() || a.IsUnspecified() || a.IsMulticast() {
|
if a.IsLoopback() || a.IsLinkLocalUnicast() || a.IsUnspecified() || a.IsMulticast() {
|
||||||
continue
|
continue
|
||||||
@@ -350,6 +394,9 @@ func ReverseCandidates(st *model.State, exposures []validate.Exposure, local []n
|
|||||||
}
|
}
|
||||||
p, group, why := suggestedPrefix(a, local)
|
p, group, why := suggestedPrefix(a, local)
|
||||||
hidden := sc.hidden
|
hidden := sc.hidden
|
||||||
|
if hidden == "" && u.view != "" {
|
||||||
|
hidden = "records in " + u.view
|
||||||
|
}
|
||||||
if seen[p] {
|
if seen[p] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -424,11 +471,23 @@ func (x *expander) reverseDismiss(req Request, dismiss bool) ([]model.Op, error)
|
|||||||
if err := decode(req.Data, &in); err != nil {
|
if err := decode(req.Data, &in); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
p, err := netip.ParsePrefix(strings.TrimSpace(in.Prefix))
|
raw := in.Prefixes
|
||||||
if err != nil {
|
if in.Prefix != "" {
|
||||||
return nil, errf(KindBadRequest, "prefix: %v", err)
|
raw = append(raw, in.Prefix)
|
||||||
|
}
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil, errf(KindBadRequest, "give the network (prefix) of the suggestion")
|
||||||
|
}
|
||||||
|
var prefixes []string
|
||||||
|
for _, r := range raw {
|
||||||
|
p, err := netip.ParsePrefix(strings.TrimSpace(r))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errf(KindBadRequest, "prefix: %v", err)
|
||||||
|
}
|
||||||
|
if s := p.Masked().String(); !slices.Contains(prefixes, s) {
|
||||||
|
prefixes = append(prefixes, s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p = p.Masked()
|
|
||||||
var v *model.View
|
var v *model.View
|
||||||
switch {
|
switch {
|
||||||
case in.ViewID != 0:
|
case in.ViewID != 0:
|
||||||
@@ -446,16 +505,20 @@ func (x *expander) reverseDismiss(req Request, dismiss bool) ([]model.Op, error)
|
|||||||
if v != nil {
|
if v != nil {
|
||||||
list, where = v.DismissedReverse, "view "+v.Name
|
list, where = v.DismissedReverse, "view "+v.Name
|
||||||
}
|
}
|
||||||
out := slices.DeleteFunc(slices.Clone(list), func(s string) bool { return s == p.String() })
|
out := slices.DeleteFunc(slices.Clone(list), func(s string) bool { return slices.Contains(prefixes, s) })
|
||||||
summary := "dismiss the reverse zone suggestion for " + p.String() + " in " + where
|
what := prefixes[0]
|
||||||
|
if len(prefixes) > 1 {
|
||||||
|
what = fmt.Sprintf("%d networks (%s, …)", len(prefixes), prefixes[0])
|
||||||
|
}
|
||||||
|
summary := "dismiss the reverse zone suggestion for " + what + " in " + where
|
||||||
if dismiss {
|
if dismiss {
|
||||||
out = append(out, p.String())
|
out = append(out, prefixes...)
|
||||||
slices.Sort(out)
|
slices.Sort(out)
|
||||||
} else {
|
} else {
|
||||||
if len(out) == len(list) {
|
if len(out) == len(list) {
|
||||||
return nil, errf(KindConflict, "%s isn't dismissed in %s", p, where)
|
return nil, errf(KindConflict, "%s isn't dismissed in %s", what, where)
|
||||||
}
|
}
|
||||||
summary = "suggest a reverse zone for " + p.String() + " in " + where + " again"
|
summary = "suggest a reverse zone for " + what + " in " + where + " again"
|
||||||
}
|
}
|
||||||
if out == nil {
|
if out == nil {
|
||||||
out = []string{}
|
out = []string{}
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ func TestReverseSuggestionGroups(t *testing.T) {
|
|||||||
t.Errorf("undismiss of one not dismissed: %v", e)
|
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"}))
|
env.submit(env.admin, NewRequest(OpReverseUndismiss, 0, DismissInput{View: "internal", Prefix: "2606:50c0:8000::153/128"}))
|
||||||
|
// Several at once.
|
||||||
|
env.submit(env.admin, NewRequest(OpReverseDismiss, 0, DismissInput{View: "internal", Prefixes: []string{"192.0.2.10/32", "192.0.2.25/32"}}))
|
||||||
|
if got := suggested(env); slices.ContainsFunc(got, func(s string) bool { return strings.Contains(s, "192.0.2.") }) {
|
||||||
|
t.Errorf("dismissed together, still suggested: %q", got)
|
||||||
|
}
|
||||||
|
env.submit(env.admin, NewRequest(OpReverseUndismiss, 0, DismissInput{View: "internal", Prefixes: []string{"192.0.2.10/32", "192.0.2.25/32"}}))
|
||||||
if got := suggested(env); !slices.Contains(got, "public internal 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)
|
t.Errorf("undismissed not suggested: %q", got)
|
||||||
}
|
}
|
||||||
@@ -256,9 +262,44 @@ func TestReverseCandidatesAll(t *testing.T) {
|
|||||||
want(t, "internal, all", list(ReverseOptions{All: true, View: "internal"}),
|
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")
|
"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"})
|
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")
|
want(t, "external, all", got, "external 192.0.2.10/32 public view", "external 192.0.2.25/32 public view", "external 2a02:8013:7f3d:10::5/128 public view")
|
||||||
|
|
||||||
env.submit(env.admin, NewRequest(OpViewUpdate, internal.ID, ViewInput{ReverseSuggestions: model.Ptr(true)}))
|
env.submit(env.admin, NewRequest(OpViewUpdate, internal.ID, ViewInput{ReverseSuggestions: model.Ptr(true)}))
|
||||||
want(t, "dismissed", list(ReverseOptions{All: true, View: "internal"}),
|
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 ")
|
"internal 192.0.2.10/32 dismissed", "internal 192.0.2.25/32 ", "internal 2a02:8013:7f3d:10::5/128 ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An address only another view has a record for: a reverse zone in the
|
||||||
|
// internal view is offered for it, and filled from that view's record.
|
||||||
|
func TestReverseFromOtherView(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: "external", Name: "vps", Type: "AAAA", Data: []string{"2600:3c01::f03c:93ff:fe2b:1"}, Local: model.Ptr(true)}))
|
||||||
|
var got []string
|
||||||
|
for _, s := range ReverseCandidates(env.state(), env.e.Exposures(env.state()), nil, ReverseOptions{All: true, View: "internal"}) {
|
||||||
|
got = append(got, s.Prefix+" "+s.Hidden)
|
||||||
|
}
|
||||||
|
if !slices.Contains(got, "2600:3c01::f03c:93ff:fe2b:1/128 records in external") {
|
||||||
|
t.Errorf("internal candidates %q", got)
|
||||||
|
}
|
||||||
|
// Not suggested unasked: the internal view's own records don't use it.
|
||||||
|
for _, s := range ReverseSuggestions(env.state(), env.e.Exposures(env.state()), nil) {
|
||||||
|
if strings.HasPrefix(s.Prefix, "2600:") {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p := env.ptrs("internal", zone); 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") }) {
|
||||||
|
t.Errorf("warnings %q", res.Warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { h, mount, table, modal, field, input, select, textarea, lines, toast, errorBox, display, badge, viewLink, lastList, checkbox, plural } from "../ui.js";
|
import { h, mount, table, modal, field, input, select, textarea, lines, toast, errorBox, display, badge, viewLink, lastList, checkbox, plural, confirmDialog } from "../ui.js";
|
||||||
import { get, change, changeMany, describeResult, can, draftQuery } from "../api.js";
|
import { get, change, changeMany, describeResult, can, draftQuery } from "../api.js";
|
||||||
import { rerender } from "../app.js";
|
import { rerender } from "../app.js";
|
||||||
import { checkDomain, reverseZoneFor, maskNetwork } from "../names.js";
|
import { checkDomain, reverseZoneFor, maskNetwork } from "../names.js";
|
||||||
@@ -66,7 +66,20 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
|||||||
// For a reverse zone: sizes to pick from, and the networks the records
|
// For a reverse zone: sizes to pick from, and the networks the records
|
||||||
// use without a reverse zone, in every view (docs/reverse.md).
|
// use without a reverse zone, in every view (docs/reverse.md).
|
||||||
const helper = h("div", { class: "reverse-helper" });
|
const helper = h("div", { class: "reverse-helper" });
|
||||||
|
// The networks per view picked ("" for all views): a chosen view's list
|
||||||
|
// also has the addresses only other views' records use.
|
||||||
|
const candsBy = {};
|
||||||
let cands = null;
|
let cands = null;
|
||||||
|
const candQ = input({ type: "search", placeholder: "Search: an address, a name or a view", class: "small", style: { marginBottom: "6px" } });
|
||||||
|
candQ.addEventListener("input", () => drawHelper());
|
||||||
|
const loadCands = () => {
|
||||||
|
const k = view.value || "";
|
||||||
|
if (candsBy[k]) { cands = candsBy[k]; return drawHelper(); }
|
||||||
|
get("/reverse-zones/suggestions", draftQuery({ all: 1, view: k || undefined })).catch(() => []).then((c) => {
|
||||||
|
candsBy[k] = c;
|
||||||
|
if ((view.value || "") === k) { cands = c; drawHelper(); }
|
||||||
|
});
|
||||||
|
};
|
||||||
// sizeFrom is the address the size buttons work from: a record's, once a
|
// sizeFrom is the address the size buttons work from: a record's, once a
|
||||||
// network was picked, so a /48 made smaller becomes that record's /64,
|
// network was picked, so a /48 made smaller becomes that record's /64,
|
||||||
// not the /48's first one.
|
// not the /48's first one.
|
||||||
@@ -159,7 +172,7 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
|||||||
name.placeholder = k === "reverse" ? "10.10.0.0/24 or 2001:db8:1::/48" : "adbraun.lol";
|
name.placeholder = k === "reverse" ? "10.10.0.0/24 or 2001:db8:1::/48" : "adbraun.lol";
|
||||||
for (const b of kindButtons.children) b.classList.toggle("active", b.dataset.kind === k);
|
for (const b of kindButtons.children) b.classList.toggle("active", b.dataset.kind === k);
|
||||||
syncFill();
|
syncFill();
|
||||||
if (k === "reverse" && !cands) get("/reverse-zones/suggestions", draftQuery({ all: 1 })).catch(() => []).then((c) => { cands = c; drawHelper(); });
|
if (k === "reverse") loadCands();
|
||||||
drawHelper();
|
drawHelper();
|
||||||
if (views.length) view.options[0].textContent = k === "reverse" ? "– the private view that uses these addresses –" : "– choose a view –";
|
if (views.length) view.options[0].textContent = k === "reverse" ? "– the private view that uses these addresses –" : "– choose a view –";
|
||||||
check();
|
check();
|
||||||
@@ -180,9 +193,13 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
|||||||
(v6 ? [[48, "a whole site (all its /64s)"], [56, "a smaller delegation"], [64, "one subnet"], [128, "one address"]] : [[16, "a /16"], [24, "one network"], [32, "one address"]])
|
(v6 ? [[48, "a whole site (all its /64s)"], [56, "a smaller delegation"], [64, "one subnet"], [128, "one address"]] : [[16, "a /16"], [24, "one network"], [32, "one address"]])
|
||||||
.map(([b, title]) => h("button", { type: "button", class: "small" + (bits === b ? " active" : ""), title, onclick: () => { name.value = maskNetwork(sizeFrom || name.value, b); changed(); } }, "/" + b)),
|
.map(([b, title]) => h("button", { type: "button", class: "small" + (bits === b ? " active" : ""), title, onclick: () => { name.value = maskNetwork(sizeFrom || name.value, b); changed(); } }, "/" + b)),
|
||||||
sizeFrom ? h("span", { class: "muted small" }, " around " + sizeFrom) : null) : null;
|
sizeFrom ? h("span", { class: "muted small" }, " around " + sizeFrom) : null) : null;
|
||||||
const list = (cands || []).filter((c) => !view.value || c.view === view.value);
|
const q = candQ.value.trim().toLowerCase();
|
||||||
const shown = list.slice(0, 12);
|
// Yours first, then the networks with the most records.
|
||||||
|
const list = (cands || []).filter((c) => !q || [c.prefix, c.zone, c.view, c.hidden, ...c.samples].join(" ").toLowerCase().includes(q))
|
||||||
|
.sort((a, b) => (a.group === "yours" ? 0 : 1) - (b.group === "yours" ? 0 : 1) || b.fill - a.fill);
|
||||||
|
const shown = list.slice(0, 15);
|
||||||
mount(helper, sizes,
|
mount(helper, sizes,
|
||||||
|
(cands || []).length > 8 ? candQ : null,
|
||||||
shown.length ? h("div", { class: "cands" },
|
shown.length ? h("div", { class: "cands" },
|
||||||
h("div", { class: "muted small" }, "Networks your records use without a reverse zone" + (view.value ? " in view " + view.value : "") + ": pick one, then change its size if you need"),
|
h("div", { class: "muted small" }, "Networks your records use without a reverse zone" + (view.value ? " in view " + view.value : "") + ": pick one, then change its size if you need"),
|
||||||
shown.map((c) => h("button", { type: "button", class: "cand", title: c.why, onclick: () => {
|
shown.map((c) => h("button", { type: "button", class: "cand", title: c.why, onclick: () => {
|
||||||
@@ -195,11 +212,12 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
|||||||
views.length && !view.value ? h("span", { class: "muted" }, " " + c.view) : null,
|
views.length && !view.value ? h("span", { class: "muted" }, " " + c.view) : null,
|
||||||
h("span", { class: "muted small" }, " " + c.fill + " PTR" + (c.samples.length ? " · " + c.samples.slice(0, 2).join(", ") : "")),
|
h("span", { class: "muted small" }, " " + c.fill + " PTR" + (c.samples.length ? " · " + c.samples.slice(0, 2).join(", ") : "")),
|
||||||
c.hidden ? [" ", badge(c.hidden, "off")] : null)),
|
c.hidden ? [" ", badge(c.hidden, "off")] : null)),
|
||||||
list.length > shown.length ? h("div", { class: "muted small" }, "… and " + (list.length - shown.length) + " more (pick a view to narrow the list)") : null)
|
list.length > shown.length ? h("div", { class: "muted small" }, "… and " + (list.length - shown.length) + " more: search to find one") : null)
|
||||||
|
: q && cands && cands.length ? h("div", { class: "muted small" }, "No network matches " + JSON.stringify(candQ.value.trim()) + ".")
|
||||||
: cands && !list.length ? h("div", { class: "muted small" }, "Every address your records use" + (view.value ? " in view " + view.value : "") + " has a reverse zone.") : null);
|
: cands && !list.length ? h("div", { class: "muted small" }, "Every address your records use" + (view.value ? " in view " + view.value : "") + " has a reverse zone.") : null);
|
||||||
};
|
};
|
||||||
name.addEventListener("input", typed);
|
name.addEventListener("input", typed);
|
||||||
view.addEventListener("change", () => { check(); drawHelper(); });
|
view.addEventListener("change", () => { check(); if (kind === "reverse") loadCands(); else drawHelper(); });
|
||||||
const glueRow = field("Name server addresses", nsAddr, "Optional, for name servers inside this zone: 'ns1 192.0.2.1' per line (glue).");
|
const glueRow = field("Name server addresses", nsAddr, "Optional, for name servers inside this zone: 'ns1 192.0.2.1' per line (glue).");
|
||||||
const cloneRow = field("Copy records from", cloneWrap, "For split-horizon setups: start with the records of this zone in another view. Linked, Jarl keeps the shared records the same in both (docs/views.md#linked-zones).");
|
const cloneRow = field("Copy records from", cloneWrap, "For split-horizon setups: start with the records of this zone in another view. Linked, Jarl keeps the shared records the same in both (docs/views.md#linked-zones).");
|
||||||
const rows = {
|
const rows = {
|
||||||
@@ -364,7 +382,48 @@ function suggestPanel(sugg, views) {
|
|||||||
yours.length > 1 ? h("div", { class: "actions" }, h("button", { onclick: () => create(yours) }, "Create all " + yours.length)) : null),
|
yours.length > 1 ? h("div", { class: "actions" }, h("button", { onclick: () => create(yours) }, "Create all " + yours.length)) : null),
|
||||||
h("p", { class: "muted small" }, "Your records use these addresses, but no reverse zone answers \"which host has this address?\" for them" + (views.length ? " in their view" : "") + ". Jarl creates each with a PTR record for every address there. Only your own clients ask this server: the internet asks whoever owns the addresses."),
|
h("p", { class: "muted small" }, "Your records use these addresses, but no reverse zone answers \"which host has this address?\" for them" + (views.length ? " in their view" : "") + ". Jarl creates each with a PTR record for every address there. Only your own clients ask this server: the internet asks whoever owns the addresses."),
|
||||||
yours.length ? table(cols(false), yours) : null,
|
yours.length ? table(cols(false), yours) : null,
|
||||||
pub.length ? [h("h3", null, "Public addresses: only if they're yours"),
|
pub.length ? publicGroup(pub, cols(true), create) : null);
|
||||||
h("p", { class: "muted small" }, "Jarl can't tell your own servers from someone else's (a web host's). Create a zone for an address that is yours; dismiss the others. Each gets a zone of its own: the rest of its network belongs to others."),
|
}
|
||||||
table(cols(true), pub)] : null);
|
|
||||||
|
// publicGroup lists the public addresses, collapsed: there can be hundreds
|
||||||
|
// (a web host's), so it has a search, shows 25 at a time, and dismisses
|
||||||
|
// what the search found at once.
|
||||||
|
function publicGroup(pub, cols, create) {
|
||||||
|
const PAGE = 25;
|
||||||
|
let limit = PAGE;
|
||||||
|
const q = input({ type: "search", placeholder: "Search: an address, a name or a view", class: "small", style: { width: "300px" } });
|
||||||
|
const body = h("div");
|
||||||
|
const match = () => {
|
||||||
|
const t = q.value.trim().toLowerCase();
|
||||||
|
return pub.filter((s) => !t || [s.prefix, s.zone, s.view, ...s.samples].join(" ").toLowerCase().includes(t));
|
||||||
|
};
|
||||||
|
const dismissAll = async (list) => {
|
||||||
|
const byView = new Map();
|
||||||
|
for (const s of list) {
|
||||||
|
const k = s.view_id || 0;
|
||||||
|
if (!byView.has(k)) byView.set(k, []);
|
||||||
|
byView.get(k).push(s.prefix);
|
||||||
|
}
|
||||||
|
if (!await confirmDialog("Dismiss " + plural(list.length, "address", "addresses"), "Jarl stops suggesting reverse zones for " + (list.length === 1 ? list[0].prefix : list.length + " public addresses" + (q.value.trim() ? " matching " + JSON.stringify(q.value.trim()) : "")) + ". Bring one back with: jarl zone suggest-reverse --undismiss NETWORK --view VIEW.", "Dismiss")) return;
|
||||||
|
try {
|
||||||
|
const ops = [...byView].map(([vid, prefixes]) => ({ op: "reverse.dismiss", data: { view_id: vid || undefined, prefixes } }));
|
||||||
|
toast(describeResult(await changeMany(ops, "dismiss " + plural(list.length, "reverse zone suggestion"))));
|
||||||
|
rerender();
|
||||||
|
} catch (e) { toast(e.message, "err", 8000); }
|
||||||
|
};
|
||||||
|
const draw = () => {
|
||||||
|
const list = match();
|
||||||
|
const shown = list.slice(0, limit);
|
||||||
|
mount(body,
|
||||||
|
h("div", { class: "compare-controls" }, q,
|
||||||
|
list.length ? h("button", { class: "small", title: "Not yours: stop suggesting all of these", onclick: () => dismissAll(list) }, list.length === pub.length ? "Dismiss all " + list.length : "Dismiss these " + list.length) : null),
|
||||||
|
shown.length ? table(cols, shown) : h("div", { class: "muted small" }, "No address matches."),
|
||||||
|
list.length > shown.length ? h("button", { class: "small", onclick: () => { limit += PAGE; draw(); } }, "Show " + Math.min(PAGE, list.length - shown.length) + " more (of " + (list.length - shown.length) + ")") : null);
|
||||||
|
};
|
||||||
|
q.addEventListener("input", () => { limit = PAGE; draw(); });
|
||||||
|
draw();
|
||||||
|
return h("details", { class: "compare-only", open: pub.length <= 10 },
|
||||||
|
h("summary", null, h("strong", null, "Public addresses: " + pub.length), h("span", { class: "muted small" }, " · create a zone only for an address that is yours; dismiss the others (a web host's)")),
|
||||||
|
h("p", { class: "muted small" }, "Jarl can't tell your own servers from someone else's. Each gets a zone of its own: the rest of its network belongs to others."),
|
||||||
|
body);
|
||||||
}
|
}
|
||||||
@@ -589,4 +589,4 @@ pre.wrap { white-space: pre-wrap; word-break: break-all; }
|
|||||||
.size-chips { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
|
.size-chips { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
|
||||||
.size-chips button.active { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
.size-chips button.active { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
||||||
.cands { display: flex; flex-direction: column; gap: 3px; max-height: 260px; overflow-y: auto; }
|
.cands { display: flex; flex-direction: column; gap: 3px; max-height: 260px; overflow-y: auto; }
|
||||||
.cands button.cand { text-align: left; font-weight: normal; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.cands button.cand { flex-shrink: 0; text-align: left; font-weight: normal; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
+11
-2
@@ -696,9 +696,11 @@ expect_dig "PTR gone with it" "" -x 10.99.1.13
|
|||||||
run "a web host's address" 0 $J record add revtest.test blog A 185.199.108.153 --view internal
|
run "a web host's address" 0 $J record add revtest.test blog A 185.199.108.153 --view internal
|
||||||
run "suggested as public" 0 $J zone suggest-reverse
|
run "suggested as public" 0 $J zone suggest-reverse
|
||||||
contains "^public *internal *185.199.108.153/32"
|
contains "^public *internal *185.199.108.153/32"
|
||||||
run "dismiss it" 0 $J zone suggest-reverse --dismiss 185.199.108.153/32 --view internal
|
run "two more web host addresses" 0 $J record add revtest.test docs A 185.199.109.153 --view internal
|
||||||
|
run "and" 0 $J record add revtest.test pages A 185.199.110.153 --view internal
|
||||||
|
run "dismiss three at once" 0 $J zone suggest-reverse --dismiss 185.199.108.153/32,185.199.109.153/32 --dismiss 185.199.110.153/32 --view internal
|
||||||
run "gone from the suggestions" 0 $J zone suggest-reverse
|
run "gone from the suggestions" 0 $J zone suggest-reverse
|
||||||
not_contains "185.199.108.153"
|
not_contains "185.199."
|
||||||
run "suggest-reverse --create: yours only" 0 $J zone suggest-reverse --create
|
run "suggest-reverse --create: yours only" 0 $J zone suggest-reverse --create
|
||||||
run "none of yours missing now" 0 $J zone suggest-reverse
|
run "none of yours missing now" 0 $J zone suggest-reverse
|
||||||
not_contains "^yours"
|
not_contains "^yours"
|
||||||
@@ -708,6 +710,13 @@ not_contains "internal"
|
|||||||
run "but --all still lists its networks" 0 $J zone suggest-reverse --all --view internal
|
run "but --all still lists its networks" 0 $J zone suggest-reverse --all --view internal
|
||||||
contains "^public *internal *185.199.108.153/32 .* suggestions off"
|
contains "^public *internal *185.199.108.153/32 .* suggestions off"
|
||||||
run "and back on" 0 $J view update internal --reverse-suggestions
|
run "and back on" 0 $J view update internal --reverse-suggestions
|
||||||
|
# An address only the external view has a record for: a reverse zone in
|
||||||
|
# the internal view can still name it.
|
||||||
|
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
|
||||||
|
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.
|
# 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
|
run "a /16 reverse zone" 0 $J zone create 10.88.0.0/16 --view internal
|
||||||
run "with a PTR made by hand" 0 $J record add 10.88.0.0/16 10.88.5.5 PTR printer.revtest.test --view internal
|
run "with a PTR made by hand" 0 $J record add 10.88.0.0/16 10.88.5.5 PTR printer.revtest.test --view internal
|
||||||
|
|||||||
Reference in new issue
Block a user