From ca6a46eee8605a94fef283f8bace0daefa8af5d8 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 3 Nov 2026 19:25:11 +0000 Subject: [PATCH 1/2] go.mod, build-tags.txt: bump tailscale.com Bump tailscale.com from a0e471a35b8f to 911d8f84a054 (201 commits). This brings in wireguard-go's optional hybrid post-quantum (ML-KEM-768) handshake and the wgcfg.PeerConfig.Hybrid field that enables it per peer (tailscale/tailscale#21844), which tailcat will use next. Regenerate the release omit tag lists (build-tags.txt and .goreleaser.yaml) for the new upstream exitnodehealth feature. Updates #133 Signed-off-by: Brad Fitzpatrick Change-Id: I037bf98ae3d5383bfc47deae074e1e2953489b0b --- .goreleaser.yaml | 2 +- build-tags.txt | 2 +- flake.nix | 2 +- flakehashes.json | 4 +- go.mod | 52 ++++++++++++------------ go.sum | 100 ++++++++++++++++++++++++----------------------- 6 files changed, 84 insertions(+), 78 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index bbf2cc537..a2a348bd2 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -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_exitnodehealth,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 goos: - linux - windows diff --git a/build-tags.txt b/build-tags.txt index 67ede1108..8d31da8ad 100644 --- a/build-tags.txt +++ b/build-tags.txt @@ -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_exitnodehealth,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 diff --git a/flake.nix b/flake.nix index b2fbc58ac..af62eb7fe 100644 --- a/flake.nix +++ b/flake.nix @@ -47,4 +47,4 @@ }; }); } -# nix-direnv cache busting line: sha256-E3vBmBO4K4REDf62ztSLzvko9kKGeLz5efCv2HGi/3M= +# nix-direnv cache busting line: sha256-QIVRQZ1SsWoRAzmY8oTCelk2EINTqkrUEcnyq2BEUms= diff --git a/flakehashes.json b/flakehashes.json index 2013c68c7..d813eb304 100644 --- a/flakehashes.json +++ b/flakehashes.json @@ -1,6 +1,6 @@ { "vendor": { - "goModSum": "sha256-XQZn3hG/2N0/tWNeQlOeMjln9p6g/lewLpplCd+/n8A=", - "sri": "sha256-E3vBmBO4K4REDf62ztSLzvko9kKGeLz5efCv2HGi/3M=" + "goModSum": "sha256-z79SW4OXVGw7okhwQm6cZh94gg4rzrNei2QAzc5YqN0=", + "sri": "sha256-QIVRQZ1SsWoRAzmY8oTCelk2EINTqkrUEcnyq2BEUms=" } } diff --git a/go.mod b/go.mod index e012681f0..6465de5b0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tailscale/tailcat -go 1.27.1 +go 1.27.3 require ( github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f @@ -11,18 +11,18 @@ require ( github.com/klauspost/compress v1.20.0 github.com/peterbourgon/ff/v4 v4.0.0-beta.1 github.com/pkg/sftp v1.13.11 - github.com/tailscale/gliderssh v0.3.4-0.20260716005906-1a0f895faf28 - github.com/tailscale/wireguard-go v0.0.0-20260924224943-0a83b87f7908 + github.com/tailscale/gliderssh v0.3.4-0.20261026144310-31f7cf365d3a + github.com/tailscale/wireguard-go v0.0.0-20261102205419-b884e294b6d8 github.com/toqueteos/webbrowser v1.2.1 github.com/u-root/u-root v0.16.0 go4.org/mem v0.0.0-20240501181205-ae6ca9944745 go4.org/netipx v0.0.0-20260823151212-3075585bcbeb - golang.org/x/crypto v0.57.0 + golang.org/x/crypto v0.58.0 golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba - golang.org/x/net v0.59.0 - golang.org/x/sys v0.48.0 + golang.org/x/net v0.62.0 + golang.org/x/sys v0.49.0 gvisor.dev/gvisor v0.0.0-20260915211658-a6f909f08a72 - tailscale.com v1.103.0-pre.0.20260929142145-a0e471a35b8f + tailscale.com v1.105.0-pre.0.20261103181955-911d8f84a054 ) require ( @@ -32,22 +32,24 @@ require ( github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect - github.com/aws/aws-sdk-go-v2 v1.46.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.33.3 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.20.3 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.2 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.9.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect + github.com/aws/aws-sdk-go-v2/config v1.33.9 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 // indirect github.com/aws/aws-sdk-go-v2/service/ssm v1.77.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.37.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.42.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.49.0 // indirect - github.com/aws/smithy-go v1.28.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 // indirect + github.com/aws/smithy-go v1.28.5 // indirect github.com/axiomhq/hyperloglog v0.2.6 // indirect + github.com/benbjohnson/immutable v0.4.3 // indirect + github.com/bradfitz/reco v0.0.0-20260929154613-b883fbd17e3f // indirect github.com/chromedp/sysutil v1.1.0 // indirect github.com/coder/websocket v1.8.15 // indirect github.com/coreos/go-iptables v0.8.0 // indirect @@ -100,11 +102,11 @@ require ( github.com/x448/float16 v0.8.4 // indirect golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f // indirect golang.org/x/mod v0.41.0 // indirect - golang.org/x/sync v0.23.0 // indirect - golang.org/x/term v0.46.0 // indirect - golang.org/x/text v0.42.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect golang.org/x/time v0.16.0 // indirect - golang.org/x/tools v0.50.0 // indirect + golang.org/x/tools v0.51.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v1.0.1 // indirect honnef.co/go/tools v0.8.1 // indirect diff --git a/go.sum b/go.sum index 285466f47..c5d82db0e 100644 --- a/go.sum +++ b/go.sum @@ -12,38 +12,42 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/aws/aws-sdk-go-v2 v1.46.0 h1:1kt7m/EKcEHt5mlyyxx9cSlMddRPIKbjb6DIQsu4HPk= -github.com/aws/aws-sdk-go-v2 v1.46.0/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU= -github.com/aws/aws-sdk-go-v2/config v1.33.3 h1:h090b3O5S17bF87/0ysHZuIT/7DCb4EBRFQX2PMVPCw= -github.com/aws/aws-sdk-go-v2/config v1.33.3/go.mod h1:YYDB1kTejxbfAbEVUqgCtkVp26xvNCHev9cLKABMGAk= -github.com/aws/aws-sdk-go-v2/credentials v1.20.3 h1:tToOYM/LXev4NpfWlIYGDvBvjHmJ3HXpRU9ppl+pM6k= -github.com/aws/aws-sdk-go-v2/credentials v1.20.3/go.mod h1:wfGneWyncO7p67wqXV2IQhPk14JqIc25woKlaArT3WI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.2 h1:Ldv7RPHs7qwwTscRjAl3YBud32f3BvdAGRmSvAx5L38= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.2/go.mod h1:XyK6UV8xbo66ysVqLd2783C09pBYHOm8aKTRV5DVJ30= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.2 h1:q/PSLGuRWCChWg+dLnb9dWOnrCxJtnboXbBtFoqqRrI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.2/go.mod h1:TD1jvU2LvXkJexct5vBqcd8QlNXh5EmRUeL/Z32p0n4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.2 h1:6fl86IPqKEXoySqiOWdfgbEp9OVbn44zTfEICNEBDhY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.2/go.mod h1:63HDfhFkdzBpI8WGXTSKUHPKS6mqldj4u3LJW7RZtSU= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.2 h1:XMgIRS+uW9F3yFKnXGRrI9pkHi99CXTmoz2kz2/TGBA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.2/go.mod h1:vorxDzK+n3jiv9a5ST/LG0Eu9cSv1CRdKTpG6pDMs+M= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.2 h1:ZtHYnumr6QyxhzEzNZwzQTFJEXOswrZqTTkRxthwvr4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.2/go.mod h1:a1NXrYpBd311gBzn1UI5UzJyyvXktM4xNh/ydPiPpqY= -github.com/aws/aws-sdk-go-v2/service/signin v1.9.0 h1:c3k+k/CS4L+sAIH6fxikL+g5g2LpeNczaoyjjw1iMKI= -github.com/aws/aws-sdk-go-v2/service/signin v1.9.0/go.mod h1:AGIoQg99fBrOIQnF78TLx4lj18mc4gZ0hJx1UaLIFM4= +github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk= +github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A= +github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0= +github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0= +github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M= github.com/aws/aws-sdk-go-v2/service/ssm v1.77.0 h1:JqKSBJlS68F4nxoPLwD7tB3lJ3gWEK9p+7eOiBE/lsM= github.com/aws/aws-sdk-go-v2/service/ssm v1.77.0/go.mod h1:5TMMHmQGjCODMeIopSNfxkan11jG0SmFpyjcMvCLzHA= -github.com/aws/aws-sdk-go-v2/service/sso v1.37.0 h1:+rqBaOq7jzInjY8M12hr+zEe85JpRll9BjMx38r33Ok= -github.com/aws/aws-sdk-go-v2/service/sso v1.37.0/go.mod h1:XFlVwUsw3sYh8Hw37umYVJnrcWrwXWRxbNw/JY0bblw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.42.0 h1:hzM3GslEAOBcLn3DHH6ENToFi+vXP+n02W+x6zejAIM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.42.0/go.mod h1:588e7skMkYIYkSUseT8E3WKFfbAfrA1bj3Zf+qxJtJY= -github.com/aws/aws-sdk-go-v2/service/sts v1.49.0 h1:N7Ey8obY3uSui+cxl0OUzFlFmkxSucoJnrniFhw+cLc= -github.com/aws/aws-sdk-go-v2/service/sts v1.49.0/go.mod h1:zMBwjSf4Pt8a1OHYiZ5rPD0PJRK1kQrUaxhS/Dbld8E= -github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ= -github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM= +github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k= +github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/axiomhq/hyperloglog v0.2.6 h1:sRhvvF3RIXWQgAXaTphLp4yJiX4S0IN3MWTaAgZoRJw= github.com/axiomhq/hyperloglog v0.2.6/go.mod h1:YjX/dQqCR/7QYX0g8mu8UZAjpIenz1FKM71UEsjFoTo= +github.com/benbjohnson/immutable v0.4.3 h1:GYHcksoJ9K6HyAUpGxwZURrbTkXA0Dh4otXGqbhdrjA= +github.com/benbjohnson/immutable v0.4.3/go.mod h1:qJIKKSmdqz1tVzNtst1DZzvaqOU1onk1rc03IeM3Owk= +github.com/bradfitz/reco v0.0.0-20260929154613-b883fbd17e3f h1:jdXGSp5Ge5/kzikuxGOw9+6HSStUjkNBJZjGNtpItek= +github.com/bradfitz/reco v0.0.0-20260929154613-b883fbd17e3f/go.mod h1:oiCPXxBoDKSkb/RGEZwAZk8y/fwDlQ1vYlBbNRxPysw= github.com/bramvdbogaerde/go-scp v1.6.1 h1:HE63uMkmMEB62JwqdRrVqRTBi7H/L/rkTp8TPC7uffY= github.com/bramvdbogaerde/go-scp v1.6.1/go.mod h1:Lqx75BOgAvQoeBbPuPN/MfU3jf5tlfZVmUqpPbSZkZc= github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f h1:0Z1zcSLEmnj2c2CmJYBqewtS6pxhB39bNWUSEUAWjgk= @@ -187,8 +191,8 @@ github.com/studio-b12/gowebdav v0.13.0 h1:OcwSg6IQHOFNdYHn3bPOHwSE8looG8N56Y5xTT github.com/studio-b12/gowebdav v0.13.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d h1:JcGKBZAL7ePLwOhUdN8qGQZlP5GueEiIZwY7R62pejE= github.com/tailscale/certstore v0.1.1-0.20260409135935-3638fb84b77d/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= -github.com/tailscale/gliderssh v0.3.4-0.20260716005906-1a0f895faf28 h1:Azz5ILxxVsHN/KjIu3wkJPAmmtiijucZw4Ax5Ye8n+s= -github.com/tailscale/gliderssh v0.3.4-0.20260716005906-1a0f895faf28/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw= +github.com/tailscale/gliderssh v0.3.4-0.20261026144310-31f7cf365d3a h1:LQfS++EBVN2Bk9/FbFjJmdP/CsebU0OupOX58tO4p04= +github.com/tailscale/gliderssh v0.3.4-0.20261026144310-31f7cf365d3a/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= github.com/tailscale/golang-x-crypto v0.0.0-20260720160339-c86ca1284b96 h1:m9EAsCu/afUkg9YkknXaRqMPl/omkUNRol5yB7hbG0Y= @@ -203,8 +207,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20260917222731-e0ed2d0d0fea h1:a github.com/tailscale/web-client-prebuilt v0.0.0-20260917222731-e0ed2d0d0fea/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= -github.com/tailscale/wireguard-go v0.0.0-20260924224943-0a83b87f7908 h1:+nNMmeMU/JoRY1Y4iWrCWYHMOMQhFpx9NKBAb+DH7So= -github.com/tailscale/wireguard-go v0.0.0-20260924224943-0a83b87f7908/go.mod h1:rUelGmuK4UnSJYM5gl5Mknp6YbwwcL8+VAPMhNYe+jg= +github.com/tailscale/wireguard-go v0.0.0-20261102205419-b884e294b6d8 h1:bzkevp32ugVKkbzw4ctS7Iou+FUsIPGSbojnHzbATV4= +github.com/tailscale/wireguard-go v0.0.0-20261102205419-b884e294b6d8/go.mod h1:rUelGmuK4UnSJYM5gl5Mknp6YbwwcL8+VAPMhNYe+jg= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.3.1 h1:CwRjEGrKdbi5CvZ4ID+iyVhgyfatxFoizjPhzez9Io4= @@ -230,8 +234,8 @@ go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4 go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20260823151212-3075585bcbeb h1:XBM4hvfwGAttkkiTIFfeigdfcL1xIfdKXqFdgiHGtDs= go4.org/netipx v0.0.0-20260823151212-3075585bcbeb/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f h1:+lI8cDJ4uceLipg2f1ODay7fEuLkk0BIHXd6PB8icxo= @@ -240,26 +244,26 @@ golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= -golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= -golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= -golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= -golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= -golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= @@ -280,5 +284,5 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= software.sslmate.com/src/go-pkcs12 v0.7.3 h1:JBQD3FDqYjTeyDAeZQklj2ar88ykBLtALloPJHyAauU= software.sslmate.com/src/go-pkcs12 v0.7.3/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.103.0-pre.0.20260929142145-a0e471a35b8f h1:a9y2RcAkvFGRsyloa8maBtoKmCc+TwwJpxbaWjrrt0I= -tailscale.com v1.103.0-pre.0.20260929142145-a0e471a35b8f/go.mod h1:axJuLvuCmXen5u0tyk8KZBZsjW4Wn9AZ+LbzezqsLdk= +tailscale.com v1.105.0-pre.0.20261103181955-911d8f84a054 h1:NaOUGdGGhA7XuFpQ/uikTf8uT6kq6FSnybbGDQJwvqk= +tailscale.com v1.105.0-pre.0.20261103181955-911d8f84a054/go.mod h1:anhQD2+ycfyvX7Q0atr6+QOuNWqzGxGPn4/r3fbM9Pw= From 3c95617776904c823cdddc9252d2d68e38496ed7 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 3 Nov 2026 19:33:07 +0000 Subject: [PATCH 2/2] tailcat, cmd/tailcat: add experimental --hybrid post-quantum handshake Add an experimental "tailcat serve --hybrid" flag (Server.Hybrid in the Go API) that makes the server use wireguard-go's hybrid post-quantum (ML-KEM-768) WireGuard handshake. Unlike the pre-shared key's post-quantum protection, this doesn't depend on the tailcat address staying secret, so it also protects servers whose addresses are published, as in DNS TXT records. Both peers must agree on the handshake type, with no fallback, so hybrid addresses say so with a new CBOR bool ("y", omitted when false) and clients set the matching wgcfg.PeerConfig.Hybrid. With the hybrid handshake, the pre-shared key only needs to keep out strangers who know the server's public key, such as a DERP operator, so hybrid addresses carry just 128 bits of it (zero-padded to the WireGuard key size), making them shorter. 128 bits rather than fewer because any client the server admits gets a handshake response it can test guesses against offline. The shorter key also makes clients from before this change fail early: they require a 32-byte key in the "q" field, so they reject hybrid addresses at parse time instead of retrying a classic handshake that can never succeed. (Renaming the required server key field wouldn't have: old clients don't check that it's present.) Hybrid addresses made with --psk=false have no such early failure. Updates #133 Signed-off-by: Brad Fitzpatrick Change-Id: I496cab05fe004f041131dbe9b85381e06baea48e --- CHANGELOG.md | 7 ++ README.md | 16 +++- cmd/tailcat/cli_test.go | 4 +- cmd/tailcat/serve_test.go | 31 ++++++++ cmd/tailcat/tailcat.go | 8 +- tailcat.go | 63 ++++++++++++++- tailcat_test.go | 163 ++++++++++++++++++++++++++++++++++++++ wire.go | 16 +++- wire_test.go | 1 + 9 files changed, 301 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b87d718e3..d86b498bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- New experimental `tailcat serve --hybrid` flag (and `Server.Hybrid` + in the Go API) uses wireguard-go's hybrid post-quantum (ML-KEM-768) + WireGuard handshake, whose post-quantum protection doesn't depend on + the tailcat address staying secret. Hybrid addresses say so, so + clients use the hybrid handshake too, and carry a shorter 128-bit + pre-shared key, so they're shorter. Older clients reject hybrid + addresses. ([#133](https://github.com/tailscale/tailcat/issues/133)) - **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 diff --git a/README.md b/README.md index 3082f93ef..f86831f23 100644 --- a/README.md +++ b/README.md @@ -434,6 +434,16 @@ compatibility with tailcat clients v0.5.0 and earlier, `--psk=false` on `serve` or `genkey` produces shorter addresses, but removes post-quantum protection and protection from public DERP operators that observe the peers' public keys. +The pre-shared key's post-quantum protection only lasts while the address +stays secret. The experimental `tailcat serve --hybrid` flag instead uses +a hybrid post-quantum (ML-KEM-768) WireGuard handshake, which protects +recorded traffic even when the address is public, such as in a DNS TXT +record. A hybrid address still contains a pre-shared key by default, to +keep out DERP operators, but a shorter 128-bit one, so the address is +shorter too. +Clients need a tailcat version with hybrid support; older clients reject +hybrid addresses. + ```sh $ tailcat genkey --key=default --region=nyc # prints the tailcat address; key saved to ~/.config/tailcat/keys/default.private.json @@ -706,8 +716,10 @@ followed by base64-encoded [CBOR](https://cbor.io/) containing: - A separate path-discovery public key (Curve25519, 32 bytes) - By default, an independent WireGuard pre-shared key (256 random bits), which prevents a DERP operator that observes the peers' public keys from - joining the tunnel and provides post-quantum protection against recorded - traffic + joining the tunnel and, while the address stays secret, provides + post-quantum protection against recorded traffic +- With `serve --hybrid` (experimental), a flag saying to use the hybrid + post-quantum WireGuard handshake, and only 128 bits of pre-shared key - DERP info. Either: 1. a small integer referencing one of the default [Tailscale-run tailcat servers](https://tailcat.dev/derpmap.json), or 2. full DERP server metadata, to either use a custom DERP server, or to avoid the client needing a potential round-trip to fetch the latest DERP map (the `tailcat serve --full-address` flag and the `tailcat resolve` subcommand produce this form) diff --git a/cmd/tailcat/cli_test.go b/cmd/tailcat/cli_test.go index ebdb8b0f0..34643daff 100644 --- a/cmd/tailcat/cli_test.go +++ b/cmd/tailcat/cli_test.go @@ -77,7 +77,7 @@ func TestHelpListsCommandTree(t *testing.T) { // Server-only flags live on the serve subcommand, not the root. // (The long help prose may still mention them; only reject them // as rendered flag entries.) - for _, notWant := range []string{"\n --allow", "\n --full-address", "\n --psk", "\n --ssh-authorized-keys"} { + for _, notWant := range []string{"\n --allow", "\n --full-address", "\n --psk", "\n --hybrid", "\n --ssh-authorized-keys"} { if strings.Contains(help, notWant) { t.Errorf("root help lists server-only flag %q", strings.TrimSpace(notWant)) } @@ -99,7 +99,7 @@ func TestServeHelpListsServerFlags(t *testing.T) { t.Fatal("no serve subcommand") } help := ffhelp.Command(serve).String() - for _, want := range []string{"--allow", "--full-address", "--key", "--psk", "--ssh-authorized-keys"} { + for _, want := range []string{"--allow", "--full-address", "--hybrid", "--key", "--psk", "--ssh-authorized-keys"} { if !strings.Contains(help, want) { t.Errorf("serve help is missing %q", want) } diff --git a/cmd/tailcat/serve_test.go b/cmd/tailcat/serve_test.go index 8bf3c53ff..951d289a0 100644 --- a/cmd/tailcat/serve_test.go +++ b/cmd/tailcat/serve_test.go @@ -92,6 +92,37 @@ func TestServeWithoutPSK(t *testing.T) { } } +func TestServeHybrid(t *testing.T) { + for _, psk := range []bool{true, false} { + t.Run(fmt.Sprintf("psk=%v", psk), func(t *testing.T) { + t.Parallel() + e := newTestEnv(t) + port := startEchoListener(t) + _, addr, serverStderr := e.startServer("serve", "--hybrid", fmt.Sprintf("--psk=%v", psk), strconv.Itoa(int(port))) + + ci, err := tailcat.ParseAddr(tailcat.Addr(addr)) + if err != nil { + t.Fatal(err) + } + if !ci.Hybrid { + t.Fatal("serve --hybrid produced a non-hybrid address") + } + if got := !ci.PresharedKey.IsZero(); got != psk { + t.Fatalf("address has PSK = %v; want %v", got, psk) + } + + const payload = "echo over a hybrid handshake" + got, err := runClient(t, e.cmd("--key=new", "--derpmap-url="+e.derpMapURL, addr, strconv.Itoa(int(port))), serverStderr, payload) + if err != nil { + t.Fatalf("client to hybrid server: %v", err) + } + if got != payload { + t.Errorf("server echoed %q; want %q", got, payload) + } + }) + } +} + func TestServeRemembersSavedKeyWithoutPSK(t *testing.T) { t.Parallel() e := newTestEnv(t) diff --git a/cmd/tailcat/tailcat.go b/cmd/tailcat/tailcat.go index 31d0ab4ec..8d3fa4902 100644 --- a/cmd/tailcat/tailcat.go +++ b/cmd/tailcat/tailcat.go @@ -58,6 +58,7 @@ var ( flagFiles *string flagSSHAuthorizedKeys *string flagPSK *bool + flagHybrid *bool flagVerbose *bool flagFullAddress *bool flagJSON *bool @@ -105,6 +106,7 @@ func newRootCommand() *ff.Command { flagFiles = serveFS.StringLong("files", "", "directory to serve to SFTP clients (scp, sftp) with the 'files' service, with an optional :ro (read-only, the default), :rw (read-write), :wo (flat write-only drop box), or :wo+ (recursive write-only drop box) suffix. If empty, the current directory is served read-only. Giving --files implies the 'files' service.") flagSSHAuthorizedKeys = serveFS.StringLong("ssh-authorized-keys", "", "comma-separated SSH public key sources for the 'ssh' service: authorized_keys file paths, literal OpenSSH public key lines, or names like 'alice@github' (fetched from https://github.com/alice.keys). All sources are loaded and validated at startup.") flagPSK = serveFS.BoolLongDefault("psk", true, "include a WireGuard pre-shared key in the tailcat address (recommended). Set false only for shorter addresses and compatibility with tailcat clients v0.5.0 and earlier; this weakens security.") + flagHybrid = serveFS.BoolLong("hybrid", "EXPERIMENTAL: use the hybrid post-quantum (ML-KEM-768) WireGuard handshake, which doesn't depend on the tailcat address staying secret for its post-quantum protection. The address is shorter, with a 128-bit pre-shared key, but only works with clients that support the hybrid handshake; older clients reject it.") recvFS := ff.NewFlagSet("recv").SetParent(serveFS) flagRecvAcceptDirs := recvFS.BoolLong("accept-dirs", "accept directory trees (tailcat cp -r), keeping requested file names when available. The trade-off: senders can then make and stat directories and learn whether some names already exist in the drop box. The default flat mode reveals nothing about existing files, but accepts only single files, each saved under a server-chosen unique name.") @@ -1362,6 +1364,9 @@ func server(logf logger.Logf, serveSpec string, execArgs []string) { ci.PresharedKey = tailcat.PresharedKey{} } psk := ci.PresharedKey + if *flagHybrid { + psk = psk.ForHybrid() + } if reg == nil { // A key created with custom DERP hostnames (genkey --region=) // has pre-populated regions with no DERP map ID to reference, so its @@ -1393,9 +1398,10 @@ func server(logf logger.Logf, serveSpec string, execArgs []string) { } ci.ServerPublic = tailcat.NodePublic{NodePublic: priv.Public()} ci.ServerDiscoPublic = tailcat.DiscoPublicForNode(priv) + ci.Hybrid = *flagHybrid connStr := ci.Addr() - s := &tailcat.Server{Key: priv, PresharedKey: psk, DisablePresharedKey: !usePSK, Logf: logf, Region: reg} + s := &tailcat.Server{Key: priv, PresharedKey: psk, DisablePresharedKey: !usePSK, Hybrid: *flagHybrid, Logf: logf, Region: reg} sshServices := services.Contains("ssh") || services.Contains("no-auth-ssh") || services.Contains("files") if sshServices && !tailcat.SupportsSSHServer() { log.Fatalf("Tailscale SSH server not supported on %v", runtime.GOOS) diff --git a/tailcat.go b/tailcat.go index 7585d3ace..65bafa7b4 100644 --- a/tailcat.go +++ b/tailcat.go @@ -168,6 +168,14 @@ type ConnInfo struct { // layer for compatibility with old clients. PresharedKey PresharedKey + // Hybrid is whether the server uses the experimental hybrid + // post-quantum (ML-KEM-768) WireGuard handshake, which clients must + // then use too: there is no fallback between handshake types. Its + // post-quantum protection doesn't depend on the address staying + // secret, so a hybrid address carries a shorter, 128-bit pre-shared + // key (see [PresharedKey.ForHybrid]), if any. + Hybrid bool `json:",omitempty"` + // Region, if non-empty, lists the regions of a DERPMap. // Either Region or RegionID must be set. If Region is set // the client can avoid doing a lookup to discover the DERP map @@ -201,6 +209,19 @@ type DiscoPublic struct { const presharedKeyLen = device.NoisePresharedKeySize +// hybridPresharedKeyLen is the length of the pre-shared key in an address +// using the hybrid handshake. The key there only keeps strangers who know +// the server's public key, such as a DERP operator, out of the tunnel, so +// it doesn't need 256 bits. It still needs enough that it can't be guessed +// offline: any client the server admits gets a handshake response, from +// which it can test guesses at the key. +// +// Clients from before the hybrid handshake reject an address with a +// pre-shared key of this length, so they fail early with a parse error +// rather than retrying a handshake that can never succeed. (A hybrid +// address without a pre-shared key gets no such early failure.) +const hybridPresharedKeyLen = 16 + // PresharedKey is an optional 256-bit WireGuard pre-shared key. Tailcat // addresses generated by current servers always contain a non-zero key. It is // a named form of [device.NoisePresharedKey] so it can define the CBOR and JSON @@ -218,6 +239,14 @@ func NewPresharedKey() (ret PresharedKey) { return ret } +// ForHybrid returns p truncated to the 128-bit pre-shared key used with +// the hybrid handshake, zero-padded to the WireGuard key size. A hybrid +// address encodes only the first 128 bits, and both peers use this form. +func (p PresharedKey) ForHybrid() PresharedKey { + clear(p[hybridPresharedKeyLen:]) + return p +} + // IsZero reports whether p is the zero value. func (p PresharedKey) IsZero() bool { var zero PresharedKey @@ -342,6 +371,7 @@ type locoBackend struct { serverPub key.NodePublic // non-zero if we're a client (server's public key) serverDiscoPub key.DiscoPublic // non-zero if we're a client (server's disco key) presharedKey PresharedKey + hybrid bool // whether to use the hybrid post-quantum WireGuard handshake isServer bool // discoPublic returns the node's disco public key, memoized to @@ -426,6 +456,15 @@ type Server struct { // addresses compatible with tailcat clients v0.5.0 and earlier. DisablePresharedKey bool + // Hybrid, if true, makes the server use the experimental hybrid + // post-quantum (ML-KEM-768) WireGuard handshake with all clients. + // Clients must support it too; older ones reject the server's + // address. The server uses only the first 128 bits of PresharedKey + // (see [PresharedKey.ForHybrid]), giving shorter addresses. + // + // It must be set before calling Start. + Hybrid bool + // Logf is the logger used for debug messages. // If nil, log.Printf is used. Logf logger.Logf @@ -624,6 +663,9 @@ func (s *Server) startLocked(ctx context.Context) error { } else if psk.IsZero() { psk = NewPresharedKey() } + if s.Hybrid { + psk = psk.ForHybrid() + } reg := s.Region if reg == nil { ci := &ConnInfo{RegionID: cmp.Or(s.RegionID, -1)} @@ -644,6 +686,7 @@ func (s *Server) startLocked(ctx context.Context) error { } lb := newLocoBackend(priv, psk) + lb.hybrid = s.Hybrid lb.logf = logf lb.dm = &tailcfg.DERPMap{} mak.Set(&lb.dm.Regions, reg.RegionID, reg) @@ -1049,6 +1092,7 @@ func (lb *locoBackend) tailcatAddr() Addr { ci.ServerPublic = NodePublic{lb.pub} ci.ServerDiscoPublic = DiscoPublic{lb.discoPublic()} ci.PresharedKey = lb.presharedKey + ci.Hybrid = lb.hybrid for _, r := range lb.dm.Regions { ci.Region = append(ci.Region, r) } @@ -1075,8 +1119,13 @@ func (ci *ConnInfo) Addr() Addr { if !ci.ServerDiscoPublic.IsZero() { w.ServerDiscoPublic = &ci.ServerDiscoPublic } + w.Hybrid = ci.Hybrid if !ci.PresharedKey.IsZero() { - w.PresharedKey = &ci.PresharedKey + if ci.Hybrid { + w.PresharedKey = ci.PresharedKey[:hybridPresharedKeyLen] + } else { + w.PresharedKey = ci.PresharedKey[:] + } } for _, r := range ci.Region { wr := wireRegionOf(r) @@ -1175,8 +1224,16 @@ func ParseAddr(addr Addr) (ConnInfo, error) { if w.ServerDiscoPublic != nil { ci.ServerDiscoPublic = *w.ServerDiscoPublic } + ci.Hybrid = w.Hybrid + wantPSKLen := presharedKeyLen + if w.Hybrid { + wantPSKLen = hybridPresharedKeyLen + } if w.PresharedKey != nil { - ci.PresharedKey = *w.PresharedKey + if len(w.PresharedKey) != wantPSKLen { + return zero, fmt.Errorf("invalid tailcat address: WireGuard pre-shared key length %d, want %d", len(w.PresharedKey), wantPSKLen) + } + copy(ci.PresharedKey[:], w.PresharedKey) } for i, wr := range w.Region { // CBOR nulls decode to nil pointers, and addresses come from @@ -1457,6 +1514,7 @@ func (b *locoBackend) peerConfig(k key.NodePublic) (_ wgcfg.PeerConfig, ok bool) return wgcfg.PeerConfig{ AllowedIPs: allowedIPs, PresharedKey: device.NoisePresharedKey(b.presharedKey), + Hybrid: b.hybrid, } } if !b.serverPub.IsZero() { @@ -2007,6 +2065,7 @@ func (c *Client) initLocked() error { lb.dm = &tailcfg.DERPMap{} lb.serverPub = ci.ServerPublic.NodePublic lb.serverDiscoPub = ci.ServerDiscoPublic.DiscoPublic + lb.hybrid = ci.Hybrid sys := &lb.sys bus := eventbus.New() diff --git a/tailcat_test.go b/tailcat_test.go index 6b7e7b285..a3b76567f 100644 --- a/tailcat_test.go +++ b/tailcat_test.go @@ -40,6 +40,7 @@ import ( "tailscale.com/tstest/integration" "tailscale.com/types/key" "tailscale.com/types/logger" + "tailscale.com/util/clientmetric" "tailscale.com/wgengine/filter" ) @@ -1517,6 +1518,168 @@ func TestPeerConfigIncludesPresharedKey(t *testing.T) { } } +func TestAddrHybrid(t *testing.T) { + priv := NewPrivateKey() + priv.Public.RegionID = 10 + classic := priv.Public + hybrid := priv.Public + hybrid.Hybrid = true + + got, err := ParseAddr(hybrid.Addr()) + if err != nil { + t.Fatal(err) + } + if !got.Hybrid { + t.Error("Hybrid lost in address round trip") + } + if want := classic.PresharedKey.ForHybrid(); !got.PresharedKey.Equal(want) { + t.Errorf("hybrid pre-shared key = %x; want %x", got.PresharedKey, want) + } + if got, err := ParseAddr(classic.Addr()); err != nil || got.Hybrid { + t.Errorf("classic address parsed as Hybrid=%v, %v; want false, nil", got.Hybrid, err) + } + + noPSK := hybrid + noPSK.PresharedKey = PresharedKey{} + if got, err := ParseAddr(noPSK.Addr()); err != nil || !got.Hybrid || !got.PresharedKey.IsZero() { + t.Errorf("hybrid address without PSK parsed as Hybrid=%v, PSK %x, %v; want true, zero, nil", got.Hybrid, got.PresharedKey, err) + } + + // The CBOR bool costs 2 bytes; the shorter key saves 16. + if got, want := len(hybrid.Addr()), len(classic.Addr())-18; got > want { + t.Errorf("hybrid address length = %d; want at most %d", got, want) + } +} + +// TestOldClientsRejectHybridAddr verifies that tailcat clients from before +// the hybrid handshake, which decoded the pre-shared key straight into a +// [PresharedKey], fail to parse a hybrid address instead of trying a +// classic handshake the server never answers. +func TestOldClientsRejectHybridAddr(t *testing.T) { + type oldWireConnInfo struct { + ServerPublic NodePublic `cbor:"p"` + ServerDiscoPublic *DiscoPublic `cbor:"k,omitempty"` + PresharedKey *PresharedKey `cbor:"q,omitempty"` + RegionID int64 `cbor:"i,omitempty"` + } + decodeOld := func(a Addr) error { + x, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(string(a), "tc")) + if err != nil { + t.Fatal(err) + } + return cbor.Unmarshal(x, new(oldWireConnInfo)) + } + + priv := NewPrivateKey() + priv.Public.RegionID = 10 + if err := decodeOld(priv.Public.Addr()); err != nil { + t.Fatalf("old decoder rejected classic address: %v", err) + } + priv.Public.Hybrid = true + if err := decodeOld(priv.Public.Addr()); err == nil { + t.Fatal("old decoder accepted hybrid address") + } +} + +func TestParseAddrMalformedHybrid(t *testing.T) { + for name, fields := range map[string]map[string]any{ + "long-psk": {"y": true, "q": make([]byte, presharedKeyLen)}, + "classic-short-psk": {"q": make([]byte, hybridPresharedKeyLen)}, + } { + t.Run(name, func(t *testing.T) { + fields["p"] = key.NewNode().Public().AppendTo(nil) + raw, err := cbor.Marshal(fields) + if err != nil { + t.Fatal(err) + } + addr := Addr("tc" + base64.RawURLEncoding.EncodeToString(raw)) + if _, err := ParseAddr(addr); err == nil { + t.Fatal("ParseAddr unexpectedly accepted malformed hybrid address") + } + }) + } +} + +func TestPeerConfigHybrid(t *testing.T) { + server := key.NewNode().Public() + b := &locoBackend{serverPub: server, presharedKey: NewPresharedKey(), hybrid: true} + conf, ok := b.peerConfig(server) + if !ok { + t.Fatal("peerConfig did not find server peer") + } + if !conf.Hybrid { + t.Fatal("peerConfig Hybrid = false; want true") + } +} + +// TestHybridTunnel verifies that a hybrid server and client complete a +// hybrid WireGuard handshake, and that a client using the classic +// handshake with the same keys cannot reach the server. +func TestHybridTunnel(t *testing.T) { + t.Parallel() + + dm := integration.RunDERPAndSTUN(t, mkLogger(t, "derpstun"), "127.0.0.1") + s := &Server{Logf: mkLogger(t, "server"), Region: dm.Regions[1], Hybrid: true} + t.Cleanup(func() { s.Close() }) + s.OnTCP = func(port uint16) (handler func(net.Conn)) { + return func(c net.Conn) { + io.WriteString(c, "hybrid hello") + c.Close() + } + } + if err := s.Start(); err != nil { + t.Fatal(err) + } + ci, err := ParseAddr(s.TailcatAddr()) + if err != nil { + t.Fatal(err) + } + if !ci.Hybrid { + t.Fatal("hybrid server's address isn't hybrid") + } + + classicInfo := ci + classicInfo.Hybrid = false + classic := &Client{Server: classicInfo.Addr(), Logf: mkLogger(t, "classic-client")} + PingForTest(t, s, classic) // the pre-WireGuard discovery handshake still works + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + if conn, err := classic.DialTCPPort(ctx, 80); err == nil { + conn.Close() + t.Fatal("classic-handshake client established a tunnel with a hybrid server") + } + cancel() + classic.Close() + + before := hybridResponderHandshakes() + c := &Client{Server: s.TailcatAddr(), Logf: mkLogger(t, "client")} + t.Cleanup(func() { c.Close() }) + PingForTest(t, s, c) + ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + conn, err := c.DialTCPPort(ctx, 80) + if err != nil { + t.Fatal(err) + } + all, err := io.ReadAll(conn) + if string(all) != "hybrid hello" { + t.Fatalf("read %q, %v; want %q", all, err, "hybrid hello") + } + if after := hybridResponderHandshakes(); after <= before { + t.Errorf("hybrid responder handshakes = %d, unchanged from before the dial", after) + } +} + +// hybridResponderHandshakes returns wireguard-go's process-wide count of +// completed hybrid handshakes in which it was the responder. +func hybridResponderHandshakes() int64 { + for _, m := range clientmetric.Metrics() { + if m.Name() == "wireguard_handshake_hybrid_responder_completed" { + return m.Value() + } + } + return 0 +} + // TestFetchDERPMapMemoryCache verifies the default in-memory DERP map // cache: a second fetch of the same URL within the freshness window // makes no network request. diff --git a/wire.go b/wire.go index dbcb67f5e..71edab52a 100644 --- a/wire.go +++ b/wire.go @@ -4,6 +4,8 @@ package tailcat import ( + "encoding/hex" + "tailscale.com/tailcfg" ) @@ -26,11 +28,23 @@ import ( type wireConnInfo struct { ServerPublic NodePublic `cbor:"p" json:"ServerPublic"` ServerDiscoPublic *DiscoPublic `cbor:"k,omitempty" json:"ServerDiscoPublic,omitempty"` - PresharedKey *PresharedKey `cbor:"q,omitempty" json:"PresharedKey,omitempty"` + PresharedKey wirePSK `cbor:"q,omitempty" json:"PresharedKey,omitempty"` + Hybrid bool `cbor:"y,omitempty" json:"Hybrid,omitempty"` Region []*wireRegion `cbor:"r,omitempty" json:"Region,omitempty"` RegionID int64 `cbor:"i,omitempty" json:"RegionID,omitempty"` } +// wirePSK is the wire form of a [PresharedKey]: all 32 bytes normally, or +// just the first 16 when the address uses the hybrid handshake (see +// hybridPresharedKeyLen). +type wirePSK []byte + +// MarshalText implements encoding.TextMarshaler for JSON display, in the +// same "psk:" hex form as [PresharedKey.MarshalText]. +func (p wirePSK) MarshalText() ([]byte, error) { + return append([]byte("psk:"), hex.EncodeToString(p)...), nil +} + // wireRegion is the wire form of [tailcfg.DERPRegion]. type wireRegion struct { RegionID int64 `cbor:"i,omitempty" json:"RegionID,omitempty"` diff --git a/wire_test.go b/wire_test.go index 2be4e01e2..e540f63c2 100644 --- a/wire_test.go +++ b/wire_test.go @@ -23,6 +23,7 @@ var wireFieldNames = map[string]string{ "p": "ServerPublic", "k": "ServerDiscoPublic", "q": "PresharedKey", + "y": "Hybrid", "r": "Region", "i": "RegionID", "c": "RegionCode",