From a4ef345737abf756ccb671dc5863dfc0518e67ec Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 27 Mar 2020 13:26:35 -0700 Subject: [PATCH] cmd/tailscale: add status subcommand Signed-off-by: Brad Fitzpatrick --- cmd/tailscale/netcheck.go | 10 ++- cmd/tailscale/status.go | 142 +++++++++++++++++++++++++++++++++++ cmd/tailscale/tailscale.go | 93 +++++++++++++---------- go.mod | 1 + go.sum | 2 + ipn/backend.go | 23 +++--- ipn/fake_test.go | 6 ++ ipn/handle.go | 4 + ipn/ipnserver/server.go | 104 +------------------------ ipn/ipnstate/ipnstate.go | 102 +++++++++++++++++++++++++ ipn/local.go | 11 +++ ipn/message.go | 12 +++ net/interfaces/interfaces.go | 67 +++++++++++++---- 13 files changed, 411 insertions(+), 166 deletions(-) create mode 100644 cmd/tailscale/status.go diff --git a/cmd/tailscale/netcheck.go b/cmd/tailscale/netcheck.go index ae5849c19..2ee1c23ca 100644 --- a/cmd/tailscale/netcheck.go +++ b/cmd/tailscale/netcheck.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package main // import "tailscale.com/cmd/tailscale" +package main import ( "context" @@ -10,11 +10,19 @@ import ( "log" "sort" + "github.com/peterbourgon/ff/v2/ffcli" "tailscale.com/derp/derpmap" "tailscale.com/net/dnscache" "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 { c := &netcheck.Client{ DERP: derpmap.Prod(), diff --git a/cmd/tailscale/status.go b/cmd/tailscale/status.go new file mode 100644 index 000000000..2b465838f --- /dev/null +++ b/cmd/tailscale/status.go @@ -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 +} diff --git a/cmd/tailscale/tailscale.go b/cmd/tailscale/tailscale.go index 95b2b2817..57f7cbdf7 100644 --- a/cmd/tailscale/tailscale.go +++ b/cmd/tailscale/tailscale.go @@ -14,6 +14,7 @@ import ( "net" "os" "os/signal" + "runtime" "strings" "syscall" @@ -34,18 +35,8 @@ import ( // later, the global state key doesn't look like a username. const globalStateKey = "_daemon" -// pump receives backend messages on conn and pushes them into bc. -func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) { - 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) - } +var rootArgs struct { + socket string } func main() { @@ -55,7 +46,6 @@ func main() { } 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.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") @@ -79,12 +69,8 @@ options are reset to their default. Exec: runUp, } - netcheckCmd := &ffcli.Command{ - Name: "netcheck", - ShortUsage: "netcheck", - ShortHelp: "Print an analysis of local network conditions", - Exec: runNetcheck, - } + rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError) + rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket") rootCmd := &ffcli.Command{ Name: "tailscale", @@ -97,8 +83,10 @@ change in the future. Subcommands: []*ffcli.Command{ upCmd, 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 { @@ -106,14 +94,13 @@ change in the future. } } -var upArgs = struct { - socket string +var upArgs struct { server string acceptRoutes bool noSingleRoutes bool noPacketFilter bool advertiseRoutes string -}{} +} func runUp(ctx context.Context, args []string) error { if len(args) > 0 { @@ -142,25 +129,9 @@ func runUp(ctx context.Context, args []string) error { prefs.UsePacketFilter = !upArgs.noPacketFilter prefs.AdvertiseRoutes = adv - c, err := safesocket.Connect(upArgs.socket, 41112) - 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) + c, bc, ctx, cancel := connect(ctx) 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) opts := ipn.Options{ StateKey: globalStateKey, @@ -197,3 +168,45 @@ func runUp(ctx context.Context, args []string) error { 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) + } +} diff --git a/go.mod b/go.mod index 0e037c689..e614ff432 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3 // indirect github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f 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/net v0.0.0-20200301022130-244492dfa37a // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d diff --git a/go.sum b/go.sum index a13b66c25..c0aa26d9e 100644 --- a/go.sum +++ b/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-20200325185614-bd634ffe2ded h1:h5xaqGuzy578xFcIpbBIP1vWeFwggf5RC8PFBEldHr4= 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/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= diff --git a/ipn/backend.go b/ipn/backend.go index 19c809a0a..f47477207 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -8,6 +8,7 @@ import ( "time" "tailscale.com/control/controlclient" + "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" "tailscale.com/types/empty" "tailscale.com/wgengine" @@ -45,15 +46,16 @@ type NetworkMap = controlclient.NetworkMap // that they have not changed. // They are JSON-encoded on the wire, despite the lack of struct tags. type Notify struct { - Version string // version number of IPN backend - ErrMessage *string // critical error message, if any - LoginFinished *empty.Message // event: non-nil when login process succeeded - State *State // current IPN state has changed - Prefs *Prefs // preferences were changed - NetMap *NetworkMap // new netmap received - Engine *EngineStatus // wireguard engine stats - BrowseToURL *string // UI should open a browser right now - BackendLogID *string // public logtail id used by backend + Version string // version number of IPN backend + ErrMessage *string // critical error message, if any + LoginFinished *empty.Message // event: non-nil when login process succeeded + State *State // current IPN state has changed + Prefs *Prefs // preferences were changed + NetMap *NetworkMap // new netmap received + Engine *EngineStatus // wireguard engine stats + Status *ipnstate.Status // full status + 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 } @@ -126,6 +128,9 @@ type Backend interface { // counts. Connection events are emitted automatically without // polling. RequestEngineStatus() + // RequestStatus requests that a full Status update + // notification is sent. + RequestStatus() // FakeExpireAfter pretends that the current key is going to // expire after duration x. This is useful for testing GUIs to // make sure they react properly with keys that are going to diff --git a/ipn/fake_test.go b/ipn/fake_test.go index c0191cb00..dc9137358 100644 --- a/ipn/fake_test.go +++ b/ipn/fake_test.go @@ -7,6 +7,8 @@ package ipn import ( "log" "time" + + "tailscale.com/ipn/ipnstate" ) type FakeBackend struct { @@ -71,6 +73,10 @@ func (b *FakeBackend) RequestEngineStatus() { b.notify(Notify{Engine: &EngineStatus{}}) } +func (b *FakeBackend) RequestStatus() { + b.notify(Notify{Status: &ipnstate.Status{}}) +} + func (b *FakeBackend) FakeExpireAfter(x time.Duration) { b.notify(Notify{NetMap: &NetworkMap{}}) } diff --git a/ipn/handle.go b/ipn/handle.go index 2d05ee91d..3ac952b7e 100644 --- a/ipn/handle.go +++ b/ipn/handle.go @@ -161,6 +161,10 @@ func (h *Handle) RequestEngineStatus() { h.b.RequestEngineStatus() } +func (h *Handle) RequestStatus() { + h.b.RequestStatus() +} + func (h *Handle) FakeExpireAfter(x time.Duration) { h.b.FakeExpireAfter(x) } diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 283f32006..0e71bb913 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -8,7 +8,6 @@ import ( "bufio" "context" "fmt" - "html" "log" "net" "net/http" @@ -120,7 +119,10 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w if opts.DebugMux != nil { 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(``) - f("

IPN state

Run args

") - f("

logid: %s

\n", logid) - f("

opts: %s

\n", html.EscapeString(fmt.Sprintf("%+v", opts))) - - st := b.Status() - f("") - - 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 = "offline" - } else if !ps.KeepAlive { - hsAgo = "on demand" - } else { - hsAgo = "pending" - } - } - 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("", - peer.ShortString(), - osEmoji(ps.OS)+" "+html.EscapeString(simplifyHostname(ps.HostName)), - html.EscapeString(owner), - ps.TailAddr, - ps.RxBytes, - ps.TxBytes, - hsAgo, - ) - f("") // end Addrs - - f("\n") - } - f("
PeerNodeRxTxHandshakeEndpoints
%s%s
%s
%s
%v%v%v") - match := false - for _, addr := range ps.Addrs { - if addr == ps.CurAddr { - match = true - f("%s 🔗
\n", addr) - } else { - f("%s
\n", addr) - } - } - if ps.CurAddr != "" && !match { - f("%s \xf0\x9f\xa7\xb3
\n", ps.CurAddr) - } - f("
") -} - -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 -} diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index d2f26aa20..cc01d31fe 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -9,8 +9,12 @@ package ipnstate import ( "bytes" + "fmt" + "html" + "io" "log" "sort" + "strings" "sync" "time" @@ -66,6 +70,14 @@ type PeerStatus struct { 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 { mu sync.Mutex locked bool @@ -170,3 +182,93 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) { type StatusUpdater interface { UpdateStatus(*StatusBuilder) } + +func (st *Status) WriteHTML(w io.Writer) { + f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) } + + f(``) + f("

Tailscale State

") + //f("

logid: %s

\n", logid) + //f("

opts: %s

\n", html.EscapeString(fmt.Sprintf("%+v", opts))) + + f("") + + 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 = "offline" + } else if !ps.KeepAlive { + hsAgo = "on demand" + } else { + hsAgo = "pending" + } + } + 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("", + peer.ShortString(), + osEmoji(ps.OS)+" "+html.EscapeString(ps.SimpleHostName()), + html.EscapeString(owner), + ps.TailAddr, + ps.RxBytes, + ps.TxBytes, + hsAgo, + ) + f("") // end Addrs + + f("\n") + } + f("
PeerNodeRxTxHandshakeEndpoints
%s%s
%s
%s
%v%v%v") + match := false + for _, addr := range ps.Addrs { + if addr == ps.CurAddr { + match = true + f("%s 🔗
\n", addr) + } else { + f("%s
\n", addr) + } + } + if ps.CurAddr != "" && !match { + f("%s \xf0\x9f\xa7\xb3
\n", ps.CurAddr) + } + f("
") +} + +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 "👽" +} diff --git a/ipn/local.go b/ipn/local.go index 944fd29ec..abf8d62f5 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -499,6 +499,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st return nil } +// State returns the backend's state. func (b *LocalBackend) State() State { b.mu.Lock() defer b.mu.Unlock() @@ -506,6 +507,11 @@ func (b *LocalBackend) State() 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 { b.mu.Lock() defer b.mu.Unlock() @@ -785,6 +791,11 @@ func (b *LocalBackend) RequestEngineStatus() { 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? // Or maybe just call the state machine from fewer places. func (b *LocalBackend) stateMachine() { diff --git a/ipn/message.go b/ipn/message.go index bb263cf87..d176ca8e6 100644 --- a/ipn/message.go +++ b/ipn/message.go @@ -43,6 +43,7 @@ type Command struct { Logout *NoArgs SetPrefs *SetPrefsArgs RequestEngineStatus *NoArgs + RequestStatus *NoArgs FakeExpireAfter *FakeExpireAfterArgs } @@ -115,6 +116,9 @@ func (bs *BackendServer) GotCommand(cmd *Command) error { } else if c := cmd.RequestEngineStatus; c != nil { bs.b.RequestEngineStatus() return nil + } else if c := cmd.RequestStatus; c != nil { + bs.b.RequestStatus() + return nil } else if c := cmd.FakeExpireAfter; c != nil { bs.b.FakeExpireAfter(c.Duration) return nil @@ -172,6 +176,10 @@ func (bc *BackendClient) send(cmd Command) { bc.sendCommandMsg(b) } +func (bc *BackendClient) SetNotifyCallback(fn func(Notify)) { + bc.notify = fn +} + func (bc *BackendClient) Quit() error { bc.send(Command{Quit: &NoArgs{}}) return nil @@ -200,6 +208,10 @@ func (bc *BackendClient) RequestEngineStatus() { bc.send(Command{RequestEngineStatus: &NoArgs{}}) } +func (bc *BackendClient) RequestStatus() { + bc.send(Command{RequestStatus: &NoArgs{}}) +} + func (bc *BackendClient) FakeExpireAfter(x time.Duration) { bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}}) } diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go index 11804dd61..6e9e410b5 100644 --- a/net/interfaces/interfaces.go +++ b/net/interfaces/interfaces.go @@ -6,6 +6,7 @@ package interfaces import ( + "fmt" "net" "reflect" "strings" @@ -168,22 +169,6 @@ func ForeachInterfaceAddress(fn func(Interface, net.IP)) error { 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, // routing table, and other network configuration. // For now it's pretty basic. @@ -216,3 +201,53 @@ func GetState() (*State, error) { } 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") +)