├── readme.md ├── example_httpts.go ├── cmd └── tailweb │ └── tailweb.go ├── LICENSE ├── go.mod ├── httpts.go ├── go.sum └── internal └── tsnet ├── tsnet_test.go └── tsnet.go /readme.md: -------------------------------------------------------------------------------- 1 | httpts is a Go package to make it easy to serve http on a tailnet 2 | -------------------------------------------------------------------------------- /example_httpts.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | // This example demos serving HTTP on your tailnet. 5 | // 6 | // To run entirely locally use: 7 | // 8 | // go run ./example_httpts.go -devport 8080 9 | // 10 | // This is useful for local development on the bus. 11 | // To run on a tailnet, drop the -devport flag: 12 | // 13 | // go run ./example_httpts.go 14 | // 15 | // The first time, a tailscale login URL will be printed to put it on a tailnet. 16 | // Then it will print out the full URL of this server. 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "log" 23 | "net/http" 24 | 25 | "github.com/crawshaw/httpts" 26 | ) 27 | 28 | func main() { 29 | devPort := flag.Int("devport", 0, "localhost port to run in dev mode, 0 to disable") 30 | flag.Parse() 31 | 32 | s := httpts.Server{ 33 | Handler: http.HandlerFunc(handler), 34 | InsecureLocalPortOnly: *devPort, 35 | } 36 | log.Fatal(s.Serve("httpts-example")) 37 | } 38 | 39 | func handler(w http.ResponseWriter, r *http.Request) { 40 | fmt.Fprintf(w, "Hello, %s!", httpts.WhoFromCtx(r.Context()).LoginName) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/tailweb/tailweb.go: -------------------------------------------------------------------------------- 1 | // The tailweb command serves out the current directory to your tailnet. 2 | // 3 | // This is the tailscale equivalent of the classic python one-liner: 4 | // 5 | // python -m http.server 8000 6 | // 7 | // Install tailweb with: 8 | // 9 | // go install github.com/crawshaw/httpts/cmd/tailweb@latest 10 | // 11 | // then to serve the current directory, run: 12 | // 13 | // tailweb 14 | package main 15 | 16 | import ( 17 | "flag" 18 | "fmt" 19 | "net/http" 20 | "os" 21 | 22 | "github.com/crawshaw/httpts" 23 | ) 24 | 25 | func usage() { 26 | fmt.Fprintf(os.Stderr, "usage: tailweb [dir]\n") 27 | flag.PrintDefaults() 28 | os.Exit(2) 29 | } 30 | 31 | func main() { 32 | flag.Usage = usage 33 | hostname, err := os.Hostname() 34 | if err != nil { 35 | panic(err) 36 | } 37 | tsHostname := flag.String("hostname", hostname+"-tailweb", "hostname to use for the server") 38 | flag.Parse() 39 | 40 | dir, err := os.Getwd() 41 | if err != nil { 42 | panic(err) 43 | } 44 | if args := flag.Args(); len(args) == 1 { 45 | dir = flag.Args()[0] 46 | } else if len(flag.Args()) > 1 { 47 | usage() 48 | } 49 | 50 | s := httpts.Server{Handler: http.FileServer(http.Dir(dir))} 51 | panic(s.Serve(*tsHostname)) 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024 David Crawshaw 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crawshaw/httpts 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/prometheus/client_model v0.5.0 7 | github.com/prometheus/common v0.48.0 8 | golang.org/x/net v0.27.0 9 | tailscale.com v1.76.1 10 | ) 11 | 12 | require ( 13 | filippo.io/edwards25519 v1.1.0 // indirect 14 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 15 | github.com/akutz/memconn v0.1.0 // indirect 16 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect 17 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 18 | github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect 19 | github.com/aws/aws-sdk-go-v2/config v1.26.5 // indirect 20 | github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect 21 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect 22 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect 23 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect 24 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect 31 | github.com/aws/smithy-go v1.19.0 // indirect 32 | github.com/bits-and-blooms/bitset v1.13.0 // indirect 33 | github.com/coder/websocket v1.8.12 // indirect 34 | github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect 35 | github.com/creack/pty v1.1.23 // indirect 36 | github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect 37 | github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect 38 | github.com/djherbis/times v1.6.0 // indirect 39 | github.com/fxamacker/cbor/v2 v2.6.0 // indirect 40 | github.com/gaissmai/bart v0.11.1 // indirect 41 | github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect 42 | github.com/go-ole/go-ole v1.3.0 // indirect 43 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect 44 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 45 | github.com/google/btree v1.1.2 // indirect 46 | github.com/google/go-cmp v0.6.0 // indirect 47 | github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect 48 | github.com/google/uuid v1.6.0 // indirect 49 | github.com/gorilla/csrf v1.7.2 // indirect 50 | github.com/gorilla/securecookie v1.1.2 // indirect 51 | github.com/hdevalence/ed25519consensus v0.2.0 // indirect 52 | github.com/illarion/gonotify/v2 v2.0.3 // indirect 53 | github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect 54 | github.com/jellydator/ttlcache/v3 v3.1.0 // indirect 55 | github.com/jmespath/go-jmespath v0.4.0 // indirect 56 | github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect 57 | github.com/jsimonetti/rtnetlink v1.4.0 // indirect 58 | github.com/klauspost/compress v1.17.4 // indirect 59 | github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect 60 | github.com/kr/fs v0.1.0 // indirect 61 | github.com/mdlayher/genetlink v1.3.2 // indirect 62 | github.com/mdlayher/netlink v1.7.2 // indirect 63 | github.com/mdlayher/sdnotify v1.0.0 // indirect 64 | github.com/mdlayher/socket v0.5.0 // indirect 65 | github.com/miekg/dns v1.1.58 // indirect 66 | github.com/mitchellh/go-ps v1.0.0 // indirect 67 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 68 | github.com/pkg/sftp v1.13.6 // indirect 69 | github.com/prometheus-community/pro-bing v0.4.0 // indirect 70 | github.com/safchain/ethtool v0.3.0 // indirect 71 | github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect 72 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect 73 | github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect 74 | github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect 75 | github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect 76 | github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect 77 | github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect 78 | github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect 79 | github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 // indirect 80 | github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc // indirect 81 | github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e // indirect 82 | github.com/tcnksm/go-httpstat v0.2.0 // indirect 83 | github.com/u-root/u-root v0.12.0 // indirect 84 | github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect 85 | github.com/vishvananda/netns v0.0.4 // indirect 86 | github.com/x448/float16 v0.8.4 // indirect 87 | go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect 88 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect 89 | golang.org/x/crypto v0.25.0 // indirect 90 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect 91 | golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect 92 | golang.org/x/mod v0.19.0 // indirect 93 | golang.org/x/oauth2 v0.23.0 // indirect 94 | golang.org/x/sync v0.7.0 // indirect 95 | golang.org/x/sys v0.22.0 // indirect 96 | golang.org/x/term v0.22.0 // indirect 97 | golang.org/x/text v0.16.0 // indirect 98 | golang.org/x/time v0.5.0 // indirect 99 | golang.org/x/tools v0.23.0 // indirect 100 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 101 | golang.zx2c4.com/wireguard/windows v0.5.3 // indirect 102 | google.golang.org/protobuf v1.33.0 // indirect 103 | gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect 104 | honnef.co/go/tools v0.5.1 // indirect 105 | ) 106 | -------------------------------------------------------------------------------- /httpts.go: -------------------------------------------------------------------------------- 1 | // Package httpts provides an HTTP server that runs on a Tailscale tailnet. 2 | // 3 | // Every http.Request context served by this package has httpts.Who attached 4 | // to it, telling you who is calling. 5 | package httpts 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "log" 13 | "net" 14 | "net/http" 15 | "os" 16 | "path/filepath" 17 | "strings" 18 | "sync" 19 | 20 | "github.com/crawshaw/httpts/internal/tsnet" 21 | "golang.org/x/oauth2/clientcredentials" 22 | "tailscale.com/client/tailscale" 23 | "tailscale.com/ipn" 24 | "tailscale.com/tailcfg" 25 | ) 26 | 27 | // Server is a drop-in for http.Server that serves a Handler on a tailnet. 28 | type Server struct { 29 | // Handler answers requests from the tailnet. 30 | Handler http.Handler 31 | 32 | // FunnelHandler, if non-nil, answers requsts from the internet via Tailscale Funnel. 33 | // Unused if InsecureLocalPortOnly is true. 34 | FunnelHandler http.Handler 35 | 36 | // InsecureLocalPortOnly, if non-zero, means that no tsnet server is started 37 | // and instead the server listens over http:// on the specified 127.0.0.1 port. 38 | // It is insecure because all localhost handling is passed to Handler. 39 | InsecureLocalPortOnly int 40 | 41 | // StateStore, if non-nil, is used to store state for the tailscale client. 42 | StateStore ipn.StateStore 43 | 44 | AdvertiseTags []string 45 | 46 | // OauthClientSecret is used to authenticate the node if it is not already. 47 | // Create one at https://login.tailscale.com/admin/settings/oauth. 48 | // The client must be created with a tag that matches AdvertiseTags. 49 | // Note that the client secret must start with `tskey-client-`. 50 | // 51 | // Ignored if AuthKey is non-empty. 52 | // 53 | // Do not pass an OauthClientSecret to a server that you do not trust 54 | // to add nodes to your tailnet. 55 | OauthClientSecret string 56 | 57 | // AuthKey, if non-empty, is the auth key to create the node. 58 | AuthKey string 59 | 60 | ts *tsnet.Server 61 | httpsrv *http.Server 62 | lc *tailscale.LocalClient 63 | 64 | ctx context.Context 65 | ctxCancel func() 66 | 67 | started struct { 68 | mu sync.Mutex 69 | ch chan struct{} // closed when tsnet is serving, access via startedCh 70 | } 71 | } 72 | 73 | func (s *Server) startedCh() chan struct{} { 74 | s.started.mu.Lock() 75 | defer s.started.mu.Unlock() 76 | if s.started.ch == nil { 77 | s.started.ch = make(chan struct{}) 78 | } 79 | return s.started.ch 80 | } 81 | 82 | // Who is attached to every http.Request context naming the HTTP client. 83 | type Who struct { 84 | LoginName string 85 | PeerCap tailcfg.PeerCapMap 86 | } 87 | 88 | type whoCtxKeyType struct{} 89 | 90 | var whoCtxKey = whoCtxKeyType{} 91 | 92 | func WhoFromCtx(ctx context.Context) *Who { 93 | who, ok := ctx.Value(whoCtxKey).(*Who) 94 | if !ok { 95 | return nil 96 | } 97 | return who 98 | } 99 | 100 | func (s *Server) mkhttpsrv() { 101 | if s.httpsrv != nil { 102 | return 103 | } 104 | s.ctx, s.ctxCancel = context.WithCancel(context.Background()) 105 | s.httpsrv = &http.Server{ 106 | Handler: http.HandlerFunc(s.whoHandler), 107 | } 108 | } 109 | 110 | func (s *Server) whoHandler(w http.ResponseWriter, r *http.Request) { 111 | var who Who 112 | if s.InsecureLocalPortOnly != 0 { 113 | who = Who{LoginName: "insecure-localhost"} 114 | } else { 115 | whoResp, err := s.lc.WhoIs(r.Context(), r.RemoteAddr) 116 | if s.FunnelHandler != nil && errors.Is(err, tailscale.ErrPeerNotFound) { 117 | // TODO: pass an empty Who? 118 | s.FunnelHandler.ServeHTTP(w, r) 119 | return 120 | } else if err != nil { 121 | http.Error(w, "httpts: "+err.Error(), http.StatusUnauthorized) 122 | return 123 | } 124 | who = Who{ 125 | LoginName: whoResp.UserProfile.LoginName, 126 | PeerCap: whoResp.CapMap, 127 | } 128 | } 129 | r = r.WithContext(context.WithValue(r.Context(), whoCtxKey, &who)) 130 | s.Handler.ServeHTTP(w, r) 131 | } 132 | 133 | // Dial dials the address on the tailnet. 134 | func (s *Server) Dial(ctx context.Context, network, address string) (net.Conn, error) { 135 | select { 136 | case <-s.startedCh(): 137 | return s.ts.Dial(ctx, network, address) 138 | case <-ctx.Done(): 139 | return nil, ctx.Err() 140 | } 141 | } 142 | 143 | // Serve serves :443 and a :80 redirect on a tailnet. 144 | func (s *Server) Serve(tsHostname string) error { 145 | s.mkhttpsrv() 146 | 147 | confDir, err := os.UserConfigDir() 148 | if err != nil { 149 | return fmt.Errorf("httpts: %w", err) 150 | } 151 | 152 | s.ts = &tsnet.Server{ 153 | Dir: filepath.Join(confDir, "httpts-"+tsHostname), 154 | Store: s.StateStore, 155 | Hostname: tsHostname, 156 | AdvertiseTags: s.AdvertiseTags, 157 | AuthKey: s.AuthKey, 158 | } 159 | defer s.ts.Close() 160 | 161 | if s.InsecureLocalPortOnly != 0 { 162 | s.httpsrv.Addr = fmt.Sprintf("127.0.0.1:%d", s.InsecureLocalPortOnly) 163 | log.Printf("Serving: http://%s", s.httpsrv.Addr) 164 | 165 | // Return before calling Up, so local-port-only mode 166 | // does not necessarily invoke Tailscale, unless you call Dial. 167 | close(s.startedCh()) 168 | return s.httpsrv.ListenAndServe() 169 | } 170 | 171 | if s.AuthKey == "" && s.OauthClientSecret != "" { 172 | var err error 173 | s.ts.AuthKey, err = s.createAuthKey(s.ctx) 174 | if err != nil { 175 | return fmt.Errorf("create auth key: %w", err) 176 | } 177 | } 178 | 179 | // Call Up explicitly with a context that is canceled on Shutdown 180 | // so we don't get stuck in ListenTLS on Shutdown. 181 | if _, err := s.ts.Up(s.ctx); err != nil { 182 | return fmt.Errorf("httpts.up: %w", err) 183 | } 184 | close(s.startedCh()) 185 | 186 | var ln net.Listener 187 | if s.FunnelHandler != nil { 188 | ln, err = s.ts.ListenFunnel("tcp", ":443") 189 | } else { 190 | ln, err = s.ts.ListenTLS("tcp", ":443") 191 | } 192 | if err != nil { 193 | return fmt.Errorf("httpts: %w", err) 194 | } 195 | lc, err := s.ts.LocalClient() 196 | if err != nil { 197 | return fmt.Errorf("httpts: %w", err) 198 | } 199 | s.lc = lc 200 | if status, err := lc.Status(context.Background()); err != nil { 201 | return fmt.Errorf("httpts: %w", err) 202 | } else { 203 | log.Printf("Serving: https://%s/\n", strings.TrimSuffix(status.Self.DNSName, ".")) 204 | } 205 | 206 | ln80, err := s.ts.Listen("tcp", ":80") 207 | if err != nil { 208 | return fmt.Errorf("httpts: %w", err) 209 | } 210 | srv80 := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 211 | target := "https://" + r.Host + r.URL.Path 212 | if len(r.URL.RawQuery) > 0 { 213 | target += "?" + r.URL.RawQuery 214 | } 215 | http.Redirect(w, r, target, http.StatusMovedPermanently) 216 | })} 217 | go func() { 218 | err := srv80.Serve(ln80) 219 | if errors.Is(err, http.ErrServerClosed) { 220 | return 221 | } 222 | panic(err) 223 | }() 224 | 225 | s.httpsrv.RegisterOnShutdown(func() { 226 | ctx, cancel := context.WithCancel(context.Background()) 227 | cancel() // shut down immediately 228 | srv80.Shutdown(ctx) 229 | }) 230 | err = s.httpsrv.Serve(ln) 231 | s.lc = nil 232 | return err 233 | } 234 | 235 | // Shutdown shuts down the HTTP server and Tailscale client. 236 | func (s *Server) Shutdown(ctx context.Context) error { 237 | s.ctxCancel() 238 | var err, err2 error 239 | err = s.httpsrv.Shutdown(ctx) 240 | if s.ts != nil { 241 | err2 = s.ts.Close() 242 | } 243 | s.ts = nil 244 | if err == nil { 245 | err = err2 246 | } 247 | return err 248 | } 249 | 250 | func tsClientConfig(clientSecret string) (*clientcredentials.Config, error) { 251 | oauthConfig := &clientcredentials.Config{ 252 | ClientSecret: clientSecret, 253 | TokenURL: "https://api.tailscale.com/api/v2/oauth/token", 254 | } 255 | if s := strings.TrimPrefix(oauthConfig.ClientSecret, "tskey-client-"); s == oauthConfig.ClientSecret { 256 | return nil, fmt.Errorf("OauthClientSecret must start with `tskey-client-`") 257 | } else { 258 | oauthConfig.ClientID, _, _ = strings.Cut(s, "-") 259 | } 260 | return oauthConfig, nil 261 | } 262 | 263 | func checkTSClientConfig(ctx context.Context, oauthConfig *clientcredentials.Config) error { 264 | tsClient := oauthConfig.Client(ctx) 265 | resp, err := tsClient.Get("https://api.tailscale.com/api/v2/tailnet/-/devices") 266 | if err != nil { 267 | return fmt.Errorf("oauth client failure: %w", err) 268 | } 269 | body, err := io.ReadAll(resp.Body) 270 | if err != nil { 271 | return fmt.Errorf("oauth client failure: %w", err) 272 | } 273 | if resp.StatusCode != 200 { 274 | return fmt.Errorf("basic device list failed: %s", body) 275 | } 276 | return nil 277 | } 278 | 279 | func (s *Server) createAuthKey(ctx context.Context) (string, error) { 280 | return CreateAuthKey(ctx, s.OauthClientSecret, tailscale.KeyDeviceCreateCapabilities{ 281 | Reusable: false, 282 | Ephemeral: false, // TODO export 283 | Preauthorized: true, // false, // TODO export 284 | Tags: s.AdvertiseTags, 285 | }) 286 | } 287 | 288 | func CreateAuthKey(ctx context.Context, clientSecret string, deviceCaps tailscale.KeyDeviceCreateCapabilities) (string, error) { 289 | oauthCfg, err := tsClientConfig(clientSecret) 290 | if err != nil { 291 | return "", err 292 | } 293 | if err := checkTSClientConfig(ctx, oauthCfg); err != nil { 294 | return "", err 295 | } 296 | 297 | tailscale.I_Acknowledge_This_API_Is_Unstable = true 298 | tsClient := tailscale.NewClient("-", nil) 299 | tsClient.HTTPClient = oauthCfg.Client(ctx) 300 | 301 | caps := tailscale.KeyCapabilities{ 302 | Devices: tailscale.KeyDeviceCapabilities{ 303 | Create: deviceCaps, 304 | }, 305 | } 306 | authkey, _, err := tsClient.CreateKey(ctx, caps) 307 | if err != nil { 308 | return "", err 309 | } 310 | return authkey, nil 311 | } 312 | 313 | // TestWhoRequest sets up a Who object on a http.Request for use in testing. 314 | func TestWhoRequest(r *http.Request) *http.Request { 315 | who := Who{LoginName: "insecure-test"} 316 | return r.WithContext(context.WithValue(r.Context(), whoCtxKey, &who)) 317 | } 318 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= 4 | filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= 5 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 6 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 7 | github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= 8 | github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= 9 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= 10 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 11 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 12 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 13 | github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= 14 | github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= 15 | github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw= 16 | github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU= 17 | github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8= 18 | github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0= 19 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= 20 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= 21 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= 22 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= 23 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= 24 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= 25 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= 26 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= 27 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= 28 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= 29 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= 30 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= 31 | github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE= 32 | github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM= 33 | github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow= 34 | github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= 35 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= 36 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= 37 | github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= 38 | github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= 39 | github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= 40 | github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= 41 | github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= 42 | github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 43 | github.com/bramvdbogaerde/go-scp v1.4.0 h1:jKMwpwCbcX1KyvDbm/PDJuXcMuNVlLGi0Q0reuzjyKY= 44 | github.com/bramvdbogaerde/go-scp v1.4.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ= 45 | github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= 46 | github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= 47 | github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= 48 | github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= 49 | github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= 50 | github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= 51 | github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= 52 | github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 53 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 54 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 56 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= 58 | github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= 59 | github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= 60 | github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= 61 | github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= 62 | github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= 63 | github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= 64 | github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= 65 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 66 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 67 | github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= 68 | github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 69 | github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= 70 | github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= 71 | github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= 72 | github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= 73 | github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= 74 | github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= 75 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 76 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 77 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= 78 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= 79 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 80 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 81 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 82 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 83 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 84 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 85 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 86 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 87 | github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= 88 | github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= 89 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 90 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 91 | github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI= 92 | github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= 93 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 94 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 95 | github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= 96 | github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= 97 | github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f h1:ov45/OzrJG8EKbGjn7jJZQJTN7Z1t73sFYNIRd64YlI= 98 | github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f/go.mod h1:JoDrYMZpDPYo6uH9/f6Peqms3zNNWT2XiGgioMOIGuI= 99 | github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A= 100 | github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE= 101 | github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= 102 | github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= 103 | github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= 104 | github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= 105 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 106 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 107 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 108 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 109 | github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 110 | github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= 111 | github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= 112 | github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= 113 | github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= 114 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 115 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 116 | github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= 117 | github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= 118 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 119 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 120 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 121 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 122 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 123 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 124 | github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= 125 | github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= 126 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 127 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 128 | github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= 129 | github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= 130 | github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= 131 | github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= 132 | github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= 133 | github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= 134 | github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= 135 | github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= 136 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 137 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 138 | github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 139 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 140 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 141 | github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= 142 | github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= 143 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 144 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 145 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 146 | github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= 147 | github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= 148 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 149 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 150 | github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= 151 | github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= 152 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 153 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 154 | github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= 155 | github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= 156 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 157 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 158 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 159 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 160 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 161 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 162 | github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU= 163 | github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= 164 | github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= 165 | github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= 166 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= 167 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= 168 | github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw= 169 | github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= 170 | github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= 171 | github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= 172 | github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= 173 | github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= 174 | github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= 175 | github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= 176 | github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w= 177 | github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU= 178 | github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g= 179 | github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= 180 | github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= 181 | github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= 182 | github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc h1:cezaQN9pvKVaw56Ma5qr/G646uKIYP0yQf+OyWN/okc= 183 | github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= 184 | github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= 185 | github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= 186 | github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= 187 | github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= 188 | github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= 189 | github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= 190 | github.com/u-root/gobusybox/src v0.0.0-20231228173702-b69f654846aa h1:unMPGGK/CRzfg923allsikmvk2l7beBeFPUNC4RVX/8= 191 | github.com/u-root/gobusybox/src v0.0.0-20231228173702-b69f654846aa/go.mod h1:Zj4Tt22fJVn/nz/y6Ergm1SahR9dio1Zm/D2/S0TmXM= 192 | github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs= 193 | github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI= 194 | github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw= 195 | github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= 196 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 197 | github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= 198 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 199 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 200 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 201 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 202 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 203 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 204 | go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= 205 | go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= 206 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= 207 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= 208 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 209 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 210 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 211 | golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 212 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 213 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= 214 | golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 215 | golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= 216 | golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 217 | golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= 218 | golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= 219 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 220 | golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= 221 | golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 222 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 223 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 224 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 225 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 226 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 227 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 228 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 229 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 230 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 231 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 232 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 233 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 234 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 235 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 236 | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 240 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 241 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 244 | golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 246 | golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 247 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 248 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 249 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 250 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 251 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 252 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 253 | golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= 254 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 255 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 256 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 257 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 258 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 259 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 260 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 261 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 262 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 263 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 264 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 265 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 266 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 267 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 268 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 269 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= 270 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 271 | golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= 272 | golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= 273 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 274 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 275 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 276 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 277 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 278 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 279 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 280 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 281 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 282 | gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= 283 | gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= 284 | honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= 285 | honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= 286 | howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= 287 | howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 288 | software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= 289 | software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= 290 | tailscale.com v1.76.1 h1:Gv0w6LdASTbkihnvNZM2sBVAU3EY0qgeSJ7yZlHxRE8= 291 | tailscale.com v1.76.1/go.mod h1:myCwmhYBvMCF/5OgBYuIW42zscuEo30bAml7wABVZLk= 292 | -------------------------------------------------------------------------------- /internal/tsnet/tsnet_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tailscale Inc & AUTHORS 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | package tsnet 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "context" 10 | "crypto/ecdsa" 11 | "crypto/elliptic" 12 | "crypto/rand" 13 | "crypto/tls" 14 | "crypto/x509" 15 | "crypto/x509/pkix" 16 | "errors" 17 | "flag" 18 | "fmt" 19 | "io" 20 | "log" 21 | "math/big" 22 | "net" 23 | "net/http" 24 | "net/http/httptest" 25 | "net/netip" 26 | "os" 27 | "path/filepath" 28 | "reflect" 29 | "runtime" 30 | "strings" 31 | "sync" 32 | "sync/atomic" 33 | "testing" 34 | "time" 35 | 36 | dto "github.com/prometheus/client_model/go" 37 | "github.com/prometheus/common/expfmt" 38 | "golang.org/x/net/proxy" 39 | "tailscale.com/cmd/testwrapper/flakytest" 40 | "tailscale.com/health" 41 | "tailscale.com/ipn" 42 | "tailscale.com/ipn/store/mem" 43 | "tailscale.com/net/netns" 44 | "tailscale.com/tailcfg" 45 | "tailscale.com/tstest" 46 | "tailscale.com/tstest/integration" 47 | "tailscale.com/tstest/integration/testcontrol" 48 | "tailscale.com/types/key" 49 | "tailscale.com/types/logger" 50 | "tailscale.com/util/must" 51 | ) 52 | 53 | // TestListener_Server ensures that the listener type always keeps the Server 54 | // method, which is used by some external applications to identify a tsnet.Listener 55 | // from other net.Listeners, as well as access the underlying Server. 56 | func TestListener_Server(t *testing.T) { 57 | s := &Server{} 58 | ln := listener{s: s} 59 | if ln.Server() != s { 60 | t.Errorf("listener.Server() returned %v, want %v", ln.Server(), s) 61 | } 62 | } 63 | 64 | func TestListenerPort(t *testing.T) { 65 | errNone := errors.New("sentinel start error") 66 | 67 | tests := []struct { 68 | network string 69 | addr string 70 | wantErr bool 71 | }{ 72 | {"tcp", ":80", false}, 73 | {"foo", ":80", true}, 74 | {"tcp", ":http", false}, // built-in name to Go; doesn't require cgo, /etc/services 75 | {"tcp", ":https", false}, // built-in name to Go; doesn't require cgo, /etc/services 76 | {"tcp", ":gibberishsdlkfj", true}, 77 | {"tcp", ":%!d(string=80)", true}, // issue 6201 78 | {"udp", ":80", false}, 79 | {"udp", "100.102.104.108:80", false}, 80 | {"udp", "not-an-ip:80", true}, 81 | {"udp4", ":80", false}, 82 | {"udp4", "100.102.104.108:80", false}, 83 | {"udp4", "not-an-ip:80", true}, 84 | 85 | // Verify network type matches IP 86 | {"tcp4", "1.2.3.4:80", false}, 87 | {"tcp6", "1.2.3.4:80", true}, 88 | {"tcp4", "[12::34]:80", true}, 89 | {"tcp6", "[12::34]:80", false}, 90 | } 91 | for _, tt := range tests { 92 | s := &Server{} 93 | s.initOnce.Do(func() { s.initErr = errNone }) 94 | _, err := s.Listen(tt.network, tt.addr) 95 | gotErr := err != nil && err != errNone 96 | if gotErr != tt.wantErr { 97 | t.Errorf("Listen(%q, %q) error = %v, want %v", tt.network, tt.addr, gotErr, tt.wantErr) 98 | } 99 | } 100 | } 101 | 102 | var verboseDERP = flag.Bool("verbose-derp", false, "if set, print DERP and STUN logs") 103 | var verboseNodes = flag.Bool("verbose-nodes", false, "if set, print tsnet.Server logs") 104 | 105 | func startControl(t *testing.T) (controlURL string, control *testcontrol.Server) { 106 | // Corp#4520: don't use netns for tests. 107 | netns.SetEnabled(false) 108 | t.Cleanup(func() { 109 | netns.SetEnabled(true) 110 | }) 111 | 112 | derpLogf := logger.Discard 113 | if *verboseDERP { 114 | derpLogf = t.Logf 115 | } 116 | derpMap := integration.RunDERPAndSTUN(t, derpLogf, "127.0.0.1") 117 | control = &testcontrol.Server{ 118 | DERPMap: derpMap, 119 | DNSConfig: &tailcfg.DNSConfig{ 120 | Proxied: true, 121 | }, 122 | MagicDNSDomain: "tail-scale.ts.net", 123 | } 124 | control.HTTPTestServer = httptest.NewUnstartedServer(control) 125 | control.HTTPTestServer.Start() 126 | t.Cleanup(control.HTTPTestServer.Close) 127 | controlURL = control.HTTPTestServer.URL 128 | t.Logf("testcontrol listening on %s", controlURL) 129 | return controlURL, control 130 | } 131 | 132 | type testCertIssuer struct { 133 | mu sync.Mutex 134 | certs map[string]*tls.Certificate 135 | 136 | root *x509.Certificate 137 | rootKey *ecdsa.PrivateKey 138 | } 139 | 140 | func newCertIssuer() *testCertIssuer { 141 | rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 142 | if err != nil { 143 | panic(err) 144 | } 145 | t := &x509.Certificate{ 146 | SerialNumber: big.NewInt(1), 147 | Subject: pkix.Name{ 148 | CommonName: "root", 149 | }, 150 | NotBefore: time.Now(), 151 | NotAfter: time.Now().Add(time.Hour), 152 | IsCA: true, 153 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 154 | KeyUsage: x509.KeyUsageCertSign, 155 | BasicConstraintsValid: true, 156 | } 157 | rootDER, err := x509.CreateCertificate(rand.Reader, t, t, &rootKey.PublicKey, rootKey) 158 | if err != nil { 159 | panic(err) 160 | } 161 | rootCA, err := x509.ParseCertificate(rootDER) 162 | if err != nil { 163 | panic(err) 164 | } 165 | return &testCertIssuer{ 166 | certs: make(map[string]*tls.Certificate), 167 | root: rootCA, 168 | rootKey: rootKey, 169 | } 170 | } 171 | 172 | func (tci *testCertIssuer) getCert(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { 173 | tci.mu.Lock() 174 | defer tci.mu.Unlock() 175 | cert, ok := tci.certs[chi.ServerName] 176 | if ok { 177 | return cert, nil 178 | } 179 | 180 | certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 181 | if err != nil { 182 | return nil, err 183 | } 184 | certTmpl := &x509.Certificate{ 185 | SerialNumber: big.NewInt(1), 186 | DNSNames: []string{chi.ServerName}, 187 | NotBefore: time.Now(), 188 | NotAfter: time.Now().Add(time.Hour), 189 | } 190 | certDER, err := x509.CreateCertificate(rand.Reader, certTmpl, tci.root, &certPrivKey.PublicKey, tci.rootKey) 191 | if err != nil { 192 | return nil, err 193 | } 194 | cert = &tls.Certificate{ 195 | Certificate: [][]byte{certDER, tci.root.Raw}, 196 | PrivateKey: certPrivKey, 197 | } 198 | tci.certs[chi.ServerName] = cert 199 | return cert, nil 200 | } 201 | 202 | func (tci *testCertIssuer) Pool() *x509.CertPool { 203 | p := x509.NewCertPool() 204 | p.AddCert(tci.root) 205 | return p 206 | } 207 | 208 | var testCertRoot = newCertIssuer() 209 | 210 | func startServer(t *testing.T, ctx context.Context, controlURL, hostname string) (*Server, netip.Addr, key.NodePublic) { 211 | t.Helper() 212 | 213 | tmp := filepath.Join(t.TempDir(), hostname) 214 | os.MkdirAll(tmp, 0755) 215 | s := &Server{ 216 | Dir: tmp, 217 | ControlURL: controlURL, 218 | Hostname: hostname, 219 | Store: new(mem.Store), 220 | Ephemeral: true, 221 | getCertForTesting: testCertRoot.getCert, 222 | } 223 | if *verboseNodes { 224 | s.Logf = log.Printf 225 | } 226 | t.Cleanup(func() { s.Close() }) 227 | 228 | status, err := s.Up(ctx) 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | return s, status.TailscaleIPs[0], status.Self.PublicKey 233 | } 234 | 235 | func TestConn(t *testing.T) { 236 | tstest.ResourceCheck(t) 237 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 238 | defer cancel() 239 | 240 | controlURL, c := startControl(t) 241 | s1, s1ip, s1PubKey := startServer(t, ctx, controlURL, "s1") 242 | s2, _, _ := startServer(t, ctx, controlURL, "s2") 243 | 244 | s1.lb.EditPrefs(&ipn.MaskedPrefs{ 245 | Prefs: ipn.Prefs{ 246 | AdvertiseRoutes: []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}, 247 | }, 248 | AdvertiseRoutesSet: true, 249 | }) 250 | c.SetSubnetRoutes(s1PubKey, []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}) 251 | 252 | lc2, err := s2.LocalClient() 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | 257 | // ping to make sure the connection is up. 258 | res, err := lc2.Ping(ctx, s1ip, tailcfg.PingICMP) 259 | if err != nil { 260 | t.Fatal(err) 261 | } 262 | t.Logf("ping success: %#+v", res) 263 | 264 | // pass some data through TCP. 265 | ln, err := s1.Listen("tcp", ":8081") 266 | if err != nil { 267 | t.Fatal(err) 268 | } 269 | defer ln.Close() 270 | 271 | w, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip)) 272 | if err != nil { 273 | t.Fatal(err) 274 | } 275 | 276 | r, err := ln.Accept() 277 | if err != nil { 278 | t.Fatal(err) 279 | } 280 | 281 | want := "hello" 282 | if _, err := io.WriteString(w, want); err != nil { 283 | t.Fatal(err) 284 | } 285 | 286 | got := make([]byte, len(want)) 287 | if _, err := io.ReadAtLeast(r, got, len(got)); err != nil { 288 | t.Fatal(err) 289 | } 290 | t.Logf("got: %q", got) 291 | if string(got) != want { 292 | t.Errorf("got %q, want %q", got, want) 293 | } 294 | 295 | _, err = s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8082", s1ip)) // some random port 296 | if err == nil { 297 | t.Fatalf("unexpected success; should have seen a connection refused error") 298 | } 299 | 300 | // s1 is a subnet router for TEST-NET-1 (192.0.2.0/24). Lets dial to that 301 | // subnet from s2 to ensure a listener without an IP address (i.e. ":8081") 302 | // only matches destination IPs corresponding to the node's IP, and not 303 | // to any random IP a subnet is routing. 304 | _, err = s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", "192.0.2.1")) 305 | if err == nil { 306 | t.Fatalf("unexpected success; should have seen a connection refused error") 307 | } 308 | } 309 | 310 | func TestLoopbackLocalAPI(t *testing.T) { 311 | flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8557") 312 | tstest.ResourceCheck(t) 313 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 314 | defer cancel() 315 | 316 | controlURL, _ := startControl(t) 317 | s1, _, _ := startServer(t, ctx, controlURL, "s1") 318 | 319 | addr, proxyCred, localAPICred, err := s1.Loopback() 320 | if err != nil { 321 | t.Fatal(err) 322 | } 323 | if proxyCred == localAPICred { 324 | t.Fatal("proxy password matches local API password, they should be different") 325 | } 326 | 327 | url := "http://" + addr + "/localapi/v0/status" 328 | req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 329 | if err != nil { 330 | t.Fatal(err) 331 | } 332 | res, err := http.DefaultClient.Do(req) 333 | if err != nil { 334 | t.Fatal(err) 335 | } 336 | res.Body.Close() 337 | if res.StatusCode != 403 { 338 | t.Errorf("GET %s returned %d, want 403 without Sec- header", url, res.StatusCode) 339 | } 340 | 341 | req, err = http.NewRequestWithContext(ctx, "GET", url, nil) 342 | if err != nil { 343 | t.Fatal(err) 344 | } 345 | req.Header.Set("Sec-Tailscale", "localapi") 346 | res, err = http.DefaultClient.Do(req) 347 | if err != nil { 348 | t.Fatal(err) 349 | } 350 | res.Body.Close() 351 | if res.StatusCode != 401 { 352 | t.Errorf("GET %s returned %d, want 401 without basic auth", url, res.StatusCode) 353 | } 354 | 355 | req, err = http.NewRequestWithContext(ctx, "GET", url, nil) 356 | if err != nil { 357 | t.Fatal(err) 358 | } 359 | req.SetBasicAuth("", localAPICred) 360 | res, err = http.DefaultClient.Do(req) 361 | if err != nil { 362 | t.Fatal(err) 363 | } 364 | res.Body.Close() 365 | if res.StatusCode != 403 { 366 | t.Errorf("GET %s returned %d, want 403 without Sec- header", url, res.StatusCode) 367 | } 368 | 369 | req, err = http.NewRequestWithContext(ctx, "GET", url, nil) 370 | if err != nil { 371 | t.Fatal(err) 372 | } 373 | req.Header.Set("Sec-Tailscale", "localapi") 374 | req.SetBasicAuth("", localAPICred) 375 | res, err = http.DefaultClient.Do(req) 376 | if err != nil { 377 | t.Fatal(err) 378 | } 379 | res.Body.Close() 380 | if res.StatusCode != 200 { 381 | t.Errorf("GET /status returned %d, want 200", res.StatusCode) 382 | } 383 | } 384 | 385 | func TestLoopbackSOCKS5(t *testing.T) { 386 | flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8198") 387 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 388 | defer cancel() 389 | 390 | controlURL, _ := startControl(t) 391 | s1, s1ip, _ := startServer(t, ctx, controlURL, "s1") 392 | s2, _, _ := startServer(t, ctx, controlURL, "s2") 393 | 394 | addr, proxyCred, _, err := s2.Loopback() 395 | if err != nil { 396 | t.Fatal(err) 397 | } 398 | 399 | ln, err := s1.Listen("tcp", ":8081") 400 | if err != nil { 401 | t.Fatal(err) 402 | } 403 | defer ln.Close() 404 | 405 | auth := &proxy.Auth{User: "tsnet", Password: proxyCred} 406 | socksDialer, err := proxy.SOCKS5("tcp", addr, auth, proxy.Direct) 407 | if err != nil { 408 | t.Fatal(err) 409 | } 410 | 411 | w, err := socksDialer.Dial("tcp", fmt.Sprintf("%s:8081", s1ip)) 412 | if err != nil { 413 | t.Fatal(err) 414 | } 415 | 416 | r, err := ln.Accept() 417 | if err != nil { 418 | t.Fatal(err) 419 | } 420 | 421 | want := "hello" 422 | if _, err := io.WriteString(w, want); err != nil { 423 | t.Fatal(err) 424 | } 425 | 426 | got := make([]byte, len(want)) 427 | if _, err := io.ReadAtLeast(r, got, len(got)); err != nil { 428 | t.Fatal(err) 429 | } 430 | t.Logf("got: %q", got) 431 | if string(got) != want { 432 | t.Errorf("got %q, want %q", got, want) 433 | } 434 | } 435 | 436 | func TestTailscaleIPs(t *testing.T) { 437 | controlURL, _ := startControl(t) 438 | 439 | tmp := t.TempDir() 440 | tmps1 := filepath.Join(tmp, "s1") 441 | os.MkdirAll(tmps1, 0755) 442 | s1 := &Server{ 443 | Dir: tmps1, 444 | ControlURL: controlURL, 445 | Hostname: "s1", 446 | Store: new(mem.Store), 447 | Ephemeral: true, 448 | } 449 | defer s1.Close() 450 | 451 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 452 | defer cancel() 453 | 454 | s1status, err := s1.Up(ctx) 455 | if err != nil { 456 | t.Fatal(err) 457 | } 458 | 459 | var upIp4, upIp6 netip.Addr 460 | for _, ip := range s1status.TailscaleIPs { 461 | if ip.Is6() { 462 | upIp6 = ip 463 | } 464 | if ip.Is4() { 465 | upIp4 = ip 466 | } 467 | } 468 | 469 | sIp4, sIp6 := s1.TailscaleIPs() 470 | if !(upIp4 == sIp4 && upIp6 == sIp6) { 471 | t.Errorf("s1.TailscaleIPs returned a different result than S1.Up, (%s, %s) != (%s, %s)", 472 | sIp4, upIp4, sIp6, upIp6) 473 | } 474 | } 475 | 476 | // TestListenerCleanup is a regression test to verify that s.Close doesn't 477 | // deadlock if a listener is still open. 478 | func TestListenerCleanup(t *testing.T) { 479 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 480 | defer cancel() 481 | 482 | controlURL, _ := startControl(t) 483 | s1, _, _ := startServer(t, ctx, controlURL, "s1") 484 | 485 | ln, err := s1.Listen("tcp", ":8081") 486 | if err != nil { 487 | t.Fatal(err) 488 | } 489 | 490 | if err := s1.Close(); err != nil { 491 | t.Fatal(err) 492 | } 493 | 494 | if err := ln.Close(); !errors.Is(err, net.ErrClosed) { 495 | t.Fatalf("second ln.Close error: %v, want net.ErrClosed", err) 496 | } 497 | } 498 | 499 | // tests https://github.com/tailscale/tailscale/issues/6973 -- that we can start a tsnet server, 500 | // stop it, and restart it, even on Windows. 501 | func TestStartStopStartGetsSameIP(t *testing.T) { 502 | controlURL, _ := startControl(t) 503 | 504 | tmp := t.TempDir() 505 | tmps1 := filepath.Join(tmp, "s1") 506 | os.MkdirAll(tmps1, 0755) 507 | 508 | newServer := func() *Server { 509 | return &Server{ 510 | Dir: tmps1, 511 | ControlURL: controlURL, 512 | Hostname: "s1", 513 | Logf: tstest.WhileTestRunningLogger(t), 514 | } 515 | } 516 | s1 := newServer() 517 | defer s1.Close() 518 | 519 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 520 | defer cancel() 521 | 522 | s1status, err := s1.Up(ctx) 523 | if err != nil { 524 | t.Fatal(err) 525 | } 526 | 527 | firstIPs := s1status.TailscaleIPs 528 | t.Logf("IPs: %v", firstIPs) 529 | 530 | if err := s1.Close(); err != nil { 531 | t.Fatalf("Close: %v", err) 532 | } 533 | 534 | s2 := newServer() 535 | defer s2.Close() 536 | 537 | s2status, err := s2.Up(ctx) 538 | if err != nil { 539 | t.Fatalf("second Up: %v", err) 540 | } 541 | 542 | secondIPs := s2status.TailscaleIPs 543 | t.Logf("IPs: %v", secondIPs) 544 | 545 | if !reflect.DeepEqual(firstIPs, secondIPs) { 546 | t.Fatalf("got %v but later %v", firstIPs, secondIPs) 547 | } 548 | } 549 | 550 | func TestFunnel(t *testing.T) { 551 | ctx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second) 552 | defer dialCancel() 553 | 554 | controlURL, _ := startControl(t) 555 | s1, _, _ := startServer(t, ctx, controlURL, "s1") 556 | s2, _, _ := startServer(t, ctx, controlURL, "s2") 557 | 558 | ln := must.Get(s1.ListenFunnel("tcp", ":443")) 559 | defer ln.Close() 560 | wantSrcAddrPort := netip.MustParseAddrPort("127.0.0.1:1234") 561 | wantTarget := ipn.HostPort("s1.tail-scale.ts.net:443") 562 | srv := &http.Server{ 563 | ConnContext: func(ctx context.Context, c net.Conn) context.Context { 564 | tc, ok := c.(*tls.Conn) 565 | if !ok { 566 | t.Errorf("ConnContext called with non-TLS conn: %T", c) 567 | } 568 | if fc, ok := tc.NetConn().(*ipn.FunnelConn); !ok { 569 | t.Errorf("ConnContext called with non-FunnelConn: %T", c) 570 | } else if fc.Src != wantSrcAddrPort { 571 | t.Errorf("ConnContext called with wrong SrcAddrPort; got %v, want %v", fc.Src, wantSrcAddrPort) 572 | } else if fc.Target != wantTarget { 573 | t.Errorf("ConnContext called with wrong Target; got %q, want %q", fc.Target, wantTarget) 574 | } 575 | return ctx 576 | }, 577 | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 578 | fmt.Fprintf(w, "hello") 579 | }), 580 | } 581 | go srv.Serve(ln) 582 | 583 | c := &http.Client{ 584 | Transport: &http.Transport{ 585 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 586 | return dialIngressConn(s2, s1, addr) 587 | }, 588 | TLSClientConfig: &tls.Config{ 589 | RootCAs: testCertRoot.Pool(), 590 | }, 591 | }, 592 | } 593 | resp, err := c.Get("https://s1.tail-scale.ts.net:443") 594 | if err != nil { 595 | t.Fatal(err) 596 | } 597 | defer resp.Body.Close() 598 | if resp.StatusCode != 200 { 599 | t.Errorf("unexpected status code: %v", resp.StatusCode) 600 | return 601 | } 602 | body, err := io.ReadAll(resp.Body) 603 | if err != nil { 604 | t.Fatal(err) 605 | } 606 | if string(body) != "hello" { 607 | t.Errorf("unexpected body: %q", body) 608 | } 609 | } 610 | 611 | func dialIngressConn(from, to *Server, target string) (net.Conn, error) { 612 | toLC := must.Get(to.LocalClient()) 613 | toStatus := must.Get(toLC.StatusWithoutPeers(context.Background())) 614 | peer6 := toStatus.Self.PeerAPIURL[1] // IPv6 615 | toPeerAPI, ok := strings.CutPrefix(peer6, "http://") 616 | if !ok { 617 | return nil, fmt.Errorf("unexpected PeerAPIURL %q", peer6) 618 | } 619 | 620 | dialCtx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second) 621 | outConn, err := from.Dial(dialCtx, "tcp", toPeerAPI) 622 | dialCancel() 623 | if err != nil { 624 | return nil, err 625 | } 626 | 627 | req, err := http.NewRequest("POST", "/v0/ingress", nil) 628 | if err != nil { 629 | return nil, err 630 | } 631 | req.Host = toPeerAPI 632 | req.Header.Set("Tailscale-Ingress-Src", "127.0.0.1:1234") 633 | req.Header.Set("Tailscale-Ingress-Target", target) 634 | if err := req.Write(outConn); err != nil { 635 | return nil, err 636 | } 637 | 638 | br := bufio.NewReader(outConn) 639 | res, err := http.ReadResponse(br, req) 640 | if err != nil { 641 | return nil, err 642 | } 643 | defer res.Body.Close() // just to appease vet 644 | if res.StatusCode != 101 { 645 | return nil, fmt.Errorf("unexpected status code: %v", res.StatusCode) 646 | } 647 | return &bufferedConn{outConn, br}, nil 648 | } 649 | 650 | type bufferedConn struct { 651 | net.Conn 652 | reader *bufio.Reader 653 | } 654 | 655 | func (c *bufferedConn) Read(b []byte) (int, error) { 656 | return c.reader.Read(b) 657 | } 658 | 659 | func TestFallbackTCPHandler(t *testing.T) { 660 | tstest.ResourceCheck(t) 661 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 662 | defer cancel() 663 | 664 | controlURL, _ := startControl(t) 665 | s1, s1ip, _ := startServer(t, ctx, controlURL, "s1") 666 | s2, _, _ := startServer(t, ctx, controlURL, "s2") 667 | 668 | lc2, err := s2.LocalClient() 669 | if err != nil { 670 | t.Fatal(err) 671 | } 672 | 673 | // ping to make sure the connection is up. 674 | res, err := lc2.Ping(ctx, s1ip, tailcfg.PingICMP) 675 | if err != nil { 676 | t.Fatal(err) 677 | } 678 | t.Logf("ping success: %#+v", res) 679 | 680 | var s1TcpConnCount atomic.Int32 681 | deregister := s1.RegisterFallbackTCPHandler(func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) { 682 | s1TcpConnCount.Add(1) 683 | return nil, false 684 | }) 685 | 686 | if _, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip)); err == nil { 687 | t.Fatal("Expected dial error because fallback handler did not intercept") 688 | } 689 | if got := s1TcpConnCount.Load(); got != 1 { 690 | t.Errorf("s1TcpConnCount = %d, want %d", got, 1) 691 | } 692 | deregister() 693 | if _, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip)); err == nil { 694 | t.Fatal("Expected dial error because nothing would intercept") 695 | } 696 | if got := s1TcpConnCount.Load(); got != 1 { 697 | t.Errorf("s1TcpConnCount = %d, want %d", got, 1) 698 | } 699 | } 700 | 701 | func TestCapturePcap(t *testing.T) { 702 | const timeLimit = 120 703 | ctx, cancel := context.WithTimeout(context.Background(), timeLimit*time.Second) 704 | defer cancel() 705 | 706 | dir := t.TempDir() 707 | s1Pcap := filepath.Join(dir, "s1.pcap") 708 | s2Pcap := filepath.Join(dir, "s2.pcap") 709 | 710 | controlURL, _ := startControl(t) 711 | s1, s1ip, _ := startServer(t, ctx, controlURL, "s1") 712 | s2, _, _ := startServer(t, ctx, controlURL, "s2") 713 | s1.CapturePcap(ctx, s1Pcap) 714 | s2.CapturePcap(ctx, s2Pcap) 715 | 716 | lc2, err := s2.LocalClient() 717 | if err != nil { 718 | t.Fatal(err) 719 | } 720 | 721 | // send a packet which both nodes will capture 722 | res, err := lc2.Ping(ctx, s1ip, tailcfg.PingICMP) 723 | if err != nil { 724 | t.Fatal(err) 725 | } 726 | t.Logf("ping success: %#+v", res) 727 | 728 | fileSize := func(name string) int64 { 729 | fi, err := os.Stat(name) 730 | if err != nil { 731 | return 0 732 | } 733 | return fi.Size() 734 | } 735 | 736 | const pcapHeaderSize = 24 737 | 738 | // there is a lag before the io.Copy writes a packet to the pcap files 739 | for range timeLimit * 10 { 740 | time.Sleep(100 * time.Millisecond) 741 | if (fileSize(s1Pcap) > pcapHeaderSize) && (fileSize(s2Pcap) > pcapHeaderSize) { 742 | break 743 | } 744 | } 745 | 746 | if got := fileSize(s1Pcap); got <= pcapHeaderSize { 747 | t.Errorf("s1 pcap file size = %d, want > pcapHeaderSize(%d)", got, pcapHeaderSize) 748 | } 749 | if got := fileSize(s2Pcap); got <= pcapHeaderSize { 750 | t.Errorf("s2 pcap file size = %d, want > pcapHeaderSize(%d)", got, pcapHeaderSize) 751 | } 752 | } 753 | 754 | func TestUDPConn(t *testing.T) { 755 | tstest.ResourceCheck(t) 756 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 757 | defer cancel() 758 | 759 | controlURL, _ := startControl(t) 760 | s1, s1ip, _ := startServer(t, ctx, controlURL, "s1") 761 | s2, s2ip, _ := startServer(t, ctx, controlURL, "s2") 762 | 763 | lc2, err := s2.LocalClient() 764 | if err != nil { 765 | t.Fatal(err) 766 | } 767 | 768 | // ping to make sure the connection is up. 769 | res, err := lc2.Ping(ctx, s1ip, tailcfg.PingICMP) 770 | if err != nil { 771 | t.Fatal(err) 772 | } 773 | t.Logf("ping success: %#+v", res) 774 | 775 | pc := must.Get(s1.ListenPacket("udp", fmt.Sprintf("%s:8081", s1ip))) 776 | defer pc.Close() 777 | 778 | // Dial to s1 from s2 779 | w, err := s2.Dial(ctx, "udp", fmt.Sprintf("%s:8081", s1ip)) 780 | if err != nil { 781 | t.Fatal(err) 782 | } 783 | defer w.Close() 784 | 785 | // Send a packet from s2 to s1 786 | want := "hello" 787 | if _, err := io.WriteString(w, want); err != nil { 788 | t.Fatal(err) 789 | } 790 | 791 | // Receive the packet on s1 792 | got := make([]byte, 1024) 793 | n, from, err := pc.ReadFrom(got) 794 | if err != nil { 795 | t.Fatal(err) 796 | } 797 | got = got[:n] 798 | t.Logf("got: %q", got) 799 | if string(got) != want { 800 | t.Errorf("got %q, want %q", got, want) 801 | } 802 | if from.(*net.UDPAddr).AddrPort().Addr() != s2ip { 803 | t.Errorf("got from %v, want %v", from, s2ip) 804 | } 805 | 806 | // Write a response back to s2 807 | if _, err := pc.WriteTo([]byte("world"), from); err != nil { 808 | t.Fatal(err) 809 | } 810 | 811 | // Receive the response on s2 812 | got = make([]byte, 1024) 813 | n, err = w.Read(got) 814 | if err != nil { 815 | t.Fatal(err) 816 | } 817 | got = got[:n] 818 | t.Logf("got: %q", got) 819 | if string(got) != "world" { 820 | t.Errorf("got %q, want world", got) 821 | } 822 | } 823 | 824 | // testWarnable is a Warnable that is used within this package for testing purposes only. 825 | var testWarnable = health.Register(&health.Warnable{ 826 | Code: "test-warnable-tsnet", 827 | Title: "Test warnable", 828 | Severity: health.SeverityLow, 829 | Text: func(args health.Args) string { 830 | return args[health.ArgError] 831 | }, 832 | }) 833 | 834 | func parseMetrics(m []byte) (map[string]float64, error) { 835 | metrics := make(map[string]float64) 836 | 837 | var parser expfmt.TextParser 838 | mf, err := parser.TextToMetricFamilies(bytes.NewReader(m)) 839 | if err != nil { 840 | return nil, err 841 | } 842 | 843 | for _, f := range mf { 844 | for _, ff := range f.Metric { 845 | val := float64(0) 846 | 847 | switch f.GetType() { 848 | case dto.MetricType_COUNTER: 849 | val = ff.GetCounter().GetValue() 850 | case dto.MetricType_GAUGE: 851 | val = ff.GetGauge().GetValue() 852 | } 853 | 854 | metrics[f.GetName()+promMetricLabelsStr(ff.GetLabel())] = val 855 | } 856 | } 857 | 858 | return metrics, nil 859 | } 860 | 861 | func promMetricLabelsStr(labels []*dto.LabelPair) string { 862 | if len(labels) == 0 { 863 | return "" 864 | } 865 | var b strings.Builder 866 | b.WriteString("{") 867 | for i, l := range labels { 868 | if i > 0 { 869 | b.WriteString(",") 870 | } 871 | b.WriteString(fmt.Sprintf("%s=%q", l.GetName(), l.GetValue())) 872 | } 873 | b.WriteString("}") 874 | return b.String() 875 | } 876 | 877 | func TestUserMetrics(t *testing.T) { 878 | flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/13420") 879 | tstest.ResourceCheck(t) 880 | ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) 881 | defer cancel() 882 | 883 | controlURL, c := startControl(t) 884 | s1, s1ip, s1PubKey := startServer(t, ctx, controlURL, "s1") 885 | s2, _, _ := startServer(t, ctx, controlURL, "s2") 886 | 887 | s1.lb.EditPrefs(&ipn.MaskedPrefs{ 888 | Prefs: ipn.Prefs{ 889 | AdvertiseRoutes: []netip.Prefix{ 890 | netip.MustParsePrefix("192.0.2.0/24"), 891 | netip.MustParsePrefix("192.0.3.0/24"), 892 | netip.MustParsePrefix("192.0.5.1/32"), 893 | netip.MustParsePrefix("0.0.0.0/0"), 894 | }, 895 | }, 896 | AdvertiseRoutesSet: true, 897 | }) 898 | c.SetSubnetRoutes(s1PubKey, []netip.Prefix{ 899 | netip.MustParsePrefix("192.0.2.0/24"), 900 | netip.MustParsePrefix("192.0.5.1/32"), 901 | netip.MustParsePrefix("0.0.0.0/0"), 902 | }) 903 | 904 | lc1, err := s1.LocalClient() 905 | if err != nil { 906 | t.Fatal(err) 907 | } 908 | 909 | lc2, err := s2.LocalClient() 910 | if err != nil { 911 | t.Fatal(err) 912 | } 913 | 914 | // ping to make sure the connection is up. 915 | res, err := lc2.Ping(ctx, s1ip, tailcfg.PingICMP) 916 | if err != nil { 917 | t.Fatalf("pinging: %s", err) 918 | } 919 | t.Logf("ping success: %#+v", res) 920 | 921 | ht := s1.lb.HealthTracker() 922 | ht.SetUnhealthy(testWarnable, health.Args{"Text": "Hello world 1"}) 923 | 924 | // Force an update to the netmap to ensure that the metrics are up-to-date. 925 | s1.lb.DebugForceNetmapUpdate() 926 | s2.lb.DebugForceNetmapUpdate() 927 | 928 | wantRoutes := float64(2) 929 | if runtime.GOOS == "windows" { 930 | wantRoutes = 0 931 | } 932 | 933 | // Wait for the routes to be propagated to node 1 to ensure 934 | // that the metrics are up-to-date. 935 | waitForCondition(t, "primary routes available for node1", 90*time.Second, func() bool { 936 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 937 | defer cancel() 938 | status1, err := lc1.Status(ctx) 939 | if err != nil { 940 | t.Logf("getting status: %s", err) 941 | return false 942 | } 943 | if runtime.GOOS == "windows" { 944 | // Windows does not seem to support or report back routes when running in 945 | // userspace via tsnet. So, we skip this check on Windows. 946 | // TODO(kradalby): Figure out if this is correct. 947 | return true 948 | } 949 | // Wait for the primary routes to reach our desired routes, which is wantRoutes + 1, because 950 | // the PrimaryRoutes list will contain a exit node route, which the metric does not count. 951 | return status1.Self.PrimaryRoutes != nil && status1.Self.PrimaryRoutes.Len() == int(wantRoutes)+1 952 | }) 953 | 954 | ctxLc, cancelLc := context.WithTimeout(context.Background(), 5*time.Second) 955 | defer cancelLc() 956 | metrics1, err := lc1.UserMetrics(ctxLc) 957 | if err != nil { 958 | t.Fatal(err) 959 | } 960 | 961 | status1, err := lc1.Status(ctxLc) 962 | if err != nil { 963 | t.Fatal(err) 964 | } 965 | 966 | parsedMetrics1, err := parseMetrics(metrics1) 967 | if err != nil { 968 | t.Fatal(err) 969 | } 970 | 971 | t.Logf("Metrics1:\n%s\n", metrics1) 972 | 973 | // The node is advertising 4 routes: 974 | // - 192.0.2.0/24 975 | // - 192.0.3.0/24 976 | // - 192.0.5.1/32 977 | if got, want := parsedMetrics1["tailscaled_advertised_routes"], 3.0; got != want { 978 | t.Errorf("metrics1, tailscaled_advertised_routes: got %v, want %v", got, want) 979 | } 980 | 981 | // The control has approved 2 routes: 982 | // - 192.0.2.0/24 983 | // - 192.0.5.1/32 984 | if got, want := parsedMetrics1["tailscaled_approved_routes"], wantRoutes; got != want { 985 | t.Errorf("metrics1, tailscaled_approved_routes: got %v, want %v", got, want) 986 | } 987 | 988 | // Validate the health counter metric against the status of the node 989 | if got, want := parsedMetrics1[`tailscaled_health_messages{type="warning"}`], float64(len(status1.Health)); got != want { 990 | t.Errorf("metrics1, tailscaled_health_messages: got %v, want %v", got, want) 991 | } 992 | 993 | // The node is the primary subnet router for 2 routes: 994 | // - 192.0.2.0/24 995 | // - 192.0.5.1/32 996 | if got, want := parsedMetrics1["tailscaled_primary_routes"], wantRoutes; got != want { 997 | t.Errorf("metrics1, tailscaled_primary_routes: got %v, want %v", got, want) 998 | } 999 | 1000 | metrics2, err := lc2.UserMetrics(ctx) 1001 | if err != nil { 1002 | t.Fatal(err) 1003 | } 1004 | 1005 | status2, err := lc2.Status(ctx) 1006 | if err != nil { 1007 | t.Fatal(err) 1008 | } 1009 | 1010 | parsedMetrics2, err := parseMetrics(metrics2) 1011 | if err != nil { 1012 | t.Fatal(err) 1013 | } 1014 | 1015 | t.Logf("Metrics2:\n%s\n", metrics2) 1016 | 1017 | // The node is advertising 0 routes 1018 | if got, want := parsedMetrics2["tailscaled_advertised_routes"], 0.0; got != want { 1019 | t.Errorf("metrics2, tailscaled_advertised_routes: got %v, want %v", got, want) 1020 | } 1021 | 1022 | // The control has approved 0 routes 1023 | if got, want := parsedMetrics2["tailscaled_approved_routes"], 0.0; got != want { 1024 | t.Errorf("metrics2, tailscaled_approved_routes: got %v, want %v", got, want) 1025 | } 1026 | 1027 | // Validate the health counter metric against the status of the node 1028 | if got, want := parsedMetrics2[`tailscaled_health_messages{type="warning"}`], float64(len(status2.Health)); got != want { 1029 | t.Errorf("metrics2, tailscaled_health_messages: got %v, want %v", got, want) 1030 | } 1031 | 1032 | // The node is the primary subnet router for 0 routes 1033 | if got, want := parsedMetrics2["tailscaled_primary_routes"], 0.0; got != want { 1034 | t.Errorf("metrics2, tailscaled_primary_routes: got %v, want %v", got, want) 1035 | } 1036 | } 1037 | 1038 | func waitForCondition(t *testing.T, msg string, waitTime time.Duration, f func() bool) { 1039 | t.Helper() 1040 | for deadline := time.Now().Add(waitTime); time.Now().Before(deadline); time.Sleep(1 * time.Second) { 1041 | if f() { 1042 | return 1043 | } 1044 | } 1045 | t.Fatalf("waiting for condition: %s", msg) 1046 | } 1047 | -------------------------------------------------------------------------------- /internal/tsnet/tsnet.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tailscale Inc & AUTHORS 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | // Package tsnet provides Tailscale as a library. 5 | package tsnet 6 | 7 | import ( 8 | "context" 9 | crand "crypto/rand" 10 | "crypto/tls" 11 | "encoding/hex" 12 | "errors" 13 | "fmt" 14 | "io" 15 | "log" 16 | "math" 17 | "net" 18 | "net/http" 19 | "net/netip" 20 | "os" 21 | "path/filepath" 22 | "runtime" 23 | "slices" 24 | "strconv" 25 | "strings" 26 | "sync" 27 | "time" 28 | 29 | "tailscale.com/client/tailscale" 30 | "tailscale.com/control/controlclient" 31 | "tailscale.com/envknob" 32 | "tailscale.com/health" 33 | "tailscale.com/hostinfo" 34 | "tailscale.com/ipn" 35 | "tailscale.com/ipn/ipnlocal" 36 | "tailscale.com/ipn/ipnstate" 37 | "tailscale.com/ipn/localapi" 38 | "tailscale.com/ipn/store" 39 | "tailscale.com/ipn/store/mem" 40 | "tailscale.com/logpolicy" 41 | "tailscale.com/logtail" 42 | "tailscale.com/logtail/filch" 43 | "tailscale.com/net/memnet" 44 | "tailscale.com/net/netmon" 45 | "tailscale.com/net/proxymux" 46 | "tailscale.com/net/socks5" 47 | "tailscale.com/net/tsdial" 48 | "tailscale.com/tsd" 49 | "tailscale.com/types/logger" 50 | "tailscale.com/types/logid" 51 | "tailscale.com/types/nettype" 52 | "tailscale.com/util/clientmetric" 53 | "tailscale.com/util/mak" 54 | "tailscale.com/util/set" 55 | "tailscale.com/util/testenv" 56 | "tailscale.com/wgengine" 57 | "tailscale.com/wgengine/netstack" 58 | ) 59 | 60 | // Server is an embedded Tailscale server. 61 | // 62 | // Its exported fields may be changed until the first method call. 63 | type Server struct { 64 | // Dir specifies the name of the directory to use for 65 | // state. If empty, a directory is selected automatically 66 | // under os.UserConfigDir (https://golang.org/pkg/os/#UserConfigDir). 67 | // based on the name of the binary. 68 | // 69 | // If you want to use multiple tsnet services in the same 70 | // binary, you will need to make sure that Dir is set uniquely 71 | // for each service. A good pattern for this is to have a 72 | // "base" directory (such as your mutable storage folder) and 73 | // then append the hostname on the end of it. 74 | Dir string 75 | 76 | // Store specifies the state store to use. 77 | // 78 | // If nil, a new FileStore is initialized at `Dir/tailscaled.state`. 79 | // See tailscale.com/ipn/store for supported stores. 80 | // 81 | // Logs will automatically be uploaded to log.tailscale.io, 82 | // where the configuration file for logging will be saved at 83 | // `Dir/tailscaled.log.conf`. 84 | Store ipn.StateStore 85 | 86 | // Hostname is the hostname to present to the control server. 87 | // If empty, the binary name is used. 88 | Hostname string 89 | 90 | // UserLogf, if non-nil, specifies the logger to use for logs generated by 91 | // the Server itself intended to be seen by the user such as the AuthURL for 92 | // login and status updates. If unset, log.Printf is used. 93 | UserLogf logger.Logf 94 | 95 | // Logf, if set is used for logs generated by the backend such as the 96 | // LocalBackend and MagicSock. It is verbose and intended for debugging. 97 | // If unset, logs are discarded. 98 | Logf logger.Logf 99 | 100 | // Ephemeral, if true, specifies that the instance should register 101 | // as an Ephemeral node (https://tailscale.com/s/ephemeral-nodes). 102 | Ephemeral bool 103 | 104 | // AuthKey, if non-empty, is the auth key to create the node 105 | // and will be preferred over the TS_AUTHKEY environment 106 | // variable. If the node is already created (from state 107 | // previously stored in Store), then this field is not 108 | // used. 109 | AuthKey string 110 | 111 | // AdvertiseTags are the Tailscale tags this node should advertise. 112 | AdvertiseTags []string 113 | 114 | // ControlURL optionally specifies the coordination server URL. 115 | // If empty, the Tailscale default is used. 116 | ControlURL string 117 | 118 | // RunWebClient, if true, runs a client for managing this node over 119 | // its Tailscale interface on port 5252. 120 | RunWebClient bool 121 | 122 | // Port is the UDP port to listen on for WireGuard and peer-to-peer 123 | // traffic. If zero, a port is automatically selected. Leave this 124 | // field at zero unless you know what you are doing. 125 | Port uint16 126 | 127 | getCertForTesting func(*tls.ClientHelloInfo) (*tls.Certificate, error) 128 | 129 | initOnce sync.Once 130 | initErr error 131 | lb *ipnlocal.LocalBackend 132 | netstack *netstack.Impl 133 | netMon *netmon.Monitor 134 | rootPath string // the state directory 135 | hostname string 136 | shutdownCtx context.Context 137 | shutdownCancel context.CancelFunc 138 | proxyCred string // SOCKS5 proxy auth for loopbackListener 139 | localAPICred string // basic auth password for loopbackListener 140 | loopbackListener net.Listener // optional loopback for localapi and proxies 141 | localAPIListener net.Listener // in-memory, used by localClient 142 | localClient *tailscale.LocalClient // in-memory 143 | localAPIServer *http.Server 144 | logbuffer *filch.Filch 145 | logtail *logtail.Logger 146 | logid logid.PublicID 147 | 148 | mu sync.Mutex 149 | listeners map[listenKey]*listener 150 | fallbackTCPHandlers set.HandleSet[FallbackTCPHandler] 151 | dialer *tsdial.Dialer 152 | closed bool 153 | } 154 | 155 | // FallbackTCPHandler describes the callback which 156 | // conditionally handles an incoming TCP flow for the 157 | // provided (src/port, dst/port) 4-tuple. These are registered 158 | // as handlers of last resort, and are called only if no 159 | // listener could handle the incoming flow. 160 | // 161 | // If the callback returns intercept=false, the flow is rejected. 162 | // 163 | // When intercept=true, the behavior depends on whether the returned handler 164 | // is non-nil: if nil, the connection is rejected. If non-nil, handler takes 165 | // over the TCP conn. 166 | type FallbackTCPHandler func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) 167 | 168 | // Dial connects to the address on the tailnet. 169 | // It will start the server if it has not been started yet. 170 | func (s *Server) Dial(ctx context.Context, network, address string) (net.Conn, error) { 171 | if err := s.Start(); err != nil { 172 | return nil, err 173 | } 174 | return s.dialer.UserDial(ctx, network, address) 175 | } 176 | 177 | // HTTPClient returns an HTTP client that is configured to connect over Tailscale. 178 | // 179 | // This is useful if you need to have your tsnet services connect to other devices on 180 | // your tailnet. 181 | func (s *Server) HTTPClient() *http.Client { 182 | return &http.Client{ 183 | Transport: &http.Transport{ 184 | DialContext: s.Dial, 185 | }, 186 | } 187 | } 188 | 189 | // LocalClient returns a LocalClient that speaks to s. 190 | // 191 | // It will start the server if it has not been started yet. If the server's 192 | // already been started successfully, it doesn't return an error. 193 | func (s *Server) LocalClient() (*tailscale.LocalClient, error) { 194 | if err := s.Start(); err != nil { 195 | return nil, err 196 | } 197 | return s.localClient, nil 198 | } 199 | 200 | // Loopback starts a routing server on a loopback address. 201 | // 202 | // The server has multiple functions. 203 | // 204 | // It can be used as a SOCKS5 proxy onto the tailnet. 205 | // Authentication is required with the username "tsnet" and 206 | // the value of proxyCred used as the password. 207 | // 208 | // The HTTP server also serves out the "LocalAPI" on /localapi. 209 | // As the LocalAPI is powerful, access to endpoints requires BOTH passing a 210 | // "Sec-Tailscale: localapi" HTTP header and passing localAPICred as basic auth. 211 | // 212 | // If you only need to use the LocalAPI from Go, then prefer LocalClient 213 | // as it does not require communication via TCP. 214 | func (s *Server) Loopback() (addr string, proxyCred, localAPICred string, err error) { 215 | if err := s.Start(); err != nil { 216 | return "", "", "", err 217 | } 218 | 219 | if s.loopbackListener == nil { 220 | var proxyCred [16]byte 221 | if _, err := crand.Read(proxyCred[:]); err != nil { 222 | return "", "", "", err 223 | } 224 | s.proxyCred = hex.EncodeToString(proxyCred[:]) 225 | 226 | var cred [16]byte 227 | if _, err := crand.Read(cred[:]); err != nil { 228 | return "", "", "", err 229 | } 230 | s.localAPICred = hex.EncodeToString(cred[:]) 231 | 232 | ln, err := net.Listen("tcp", "127.0.0.1:0") 233 | if err != nil { 234 | return "", "", "", err 235 | } 236 | s.loopbackListener = ln 237 | 238 | socksLn, httpLn := proxymux.SplitSOCKSAndHTTP(ln) 239 | 240 | // TODO: add HTTP proxy support. Probably requires factoring 241 | // out the CONNECT code from tailscaled/proxy.go that uses 242 | // httputil.ReverseProxy and adding auth support. 243 | go func() { 244 | lah := localapi.NewHandler(s.lb, s.logf, s.logid) 245 | lah.PermitWrite = true 246 | lah.PermitRead = true 247 | lah.RequiredPassword = s.localAPICred 248 | h := &localSecHandler{h: lah, cred: s.localAPICred} 249 | 250 | if err := http.Serve(httpLn, h); err != nil { 251 | s.logf("localapi tcp serve error: %v", err) 252 | } 253 | }() 254 | s5l := logger.WithPrefix(s.logf, "socks5: ") 255 | s5s := &socks5.Server{ 256 | Logf: s5l, 257 | Dialer: s.dialer.UserDial, 258 | Username: "tsnet", 259 | Password: s.proxyCred, 260 | } 261 | go func() { 262 | s5l("SOCKS5 server exited: %v", s5s.Serve(socksLn)) 263 | }() 264 | } 265 | 266 | lbAddr := s.loopbackListener.Addr() 267 | if lbAddr == nil { 268 | // https://github.com/tailscale/tailscale/issues/7488 269 | panic("loopbackListener has no Addr") 270 | } 271 | return lbAddr.String(), s.proxyCred, s.localAPICred, nil 272 | } 273 | 274 | type localSecHandler struct { 275 | h http.Handler 276 | cred string 277 | } 278 | 279 | func (h *localSecHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 280 | if r.Header.Get("Sec-Tailscale") != "localapi" { 281 | w.WriteHeader(403) 282 | io.WriteString(w, "missing 'Sec-Tailscale: localapi' header") 283 | return 284 | } 285 | h.h.ServeHTTP(w, r) 286 | } 287 | 288 | // Start connects the server to the tailnet. 289 | // Optional: any calls to Dial/Listen will also call Start. 290 | func (s *Server) Start() error { 291 | hostinfo.SetPackage("tsnet") 292 | s.initOnce.Do(s.doInit) 293 | return s.initErr 294 | } 295 | 296 | // Up connects the server to the tailnet and waits until it is running. 297 | // On success it returns the current status, including a Tailscale IP address. 298 | func (s *Server) Up(ctx context.Context) (*ipnstate.Status, error) { 299 | lc, err := s.LocalClient() // calls Start 300 | if err != nil { 301 | return nil, fmt.Errorf("tsnet.Up: %w", err) 302 | } 303 | 304 | watcher, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialState|ipn.NotifyNoPrivateKeys) 305 | if err != nil { 306 | return nil, fmt.Errorf("tsnet.Up: %w", err) 307 | } 308 | defer watcher.Close() 309 | 310 | for { 311 | n, err := watcher.Next() 312 | if err != nil { 313 | return nil, fmt.Errorf("tsnet.Up: %w", err) 314 | } 315 | if n.ErrMessage != nil { 316 | return nil, fmt.Errorf("tsnet.Up: backend: %s", *n.ErrMessage) 317 | } 318 | if s := n.State; s != nil { 319 | if *s == ipn.Running { 320 | status, err := lc.Status(ctx) 321 | if err != nil { 322 | return nil, fmt.Errorf("tsnet.Up: %w", err) 323 | } 324 | if len(status.TailscaleIPs) == 0 { 325 | return nil, errors.New("tsnet.Up: running, but no ip") 326 | } 327 | 328 | // Clear the persisted serve config state to prevent stale configuration 329 | // from code changes. This is a temporary workaround until we have a better 330 | // way to handle this. (2023-03-11) 331 | if err := lc.SetServeConfig(ctx, new(ipn.ServeConfig)); err != nil { 332 | return nil, fmt.Errorf("tsnet.Up: %w", err) 333 | } 334 | 335 | return status, nil 336 | } 337 | // TODO: in the future, return an error on ipn.NeedsLogin 338 | // and ipn.NeedsMachineAuth to improve the UX of trying 339 | // out the tsnet package. 340 | // 341 | // Unfortunately today, even when using an AuthKey we 342 | // briefly see these states. It would be nice to fix. 343 | } 344 | } 345 | } 346 | 347 | // Close stops the server. 348 | // 349 | // It must not be called before or concurrently with Start. 350 | func (s *Server) Close() error { 351 | s.mu.Lock() 352 | defer s.mu.Unlock() 353 | if s.closed { 354 | return fmt.Errorf("tsnet: %w", net.ErrClosed) 355 | } 356 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 357 | defer cancel() 358 | var wg sync.WaitGroup 359 | wg.Add(1) 360 | go func() { 361 | defer wg.Done() 362 | // Perform a best-effort final flush. 363 | if s.logtail != nil { 364 | s.logtail.Shutdown(ctx) 365 | } 366 | if s.logbuffer != nil { 367 | s.logbuffer.Close() 368 | } 369 | }() 370 | wg.Add(1) 371 | go func() { 372 | defer wg.Done() 373 | if s.localAPIServer != nil { 374 | s.localAPIServer.Shutdown(ctx) 375 | } 376 | }() 377 | 378 | if s.netstack != nil { 379 | s.netstack.Close() 380 | s.netstack = nil 381 | } 382 | if s.shutdownCancel != nil { 383 | s.shutdownCancel() 384 | } 385 | if s.lb != nil { 386 | s.lb.Shutdown() 387 | } 388 | if s.netMon != nil { 389 | s.netMon.Close() 390 | } 391 | if s.dialer != nil { 392 | s.dialer.Close() 393 | } 394 | if s.localAPIListener != nil { 395 | s.localAPIListener.Close() 396 | } 397 | if s.loopbackListener != nil { 398 | s.loopbackListener.Close() 399 | } 400 | 401 | for _, ln := range s.listeners { 402 | ln.closeLocked() 403 | } 404 | 405 | wg.Wait() 406 | s.closed = true 407 | return nil 408 | } 409 | 410 | func (s *Server) doInit() { 411 | s.shutdownCtx, s.shutdownCancel = context.WithCancel(context.Background()) 412 | if err := s.start(); err != nil { 413 | s.initErr = fmt.Errorf("tsnet: %w", err) 414 | } 415 | } 416 | 417 | // CertDomains returns the list of domains for which the server can 418 | // provide TLS certificates. These are also the DNS names for the 419 | // Server. 420 | // If the server is not running, it returns nil. 421 | func (s *Server) CertDomains() []string { 422 | nm := s.lb.NetMap() 423 | if nm == nil { 424 | return nil 425 | } 426 | return slices.Clone(nm.DNS.CertDomains) 427 | } 428 | 429 | // TailscaleIPs returns IPv4 and IPv6 addresses for this node. If the node 430 | // has not yet joined a tailnet or is otherwise unaware of its own IP addresses, 431 | // the returned ip4, ip6 will be !netip.IsValid(). 432 | func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) { 433 | nm := s.lb.NetMap() 434 | if nm == nil { 435 | return 436 | } 437 | addrs := nm.GetAddresses() 438 | for i := range addrs.Len() { 439 | addr := addrs.At(i) 440 | ip := addr.Addr() 441 | if ip.Is6() { 442 | ip6 = ip 443 | } 444 | if ip.Is4() { 445 | ip4 = ip 446 | } 447 | } 448 | 449 | return ip4, ip6 450 | } 451 | 452 | func (s *Server) getAuthKey() string { 453 | if v := s.AuthKey; v != "" { 454 | return v 455 | } 456 | if v := os.Getenv("TS_AUTHKEY"); v != "" { 457 | return v 458 | } 459 | return os.Getenv("TS_AUTH_KEY") 460 | } 461 | 462 | func (s *Server) start() (reterr error) { 463 | var closePool closeOnErrorPool 464 | defer closePool.closeAllIfError(&reterr) 465 | 466 | exe, err := os.Executable() 467 | if err != nil { 468 | switch runtime.GOOS { 469 | case "js", "wasip1": 470 | // These platforms don't implement os.Executable (at least as of Go 471 | // 1.21), but we don't really care much: it's only used as a default 472 | // directory and hostname when they're not supplied. But we can fall 473 | // back to "tsnet" as well. 474 | exe = "tsnet" 475 | default: 476 | return err 477 | } 478 | } 479 | prog := strings.TrimSuffix(strings.ToLower(filepath.Base(exe)), ".exe") 480 | 481 | s.hostname = s.Hostname 482 | if s.hostname == "" { 483 | s.hostname = prog 484 | } 485 | 486 | s.rootPath = s.Dir 487 | if s.Store != nil { 488 | _, isMemStore := s.Store.(*mem.Store) 489 | if isMemStore && !s.Ephemeral { 490 | return fmt.Errorf("in-memory store is only supported for Ephemeral nodes") 491 | } 492 | } 493 | 494 | if s.rootPath == "" { 495 | confDir, err := os.UserConfigDir() 496 | if err != nil { 497 | return err 498 | } 499 | s.rootPath, err = getTSNetDir(s.logf, confDir, prog) 500 | if err != nil { 501 | return err 502 | } 503 | } 504 | if err := os.MkdirAll(s.rootPath, 0700); err != nil { 505 | return err 506 | } 507 | if fi, err := os.Stat(s.rootPath); err != nil { 508 | return err 509 | } else if !fi.IsDir() { 510 | return fmt.Errorf("%v is not a directory", s.rootPath) 511 | } 512 | 513 | tsLogf := func(format string, a ...any) { 514 | if s.logtail != nil { 515 | s.logtail.Logf(format, a...) 516 | } 517 | if s.Logf == nil { 518 | return 519 | } 520 | s.Logf(format, a...) 521 | } 522 | 523 | sys := new(tsd.System) 524 | if err := s.startLogger(&closePool, sys.HealthTracker(), tsLogf); err != nil { 525 | return err 526 | } 527 | 528 | s.netMon, err = netmon.New(tsLogf) 529 | if err != nil { 530 | return err 531 | } 532 | closePool.add(s.netMon) 533 | 534 | s.dialer = &tsdial.Dialer{Logf: tsLogf} // mutated below (before used) 535 | eng, err := wgengine.NewUserspaceEngine(tsLogf, wgengine.Config{ 536 | ListenPort: s.Port, 537 | NetMon: s.netMon, 538 | Dialer: s.dialer, 539 | SetSubsystem: sys.Set, 540 | ControlKnobs: sys.ControlKnobs(), 541 | HealthTracker: sys.HealthTracker(), 542 | Metrics: sys.UserMetricsRegistry(), 543 | }) 544 | if err != nil { 545 | return err 546 | } 547 | closePool.add(s.dialer) 548 | sys.Set(eng) 549 | sys.HealthTracker().SetMetricsRegistry(sys.UserMetricsRegistry()) 550 | 551 | // TODO(oxtoacart): do we need to support Taildrive on tsnet, and if so, how? 552 | ns, err := netstack.Create(tsLogf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil) 553 | if err != nil { 554 | return fmt.Errorf("netstack.Create: %w", err) 555 | } 556 | sys.Tun.Get().Start() 557 | sys.Set(ns) 558 | ns.ProcessLocalIPs = true 559 | ns.ProcessSubnets = true 560 | ns.GetTCPHandlerForFlow = s.getTCPHandlerForFlow 561 | ns.GetUDPHandlerForFlow = s.getUDPHandlerForFlow 562 | s.netstack = ns 563 | s.dialer.UseNetstackForIP = func(ip netip.Addr) bool { 564 | _, ok := eng.PeerForIP(ip) 565 | return ok 566 | } 567 | s.dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) { 568 | // Note: don't just return ns.DialContextTCP or we'll return 569 | // *gonet.TCPConn(nil) instead of a nil interface which trips up 570 | // callers. 571 | tcpConn, err := ns.DialContextTCP(ctx, dst) 572 | if err != nil { 573 | return nil, err 574 | } 575 | return tcpConn, nil 576 | } 577 | s.dialer.NetstackDialUDP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) { 578 | // Note: don't just return ns.DialContextUDP or we'll return 579 | // *gonet.UDPConn(nil) instead of a nil interface which trips up 580 | // callers. 581 | udpConn, err := ns.DialContextUDP(ctx, dst) 582 | if err != nil { 583 | return nil, err 584 | } 585 | return udpConn, nil 586 | } 587 | 588 | if s.Store == nil { 589 | stateFile := filepath.Join(s.rootPath, "tailscaled.state") 590 | s.logf("tsnet running state path %s", stateFile) 591 | s.Store, err = store.New(tsLogf, stateFile) 592 | if err != nil { 593 | return err 594 | } 595 | } 596 | sys.Set(s.Store) 597 | 598 | loginFlags := controlclient.LoginDefault 599 | if s.Ephemeral { 600 | loginFlags = controlclient.LoginEphemeral 601 | } 602 | lb, err := ipnlocal.NewLocalBackend(tsLogf, s.logid, sys, loginFlags|controlclient.LocalBackendStartKeyOSNeutral) 603 | if err != nil { 604 | return fmt.Errorf("NewLocalBackend: %v", err) 605 | } 606 | lb.SetTCPHandlerForFunnelFlow(s.getTCPHandlerForFunnelFlow) 607 | lb.SetVarRoot(s.rootPath) 608 | s.logf("tsnet starting with hostname %q, varRoot %q", s.hostname, s.rootPath) 609 | s.lb = lb 610 | if err := ns.Start(lb); err != nil { 611 | return fmt.Errorf("failed to start netstack: %w", err) 612 | } 613 | closePool.addFunc(func() { s.lb.Shutdown() }) 614 | prefs := ipn.NewPrefs() 615 | prefs.Hostname = s.hostname 616 | prefs.WantRunning = true 617 | prefs.ControlURL = s.ControlURL 618 | prefs.AdvertiseTags = s.AdvertiseTags 619 | prefs.RunWebClient = s.RunWebClient 620 | authKey := s.getAuthKey() 621 | err = lb.Start(ipn.Options{ 622 | UpdatePrefs: prefs, 623 | AuthKey: authKey, 624 | }) 625 | if err != nil { 626 | return fmt.Errorf("starting backend: %w", err) 627 | } 628 | st := lb.State() 629 | if st == ipn.NeedsLogin || envknob.Bool("TSNET_FORCE_LOGIN") { 630 | s.logf("LocalBackend state is %v; running StartLoginInteractive...", st) 631 | if err := s.lb.StartLoginInteractive(s.shutdownCtx); err != nil { 632 | return fmt.Errorf("StartLoginInteractive: %w", err) 633 | } 634 | } else if authKey != "" { 635 | s.logf("Authkey is set; but state is %v. Ignoring authkey. Re-run with TSNET_FORCE_LOGIN=1 to force use of authkey.", st) 636 | } 637 | go s.printAuthURLLoop() 638 | 639 | // Run the localapi handler, to allow fetching LetsEncrypt certs. 640 | lah := localapi.NewHandler(lb, tsLogf, s.logid) 641 | lah.PermitWrite = true 642 | lah.PermitRead = true 643 | 644 | // Create an in-process listener. 645 | // nettest.Listen provides a in-memory pipe based implementation for net.Conn. 646 | lal := memnet.Listen("local-tailscaled.sock:80") 647 | s.localAPIListener = lal 648 | s.localClient = &tailscale.LocalClient{Dial: lal.Dial} 649 | s.localAPIServer = &http.Server{Handler: lah} 650 | s.lb.ConfigureWebClient(s.localClient) 651 | go func() { 652 | if err := s.localAPIServer.Serve(lal); err != nil && err != http.ErrServerClosed { 653 | s.logf("localapi serve error: %v", err) 654 | } 655 | }() 656 | closePool.add(s.localAPIListener) 657 | return nil 658 | } 659 | 660 | func (s *Server) startLogger(closePool *closeOnErrorPool, health *health.Tracker, tsLogf logger.Logf) error { 661 | if testenv.InTest() { 662 | return nil 663 | } 664 | cfgPath := filepath.Join(s.rootPath, "tailscaled.log.conf") 665 | lpc, err := logpolicy.ConfigFromFile(cfgPath) 666 | switch { 667 | case os.IsNotExist(err): 668 | lpc = logpolicy.NewConfig(logtail.CollectionNode) 669 | if err := lpc.Save(cfgPath); err != nil { 670 | return fmt.Errorf("logpolicy.Config.Save for %v: %w", cfgPath, err) 671 | } 672 | case err != nil: 673 | return fmt.Errorf("logpolicy.LoadConfig for %v: %w", cfgPath, err) 674 | } 675 | if err := lpc.Validate(logtail.CollectionNode); err != nil { 676 | return fmt.Errorf("logpolicy.Config.Validate for %v: %w", cfgPath, err) 677 | } 678 | s.logid = lpc.PublicID 679 | 680 | s.logbuffer, err = filch.New(filepath.Join(s.rootPath, "tailscaled"), filch.Options{ReplaceStderr: false}) 681 | if err != nil { 682 | return fmt.Errorf("error creating filch: %w", err) 683 | } 684 | closePool.add(s.logbuffer) 685 | c := logtail.Config{ 686 | Collection: lpc.Collection, 687 | PrivateID: lpc.PrivateID, 688 | Stderr: io.Discard, // log everything to Buffer 689 | Buffer: s.logbuffer, 690 | CompressLogs: true, 691 | HTTPC: &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, s.netMon, health, tsLogf)}, 692 | MetricsDelta: clientmetric.EncodeLogTailMetricsDelta, 693 | } 694 | s.logtail = logtail.NewLogger(c, tsLogf) 695 | closePool.addFunc(func() { s.logtail.Shutdown(context.Background()) }) 696 | return nil 697 | } 698 | 699 | type closeOnErrorPool []func() 700 | 701 | func (p *closeOnErrorPool) add(c io.Closer) { *p = append(*p, func() { c.Close() }) } 702 | func (p *closeOnErrorPool) addFunc(fn func()) { *p = append(*p, fn) } 703 | func (p closeOnErrorPool) closeAllIfError(errp *error) { 704 | if *errp != nil { 705 | for _, closeFn := range p { 706 | closeFn() 707 | } 708 | } 709 | } 710 | 711 | func (s *Server) logf(format string, a ...any) { 712 | if s.logtail != nil { 713 | s.logtail.Logf(format, a...) 714 | } 715 | if s.UserLogf != nil { 716 | s.UserLogf(format, a...) 717 | return 718 | } 719 | log.Printf(format, a...) 720 | } 721 | 722 | // printAuthURLLoop loops once every few seconds while the server is still running and 723 | // is in NeedsLogin state, printing out the auth URL. 724 | func (s *Server) printAuthURLLoop() { 725 | for { 726 | if s.shutdownCtx.Err() != nil { 727 | return 728 | } 729 | if st := s.lb.State(); st != ipn.NeedsLogin && st != ipn.NoState { 730 | s.logf("AuthLoop: state is %v; done", st) 731 | return 732 | } 733 | st := s.lb.StatusWithoutPeers() 734 | if st.AuthURL != "" { 735 | s.logf("To start this tsnet server, restart with TS_AUTHKEY set, or go to: %s", st.AuthURL) 736 | } 737 | select { 738 | case <-time.After(5 * time.Second): 739 | case <-s.shutdownCtx.Done(): 740 | return 741 | } 742 | } 743 | } 744 | 745 | // networkForFamily returns one of "tcp4", "tcp6", "udp4", or "udp6". 746 | // 747 | // netBase is "tcp" or "udp" (without any '4' or '6' suffix). 748 | func networkForFamily(netBase string, is6 bool) string { 749 | switch netBase { 750 | case "tcp": 751 | if is6 { 752 | return "tcp6" 753 | } 754 | return "tcp4" 755 | case "udp": 756 | if is6 { 757 | return "udp6" 758 | } 759 | return "udp4" 760 | } 761 | panic("unexpected") 762 | } 763 | 764 | // listenerForDstAddr returns a listener for the provided network and 765 | // destination IP/port. It matches from most specific to least specific. 766 | // For example: 767 | // 768 | // - ("tcp4", IP, port) 769 | // - ("tcp", IP, port) 770 | // - ("tcp4", "", port) 771 | // - ("tcp", "", port) 772 | // 773 | // The netBase is "tcp" or "udp" (without any '4' or '6' suffix). 774 | // 775 | // Listeners which do not specify an IP address will match for traffic 776 | // for the local node (that is, a destination address of the IPv4 or 777 | // IPv6 address of this node) only. To listen for traffic on other addresses 778 | // such as those routed inbound via subnet routes, explicitly specify 779 | // the listening address or use RegisterFallbackTCPHandler. 780 | func (s *Server) listenerForDstAddr(netBase string, dst netip.AddrPort, funnel bool) (_ *listener, ok bool) { 781 | s.mu.Lock() 782 | defer s.mu.Unlock() 783 | 784 | // Search for a listener with the specified IP 785 | for _, net := range [2]string{ 786 | networkForFamily(netBase, dst.Addr().Is6()), 787 | netBase, 788 | } { 789 | if ln, ok := s.listeners[listenKey{net, dst.Addr(), dst.Port(), funnel}]; ok { 790 | return ln, true 791 | } 792 | } 793 | 794 | // Search for a listener without an IP if the destination was 795 | // one of the native IPs of the node. 796 | if ip4, ip6 := s.TailscaleIPs(); dst.Addr() == ip4 || dst.Addr() == ip6 { 797 | for _, net := range [2]string{ 798 | networkForFamily(netBase, dst.Addr().Is6()), 799 | netBase, 800 | } { 801 | if ln, ok := s.listeners[listenKey{net, netip.Addr{}, dst.Port(), funnel}]; ok { 802 | return ln, true 803 | } 804 | } 805 | } 806 | 807 | return nil, false 808 | } 809 | 810 | func (s *Server) getTCPHandlerForFunnelFlow(src netip.AddrPort, dstPort uint16) (handler func(net.Conn)) { 811 | ipv4, ipv6 := s.TailscaleIPs() 812 | var dst netip.AddrPort 813 | if src.Addr().Is4() { 814 | if !ipv4.IsValid() { 815 | return nil 816 | } 817 | dst = netip.AddrPortFrom(ipv4, dstPort) 818 | } else { 819 | if !ipv6.IsValid() { 820 | return nil 821 | } 822 | dst = netip.AddrPortFrom(ipv6, dstPort) 823 | } 824 | ln, ok := s.listenerForDstAddr("tcp", dst, true) 825 | if !ok { 826 | return nil 827 | } 828 | return ln.handle 829 | } 830 | 831 | func (s *Server) getTCPHandlerForFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) { 832 | ln, ok := s.listenerForDstAddr("tcp", dst, false) 833 | if !ok { 834 | s.mu.Lock() 835 | defer s.mu.Unlock() 836 | for _, handler := range s.fallbackTCPHandlers { 837 | connHandler, intercept := handler(src, dst) 838 | if intercept { 839 | return connHandler, intercept 840 | } 841 | } 842 | return nil, true // don't handle, don't forward to localhost 843 | } 844 | return ln.handle, true 845 | } 846 | 847 | func (s *Server) getUDPHandlerForFlow(src, dst netip.AddrPort) (handler func(nettype.ConnPacketConn), intercept bool) { 848 | ln, ok := s.listenerForDstAddr("udp", dst, false) 849 | if !ok { 850 | return nil, true // don't handle, don't forward to localhost 851 | } 852 | return func(c nettype.ConnPacketConn) { ln.handle(c) }, true 853 | } 854 | 855 | // getTSNetDir usually just returns filepath.Join(confDir, "tsnet-"+prog) 856 | // with no error. 857 | // 858 | // One special case is that it renames old "tslib-" directories to 859 | // "tsnet-", and that rename might return an error. 860 | // 861 | // TODO(bradfitz): remove this maybe 6 months after 2022-03-17, 862 | // once people (notably Tailscale corp services) have updated. 863 | func getTSNetDir(logf logger.Logf, confDir, prog string) (string, error) { 864 | oldPath := filepath.Join(confDir, "tslib-"+prog) 865 | newPath := filepath.Join(confDir, "tsnet-"+prog) 866 | 867 | fi, err := os.Lstat(oldPath) 868 | if os.IsNotExist(err) { 869 | // Common path. 870 | return newPath, nil 871 | } 872 | if err != nil { 873 | return "", err 874 | } 875 | if !fi.IsDir() { 876 | return "", fmt.Errorf("expected old tslib path %q to be a directory; got %v", oldPath, fi.Mode()) 877 | } 878 | 879 | // At this point, oldPath exists and is a directory. But does 880 | // the new path exist? 881 | 882 | fi, err = os.Lstat(newPath) 883 | if err == nil && fi.IsDir() { 884 | // New path already exists somehow. Ignore the old one and 885 | // don't try to migrate it. 886 | return newPath, nil 887 | } 888 | if err != nil && !os.IsNotExist(err) { 889 | return "", err 890 | } 891 | if err := os.Rename(oldPath, newPath); err != nil { 892 | return "", err 893 | } 894 | logf("renamed old tsnet state storage directory %q to %q", oldPath, newPath) 895 | return newPath, nil 896 | } 897 | 898 | // APIClient returns a tailscale.Client that can be used to make authenticated 899 | // requests to the Tailscale control server. 900 | // It requires the user to set tailscale.I_Acknowledge_This_API_Is_Unstable. 901 | func (s *Server) APIClient() (*tailscale.Client, error) { 902 | if !tailscale.I_Acknowledge_This_API_Is_Unstable { 903 | return nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable") 904 | } 905 | if err := s.Start(); err != nil { 906 | return nil, err 907 | } 908 | 909 | c := tailscale.NewClient("-", nil) 910 | c.HTTPClient = &http.Client{Transport: s.lb.KeyProvingNoiseRoundTripper()} 911 | return c, nil 912 | } 913 | 914 | // Listen announces only on the Tailscale network. 915 | // It will start the server if it has not been started yet. 916 | // 917 | // Listeners which do not specify an IP address will match for traffic 918 | // for the local node (that is, a destination address of the IPv4 or 919 | // IPv6 address of this node) only. To listen for traffic on other addresses 920 | // such as those routed inbound via subnet routes, explicitly specify 921 | // the listening address or use RegisterFallbackTCPHandler. 922 | func (s *Server) Listen(network, addr string) (net.Listener, error) { 923 | return s.listen(network, addr, listenOnTailnet) 924 | } 925 | 926 | // ListenPacket announces on the Tailscale network. 927 | // 928 | // The network must be "udp", "udp4" or "udp6". The addr must be of the form 929 | // "ip:port" (or "[ip]:port") where ip is a valid IPv4 or IPv6 address 930 | // corresponding to "udp4" or "udp6" respectively. IP must be specified. 931 | // 932 | // If s has not been started yet, it will be started. 933 | func (s *Server) ListenPacket(network, addr string) (net.PacketConn, error) { 934 | ap, err := resolveListenAddr(network, addr) 935 | if err != nil { 936 | return nil, err 937 | } 938 | if !ap.Addr().IsValid() { 939 | return nil, fmt.Errorf("tsnet.ListenPacket(%q, %q): address must be a valid IP", network, addr) 940 | } 941 | if network == "udp" { 942 | if ap.Addr().Is4() { 943 | network = "udp4" 944 | } else { 945 | network = "udp6" 946 | } 947 | } 948 | if err := s.Start(); err != nil { 949 | return nil, err 950 | } 951 | return s.netstack.ListenPacket(network, ap.String()) 952 | } 953 | 954 | // ListenTLS announces only on the Tailscale network. 955 | // It returns a TLS listener wrapping the tsnet listener. 956 | // It will start the server if it has not been started yet. 957 | func (s *Server) ListenTLS(network, addr string) (net.Listener, error) { 958 | if network != "tcp" { 959 | return nil, fmt.Errorf("ListenTLS(%q, %q): only tcp is supported", network, addr) 960 | } 961 | ctx := context.Background() 962 | st, err := s.Up(ctx) 963 | if err != nil { 964 | return nil, err 965 | } 966 | if !st.CurrentTailnet.MagicDNSEnabled { 967 | return nil, errors.New("tsnet: you must enable MagicDNS in the DNS page of the admin panel to proceed. See https://tailscale.com/s/https") 968 | } 969 | if len(st.CertDomains) == 0 { 970 | return nil, errors.New("tsnet: you must enable HTTPS in the admin panel to proceed. See https://tailscale.com/s/https") 971 | } 972 | 973 | ln, err := s.listen(network, addr, listenOnTailnet) 974 | if err != nil { 975 | return nil, err 976 | } 977 | return tls.NewListener(ln, &tls.Config{ 978 | GetCertificate: s.getCert, 979 | }), nil 980 | } 981 | 982 | // RegisterFallbackTCPHandler registers a callback which will be called 983 | // to handle a TCP flow to this tsnet node, for which no listeners will handle. 984 | // 985 | // If multiple fallback handlers are registered, they will be called in an 986 | // undefined order. See FallbackTCPHandler for details on handling a flow. 987 | // 988 | // The returned function can be used to deregister this callback. 989 | func (s *Server) RegisterFallbackTCPHandler(cb FallbackTCPHandler) func() { 990 | s.mu.Lock() 991 | defer s.mu.Unlock() 992 | hnd := s.fallbackTCPHandlers.Add(cb) 993 | return func() { 994 | s.mu.Lock() 995 | defer s.mu.Unlock() 996 | delete(s.fallbackTCPHandlers, hnd) 997 | } 998 | } 999 | 1000 | // getCert is the GetCertificate function used by ListenTLS. 1001 | // 1002 | // It calls GetCertificate on the localClient, passing in the ClientHelloInfo. 1003 | // For testing, if s.getCertForTesting is set, it will call that instead. 1004 | func (s *Server) getCert(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { 1005 | if s.getCertForTesting != nil { 1006 | return s.getCertForTesting(hi) 1007 | } 1008 | lc, err := s.LocalClient() 1009 | if err != nil { 1010 | return nil, err 1011 | } 1012 | return lc.GetCertificate(hi) 1013 | } 1014 | 1015 | // FunnelOption is an option passed to ListenFunnel to configure the listener. 1016 | type FunnelOption interface { 1017 | funnelOption() 1018 | } 1019 | 1020 | type funnelOnly int 1021 | 1022 | func (funnelOnly) funnelOption() {} 1023 | 1024 | // FunnelOnly configures the listener to only respond to connections from Tailscale Funnel. 1025 | // The local tailnet will not be able to connect to the listener. 1026 | func FunnelOnly() FunnelOption { return funnelOnly(1) } 1027 | 1028 | // ListenFunnel announces on the public internet using Tailscale Funnel. 1029 | // 1030 | // It also by default listens on your local tailnet, so connections can 1031 | // come from either inside or outside your network. To restrict connections 1032 | // to be just from the internet, use the FunnelOnly option. 1033 | // 1034 | // Currently (2023-03-10), Funnel only supports TCP on ports 443, 8443, and 10000. 1035 | // The supported host name is limited to that configured for the tsnet.Server. 1036 | // As such, the standard way to create funnel is: 1037 | // 1038 | // s.ListenFunnel("tcp", ":443") 1039 | // 1040 | // and the only other supported addrs currently are ":8443" and ":10000". 1041 | // 1042 | // It will start the server if it has not been started yet. 1043 | func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.Listener, error) { 1044 | if network != "tcp" { 1045 | return nil, fmt.Errorf("ListenFunnel(%q, %q): only tcp is supported", network, addr) 1046 | } 1047 | host, portStr, err := net.SplitHostPort(addr) 1048 | if err != nil { 1049 | return nil, err 1050 | } 1051 | if host != "" { 1052 | return nil, fmt.Errorf("ListenFunnel(%q, %q): host must be empty", network, addr) 1053 | } 1054 | port, err := strconv.ParseUint(portStr, 10, 16) 1055 | if err != nil { 1056 | return nil, err 1057 | } 1058 | 1059 | ctx := context.Background() 1060 | st, err := s.Up(ctx) 1061 | if err != nil { 1062 | return nil, err 1063 | } 1064 | // TODO(sonia,tailscale/corp#10577): We may want to use the interactive enable 1065 | // flow here instead of CheckFunnelAccess to allow the user to turn on Funnel 1066 | // if not already on. Specifically when running from a terminal. 1067 | // See cli.serveEnv.verifyFunnelEnabled. 1068 | if err := ipn.CheckFunnelAccess(uint16(port), st.Self); err != nil { 1069 | return nil, err 1070 | } 1071 | 1072 | lc := s.localClient 1073 | 1074 | // May not have funnel enabled. Enable it. 1075 | srvConfig, err := lc.GetServeConfig(ctx) 1076 | if err != nil { 1077 | return nil, err 1078 | } 1079 | if srvConfig == nil { 1080 | srvConfig = &ipn.ServeConfig{} 1081 | } 1082 | if len(st.CertDomains) == 0 { 1083 | return nil, errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https") 1084 | } 1085 | domain := st.CertDomains[0] 1086 | hp := ipn.HostPort(domain + ":" + portStr) 1087 | if !srvConfig.AllowFunnel[hp] { 1088 | mak.Set(&srvConfig.AllowFunnel, hp, true) 1089 | srvConfig.AllowFunnel[hp] = true 1090 | if err := lc.SetServeConfig(ctx, srvConfig); err != nil { 1091 | return nil, err 1092 | } 1093 | } 1094 | 1095 | // Start a funnel listener. 1096 | lnOn := listenOnBoth 1097 | for _, opt := range opts { 1098 | if _, ok := opt.(funnelOnly); ok { 1099 | lnOn = listenOnFunnel 1100 | } 1101 | } 1102 | ln, err := s.listen(network, addr, lnOn) 1103 | if err != nil { 1104 | return nil, err 1105 | } 1106 | return tls.NewListener(ln, &tls.Config{ 1107 | GetCertificate: s.getCert, 1108 | }), nil 1109 | } 1110 | 1111 | type listenOn string 1112 | 1113 | const ( 1114 | listenOnTailnet = listenOn("listen-on-tailnet") 1115 | listenOnFunnel = listenOn("listen-on-funnel") 1116 | listenOnBoth = listenOn("listen-on-both") 1117 | ) 1118 | 1119 | // resolveListenAddr resolves a network and address into a netip.AddrPort. The 1120 | // returned netip.AddrPort.Addr will be the zero value if the address is empty. 1121 | // The port must be a valid port number. The caller is responsible for checking 1122 | // the network and address are valid. 1123 | // 1124 | // It resolves well-known port names and validates the address is a valid IP 1125 | // literal for the network. 1126 | func resolveListenAddr(network, addr string) (netip.AddrPort, error) { 1127 | var zero netip.AddrPort 1128 | host, portStr, err := net.SplitHostPort(addr) 1129 | if err != nil { 1130 | return zero, fmt.Errorf("tsnet: %w", err) 1131 | } 1132 | port, err := net.LookupPort(network, portStr) 1133 | if err != nil || port < 0 || port > math.MaxUint16 { 1134 | // LookupPort returns an error on out of range values so the bounds 1135 | // checks on port should be unnecessary, but harmless. If they do 1136 | // match, worst case this error message says "invalid port: ". 1137 | return zero, fmt.Errorf("invalid port: %w", err) 1138 | } 1139 | if host == "" { 1140 | return netip.AddrPortFrom(netip.Addr{}, uint16(port)), nil 1141 | } 1142 | 1143 | bindHostOrZero, err := netip.ParseAddr(host) 1144 | if err != nil { 1145 | return zero, fmt.Errorf("invalid Listen addr %q; host part must be empty or IP literal", host) 1146 | } 1147 | if strings.HasSuffix(network, "4") && !bindHostOrZero.Is4() { 1148 | return zero, fmt.Errorf("invalid non-IPv4 addr %v for network %q", host, network) 1149 | } 1150 | if strings.HasSuffix(network, "6") && !bindHostOrZero.Is6() { 1151 | return zero, fmt.Errorf("invalid non-IPv6 addr %v for network %q", host, network) 1152 | } 1153 | return netip.AddrPortFrom(bindHostOrZero, uint16(port)), nil 1154 | } 1155 | 1156 | func (s *Server) listen(network, addr string, lnOn listenOn) (net.Listener, error) { 1157 | switch network { 1158 | case "", "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6": 1159 | default: 1160 | return nil, errors.New("unsupported network type") 1161 | } 1162 | host, err := resolveListenAddr(network, addr) 1163 | if err != nil { 1164 | return nil, err 1165 | } 1166 | if err := s.Start(); err != nil { 1167 | return nil, err 1168 | } 1169 | var keys []listenKey 1170 | switch lnOn { 1171 | case listenOnTailnet: 1172 | keys = append(keys, listenKey{network, host.Addr(), host.Port(), false}) 1173 | case listenOnFunnel: 1174 | keys = append(keys, listenKey{network, host.Addr(), host.Port(), true}) 1175 | case listenOnBoth: 1176 | keys = append(keys, listenKey{network, host.Addr(), host.Port(), false}) 1177 | keys = append(keys, listenKey{network, host.Addr(), host.Port(), true}) 1178 | } 1179 | 1180 | ln := &listener{ 1181 | s: s, 1182 | keys: keys, 1183 | addr: addr, 1184 | 1185 | conn: make(chan net.Conn), 1186 | } 1187 | s.mu.Lock() 1188 | for _, key := range keys { 1189 | if _, ok := s.listeners[key]; ok { 1190 | s.mu.Unlock() 1191 | return nil, fmt.Errorf("tsnet: listener already open for %s, %s", network, addr) 1192 | } 1193 | } 1194 | if s.listeners == nil { 1195 | s.listeners = make(map[listenKey]*listener) 1196 | } 1197 | for _, key := range keys { 1198 | s.listeners[key] = ln 1199 | } 1200 | s.mu.Unlock() 1201 | return ln, nil 1202 | } 1203 | 1204 | // CapturePcap can be called by the application code compiled with tsnet to save a pcap 1205 | // of packets which the netstack within tsnet sees. This is expected to be useful during 1206 | // debugging, probably not useful for production. 1207 | // 1208 | // Packets will be written to the pcap until the process exits. The pcap needs a Lua dissector 1209 | // to be installed in WireShark in order to decode properly: wgengine/capture/ts-dissector.lua 1210 | // in this repository. 1211 | // https://tailscale.com/kb/1023/troubleshooting/#can-i-examine-network-traffic-inside-the-encrypted-tunnel 1212 | func (s *Server) CapturePcap(ctx context.Context, pcapFile string) error { 1213 | stream, err := s.localClient.StreamDebugCapture(ctx) 1214 | if err != nil { 1215 | return err 1216 | } 1217 | 1218 | f, err := os.OpenFile(pcapFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 1219 | if err != nil { 1220 | stream.Close() 1221 | return err 1222 | } 1223 | 1224 | go func(stream io.ReadCloser, f *os.File) { 1225 | defer stream.Close() 1226 | defer f.Close() 1227 | _, _ = io.Copy(f, stream) 1228 | }(stream, f) 1229 | 1230 | return nil 1231 | } 1232 | 1233 | type listenKey struct { 1234 | network string 1235 | host netip.Addr // or zero value for unspecified 1236 | port uint16 1237 | funnel bool 1238 | } 1239 | 1240 | type listener struct { 1241 | s *Server 1242 | keys []listenKey 1243 | addr string 1244 | conn chan net.Conn 1245 | closed bool // guarded by s.mu 1246 | } 1247 | 1248 | func (ln *listener) Accept() (net.Conn, error) { 1249 | c, ok := <-ln.conn 1250 | if !ok { 1251 | return nil, fmt.Errorf("tsnet: %w", net.ErrClosed) 1252 | } 1253 | return c, nil 1254 | } 1255 | 1256 | func (ln *listener) Addr() net.Addr { return addr{ln} } 1257 | 1258 | func (ln *listener) Close() error { 1259 | ln.s.mu.Lock() 1260 | defer ln.s.mu.Unlock() 1261 | return ln.closeLocked() 1262 | } 1263 | 1264 | // closeLocked closes the listener. 1265 | // It must be called with ln.s.mu held. 1266 | func (ln *listener) closeLocked() error { 1267 | if ln.closed { 1268 | return fmt.Errorf("tsnet: %w", net.ErrClosed) 1269 | } 1270 | for _, key := range ln.keys { 1271 | if v, ok := ln.s.listeners[key]; ok && v == ln { 1272 | delete(ln.s.listeners, key) 1273 | } 1274 | } 1275 | close(ln.conn) 1276 | ln.closed = true 1277 | return nil 1278 | } 1279 | 1280 | func (ln *listener) handle(c net.Conn) { 1281 | t := time.NewTimer(time.Second) 1282 | defer t.Stop() 1283 | select { 1284 | case ln.conn <- c: 1285 | case <-t.C: 1286 | // TODO(bradfitz): this isn't ideal. Think about how 1287 | // we how we want to do pushback. 1288 | c.Close() 1289 | } 1290 | } 1291 | 1292 | // Server returns the tsnet Server associated with the listener. 1293 | func (ln *listener) Server() *Server { return ln.s } 1294 | 1295 | type addr struct{ ln *listener } 1296 | 1297 | func (a addr) Network() string { return a.ln.keys[0].network } 1298 | func (a addr) String() string { return a.ln.addr } 1299 | --------------------------------------------------------------------------------