diff --git a/cmd/tailscale/cli/serve_dev.go b/cmd/tailscale/cli/serve_dev.go index d67938789..5b5da30d0 100644 --- a/cmd/tailscale/cli/serve_dev.go +++ b/cmd/tailscale/cli/serve_dev.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/peterbourgon/ff/v3/ffcli" + "tailscale.com/client/tailscale" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" @@ -188,99 +189,37 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc { mount = "/" } - if e.bg || turnOff || e.setPath != "" { - srvType, srvPort, err := srvTypeAndPortFromFlags(e) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n\n", err) - return errHelp - } + if e.setPath != "" { + // TODO(marwan-at-work): either + // 1. Warn the user that this is a side effect. + // 2. Force the user to pass --bg + // 3. Allow set-path to be in the foreground. + e.bg = true + } - if turnOff { - err := e.unsetServe(ctx, srvType, srvPort, mount) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n\n", err) - return errHelp - } - return nil - } + srvType, srvPort, err := srvTypeAndPortFromFlags(e) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n\n", err) + return errHelp + } - err = e.setServe(ctx, st, srvType, srvPort, mount, target, funnel) + if turnOff { + err := e.unsetServe(ctx, srvType, srvPort, mount) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n\n", err) return errHelp } - return nil } - dnsName := strings.TrimSuffix(st.Self.DNSName, ".") - hp := ipn.HostPort(dnsName + ":443") // TODO(marwan-at-work): support the 2 other ports - - // TODO(marwan-at-work): combine this with the above setServe code. - // Foreground and background should be the same, we just pass - // a foreground config instead of the top level background one. - return e.streamServe(ctx, ipn.ServeStreamRequest{ - Funnel: funnel, - HostPort: hp, - Source: target, - MountPoint: mount, - }) - } -} - -func (e *serveEnv) streamServe(ctx context.Context, req ipn.ServeStreamRequest) error { - watcher, err := e.lc.WatchIPNBus(ctx, ipn.NotifyInitialState) - if err != nil { - return err - } - defer watcher.Close() - n, err := watcher.Next() - if err != nil { - return err - } - sessionID := n.SessionID - if sessionID == "" { - return errors.New("missing SessionID") - } - sc, err := e.lc.GetServeConfig(ctx) - if err != nil { - return fmt.Errorf("error getting serve config: %w", err) - } - if sc == nil { - sc = &ipn.ServeConfig{} - } - setHandler(sc, req, sessionID) - err = e.lc.SetServeConfig(ctx, sc) - if err != nil { - return fmt.Errorf("error setting serve config: %w", err) - } - - fmt.Fprintf(os.Stderr, "Funnel started on \"https://%s\".\n", strings.TrimSuffix(string(req.HostPort), ":443")) - fmt.Fprintf(os.Stderr, "Press Ctrl-C to stop Funnel.\n\n") - - for { - _, err = watcher.Next() + err = e.setServe(ctx, st, srvType, srvPort, mount, target, funnel) if err != nil { - if errors.Is(err, context.Canceled) { - return nil - } - return err + fmt.Fprintf(os.Stderr, "error: %v\n\n", err) + return errHelp } - } -} -// setHandler modifies sc to add a Foreground config (described by req) with the given sessionID. -func setHandler(sc *ipn.ServeConfig, req ipn.ServeStreamRequest, sessionID string) { - fconf := &ipn.ServeConfig{} - mak.Set(&sc.Foreground, sessionID, fconf) - mak.Set(&fconf.TCP, 443, &ipn.TCPPortHandler{HTTPS: true}) - - wsc := &ipn.WebServerConfig{} - mak.Set(&fconf.Web, req.HostPort, wsc) - mak.Set(&wsc.Handlers, req.MountPoint, &ipn.HTTPHandler{ - Proxy: req.Source, - }) - mak.Set(&fconf.AllowFunnel, req.HostPort, true) + return nil + } } func (e *serveEnv) setServe(ctx context.Context, st *ipnstate.Status, srvType string, srvPort uint16, mount string, target string, allowFunnel bool) error { @@ -305,14 +244,42 @@ func (e *serveEnv) setServe(ctx context.Context, st *ipnstate.Status, srvType st return err } + // nil if no config + if sc == nil { + sc = new(ipn.ServeConfig) + } + + // set parent serve config to always be persisted + // at the top level, but a nested config might be + // the one that gets manipulated depending on + // foreground or background. + parentSC := sc + dnsName, err := e.getSelfDNSName(ctx) if err != nil { return err } - // nil if no config - if sc == nil { - sc = new(ipn.ServeConfig) + // if foreground mode, create a WatchIPNBus session + // and use the nested config for all following operations + // TODO(marwan-at-work): nested-config validations should happen here or previous to this point. + var watcher *tailscale.IPNBusWatcher + if !e.bg { + watcher, err = e.lc.WatchIPNBus(ctx, ipn.NotifyInitialState) + if err != nil { + return err + } + defer watcher.Close() + n, err := watcher.Next() + if err != nil { + return err + } + if n.SessionID == "" { + return errors.New("missing SessionID") + } + fsc := &ipn.ServeConfig{} + mak.Set(&sc.Foreground, n.SessionID, fsc) + sc = fsc } // update serve config based on the type @@ -340,7 +307,7 @@ func (e *serveEnv) setServe(ctx context.Context, st *ipnstate.Status, srvType st e.applyFunnel(sc, dnsName, srvPort, allowFunnel) // persist the serve config changes - if err := e.lc.SetServeConfig(ctx, sc); err != nil { + if err := e.lc.SetServeConfig(ctx, parentSC); err != nil { return err } @@ -352,6 +319,18 @@ func (e *serveEnv) setServe(ctx context.Context, st *ipnstate.Status, srvType st fmt.Fprintln(os.Stderr, m) + if !e.bg { + for { + _, err = watcher.Next() + if err != nil { + if errors.Is(err, context.Canceled) { + return nil + } + return err + } + } + } + return nil } @@ -423,8 +402,12 @@ func (e *serveEnv) messageForPort(ctx context.Context, sc *ipn.ServeConfig, st * output.WriteString(fmt.Sprintf("|--> tcp://%s\n", h.TCPForward)) } - output.WriteString("\nServe started and running in the background.\n") - output.WriteString(fmt.Sprintf("To disable the proxy, run: tailscale %s off", infoMap[e.subcmd].Name)) + if e.bg { + output.WriteString("\nServe started and running in the background.\n") + output.WriteString(fmt.Sprintf("To disable the proxy, run: tailscale %s off", infoMap[e.subcmd].Name)) + } else { + // TODO(marwan-at-work): give the user more context on their foreground process. + } return output.String(), nil }