mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-30 00:29:48 +00:00
* cmd/tailscale/cli: Add service flag to serve command This commit adds the service flag to serve command which allows serving a service and add the service to the advertisedServices field in prefs (What advertise command does that will be removed later). When adding proxies, TCP proxies and WEB proxies work the same way as normal serve, just under a different DNSname. There is a services specific L3 serving mode called Tun, can be set via --tun flag. Serving a service is always in --bg mode. If --bg is explicitly set t o false, an error message will be sent out. The restriction on proxy target being localhost or 127.0.0.1 also applies to services. When removing proxies, TCP proxies can be removed with type and port flag and off argument. Web proxies can be removed with type, port, setPath flag and off argument. To align with normal serve, when setPath is not set, all handler under the hostport will be removed. When flags are not set but off argument was passed by user, it will be a noop. Removing all config for a service will be available later with a new subcommand clear. Updates tailscale/corp#22954 Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: fix ai comments and fix a test Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Add a test for addServiceToPrefs Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: fix comment Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * add dnsName in error message Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * change the cli input flag variable type Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace FindServiceConfig with map lookup Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * some code simplification and add asServiceName This commit cotains code simplification for IsServingHTTPS, SetWebHandler, SetTCPForwarding Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace IsServiceName with tailcfg.AsServiceName Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace all assemble of host name for service with strings.Join Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: adjust parameter order and update output message This commit updates the parameter order for IsTCPForwardingOnPort and SetWebHandler. Also updated the message msgServiceIPNotAssigned to msgServiceWaitingApproval to adapt to latest terminologies around services. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: flip bool condition This commit fixes a previous bug added that throws error when serve funnel without service. It should've been the opposite, which throws error when serve funnel with service. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: change parameter of IsTCPForwardingOnPort This commit changes the dnsName string parameter for IsTCPForwardingOnPort to svcName tailcfg.ServiceName. This change is made to reduce ambiguity when a single service might have different dnsNames Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * ipn/ipnlocal: replace the key to webHandler for services This commit changes the way we get the webhandler for vipServices. It used to use the host name from request to find the webHandler, now everything targeting the vipService IP have the same set of handlers. This commit also stores service:port instead of FQDN:port as the key in serviceConfig for Web map. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Updated use of service name. This commit removes serviceName.IsEmpty and use direct comparison to instead. In legacy code, when an empty service name needs to be passed, a new constant noService is passed. Removed redundant code for checking service name validity and string method for serviceNameFlag. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Update bgBoolFlag This commit update field name, set and string method of bgBoolFlag to make code cleaner. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: remove isDefaultService output from srvTypeAndPortFromFlags This commit removes the isDefaultService out put as it's no longer needed. Also deleted redundant code. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: remove unnessesary variable declare in messageForPort Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace bool output for AsServiceName with err Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Replace DNSName with NoService if DNSname only used to identify service This commit moves noService constant to tailcfg, updates AsServiceName to return tailcfg.NoService if the input is not a valid service name. This commit also removes using the local DNSName as scvName parameter. When a function is only using DNSName to identify if it's working with a service, the input in replaced with svcName and expect caller to pass tailcfg.NoService if it's a local serve. This commit also replaces some use of Sprintf with net.JoinHostPort for ipn.HostPort creation. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: Remove the returned error for AsServiceName Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * apply suggested code and comment Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * replace local dnsName in test with tailcfg.NoService Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * cmd/tailscale/cli: move noService back and use else where The constant serves the purpose of provide readability for passing as a function parameter. It's more meaningful comparing to a . It can just be an empty string in other places. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * ipn: Make WebHandlerExists and RemoveTCPForwarding accept svcName This commit replaces two functions' string input with svcName input since they only use the dnsName to identify service. Also did some minor cleanups Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> --------- Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
341 lines
8.8 KiB
Go
341 lines
8.8 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
"github.com/toqueteos/webbrowser"
|
|
"golang.org/x/net/idna"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/net/netmon"
|
|
"tailscale.com/util/dnsname"
|
|
)
|
|
|
|
var statusCmd = &ffcli.Command{
|
|
Name: "status",
|
|
ShortUsage: "tailscale status [--active] [--web] [--json]",
|
|
ShortHelp: "Show state of tailscaled and its connections",
|
|
LongHelp: strings.TrimSpace(`
|
|
|
|
JSON FORMAT
|
|
|
|
Warning: this format has changed between releases and might change more
|
|
in the future.
|
|
|
|
For a description of the fields, see the "type Status" declaration at:
|
|
|
|
https://github.com/tailscale/tailscale/blob/main/ipn/ipnstate/ipnstate.go
|
|
|
|
(and be sure to select branch/tag that corresponds to the version
|
|
of Tailscale you're running)
|
|
|
|
`),
|
|
Exec: runStatus,
|
|
FlagSet: (func() *flag.FlagSet {
|
|
fs := newFlagSet("status")
|
|
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
|
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
|
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
|
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
|
fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
|
|
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address for web mode; use port 0 for automatic")
|
|
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
|
return fs
|
|
})(),
|
|
}
|
|
|
|
var statusArgs struct {
|
|
json bool // JSON output mode
|
|
web bool // run webserver
|
|
listen string // in web mode, webserver address to listen on, empty means auto
|
|
browser bool // in web mode, whether to open browser
|
|
active bool // in CLI mode, filter output to only peers with active sessions
|
|
self bool // in CLI mode, show status of local machine
|
|
peers bool // in CLI mode, show status of peer machines
|
|
}
|
|
|
|
func runStatus(ctx context.Context, args []string) error {
|
|
if len(args) > 0 {
|
|
return errors.New("unexpected non-flag arguments to 'tailscale status'")
|
|
}
|
|
getStatus := localClient.Status
|
|
if !statusArgs.peers {
|
|
getStatus = localClient.StatusWithoutPeers
|
|
}
|
|
st, err := getStatus(ctx)
|
|
if err != nil {
|
|
return fixTailscaledConnectError(err)
|
|
}
|
|
if statusArgs.json {
|
|
if statusArgs.active {
|
|
for peer, ps := range st.Peer {
|
|
if !ps.Active {
|
|
delete(st.Peer, peer)
|
|
}
|
|
}
|
|
}
|
|
j, err := json.MarshalIndent(st, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printf("%s", j)
|
|
return nil
|
|
}
|
|
if statusArgs.web {
|
|
ln, err := net.Listen("tcp", statusArgs.listen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
statusURL := netmon.HTTPOfListener(ln)
|
|
printf("Serving Tailscale status at %v ...\n", statusURL)
|
|
go func() {
|
|
<-ctx.Done()
|
|
ln.Close()
|
|
}()
|
|
if statusArgs.browser {
|
|
go webbrowser.Open(statusURL)
|
|
}
|
|
err = http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.RequestURI != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
st, err := localClient.Status(ctx)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 500)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
st.WriteHTML(w)
|
|
}))
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return err
|
|
}
|
|
|
|
printHealth := func() {
|
|
printf("# Health check:\n")
|
|
for _, m := range st.Health {
|
|
printf("# - %s\n", m)
|
|
}
|
|
}
|
|
|
|
description, ok := isRunningOrStarting(st)
|
|
if !ok {
|
|
// print health check information if we're in a weird state, as it might
|
|
// provide context about why we're in that weird state.
|
|
if len(st.Health) > 0 && (st.BackendState == ipn.Starting.String() || st.BackendState == ipn.NoState.String()) {
|
|
printHealth()
|
|
outln()
|
|
}
|
|
outln(description)
|
|
os.Exit(1)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
f := func(format string, a ...any) { fmt.Fprintf(&buf, format, a...) }
|
|
printPS := func(ps *ipnstate.PeerStatus) {
|
|
f("%-15s %-20s %-12s %-7s ",
|
|
firstIPString(ps.TailscaleIPs),
|
|
dnsOrQuoteHostname(st, ps),
|
|
ownerLogin(st, ps),
|
|
ps.OS,
|
|
)
|
|
relay := ps.Relay
|
|
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
|
var offline string
|
|
if !ps.Online {
|
|
offline = "; offline"
|
|
}
|
|
if !ps.Active {
|
|
if ps.ExitNode {
|
|
f("idle; exit node" + offline)
|
|
} else if ps.ExitNodeOption {
|
|
f("idle; offers exit node" + offline)
|
|
} else if anyTraffic {
|
|
f("idle" + offline)
|
|
} else if !ps.Online {
|
|
f("offline")
|
|
} else {
|
|
f("-")
|
|
}
|
|
} else {
|
|
f("active; ")
|
|
if ps.ExitNode {
|
|
f("exit node; ")
|
|
} else if ps.ExitNodeOption {
|
|
f("offers exit node; ")
|
|
}
|
|
if relay != "" && ps.CurAddr == "" && ps.PeerRelay == "" {
|
|
f("relay %q", relay)
|
|
} else if ps.CurAddr != "" {
|
|
f("direct %s", ps.CurAddr)
|
|
} else if ps.PeerRelay != "" {
|
|
f("peer-relay %s", ps.PeerRelay)
|
|
}
|
|
if !ps.Online {
|
|
f("; offline")
|
|
}
|
|
}
|
|
if anyTraffic {
|
|
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
|
|
}
|
|
f("\n")
|
|
}
|
|
|
|
if statusArgs.self && st.Self != nil {
|
|
printPS(st.Self)
|
|
}
|
|
|
|
locBasedExitNode := false
|
|
if statusArgs.peers {
|
|
var peers []*ipnstate.PeerStatus
|
|
for _, peer := range st.Peers() {
|
|
ps := st.Peer[peer]
|
|
if ps.ShareeNode {
|
|
continue
|
|
}
|
|
if ps.Location != nil && ps.ExitNodeOption && !ps.ExitNode {
|
|
// Location based exit nodes are only shown with the
|
|
// `exit-node list` command.
|
|
locBasedExitNode = true
|
|
continue
|
|
}
|
|
peers = append(peers, ps)
|
|
}
|
|
ipnstate.SortPeers(peers)
|
|
for _, ps := range peers {
|
|
if statusArgs.active && !ps.Active {
|
|
continue
|
|
}
|
|
printPS(ps)
|
|
}
|
|
}
|
|
Stdout.Write(buf.Bytes())
|
|
if locBasedExitNode {
|
|
outln()
|
|
printf("# To see the full list of exit nodes, including location-based exit nodes, run `tailscale exit-node list` \n")
|
|
}
|
|
if len(st.Health) > 0 {
|
|
outln()
|
|
printHealth()
|
|
}
|
|
printFunnelStatus(ctx)
|
|
return nil
|
|
}
|
|
|
|
// printFunnelStatus prints the status of the funnel, if it's running.
|
|
// It prints nothing if the funnel is not running.
|
|
func printFunnelStatus(ctx context.Context) {
|
|
sc, err := localClient.GetServeConfig(ctx)
|
|
if err != nil {
|
|
outln()
|
|
printf("# Funnel:\n")
|
|
printf("# - Unable to get Funnel status: %v\n", err)
|
|
return
|
|
}
|
|
if !sc.IsFunnelOn() {
|
|
return
|
|
}
|
|
outln()
|
|
printf("# Funnel on:\n")
|
|
for hp, on := range sc.AllowFunnel {
|
|
if !on { // if present, should be on
|
|
continue
|
|
}
|
|
sni, portStr, _ := net.SplitHostPort(string(hp))
|
|
p, _ := strconv.ParseUint(portStr, 10, 16)
|
|
isTCP := sc.IsTCPForwardingOnPort(uint16(p), noService)
|
|
url := "https://"
|
|
if isTCP {
|
|
url = "tcp://"
|
|
}
|
|
url += sni
|
|
if isTCP || p != 443 {
|
|
url += ":" + portStr
|
|
}
|
|
printf("# - %s\n", url)
|
|
}
|
|
outln()
|
|
}
|
|
|
|
// isRunningOrStarting reports whether st is in state Running or Starting.
|
|
// It also returns a description of the status suitable to display to a user.
|
|
func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
|
|
switch st.BackendState {
|
|
default:
|
|
return fmt.Sprintf("unexpected state: %s", st.BackendState), false
|
|
case ipn.Stopped.String():
|
|
return "Tailscale is stopped.", false
|
|
case ipn.NeedsLogin.String():
|
|
s := "Logged out."
|
|
if st.AuthURL != "" {
|
|
s += fmt.Sprintf("\nLog in at: %s", st.AuthURL)
|
|
}
|
|
return s, false
|
|
case ipn.NeedsMachineAuth.String():
|
|
return "Machine is not yet approved by tailnet admin.", false
|
|
case ipn.Running.String(), ipn.Starting.String():
|
|
return st.BackendState, true
|
|
}
|
|
}
|
|
|
|
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
|
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
|
if baseName != "" {
|
|
if strings.HasPrefix(baseName, "xn-") {
|
|
if u, err := idna.ToUnicode(baseName); err == nil {
|
|
return fmt.Sprintf("%s (%s)", baseName, u)
|
|
}
|
|
}
|
|
return baseName
|
|
}
|
|
return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
|
|
}
|
|
|
|
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
|
// We prioritize showing the name of the sharer as the owner of a node if
|
|
// it's different from the node's user. This is less surprising: if user B
|
|
// from a company shares user's C node from the same company with user A who
|
|
// don't know user C, user A might be surprised to see user C listed in
|
|
// their netmap. We've historically (2021-01..2023-08) always shown the
|
|
// sharer's name in the UI. Perhaps we want to show both here? But the CLI's
|
|
// a bit space constrained.
|
|
uid := cmp.Or(ps.AltSharerUserID, ps.UserID)
|
|
if uid.IsZero() {
|
|
return "-"
|
|
}
|
|
u, ok := st.User[uid]
|
|
if !ok {
|
|
return fmt.Sprint(uid)
|
|
}
|
|
if i := strings.Index(u.LoginName, "@"); i != -1 {
|
|
return u.LoginName[:i+1]
|
|
}
|
|
return u.LoginName
|
|
}
|
|
|
|
func firstIPString(v []netip.Addr) string {
|
|
if len(v) == 0 {
|
|
return ""
|
|
}
|
|
return v[0].String()
|
|
}
|