// invite.go: the one-paste invitation code a hub prints and a peer pastes // into their config, replacing separately copying an IP and a token. package main import ( "encoding/base64" "encoding/json" "fmt" "net" "strings" ) type inviteCode struct { Host string `json:"host"` Port string `json:"port"` Fingerprint string `json:"fingerprint"` Token string `json:"token"` } func encodeInvite(c inviteCode) string { b, _ := json.Marshal(c) // a struct of plain strings: Marshal can't fail return base64.RawURLEncoding.EncodeToString(b) } func decodeInvite(s string) (inviteCode, error) { raw, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(s)) if err != nil { return inviteCode{}, fmt.Errorf("invite code isn't valid base64: %w", err) } var c inviteCode if err := json.Unmarshal(raw, &c); err != nil { return inviteCode{}, fmt.Errorf("invite code doesn't decode to a valid invite: %w", err) } if c.Host == "" || c.Port == "" || c.Token == "" { return inviteCode{}, fmt.Errorf("invite code is missing a host, port, or token") } if !validFingerprint(c.Fingerprint) { return inviteCode{}, fmt.Errorf("invite code's fingerprint doesn't look like a SHA-256 hex digest") } return c, nil } // tailscaleCGNAT is the address range Tailscale assigns its clients from. var tailscaleCGNAT = mustParseCIDR("100.64.0.0/10") func mustParseCIDR(s string) *net.IPNet { _, n, err := net.ParseCIDR(s) if err != nil { panic(err) // a hardcoded literal: only fails if this code is wrong } return n } // pickBestHost picks the address a co-op partner is most likely to be // able to reach: a Tailscale address over a plain private-LAN one (a VPN // like ZeroTier or WireGuard usually also hands out a private-range // address, so this still prefers it over nothing) over nothing at all. // It's a guess, not a guarantee — the hub operator can always override it // with the "hub" config key if it's wrong. func pickBestHost(addrs []string) string { var private string for _, a := range addrs { ip := net.ParseIP(a) if ip == nil { continue } if tailscaleCGNAT.Contains(ip) { return a } if private == "" && ip.IsPrivate() { private = a } } return private } // candidateIPv4s lists this machine's non-loopback IPv4 addresses, for // pickBestHost to choose from and for logging the full list so a human // can pick a different one if the guess is wrong. func candidateIPv4s() []string { addrs, err := net.InterfaceAddrs() if err != nil { return nil } var out []string for _, a := range addrs { ipNet, ok := a.(*net.IPNet) if !ok || ipNet.IP.IsLoopback() { continue } ip4 := ipNet.IP.To4() if ip4 == nil { continue } out = append(out, ip4.String()) } return out }