From 275345f498a3ca949c79042fdc73874e27feb43e Mon Sep 17 00:00:00 2001 From: Anton Tolchanov Date: Thu, 4 Sep 2025 19:44:48 +0100 Subject: [PATCH] Add to tailcfg --- ipn/ipnlocal/c2n.go | 65 ++++++++++++++++++++++----------------------- tailcfg/c2ntypes.go | 17 +++++++++++- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index d3b0d874a..a1076e5e5 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -11,7 +11,6 @@ import ( "fmt" "io" "net/http" - "net/netip" "os" "os/exec" "path" @@ -21,8 +20,6 @@ import ( "strings" "time" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "tailscale.com/clientupdate" "tailscale.com/control/controlclient" "tailscale.com/envknob" @@ -30,12 +27,9 @@ import ( "tailscale.com/net/sockstats" "tailscale.com/posture" "tailscale.com/tailcfg" - "tailscale.com/types/ipproto" - "tailscale.com/types/key" - "tailscale.com/types/netmap" - "tailscale.com/types/views" "tailscale.com/util/clientmetric" "tailscale.com/util/goroutines" + "tailscale.com/util/httpm" "tailscale.com/util/set" "tailscale.com/util/syspolicy/pkey" "tailscale.com/util/syspolicy/ptype" @@ -54,7 +48,7 @@ var c2nHandlers = map[methodAndPath]c2nHandler{ req("/debug/metrics"): handleC2NDebugMetrics, req("/debug/component-logging"): handleC2NDebugComponentLogging, req("/debug/logheap"): handleC2NDebugLogHeap, - req("POST /debug/netmap"): handleC2NDebugNetMap, + req("/debug/netmap"): handleC2NDebugNetMap, // PPROF - We only expose a subset of typical pprof endpoints for security. req("/debug/pprof/heap"): handleC2NPprof, @@ -161,37 +155,42 @@ func handleC2NLogtailFlush(b *LocalBackend, w http.ResponseWriter, r *http.Reque func handleC2NDebugNetMap(b *LocalBackend, w http.ResponseWriter, r *http.Request) { ctx := r.Context() - b.logf("c2n: POST /debug/netmap received") - var body struct { - // If set, this MapResponse is used to generate a candidate netmap - // to compare against the current one. - Candidate *tailcfg.MapResponse `json:"candidate,omitempty"` - } - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + if r.Method != httpm.POST && r.Method != httpm.GET { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } - resp := struct { - // Current netmap, based on the existing map poll + all the deltas. - Current *netmap.NetworkMap `json:"current"` - // A fresh netmap generated based on the MapResponse from the request. - Candidate *netmap.NetworkMap `json:"candidate,omitempty"` // optional - Diff string - }{ - Current: b.NetMapWithPeers(), + b.logf("c2n: %s /debug/netmap received", r.Method) + + cur, err := json.Marshal(b.NetMapWithPeers()) + if err != nil { + http.Error(w, fmt.Sprintf("failed to marshal current netmap: %v", err), http.StatusInternalServerError) + return } - if body.Candidate != nil { - cand, err := controlclient.NetmapFromMapResponse(ctx, b.pm.CurrentPrefs(), body.Candidate) - if err != nil { - http.Error(w, fmt.Sprintf("failed to convert candidate MapResponse: %v", err), http.StatusBadRequest) + + resp := &tailcfg.C2NDebugNetmapResponse{Current: cur} + + if r.Method == httpm.POST { + var req tailcfg.C2NDebugNetmapRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return } - resp.Candidate = cand - resp.Diff = cmp.Diff(resp.Candidate, resp.Current, - cmp.AllowUnexported(netmap.NetworkMap{}), - cmpopts.IgnoreUnexported(views.Slice[ipproto.Proto]{}, views.Slice[tailcfg.FilterRule]{}), - cmpopts.EquateComparable(key.NodePublic{}, key.MachinePublic{}, ipproto.Proto(0), netip.Prefix{})) + + if req.Candidate != nil { + // cand, err := b.ccAuto.DirectForTest().NetmapFromMapResponse(ctx, b.pm.CurrentPrefs(), body.Candidate) + cand, err := controlclient.NetmapFromMapResponse(ctx, b.pm.CurrentPrefs(), req.Candidate) + if err != nil { + http.Error(w, fmt.Sprintf("failed to convert candidate MapResponse: %v", err), http.StatusBadRequest) + return + } + candJSON, err := json.Marshal(cand) + if err != nil { + http.Error(w, fmt.Sprintf("failed to marshal candidate netmap: %v", err), http.StatusInternalServerError) + return + } + resp.Candidate = candJSON + } } writeJSON(w, resp) } diff --git a/tailcfg/c2ntypes.go b/tailcfg/c2ntypes.go index 66f95785c..bab9a2e1d 100644 --- a/tailcfg/c2ntypes.go +++ b/tailcfg/c2ntypes.go @@ -5,7 +5,10 @@ package tailcfg -import "net/netip" +import ( + "encoding/json" + "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. @@ -117,3 +120,15 @@ type C2NVIPServicesResponse struct { // changes. This value matches what is reported in latest [Hostinfo.ServicesHash]. ServicesHash string } + +type C2NDebugNetmapRequest struct { + Candidate *MapResponse `json:"candidate,omitempty"` +} + +type C2NDebugNetmapResponse struct { + // Current is the current network map (netmap.NetworkMap). + Current json.RawMessage `json:"current"` + + // Candidate is a network map produced based on the candidate MapResponse. + Candidate json.RawMessage `json:"candidate,omitempty"` +}