feature/portmapper: make the portmapper & its debugging tools modular

Starting at a minimal binary and adding one feature back...
    tailscaled tailscale combined (linux/amd64)
     30073135  17451704  31543692 omitting everything
    +  480302 +   10258 +  493896 .. add debugportmapper
    +  475317 +  151943 +  467660 .. add portmapper
    +  500086 +  162873 +  510511 .. add portmapper+debugportmapper

Fixes #17148

Change-Id: I90bd0e9d1bd8cbe64fa2e885e9afef8fb5ee74b1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-15 19:50:21 -07:00
committed by Brad Fitzpatrick
parent 2b0f59cd38
commit 99b3f69126
36 changed files with 757 additions and 398 deletions
+79
View File
@@ -0,0 +1,79 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !ts_omit_debugportmapper
package cli
import (
"context"
"flag"
"fmt"
"io"
"net/netip"
"os"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/local"
)
func init() {
debugPortmapCmd = mkDebugPortmapCmd
}
func mkDebugPortmapCmd() *ffcli.Command {
return &ffcli.Command{
Name: "portmap",
ShortUsage: "tailscale debug portmap",
Exec: debugPortmap,
ShortHelp: "Run portmap debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("portmap")
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
fs.StringVar(&debugPortmapArgs.ty, "type", "", `portmap debug type (one of "", "pmp", "pcp", or "upnp")`)
fs.StringVar(&debugPortmapArgs.gatewayAddr, "gateway-addr", "", `override gateway IP (must also pass --self-addr)`)
fs.StringVar(&debugPortmapArgs.selfAddr, "self-addr", "", `override self IP (must also pass --gateway-addr)`)
fs.BoolVar(&debugPortmapArgs.logHTTP, "log-http", false, `print all HTTP requests and responses to the log`)
return fs
})(),
}
}
var debugPortmapArgs struct {
duration time.Duration
gatewayAddr string
selfAddr string
ty string
logHTTP bool
}
func debugPortmap(ctx context.Context, args []string) error {
opts := &local.DebugPortmapOpts{
Duration: debugPortmapArgs.duration,
Type: debugPortmapArgs.ty,
LogHTTP: debugPortmapArgs.logHTTP,
}
if (debugPortmapArgs.gatewayAddr != "") != (debugPortmapArgs.selfAddr != "") {
return fmt.Errorf("if one of --gateway-addr and --self-addr is provided, the other must be as well")
}
if debugPortmapArgs.gatewayAddr != "" {
var err error
opts.GatewayAddr, err = netip.ParseAddr(debugPortmapArgs.gatewayAddr)
if err != nil {
return fmt.Errorf("invalid --gateway-addr: %w", err)
}
opts.SelfAddr, err = netip.ParseAddr(debugPortmapArgs.selfAddr)
if err != nil {
return fmt.Errorf("invalid --self-addr: %w", err)
}
}
rc, err := localClient.DebugPortmap(ctx, opts)
if err != nil {
return err
}
defer rc.Close()
_, err = io.Copy(os.Stdout, rc)
return err
}
+2 -54
View File
@@ -30,7 +30,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/net/http/httpproxy"
"golang.org/x/net/http2"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlhttp"
"tailscale.com/hostinfo"
@@ -50,6 +49,7 @@ import (
var (
debugCaptureCmd func() *ffcli.Command // or nil
debugPortmapCmd func() *ffcli.Command // or nil
)
func debugCmd() *ffcli.Command {
@@ -319,21 +319,7 @@ func debugCmd() *ffcli.Command {
ShortHelp: "Test a DERP configuration",
},
ccall(debugCaptureCmd),
{
Name: "portmap",
ShortUsage: "tailscale debug portmap",
Exec: debugPortmap,
ShortHelp: "Run portmap debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("portmap")
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
fs.StringVar(&debugPortmapArgs.ty, "type", "", `portmap debug type (one of "", "pmp", "pcp", or "upnp")`)
fs.StringVar(&debugPortmapArgs.gatewayAddr, "gateway-addr", "", `override gateway IP (must also pass --self-addr)`)
fs.StringVar(&debugPortmapArgs.selfAddr, "self-addr", "", `override self IP (must also pass --gateway-addr)`)
fs.BoolVar(&debugPortmapArgs.logHTTP, "log-http", false, `print all HTTP requests and responses to the log`)
return fs
})(),
},
ccall(debugPortmapCmd),
{
Name: "peer-endpoint-changes",
ShortUsage: "tailscale debug peer-endpoint-changes <hostname-or-IP>",
@@ -1210,44 +1196,6 @@ func runSetExpire(ctx context.Context, args []string) error {
return localClient.DebugSetExpireIn(ctx, setExpireArgs.in)
}
var debugPortmapArgs struct {
duration time.Duration
gatewayAddr string
selfAddr string
ty string
logHTTP bool
}
func debugPortmap(ctx context.Context, args []string) error {
opts := &local.DebugPortmapOpts{
Duration: debugPortmapArgs.duration,
Type: debugPortmapArgs.ty,
LogHTTP: debugPortmapArgs.logHTTP,
}
if (debugPortmapArgs.gatewayAddr != "") != (debugPortmapArgs.selfAddr != "") {
return fmt.Errorf("if one of --gateway-addr and --self-addr is provided, the other must be as well")
}
if debugPortmapArgs.gatewayAddr != "" {
var err error
opts.GatewayAddr, err = netip.ParseAddr(debugPortmapArgs.gatewayAddr)
if err != nil {
return fmt.Errorf("invalid --gateway-addr: %w", err)
}
opts.SelfAddr, err = netip.ParseAddr(debugPortmapArgs.selfAddr)
if err != nil {
return fmt.Errorf("invalid --self-addr: %w", err)
}
}
rc, err := localClient.DebugPortmap(ctx, opts)
if err != nil {
return err
}
defer rc.Close()
_, err = io.Copy(os.Stdout, rc)
return err
}
func runPeerEndpointChanges(ctx context.Context, args []string) error {
st, err := localClient.Status(ctx)
if err != nil {
+20 -9
View File
@@ -17,14 +17,23 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/envknob"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn"
"tailscale.com/net/netcheck"
"tailscale.com/net/netmon"
"tailscale.com/net/portmapper"
"tailscale.com/net/portmapper/portmappertype"
"tailscale.com/net/tlsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
// The "netcheck" command also wants the portmapper linked.
//
// TODO: make that subcommand either hit LocalAPI for that info, or use a
// tailscaled subcommand, to avoid making the CLI also link in the portmapper.
// For now (2025-09-15), keep doing what we've done for the past five years and
// keep linking it here.
_ "tailscale.com/feature/condregister/portmapper"
)
var netcheckCmd = &ffcli.Command{
@@ -56,14 +65,13 @@ func runNetcheck(ctx context.Context, args []string) error {
return err
}
// Ensure that we close the portmapper after running a netcheck; this
// will release any port mappings created.
pm := portmapper.NewClient(portmapper.Config{
Logf: logf,
NetMon: netMon,
EventBus: bus,
})
defer pm.Close()
var pm portmappertype.Client
if buildfeatures.HasPortMapper {
// Ensure that we close the portmapper after running a netcheck; this
// will release any port mappings created.
pm = portmappertype.HookNewPortMapper.Get()(logf, bus, netMon, nil, nil)
defer pm.Close()
}
c := &netcheck.Client{
NetMon: netMon,
@@ -210,6 +218,9 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
}
func portMapping(r *netcheck.Report) string {
if !buildfeatures.HasPortMapper {
return "binary built without portmapper support"
}
if !r.AnyPortMappingChecked() {
return "not checked"
}