Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ builds:
# internal/buildtags.ReleaseTags, which a test enforces.
# Regenerate with: go run ./internal/buildtags/printtags
flags:
- -tags=ts_omit_ace,ts_omit_acme,ts_omit_advertiseexitnode,ts_omit_advertiseroutes,ts_omit_appconnectors,ts_omit_aws,ts_omit_bird,ts_omit_cachenetmap,ts_omit_captiveportal,ts_omit_capture,ts_omit_cliconndiag,ts_omit_clientmetrics,ts_omit_clientupdate,ts_omit_cloud,ts_omit_colorable,ts_omit_completion,ts_omit_completion_scripts,ts_omit_conn25,ts_omit_connreject,ts_omit_debug,ts_omit_debugeventbus,ts_omit_debugportmapper,ts_omit_desktop_sessions,ts_omit_dns,ts_omit_dnsresolvecache,ts_omit_doctor,ts_omit_drive,ts_omit_favorites,ts_omit_flashappliance,ts_omit_health,ts_omit_hujsonconf,ts_omit_identityfederation,ts_omit_ipnbus,ts_omit_iptables,ts_omit_kube,ts_omit_linkspeed,ts_omit_linuxdnsfight,ts_omit_listenrawdisco,ts_omit_logtail,ts_omit_netlog,ts_omit_networkmanager,ts_omit_oauthkey,ts_omit_osrouter,ts_omit_outboundproxy,ts_omit_peerapiclient,ts_omit_peerapiserver,ts_omit_portlist,ts_omit_portmapper,ts_omit_posture,ts_omit_qrcodes,ts_omit_relayserver,ts_omit_remoteconfig,ts_omit_resolved,ts_omit_routecheck,ts_omit_runtimemetrics,ts_omit_sdnotify,ts_omit_serve,ts_omit_serviceclientprefs,ts_omit_synology,ts_omit_syslog,ts_omit_syspolicy,ts_omit_systray,ts_omit_taildrop,ts_omit_tailnetlock,ts_omit_tap,ts_omit_tpm,ts_omit_tundevstats,ts_omit_unixsocketidentity,ts_omit_useexitnode,ts_omit_useproxy,ts_omit_usermetrics,ts_omit_useroutes,ts_omit_wakeonlan,ts_omit_webbrowser,ts_omit_webclient
- -tags=ts_omit_ace,ts_omit_acme,ts_omit_advertiseexitnode,ts_omit_advertiseroutes,ts_omit_appconnectors,ts_omit_aws,ts_omit_bird,ts_omit_cachenetmap,ts_omit_captiveportal,ts_omit_capture,ts_omit_cliconndiag,ts_omit_clientmetrics,ts_omit_clientupdate,ts_omit_cloud,ts_omit_colorable,ts_omit_completion,ts_omit_completion_scripts,ts_omit_conn25,ts_omit_connreject,ts_omit_debug,ts_omit_debugeventbus,ts_omit_debugportmapper,ts_omit_desktop_sessions,ts_omit_dns,ts_omit_dnsresolvecache,ts_omit_doctor,ts_omit_drive,ts_omit_favorites,ts_omit_flashappliance,ts_omit_health,ts_omit_hujsonconf,ts_omit_identityfederation,ts_omit_ipnbus,ts_omit_iptables,ts_omit_kube,ts_omit_linkspeed,ts_omit_linuxdnsfight,ts_omit_listenrawdisco,ts_omit_logtail,ts_omit_netlog,ts_omit_networkmanager,ts_omit_oauthkey,ts_omit_osrouter,ts_omit_outboundproxy,ts_omit_peerapiclient,ts_omit_peerapiserver,ts_omit_portlist,ts_omit_posture,ts_omit_qrcodes,ts_omit_relayserver,ts_omit_remoteconfig,ts_omit_resolved,ts_omit_routecheck,ts_omit_runtimemetrics,ts_omit_sdnotify,ts_omit_serve,ts_omit_serviceclientprefs,ts_omit_synology,ts_omit_syslog,ts_omit_syspolicy,ts_omit_systray,ts_omit_taildrop,ts_omit_tailnetlock,ts_omit_tap,ts_omit_tpm,ts_omit_tundevstats,ts_omit_unixsocketidentity,ts_omit_useexitnode,ts_omit_useproxy,ts_omit_usermetrics,ts_omit_useroutes,ts_omit_wakeonlan,ts_omit_webbrowser,ts_omit_webclient
goos:
- linux
- windows
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## Unreleased

- Native builds now include Tailscale's port mapper: each side asks
its local router for a public UDP port mapping over UPnP IGD,
NAT-PMP, or PCP, and advertises that endpoint alongside its STUN and
local ones. A peer behind a NAT that allocates a new public port per
destination (common on hotel, office, and carrier networks) can now
get a direct path to a peer behind an ordinary home router, where it
previously stayed relayed over DERP for the whole connection. Set
`TS_DISABLE_PORTMAPPER=1` to turn it off, or `TS_DISABLE_UPNP=1` to
turn off only UPnP. The wasm build is unchanged. It adds about 350 KB
(1.9%) to the stripped linux/amd64 binary.
([#143](https://github.com/tailscale/tailcat/issues/143))
- **Breaking Go API change:** `Server.AllowedClients` and
`Server.AddAllowedClient` are replaced by the `Server.AllowClient`
hook, a `func(key.NodePublic) bool` asked about each client as it
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,9 @@ without the control plane.
implementation for encrypting all tunnel traffic. It doesn't use a kernel TUN/TAP device (nor does it configure any networking routes or DNS settings), so `root` isn't required.
- **magicsock** -- Tailscale's transport layer that multiplexes traffic
over direct UDP and DERP relays. It handles STUN-based endpoint
discovery and UDP hole-punching for NAT traversal.
discovery and UDP hole-punching for NAT traversal, and asks the
local router for a public port mapping (UPnP IGD, NAT-PMP, or PCP)
when the router offers one.
- **Netstack** (gVisor) -- a userspace TCP/IP stack that terminates
TCP connections inside the process. This is what lets Tailcat
accept inbound connections and dial outbound ones without any OS
Expand Down Expand Up @@ -767,9 +769,15 @@ without the control plane.
encrypted traffic can flow.

5. **NAT traversal.** In parallel, each side advertises its UDP
endpoints (public IP:port learned via STUN, plus local interface
addresses) to the other in disco call-me-maybe messages over DERP,
re-advertising whenever they change. Both sides then run Tailscale's
endpoints (public IP:port learned via STUN, a port mapped on the
local router via UPnP IGD, NAT-PMP, or PCP when the router allows
it, plus local interface addresses) to the other in disco
call-me-maybe messages over DERP, re-advertising whenever they
change. A mapped port accepts packets from any source, so it gives
a direct path to a peer behind a NAT that allocates a new public
port per destination, where STUN endpoints alone fail. Set
`TS_DISABLE_PORTMAPPER=1` to skip port mapping, or
`TS_DISABLE_UPNP=1` to skip only UPnP. Both sides then run Tailscale's
disco protocol and attempt UDP hole-punching. If
successful, traffic upgrades from the DERP relay to a direct
peer-to-peer path. If hole-punching fails, DERP continues as a
Expand Down
2 changes: 1 addition & 1 deletion build-tags.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ts_omit_ace,ts_omit_acme,ts_omit_advertiseexitnode,ts_omit_advertiseroutes,ts_omit_appconnectors,ts_omit_aws,ts_omit_bird,ts_omit_cachenetmap,ts_omit_captiveportal,ts_omit_capture,ts_omit_cliconndiag,ts_omit_clientmetrics,ts_omit_clientupdate,ts_omit_cloud,ts_omit_colorable,ts_omit_completion,ts_omit_completion_scripts,ts_omit_conn25,ts_omit_connreject,ts_omit_debug,ts_omit_debugeventbus,ts_omit_debugportmapper,ts_omit_desktop_sessions,ts_omit_dns,ts_omit_dnsresolvecache,ts_omit_doctor,ts_omit_drive,ts_omit_favorites,ts_omit_flashappliance,ts_omit_health,ts_omit_hujsonconf,ts_omit_identityfederation,ts_omit_ipnbus,ts_omit_iptables,ts_omit_kube,ts_omit_linkspeed,ts_omit_linuxdnsfight,ts_omit_listenrawdisco,ts_omit_logtail,ts_omit_netlog,ts_omit_networkmanager,ts_omit_oauthkey,ts_omit_osrouter,ts_omit_outboundproxy,ts_omit_peerapiclient,ts_omit_peerapiserver,ts_omit_portlist,ts_omit_portmapper,ts_omit_posture,ts_omit_qrcodes,ts_omit_relayserver,ts_omit_remoteconfig,ts_omit_resolved,ts_omit_routecheck,ts_omit_runtimemetrics,ts_omit_sdnotify,ts_omit_serve,ts_omit_serviceclientprefs,ts_omit_synology,ts_omit_syslog,ts_omit_syspolicy,ts_omit_systray,ts_omit_taildrop,ts_omit_tailnetlock,ts_omit_tap,ts_omit_tpm,ts_omit_tundevstats,ts_omit_unixsocketidentity,ts_omit_useexitnode,ts_omit_useproxy,ts_omit_usermetrics,ts_omit_useroutes,ts_omit_wakeonlan,ts_omit_webbrowser,ts_omit_webclient
ts_omit_ace,ts_omit_acme,ts_omit_advertiseexitnode,ts_omit_advertiseroutes,ts_omit_appconnectors,ts_omit_aws,ts_omit_bird,ts_omit_cachenetmap,ts_omit_captiveportal,ts_omit_capture,ts_omit_cliconndiag,ts_omit_clientmetrics,ts_omit_clientupdate,ts_omit_cloud,ts_omit_colorable,ts_omit_completion,ts_omit_completion_scripts,ts_omit_conn25,ts_omit_connreject,ts_omit_debug,ts_omit_debugeventbus,ts_omit_debugportmapper,ts_omit_desktop_sessions,ts_omit_dns,ts_omit_dnsresolvecache,ts_omit_doctor,ts_omit_drive,ts_omit_favorites,ts_omit_flashappliance,ts_omit_health,ts_omit_hujsonconf,ts_omit_identityfederation,ts_omit_ipnbus,ts_omit_iptables,ts_omit_kube,ts_omit_linkspeed,ts_omit_linuxdnsfight,ts_omit_listenrawdisco,ts_omit_logtail,ts_omit_netlog,ts_omit_networkmanager,ts_omit_oauthkey,ts_omit_osrouter,ts_omit_outboundproxy,ts_omit_peerapiclient,ts_omit_peerapiserver,ts_omit_portlist,ts_omit_posture,ts_omit_qrcodes,ts_omit_relayserver,ts_omit_remoteconfig,ts_omit_resolved,ts_omit_routecheck,ts_omit_runtimemetrics,ts_omit_sdnotify,ts_omit_serve,ts_omit_serviceclientprefs,ts_omit_synology,ts_omit_syslog,ts_omit_syspolicy,ts_omit_systray,ts_omit_taildrop,ts_omit_tailnetlock,ts_omit_tap,ts_omit_tpm,ts_omit_tundevstats,ts_omit_unixsocketidentity,ts_omit_useexitnode,ts_omit_useproxy,ts_omit_usermetrics,ts_omit_useroutes,ts_omit_wakeonlan,ts_omit_webbrowser,ts_omit_webclient
10 changes: 8 additions & 2 deletions internal/buildtags/buildtags.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,21 @@ var wasmKeep = []featuretags.FeatureTag{
// without them magicsock is DERP-only, and direct paths between peers
// are the whole point of tailcat. The wasm build omits both: a browser
// has no UDP sockets, so all of its traffic is relayed over DERP
// regardless. Note that featuretags.Requires pulls in ssh's c2n and
// dbus dependencies too.
// regardless. Native builds also keep portmapper (UPnP IGD, NAT-PMP,
// and PCP), so a peer behind a home router asks it for a public port
// mapping and advertises that endpoint; it is what lets a peer behind a
// NAT that allocates a new public port per destination get a direct
// path to it instead of staying on DERP. The wasm build omits it along
// with udptransport. Note that featuretags.Requires pulls in ssh's c2n
// and dbus dependencies too.
var releaseKeep = []featuretags.FeatureTag{
"netstack",
"ssh",
"gro",
"bakedroots",
"androidbin",
"nattraversal",
"portmapper",
}

// WasmTags returns the comma-joined -tags value for the wasm build,
Expand Down
4 changes: 2 additions & 2 deletions internal/buildtags/buildtags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ func TestTags(t *testing.T) {
t.Errorf("ReleaseTags contains %q; see the comment in buildtags.go", notWant)
}
}
for _, notWant := range []string{"ts_omit_netstack", "ts_omit_ssh", "ts_omit_gro", "ts_omit_c2n", "ts_omit_dbus", "ts_omit_udptransport", "ts_omit_nattraversal"} {
for _, notWant := range []string{"ts_omit_netstack", "ts_omit_ssh", "ts_omit_gro", "ts_omit_c2n", "ts_omit_dbus", "ts_omit_udptransport", "ts_omit_nattraversal", "ts_omit_portmapper"} {
if release[notWant] {
t.Errorf("ReleaseTags contains %q; that feature must stay linked in release builds", notWant)
}
}

wasm := tagSet(t, WasmTags())
for _, want := range []string{"ts_omit_ssh", "ts_omit_gro", "ts_omit_udptransport", "ts_omit_nattraversal"} {
for _, want := range []string{"ts_omit_ssh", "ts_omit_gro", "ts_omit_udptransport", "ts_omit_nattraversal", "ts_omit_portmapper"} {
if !wasm[want] {
t.Errorf("WasmTags missing %q", want)
}
Expand Down
49 changes: 49 additions & 0 deletions internal/buildtags/deps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package buildtags

import (
"os"
"os/exec"
"slices"
"strings"
"testing"
)

// listDeps returns the import paths that pkg links when built for
// goos/goarch with the given tags, as reported by go list.
func listDeps(t *testing.T, goos, goarch, tags, pkg string) []string {
t.Helper()
cmd := exec.Command("go", "list", "-deps", "-tags", tags, pkg)
cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)
out, err := cmd.Output()
if err != nil {
var stderr []byte
if ee, ok := err.(*exec.ExitError); ok {
stderr = ee.Stderr
}
t.Fatalf("go list -deps %s: %v\n%s", pkg, err, stderr)
}
return strings.Fields(string(out))
}

// TestReleaseLinksPortMapper checks the dependency graph of the binaries
// themselves, not the test binary: tests link the port mapper on their
// own through tailscale.com/feature/condregister, so only go list shows
// whether a release build of cmd/tailcat has it. magicsock silently runs
// without a port mapper when nothing registers one.
func TestReleaseLinksPortMapper(t *testing.T) {
if testing.Short() {
t.Skip("runs go list")
}
const portmapper = "tailscale.com/feature/portmapper"
native := listDeps(t, "linux", "amd64", ReleaseTags(), "github.com/tailscale/tailcat/cmd/tailcat")
if !slices.Contains(native, portmapper) {
t.Errorf("release build of cmd/tailcat does not link %s", portmapper)
}
wasm := listDeps(t, "js", "wasm", WasmTags(), "github.com/tailscale/tailcat/cmd/tailcat-web")
if slices.Contains(wasm, portmapper) {
t.Errorf("wasm build of cmd/tailcat-web links %s; a browser has no UDP sockets to map", portmapper)
}
}
20 changes: 20 additions & 0 deletions portmapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package tailcat

// The port mapper asks the local router for a public UDP port mapping
// (UPnP IGD, NAT-PMP, or PCP) and adds it to the endpoints magicsock
// advertises. Without it, a peer behind a NAT that allocates a new
// public port per destination (a "hard" NAT, common on hotel, office,
// and carrier networks) can reach a peer behind an ordinary home
// router only over DERP: neither side's STUN-learned endpoint is
// reachable from the other, so hole punching fails. With a mapping,
// the home side has an endpoint that accepts packets from any source,
// and the path goes direct.
//
// magicsock only creates a port mapper when this package registers
// one; it doesn't need to be linked by any other path. The import is
// a no-op under the ts_omit_portmapper build tag, which the wasm build
// sets: a browser has no UDP sockets, so it has nothing to map.
import _ "tailscale.com/feature/condregister/portmapper"