diff --git a/go.mod b/go.mod index 0e80699..d4ecfc2 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index e702a3d..a645816 100644 --- a/go.sum +++ b/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= diff --git a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloFromTailscaleApp.swift b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloFromTailscaleApp.swift index 8ec6999..0e4dc50 100644 --- a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloFromTailscaleApp.swift +++ b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloFromTailscaleApp.swift @@ -9,7 +9,7 @@ struct HelloFromTailscaleApp: App { var body: some Scene { WindowGroup { - HelloView(dialer: manager) + HelloView(dialer: manager, model: manager.model) } } } diff --git a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloManager.swift b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloManager.swift index 1c51320..66b21ec 100644 --- a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloManager.swift +++ b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloManager.swift @@ -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... diff --git a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloModel.swift b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloModel.swift new file mode 100644 index 0000000..4a8239a --- /dev/null +++ b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloModel.swift @@ -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 { + AsyncStream { 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 { + AsyncStream { continuation in + let uuid = UUID() + self.stateHandlers[uuid] = { state in + _ = continuation.yield(state) + } + continuation.onTermination = { _ in + Task { await self.removeStateHandler(uuid) } + } + } + } +} diff --git a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloView.swift b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloView.swift index de928a9..ee2d8cc 100644 --- a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloView.swift +++ b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloView.swift @@ -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())) } diff --git a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloViewModel.swift b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloViewModel.swift index 95cb318..d23ab18 100644 --- a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloViewModel.swift +++ b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloViewModel.swift @@ -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]() + + @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) + } + } } } } diff --git a/swift/Examples/TailscaleKitHello/HelloFromTailscale/Info_iOS.plist b/swift/Examples/TailscaleKitHello/HelloFromTailscale/Info_iOS.plist new file mode 100644 index 0000000..365ab61 --- /dev/null +++ b/swift/Examples/TailscaleKitHello/HelloFromTailscale/Info_iOS.plist @@ -0,0 +1,19 @@ + + + + + NSAppTransportSecurity + + NSExceptionDomains + + ts.net + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + + + + diff --git a/swift/Examples/TailscaleKitHello/HelloFromTailscale/TailnetSettings.swift b/swift/Examples/TailscaleKitHello/HelloFromTailscale/TailnetSettings.swift index ea24492..6c7d0ea 100644 --- a/swift/Examples/TailscaleKitHello/HelloFromTailscale/TailnetSettings.swift +++ b/swift/Examples/TailscaleKitHello/HelloFromTailscale/TailnetSettings.swift @@ -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" diff --git a/swift/Examples/TailscaleKitHello/TailscaleKitHello.xcodeproj/project.pbxproj b/swift/Examples/TailscaleKitHello/TailscaleKitHello.xcodeproj/project.pbxproj index ee16377..6ef9e99 100644 --- a/swift/Examples/TailscaleKitHello/TailscaleKitHello.xcodeproj/project.pbxproj +++ b/swift/Examples/TailscaleKitHello/TailscaleKitHello.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; + 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 = ""; }; /* 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 = ""; @@ -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 = ""; @@ -85,6 +119,7 @@ isa = PBXGroup; children = ( C2525FC52D7A69DE00BD3CCA /* TailscaleKit.framework */, + C28980492DBAA7E50019B7EB /* TailscaleKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -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 */; diff --git a/swift/Examples/TailscaleKitHello/TailscaleKitHello.xcodeproj/xcshareddata/xcschemes/HelloFromTailscale iOS.xcscheme b/swift/Examples/TailscaleKitHello/TailscaleKitHello.xcodeproj/xcshareddata/xcschemes/HelloFromTailscale iOS.xcscheme new file mode 100644 index 0000000..ffcaa5a --- /dev/null +++ b/swift/Examples/TailscaleKitHello/TailscaleKitHello.xcodeproj/xcshareddata/xcschemes/HelloFromTailscale iOS.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swift/TailscaleKit/LocalAPI/GoTime.swift b/swift/TailscaleKit/LocalAPI/GoTime.swift new file mode 100644 index 0000000..211277a --- /dev/null +++ b/swift/TailscaleKit/LocalAPI/GoTime.swift @@ -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 + } +} diff --git a/swift/TailscaleKit/LocalAPI/LocalAPIClient.swift b/swift/TailscaleKit/LocalAPI/LocalAPIClient.swift new file mode 100644 index 0000000..01387ba --- /dev/null +++ b/swift/TailscaleKit/LocalAPI/LocalAPIClient.swift @@ -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 { + + 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( + endpoint: LocalAPIEndpoint, + path: String? = nil, + method: HTTPMethod, + bodyAsJSON: BodyT, + headers: [String: String]? = nil, + timeoutInterval: TimeInterval = 60, + resultTransformer: @escaping (_ result: Result) -> 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( + endpoint: LocalAPIEndpoint, + path: String? = nil, + params: [URLQueryItem]? = nil, + method: HTTPMethod, + body: Data? = nil, + headers: [String: String]? = nil, + timeoutInterval: TimeInterval = 60, + resultTransformer: @escaping (_ result: Result) -> 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 ?? "") failed with \(error)") + return resultTransformer(.failure(error)) + } + } catch { + return resultTransformer(.failure(error)) + } + } + + // MARK: - Transformers + + func errorTransformer(result: Result) -> Error? { + switch result { + case .success: return nil + case .failure(let error): return error + } + } + + func jsonDecodeTransformer(_ type: T.Type) -> (_ result: Result) -> Result { + 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) + } + } + } +} diff --git a/swift/TailscaleKit/LocalAPI/MessageProcessor.swift b/swift/TailscaleKit/LocalAPI/MessageProcessor.swift new file mode 100644 index 0000000..7d680db --- /dev/null +++ b/swift/TailscaleKit/LocalAPI/MessageProcessor.swift @@ -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? + + 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) + } + } +} diff --git a/swift/TailscaleKit/LocalAPI/MessageReader.swift b/swift/TailscaleKit/LocalAPI/MessageReader.swift new file mode 100644 index 0000000..9dd8ff9 --- /dev/null +++ b/swift/TailscaleKit/LocalAPI/MessageReader.swift @@ -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) + } + } + } +} diff --git a/swift/TailscaleKit/LocalAPI/Types.swift b/swift/TailscaleKit/LocalAPI/Types.swift new file mode 100644 index 0000000..182fbd6 --- /dev/null +++ b/swift/TailscaleKit/LocalAPI/Types.swift @@ -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 + } +} diff --git a/swift/TailscaleKit/TailscaleError.swift b/swift/TailscaleKit/TailscaleError.swift index 00dd274..d979101 100644 --- a/swift/TailscaleKit/TailscaleError.swift +++ b/swift/TailscaleKit/TailscaleError.swift @@ -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) } diff --git a/swift/TailscaleKit/TailscaleKit.h b/swift/TailscaleKit/TailscaleKit.h index c157d66..184fa62 100644 --- a/swift/TailscaleKit/TailscaleKit.h +++ b/swift/TailscaleKit/TailscaleKit.h @@ -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 diff --git a/swift/TailscaleKit/TailscaleNode.swift b/swift/TailscaleKit/TailscaleNode.swift index 3ea903e..c7cd808 100644 --- a/swift/TailscaleKit/TailscaleNode.swift +++ b/swift/TailscaleKit/TailscaleNode.swift @@ -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? diff --git a/swift/TailscaleKit/URLSession+Tailscale.swift b/swift/TailscaleKit/URLSession+Tailscale.swift index ffbe3fc..f579320 100644 --- a/swift/TailscaleKit/URLSession+Tailscale.swift +++ b/swift/TailscaleKit/URLSession+Tailscale.swift @@ -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 : - 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 - } diff --git a/swift/TailscaleKitXCTests/TailscaleKitTests.swift b/swift/TailscaleKitXCTests/TailscaleKitTests.swift index 7d543a5..d17eeaa 100644 --- a/swift/TailscaleKitXCTests/TailscaleKitTests.swift +++ b/swift/TailscaleKitXCTests/TailscaleKitTests.swift @@ -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