mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-24 17:47:30 +00:00
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:
committed by
Brad Fitzpatrick
parent
2b0f59cd38
commit
99b3f69126
84
client/local/debugportmapper.go
Normal file
84
client/local/debugportmapper.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -591,70 +591,6 @@ func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, erro
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
|
||||
// The schema (including when keys are re-read) is not a stable interface.
|
||||
func (lc *Client) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
|
||||
|
||||
@@ -32,11 +32,6 @@ type IPNBusWatcher = local.IPNBusWatcher
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
type BugReportOpts = local.BugReportOpts
|
||||
|
||||
// DebugPortmapOpts is an alias for [tailscale.com/client/local.DebugPortmapOpts].
|
||||
//
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
type DebugPortmapOpts = local.DebugPortmapOpts
|
||||
|
||||
// PingOpts is an alias for [tailscale.com/client/local.PingOpts].
|
||||
//
|
||||
// Deprecated: import [tailscale.com/client/local] instead.
|
||||
|
||||
@@ -798,7 +798,9 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/feature/condregister/portmapper from tailscale.com/tsnet
|
||||
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
|
||||
tailscale.com/feature/syspolicy from tailscale.com/logpolicy
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal+
|
||||
@@ -866,7 +868,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/net/packet from tailscale.com/net/connstats+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/tsnet
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
|
||||
var (
|
||||
cacheDir = flag.String("cachedir", "", "if non-empty, use this directory to store cached size results to speed up subsequent runs. The tool does not consider the git status when deciding whether to use the cache. It's on you to nuke it between runs if the tree changed.")
|
||||
features = flag.String("features", "", "comma-separated list of features to consider, with or without the ts_omit_ prefix")
|
||||
features = flag.String("features", "", "comma-separated list of features to list in the table, with or without the ts_omit_ prefix. It may also contain a '+' sign(s) for ANDing features together. If empty, all omittable features are considered one at a time.")
|
||||
|
||||
showRemovals = flag.Bool("show-removals", false, "if true, show a table of sizes removing one feature at a time from the full set")
|
||||
showRemovals = flag.Bool("show-removals", false, "if true, show a table of sizes removing one feature at a time from the full set.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -43,10 +43,14 @@ func main() {
|
||||
all = slices.Clone(allOmittable)
|
||||
} else {
|
||||
for v := range strings.SplitSeq(*features, ",") {
|
||||
if !strings.HasPrefix(v, "ts_omit_") {
|
||||
v = "ts_omit_" + v
|
||||
var withOmit []string
|
||||
for v := range strings.SplitSeq(v, "+") {
|
||||
if !strings.HasPrefix(v, "ts_omit_") {
|
||||
v = "ts_omit_" + v
|
||||
}
|
||||
withOmit = append(withOmit, v)
|
||||
}
|
||||
all = append(all, v)
|
||||
all = append(all, strings.Join(withOmit, "+"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +74,9 @@ func main() {
|
||||
fmt.Printf("-%8d -%8d -%8d omit-all\n", baseD-minD, baseC-minC, baseBoth-minBoth)
|
||||
|
||||
for _, t := range all {
|
||||
if strings.Contains(t, "+") {
|
||||
log.Fatalf("TODO: make --show-removals support ANDed features like %q", t)
|
||||
}
|
||||
sizeD := measure("tailscaled", t)
|
||||
sizeC := measure("tailscale", t)
|
||||
sizeBoth := measure("tailscaled", append([]string{t}, "ts_include_cli")...)
|
||||
@@ -84,17 +91,17 @@ func main() {
|
||||
fmt.Printf("%9s %9s %9s\n", "tailscaled", "tailscale", "combined (linux/amd64)")
|
||||
fmt.Printf("%9d %9d %9d omitting everything\n", minD, minC, minBoth)
|
||||
for _, t := range all {
|
||||
tags := allExcept(allOmittable, t)
|
||||
tags := allExcept(allOmittable, strings.Split(t, "+"))
|
||||
sizeD := measure("tailscaled", tags...)
|
||||
sizeC := measure("tailscale", tags...)
|
||||
sizeBoth := measure("tailscaled", append(tags, "ts_include_cli")...)
|
||||
fmt.Printf("+%8d +%8d +%8d .. add %s\n", max(sizeD-minD, 0), max(sizeC-minC, 0), max(sizeBoth-minBoth, 0), strings.TrimPrefix(t, "ts_omit_"))
|
||||
fmt.Printf("+%8d +%8d +%8d .. add %s\n", max(sizeD-minD, 0), max(sizeC-minC, 0), max(sizeBoth-minBoth, 0), strings.ReplaceAll(t, "ts_omit_", ""))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func allExcept(all []string, omit string) []string {
|
||||
return slices.DeleteFunc(slices.Clone(all), func(s string) bool { return s == omit })
|
||||
func allExcept(all, omit []string) []string {
|
||||
return slices.DeleteFunc(slices.Clone(all), func(s string) bool { return slices.Contains(omit, s) })
|
||||
}
|
||||
|
||||
func measure(bin string, tags ...string) int64 {
|
||||
|
||||
79
cmd/tailscale/cli/debug-portmap.go
Normal file
79
cmd/tailscale/cli/debug-portmap.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
|
||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derpconst from tailscale.com/derp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
@@ -105,7 +104,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web
|
||||
tailscale.com/feature from tailscale.com/tsweb+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/feature/capture/dissector from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/feature/condregister/portmapper from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
|
||||
tailscale.com/feature/syspolicy from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/health from tailscale.com/net/tlsdial+
|
||||
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
|
||||
@@ -131,7 +133,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/netutil from tailscale.com/client/local+
|
||||
tailscale.com/net/netx from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
@@ -175,7 +178,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/util/eventbus from tailscale.com/net/portmapper+
|
||||
tailscale.com/util/eventbus from tailscale.com/client/local+
|
||||
tailscale.com/util/groupmember from tailscale.com/client/web
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
@@ -351,7 +354,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
encoding/xml from github.com/godbus/dbus/v5/introspect+
|
||||
errors from archive/tar+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v3+
|
||||
|
||||
@@ -272,10 +272,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/feature/wakeonlan+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/feature/capture from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/condregister from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/feature/condregister/portmapper from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/debugportmapper from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/drive from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
|
||||
tailscale.com/feature/relayserver from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/syspolicy from tailscale.com/feature/condregister+
|
||||
tailscale.com/feature/taildrop from tailscale.com/feature/condregister
|
||||
@@ -338,7 +341,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/packet from tailscale.com/net/connstats+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper+
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/feature/portmapper+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock+
|
||||
|
||||
@@ -90,3 +90,21 @@ func TestOmitTailnetLock(t *testing.T) {
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
func TestOmitPortmapper(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
Tags: "ts_omit_portmapper,ts_include_cli,ts_omit_debugportmapper",
|
||||
OnDep: func(dep string) {
|
||||
if dep == "tailscale.com/net/portmapper" {
|
||||
t.Errorf("unexpected dep with ts_omit_portmapper: %q", dep)
|
||||
return
|
||||
}
|
||||
if strings.Contains(dep, "goupnp") || strings.Contains(dep, "/soap") ||
|
||||
strings.Contains(dep, "internetgateway2") {
|
||||
t.Errorf("unexpected dep with ts_omit_portmapper: %q", dep)
|
||||
}
|
||||
},
|
||||
}.Check(t)
|
||||
}
|
||||
|
||||
@@ -239,7 +239,9 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/feature/condregister/portmapper from tailscale.com/tsnet
|
||||
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
|
||||
tailscale.com/feature/syspolicy from tailscale.com/logpolicy
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal+
|
||||
@@ -295,7 +297,8 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/net/packet from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/tsnet
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
||||
|
||||
13
feature/buildfeatures/feature_debugportmapper_disabled.go
Normal file
13
feature/buildfeatures/feature_debugportmapper_disabled.go
Normal file
@@ -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
|
||||
13
feature/buildfeatures/feature_debugportmapper_enabled.go
Normal file
13
feature/buildfeatures/feature_debugportmapper_enabled.go
Normal file
@@ -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
|
||||
13
feature/buildfeatures/feature_portmapper_disabled.go
Normal file
13
feature/buildfeatures/feature_portmapper_disabled.go
Normal file
@@ -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
|
||||
13
feature/buildfeatures/feature_portmapper_enabled.go
Normal file
13
feature/buildfeatures/feature_portmapper_enabled.go
Normal file
@@ -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
|
||||
@@ -5,3 +5,10 @@
|
||||
// by build tags. It is one central package that callers can empty import
|
||||
// to ensure all conditional features are registered.
|
||||
package condregister
|
||||
|
||||
// Portmapper is special in that the CLI also needs to link it in,
|
||||
// so it's pulled out into its own package, rather than using a maybe_*.go
|
||||
// file in condregister.
|
||||
import (
|
||||
_ "tailscale.com/feature/condregister/portmapper"
|
||||
)
|
||||
|
||||
8
feature/condregister/maybe_debugportmapper.go
Normal file
8
feature/condregister/maybe_debugportmapper.go
Normal file
@@ -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"
|
||||
6
feature/condregister/portmapper/doc.go
Normal file
6
feature/condregister/portmapper/doc.go
Normal file
@@ -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
|
||||
8
feature/condregister/portmapper/maybe_portmapper.go
Normal file
8
feature/condregister/portmapper/maybe_portmapper.go
Normal file
@@ -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"
|
||||
204
feature/debugportmapper/debugportmapper.go
Normal file
204
feature/debugportmapper/debugportmapper.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -48,9 +48,11 @@ var Features = map[FeatureTag]FeatureMeta{
|
||||
"cli": {"CLI", "embed the CLI into the tailscaled binary"},
|
||||
"completion": {"Completion", "CLI shell completion"},
|
||||
"debugeventbus": {"DebugEventBus", "eventbus debug support"},
|
||||
"debugportmapper": {"DebugPortMapper", "portmapper debug support"},
|
||||
"desktop_sessions": {"DesktopSessions", "Desktop sessions support"},
|
||||
"drive": {"Drive", "Tailscale Drive (file server) support"},
|
||||
"kube": {"Kube", "Kubernetes integration"},
|
||||
"portmapper": {"PortMapper", "NAT-PMP/PCP/UPnP port mapping support"},
|
||||
"relayserver": {"RelayServer", "Relay server"},
|
||||
"serve": {"Serve", "Serve and Funnel support"},
|
||||
"ssh": {"SSH", "Tailscale SSH support"},
|
||||
|
||||
38
feature/portmapper/portmapper.go
Normal file
38
feature/portmapper/portmapper.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -6780,6 +6780,11 @@ func (b *LocalBackend) ControlKnobs() *controlknobs.Knobs {
|
||||
return b.sys.ControlKnobs()
|
||||
}
|
||||
|
||||
// EventBus returns the node's event bus.
|
||||
func (b *LocalBackend) EventBus() *eventbus.Bus {
|
||||
return b.sys.Bus.Get()
|
||||
}
|
||||
|
||||
// MagicConn returns the backend's *magicsock.Conn.
|
||||
func (b *LocalBackend) MagicConn() *magicsock.Conn {
|
||||
return b.sys.MagicSock.Get()
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
_ "tailscale.com/feature/condregister/portmapper"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
|
||||
@@ -35,9 +35,7 @@ import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/dnstype"
|
||||
@@ -90,7 +88,6 @@ var handler = map[string]LocalAPIHandler{
|
||||
"debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches,
|
||||
"debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules,
|
||||
"debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges,
|
||||
"debug-portmap": (*Handler).serveDebugPortmap,
|
||||
"derpmap": (*Handler).serveDERPMap,
|
||||
"dev-set-state-store": (*Handler).serveDevSetStateStore,
|
||||
"dial": (*Handler).serveDial,
|
||||
@@ -762,166 +759,6 @@ 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
|
||||
}
|
||||
|
||||
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.b.NetMon(),
|
||||
DebugKnobs: debugKnobs,
|
||||
ControlKnobs: h.b.ControlKnobs(),
|
||||
EventBus: h.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EventError provides the JSON encoding of internal errors from event processing.
|
||||
type EventError struct {
|
||||
Error string
|
||||
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/ping"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/portmapper/portmappertype"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/syncs"
|
||||
@@ -215,7 +215,7 @@ type Client struct {
|
||||
|
||||
// PortMapper, if non-nil, is used for portmap queries.
|
||||
// If nil, portmap discovery is not done.
|
||||
PortMapper *portmapper.Client // lazily initialized on first use
|
||||
PortMapper portmappertype.Client
|
||||
|
||||
// UseDNSCache controls whether this client should use a
|
||||
// *dnscache.Resolver to resolve DERP hostnames, when no IP address is
|
||||
@@ -730,7 +730,7 @@ func (rs *reportState) probePortMapServices() {
|
||||
|
||||
res, err := rs.c.PortMapper.Probe(context.Background())
|
||||
if err != nil {
|
||||
if !errors.Is(err, portmapper.ErrGatewayRange) {
|
||||
if !errors.Is(err, portmappertype.ErrGatewayRange) {
|
||||
// "skipping portmap; gateway range likely lacks support"
|
||||
// is not very useful, and too spammy on cloud systems.
|
||||
// If there are other errors, we want to log those.
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/syncs"
|
||||
@@ -273,10 +272,9 @@ func newTestClient(t *testing.T, igd *TestIGD, bus *eventbus.Bus) *Client {
|
||||
}
|
||||
var c *Client
|
||||
c = NewClient(Config{
|
||||
Logf: tstest.WhileTestRunningLogger(t),
|
||||
NetMon: netmon.NewStatic(),
|
||||
ControlKnobs: new(controlknobs.Knobs),
|
||||
EventBus: bus,
|
||||
Logf: tstest.WhileTestRunningLogger(t),
|
||||
NetMon: netmon.NewStatic(),
|
||||
EventBus: bus,
|
||||
OnChange: func() { // TODO(creachadair): Remove.
|
||||
t.Logf("port map changed")
|
||||
t.Logf("have mapping: %v", c.HaveMapping())
|
||||
|
||||
@@ -8,7 +8,6 @@ package portmapper
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -20,12 +19,12 @@ import (
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/neterror"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/portmapper/portmappertype"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -34,6 +33,13 @@ import (
|
||||
"tailscale.com/util/eventbus"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoPortMappingServices = portmappertype.ErrNoPortMappingServices
|
||||
ErrGatewayRange = portmappertype.ErrGatewayRange
|
||||
ErrGatewayIPv6 = portmappertype.ErrGatewayIPv6
|
||||
ErrPortMappingDisabled = portmappertype.ErrPortMappingDisabled
|
||||
)
|
||||
|
||||
var disablePortMapperEnv = envknob.RegisterBool("TS_DISABLE_PORTMAPPER")
|
||||
|
||||
// DebugKnobs contains debug configuration that can be provided when creating a
|
||||
@@ -49,15 +55,33 @@ type DebugKnobs struct {
|
||||
LogHTTP bool
|
||||
|
||||
// Disable* disables a specific service from mapping.
|
||||
DisableUPnP bool
|
||||
DisablePMP bool
|
||||
DisablePCP bool
|
||||
// If the funcs are nil or return false, the service is not disabled.
|
||||
// Use the corresponding accessor methods without the "Func" suffix
|
||||
// to check whether a service is disabled.
|
||||
DisableUPnPFunc func() bool
|
||||
DisablePMPFunc func() bool
|
||||
DisablePCPFunc func() bool
|
||||
|
||||
// DisableAll, if non-nil, is a func that reports whether all port
|
||||
// mapping attempts should be disabled.
|
||||
DisableAll func() bool
|
||||
}
|
||||
|
||||
// DisableUPnP reports whether UPnP is disabled.
|
||||
func (k *DebugKnobs) DisableUPnP() bool {
|
||||
return k != nil && k.DisableUPnPFunc != nil && k.DisableUPnPFunc()
|
||||
}
|
||||
|
||||
// DisablePMP reports whether NAT-PMP is disabled.
|
||||
func (k *DebugKnobs) DisablePMP() bool {
|
||||
return k != nil && k.DisablePMPFunc != nil && k.DisablePMPFunc()
|
||||
}
|
||||
|
||||
// DisablePCP reports whether PCP is disabled.
|
||||
func (k *DebugKnobs) DisablePCP() bool {
|
||||
return k != nil && k.DisablePCPFunc != nil && k.DisablePCPFunc()
|
||||
}
|
||||
|
||||
func (k *DebugKnobs) disableAll() bool {
|
||||
if disablePortMapperEnv() {
|
||||
return true
|
||||
@@ -88,11 +112,10 @@ type Client struct {
|
||||
// The following two fields must both be non-nil.
|
||||
// Both are immutable after construction.
|
||||
pubClient *eventbus.Client
|
||||
updates *eventbus.Publisher[Mapping]
|
||||
updates *eventbus.Publisher[portmappertype.Mapping]
|
||||
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
|
||||
controlKnobs *controlknobs.Knobs
|
||||
ipAndGateway func() (gw, ip netip.Addr, ok bool)
|
||||
onChange func() // or nil
|
||||
debug DebugKnobs
|
||||
@@ -130,6 +153,8 @@ type Client struct {
|
||||
mapping mapping // non-nil if we have a mapping
|
||||
}
|
||||
|
||||
var _ portmappertype.Client = (*Client)(nil)
|
||||
|
||||
func (c *Client) vlogf(format string, args ...any) {
|
||||
if c.debug.VerboseLogs {
|
||||
c.logf(format, args...)
|
||||
@@ -159,7 +184,6 @@ type mapping interface {
|
||||
MappingDebug() string
|
||||
}
|
||||
|
||||
// HaveMapping reports whether we have a current valid mapping.
|
||||
func (c *Client) HaveMapping() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -223,10 +247,6 @@ type Config struct {
|
||||
// debugging. If nil, a sensible set of defaults will be used.
|
||||
DebugKnobs *DebugKnobs
|
||||
|
||||
// ControlKnobs, if non-nil, specifies knobs from the control plane that
|
||||
// might disable port mapping.
|
||||
ControlKnobs *controlknobs.Knobs
|
||||
|
||||
// OnChange is called to run in a new goroutine whenever the port mapping
|
||||
// status has changed. If nil, no callback is issued.
|
||||
OnChange func()
|
||||
@@ -246,10 +266,9 @@ func NewClient(c Config) *Client {
|
||||
netMon: c.NetMon,
|
||||
ipAndGateway: netmon.LikelyHomeRouterIP, // TODO(bradfitz): move this to method on netMon
|
||||
onChange: c.OnChange,
|
||||
controlKnobs: c.ControlKnobs,
|
||||
}
|
||||
ret.pubClient = c.EventBus.Client("portmapper")
|
||||
ret.updates = eventbus.Publish[Mapping](ret.pubClient)
|
||||
ret.updates = eventbus.Publish[portmappertype.Mapping](ret.pubClient)
|
||||
if ret.logf == nil {
|
||||
ret.logf = logger.Discard
|
||||
}
|
||||
@@ -448,13 +467,6 @@ func IsNoMappingError(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -512,7 +524,7 @@ func (c *Client) createMapping() {
|
||||
// the control flow to eliminate that possibility. Meanwhile, this
|
||||
// mitigates a panic downstream, cf. #16662.
|
||||
}
|
||||
c.updates.Publish(Mapping{
|
||||
c.updates.Publish(portmappertype.Mapping{
|
||||
External: mapping.External(),
|
||||
Type: mapping.MappingType(),
|
||||
GoodUntil: mapping.GoodUntil(),
|
||||
@@ -524,15 +536,6 @@ func (c *Client) createMapping() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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?
|
||||
}
|
||||
|
||||
// wildcardIP is used when the previous external IP is not known for PCP port mapping.
|
||||
var wildcardIP = netip.MustParseAddr("0.0.0.0")
|
||||
|
||||
@@ -545,7 +548,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
|
||||
if c.debug.disableAll() {
|
||||
return nil, netip.AddrPort{}, NoMappingError{ErrPortMappingDisabled}
|
||||
}
|
||||
if c.debug.DisableUPnP && c.debug.DisablePCP && c.debug.DisablePMP {
|
||||
if c.debug.DisableUPnP() && c.debug.DisablePCP() && c.debug.DisablePMP() {
|
||||
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
gw, myIP, ok := c.gatewayAndSelfIP()
|
||||
@@ -624,7 +627,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
|
||||
prevPort = m.External().Port()
|
||||
}
|
||||
|
||||
if c.debug.DisablePCP && c.debug.DisablePMP {
|
||||
if c.debug.DisablePCP() && c.debug.DisablePMP() {
|
||||
c.mu.Unlock()
|
||||
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
|
||||
return nil, external, nil
|
||||
@@ -675,7 +678,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
|
||||
|
||||
pxpAddr := netip.AddrPortFrom(gw, c.pxpPort())
|
||||
|
||||
preferPCP := !c.debug.DisablePCP && (c.debug.DisablePMP || (!haveRecentPMP && haveRecentPCP))
|
||||
preferPCP := !c.debug.DisablePCP() && (c.debug.DisablePMP() || (!haveRecentPMP && haveRecentPCP))
|
||||
|
||||
// Create a mapping, defaulting to PMP unless only PCP was seen recently.
|
||||
if preferPCP {
|
||||
@@ -860,19 +863,13 @@ func parsePMPResponse(pkt []byte) (res pmpResponse, ok bool) {
|
||||
return res, true
|
||||
}
|
||||
|
||||
type ProbeResult struct {
|
||||
PCP bool
|
||||
PMP bool
|
||||
UPnP bool
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
func (c *Client) Probe(ctx context.Context) (res portmappertype.ProbeResult, err error) {
|
||||
if c.debug.disableAll() {
|
||||
return res, ErrPortMappingDisabled
|
||||
}
|
||||
@@ -907,19 +904,19 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
// https://github.com/tailscale/tailscale/issues/1001
|
||||
if c.sawPMPRecently() {
|
||||
res.PMP = true
|
||||
} else if !c.debug.DisablePMP {
|
||||
} else if !c.debug.DisablePMP() {
|
||||
metricPMPSent.Add(1)
|
||||
uc.WriteToUDPAddrPort(pmpReqExternalAddrPacket, pxpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
res.PCP = true
|
||||
} else if !c.debug.DisablePCP {
|
||||
} else if !c.debug.DisablePCP() {
|
||||
metricPCPSent.Add(1)
|
||||
uc.WriteToUDPAddrPort(pcpAnnounceRequest(myIP), pxpAddr)
|
||||
}
|
||||
if c.sawUPnPRecently() {
|
||||
res.UPnP = true
|
||||
} else if !c.debug.DisableUPnP {
|
||||
} else if !c.debug.DisableUPnP() {
|
||||
// Strictly speaking, you discover UPnP services by sending an
|
||||
// SSDP query (which uPnPPacket is) to udp/1900 on the SSDP
|
||||
// multicast address, and then get a flood of responses back
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/net/portmapper/portmappertype"
|
||||
"tailscale.com/util/eventbus/eventbustest"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestCreateOrGetMapping(t *testing.T) {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
|
||||
t.Skip("skipping test without HIT_NETWORK=1")
|
||||
}
|
||||
c := NewClient(Config{Logf: t.Logf, ControlKnobs: new(controlknobs.Knobs)})
|
||||
c := NewClient(Config{Logf: t.Logf})
|
||||
defer c.Close()
|
||||
c.SetLocalPort(1234)
|
||||
for i := range 2 {
|
||||
@@ -35,7 +35,7 @@ func TestClientProbe(t *testing.T) {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
|
||||
t.Skip("skipping test without HIT_NETWORK=1")
|
||||
}
|
||||
c := NewClient(Config{Logf: t.Logf, ControlKnobs: new(controlknobs.Knobs)})
|
||||
c := NewClient(Config{Logf: t.Logf})
|
||||
defer c.Close()
|
||||
for i := range 3 {
|
||||
if i > 0 {
|
||||
@@ -50,7 +50,7 @@ func TestClientProbeThenMap(t *testing.T) {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
|
||||
t.Skip("skipping test without HIT_NETWORK=1")
|
||||
}
|
||||
c := NewClient(Config{Logf: t.Logf, ControlKnobs: new(controlknobs.Knobs)})
|
||||
c := NewClient(Config{Logf: t.Logf})
|
||||
defer c.Close()
|
||||
c.debug.VerboseLogs = true
|
||||
c.SetLocalPort(1234)
|
||||
@@ -150,7 +150,7 @@ func TestUpdateEvent(t *testing.T) {
|
||||
t.Fatalf("Probe failed: %v", err)
|
||||
}
|
||||
c.GetCachedMappingOrStartCreatingOne()
|
||||
if err := eventbustest.Expect(tw, eventbustest.Type[Mapping]()); err != nil {
|
||||
if err := eventbustest.Expect(tw, eventbustest.Type[portmappertype.Mapping]()); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
88
net/portmapper/portmappertype/portmappertype.go
Normal file
88
net/portmapper/portmappertype/portmappertype.go
Normal file
@@ -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?
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func addAnyPortMapping(
|
||||
// The meta is the most recently parsed UDP discovery packet response
|
||||
// from the Internet Gateway Device.
|
||||
func getUPnPRootDevice(ctx context.Context, logf logger.Logf, debug DebugKnobs, gw netip.Addr, meta uPnPDiscoResponse) (rootDev *goupnp.RootDevice, loc *url.URL, err error) {
|
||||
if debug.DisableUPnP {
|
||||
if debug.DisableUPnP() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
@@ -434,7 +434,7 @@ func (c *Client) getUPnPPortMapping(
|
||||
internal netip.AddrPort,
|
||||
prevPort uint16,
|
||||
) (external netip.AddrPort, ok bool) {
|
||||
if disableUPnpEnv() || c.debug.DisableUPnP || (c.controlKnobs != nil && c.controlKnobs.DisableUPnP.Load()) {
|
||||
if disableUPnpEnv() || c.debug.DisableUPnP() {
|
||||
return netip.AddrPort{}, false
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/portmapper/portmappertype"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -1039,7 +1040,7 @@ func (u *upnpServer) handleControl(w http.ResponseWriter, r *http.Request, handl
|
||||
}
|
||||
}
|
||||
|
||||
func mustProbeUPnP(tb testing.TB, ctx context.Context, c *Client) ProbeResult {
|
||||
func mustProbeUPnP(tb testing.TB, ctx context.Context, c *Client) portmappertype.ProbeResult {
|
||||
tb.Helper()
|
||||
res, err := c.Probe(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -235,7 +235,9 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/feature/condregister/portmapper from tailscale.com/tsnet
|
||||
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
|
||||
tailscale.com/feature/syspolicy from tailscale.com/logpolicy
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal+
|
||||
@@ -291,7 +293,8 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
tailscale.com/net/packet from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/tsnet
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
_ "tailscale.com/feature/condregister/portmapper"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -44,7 +45,7 @@ import (
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/ping"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/portmapper/portmappertype"
|
||||
"tailscale.com/net/sockopts"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/stun"
|
||||
@@ -177,7 +178,7 @@ type Conn struct {
|
||||
|
||||
// These [eventbus.Subscriber] fields are solely accessed by
|
||||
// consumeEventbusTopics once initialized.
|
||||
pmSub *eventbus.Subscriber[portmapper.Mapping]
|
||||
pmSub *eventbus.Subscriber[portmappertype.Mapping]
|
||||
filterSub *eventbus.Subscriber[FilterUpdate]
|
||||
nodeViewsSub *eventbus.Subscriber[NodeViewsUpdate]
|
||||
nodeMutsSub *eventbus.Subscriber[NodeMutationsUpdate]
|
||||
@@ -207,7 +208,8 @@ type Conn struct {
|
||||
|
||||
// portMapper is the NAT-PMP/PCP/UPnP prober/client, for requesting
|
||||
// port mappings from NAT devices.
|
||||
portMapper *portmapper.Client
|
||||
// If nil, the portmapper is disabled.
|
||||
portMapper portmappertype.Client
|
||||
|
||||
// derpRecvCh is used by receiveDERP to read DERP messages.
|
||||
// It must have buffer size > 0; see issue 3736.
|
||||
@@ -731,7 +733,7 @@ func NewConn(opts Options) (*Conn, error) {
|
||||
|
||||
// Subscribe calls must return before NewConn otherwise published
|
||||
// events can be missed.
|
||||
c.pmSub = eventbus.Subscribe[portmapper.Mapping](c.eventClient)
|
||||
c.pmSub = eventbus.Subscribe[portmappertype.Mapping](c.eventClient)
|
||||
c.filterSub = eventbus.Subscribe[FilterUpdate](c.eventClient)
|
||||
c.nodeViewsSub = eventbus.Subscribe[NodeViewsUpdate](c.eventClient)
|
||||
c.nodeMutsSub = eventbus.Subscribe[NodeMutationsUpdate](c.eventClient)
|
||||
@@ -747,19 +749,21 @@ func NewConn(opts Options) (*Conn, error) {
|
||||
|
||||
// Don't log the same log messages possibly every few seconds in our
|
||||
// portmapper.
|
||||
portmapperLogf := logger.WithPrefix(c.logf, "portmapper: ")
|
||||
portmapperLogf = netmon.LinkChangeLogLimiter(c.connCtx, portmapperLogf, opts.NetMon)
|
||||
portMapOpts := &portmapper.DebugKnobs{
|
||||
DisableAll: func() bool { return opts.DisablePortMapper || c.onlyTCP443.Load() },
|
||||
if buildfeatures.HasPortMapper && !opts.DisablePortMapper {
|
||||
portmapperLogf := logger.WithPrefix(c.logf, "portmapper: ")
|
||||
portmapperLogf = netmon.LinkChangeLogLimiter(c.connCtx, portmapperLogf, opts.NetMon)
|
||||
var disableUPnP func() bool
|
||||
if c.controlKnobs != nil {
|
||||
disableUPnP = c.controlKnobs.DisableUPnP.Load
|
||||
}
|
||||
newPortMapper, ok := portmappertype.HookNewPortMapper.GetOk()
|
||||
if ok {
|
||||
c.portMapper = newPortMapper(portmapperLogf, opts.EventBus, opts.NetMon, disableUPnP, c.onlyTCP443.Load)
|
||||
} else if !testenv.InTest() {
|
||||
panic("unexpected: HookNewPortMapper not set")
|
||||
}
|
||||
}
|
||||
c.portMapper = portmapper.NewClient(portmapper.Config{
|
||||
EventBus: c.eventBus,
|
||||
Logf: portmapperLogf,
|
||||
NetMon: opts.NetMon,
|
||||
DebugKnobs: portMapOpts,
|
||||
ControlKnobs: opts.ControlKnobs,
|
||||
})
|
||||
c.portMapper.SetGatewayLookupFunc(opts.NetMon.GatewayAndSelfIP)
|
||||
|
||||
c.netMon = opts.NetMon
|
||||
c.health = opts.HealthTracker
|
||||
c.onPortUpdate = opts.OnPortUpdate
|
||||
@@ -1081,7 +1085,9 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) {
|
||||
UPnP: report.UPnP,
|
||||
PMP: report.PMP,
|
||||
PCP: report.PCP,
|
||||
HavePortMap: c.portMapper.HaveMapping(),
|
||||
}
|
||||
if c.portMapper != nil {
|
||||
ni.HavePortMap = c.portMapper.HaveMapping()
|
||||
}
|
||||
for rid, d := range report.RegionV4Latency {
|
||||
ni.DERPLatency[fmt.Sprintf("%d-v4", rid)] = d.Seconds()
|
||||
@@ -1248,7 +1254,7 @@ func (c *Conn) DiscoPublicKey() key.DiscoPublic {
|
||||
func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, error) {
|
||||
var havePortmap bool
|
||||
var portmapExt netip.AddrPort
|
||||
if runtime.GOOS != "js" {
|
||||
if runtime.GOOS != "js" && c.portMapper != nil {
|
||||
portmapExt, havePortmap = c.portMapper.GetCachedMappingOrStartCreatingOne()
|
||||
}
|
||||
|
||||
@@ -1288,7 +1294,7 @@ func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, erro
|
||||
}
|
||||
|
||||
// If we didn't have a portmap earlier, maybe it's done by now.
|
||||
if !havePortmap {
|
||||
if !havePortmap && c.portMapper != nil {
|
||||
portmapExt, havePortmap = c.portMapper.GetCachedMappingOrStartCreatingOne()
|
||||
}
|
||||
if havePortmap {
|
||||
@@ -2662,7 +2668,9 @@ func (c *Conn) SetNetworkUp(up bool) {
|
||||
if up {
|
||||
c.startDerpHomeConnectLocked()
|
||||
} else {
|
||||
c.portMapper.NoteNetworkDown()
|
||||
if c.portMapper != nil {
|
||||
c.portMapper.NoteNetworkDown()
|
||||
}
|
||||
c.closeAllDerpLocked("network-down")
|
||||
}
|
||||
}
|
||||
@@ -3324,7 +3332,9 @@ func (c *Conn) Close() error {
|
||||
c.derpCleanupTimer.Stop()
|
||||
}
|
||||
c.stopPeriodicReSTUNTimerLocked()
|
||||
c.portMapper.Close()
|
||||
if c.portMapper != nil {
|
||||
c.portMapper.Close()
|
||||
}
|
||||
|
||||
c.peerMap.forEachEndpoint(func(ep *endpoint) {
|
||||
ep.stopAndReset()
|
||||
@@ -3577,7 +3587,9 @@ func (c *Conn) rebind(curPortFate currentPortFate) error {
|
||||
if err := c.bindSocket(&c.pconn4, "udp4", curPortFate); err != nil {
|
||||
return fmt.Errorf("magicsock: Rebind IPv4 failed: %w", err)
|
||||
}
|
||||
c.portMapper.SetLocalPort(c.LocalPort())
|
||||
if c.portMapper != nil {
|
||||
c.portMapper.SetLocalPort(c.LocalPort())
|
||||
}
|
||||
c.UpdatePMTUD()
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user