Skip to content

Commit fa20658

Browse files
committed
fix: include detected routes in list output
1 parent 711821a commit fa20658

9 files changed

Lines changed: 201 additions & 25 deletions

File tree

‎cmd/tailmix/main.go‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,12 +943,20 @@ func writeIPRoutes(w io.Writer, routes controlapi.IPRoutes, available bool) {
943943
return
944944
}
945945
fmt.Fprintln(table, "PREFIX\tPROFILE\tPOLICY\tADVERTISED BY\tSTATE\tOVERRIDDEN BY")
946+
shown := map[string]bool{}
946947
for _, route := range append(append([]controlapi.IPRouteBinding(nil), routes.Bindings...), routes.Imported...) {
947948
state := route.State
948949
if route.Reason != "" {
949950
state += ":" + route.Reason
950951
}
951952
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t%s\n", route.Prefix, route.ProfileName, route.Policy, route.PrimaryRouter, state, ipOverrideLabel(route.OverriddenBy, route.OverrideProfileName))
953+
shown[route.Prefix.String()+"\x00"+route.ProfileID+"\x00"+route.PrimaryRouter] = true
954+
}
955+
for _, route := range routes.Available {
956+
if shown[route.Prefix.String()+"\x00"+route.ProfileID+"\x00"+route.PrimaryRouter] {
957+
continue
958+
}
959+
fmt.Fprintf(table, "%s\t%s\tdetected\t%s\tavailable\t\n", route.Prefix, route.ProfileName, route.PrimaryRouter)
952960
}
953961
for _, accepted := range routes.AcceptAllProfiles {
954962
state := accepted.State
@@ -975,12 +983,20 @@ func writeDNSRoutes(w io.Writer, routes controlapi.DNSRoutes, available bool) {
975983
}
976984
fmt.Fprintln(table, "DOMAIN\tPROFILE\tSOURCE\tPOLICY\tSTATE\tOVERRIDDEN BY")
977985
all := append(append(append([]controlapi.DNSRouteBinding(nil), routes.Bindings...), routes.Imported...), routes.Automatic...)
986+
shown := map[string]bool{}
978987
for _, route := range all {
979988
state := route.State
980989
if route.Reason != "" {
981990
state += ":" + route.Reason
982991
}
983992
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t%s\n", route.Domain, route.ProfileName, route.Source, route.Policy, state, overrideLabel(route.OverriddenBy, route.OverrideProfileName))
993+
shown[route.Domain+"\x00"+route.ProfileID+"\x00"+route.Source] = true
994+
}
995+
for _, route := range routes.Available {
996+
if shown[route.Domain+"\x00"+route.ProfileID+"\x00"+route.Source] {
997+
continue
998+
}
999+
fmt.Fprintf(table, "%s\t%s\t%s\tdetected\tavailable\t\n", route.Domain, route.ProfileName, route.Source)
9841000
}
9851001
for _, accepted := range routes.AcceptAllProfiles {
9861002
state := accepted.State
@@ -1006,13 +1022,21 @@ func writeSearchDomains(w io.Writer, domains controlapi.SearchDomains) {
10061022
}
10071023
table := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
10081024
fmt.Fprintln(table, "ORDER\tDOMAIN\tPROFILE\tSTATE")
1025+
shown := map[string]bool{}
10091026
for i, domain := range domains.Desired {
10101027
if active, ok := installed[domain]; ok {
10111028
fmt.Fprintf(table, "%d\t%s\t%s\tinstalled\n", i+1, domain, active.ProfileName)
1029+
shown[domain+"\x00"+active.ProfileID] = true
10121030
} else {
10131031
fmt.Fprintf(table, "%d\t%s\t\twaiting:%s\n", i+1, domain, waiting[domain])
10141032
}
10151033
}
1034+
for _, domain := range domains.Available {
1035+
if shown[domain.Domain+"\x00"+domain.ProfileID] {
1036+
continue
1037+
}
1038+
fmt.Fprintf(table, "\t%s\t%s\tavailable\n", domain.Domain, domain.ProfileName)
1039+
}
10161040
if domains.ReconcileError != "" {
10171041
fmt.Fprintf(table, "!\t\t\tfailed:%s\n", domains.ReconcileError)
10181042
}
@@ -1120,6 +1144,8 @@ const routesHelp = `Usage:
11201144
11211145
Bindings are explicit overrides: the longest matching binding wins before any
11221146
accept-all import. Overridden and conflicting imports are shown by "routes list".
1147+
The default list also includes every detected route; --available shows only
1148+
detected routes.
11231149
Default routes use exit-node policy.
11241150
`
11251151

@@ -1137,6 +1163,8 @@ const dnsRoutesHelp = `Usage:
11371163
Bindings are explicit overrides: the longest matching binding wins before any
11381164
accept-all import or automatic MagicDNS route. Overridden and conflicting
11391165
imports are shown by "dns routes list".
1166+
The default list also includes every detected DNS route; --available shows only
1167+
detected routes.
11401168
The root suffix "." selects a profile's default DNS resolver route.
11411169
`
11421170

@@ -1146,6 +1174,9 @@ const dnsSearchHelp = `Usage:
11461174
tailmix dns search add <domain>... [--json]
11471175
tailmix dns search remove <domain>... [--json]
11481176
tailmix dns search clear [--json]
1177+
1178+
The list includes configured search domains and every search domain detected
1179+
from each tailnet.
11491180
`
11501181

11511182
const tailscaleHelp = `Usage:

‎cmd/tailmix/main_test.go‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,77 @@ func TestRouteHelpDocumentsOverrides(t *testing.T) {
218218
}
219219
}
220220
}
221+
222+
func TestDefaultIPRouteListIncludesEveryDetectedRoute(t *testing.T) {
223+
prefix := netip.MustParsePrefix("10.0.0.0/8")
224+
routes := controlapi.IPRoutes{
225+
Bindings: []controlapi.IPRouteBinding{{
226+
Prefix: prefix, ProfileID: "work-id", ProfileName: "work", Policy: "bound",
227+
PrimaryRouter: "work-router", State: "installed",
228+
}},
229+
Available: []controlapi.AvailableIPRoute{
230+
{Prefix: prefix, ProfileID: "work-id", ProfileName: "work", PrimaryRouter: "work-router"},
231+
{Prefix: prefix, ProfileID: "work-id", ProfileName: "work", PrimaryRouter: "backup-router"},
232+
{Prefix: prefix, ProfileID: "lab-id", ProfileName: "lab", PrimaryRouter: "lab-router"},
233+
{Prefix: netip.MustParsePrefix("172.16.0.0/12"), ProfileID: "lab-id", ProfileName: "lab", PrimaryRouter: "lab-router"},
234+
},
235+
}
236+
var output bytes.Buffer
237+
writeIPRoutes(&output, routes, false)
238+
if got := strings.Count(output.String(), prefix.String()); got != 3 {
239+
t.Fatalf("route appears %d times, want selected plus every other detection:\n%s", got, output.String())
240+
}
241+
for _, want := range []string{"172.16.0.0/12", "detected", "available", "lab-router", "backup-router"} {
242+
if !strings.Contains(output.String(), want) {
243+
t.Fatalf("IP route list does not contain %q:\n%s", want, output.String())
244+
}
245+
}
246+
}
247+
248+
func TestDefaultDNSRouteListIncludesEveryDetectedRoute(t *testing.T) {
249+
routes := controlapi.DNSRoutes{
250+
Automatic: []controlapi.DNSRouteBinding{{
251+
Domain: "corp.example", ProfileID: "work-id", ProfileName: "work",
252+
Source: "magicdns", Policy: "automatic", State: "installed",
253+
}},
254+
Available: []controlapi.AvailableDNSRoute{
255+
{Domain: "corp.example", ProfileID: "work-id", ProfileName: "work", Source: "magicdns"},
256+
{Domain: "corp.example", ProfileID: "lab-id", ProfileName: "lab", Source: "split-dns"},
257+
{Domain: "dev.example", ProfileID: "lab-id", ProfileName: "lab", Source: "split-dns"},
258+
},
259+
}
260+
var output bytes.Buffer
261+
writeDNSRoutes(&output, routes, false)
262+
if got := strings.Count(output.String(), "corp.example"); got != 2 {
263+
t.Fatalf("domain appears %d times, want selected and other-profile detection:\n%s", got, output.String())
264+
}
265+
for _, want := range []string{"dev.example", "detected", "available", "split-dns"} {
266+
if !strings.Contains(output.String(), want) {
267+
t.Fatalf("DNS route list does not contain %q:\n%s", want, output.String())
268+
}
269+
}
270+
}
271+
272+
func TestSearchListIncludesEveryDetectedSearchDomain(t *testing.T) {
273+
domains := controlapi.SearchDomains{
274+
Desired: []string{"corp.example"},
275+
Installed: []controlapi.InstalledSearchDomain{{
276+
Domain: "corp.example", ProfileID: "work-id", ProfileName: "work",
277+
}},
278+
Available: []controlapi.AvailableSearchDomain{
279+
{Domain: "corp.example", ProfileID: "work-id", ProfileName: "work"},
280+
{Domain: "corp.example", ProfileID: "lab-id", ProfileName: "lab"},
281+
{Domain: "dev.example", ProfileID: "lab-id", ProfileName: "lab"},
282+
},
283+
}
284+
var output bytes.Buffer
285+
writeSearchDomains(&output, domains)
286+
if got := strings.Count(output.String(), "corp.example"); got != 2 {
287+
t.Fatalf("search domain appears %d times, want selected and other-profile detection:\n%s", got, output.String())
288+
}
289+
for _, want := range []string{"dev.example", "available", "lab"} {
290+
if !strings.Contains(output.String(), want) {
291+
t.Fatalf("search domain list does not contain %q:\n%s", want, output.String())
292+
}
293+
}
294+
}

‎cmd/tailmixd/policyapi.go‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ func (s *supervisor) IPRoutes(_ context.Context, available bool) (controlapi.IPR
2121
result.AcceptAllProfiles = nil
2222
result.Bindings = nil
2323
result.Imported = nil
24-
} else {
25-
result.Available = nil
2624
}
2725
return result, nil
2826
}
@@ -176,8 +174,6 @@ func (s *supervisor) DNSRoutes(_ context.Context, available bool) (controlapi.DN
176174
result.Bindings = nil
177175
result.Imported = nil
178176
result.Automatic = nil
179-
} else {
180-
result.Available = nil
181177
}
182178
return result, nil
183179
}

‎cmd/tailmixd/supervisor.go‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,17 @@ func (s *supervisor) projectProfileLocked(configured state.Profile) controlapi.P
656656
Resolvers: resolverResourcesForAPI(available.Resolvers),
657657
})
658658
}
659+
for _, domain := range status.SearchDomains {
660+
domain = routingpolicy.NormalizeDomain(domain)
661+
if domain == "" || domain == "." {
662+
continue
663+
}
664+
result.AvailableSearchDomains = append(result.AvailableSearchDomains, controlapi.AvailableSearchDomain{
665+
Domain: domain,
666+
ProfileID: configured.ID,
667+
ProfileName: result.Name,
668+
})
669+
}
659670
}
660671
if path, err := profilesocket.Path(s.cfg.SocketDir, configured.ID); err == nil {
661672
result.LocalAPISocket = path

‎controlapi/types.go‎

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,28 @@ func (e *Error) Error() string {
2020
}
2121

2222
type Profile struct {
23-
ID string `json:"id"`
24-
Name string `json:"name"`
25-
StateDir string `json:"stateDir"`
26-
Hostname string `json:"hostname,omitempty"`
27-
ControlURL string `json:"controlUrl,omitempty"`
28-
Enabled bool `json:"enabled"`
29-
Removed bool `json:"removed"`
30-
AcceptAllRoutes bool `json:"acceptAllRoutes"`
31-
AcceptAllDNSRoutes bool `json:"acceptAllDnsRoutes"`
32-
RuntimeState string `json:"runtimeState"`
33-
BackendState string `json:"backendState,omitempty"`
34-
MagicDNSSuffix string `json:"magicDnsSuffix,omitempty"`
35-
SelfDNSName string `json:"selfDnsName,omitempty"`
36-
SelfIPs []netip.Addr `json:"selfIps,omitempty"`
37-
PeerCount int `json:"peerCount"`
38-
ShieldsUp bool `json:"shieldsUp"`
39-
AuthURL string `json:"authUrl,omitempty"`
40-
LocalAPISocket string `json:"localApiSocket,omitempty"`
41-
AvailableRoutes []AvailableIPRoute `json:"availableRoutes,omitempty"`
42-
AvailableDNSRoutes []AvailableDNSRoute `json:"availableDnsRoutes,omitempty"`
43-
LastError string `json:"lastError,omitempty"`
23+
ID string `json:"id"`
24+
Name string `json:"name"`
25+
StateDir string `json:"stateDir"`
26+
Hostname string `json:"hostname,omitempty"`
27+
ControlURL string `json:"controlUrl,omitempty"`
28+
Enabled bool `json:"enabled"`
29+
Removed bool `json:"removed"`
30+
AcceptAllRoutes bool `json:"acceptAllRoutes"`
31+
AcceptAllDNSRoutes bool `json:"acceptAllDnsRoutes"`
32+
RuntimeState string `json:"runtimeState"`
33+
BackendState string `json:"backendState,omitempty"`
34+
MagicDNSSuffix string `json:"magicDnsSuffix,omitempty"`
35+
SelfDNSName string `json:"selfDnsName,omitempty"`
36+
SelfIPs []netip.Addr `json:"selfIps,omitempty"`
37+
PeerCount int `json:"peerCount"`
38+
ShieldsUp bool `json:"shieldsUp"`
39+
AuthURL string `json:"authUrl,omitempty"`
40+
LocalAPISocket string `json:"localApiSocket,omitempty"`
41+
AvailableRoutes []AvailableIPRoute `json:"availableRoutes,omitempty"`
42+
AvailableDNSRoutes []AvailableDNSRoute `json:"availableDnsRoutes,omitempty"`
43+
AvailableSearchDomains []AvailableSearchDomain `json:"availableSearchDomains,omitempty"`
44+
LastError string `json:"lastError,omitempty"`
4445
}
4546

4647
type Profiles struct {
@@ -191,10 +192,17 @@ type WaitingSearchDomain struct {
191192
Reason string `json:"reason"`
192193
}
193194

195+
type AvailableSearchDomain struct {
196+
Domain string `json:"domain"`
197+
ProfileID string `json:"profileId"`
198+
ProfileName string `json:"profileName"`
199+
}
200+
194201
type SearchDomains struct {
195202
Desired []string `json:"desired"`
196203
Installed []InstalledSearchDomain `json:"installed,omitempty"`
197204
Waiting []WaitingSearchDomain `json:"waiting,omitempty"`
205+
Available []AvailableSearchDomain `json:"available,omitempty"`
198206
ReconcileError string `json:"reconcileError,omitempty"`
199207
}
200208

‎profile/profile.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type Status struct {
4040
ShieldsUp bool
4141
AvailableRoutes []RouteStatus
4242
DNSRoutes []DNSRouteStatus
43+
SearchDomains []string
4344
}
4445

4546
type PeerStatus struct {

‎profile/tsnetengine.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ func (e *TSNetEngine) Status(ctx context.Context) (Status, error) {
203203
shieldsUp = prefs.ShieldsUp
204204
}
205205
var dnsRoutes []DNSRouteStatus
206+
var searchDomains []string
206207
if backend, err := e.LocalBackend(); err == nil {
207208
if nm := backend.NetMapNoPeers(); nm != nil {
208209
if suffix != "" && nm.DNS.Proxied {
@@ -230,6 +231,15 @@ func (e *TSNetEngine) Status(ctx context.Context) (Status, error) {
230231
Resolvers: cloneResolvers(defaultResolvers),
231232
})
232233
}
234+
seenSearchDomains := map[string]bool{}
235+
for _, raw := range nm.DNS.Domains {
236+
domain := normalizeDNSRoute(raw)
237+
if domain == "" || domain == "." || seenSearchDomains[domain] {
238+
continue
239+
}
240+
seenSearchDomains[domain] = true
241+
searchDomains = append(searchDomains, domain)
242+
}
233243
}
234244
}
235245
sort.Slice(dnsRoutes, func(i, j int) bool {
@@ -238,6 +248,7 @@ func (e *TSNetEngine) Status(ctx context.Context) (Status, error) {
238248
}
239249
return dnsRoutes[i].Source < dnsRoutes[j].Source
240250
})
251+
sort.Strings(searchDomains)
241252
return Status{
242253
ProfileID: e.cfg.ProfileID,
243254
Alias: e.cfg.Alias,
@@ -252,6 +263,7 @@ func (e *TSNetEngine) Status(ctx context.Context) (Status, error) {
252263
ShieldsUp: shieldsUp,
253264
AvailableRoutes: availableRoutes,
254265
DNSRoutes: dnsRoutes,
266+
SearchDomains: searchDomains,
255267
}, nil
256268
}
257269

‎routingpolicy/policy.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ func BuildDNS(st state.State, statuses []profile.Status) DNSPlan {
184184
names[configured.ID] = profileName(configured)
185185
}
186186
statusByID := make(map[string]profile.Status, len(statuses))
187+
availableSearch := map[string]bool{}
187188
var plan DNSPlan
188189
for _, status := range statuses {
189190
statusByID[status.ProfileID] = status
@@ -200,6 +201,22 @@ func BuildDNS(st state.State, statuses []profile.Status) DNSPlan {
200201
Resolvers: resolverResources(available.Resolvers),
201202
})
202203
}
204+
for _, raw := range status.SearchDomains {
205+
domain := NormalizeDomain(raw)
206+
if domain == "" || domain == "." {
207+
continue
208+
}
209+
key := status.ProfileID + "\x00" + domain
210+
if availableSearch[key] {
211+
continue
212+
}
213+
availableSearch[key] = true
214+
plan.Search.Available = append(plan.Search.Available, controlapi.AvailableSearchDomain{
215+
Domain: domain,
216+
ProfileID: status.ProfileID,
217+
ProfileName: names[status.ProfileID],
218+
})
219+
}
203220
}
204221

205222
for _, binding := range st.DNSRouteBindings {
@@ -701,4 +718,10 @@ func sortDNSPlan(plan *DNSPlan) {
701718
sort.Slice(plan.Resource.AcceptAllProfiles, func(i, j int) bool {
702719
return plan.Resource.AcceptAllProfiles[i].ProfileName < plan.Resource.AcceptAllProfiles[j].ProfileName
703720
})
721+
sort.Slice(plan.Search.Available, func(i, j int) bool {
722+
if plan.Search.Available[i].Domain != plan.Search.Available[j].Domain {
723+
return plan.Search.Available[i].Domain < plan.Search.Available[j].Domain
724+
}
725+
return plan.Search.Available[i].ProfileName < plan.Search.Available[j].ProfileName
726+
})
704727
}

‎routingpolicy/policy_test.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,23 @@ func TestAcceptAllDoesNotImportDirectTailscaleAddressSpace(t *testing.T) {
121121
t.Fatalf("reserved import = %+v", plan.Imported)
122122
}
123123
}
124+
125+
func TestBuildDNSListsDetectedSearchDomainsByProfile(t *testing.T) {
126+
st := state.State{Profiles: []state.Profile{
127+
{ID: "work-id", Name: "work"},
128+
{ID: "lab-id", Name: "lab"},
129+
}}
130+
plan := BuildDNS(st, []profile.Status{
131+
{ProfileID: "work-id", SearchDomains: []string{"Corp.Example.", "corp.example", "."}},
132+
{ProfileID: "lab-id", SearchDomains: []string{"Lab.Example."}},
133+
})
134+
if len(plan.Search.Available) != 2 {
135+
t.Fatalf("available search domains = %+v", plan.Search.Available)
136+
}
137+
if got := plan.Search.Available[0]; got.Domain != "corp.example" || got.ProfileName != "work" {
138+
t.Fatalf("first available search domain = %+v", got)
139+
}
140+
if got := plan.Search.Available[1]; got.Domain != "lab.example" || got.ProfileName != "lab" {
141+
t.Fatalf("second available search domain = %+v", got)
142+
}
143+
}

0 commit comments

Comments
 (0)