diff --git a/appc/appconnector.go b/appc/appconnector.go index 82ac8af83..17a642887 100644 --- a/appc/appconnector.go +++ b/appc/appconnector.go @@ -81,6 +81,20 @@ func (e *AppConnector) Domains() views.Slice[string] { return views.SliceOf(xmaps.Keys(e.domains)) } +// DomainRoutes returns a map of domains to resolved IP +// addresses. +func (e *AppConnector) DomainRoutes() map[string][]netip.Addr { + e.mu.Lock() + defer e.mu.Unlock() + + drCopy := make(map[string][]netip.Addr) + for k, v := range e.domains { + copy(drCopy[k], v) + } + + return drCopy +} + // ObserveDNSResponse is a callback invoked by the DNS resolver when a DNS // response is being returned over the PeerAPI. The response is parsed and // matched against the configured domains, if matched the routeAdvertiser is diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index 300abff55..6b4aa094a 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -121,6 +121,15 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) { return } writeJSON(w, res) + case "/appconnector/routes": + switch r.Method { + case httpm.GET: + b.handleC2NAppConnectorDomainRoutesGet(w, r) + return + default: + http.Error(w, "bad method", http.StatusMethodNotAllowed) + return + } case "/sockstats": if r.Method != "POST" { http.Error(w, "bad method", http.StatusMethodNotAllowed) @@ -139,6 +148,26 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) { } } +// handleC2NAppConnectorDomainRoutesGet handles returning the domains +// that the app connector is responsible for, as well as the resolved +// IP addresses for each domain. If the node is not configured as +// an app connector, an empty map is returned. +func (b *LocalBackend) handleC2NAppConnectorDomainRoutesGet(w http.ResponseWriter, r *http.Request) { + b.logf("c2n: GET /appconnector/routes received") + + var res tailcfg.C2NAppConnectorDomainRoutesResponse + if b.appConnector == nil { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(res) + return + } + + res.Domains = b.appConnector.DomainRoutes() + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(res) +} + func (b *LocalBackend) handleC2NUpdateGet(w http.ResponseWriter, r *http.Request) { b.logf("c2n: GET /update received") diff --git a/tailcfg/c2ntypes.go b/tailcfg/c2ntypes.go index 042595e22..550b8affe 100644 --- a/tailcfg/c2ntypes.go +++ b/tailcfg/c2ntypes.go @@ -5,6 +5,8 @@ package tailcfg +import "net/netip" + // C2NSSHUsernamesRequest is the request for the /ssh/usernames. // A GET request without a request body is equivalent to the zero value of this type. // Otherwise, a POST request with a JSON-encoded request body is expected. @@ -64,3 +66,12 @@ type C2NPostureIdentityResponse struct { // device posture collection. PostureDisabled bool `json:",omitempty"` } + +// C2NAppConnectorDomainRoutesResponse contains a map of domains to +// slice of addresses, indicating what IP addresses have been resolved +// for each domain. +type C2NAppConnectorDomainRoutesResponse struct { + // Domains is a map of lower case domain names with no trailing dot, + // to a list of resolved IP addresses. + Domains map[string][]netip.Addr +}