diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 6bd5032c8..3a8a74e8b 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -143,17 +143,11 @@ func warnf(format string, args ...interface{}) { ipv6default = netaddr.MustParseIPPrefix("::/0") ) -// prefsFromUpArgs returns the ipn.Prefs for the provided args. -// -// Note that the parameters upArgs and warnf are named intentionally -// to shadow the globals to prevent accidental misuse of them. This -// function exists for testing and should have no side effects or -// outside interactions (e.g. no making Tailscale local API calls). -func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) { +func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netaddr.IPPrefix, error) { routeMap := map[netaddr.IPPrefix]bool{} - var default4, default6 bool - if upArgs.advertiseRoutes != "" { - advroutes := strings.Split(upArgs.advertiseRoutes, ",") + if advertiseRoutes != "" { + var default4, default6 bool + advroutes := strings.Split(advertiseRoutes, ",") for _, s := range advroutes { ipp, err := netaddr.ParseIPPrefix(s) if err != nil { @@ -175,7 +169,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default) } } - if upArgs.advertiseDefaultRoute { + if advertiseDefaultRoute { routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true routeMap[netaddr.MustParseIPPrefix("::/0")] = true } @@ -189,6 +183,20 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo } return routes[i].IP().Less(routes[j].IP()) }) + return routes, nil +} + +// prefsFromUpArgs returns the ipn.Prefs for the provided args. +// +// Note that the parameters upArgs and warnf are named intentionally +// to shadow the globals to prevent accidental misuse of them. This +// function exists for testing and should have no side effects or +// outside interactions (e.g. no making Tailscale local API calls). +func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) { + routes, err := calcAdvertiseRoutes(upArgs.advertiseRoutes, upArgs.advertiseDefaultRoute) + if err != nil { + return nil, err + } var exitNodeIP netaddr.IP if upArgs.exitNodeIP != "" { diff --git a/cmd/tailscale/cli/web.css b/cmd/tailscale/cli/web.css index 64672224d..0876c1b3c 100644 --- a/cmd/tailscale/cli/web.css +++ b/cmd/tailscale/cli/web.css @@ -1335,3 +1335,14 @@ html { border-color: #6c94ec; border-color: rgba(108, 148, 236, var(--border-opacity)); } + +.button-red { + background-color: #d04841; + border-color: #d04841; + color: #fff; +} + +.button-red:enabled:hover { + background-color: #b22d30; + border-color: #b22d30; +} diff --git a/cmd/tailscale/cli/web.go b/cmd/tailscale/cli/web.go index cb2088618..07a6565b6 100644 --- a/cmd/tailscale/cli/web.go +++ b/cmd/tailscale/cli/web.go @@ -14,6 +14,7 @@ "flag" "fmt" "html/template" + "io" "io/ioutil" "log" "net" @@ -26,6 +27,7 @@ "strings" "github.com/peterbourgon/ff/v3/ffcli" + "inet.af/netaddr" "tailscale.com/client/tailscale" "tailscale.com/ipn" "tailscale.com/tailcfg" @@ -51,11 +53,13 @@ func init() { } type tmplData struct { - Profile tailcfg.UserProfile - SynologyUser string - Status string - DeviceName string - IP string + Profile tailcfg.UserProfile + SynologyUser string + Status string + DeviceName string + IP string + AdvertiseExitNode bool + AdvertiseRoutes string } var webCmd = &ffcli.Command{ @@ -303,15 +307,45 @@ func webHandler(w http.ResponseWriter, r *http.Request) { } if r.Method == "POST" { + defer r.Body.Close() + var postData struct { + AdvertiseRoutes string + AdvertiseExitNode bool + Reauthenticate bool + } type mi map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&postData); err != nil { + w.WriteHeader(400) + json.NewEncoder(w).Encode(mi{"error": err.Error()}) + return + } + prefs, err := tailscale.GetPrefs(r.Context()) + if err != nil && !postData.Reauthenticate { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(mi{"error": err.Error()}) + return + } else { + routes, err := calcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(mi{"error": err.Error()}) + return + } + prefs.AdvertiseRoutes = routes + } + w.Header().Set("Content-Type", "application/json") - url, err := tailscaleUpForceReauth(r.Context()) + url, err := tailscaleUp(r.Context(), prefs, postData.Reauthenticate) if err != nil { w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(mi{"error": err.Error()}) return } - json.NewEncoder(w).Encode(mi{"url": url}) + if url != "" { + json.NewEncoder(w).Encode(mi{"url": url}) + } else { + io.WriteString(w, "{}") + } return } @@ -320,6 +354,11 @@ type mi map[string]interface{ http.Error(w, err.Error(), http.StatusInternalServerError) return } + prefs, err := tailscale.GetPrefs(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } profile := st.User[st.Self.UserID] deviceName := strings.Split(st.Self.DNSName, ".")[0] @@ -329,6 +368,18 @@ type mi map[string]interface{ Status: st.BackendState, DeviceName: deviceName, } + exitNodeRouteV4 := netaddr.MustParseIPPrefix("0.0.0.0/0") + exitNodeRouteV6 := netaddr.MustParseIPPrefix("::/0") + for _, r := range prefs.AdvertiseRoutes { + if r == exitNodeRouteV4 || r == exitNodeRouteV6 { + data.AdvertiseExitNode = true + } else { + if data.AdvertiseRoutes != "" { + data.AdvertiseRoutes = "," + } + data.AdvertiseRoutes += r.String() + } + } if len(st.TailscaleIPs) != 0 { data.IP = st.TailscaleIPs[0].String() } @@ -342,13 +393,15 @@ type mi map[string]interface{ } // TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything? -func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error) { - prefs := ipn.NewPrefs() - prefs.ControlURL = ipn.DefaultControlURL - prefs.WantRunning = true - prefs.CorpDNS = true - prefs.AllowSingleHosts = true - prefs.ForceDaemon = (runtime.GOOS == "windows") +func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authURL string, retErr error) { + if prefs == nil { + prefs = ipn.NewPrefs() + prefs.ControlURL = ipn.DefaultControlURL + prefs.WantRunning = true + prefs.CorpDNS = true + prefs.AllowSingleHosts = true + prefs.ForceDaemon = (runtime.GOOS == "windows") + } if distro.Get() == distro.Synology { prefs.NetfilterMode = preftype.NetfilterOff @@ -395,6 +448,14 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error) authURL = *url cancel() } + if !forceReauth && n.Prefs != nil { + p1, p2 := *n.Prefs, *prefs + p1.Persist = nil + p2.Persist = nil + if p1.Equals(&p2) { + cancel() + } + } }) // Wait for backend client to be connected so we know // we're subscribed to updates. Otherwise we can miss @@ -412,10 +473,15 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error) bc.Start(ipn.Options{ StateKey: ipn.GlobalDaemonStateKey, }) - bc.StartLoginInteractive() + if forceReauth { + bc.StartLoginInteractive() + } <-pumpCtx.Done() // wait for authURL or complete failure if authURL == "" && retErr == nil { + if !forceReauth { + return "", nil // no auth URL is fine + } retErr = pumpCtx.Err() } if authURL == "" && retErr == nil { diff --git a/cmd/tailscale/cli/web.html b/cmd/tailscale/cli/web.html index 93fc9f2e0..429708cf8 100644 --- a/cmd/tailscale/cli/web.html +++ b/cmd/tailscale/cli/web.html @@ -86,14 +86,30 @@

You are connected! Access this device over Tailscale using the device name or IP address above.

- Reauthenticate +
+ + {{if .AdvertiseExitNode}} + + {{else}} + + {{end}} + +
+
+ Reauthenticate +
{{ end }}