swift, go.mod: adding localAPI support via SOCK5

updates tailscale/tailscale#13937

This adds localAPI support into TailscaleKit.  LocalAPI can now be queried
via the SOCK5 proxy on both MacOS and iOS.   This also fixes SOCKS5
support for iOS so you can simply apply our config to a URLSession.

This pulls in most of LocalAPI - though much of it is untested, it's based
on the implementation in tailscale/corp/xcode.

Unit tests pending.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
This commit is contained in:
Jonathan Nobels
2025-04-24 14:12:04 -04:00
committed by Jonathan Nobels
parent a6f9249de0
commit d5a3c8e8ef
21 changed files with 1839 additions and 194 deletions

82
go.mod
View File

@@ -1,8 +1,10 @@
module github.com/tailscale/libtailscale
go 1.23.1
go 1.24.0
require tailscale.com v1.76.6
toolchain go1.24.2
require tailscale.com v1.82.5
require (
filippo.io/edwards25519 v1.1.0 // indirect
@@ -10,30 +12,29 @@ require (
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.26.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/creack/pty v1.1.23 // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
github.com/gaissmai/bart v0.11.1 // indirect
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gaissmai/bart v0.18.0 // indirect
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -41,20 +42,19 @@ require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/csrf v1.7.2 // indirect
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/illarion/gonotify/v2 v2.0.3 // indirect
github.com/illarion/gonotify/v3 v3.0.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect
github.com/jellydator/ttlcache/v3 v3.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/miekg/dns v1.1.58 // indirect
@@ -65,35 +65,33 @@ require (
github.com/safchain/ethtool v0.3.0 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 // indirect
github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc // indirect
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 // indirect
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e // indirect
github.com/tcnksm/go-httpstat v0.2.0 // indirect
github.com/u-root/u-root v0.12.0 // indirect
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.23.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.36.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.30.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 // indirect
honnef.co/go/tools v0.5.1 // indirect
)

188
go.sum
View File

@@ -10,36 +10,34 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/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.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk=
github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/bramvdbogaerde/go-scp v1.4.0 h1:jKMwpwCbcX1KyvDbm/PDJuXcMuNVlLGi0Q0reuzjyKY=
github.com/bramvdbogaerde/go-scp v1.4.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
@@ -64,14 +62,14 @@ github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo=
github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY=
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
@@ -88,16 +86,16 @@ github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdF
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f h1:ov45/OzrJG8EKbGjn7jJZQJTN7Z1t73sFYNIRd64YlI=
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f/go.mod h1:JoDrYMZpDPYo6uH9/f6Peqms3zNNWT2XiGgioMOIGuI=
github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=
github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
@@ -106,13 +104,10 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
@@ -123,8 +118,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
@@ -133,9 +128,10 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
@@ -145,54 +141,52 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
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-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca h1:ecjHwH73Yvqf/oIdQ2vxAX+zc6caQsYdPzsxNW1J3G8=
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/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-20240905161824-799c1978fafc h1:cezaQN9pvKVaw56Ma5qr/G646uKIYP0yQf+OyWN/okc=
github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw=
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
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.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/u-root/gobusybox/src v0.0.0-20231228173702-b69f654846aa h1:unMPGGK/CRzfg923allsikmvk2l7beBeFPUNC4RVX/8=
github.com/u-root/gobusybox/src v0.0.0-20231228173702-b69f654846aa/go.mod h1:Zj4Tt22fJVn/nz/y6Ergm1SahR9dio1Zm/D2/S0TmXM=
github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs=
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
@@ -201,35 +195,35 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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=
@@ -237,39 +231,37 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -277,13 +269,13 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k=
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM=
honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs=
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.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
tailscale.com v1.76.6 h1:qxRVe/ljIVWixIiCLOHrakbsoXcw/dKaKCZt25tJ7gc=
tailscale.com v1.76.6/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk=
tailscale.com v1.82.5 h1:p5owmyPoPM1tFVHR3LjquFuLfpZLzafvhe5kjVavHtE=
tailscale.com v1.82.5/go.mod h1:iU6kohVzG+bP0/5XjqBAnW8/6nSG/Du++bO+x7VJZD0=

View File

@@ -9,7 +9,7 @@ struct HelloFromTailscaleApp: App {
var body: some Scene {
WindowGroup {
HelloView(dialer: manager)
HelloView(dialer: manager, model: manager.model)
}
}
}

View File

@@ -14,7 +14,7 @@ struct Logger: TailscaleKit.LogSink {
var logFileHandle: Int32? = STDOUT_FILENO
func log(_ message: String) {
print(message)
print("hello: \(message)")
}
}
@@ -26,15 +26,65 @@ actor HelloManager: Dialer {
var node: TailscaleNode?
let logger = Logger()
let config: Configuration
var ready = false
// The model will be the consumer for our the busWatcher
let model: HelloModel
var localAPIClient: LocalAPIClient?
var processor: MessageProcessor?
init() {
let temp = getDocumentDirectoryPath().absoluteString + "tailscale"
let temp = getDocumentDirectoryPath().path() + "tailscale"
self.config = Configuration(hostName: Settings.hostName,
path: temp,
authKey: Settings.authKey,
controlURL: kDefaultControlURL,
ephemeral: true)
let model = HelloModel(logger: logger)
self.model = model
Task {
await startTailscale()
}
}
private func startTailscale() async {
do {
/// This sets up a localAPI client attached to the local node.
let node = try setupNode()
try await node.up()
let localAPIClient = LocalAPIClient(localNode: node, logger: logger)
// Once we have our local node, we can set up the local API client.
setLocalAPIClient(localAPIClient)
setReady(true)
/// This sets up a bus watcher to listen for changes in the netmap. These will be sent to the given consumer, uin
/// this case, a HelloModel which will keep track of the changes and publish them.
if let processor = await localAPIClient.watchIPNBus(mask: [.initialState, .netmap, .rateLimitNetmaps, .noPrivateKeys],
consumer: model) {
setProcessor(processor)
}
} catch {
Logger().log("Error setting up Tailscale: \(error)")
setReady(false)
}
}
func setLocalAPIClient(_ client: TailscaleKit.LocalAPIClient) {
self.localAPIClient = client
}
func setReady(_ value: Bool) {
self.ready = value
}
func setProcessor(_ processor: MessageProcessor) {
self.processor = processor
}
func setupNode() throws -> TailscaleNode {
@@ -45,17 +95,17 @@ actor HelloManager: Dialer {
func phoneHome(_ setMessage: @escaping MessageSender) async {
do {
let node = try setupNode()
await setMessage("Connecting to Tailnet...")
try await node.up()
guard let node, ready else {
await setMessage("Not ready yet!")
return
}
await setMessage("Phoning " + Settings.tailnetServer + "...")
// Create a URLSession that can access nodes on the tailnet.
// .tailscaleSession(node) is the magic sauce. This sends your URLRequest via
// userspace Tailscale's SOCKS5 proxy.
let sessionConfig = try await URLSessionConfiguration.tailscaleSession(node)
let (sessionConfig, _) = try await URLSessionConfiguration.tailscaleSession(node)
let session = URLSession(configuration: sessionConfig)
// Request a resource from the tailnet...

View File

@@ -0,0 +1,97 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import TailscaleKit
actor HelloModel: MessageConsumer {
private let logger: LogSink
init(logger: LogSink) {
self.logger = logger
}
// MARK: - Message Consumer
// Notify objects contain the Tailnet information we've subscribed to via
// the bus watcher. The state is always included. The netmap is included
// if we add .netmap to the watchopts.
func notify(_ notify: TailscaleKit.Ipn.Notify) {
if let n = notify.NetMap {
netmap = n
peers = n.Peers
netmapHandlers.values.forEach { $0(n) }
peersHandlers.values.forEach { $0(n.Peers) }
}
if let s = notify.State {
logger.log("State: \(s)")
state = s
stateHandlers.values.forEach { $0(s) }
}
}
func error(_ error: any Error) {
logger.log("\(error)")
}
// MARK: - Stream Publishers
// Alternatively, use Combine publishers
var netmap: Netmap.NetworkMap?
var state: Ipn.State?
var peers: [Tailcfg.Node]?
private var netmapHandlers: [UUID: ((Netmap.NetworkMap?) -> Void)] = [:]
private func removeNetmapHandler(_ uuid: UUID) {
netmapHandlers[uuid] = nil
}
private var stateHandlers: [UUID: ((Ipn.State?) -> Void)] = [:]
private func removeStateHandler(_ uuid: UUID) {
stateHandlers[uuid] = nil
}
private var peersHandlers: [UUID: (([Tailcfg.Node]?) -> Void)] = [:]
private func removePeersHandler(_ uuid: UUID) {
peersHandlers[uuid] = nil
}
var netmapStream: AsyncStream<Netmap.NetworkMap?> {
AsyncStream<Netmap.NetworkMap?> { continuation in
let uuid = UUID()
self.netmapHandlers[uuid] = { netmap in
_ = continuation.yield(netmap)
}
continuation.onTermination = { _ in
Task { await self.removeNetmapHandler(uuid) }
}
}
}
var peersStream: AsyncStream<[Tailcfg.Node]?> {
AsyncStream { continuation in
let uuid = UUID()
self.peersHandlers[uuid] = { peers in
_ = continuation.yield(peers)
}
continuation.onTermination = { _ in
Task { await self.removePeersHandler(uuid) }
}
}
}
var stateStream: AsyncStream<Ipn.State?> {
AsyncStream { continuation in
let uuid = UUID()
self.stateHandlers[uuid] = { state in
_ = continuation.yield(state)
}
continuation.onTermination = { _ in
Task { await self.removeStateHandler(uuid) }
}
}
}
}

View File

@@ -5,25 +5,29 @@ import SwiftUI
struct HelloView: View {
@ObservedObject var model : HelloViewModel
@State var viewModel : HelloViewModel
let dialer: Dialer
init(dialer: Dialer) {
init(dialer: Dialer, model: HelloModel) {
self.dialer = dialer
self.model = HelloViewModel()
self.viewModel = HelloViewModel(model: model)
}
var body: some View {
VStack {
Text("TailscaleKit Sample App. See README.md for setup instructions.")
.font(.title)
.font(.title3)
.padding(20)
Text(model.message)
.font(.title2)
Button("Phone Home!") {
model.runRequest(dialer)
Spacer(minLength: 5)
Text(viewModel.stateMessage)
Text(viewModel.peerCountMessage)
Spacer(minLength: 5)
Text(viewModel.message)
.font(.title3)
Button("Phone Home") {
viewModel.runRequest(dialer)
}
}
.padding()
}
@@ -37,5 +41,5 @@ actor PreviewDialer: Dialer {
#Preview {
let d = PreviewDialer()
HelloView(dialer: d)
HelloView(dialer: d, model: HelloModel(logger: Logger()))
}

View File

@@ -1,21 +1,91 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import SwiftUI
import Combine
@preconcurrency import Combine
import TailscaleKit
class HelloViewModel: ObservableObject, @unchecked Sendable {
@Published var message: String = "Ready to phone home!"
@Observable
class HelloViewModel: @unchecked Sendable {
var message: String = "Ready to phone home!"
var peerCountMessage = "Waiting for peers...."
var stateMessage = "Waiting for state...."
func setMessage(_ message: String) async {
await MainActor.run {
self.message = message
var modelObservers = [Task<Void, Never>]()
@MainActor
init(model: HelloModel) {
bindToModel(model)
}
deinit {
modelObservers.forEach { $0.cancel() }
}
@MainActor
func handleStateChange(_ state: Ipn.State?) {
guard let state else {
self.stateMessage = "Waiting for state...."
return
}
self.stateMessage = "IPNState: \(state)"
}
@MainActor
func handlePeersChange(_ peers: [Tailcfg.Node]?) {
guard let peers else {
self.peerCountMessage = "Waiting for peers..."
return
}
if peers.count > 0 {
self.peerCountMessage = "Found \(peers.count) peers"
} else {
self.peerCountMessage = "No peers found"
}
}
@MainActor
func bindToModel(_ model: HelloModel) {
modelObservers.forEach { $0.cancel() }
modelObservers.removeAll()
Task {
await handleStateChange(model.state)
await handlePeersChange(model.peers)
}
modelObservers.append( Task { [weak self] in
for await peers in await model.peersStream {
if Task.isCancelled { return }
guard let self = self else { return }
await MainActor.run { handlePeersChange(peers) }
}
})
modelObservers.append( Task { [weak self] in
for await state in await model.stateStream {
if Task.isCancelled { return }
guard let self = self else { return }
await MainActor.run { handleStateChange(state) }
}
})
}
@MainActor
func setMessage(_ message: String) {
self.message = message
}
func runRequest(_ dialer: Dialer) {
Task {
await dialer.phoneHome(setMessage)
let model = self
await dialer.phoneHome { msg in
await MainActor.run {
model.setMessage(msg)
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>ts.net</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
</dict>
</plist>

View File

@@ -5,8 +5,9 @@ import Foundation
struct Settings {
// Replace with an actual auth key generated from the Tailscale admin console
static let authKey = "tskey-auth-somekey"
static let authKey = "tskey-auth-your-auth-key"
// Note: The sample has a transport exception for http on ts.net so http:// is ok...
// The "Phone Home" button will load the contents of this URL, it should be on your Tailnet.
static let tailnetServer = "http://myserver.my-tailnet.ts.net"
// Identifies this application in the Tailscale admin console.
static let hostName = "Hello-From-Tailsacle-Sample-App"

View File

@@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
C25260032D7A71E800BD3CCA /* TailscaleKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2525FC52D7A69DE00BD3CCA /* TailscaleKit.framework */; };
C25260052D7A71FE00BD3CCA /* TailscaleKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = C2525FC52D7A69DE00BD3CCA /* TailscaleKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C289804A2DBAA8DA0019B7EB /* TailscaleKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C28980492DBAA7E50019B7EB /* TailscaleKit.framework */; };
C289804B2DBAA8DA0019B7EB /* TailscaleKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C28980492DBAA7E50019B7EB /* TailscaleKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -22,12 +24,25 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C289804C2DBAA8DA0019B7EB /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
C289804B2DBAA8DA0019B7EB /* TailscaleKit.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
C2525FC52D7A69DE00BD3CCA /* TailscaleKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TailscaleKit.framework; path = ../../build/Build/Products/Release/TailscaleKit.framework; sourceTree = "<group>"; };
C2525FF12D7A70B700BD3CCA /* HelloFromTailscale.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloFromTailscale.app; sourceTree = BUILT_PRODUCTS_DIR; };
C25260082D7A7DC400BD3CCA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
C289803E2DBA8A350019B7EB /* HelloFromTailscale_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloFromTailscale_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
C28980492DBAA7E50019B7EB /* TailscaleKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TailscaleKit.framework; path = "../../build/Build/Products/Release-iphoneos/TailscaleKit.framework"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -38,6 +53,15 @@
);
target = C2525FF02D7A70B700BD3CCA /* HelloFromTailscale */;
};
C289803F2DBA8A360019B7EB /* Exceptions for "HelloFromTailscale" folder in "HelloFromTailscale_iOS" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Assets.xcassets,
Info.plist,
"Preview Content/Preview Assets.xcassets",
);
target = C28980342DBA8A350019B7EB /* HelloFromTailscale_iOS */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -45,6 +69,7 @@
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
C25260072D7A7BAE00BD3CCA /* Exceptions for "HelloFromTailscale" folder in "HelloFromTailscale" target */,
C289803F2DBA8A360019B7EB /* Exceptions for "HelloFromTailscale" folder in "HelloFromTailscale_iOS" target */,
);
path = HelloFromTailscale;
sourceTree = "<group>";
@@ -60,6 +85,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C28980362DBA8A350019B7EB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C289804A2DBAA8DA0019B7EB /* TailscaleKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -77,6 +110,7 @@
isa = PBXGroup;
children = (
C2525FF12D7A70B700BD3CCA /* HelloFromTailscale.app */,
C289803E2DBA8A350019B7EB /* HelloFromTailscale_iOS.app */,
);
name = Products;
sourceTree = "<group>";
@@ -85,6 +119,7 @@
isa = PBXGroup;
children = (
C2525FC52D7A69DE00BD3CCA /* TailscaleKit.framework */,
C28980492DBAA7E50019B7EB /* TailscaleKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -115,6 +150,29 @@
productReference = C2525FF12D7A70B700BD3CCA /* HelloFromTailscale.app */;
productType = "com.apple.product-type.application";
};
C28980342DBA8A350019B7EB /* HelloFromTailscale_iOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = C289803B2DBA8A350019B7EB /* Build configuration list for PBXNativeTarget "HelloFromTailscale_iOS" */;
buildPhases = (
C28980352DBA8A350019B7EB /* Sources */,
C28980362DBA8A350019B7EB /* Frameworks */,
C28980382DBA8A350019B7EB /* Resources */,
C289804C2DBAA8DA0019B7EB /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
C2525FF22D7A70B700BD3CCA /* HelloFromTailscale */,
);
name = HelloFromTailscale_iOS;
packageProductDependencies = (
);
productName = HelloFromTailscale;
productReference = C289803E2DBA8A350019B7EB /* HelloFromTailscale_iOS.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -145,6 +203,7 @@
projectRoot = "";
targets = (
C2525FF02D7A70B700BD3CCA /* HelloFromTailscale */,
C28980342DBA8A350019B7EB /* HelloFromTailscale_iOS */,
);
};
/* End PBXProject section */
@@ -157,6 +216,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C28980382DBA8A350019B7EB /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -167,6 +233,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C28980352DBA8A350019B7EB /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
@@ -346,6 +419,76 @@
};
name = Release;
};
C289803C2DBA8A350019B7EB /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = HelloFromTailscale/HelloFromTailscale.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"HelloFromTailscale/Preview Content\"";
DEVELOPMENT_TEAM = W5364U7YZB;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "../../build/Build/Products/Release-iphoneOS";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HelloFromTailscale/Info_iOS.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.tailscale.HelloFromTailscale;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
C289803D2DBA8A350019B7EB /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = HelloFromTailscale/HelloFromTailscale.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"HelloFromTailscale/Preview Content\"";
DEVELOPMENT_TEAM = W5364U7YZB;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "../../build/Build/Products/Release-iphoneOS";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HelloFromTailscale/Info_iOS.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.tailscale.HelloFromTailscale;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -367,6 +510,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C289803B2DBA8A350019B7EB /* Build configuration list for PBXNativeTarget "HelloFromTailscale_iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C289803C2DBA8A350019B7EB /* Debug */,
C289803D2DBA8A350019B7EB /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = C2525FB22D7A69A500BD3CCA /* Project object */;

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C28980342DBA8A350019B7EB"
BuildableName = "HelloFromTailscale_iOS.app"
BlueprintName = "HelloFromTailscale_iOS"
ReferencedContainer = "container:TailscaleKitHello.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C28980342DBA8A350019B7EB"
BuildableName = "HelloFromTailscale_iOS.app"
BlueprintName = "HelloFromTailscale_iOS"
ReferencedContainer = "container:TailscaleKitHello.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C28980342DBA8A350019B7EB"
BuildableName = "HelloFromTailscale_iOS.app"
BlueprintName = "HelloFromTailscale_iOS"
ReferencedContainer = "container:TailscaleKitHello.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,42 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import Foundation
extension Date {
var isGoZeroTime: Bool {
return self == GoZeroTime
}
}
// GoZeroTime is a Date instance that matches Go's time.Time zero value. It's
// explicitly created using Go's zero value (0001-01-01, 00:00:00 UTC), since
// using an offset from the Unix epoch (1970-01-01, 00:00:00 UTC) does not work
// as expected on macOS 10.13.
let GoZeroTimeString = "0001-01-01T00:00:00Z"
let GoZeroTime = ISO8601DateFormatter().date(from: GoZeroTimeString)!
extension String {
func iso8601Date() -> Date? {
let iso8601DateFormatter = {
ISO8601DateFormatter()
}()
let iso8601DateFormatterFractionalSeconds: ISO8601DateFormatter = {
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions.insert(.withFractionalSeconds)
return dateFormatter
}()
// Fractional seconds are optional in RFC3339 as generated by Go/control,
// but Foundation date formatters do not parse dates with and without
// fractional seconds without specifying the option to look for them.
if let date = iso8601DateFormatterFractionalSeconds.date(from: self) {
return date
}
if let date = iso8601DateFormatter.date(from: self) {
return date
}
return nil
}
}

View File

@@ -0,0 +1,385 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import Foundation
let kLocalAPIPath = "/localapi/v0/"
public enum LocalAPIError: Error, LocalizedError {
case localAPIBadResponse
case localAPIStatusError(status: Int, body: String)
case localAPIURLRequestError
case localAPIBugReportError
case localAPIJSONEncodeError
case notConnected
case noCredentials
case noSessionID
}
enum HTTPMethod: String {
case GET
case POST
case PATCH
case PUT
case DELETE
}
enum LocalAPIEndpoint: String {
case prefs = "prefs"
case start = "start"
case loginInteractive = "login-interactive"
case resetAuth = "reset-auth"
case logout = "logout"
case profiles = "profiles"
case profilesCurrent = "profiles/current"
case status = "status"
case watchIPNBus = "watch-ipn-bus"
}
public actor LocalAPIClient {
/// The local node for proxying requests
let node: TailscaleNode
let logger: LogSink?
public init(localNode: TailscaleNode, logger: LogSink?) {
self.node = localNode
self.logger = logger
}
// MARK: - Prefs
public func getPrefs() async throws -> Ipn.Prefs {
let result = await doSimpleAPIRequest(
endpoint: .prefs,
method: .GET,
resultTransformer: jsonDecodeTransformer(Ipn.Prefs.self))
switch result {
case .success(let retVal):
return retVal
case .failure(let error):
logger?.log("Failed to getPrefs: \(error)")
throw error
}
}
@discardableResult
public func editPrefs(prefs: Ipn.MaskedPrefs) async throws -> Ipn.Prefs {
let result = await doJSONAPIRequest(
endpoint: .prefs,
method: .PATCH,
bodyAsJSON: prefs,
resultTransformer: jsonDecodeTransformer(Ipn.Prefs.self))
switch result {
case .success(let retVal):
return retVal
case .failure(let error):
logger?.log("Failed to editPrefs: \(error)")
throw error
}
}
// MARK: - Account Management
public func start(options: Ipn.Options) async throws {
let error = await doJSONAPIRequest(
endpoint: .start,
method: .POST,
bodyAsJSON: options,
resultTransformer: errorTransformer)
if let error { throw error }
}
public func startLoginInteractive() async throws {
let error = await doSimpleAPIRequest(
endpoint: .loginInteractive,
method: .POST,
resultTransformer: errorTransformer)
if let error { throw error }
}
public func resetAuth() async throws {
let error = await doSimpleAPIRequest(
endpoint: .resetAuth,
method: .POST,
resultTransformer: errorTransformer)
if let error { throw error }
}
func logout() async throws {
let error = await doSimpleAPIRequest(
endpoint: .logout,
method: .POST,
resultTransformer: errorTransformer)
if let error { throw error }
}
// MARK: - Profiles
public func profiles() async throws -> [IpnLocal.LoginProfile] {
let result = await doSimpleAPIRequest(
endpoint: .profiles,
path: "", // Important, we need the trailing /
method: .GET,
resultTransformer: jsonDecodeTransformer([IpnLocal.LoginProfile].self))
switch result {
case .success(let result): return result
case .failure(let error): throw error
}
}
public func currentProfile() async throws -> IpnLocal.LoginProfile {
let result = await doSimpleAPIRequest(
endpoint: .profilesCurrent,
method: .GET,
resultTransformer: jsonDecodeTransformer(IpnLocal.LoginProfile.self))
switch result {
case .success(let result): return result
case .failure(let error): throw error
}
}
public func addProfile() async throws {
let error = await doSimpleAPIRequest(
endpoint: .profiles,
path: "", // Important, we need the trailing /
method: .PUT,
resultTransformer: errorTransformer)
if let error {
logger?.log("Failed to add profile: \(error)")
throw error
}
}
public func switchProfile(profileID: String) async throws {
let error = await doSimpleAPIRequest(
endpoint: .profiles,
path: profileID,
method: .POST,
resultTransformer: errorTransformer)
if let error {
logger?.log("Failed to switch profile: \(error)")
throw error
}
}
public func deleteProfile(profileID: String) async throws {
let error = await doSimpleAPIRequest(
endpoint: .profiles,
path: profileID,
method: .DELETE,
resultTransformer: errorTransformer)
if let error {
logger?.log("Failed to delete profile: \(error)")
throw error
}
}
// MARK: - Status
public func backendStatus() async throws -> IpnState.Status {
let result = await doSimpleAPIRequest(
endpoint: .status,
method: .GET,
resultTransformer: jsonDecodeTransformer(IpnState.Status.self))
switch result {
case .success(let result): return result
case .failure(let error): throw error
}
}
// MARK: - IPN Bus
/// watchIPNBus subscribes to the IPN notification bus.
/// - Parameters:
/// - mask: options for what notifications
/// - notifyHandler: function invoked on the main thread with notify payloads as they are received
/// - errorHandler: function invoked on the main thread with errors as they happen
/// - Returns: function that can be called to stop the subscription
public func watchIPNBus(mask: Ipn.NotifyWatchOpt, consumer: MessageConsumer) async -> MessageProcessor? {
let params = [URLQueryItem(name: "mask", value: String(mask.rawValue))]
do {
let (request, sessionConfig) = try await self.basicAuthURLRequest(endpoint: .watchIPNBus,
method: .GET,
params: params)
let messageProcessor = await MessageProcessor(consumer: consumer, logger: logger)
messageProcessor.start(request, config: sessionConfig)
return messageProcessor
} catch {
await consumer.error(LocalAPIError.localAPIURLRequestError)
return nil
}
}
// MARK: - Requests
private func basicAuthURLRequest(endpoint: LocalAPIEndpoint,
path: String? = nil,
method: HTTPMethod,
headers: [String: String]? = nil,
params: [URLQueryItem]? = nil) async throws -> (URLRequest, URLSessionConfiguration) {
let (sessionConfig, loopbackConfig) = try await URLSessionConfiguration.tailscaleSession(node)
var endpointPath = endpoint.rawValue
if let path {
endpointPath = endpointPath + "/" + path
}
logger?.log("Requesting \(endpointPath) via \(loopbackConfig.ip!):\(loopbackConfig.port!)")
var urlComponents = URLComponents()
urlComponents.host = loopbackConfig.ip
urlComponents.port = loopbackConfig.port
urlComponents.scheme = "http"
urlComponents.path = "\(kLocalAPIPath)\(endpointPath)"
urlComponents.queryItems = params
guard let url = urlComponents.url else {
logger?.log("Cannot generate LocalAPI URL using \(urlComponents)")
throw LocalAPIError.localAPIURLRequestError
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
// Headers as required by localAPI being accessed via the SOCK5 tsnet proxy.
// See: tailscale_loopback
let basicAuthString = "tsnet:\(loopbackConfig.localAPIKey)".data(using: .utf8)!.base64EncodedString()
request.setValue("Basic \(basicAuthString)", forHTTPHeaderField: "Authorization")
request.setValue("localapi", forHTTPHeaderField: "Sec-Tailscale")
return (request, sessionConfig)
}
private func parseAPIResponse(data: Data?,
response: URLResponse?,
error: Error?) -> Result<Data, Error> {
if let error {
return .failure(error)
}
guard let response = response as? HTTPURLResponse, let data = data else {
return .failure(LocalAPIError.localAPIBadResponse)
}
guard response.statusCode < 300 else {
// Try to parse it as a Go Error (a struct with one String "Error" field,
// otherwise make an error with the string as-is.
let decodedError = try? JSONDecoder().decode(GoError.self, from: data)
let body = String(bytes: data, encoding: .utf8) ?? ""
let error = LocalAPIError.localAPIStatusError(status: response.statusCode,
body: decodedError?.Error ?? body)
return .failure(error)
}
return .success(data)
}
private func doJSONAPIRequest<BodyT: Codable, ResultT: Sendable>(
endpoint: LocalAPIEndpoint,
path: String? = nil,
method: HTTPMethod,
bodyAsJSON: BodyT,
headers: [String: String]? = nil,
timeoutInterval: TimeInterval = 60,
resultTransformer: @escaping (_ result: Result<Data, Error>) -> ResultT
) async -> ResultT {
do {
let encodedBody = try JSONEncoder().encode(bodyAsJSON)
return await doSimpleAPIRequest(endpoint: endpoint,
path: path,
method: method,
body: encodedBody,
headers: headers,
timeoutInterval: timeoutInterval,
resultTransformer: resultTransformer)
} catch {
logger?.log("Failed to encode request body as JSON: \(error)")
return resultTransformer(.failure(LocalAPIError.localAPIJSONEncodeError))
}
}
private func doSimpleAPIRequest<T: Sendable>(
endpoint: LocalAPIEndpoint,
path: String? = nil,
params: [URLQueryItem]? = nil,
method: HTTPMethod,
body: Data? = nil,
headers: [String: String]? = nil,
timeoutInterval: TimeInterval = 60,
resultTransformer: @escaping (_ result: Result<Data, Error>) -> T) async -> T {
var request: URLRequest
var sessionConfig: URLSessionConfiguration
do {
(request, sessionConfig) = try await self.basicAuthURLRequest(endpoint: endpoint,
path: path,
method: method,
headers: headers,
params: params)
} catch {
return resultTransformer(.failure(error))
}
if let body {
request.httpBody = body
}
request.timeoutInterval = timeoutInterval
do {
let session = URLSession(configuration: sessionConfig)
let (data, response) = try await session.data(for: request)
switch self.parseAPIResponse(data: data, response: response, error: nil) {
case .success(let data):
return resultTransformer(.success(data))
case .failure(let error):
logger?.log("LocalAPI request to \(path ?? "<none>") failed with \(error)")
return resultTransformer(.failure(error))
}
} catch {
return resultTransformer(.failure(error))
}
}
// MARK: - Transformers
func errorTransformer(result: Result<Data, Error>) -> Error? {
switch result {
case .success: return nil
case .failure(let error): return error
}
}
func jsonDecodeTransformer<T: Decodable>(_ type: T.Type) -> (_ result: Result<Data, Error>) -> Result<T, Error> {
return { result in
switch result {
case .success(let data):
do {
return .success(try JSONDecoder().decode(T.self, from: data))
} catch {
return .failure(error)
}
case .failure(let error):
return .failure(error)
}
}
}
}

View File

@@ -0,0 +1,105 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import Foundation
let kJsonNewline = UInt8(ascii: "\n")
/// The polling interval for the message queue
let kProcessorQueuePollInterval: UInt64 = 100_000_000 // Nanos
/// A MessageConsumer consumes incoming messages from the IPNBus and handles any
/// potential errors.
public protocol MessageConsumer: Actor {
func notify(_ notify: Ipn.Notify)
func error(_ error: Error)
}
/// MessageProcessor pulls queued Decodable messages from a MessageReader, deserializes them
/// and forwards the deserialized objects and any errors to the consumer.
public class MessageProcessor: @unchecked Sendable {
let consumer: any MessageConsumer
let reader: MessageReader
let workQueue = OperationQueue()
var logger: LogSink?
// A long running task to poll the queue
var pollTask: Task<Void, Error>?
init(consumer: any MessageConsumer, logger: LogSink?) async {
workQueue.maxConcurrentOperationCount = 1
workQueue.name = "io.tailscale.ipn.MessageProcessor.workQueue"
self.logger = logger
self.consumer = consumer
self.reader = MessageReader()
}
deinit {
cancel()
reader.stop()
}
func start(_ request: URLRequest, config: URLSessionConfiguration, errorHandler: (@Sendable (Error) -> Void)? = nil) {
workQueue.addOperation { [weak self] in
guard let self = self else { return }
logger?.log("Starting MessageProcessor for \(request.url?.absoluteString ?? "nil")")
cancel()
let errorHandler = errorHandler ?? { [weak self] error in
self?.processError(error)
}
reader.start(request, config: config, errorHandler: errorHandler)
startMessageQueuePoll()
}
}
public func cancel() {
pollTask?.cancel()
}
func startMessageQueuePoll() {
pollTask?.cancel()
pollTask = Task {
await watchMessageQueue()
}
}
func watchMessageQueue() async {
logger?.log("Watching MessageReader")
while !Task.isCancelled {
reader.consume { [weak self] data in
if let data {
self?.processMessage(data)
}
}
try? await Task.sleep(nanoseconds: kProcessorQueuePollInterval)
}
logger?.log("Unwatching MessageReader")
}
func processMessage(_ data: Data) {
workQueue.addOperation { [weak self] in
guard let self else { return }
let lines = data.split(separator: kJsonNewline)
for line in lines {
do {
let notify = try JSONDecoder().decode(Ipn.Notify.self, from: line)
Task {
await consumer.notify(notify)
}
} catch {
logger?.log("Failed to decode message: \(String(data: line, encoding: .utf8) ?? "nil")")
}
}
}
}
func processError(_ error: Error) {
Task {
await consumer.error(error)
}
}
}

View File

@@ -0,0 +1,129 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import Foundation
enum MessageQueueError: Error {
case queueCongested
}
/// The maximum number of unprocessed messages that can be queued before we start discarding
/// This needs to be large enough to handle the bursty "first time" connection messages but
/// small enough to avoid our memory footprint growing arbitrarily large.
let kMaxQueueSize = 24
/// Provides a queue for incoming messages on the IPN bus. This will keep a maximum of
/// the last kMaxQueueSize inbound messages pending processing. If the queue is congested, we will
/// stop queueing messages and throw an error once the queue has been drained.
final class MessageReader: NSObject, URLSessionDataDelegate, @unchecked Sendable {
/// All mutation and reading of local state happens in workQueue.
let workQueue = OperationQueue()
/// Holds partial incoming messages
var buffer: Data = Data()
var ipnWatchSession: URLSession?
var dataTask: URLSessionDataTask?
var logger: LogSink?
/// FIFO queue for messages awaiting processing
var pendingMessages: [Data] = []
/// Once congested, we will allow the processor to empty the queue, but we will stop queueing messages.
/// consume()ing the last messages will trigger a MessageQueueError.queueCongested error which the
/// upstream consumer can use. Typically, this means we lost messages, so the correct action is to
/// restart the processor and queue with an .initialState flag.
var congested = false
var errorHandler: (@Sendable (Error) -> Void)?
init(logger: LogSink? = nil) {
self.logger = logger
workQueue.maxConcurrentOperationCount = 1
workQueue.name = "io.tailscale.ipn.MessageReader.workQueue"
}
func stop() {
ipnWatchSession?.invalidateAndCancel()
workQueue.cancelAllOperations()
}
func start(_ request: URLRequest, config: URLSessionConfiguration, errorHandler: @escaping @Sendable (Error) -> Void ) {
workQueue.addOperation { [weak self] in
guard let self = self else { return }
self.errorHandler = errorHandler
buffer = Data()
pendingMessages = []
congested = false
dataTask?.cancel()
ipnWatchSession?.invalidateAndCancel()
ipnWatchSession = URLSession(configuration: config,
delegate: self,
delegateQueue: workQueue)
dataTask = ipnWatchSession?.dataTask(with: request)
dataTask?.resume()
}
}
func consume(_ completion: @escaping @Sendable (Data?) -> Void) {
workQueue.addOperation { [weak self] in
guard let self else { return }
if congested && pendingMessages.count == 0 {
errorHandler?(MessageQueueError.queueCongested)
completion(nil)
return
}
guard pendingMessages.count > 0 else {
completion(nil)
return
}
completion(pendingMessages.removeFirst())
}
}
// MARK: - URLSessionDataDelegate
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
if let error = error {
let nsError = error as NSError
// Ignore cancellation errors, those are deliberate.
if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled {
return
}
errorHandler?(error)
}
}
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data) {
if congested {
return
}
receiveData(data)
}
func receiveData(_ data: Data) {
workQueue.addOperation { [weak self] in
guard let self else { return }
buffer.append(data)
if buffer[buffer.count - 1] == kJsonNewline {
if pendingMessages.count >= kMaxQueueSize {
congested = true
return
}
pendingMessages.append(buffer)
buffer.removeAll(keepingCapacity: true)
}
}
}
}

View File

@@ -0,0 +1,506 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import Foundation
public struct Empty: Sendable {
public struct Message: Codable, Sendable {}
}
public struct Key: Sendable {
public typealias NodePublic = String
}
public struct IP: Sendable {
public typealias Addr = String
public typealias Prefix = String
}
public struct Time: Sendable {
public typealias Time = String
}
public struct Ipn: Sendable {
public enum State: Int, Codable, CaseIterable, Sendable {
case NoState = 0
case InUseOtherUser = 1
case NeedsLogin = 2
case NeedsMachineAuth = 3
case Stopped = 4
case Starting = 5
case Running = 6
}
public struct EngineStatus: Codable, Sendable, Equatable {
public var RBytes: Int64
public var WBytes: Int64
public var NumLive: Int
public var LivePeers: [Key.NodePublic: IpnState.PeerStatusLite]
}
public struct Notify: Codable, Sendable {
public var Version: String?
public var SessionID: String?
public var ErrMessage: String?
public var LoginFinished: Empty.Message?
public var State: State?
public var Prefs: Prefs?
public var NetMap: Netmap.NetworkMap?
public var Engine: EngineStatus?
public var BrowseToURL: String?
public var LocalTCPPort: UInt16?
public var ClientVersion: Tailcfg.ClientVersion?
}
public struct NotifyWatchOpt: OptionSet, Sendable {
public let rawValue: UInt64
public init(rawValue: UInt64) {
self.rawValue = rawValue
}
public static let engineUpdates = NotifyWatchOpt(rawValue: 1 << 0)
public static let initialState = NotifyWatchOpt(rawValue: 1 << 1)
public static let prefs = NotifyWatchOpt(rawValue: 1 << 2)
public static let netmap = NotifyWatchOpt(rawValue: 1 << 3)
public static let noPrivateKeys = NotifyWatchOpt(rawValue: 1 << 4)
public static let rateLimitNetmaps = NotifyWatchOpt(rawValue: 1 << 8)
}
public struct Prefs: Codable, Sendable {
public var ControlURL: String = ""
public var RouteAll: Bool = false
public var AllowSingleHosts: Bool = false
public var CorpDNS: Bool = false
public var WantRunning: Bool = false
public var LoggedOut: Bool = false
public var ShieldsUp: Bool = false
public var AdvertiseRoutes: [String]?
public var AdvertiseTags: [String]?
public var ExitNodeID: Tailcfg.StableNodeID = ""
public var ExitNodeAllowLANAccess: Bool = false
public var ForceDaemon: Bool? = false
public var Hostname: String = ""
}
public struct MaskedPrefs: Codable, Sendable {
public var ControlURL: String = "" {didSet {
ControlURLSet = true
}}
public var RouteAll: Bool = false {didSet {
RouteAllSet = true
}}
public var CorpDNS: Bool = false {didSet {
CorpDNSSet = true
}}
public var ExitNodeID: String = "" {didSet {
ExitNodeIDSet = true
}}
public var ExitNodeAllowLANAccess: Bool = false {didSet {
ExitNodeAllowLANAccessSet = true
}}
public var WantRunning: Bool = false {didSet {
WantRunningSet = true
}}
public var ShieldsUp: Bool = false {didSet {
ShieldsUpSet = true
}}
public var AdvertiseRoutes: [String]? {didSet {
AdvertiseRoutesSet = true
}}
public var ForceDaemon: Bool = false {didSet {
ForceDaemonSet = true
}}
public var Hostname: String = "" {didSet {
HostnameSet = true
}}
// Mask fields should not need to be manually set, they are automatically
// populated in setters.
private(set) var ControlURLSet: Bool?
private(set) var RouteAllSet: Bool?
private(set) var CorpDNSSet: Bool?
private(set) var ExitNodeIDSet: Bool?
private(set) var ExitNodeAllowLANAccessSet: Bool?
private(set) var WantRunningSet: Bool?
private(set) var ShieldsUpSet: Bool?
private(set) var AdvertiseRoutesSet: Bool?
private(set) var ForceDaemonSet: Bool?
private(set) var HostnameSet: Bool?
init() {}
// Helper builder functions which can be chained in place of the convenience
// initializer.
@discardableResult
public func controlURL(_ value: String) -> MaskedPrefs {
var p = self
p.ControlURL = value
return p
}
@discardableResult
public func routeAll(_ value: Bool) -> MaskedPrefs {
var p = self
p.RouteAll = value
return p
}
@discardableResult
public func corpDNS(_ value: Bool) -> MaskedPrefs {
var p = self
p.CorpDNS = value
return p
}
@discardableResult
public func exitNodeID(_ value: String) -> MaskedPrefs {
var p = self
p.ExitNodeID = value
return p
}
@discardableResult
public func exitNodeAllowLANAccess(_ value: Bool) -> MaskedPrefs {
var p = self
p.ExitNodeAllowLANAccess = value
return p
}
@discardableResult
public func wantRunning(_ value: Bool) -> MaskedPrefs {
var p = self
p.WantRunning = value
return p
}
@discardableResult
public func shieldsUp(_ value: Bool) -> MaskedPrefs {
var p = self
p.ShieldsUp = value
return p
}
@discardableResult
public func advertiseRoutes(_ value: [String]) -> MaskedPrefs {
var p = self
p.AdvertiseRoutes = value
return p
}
@discardableResult
public func forceDaemon(_ value: Bool) -> MaskedPrefs {
var p = self
p.ForceDaemon = value
return p
}
@discardableResult
public func hostname(_ value: String) -> MaskedPrefs {
var p = self
p.Hostname = value
return p
}
}
public struct Options: Codable {
public var UpdatePrefs: Prefs?
public var AuthKey: String?
}
}
public struct IpnLocal: Sendable {
public struct LoginProfile: Equatable, Codable, Identifiable, Sendable {
public var ID: String
public var Name: String
public var Key: String
public var UserProfile: Tailcfg.UserProfile
public var NetworkProfile: Tailcfg.NetworkProfile?
public var LocalUserID: String
public var ControlURL: String?
public var id: String { self.ID }
public func isNullUser() -> Bool {
return id.isEmpty
}
}
}
public struct IpnState: Sendable {
public struct PeerStatus: Codable, Equatable, Sendable {
public var ID: Tailcfg.StableNodeID
public var HostName: String
public var DNSName: String
public var TailscaleIPs: [IP.Addr]?
public var Tags: [String]?
public var PrimaryRoutes: [String]?
public var Addrs: [String]?
public var CurAddr: String?
public var Relay: String?
public var Online: Bool
public var ExitNode: Bool
public var ExitNodeOption: Bool
public var PeerAPIURL: [String]?
public var Capabilities: [String]?
public var SSH_HostKeys: [String]?
public var ShareeNode: Bool?
public var Expired: Bool?
}
public struct PeerStatusLite: Codable, Sendable, Equatable {
public var RxBytes: Int64
public var TxBytes: Int64
public var LastHandshake: Time.Time
public var NodeKey: String
}
public struct Status: Codable, Sendable {
enum CodingKeys: String, CodingKey {
case Version,
BackendState,
AuthURL,
TailscaleIPs,
ExitNodeStatus,
Health,
CurrentTailnet,
CertDomains,
Peer,
User,
ClientVersion
case SelfStatus = "Self"
}
public var Version: String
public var BackendState: String
public var AuthURL: String
public var TailscaleIPs: [IP.Addr]?
public var SelfStatus: PeerStatus?
public var ExitNodeStatus: ExitNodeStatus?
public var Health: [String]?
public var CurrentTailnet: TailnetStatus?
public var CertDomains: [String]?
public var Peer: [String: PeerStatus]?
public var User: [String: Tailcfg.UserProfile]?
public var ClientVersion: Tailcfg.ClientVersion?
}
public struct ExitNodeStatus: Codable, Sendable {
public var ID: Tailcfg.StableNodeID
public var Online: Bool
public var TailscaleIPs: [IP.Prefix]?
}
public struct TailnetStatus: Codable, Sendable {
public var Name: String
public var MagicDNSSuffix: String
public var MagicDNSEnabled: Bool
}
struct PingResult: Codable, Sendable {
public var IP: IP.Addr
public var Err: String
public var LatencySeconds: TimeInterval
}
}
public struct Netmap: Sendable {
public struct NetworkMap: Codable, Equatable, Sendable {
public var SelfNode: Tailcfg.Node
public var NodeKey: Key.NodePublic
public var Peers: [Tailcfg.Node]?
public var Expiry: Time.Time
public var Domain: String
public var UserProfiles: [String: Tailcfg.UserProfile] // Keys are tailcfg.UserIDs thet get stringified
public var DNS: Tailcfg.DNSConfig?
public func currentUserProfile() -> Tailcfg.UserProfile? {
return userProfile(for: SelfNode.User)
}
public func userProfile(for id: Int64) -> Tailcfg.UserProfile? {
return UserProfiles[String(id)]
}
public func isExpired() -> Bool {
if let expiryDate = Expiry.iso8601Date() {
return (expiryDate as NSDate).earlierDate(Date()) == expiryDate
}
return false
}
public static func == (lhs: Netmap.NetworkMap, rhs: Netmap.NetworkMap) -> Bool {
lhs.SelfNode == rhs.SelfNode &&
lhs.NodeKey == rhs.NodeKey &&
lhs.Peers == rhs.Peers &&
lhs.Expiry == rhs.Expiry &&
lhs.Domain == rhs.Domain &&
lhs.UserProfiles == rhs.UserProfiles &&
lhs.DNS == rhs.DNS
}
}
}
public struct Tailcfg: Sendable {
public typealias MachineKey = String
public typealias NodeID = Int64
public typealias StableNodeID = String
public typealias UserID = Int64
public struct Hostinfo: Codable, Equatable, Sendable {
public var OS: String?
public var OSVersion: String?
public var DeviceModel: String?
public var ShareeNode: Bool?
public var Hostname: String?
public var ShieldsUp: Bool?
}
public struct Node: Codable, Equatable, @unchecked Sendable {
public var ID: Tailcfg.NodeID
public var StableID: Tailcfg.StableNodeID
public var Name: String
public var User: Tailcfg.UserID
public var Sharer: Tailcfg.UserID?
public var Key: Key.NodePublic
public var KeyExpiry: Time.Time
public var Machine: Tailcfg.MachineKey
public var Addresses: [IP.Prefix]?
public var AllowedIPs: [IP.Prefix]?
public var Hostinfo: Hostinfo
public var LastSeen: Time.Time?
public var Online: Bool?
public var Capabilities: [String]?
public var Tags: [String]?
public var ComputedName: String
public var ComputedNameWithHost: String
// reports whether Node offers default routing services.
public var IsExitNode: Bool {
var default4: Bool = false
var default6: Bool = false
for ip in self.AllowedIPs ?? [] {
if ip == "0.0.0.0/0" {
default4 = true
} else if ip == "::/0" {
default6 = true
}
if default4 && default6 {
return true
}
}
return false
}
public var isAdmin: Bool {
return !(self.Capabilities ?? []).filter({ $0 == "https://tailscale.com/cap/is-admin" }).isEmpty
}
public var KeyDoesNotExpire: Bool {
if KeyExpiry == GoZeroTimeString {
return true
}
return false
}
public var HasExpiredAuth: Bool {
if KeyDoesNotExpire {
return false
}
if let expiryDate = KeyExpiry.iso8601Date() {
return (expiryDate as NSDate).earlierDate(Date()) == expiryDate && !KeyDoesNotExpire
}
return false
}
/// Returns the UserId of the user who owns this node. That's either the user who shared this node
/// with the current user if available, or the actual owner of the node.
public var SharerOrUser: Tailcfg.UserID {
Sharer ?? User
}
public var hasNonZeroLastSeen: Bool {
LastSeen != GoZeroTimeString
}
public static func == (lhs: Tailcfg.Node, rhs: Tailcfg.Node) -> Bool {
lhs.ID == rhs.ID &&
lhs.Name == rhs.Name &&
lhs.Online == rhs.Online &&
lhs.IsExitNode == rhs.IsExitNode &&
lhs.KeyExpiry == rhs.KeyExpiry &&
lhs.Addresses == rhs.Addresses &&
lhs.Capabilities == rhs.Capabilities
}
}
public struct UserProfile: Equatable, Codable, Identifiable, Hashable, Sendable {
public var ID: Int64
public var DisplayName: String
public var LoginName: String
public var ProfilePicURL: String?
public var id: Int64 { self.ID }
public var isTaggedDevice: Bool { LoginName == "tagged-devices" }
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
public struct NetworkProfile: Codable, Equatable, Sendable {
public var MagicDNSName: String?
public var DomainName: String?
}
public struct DNSRecord: Codable, Sendable, Equatable {
enum CodingKeys: String, CodingKey {
case Name
case RecordType = "Type"
case Value
}
public var Name: String
public var RecordType: String?
public var Value: String
}
public struct DNSConfig: Codable, Sendable, Equatable {
public var Resolvers: [DNSType.Resolver]?
public var Routes: [String: [DNSType.Resolver]?]?
public var FallbackResolvers: [DNSType.Resolver]?
public var Domains: [String]?
public var Nameservers: [IP.Addr]?
public var ExtraRecords: [DNSRecord]?
}
public struct ClientVersion: Codable, Sendable, Equatable {
public var RunningLatest: Bool?
public var LatestVersion: String?
public var UrgentSecurityUpdate: Bool?
public var Notify: Bool?
public var NotifyURL: String?
public var NotifyText: String?
}
}
public struct DNSType: Sendable {
public struct Resolver: Codable, Identifiable, Sendable, Equatable {
public var Addr: String?
public var BootstrapResolution: [IP.Addr]?
public var id: String { Addr ?? "" }
}
}
struct GoError: Codable, Sendable, LocalizedError {
let Error: String
init(error: String) {
self.Error = error
}
var errorDescription: String? {
return Error
}
}

View File

@@ -3,7 +3,6 @@
import Foundation
public enum TailscaleError: Error {
case badInterfaceHandle ///< The tailscale handle is bad.
case listenerClosed ///< The listener was closed and cannot accept new connections
@@ -45,7 +44,14 @@ extension TailscaleHandle {
}
let res = tailscale_errmsg(self, buf, 256)
if res != 0 {
return "Error fetch failure: \(res)"
switch res {
case EBADF:
return "Bad file descriptor"
case ERANGE:
return "Error message buffer too small"
default:
return "Error fetch failure: \(res)"
}
}
return String(cString: buf)
}

View File

@@ -138,7 +138,7 @@ typedef int tailscale_listener;
// Returns zero on success or -1 on error, call tailscale_errmsg for details.
extern int tailscale_listen(tailscale sd, const char* network, const char* addr, tailscale_listener* listener_out);
// Returns the remote address for an incoming connection for a particular listener. The address (eitehr ip4 or ip6)
// Returns the remote address for an incoming connection for a particular listener. The address (either ip4 or ip6)
// will ge written to buf on on success.
// Returns:
// 0 - Success

View File

@@ -75,6 +75,7 @@ public actor TailscaleNode {
if let authKey = config.authKey {
tailscale_set_authkey(tailscale, authKey)
}
tailscale_set_hostname(tailscale, config.hostName)
tailscale_set_dir(tailscale, config.path)
tailscale_set_control_url(tailscale, config.controlURL)
@@ -181,6 +182,24 @@ public actor TailscaleNode {
let address: String
let proxyCredential: String
let localAPIKey: String
var ip: String? {
let parts = address.split(separator: ":")
let addr = parts.first
guard parts.count == 2, let addr else {
return nil
}
return String(addr)
}
var port: Int? {
let parts = address.split(separator: ":")
let port = parts.last
guard parts.count == 2, let port else {
return nil
}
return Int(port)
}
}
private var loopbackConfig: LoopbackConfig?

View File

@@ -7,22 +7,15 @@ import UIKit
public extension URLSessionConfiguration {
// (barnstar) TODO: kCFNetworkProxiesSOCKS* is not available on iOS
// is there another way to make this work on non desktops?
#if os(macOS)
/// Adds the a connectionProxyDictionary to a URLSessionConfiguration to
/// proxy all requests through the given TailscaleNode.
///
/// This can also be use to make requests to LocalAPI
func proxyVia(_ node: TailscaleNode) async throws {
/// This can also be use to make requests to LocalAPI. See LocalAPIClient
@discardableResult
func proxyVia(_ node: TailscaleNode) async throws -> TailscaleNode.LoopbackConfig {
let proxyConfig = try await node.loopback()
// The address is always v4 and it's always <ip>:<port>
let parts = proxyConfig.address.split(separator: ":")
let addr = parts.first
let port = parts.last
guard parts.count == 2, let addr, let port else {
guard let ip = proxyConfig.ip, let port = proxyConfig.port else {
throw TailscaleError.invalidProxyAddress
}
@@ -30,17 +23,19 @@ public extension URLSessionConfiguration {
kCFProxyTypeKey: kCFProxyTypeSOCKS,
kCFProxyUsernameKey: "tsnet",
kCFProxyPasswordKey: proxyConfig.proxyCredential,
kCFNetworkProxiesSOCKSEnable: true,
kCFNetworkProxiesSOCKSProxy: addr,
kCFNetworkProxiesSOCKSPort: port
kCFNetworkProxiesHTTPEnable: true,
kCFNetworkProxiesHTTPSEnable: true,
kCFNetworkProxiesHTTPProxy: ip,
kCFNetworkProxiesHTTPPort: port,
]
return proxyConfig
}
static func tailscaleSession(_ node: TailscaleNode) async throws -> URLSessionConfiguration {
let config = URLSessionConfiguration.default
try await config.proxyVia(node)
return config
static func tailscaleSession(_ node: TailscaleNode) async throws -> (URLSessionConfiguration, TailscaleNode.LoopbackConfig) {
let session = URLSessionConfiguration.default
let config = try await session.proxyVia(node)
return (session, config)
}
#endif
}

View File

@@ -57,17 +57,14 @@ final class TailscaleKitTests: XCTestCase {
let lisetnerUp = expectation(description: "lisetnerUp")
var listenerAddr: String?
var writerAddr: String?
switch netType {
case .v4:
listenerAddr = ts1_addr.ip4
writerAddr = ts2_addr.ip4
case .v6:
// barnstar: Validity of listener IPs is loadbearing. accept fails
// in the C code if you listen on an invalid addr.
listenerAddr = if let a = ts1_addr.ip6 { "[\(a)]"} else { nil }
writerAddr = if let a = ts2_addr.ip6 { "[\(a)]"} else { nil }
case .none:
XCTFail("Invalid IP Type")
}
@@ -149,7 +146,7 @@ final class TailscaleKitTests: XCTestCase {
let ts1 = try TailscaleNode(config: config, logger: logger)
try await ts1.up()
let sessionConfig = try await URLSessionConfiguration.tailscaleSession(ts1)
let (sessionConfig, _) = try await URLSessionConfiguration.tailscaleSession(ts1)
let session = URLSession(configuration: sessionConfig)
let url = URL(string: "https://tailscale.com")!
@@ -177,7 +174,7 @@ final class TailscaleKitTests: XCTestCase {
let ts1 = try TailscaleNode(config: config, logger: logger)
try await ts1.up()
let sessionConfig = try await URLSessionConfiguration.tailscaleSession(ts1)
let (sessionConfig, _) = try await URLSessionConfiguration.tailscaleSession(ts1)
let session = URLSession(configuration: sessionConfig)
// Replace this with the IP or fqdn of a service running on your tailnet