mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-22 08:51:41 +00:00
cmd/tailscale: add status subcommand
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
810c1e9704
commit
a4ef345737
@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package main // import "tailscale.com/cmd/tailscale"
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -10,11 +10,19 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v2/ffcli"
|
||||||
"tailscale.com/derp/derpmap"
|
"tailscale.com/derp/derpmap"
|
||||||
"tailscale.com/net/dnscache"
|
"tailscale.com/net/dnscache"
|
||||||
"tailscale.com/netcheck"
|
"tailscale.com/netcheck"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var netcheckCmd = &ffcli.Command{
|
||||||
|
Name: "netcheck",
|
||||||
|
ShortUsage: "netcheck",
|
||||||
|
ShortHelp: "Print an analysis of local network conditions",
|
||||||
|
Exec: runNetcheck,
|
||||||
|
}
|
||||||
|
|
||||||
func runNetcheck(ctx context.Context, args []string) error {
|
func runNetcheck(ctx context.Context, args []string) error {
|
||||||
c := &netcheck.Client{
|
c := &netcheck.Client{
|
||||||
DERP: derpmap.Prod(),
|
DERP: derpmap.Prod(),
|
||||||
|
142
cmd/tailscale/status.go
Normal file
142
cmd/tailscale/status.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v2/ffcli"
|
||||||
|
"github.com/toqueteos/webbrowser"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/net/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
var statusCmd = &ffcli.Command{
|
||||||
|
Name: "status",
|
||||||
|
ShortUsage: "status [-web] [-json]",
|
||||||
|
ShortHelp: "Show state of tailscaled and its connections",
|
||||||
|
Exec: runStatus,
|
||||||
|
FlagSet: (func() *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet("status", flag.ExitOnError)
|
||||||
|
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.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStatus(ctx context.Context, args []string) error {
|
||||||
|
c, bc, ctx, cancel := connect(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ch := make(chan *ipnstate.Status, 1)
|
||||||
|
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||||
|
if n.ErrMessage != nil {
|
||||||
|
log.Fatal(*n.ErrMessage)
|
||||||
|
}
|
||||||
|
if n.Status != nil {
|
||||||
|
ch <- n.Status
|
||||||
|
}
|
||||||
|
})
|
||||||
|
go pump(ctx, bc, c)
|
||||||
|
|
||||||
|
getStatus := func() (*ipnstate.Status, error) {
|
||||||
|
bc.RequestStatus()
|
||||||
|
select {
|
||||||
|
case st := <-ch:
|
||||||
|
return st, nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st, err := getStatus()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if statusArgs.json {
|
||||||
|
j, err := json.MarshalIndent(st, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%s", j)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if statusArgs.web {
|
||||||
|
ln, err := net.Listen("tcp", statusArgs.listen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
statusURL := interfaces.HTTPOfListener(ln)
|
||||||
|
fmt.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 := getStatus()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||||
|
for _, peer := range st.Peers() {
|
||||||
|
ps := st.Peer[peer]
|
||||||
|
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
||||||
|
peer.ShortString(),
|
||||||
|
ps.OS,
|
||||||
|
ps.TailAddr,
|
||||||
|
ps.SimpleHostName(),
|
||||||
|
ps.TxBytes,
|
||||||
|
ps.RxBytes,
|
||||||
|
)
|
||||||
|
for i, addr := range ps.Addrs {
|
||||||
|
if i != 0 {
|
||||||
|
f(", ")
|
||||||
|
}
|
||||||
|
if addr == ps.CurAddr {
|
||||||
|
f("*%s*", addr)
|
||||||
|
} else {
|
||||||
|
f("%s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f("\n")
|
||||||
|
}
|
||||||
|
os.Stdout.Write(buf.Bytes())
|
||||||
|
return nil
|
||||||
|
}
|
@ -14,6 +14,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
@ -34,18 +35,8 @@ import (
|
|||||||
// later, the global state key doesn't look like a username.
|
// later, the global state key doesn't look like a username.
|
||||||
const globalStateKey = "_daemon"
|
const globalStateKey = "_daemon"
|
||||||
|
|
||||||
// pump receives backend messages on conn and pushes them into bc.
|
var rootArgs struct {
|
||||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
socket string
|
||||||
defer log.Printf("Control connection done.\n")
|
|
||||||
defer conn.Close()
|
|
||||||
for ctx.Err() == nil {
|
|
||||||
msg, err := ipn.ReadMsg(conn)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("ReadMsg: %v\n", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
bc.GotNotifyMsg(msg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -55,7 +46,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||||
upf.StringVar(&upArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
|
||||||
upf.StringVar(&upArgs.server, "login-server", "https://login.tailscale.com", "base URL of control server")
|
upf.StringVar(&upArgs.server, "login-server", "https://login.tailscale.com", "base URL of control server")
|
||||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||||
upf.BoolVar(&upArgs.noSingleRoutes, "no-single-routes", false, "don't install routes to single nodes")
|
upf.BoolVar(&upArgs.noSingleRoutes, "no-single-routes", false, "don't install routes to single nodes")
|
||||||
@ -79,12 +69,8 @@ options are reset to their default.
|
|||||||
Exec: runUp,
|
Exec: runUp,
|
||||||
}
|
}
|
||||||
|
|
||||||
netcheckCmd := &ffcli.Command{
|
rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError)
|
||||||
Name: "netcheck",
|
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
||||||
ShortUsage: "netcheck",
|
|
||||||
ShortHelp: "Print an analysis of local network conditions",
|
|
||||||
Exec: runNetcheck,
|
|
||||||
}
|
|
||||||
|
|
||||||
rootCmd := &ffcli.Command{
|
rootCmd := &ffcli.Command{
|
||||||
Name: "tailscale",
|
Name: "tailscale",
|
||||||
@ -97,8 +83,10 @@ change in the future.
|
|||||||
Subcommands: []*ffcli.Command{
|
Subcommands: []*ffcli.Command{
|
||||||
upCmd,
|
upCmd,
|
||||||
netcheckCmd,
|
netcheckCmd,
|
||||||
|
statusCmd,
|
||||||
},
|
},
|
||||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
FlagSet: rootfs,
|
||||||
|
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rootCmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && err != flag.ErrHelp {
|
if err := rootCmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && err != flag.ErrHelp {
|
||||||
@ -106,14 +94,13 @@ change in the future.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var upArgs = struct {
|
var upArgs struct {
|
||||||
socket string
|
|
||||||
server string
|
server string
|
||||||
acceptRoutes bool
|
acceptRoutes bool
|
||||||
noSingleRoutes bool
|
noSingleRoutes bool
|
||||||
noPacketFilter bool
|
noPacketFilter bool
|
||||||
advertiseRoutes string
|
advertiseRoutes string
|
||||||
}{}
|
}
|
||||||
|
|
||||||
func runUp(ctx context.Context, args []string) error {
|
func runUp(ctx context.Context, args []string) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@ -142,25 +129,9 @@ func runUp(ctx context.Context, args []string) error {
|
|||||||
prefs.UsePacketFilter = !upArgs.noPacketFilter
|
prefs.UsePacketFilter = !upArgs.noPacketFilter
|
||||||
prefs.AdvertiseRoutes = adv
|
prefs.AdvertiseRoutes = adv
|
||||||
|
|
||||||
c, err := safesocket.Connect(upArgs.socket, 41112)
|
c, bc, ctx, cancel := connect(ctx)
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
|
||||||
}
|
|
||||||
clientToServer := func(b []byte) {
|
|
||||||
ipn.WriteMsg(c, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
go func() {
|
|
||||||
interrupt := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-interrupt
|
|
||||||
c.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
|
||||||
bc.SetPrefs(prefs)
|
bc.SetPrefs(prefs)
|
||||||
opts := ipn.Options{
|
opts := ipn.Options{
|
||||||
StateKey: globalStateKey,
|
StateKey: globalStateKey,
|
||||||
@ -197,3 +168,45 @@ func runUp(ctx context.Context, args []string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||||
|
c, err := safesocket.Connect(rootArgs.socket, 41112)
|
||||||
|
if err != nil {
|
||||||
|
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||||
|
log.Fatalf("--socket cannot be empty")
|
||||||
|
}
|
||||||
|
log.Fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||||
|
}
|
||||||
|
clientToServer := func(b []byte) {
|
||||||
|
ipn.WriteMsg(c, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
interrupt := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-interrupt
|
||||||
|
c.Close()
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
||||||
|
return c, bc, ctx, cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
// pump receives backend messages on conn and pushes them into bc.
|
||||||
|
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
msg, err := ipn.ReadMsg(conn)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("ReadMsg: %v\n", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bc.GotNotifyMsg(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1
go.mod
1
go.mod
@ -21,6 +21,7 @@ require (
|
|||||||
github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3 // indirect
|
github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3 // indirect
|
||||||
github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f
|
github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20200325185614-bd634ffe2ded
|
github.com/tailscale/wireguard-go v0.0.0-20200325185614-bd634ffe2ded
|
||||||
|
github.com/toqueteos/webbrowser v1.2.0
|
||||||
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6
|
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||||
|
2
go.sum
2
go.sum
@ -133,6 +133,8 @@ github.com/tailscale/wireguard-go v0.0.0-20200320054525-e913b7c8517d h1:5Hc2ERvH
|
|||||||
github.com/tailscale/wireguard-go v0.0.0-20200320054525-e913b7c8517d/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
github.com/tailscale/wireguard-go v0.0.0-20200320054525-e913b7c8517d/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20200325185614-bd634ffe2ded h1:h5xaqGuzy578xFcIpbBIP1vWeFwggf5RC8PFBEldHr4=
|
github.com/tailscale/wireguard-go v0.0.0-20200325185614-bd634ffe2ded h1:h5xaqGuzy578xFcIpbBIP1vWeFwggf5RC8PFBEldHr4=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20200325185614-bd634ffe2ded/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
github.com/tailscale/wireguard-go v0.0.0-20200325185614-bd634ffe2ded/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
|
||||||
|
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||||
|
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/empty"
|
"tailscale.com/types/empty"
|
||||||
"tailscale.com/wgengine"
|
"tailscale.com/wgengine"
|
||||||
@ -45,15 +46,16 @@ type NetworkMap = controlclient.NetworkMap
|
|||||||
// that they have not changed.
|
// that they have not changed.
|
||||||
// They are JSON-encoded on the wire, despite the lack of struct tags.
|
// They are JSON-encoded on the wire, despite the lack of struct tags.
|
||||||
type Notify struct {
|
type Notify struct {
|
||||||
Version string // version number of IPN backend
|
Version string // version number of IPN backend
|
||||||
ErrMessage *string // critical error message, if any
|
ErrMessage *string // critical error message, if any
|
||||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||||
State *State // current IPN state has changed
|
State *State // current IPN state has changed
|
||||||
Prefs *Prefs // preferences were changed
|
Prefs *Prefs // preferences were changed
|
||||||
NetMap *NetworkMap // new netmap received
|
NetMap *NetworkMap // new netmap received
|
||||||
Engine *EngineStatus // wireguard engine stats
|
Engine *EngineStatus // wireguard engine stats
|
||||||
BrowseToURL *string // UI should open a browser right now
|
Status *ipnstate.Status // full status
|
||||||
BackendLogID *string // public logtail id used by backend
|
BrowseToURL *string // UI should open a browser right now
|
||||||
|
BackendLogID *string // public logtail id used by backend
|
||||||
|
|
||||||
// type is mirrored in xcode/Shared/IPN.swift
|
// type is mirrored in xcode/Shared/IPN.swift
|
||||||
}
|
}
|
||||||
@ -126,6 +128,9 @@ type Backend interface {
|
|||||||
// counts. Connection events are emitted automatically without
|
// counts. Connection events are emitted automatically without
|
||||||
// polling.
|
// polling.
|
||||||
RequestEngineStatus()
|
RequestEngineStatus()
|
||||||
|
// RequestStatus requests that a full Status update
|
||||||
|
// notification is sent.
|
||||||
|
RequestStatus()
|
||||||
// FakeExpireAfter pretends that the current key is going to
|
// FakeExpireAfter pretends that the current key is going to
|
||||||
// expire after duration x. This is useful for testing GUIs to
|
// expire after duration x. This is useful for testing GUIs to
|
||||||
// make sure they react properly with keys that are going to
|
// make sure they react properly with keys that are going to
|
||||||
|
@ -7,6 +7,8 @@ package ipn
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FakeBackend struct {
|
type FakeBackend struct {
|
||||||
@ -71,6 +73,10 @@ func (b *FakeBackend) RequestEngineStatus() {
|
|||||||
b.notify(Notify{Engine: &EngineStatus{}})
|
b.notify(Notify{Engine: &EngineStatus{}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *FakeBackend) RequestStatus() {
|
||||||
|
b.notify(Notify{Status: &ipnstate.Status{}})
|
||||||
|
}
|
||||||
|
|
||||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||||
b.notify(Notify{NetMap: &NetworkMap{}})
|
b.notify(Notify{NetMap: &NetworkMap{}})
|
||||||
}
|
}
|
||||||
|
@ -161,6 +161,10 @@ func (h *Handle) RequestEngineStatus() {
|
|||||||
h.b.RequestEngineStatus()
|
h.b.RequestEngineStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handle) RequestStatus() {
|
||||||
|
h.b.RequestStatus()
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handle) FakeExpireAfter(x time.Duration) {
|
func (h *Handle) FakeExpireAfter(x time.Duration) {
|
||||||
h.b.FakeExpireAfter(x)
|
h.b.FakeExpireAfter(x)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -120,7 +119,10 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w
|
|||||||
|
|
||||||
if opts.DebugMux != nil {
|
if opts.DebugMux != nil {
|
||||||
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
|
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
|
||||||
serveDebugHandler(w, r, logid, opts, b, e)
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
st := b.Status()
|
||||||
|
// TODO(bradfitz): add LogID and opts to st?
|
||||||
|
st.WriteHTML(w)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,101 +313,3 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveDebugHandler(w http.ResponseWriter, r *http.Request, logid string, opts Options, b *ipn.LocalBackend, e wgengine.Engine) {
|
|
||||||
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
|
|
||||||
f(`<html><head><style>
|
|
||||||
.owner { font-size: 80%%; color: #444; }
|
|
||||||
.tailaddr { font-size: 80%%; font-family: monospace: }
|
|
||||||
</style></head>`)
|
|
||||||
f("<body><h1>IPN state</h1><h2>Run args</h2>")
|
|
||||||
f("<p><b>logid:</b> %s</p>\n", logid)
|
|
||||||
f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
|
|
||||||
|
|
||||||
st := b.Status()
|
|
||||||
f("<table border=1 cellpadding=5><tr><th>Peer</th><th>Node</th><th>Rx</th><th>Tx</th><th>Handshake</th><th>Endpoints</th></tr>")
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
// The tailcontrol server rounds LastSeen to 10 minutes. So we
|
|
||||||
// declare that a longAgo seen time of 15 minutes means
|
|
||||||
// they're not connected.
|
|
||||||
longAgo := now.Add(-15 * time.Minute)
|
|
||||||
|
|
||||||
for _, peer := range st.Peers() {
|
|
||||||
ps := st.Peer[peer]
|
|
||||||
var hsAgo string
|
|
||||||
if !ps.LastHandshake.IsZero() {
|
|
||||||
hsAgo = now.Sub(ps.LastHandshake).Round(time.Second).String() + " ago"
|
|
||||||
} else {
|
|
||||||
if ps.LastSeen.Before(longAgo) {
|
|
||||||
hsAgo = "<i>offline</i>"
|
|
||||||
} else if !ps.KeepAlive {
|
|
||||||
hsAgo = "on demand"
|
|
||||||
} else {
|
|
||||||
hsAgo = "<b>pending</b>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var owner string
|
|
||||||
if up, ok := st.User[ps.UserID]; ok {
|
|
||||||
owner = up.LoginName
|
|
||||||
if i := strings.Index(owner, "@"); i != -1 {
|
|
||||||
owner = owner[:i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f("<tr><td>%s</td><td>%s<div class=owner>%s</div><div class=tailaddr>%s</div></td><td>%v</td><td>%v</td><td>%v</td>",
|
|
||||||
peer.ShortString(),
|
|
||||||
osEmoji(ps.OS)+" "+html.EscapeString(simplifyHostname(ps.HostName)),
|
|
||||||
html.EscapeString(owner),
|
|
||||||
ps.TailAddr,
|
|
||||||
ps.RxBytes,
|
|
||||||
ps.TxBytes,
|
|
||||||
hsAgo,
|
|
||||||
)
|
|
||||||
f("<td>")
|
|
||||||
match := false
|
|
||||||
for _, addr := range ps.Addrs {
|
|
||||||
if addr == ps.CurAddr {
|
|
||||||
match = true
|
|
||||||
f("<b>%s</b> 🔗<br>\n", addr)
|
|
||||||
} else {
|
|
||||||
f("%s<br>\n", addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ps.CurAddr != "" && !match {
|
|
||||||
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>\n", ps.CurAddr)
|
|
||||||
}
|
|
||||||
f("</tr>") // end Addrs
|
|
||||||
|
|
||||||
f("</tr>\n")
|
|
||||||
}
|
|
||||||
f("</table>")
|
|
||||||
}
|
|
||||||
|
|
||||||
func osEmoji(os string) string {
|
|
||||||
switch os {
|
|
||||||
case "linux":
|
|
||||||
return "🐧"
|
|
||||||
case "macOS":
|
|
||||||
return "🍎"
|
|
||||||
case "windows":
|
|
||||||
return "🖥️"
|
|
||||||
case "iOS":
|
|
||||||
return "📱"
|
|
||||||
case "android":
|
|
||||||
return "🤖"
|
|
||||||
case "freebsd":
|
|
||||||
return "👿"
|
|
||||||
case "openbsd":
|
|
||||||
return "🐡"
|
|
||||||
}
|
|
||||||
return "👽"
|
|
||||||
}
|
|
||||||
|
|
||||||
func simplifyHostname(s string) string {
|
|
||||||
s = strings.TrimSuffix(s, ".local")
|
|
||||||
s = strings.TrimSuffix(s, ".localdomain")
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
@ -9,8 +9,12 @@ package ipnstate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -66,6 +70,14 @@ type PeerStatus struct {
|
|||||||
InEngine bool
|
InEngine bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SimpleHostName returns a potentially simplified version of ps.HostName for display purposes.
|
||||||
|
func (ps *PeerStatus) SimpleHostName() string {
|
||||||
|
n := ps.HostName
|
||||||
|
n = strings.TrimSuffix(n, ".local")
|
||||||
|
n = strings.TrimSuffix(n, ".localdomain")
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
type StatusBuilder struct {
|
type StatusBuilder struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
locked bool
|
locked bool
|
||||||
@ -170,3 +182,93 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
|||||||
type StatusUpdater interface {
|
type StatusUpdater interface {
|
||||||
UpdateStatus(*StatusBuilder)
|
UpdateStatus(*StatusBuilder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (st *Status) WriteHTML(w io.Writer) {
|
||||||
|
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
|
||||||
|
|
||||||
|
f(`<html><head><style>
|
||||||
|
.owner { font-size: 80%%; color: #444; }
|
||||||
|
.tailaddr { font-size: 80%%; font-family: monospace: }
|
||||||
|
</style></head>`)
|
||||||
|
f("<body><h1>Tailscale State</h1>")
|
||||||
|
//f("<p><b>logid:</b> %s</p>\n", logid)
|
||||||
|
//f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
|
||||||
|
|
||||||
|
f("<table border=1 cellpadding=5><tr><th>Peer</th><th>Node</th><th>Rx</th><th>Tx</th><th>Handshake</th><th>Endpoints</th></tr>")
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// The tailcontrol server rounds LastSeen to 10 minutes. So we
|
||||||
|
// declare that a longAgo seen time of 15 minutes means
|
||||||
|
// they're not connected.
|
||||||
|
longAgo := now.Add(-15 * time.Minute)
|
||||||
|
|
||||||
|
for _, peer := range st.Peers() {
|
||||||
|
ps := st.Peer[peer]
|
||||||
|
var hsAgo string
|
||||||
|
if !ps.LastHandshake.IsZero() {
|
||||||
|
hsAgo = now.Sub(ps.LastHandshake).Round(time.Second).String() + " ago"
|
||||||
|
} else {
|
||||||
|
if ps.LastSeen.Before(longAgo) {
|
||||||
|
hsAgo = "<i>offline</i>"
|
||||||
|
} else if !ps.KeepAlive {
|
||||||
|
hsAgo = "on demand"
|
||||||
|
} else {
|
||||||
|
hsAgo = "<b>pending</b>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var owner string
|
||||||
|
if up, ok := st.User[ps.UserID]; ok {
|
||||||
|
owner = up.LoginName
|
||||||
|
if i := strings.Index(owner, "@"); i != -1 {
|
||||||
|
owner = owner[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f("<tr><td>%s</td><td>%s<div class=owner>%s</div><div class=tailaddr>%s</div></td><td>%v</td><td>%v</td><td>%v</td>",
|
||||||
|
peer.ShortString(),
|
||||||
|
osEmoji(ps.OS)+" "+html.EscapeString(ps.SimpleHostName()),
|
||||||
|
html.EscapeString(owner),
|
||||||
|
ps.TailAddr,
|
||||||
|
ps.RxBytes,
|
||||||
|
ps.TxBytes,
|
||||||
|
hsAgo,
|
||||||
|
)
|
||||||
|
f("<td>")
|
||||||
|
match := false
|
||||||
|
for _, addr := range ps.Addrs {
|
||||||
|
if addr == ps.CurAddr {
|
||||||
|
match = true
|
||||||
|
f("<b>%s</b> 🔗<br>\n", addr)
|
||||||
|
} else {
|
||||||
|
f("%s<br>\n", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ps.CurAddr != "" && !match {
|
||||||
|
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>\n", ps.CurAddr)
|
||||||
|
}
|
||||||
|
f("</tr>") // end Addrs
|
||||||
|
|
||||||
|
f("</tr>\n")
|
||||||
|
}
|
||||||
|
f("</table>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func osEmoji(os string) string {
|
||||||
|
switch os {
|
||||||
|
case "linux":
|
||||||
|
return "🐧"
|
||||||
|
case "macOS":
|
||||||
|
return "🍎"
|
||||||
|
case "windows":
|
||||||
|
return "🖥️"
|
||||||
|
case "iOS":
|
||||||
|
return "📱"
|
||||||
|
case "android":
|
||||||
|
return "🤖"
|
||||||
|
case "freebsd":
|
||||||
|
return "👿"
|
||||||
|
case "openbsd":
|
||||||
|
return "🐡"
|
||||||
|
}
|
||||||
|
return "👽"
|
||||||
|
}
|
||||||
|
11
ipn/local.go
11
ipn/local.go
@ -499,6 +499,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// State returns the backend's state.
|
||||||
func (b *LocalBackend) State() State {
|
func (b *LocalBackend) State() State {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
@ -506,6 +507,11 @@ func (b *LocalBackend) State() State {
|
|||||||
return b.state
|
return b.state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EngineStatus returns the engine status. See also: Status, and State.
|
||||||
|
//
|
||||||
|
// TODO(bradfitz): deprecated this and merge it with the Status method
|
||||||
|
// that returns ipnstate.Status? Maybe have that take flags for what info
|
||||||
|
// the caller cares about?
|
||||||
func (b *LocalBackend) EngineStatus() EngineStatus {
|
func (b *LocalBackend) EngineStatus() EngineStatus {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
@ -785,6 +791,11 @@ func (b *LocalBackend) RequestEngineStatus() {
|
|||||||
b.e.RequestStatus()
|
b.e.RequestStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) RequestStatus() {
|
||||||
|
st := b.Status()
|
||||||
|
b.notify(Notify{Status: st})
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(apenwarr): use a channel or something to prevent re-entrancy?
|
// TODO(apenwarr): use a channel or something to prevent re-entrancy?
|
||||||
// Or maybe just call the state machine from fewer places.
|
// Or maybe just call the state machine from fewer places.
|
||||||
func (b *LocalBackend) stateMachine() {
|
func (b *LocalBackend) stateMachine() {
|
||||||
|
@ -43,6 +43,7 @@ type Command struct {
|
|||||||
Logout *NoArgs
|
Logout *NoArgs
|
||||||
SetPrefs *SetPrefsArgs
|
SetPrefs *SetPrefsArgs
|
||||||
RequestEngineStatus *NoArgs
|
RequestEngineStatus *NoArgs
|
||||||
|
RequestStatus *NoArgs
|
||||||
FakeExpireAfter *FakeExpireAfterArgs
|
FakeExpireAfter *FakeExpireAfterArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,6 +116,9 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
|||||||
} else if c := cmd.RequestEngineStatus; c != nil {
|
} else if c := cmd.RequestEngineStatus; c != nil {
|
||||||
bs.b.RequestEngineStatus()
|
bs.b.RequestEngineStatus()
|
||||||
return nil
|
return nil
|
||||||
|
} else if c := cmd.RequestStatus; c != nil {
|
||||||
|
bs.b.RequestStatus()
|
||||||
|
return nil
|
||||||
} else if c := cmd.FakeExpireAfter; c != nil {
|
} else if c := cmd.FakeExpireAfter; c != nil {
|
||||||
bs.b.FakeExpireAfter(c.Duration)
|
bs.b.FakeExpireAfter(c.Duration)
|
||||||
return nil
|
return nil
|
||||||
@ -172,6 +176,10 @@ func (bc *BackendClient) send(cmd Command) {
|
|||||||
bc.sendCommandMsg(b)
|
bc.sendCommandMsg(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bc *BackendClient) SetNotifyCallback(fn func(Notify)) {
|
||||||
|
bc.notify = fn
|
||||||
|
}
|
||||||
|
|
||||||
func (bc *BackendClient) Quit() error {
|
func (bc *BackendClient) Quit() error {
|
||||||
bc.send(Command{Quit: &NoArgs{}})
|
bc.send(Command{Quit: &NoArgs{}})
|
||||||
return nil
|
return nil
|
||||||
@ -200,6 +208,10 @@ func (bc *BackendClient) RequestEngineStatus() {
|
|||||||
bc.send(Command{RequestEngineStatus: &NoArgs{}})
|
bc.send(Command{RequestEngineStatus: &NoArgs{}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bc *BackendClient) RequestStatus() {
|
||||||
|
bc.send(Command{RequestStatus: &NoArgs{}})
|
||||||
|
}
|
||||||
|
|
||||||
func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
|
func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
|
||||||
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
|
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package interfaces
|
package interfaces
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@ -168,22 +169,6 @@ func ForeachInterfaceAddress(fn func(Interface, net.IP)) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var cgNAT = func() *net.IPNet {
|
|
||||||
_, ipNet, err := net.ParseCIDR("100.64.0.0/10")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return ipNet
|
|
||||||
}()
|
|
||||||
|
|
||||||
var linkLocalIPv4 = func() *net.IPNet {
|
|
||||||
_, ipNet, err := net.ParseCIDR("169.254.0.0/16")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return ipNet
|
|
||||||
}()
|
|
||||||
|
|
||||||
// State is intended to store the state of the machine's network interfaces,
|
// State is intended to store the state of the machine's network interfaces,
|
||||||
// routing table, and other network configuration.
|
// routing table, and other network configuration.
|
||||||
// For now it's pretty basic.
|
// For now it's pretty basic.
|
||||||
@ -216,3 +201,53 @@ func GetState() (*State, error) {
|
|||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPOfListener returns the HTTP address to ln.
|
||||||
|
// If the listener is listening on the unspecified address, it
|
||||||
|
// it tries to find a reasonable interface address on the machine to use.
|
||||||
|
func HTTPOfListener(ln net.Listener) string {
|
||||||
|
ta, ok := ln.Addr().(*net.TCPAddr)
|
||||||
|
if !ok || !ta.IP.IsUnspecified() {
|
||||||
|
return fmt.Sprintf("http://%v/", ln.Addr())
|
||||||
|
}
|
||||||
|
|
||||||
|
var goodIP string
|
||||||
|
var privateIP string
|
||||||
|
ForeachInterfaceAddress(func(i Interface, ip net.IP) {
|
||||||
|
if isPrivateIP(ip) {
|
||||||
|
if privateIP == "" {
|
||||||
|
privateIP = ip.String()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
goodIP = ip.String()
|
||||||
|
})
|
||||||
|
if privateIP != "" {
|
||||||
|
goodIP = privateIP
|
||||||
|
}
|
||||||
|
if goodIP != "" {
|
||||||
|
return fmt.Sprintf("http://%v/", net.JoinHostPort(goodIP, fmt.Sprint(ta.Port)))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("http://localhost:%v/", fmt.Sprint(ta.Port))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPrivateIP(ip net.IP) bool {
|
||||||
|
return private1.Contains(ip) || private2.Contains(ip) || private3.Contains(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustCIDR(s string) *net.IPNet {
|
||||||
|
_, ipNet, err := net.ParseCIDR(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return ipNet
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
private1 = mustCIDR("10.0.0.0/8")
|
||||||
|
private2 = mustCIDR("172.16.0.0/12")
|
||||||
|
private3 = mustCIDR("192.168.0.0/16")
|
||||||
|
cgNAT = mustCIDR("100.64.0.0/10")
|
||||||
|
linkLocalIPv4 = mustCIDR("169.254.0.0/16")
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user