mirror of https://github.com/tailscale/tailscale/
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>
pull/17160/head
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