@ -35,6 +35,7 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/logtail"
"tailscale.com/net/netutil"
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
@ -44,6 +45,7 @@ import (
"tailscale.com/util/httpm"
"tailscale.com/util/mak"
"tailscale.com/version"
"tailscale.com/wgengine/monitor"
)
type localAPIHandler func ( * Handler , http . ResponseWriter , * http . Request )
@ -68,6 +70,7 @@ var handler = map[string]localAPIHandler{
"debug-derp-region" : ( * Handler ) . serveDebugDERPRegion ,
"debug-packet-filter-matches" : ( * Handler ) . serveDebugPacketFilterMatches ,
"debug-packet-filter-rules" : ( * Handler ) . serveDebugPacketFilterRules ,
"debug-portmap" : ( * Handler ) . serveDebugPortmap ,
"debug-capture" : ( * Handler ) . serveDebugCapture ,
"derpmap" : ( * Handler ) . serveDERPMap ,
"dev-set-state-store" : ( * Handler ) . serveDevSetStateStore ,
@ -600,6 +603,135 @@ func (h *Handler) serveDebugPacketFilterMatches(w http.ResponseWriter, r *http.R
enc . Encode ( nm . PacketFilter )
}
func ( h * Handler ) serveDebugPortmap ( w http . ResponseWriter , r * http . Request ) {
if ! h . PermitWrite {
http . Error ( w , "debug access denied" , http . StatusForbidden )
return
}
w . Header ( ) . Set ( "Content-Type" , "text/plain" )
dur , err := time . ParseDuration ( r . FormValue ( "duration" ) )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
}
gwSelf := r . FormValue ( "gateway_and_self" )
// Update portmapper debug flags
debugKnobs := & portmapper . DebugKnobs { VerboseLogs : true }
switch r . FormValue ( "type" ) {
case "" :
case "pmp" :
debugKnobs . DisablePCP = true
debugKnobs . DisableUPnP = true
case "pcp" :
debugKnobs . DisablePMP = true
debugKnobs . DisableUPnP = true
case "upnp" :
debugKnobs . DisablePCP = true
debugKnobs . DisablePMP = true
default :
http . Error ( w , "unknown portmap debug type" , http . StatusBadRequest )
return
}
var logLock sync . Mutex
logf := func ( format string , args ... any ) {
if ! strings . HasSuffix ( format , "\n" ) {
format = format + "\n"
}
logLock . Lock ( )
defer logLock . Unlock ( )
// Write and flush each line to the client so that output is streamed
fmt . Fprintf ( w , format , args ... )
if f , ok := w . ( http . Flusher ) ; ok {
f . Flush ( )
}
}
ctx , cancel := context . WithTimeout ( r . Context ( ) , dur )
defer cancel ( )
done := make ( chan bool , 1 )
var c * portmapper . Client
c = portmapper . NewClient ( logger . WithPrefix ( logf , "portmapper: " ) , debugKnobs , func ( ) {
logf ( "portmapping changed." )
logf ( "have mapping: %v" , c . HaveMapping ( ) )
if ext , ok := c . GetCachedMappingOrStartCreatingOne ( ) ; ok {
logf ( "cb: mapping: %v" , ext )
select {
case done <- true :
default :
}
return
}
logf ( "cb: no mapping" )
} )
linkMon , err := monitor . New ( logger . WithPrefix ( logf , "monitor: " ) )
if err != nil {
logf ( "error creating monitor: %v" , err )
return
}
gatewayAndSelfIP := func ( ) ( gw , self netip . Addr , ok bool ) {
if a , b , ok := strings . Cut ( gwSelf , "/" ) ; ok {
gw = netip . MustParseAddr ( a )
self = netip . MustParseAddr ( b )
return gw , self , true
}
return linkMon . GatewayAndSelfIP ( )
}
c . SetGatewayLookupFunc ( gatewayAndSelfIP )
gw , selfIP , ok := gatewayAndSelfIP ( )
if ! ok {
logf ( "no gateway or self IP; %v" , linkMon . InterfaceState ( ) )
return
}
logf ( "gw=%v; self=%v" , gw , selfIP )
uc , err := net . ListenPacket ( "udp" , "0.0.0.0:0" )
if err != nil {
return
}
defer uc . Close ( )
c . SetLocalPort ( uint16 ( uc . LocalAddr ( ) . ( * net . UDPAddr ) . Port ) )
res , err := c . Probe ( ctx )
if err != nil {
logf ( "error in Probe: %v" , err )
return
}
logf ( "Probe: %+v" , res )
if ! res . PCP && ! res . PMP && ! res . UPnP {
logf ( "no portmapping services available" )
return
}
if ext , ok := c . GetCachedMappingOrStartCreatingOne ( ) ; ok {
logf ( "mapping: %v" , ext )
} else {
logf ( "no mapping" )
}
select {
case <- done :
case <- ctx . Done ( ) :
if r . Context ( ) . Err ( ) == nil {
logf ( "serveDebugPortmap: context done: %v" , ctx . Err ( ) )
} else {
h . logf ( "serveDebugPortmap: context done: %v" , ctx . Err ( ) )
}
}
}
func ( h * Handler ) serveComponentDebugLogging ( w http . ResponseWriter , r * http . Request ) {
if ! h . PermitWrite {
http . Error ( w , "debug access denied" , http . StatusForbidden )