|
|
|
|
@ -28,132 +28,179 @@ import ( |
|
|
|
|
"tailscale.com/tailcfg" |
|
|
|
|
"tailscale.com/util/clientmetric" |
|
|
|
|
"tailscale.com/util/goroutines" |
|
|
|
|
"tailscale.com/util/httpm" |
|
|
|
|
"tailscale.com/util/set" |
|
|
|
|
"tailscale.com/util/syspolicy" |
|
|
|
|
"tailscale.com/version" |
|
|
|
|
"tailscale.com/version/distro" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go)
|
|
|
|
|
// c2nHandlers maps an HTTP method and URI path (without query parameters) to
|
|
|
|
|
// its handler. The exact method+path match is preferred, but if no entry
|
|
|
|
|
// exists for that, a map entry with an empty method is used as a fallback.
|
|
|
|
|
var c2nHandlers = map[methodAndPath]c2nHandler{ |
|
|
|
|
// Debug.
|
|
|
|
|
req("/echo"): handleC2NEcho, |
|
|
|
|
req("/debug/goroutines"): handleC2NDebugGoroutines, |
|
|
|
|
req("/debug/prefs"): handleC2NDebugPrefs, |
|
|
|
|
req("/debug/metrics"): handleC2NDebugMetrics, |
|
|
|
|
req("/debug/component-logging"): handleC2NDebugComponentLogging, |
|
|
|
|
req("/debug/logheap"): handleC2NDebugLogHeap, |
|
|
|
|
req("POST /logtail/flush"): handleC2NLogtailFlush, |
|
|
|
|
req("POST /sockstats"): handleC2NSockStats, |
|
|
|
|
|
|
|
|
|
// SSH
|
|
|
|
|
req("/ssh/usernames"): handleC2NSSHUsernames, |
|
|
|
|
|
|
|
|
|
// Auto-updates.
|
|
|
|
|
req("GET /update"): handleC2NUpdateGet, |
|
|
|
|
req("POST /update"): handleC2NUpdatePost, |
|
|
|
|
|
|
|
|
|
// Wake-on-LAN.
|
|
|
|
|
req("POST /wol"): handleC2NWoL, |
|
|
|
|
|
|
|
|
|
// Device posture.
|
|
|
|
|
req("GET /posture/identity"): handleC2NPostureIdentityGet, |
|
|
|
|
|
|
|
|
|
// App Connectors.
|
|
|
|
|
req("GET /appconnector/routes"): handleC2NAppConnectorDomainRoutesGet, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request) |
|
|
|
|
|
|
|
|
|
type methodAndPath struct { |
|
|
|
|
method string // empty string means fallback
|
|
|
|
|
path string // Request.URL.Path (without query string)
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func req(s string) methodAndPath { |
|
|
|
|
if m, p, ok := strings.Cut(s, " "); ok { |
|
|
|
|
return methodAndPath{m, p} |
|
|
|
|
} |
|
|
|
|
return methodAndPath{"", s} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// c2nHandlerPaths is all the set of paths from c2nHandlers, without their HTTP methods.
|
|
|
|
|
// It's used to detect requests with a non-matching method.
|
|
|
|
|
var c2nHandlerPaths = set.Set[string]{} |
|
|
|
|
|
|
|
|
|
func init() { |
|
|
|
|
for k := range c2nHandlers { |
|
|
|
|
c2nHandlerPaths.Add(k.path) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
// First try to match by both method and path,
|
|
|
|
|
if h, ok := c2nHandlers[methodAndPath{r.Method, r.URL.Path}]; ok { |
|
|
|
|
h(b, w, r) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
// Then try to match by just path.
|
|
|
|
|
if h, ok := c2nHandlers[methodAndPath{path: r.URL.Path}]; ok { |
|
|
|
|
h(b, w, r) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
if c2nHandlerPaths.Contains(r.URL.Path) { |
|
|
|
|
http.Error(w, "bad method", http.StatusMethodNotAllowed) |
|
|
|
|
} else { |
|
|
|
|
http.Error(w, "unknown c2n path", http.StatusBadRequest) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func writeJSON(w http.ResponseWriter, v any) { |
|
|
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
|
|
json.NewEncoder(w).Encode(v) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
switch r.URL.Path { |
|
|
|
|
case "/echo": |
|
|
|
|
// Test handler.
|
|
|
|
|
body, _ := io.ReadAll(r.Body) |
|
|
|
|
w.Write(body) |
|
|
|
|
case "/update": |
|
|
|
|
switch r.Method { |
|
|
|
|
case httpm.GET: |
|
|
|
|
b.handleC2NUpdateGet(w, r) |
|
|
|
|
case httpm.POST: |
|
|
|
|
b.handleC2NUpdatePost(w, r) |
|
|
|
|
default: |
|
|
|
|
http.Error(w, "bad method", http.StatusMethodNotAllowed) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
case "/wol": |
|
|
|
|
b.handleC2NWoL(w, r) |
|
|
|
|
func handleC2NEcho(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
// Test handler.
|
|
|
|
|
body, _ := io.ReadAll(r.Body) |
|
|
|
|
w.Write(body) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleC2NLogtailFlush(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
if b.TryFlushLogs() { |
|
|
|
|
w.WriteHeader(http.StatusNoContent) |
|
|
|
|
} else { |
|
|
|
|
http.Error(w, "no log flusher wired up", http.StatusInternalServerError) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleC2NDebugGoroutines(_ *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
|
|
w.Write(goroutines.ScrubbedGoroutineDump(true)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleC2NDebugPrefs(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
writeJSON(w, b.Prefs()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleC2NDebugMetrics(_ *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
|
|
clientmetric.WritePrometheusExpositionFormat(w) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleC2NDebugComponentLogging(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
component := r.FormValue("component") |
|
|
|
|
secs, _ := strconv.Atoi(r.FormValue("secs")) |
|
|
|
|
if secs == 0 { |
|
|
|
|
secs -= 1 |
|
|
|
|
} |
|
|
|
|
until := b.clock.Now().Add(time.Duration(secs) * time.Second) |
|
|
|
|
err := b.SetComponentDebugLogging(component, until) |
|
|
|
|
var res struct { |
|
|
|
|
Error string `json:",omitempty"` |
|
|
|
|
} |
|
|
|
|
if err != nil { |
|
|
|
|
res.Error = err.Error() |
|
|
|
|
} |
|
|
|
|
writeJSON(w, res) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go)
|
|
|
|
|
|
|
|
|
|
func handleC2NDebugLogHeap(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
if c2nLogHeap == nil { |
|
|
|
|
// Not implemented on platforms trying to optimize for binary size or
|
|
|
|
|
// reduced memory usage.
|
|
|
|
|
http.Error(w, "not implemented", http.StatusNotImplemented) |
|
|
|
|
return |
|
|
|
|
case "/logtail/flush": |
|
|
|
|
if r.Method != "POST" { |
|
|
|
|
http.Error(w, "bad method", http.StatusMethodNotAllowed) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
if b.TryFlushLogs() { |
|
|
|
|
w.WriteHeader(http.StatusNoContent) |
|
|
|
|
} else { |
|
|
|
|
http.Error(w, "no log flusher wired up", http.StatusInternalServerError) |
|
|
|
|
} |
|
|
|
|
case "/posture/identity": |
|
|
|
|
switch r.Method { |
|
|
|
|
case httpm.GET: |
|
|
|
|
b.handleC2NPostureIdentityGet(w, r) |
|
|
|
|
default: |
|
|
|
|
http.Error(w, "bad method", http.StatusMethodNotAllowed) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
case "/debug/goroutines": |
|
|
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
|
|
w.Write(goroutines.ScrubbedGoroutineDump(true)) |
|
|
|
|
case "/debug/prefs": |
|
|
|
|
writeJSON(w, b.Prefs()) |
|
|
|
|
case "/debug/metrics": |
|
|
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
|
|
clientmetric.WritePrometheusExpositionFormat(w) |
|
|
|
|
case "/debug/component-logging": |
|
|
|
|
component := r.FormValue("component") |
|
|
|
|
secs, _ := strconv.Atoi(r.FormValue("secs")) |
|
|
|
|
if secs == 0 { |
|
|
|
|
secs -= 1 |
|
|
|
|
} |
|
|
|
|
until := b.clock.Now().Add(time.Duration(secs) * time.Second) |
|
|
|
|
err := b.SetComponentDebugLogging(component, until) |
|
|
|
|
var res struct { |
|
|
|
|
Error string `json:",omitempty"` |
|
|
|
|
} |
|
|
|
|
if err != nil { |
|
|
|
|
res.Error = err.Error() |
|
|
|
|
} |
|
|
|
|
writeJSON(w, res) |
|
|
|
|
case "/debug/logheap": |
|
|
|
|
if c2nLogHeap != nil { |
|
|
|
|
c2nLogHeap(w, r) |
|
|
|
|
} else { |
|
|
|
|
http.Error(w, "not implemented", http.StatusNotImplemented) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
case "/ssh/usernames": |
|
|
|
|
var req tailcfg.C2NSSHUsernamesRequest |
|
|
|
|
if r.Method == "POST" { |
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
res, err := b.getSSHUsernames(&req) |
|
|
|
|
if err != nil { |
|
|
|
|
http.Error(w, err.Error(), 500) |
|
|
|
|
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) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
|
|
if b.sockstatLogger == nil { |
|
|
|
|
http.Error(w, "no sockstatLogger", http.StatusInternalServerError) |
|
|
|
|
} |
|
|
|
|
c2nLogHeap(w, r) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleC2NSSHUsernames(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
var req tailcfg.C2NSSHUsernamesRequest |
|
|
|
|
if r.Method == "POST" { |
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
b.sockstatLogger.Flush() |
|
|
|
|
fmt.Fprintf(w, "logid: %s\n", b.sockstatLogger.LogID()) |
|
|
|
|
fmt.Fprintf(w, "debug info: %v\n", sockstats.DebugInfo()) |
|
|
|
|
default: |
|
|
|
|
http.Error(w, "unknown c2n path", http.StatusBadRequest) |
|
|
|
|
} |
|
|
|
|
res, err := b.getSSHUsernames(&req) |
|
|
|
|
if err != nil { |
|
|
|
|
http.Error(w, err.Error(), 500) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
writeJSON(w, res) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func handleC2NSockStats(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
|
|
if b.sockstatLogger == nil { |
|
|
|
|
http.Error(w, "no sockstatLogger", http.StatusInternalServerError) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
b.sockstatLogger.Flush() |
|
|
|
|
fmt.Fprintf(w, "logid: %s\n", b.sockstatLogger.LogID()) |
|
|
|
|
fmt.Fprintf(w, "debug info: %v\n", sockstats.DebugInfo()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 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) { |
|
|
|
|
func handleC2NAppConnectorDomainRoutesGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
b.logf("c2n: GET /appconnector/routes received") |
|
|
|
|
|
|
|
|
|
var res tailcfg.C2NAppConnectorDomainRoutesResponse |
|
|
|
|
@ -169,7 +216,7 @@ func (b *LocalBackend) handleC2NAppConnectorDomainRoutesGet(w http.ResponseWrite |
|
|
|
|
json.NewEncoder(w).Encode(res) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) handleC2NUpdateGet(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
b.logf("c2n: GET /update received") |
|
|
|
|
|
|
|
|
|
res := b.newC2NUpdateResponse() |
|
|
|
|
@ -179,7 +226,7 @@ func (b *LocalBackend) handleC2NUpdateGet(w http.ResponseWriter, r *http.Request |
|
|
|
|
json.NewEncoder(w).Encode(res) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) handleC2NUpdatePost(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
func handleC2NUpdatePost(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
b.logf("c2n: POST /update received") |
|
|
|
|
res := b.newC2NUpdateResponse() |
|
|
|
|
defer func() { |
|
|
|
|
@ -255,7 +302,7 @@ func (b *LocalBackend) handleC2NUpdatePost(w http.ResponseWriter, r *http.Reques |
|
|
|
|
}() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) handleC2NPostureIdentityGet(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
b.logf("c2n: GET /posture/identity received") |
|
|
|
|
|
|
|
|
|
res := tailcfg.C2NPostureIdentityResponse{} |
|
|
|
|
@ -370,11 +417,7 @@ func regularFileExists(path string) bool { |
|
|
|
|
return err == nil && fi.Mode().IsRegular() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) handleC2NWoL(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
if r.Method != "POST" { |
|
|
|
|
http.Error(w, "bad method", http.StatusMethodNotAllowed) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
func handleC2NWoL(b *LocalBackend, w http.ResponseWriter, r *http.Request) { |
|
|
|
|
r.ParseForm() |
|
|
|
|
var macs []net.HardwareAddr |
|
|
|
|
for _, macStr := range r.Form["mac"] { |
|
|
|
|
|