mirror of
https://github.com/Drop-OSS/libtailscale.git
synced 2026-01-30 20:55:18 +01:00
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:
committed by
Jonathan Nobels
parent
a6f9249de0
commit
d5a3c8e8ef
82
go.mod
82
go.mod
@@ -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
188
go.sum
@@ -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=
|
||||
|
||||
@@ -9,7 +9,7 @@ struct HelloFromTailscaleApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
HelloView(dialer: manager)
|
||||
HelloView(dialer: manager, model: manager.model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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 */;
|
||||
|
||||
@@ -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>
|
||||
42
swift/TailscaleKit/LocalAPI/GoTime.swift
Normal file
42
swift/TailscaleKit/LocalAPI/GoTime.swift
Normal 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
|
||||
}
|
||||
}
|
||||
385
swift/TailscaleKit/LocalAPI/LocalAPIClient.swift
Normal file
385
swift/TailscaleKit/LocalAPI/LocalAPIClient.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
105
swift/TailscaleKit/LocalAPI/MessageProcessor.swift
Normal file
105
swift/TailscaleKit/LocalAPI/MessageProcessor.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
129
swift/TailscaleKit/LocalAPI/MessageReader.swift
Normal file
129
swift/TailscaleKit/LocalAPI/MessageReader.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
506
swift/TailscaleKit/LocalAPI/Types.swift
Normal file
506
swift/TailscaleKit/LocalAPI/Types.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user