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>
main
parent
2b0f59cd38
commit
99b3f69126
@ -0,0 +1,84 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_debugportmapper
|
||||
|
||||
package local |
||||
|
||||
import ( |
||||
"cmp" |
||||
"context" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/netip" |
||||
"net/url" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"tailscale.com/client/tailscale/apitype" |
||||
) |
||||
|
||||
// DebugPortmapOpts contains options for the [Client.DebugPortmap] command.
|
||||
type DebugPortmapOpts struct { |
||||
// Duration is how long the mapping should be created for. It defaults
|
||||
// to 5 seconds if not set.
|
||||
Duration time.Duration |
||||
|
||||
// Type is the kind of portmap to debug. The empty string instructs the
|
||||
// portmap client to perform all known types. Other valid options are
|
||||
// "pmp", "pcp", and "upnp".
|
||||
Type string |
||||
|
||||
// GatewayAddr specifies the gateway address used during portmapping.
|
||||
// If set, SelfAddr must also be set. If unset, it will be
|
||||
// autodetected.
|
||||
GatewayAddr netip.Addr |
||||
|
||||
// SelfAddr specifies the gateway address used during portmapping. If
|
||||
// set, GatewayAddr must also be set. If unset, it will be
|
||||
// autodetected.
|
||||
SelfAddr netip.Addr |
||||
|
||||
// LogHTTP instructs the debug-portmap endpoint to print all HTTP
|
||||
// requests and responses made to the logs.
|
||||
LogHTTP bool |
||||
} |
||||
|
||||
// DebugPortmap invokes the debug-portmap endpoint, and returns an
|
||||
// io.ReadCloser that can be used to read the logs that are printed during this
|
||||
// process.
|
||||
//
|
||||
// opts can be nil; if so, default values will be used.
|
||||
func (lc *Client) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) (io.ReadCloser, error) { |
||||
vals := make(url.Values) |
||||
if opts == nil { |
||||
opts = &DebugPortmapOpts{} |
||||
} |
||||
|
||||
vals.Set("duration", cmp.Or(opts.Duration, 5*time.Second).String()) |
||||
vals.Set("type", opts.Type) |
||||
vals.Set("log_http", strconv.FormatBool(opts.LogHTTP)) |
||||
|
||||
if opts.GatewayAddr.IsValid() != opts.SelfAddr.IsValid() { |
||||
return nil, fmt.Errorf("both GatewayAddr and SelfAddr must be provided if one is") |
||||
} else if opts.GatewayAddr.IsValid() { |
||||
vals.Set("gateway_and_self", fmt.Sprintf("%s/%s", opts.GatewayAddr, opts.SelfAddr)) |
||||
} |
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-portmap?"+vals.Encode(), nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
res, err := lc.doLocalRequestNiceError(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if res.StatusCode != 200 { |
||||
body, _ := io.ReadAll(res.Body) |
||||
res.Body.Close() |
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body) |
||||
} |
||||
|
||||
return res.Body, nil |
||||
} |
||||
@ -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 |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_debugportmapper
|
||||
|
||||
package buildfeatures |
||||
|
||||
// HasDebugPortMapper is whether the binary was built with support for modular feature "portmapper debug support".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_debugportmapper" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasDebugPortMapper = false |
||||
@ -0,0 +1,13 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_debugportmapper
|
||||
|
||||
package buildfeatures |
||||
|
||||
// HasDebugPortMapper is whether the binary was built with support for modular feature "portmapper debug support".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_debugportmapper" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasDebugPortMapper = true |
||||
@ -0,0 +1,13 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build ts_omit_portmapper
|
||||
|
||||
package buildfeatures |
||||
|
||||
// HasPortMapper is whether the binary was built with support for modular feature "NAT-PMP/PCP/UPnP port mapping support".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_portmapper" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasPortMapper = false |
||||
@ -0,0 +1,13 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by gen.go; DO NOT EDIT.
|
||||
|
||||
//go:build !ts_omit_portmapper
|
||||
|
||||
package buildfeatures |
||||
|
||||
// HasPortMapper is whether the binary was built with support for modular feature "NAT-PMP/PCP/UPnP port mapping support".
|
||||
// Specifically, it's whether the binary was NOT built with the "ts_omit_portmapper" build tag.
|
||||
// It's a const so it can be used for dead code elimination.
|
||||
const HasPortMapper = true |
||||
@ -0,0 +1,8 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_debugportmapper
|
||||
|
||||
package condregister |
||||
|
||||
import _ "tailscale.com/feature/debugportmapper" |
||||
@ -0,0 +1,6 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package portmapper registers support for portmapper
|
||||
// if it's not disabled via the ts_omit_portmapper build tag.
|
||||
package portmapper |
||||
@ -0,0 +1,8 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_portmapper
|
||||
|
||||
package portmapper |
||||
|
||||
import _ "tailscale.com/feature/portmapper" |
||||
@ -0,0 +1,204 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package debugportmapper registers support for debugging Tailscale's
|
||||
// portmapping support.
|
||||
package debugportmapper |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net" |
||||
"net/http" |
||||
"net/netip" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"tailscale.com/ipn/localapi" |
||||
"tailscale.com/net/netmon" |
||||
"tailscale.com/net/portmapper" |
||||
"tailscale.com/types/logger" |
||||
"tailscale.com/util/eventbus" |
||||
) |
||||
|
||||
func init() { |
||||
localapi.Register("debug-portmap", serveDebugPortmap) |
||||
} |
||||
|
||||
func serveDebugPortmap(h *localapi.Handler, 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") |
||||
|
||||
trueFunc := func() bool { return true } |
||||
// Update portmapper debug flags
|
||||
debugKnobs := &portmapper.DebugKnobs{VerboseLogs: true} |
||||
switch r.FormValue("type") { |
||||
case "": |
||||
case "pmp": |
||||
debugKnobs.DisablePCPFunc = trueFunc |
||||
debugKnobs.DisableUPnPFunc = trueFunc |
||||
case "pcp": |
||||
debugKnobs.DisablePMPFunc = trueFunc |
||||
debugKnobs.DisableUPnPFunc = trueFunc |
||||
case "upnp": |
||||
debugKnobs.DisablePCPFunc = trueFunc |
||||
debugKnobs.DisablePMPFunc = trueFunc |
||||
default: |
||||
http.Error(w, "unknown portmap debug type", http.StatusBadRequest) |
||||
return |
||||
} |
||||
if k := h.LocalBackend().ControlKnobs(); k != nil { |
||||
if k.DisableUPnP.Load() { |
||||
debugKnobs.DisableUPnPFunc = trueFunc |
||||
} |
||||
} |
||||
|
||||
if defBool(r.FormValue("log_http"), false) { |
||||
debugKnobs.LogHTTP = true |
||||
} |
||||
|
||||
var ( |
||||
logLock sync.Mutex |
||||
handlerDone bool |
||||
) |
||||
logf := func(format string, args ...any) { |
||||
if !strings.HasSuffix(format, "\n") { |
||||
format = format + "\n" |
||||
} |
||||
|
||||
logLock.Lock() |
||||
defer logLock.Unlock() |
||||
|
||||
// The portmapper can call this log function after the HTTP
|
||||
// handler returns, which is not allowed and can cause a panic.
|
||||
// If this happens, ignore the log lines since this typically
|
||||
// occurs due to a client disconnect.
|
||||
if handlerDone { |
||||
return |
||||
} |
||||
|
||||
// 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() |
||||
} |
||||
} |
||||
defer func() { |
||||
logLock.Lock() |
||||
handlerDone = true |
||||
logLock.Unlock() |
||||
}() |
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), dur) |
||||
defer cancel() |
||||
|
||||
done := make(chan bool, 1) |
||||
|
||||
var c *portmapper.Client |
||||
c = portmapper.NewClient(portmapper.Config{ |
||||
Logf: logger.WithPrefix(logf, "portmapper: "), |
||||
NetMon: h.LocalBackend().NetMon(), |
||||
DebugKnobs: debugKnobs, |
||||
EventBus: h.LocalBackend().EventBus(), |
||||
OnChange: 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") |
||||
}, |
||||
}) |
||||
defer c.Close() |
||||
|
||||
bus := eventbus.New() |
||||
defer bus.Close() |
||||
netMon, err := netmon.New(bus, 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 netMon.GatewayAndSelfIP() |
||||
} |
||||
|
||||
c.SetGatewayLookupFunc(gatewayAndSelfIP) |
||||
|
||||
gw, selfIP, ok := gatewayAndSelfIP() |
||||
if !ok { |
||||
logf("no gateway or self IP; %v", netMon.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 defBool(a string, def bool) bool { |
||||
if a == "" { |
||||
return def |
||||
} |
||||
v, err := strconv.ParseBool(a) |
||||
if err != nil { |
||||
return def |
||||
} |
||||
return v |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package portmapper registers support for NAT-PMP, PCP, and UPnP port
|
||||
// mapping protocols to help get direction connections through NATs.
|
||||
package portmapper |
||||
|
||||
import ( |
||||
"tailscale.com/net/netmon" |
||||
"tailscale.com/net/portmapper" |
||||
"tailscale.com/net/portmapper/portmappertype" |
||||
"tailscale.com/types/logger" |
||||
"tailscale.com/util/eventbus" |
||||
) |
||||
|
||||
func init() { |
||||
portmappertype.HookNewPortMapper.Set(newPortMapper) |
||||
} |
||||
|
||||
func newPortMapper( |
||||
logf logger.Logf, |
||||
bus *eventbus.Bus, |
||||
netMon *netmon.Monitor, |
||||
disableUPnPOrNil func() bool, |
||||
onlyTCP443OrNil func() bool) portmappertype.Client { |
||||
|
||||
pm := portmapper.NewClient(portmapper.Config{ |
||||
EventBus: bus, |
||||
Logf: logf, |
||||
NetMon: netMon, |
||||
DebugKnobs: &portmapper.DebugKnobs{ |
||||
DisableAll: onlyTCP443OrNil, |
||||
DisableUPnPFunc: disableUPnPOrNil, |
||||
}, |
||||
}) |
||||
pm.SetGatewayLookupFunc(netMon.GatewayAndSelfIP) |
||||
return pm |
||||
} |
||||
@ -0,0 +1,88 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package portmappertype defines the net/portmapper interface, which may or may not be
|
||||
// linked into the binary.
|
||||
package portmappertype |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"net/netip" |
||||
"time" |
||||
|
||||
"tailscale.com/feature" |
||||
"tailscale.com/net/netmon" |
||||
"tailscale.com/types/logger" |
||||
"tailscale.com/util/eventbus" |
||||
) |
||||
|
||||
// HookNewPortMapper is a hook to install the portmapper creation function.
|
||||
// It must be set by an init function when buildfeatures.HasPortmapper is true.
|
||||
var HookNewPortMapper feature.Hook[func(logf logger.Logf, |
||||
bus *eventbus.Bus, |
||||
netMon *netmon.Monitor, |
||||
disableUPnPOrNil, |
||||
onlyTCP443OrNil func() bool) Client] |
||||
|
||||
var ( |
||||
ErrNoPortMappingServices = errors.New("no port mapping services were found") |
||||
ErrGatewayRange = errors.New("skipping portmap; gateway range likely lacks support") |
||||
ErrGatewayIPv6 = errors.New("skipping portmap; no IPv6 support for portmapping") |
||||
ErrPortMappingDisabled = errors.New("port mapping is disabled") |
||||
) |
||||
|
||||
// ProbeResult is the result of a portmapper probe, saying
|
||||
// which port mapping protocols were discovered.
|
||||
type ProbeResult struct { |
||||
PCP bool |
||||
PMP bool |
||||
UPnP bool |
||||
} |
||||
|
||||
// Client is the interface implemented by a portmapper client.
|
||||
type Client interface { |
||||
// Probe returns a summary of which port mapping services are available on
|
||||
// the network.
|
||||
//
|
||||
// If a probe has run recently and there haven't been any network changes
|
||||
// since, the returned result might be server from the Client's cache,
|
||||
// without sending any network traffic.
|
||||
Probe(context.Context) (ProbeResult, error) |
||||
|
||||
// HaveMapping reports whether we have a current valid mapping.
|
||||
HaveMapping() bool |
||||
|
||||
// SetGatewayLookupFunc set the func that returns the machine's default
|
||||
// gateway IP, and the primary IP address for that gateway. It must be
|
||||
// called before the client is used. If not called,
|
||||
// interfaces.LikelyHomeRouterIP is used.
|
||||
SetGatewayLookupFunc(f func() (gw, myIP netip.Addr, ok bool)) |
||||
|
||||
// NoteNetworkDown should be called when the network has transitioned to a down state.
|
||||
// It's too late to release port mappings at this point (the user might've just turned off
|
||||
// their wifi), but we can make sure we invalidate mappings for later when the network
|
||||
// comes back.
|
||||
NoteNetworkDown() |
||||
|
||||
// GetCachedMappingOrStartCreatingOne quickly returns with our current cached portmapping, if any.
|
||||
// If there's not one, it starts up a background goroutine to create one.
|
||||
// If the background goroutine ends up creating one, the onChange hook registered with the
|
||||
// NewClient constructor (if any) will fire.
|
||||
GetCachedMappingOrStartCreatingOne() (external netip.AddrPort, ok bool) |
||||
|
||||
// SetLocalPort updates the local port number to which we want to port
|
||||
// map UDP traffic
|
||||
SetLocalPort(localPort uint16) |
||||
|
||||
Close() error |
||||
} |
||||
|
||||
// Mapping is an event recording the allocation of a port mapping.
|
||||
type Mapping struct { |
||||
External netip.AddrPort |
||||
Type string |
||||
GoodUntil time.Time |
||||
|
||||
// TODO(creachadair): Record whether we reused an existing mapping?
|
||||
} |
||||
Loading…
Reference in new issue