Compare commits
9 Commits
crawshaw/c
...
crawshaw/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cf0fdb760 | ||
|
|
33bc69cf1f | ||
|
|
3a1eae5b6b | ||
|
|
1e26d4ae19 | ||
|
|
eeacf84dae | ||
|
|
41e4e02e57 | ||
|
|
9659ab81e0 | ||
|
|
12ae2d73b3 | ||
|
|
f0863346c2 |
@@ -16,10 +16,14 @@ import (
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// TailscaledSocket is the tailscaled Unix socket.
|
||||
var TailscaledSocket = paths.DefaultTailscaledSocket()
|
||||
|
||||
// tsClient does HTTP requests to the local Tailscale daemon.
|
||||
var tsClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
@@ -27,14 +31,16 @@ var tsClient = &http.Client{
|
||||
if addr != "local-tailscaled.sock:80" {
|
||||
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||
}
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
if TailscaledSocket == paths.DefaultTailscaledSocket() {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.ConnectDefault()
|
||||
return safesocket.Connect(TailscaledSocket, 41112)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
@@ -68,6 +69,8 @@ change in the future.
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
pushCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
@@ -86,6 +89,8 @@ change in the future.
|
||||
return err
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
|
||||
@@ -83,37 +83,11 @@ func runPing(ctx context.Context, args []string) error {
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
hostOrIP := args[0]
|
||||
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
ip = hostOrIP
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
if ip == "" {
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
ip = ps.TailAddr
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
if ip == "" {
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
@@ -143,7 +117,11 @@ func runPing(ctx context.Context, args []string) error {
|
||||
via = "TSMP"
|
||||
}
|
||||
anyPong = true
|
||||
fmt.Printf("pong from %s (%s) via %v in %v\n", pr.NodeName, pr.NodeIP, via, latency)
|
||||
extra := ""
|
||||
if pr.PeerAPIPort != 0 {
|
||||
extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
|
||||
}
|
||||
fmt.Printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
|
||||
if pingArgs.tsmp {
|
||||
return nil
|
||||
}
|
||||
@@ -162,3 +140,31 @@ func runPing(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, err error) {
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
return hostOrIP, nil
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
return ps.TailAddr, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return "", fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return "", fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
return addrs[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
173
cmd/tailscale/cli/push.go
Normal file
173
cmd/tailscale/cli/push.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2021 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 cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
var pushCmd = &ffcli.Command{
|
||||
Name: "push",
|
||||
ShortUsage: "push [--flags] <hostname-or-IP> <file>",
|
||||
ShortHelp: "Push a file to a host",
|
||||
Exec: runPush,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("push", flag.ExitOnError)
|
||||
fs.StringVar(&pushArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
|
||||
fs.BoolVar(&pushArgs.verbose, "verbose", false, "verbose output")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var pushArgs struct {
|
||||
name string
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func runPush(ctx context.Context, args []string) error {
|
||||
if len(args) != 2 || args[0] == "" {
|
||||
return errors.New("usage: push <hostname-or-IP> <file>")
|
||||
}
|
||||
var ip string
|
||||
|
||||
hostOrIP, fileArg := args[0], args[1]
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerAPIPort, err := discoverPeerAPIPort(ctx, ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileContents io.Reader
|
||||
var name = pushArgs.name
|
||||
if fileArg == "-" {
|
||||
fileContents = os.Stdin
|
||||
if name == "" {
|
||||
name, fileContents, err = pickStdinFilename()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
f, err := os.Open(fileArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fileContents = f
|
||||
if name == "" {
|
||||
name = fileArg
|
||||
}
|
||||
}
|
||||
|
||||
dstURL := "http://" + net.JoinHostPort(ip, fmt.Sprint(peerAPIPort)) + "/v0/put/" + url.PathEscape(name)
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pushArgs.verbose {
|
||||
log.Printf("sending to %v ...", dstURL)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode == 200 {
|
||||
return nil
|
||||
}
|
||||
io.Copy(os.Stdout, res.Body)
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
|
||||
func discoverPeerAPIPort(ctx context.Context, ip string) (port uint16, err error) {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
prc := make(chan *ipnstate.PingResult, 2)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
prc <- pr
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
discoPings := 0
|
||||
timer := time.NewTimer(10 * time.Second)
|
||||
defer timer.Stop()
|
||||
|
||||
sendPings := func() {
|
||||
bc.Ping(ip, false)
|
||||
bc.Ping(ip, true)
|
||||
}
|
||||
sendPings()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
sendPings()
|
||||
case <-timer.C:
|
||||
return 0, fmt.Errorf("timeout contacting %v; it offline?", ip)
|
||||
case pr := <-prc:
|
||||
if p := pr.PeerAPIPort; p != 0 {
|
||||
return p, nil
|
||||
}
|
||||
discoPings++
|
||||
if discoPings == 3 {
|
||||
return 0, fmt.Errorf("%v is online, but seems to be running an old Tailscale version", ip)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const maxSniff = 4 << 20
|
||||
|
||||
func ext(b []byte) string {
|
||||
if len(b) < maxSniff && utf8.Valid(b) {
|
||||
return ".txt"
|
||||
}
|
||||
if exts, _ := mime.ExtensionsByType(http.DetectContentType(b)); len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// pickStdinFilename reads a bit of stdin to return a good filename
|
||||
// for its contents. The returned Reader is the concatenation of the
|
||||
// read and unread bits.
|
||||
func pickStdinFilename() (name string, r io.Reader, err error) {
|
||||
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
|
||||
}
|
||||
212
cmd/tailscale/cli/web.go
Normal file
212
cmd/tailscale/cli/web.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// Copyright (c) 2021 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 cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cgi"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
//go:embed web.html
|
||||
var webHTML string
|
||||
|
||||
var tmpl = template.Must(template.New("html").Parse(webHTML))
|
||||
|
||||
type tmplData struct {
|
||||
SynologyUser string
|
||||
Status string
|
||||
DeviceName string
|
||||
IP string
|
||||
}
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
Name: "web",
|
||||
ShortUsage: "web [flags]",
|
||||
ShortHelp: "Run a web server for controlling Tailscale",
|
||||
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
webf := flag.NewFlagSet("web", flag.ExitOnError)
|
||||
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
||||
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
||||
return webf
|
||||
})(),
|
||||
Exec: runWeb,
|
||||
}
|
||||
|
||||
var webArgs struct {
|
||||
listen string
|
||||
cgi bool
|
||||
}
|
||||
|
||||
func runWeb(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if webArgs.cgi {
|
||||
return cgi.Serve(http.HandlerFunc(webHandler))
|
||||
}
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||
}
|
||||
|
||||
func auth() (string, error) {
|
||||
if distro.Get() == distro.Synology {
|
||||
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("auth: %v: %s", err, out)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
||||
if distro.Get() != distro.Synology {
|
||||
return false
|
||||
}
|
||||
if r.Header.Get("X-Syno-Token") != "" {
|
||||
return false
|
||||
}
|
||||
if r.URL.Query().Get("SynoToken") != "" {
|
||||
return false
|
||||
}
|
||||
if r.Method == "POST" && r.FormValue("SynoToken") != "" {
|
||||
return false
|
||||
}
|
||||
// We need a SynoToken for authenticate.cgi.
|
||||
// So we tell the client to get one.
|
||||
serverURL := r.URL.Scheme + "://" + r.URL.Host
|
||||
fmt.Fprintf(w, synoTokenRedirectHTML, serverURL)
|
||||
return true
|
||||
}
|
||||
|
||||
const synoTokenRedirectHTML = `<html><body>
|
||||
Redirecting with session token...
|
||||
<script>
|
||||
var serverURL = %q;
|
||||
var req = new XMLHttpRequest();
|
||||
req.overrideMimeType("application/json");
|
||||
req.open("GET", serverURL + "/webman/login.cgi", true);
|
||||
req.onload = function() {
|
||||
var jsonResponse = JSON.parse(req.responseText);
|
||||
var token = jsonResponse["SynoToken"];
|
||||
document.location.href = serverURL + "/webman/3rdparty/Tailscale/?SynoToken=" + token;
|
||||
};
|
||||
req.send(null);
|
||||
</script>
|
||||
</body></html>
|
||||
`
|
||||
|
||||
func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if synoTokenRedirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := auth()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
type mi map[string]interface{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
url, err := tailscaleUp(r.Context())
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(mi{"error": err})
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(mi{"url": url})
|
||||
return
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
|
||||
data := tmplData{
|
||||
SynologyUser: user,
|
||||
Status: st.BackendState,
|
||||
DeviceName: st.Self.DNSName,
|
||||
}
|
||||
if len(st.TailscaleIPs) != 0 {
|
||||
data.IP = st.TailscaleIPs[0].String()
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tmpl.Execute(buf, data); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
|
||||
func tailscaleUp(ctx context.Context) (authURL string, retErr error) {
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = "https://login.tailscale.com"
|
||||
prefs.WantRunning = true
|
||||
prefs.CorpDNS = true
|
||||
prefs.AllowSingleHosts = true
|
||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
|
||||
opts := ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
msg += " (Tailscale service in use by other user?)"
|
||||
default:
|
||||
msg += " (try 'sudo tailscale up [...]')"
|
||||
}
|
||||
}
|
||||
retErr = fmt.Errorf("backend error: %v", msg)
|
||||
cancel()
|
||||
} else if url := n.BrowseToURL; url != nil {
|
||||
authURL = *url
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
}
|
||||
bc.Start(opts)
|
||||
bc.StartLoginInteractive()
|
||||
pump(ctx, bc, c)
|
||||
|
||||
if authURL == "" && retErr == nil {
|
||||
return "", fmt.Errorf("login failed with no backend error message")
|
||||
}
|
||||
return authURL, retErr
|
||||
}
|
||||
47
cmd/tailscale/cli/web.html
Normal file
47
cmd/tailscale/cli/web.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html><title>Tailscale Client</title><body>
|
||||
<h1>Tailscale</h1>
|
||||
<div style="float:right;">{{.SynologyUser}}</div>
|
||||
<table>
|
||||
<tr><th>Status:</th><td>{{.Status}}</td></tr>
|
||||
<tr><th>Device Name:</th><td>{{.DeviceName}}</td></tr>
|
||||
<tr><th>Tailscale IP:</th><td>{{.IP}}</td></tr>
|
||||
</table>
|
||||
|
||||
<p><input id="login" type="button" value="Log in…"></p>
|
||||
|
||||
<script>
|
||||
login.onclick = function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get("SynoToken");
|
||||
|
||||
var params = new URLSearchParams("up=true");
|
||||
if (token) {
|
||||
params.set("SynoToken", token)
|
||||
}
|
||||
|
||||
var req = new XMLHttpRequest();
|
||||
const url = [location.protocol, '//', location.host, location.pathname, "?", params.toString()].join('');
|
||||
req.overrideMimeType("application/json");
|
||||
req.open("POST", url, true);
|
||||
req.onload = function() {
|
||||
var jsonResponse = JSON.parse(req.responseText);
|
||||
const err = jsonResponse["error"];
|
||||
if (err) {
|
||||
document.body.innerText = err;
|
||||
return
|
||||
}
|
||||
var url = jsonResponse["url"];
|
||||
console.log("jsonResponse: ", jsonResponse);
|
||||
if (url) {
|
||||
document.location.href = url;
|
||||
} else {
|
||||
//location.reload();
|
||||
}
|
||||
};
|
||||
req.send(null);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -67,7 +67,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
@@ -115,6 +115,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
embed from tailscale.com/cmd/tailscale/cli
|
||||
encoding from encoding/json
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
@@ -130,7 +131,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate
|
||||
html from tailscale.com/ipn/ipnstate+
|
||||
html/template from tailscale.com/cmd/tailscale/cli
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from golang.org/x/sys/cpu+
|
||||
@@ -144,6 +146,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/cgi from tailscale.com/cmd/tailscale/cli
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/internal from net/http
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
@@ -154,7 +157,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
path from debug/dwarf+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from rsc.io/goversion/version
|
||||
regexp from rsc.io/goversion/version+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
sort from compress/flate+
|
||||
@@ -164,6 +167,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
|
||||
@@ -9,12 +9,18 @@ package main // import "tailscale.com/cmd/tailscale"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/cmd/tailscale/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cli.Run(os.Args[1:]); err != nil {
|
||||
args := os.Args[1:]
|
||||
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
|
||||
args = []string{"web", "-cgi"}
|
||||
}
|
||||
if err := cli.Run(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -229,7 +229,10 @@ func run() error {
|
||||
|
||||
var ns *netstack.Impl
|
||||
if useNetstack {
|
||||
tunDev, magicConn := e.(wgengine.InternalsGetter).GetInternals()
|
||||
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
log.Fatalf("%T is not a wgengine.InternalsGetter", e)
|
||||
}
|
||||
ns, err = netstack.Create(logf, tunDev, e, magicConn)
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
|
||||
@@ -950,7 +950,7 @@ func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, e
|
||||
return nil, err
|
||||
}
|
||||
if debugMap {
|
||||
if _, ok := v.(tailcfg.MapRequest); ok {
|
||||
if _, ok := v.(*tailcfg.MapRequest); ok {
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,17 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
|
||||
b.unregisterHealthWatch = health.RegisterWatcher(b.onHealthChange)
|
||||
|
||||
wiredPeerAPIPort := false
|
||||
if ig, ok := e.(wgengine.InternalsGetter); ok {
|
||||
if tunWrap, _, ok := ig.GetInternals(); ok {
|
||||
tunWrap.PeerAPIPort = b.getPeerAPIPortForTSMPPing
|
||||
wiredPeerAPIPort = true
|
||||
}
|
||||
}
|
||||
if !wiredPeerAPIPort {
|
||||
b.logf("[unexpected] failed to wire up peer API port for engine %T", e)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -1330,25 +1341,58 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||
b.send(ipn.Notify{Prefs: newp})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
if pln.ip.BitLen() == ip.BitLen() {
|
||||
return uint16(pln.Port()), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
proto := tailcfg.ServiceProto("peerapi4")
|
||||
if pln.ip.Is6() {
|
||||
proto = "peerapi6"
|
||||
}
|
||||
ret = append(ret, tailcfg.Service{
|
||||
Proto: proto,
|
||||
Port: uint16(pln.Port()),
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
|
||||
// possibly after mangling the given hostinfo.
|
||||
//
|
||||
// TODO(danderson): we shouldn't be mangling hostinfo here after
|
||||
// painstakingly constructing it in twelvety other places.
|
||||
func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
|
||||
hi2 := *hi
|
||||
b.mu.Lock()
|
||||
cc := b.c
|
||||
if cc == nil {
|
||||
// Control client isn't up yet.
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
peerAPIServices := b.peerAPIServicesLocked()
|
||||
b.mu.Unlock()
|
||||
|
||||
// Make a shallow copy of hostinfo so we can mutate
|
||||
// at the Service field.
|
||||
hi2 := *hi // shallow copy
|
||||
if !b.shouldUploadServices() {
|
||||
hi2.Services = []tailcfg.Service{}
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
cli := b.c
|
||||
b.mu.Unlock()
|
||||
|
||||
// b.c might not be started yet
|
||||
if cli != nil {
|
||||
cli.SetHostinfo(&hi2)
|
||||
}
|
||||
// Don't mutate hi.Service's underlying array. Append to
|
||||
// the slice with no free capacity.
|
||||
c := len(hi2.Services)
|
||||
hi2.Services = append(hi2.Services[:c:c], peerAPIServices...)
|
||||
cc.SetHostinfo(&hi2)
|
||||
}
|
||||
|
||||
// NetMap returns the latest cached network map received from
|
||||
@@ -1471,8 +1515,9 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
|
||||
var tunName string
|
||||
if ge, ok := b.e.(wgengine.InternalsGetter); ok {
|
||||
tunDev, _ := ge.GetInternals()
|
||||
tunName, _ = tunDev.Name()
|
||||
if tunWrap, _, ok := ge.GetInternals(); ok {
|
||||
tunName, _ = tunWrap.Name()
|
||||
}
|
||||
}
|
||||
|
||||
ps := &peerAPIServer{
|
||||
@@ -1490,6 +1535,7 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
}
|
||||
pln := &peerAPIListener{
|
||||
ps: ps,
|
||||
ip: a.IP,
|
||||
ln: ln,
|
||||
lb: b,
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
|
||||
|
||||
type peerAPIListener struct {
|
||||
ps *peerAPIServer
|
||||
ip netaddr.IP
|
||||
ln net.Listener
|
||||
lb *LocalBackend
|
||||
urlStr string
|
||||
|
||||
@@ -418,6 +418,11 @@ type PingResult struct {
|
||||
// It is not currently set for TSMP pings.
|
||||
DERPRegionCode string
|
||||
|
||||
// PeerAPIPort is set by TSMP ping responses for peers that
|
||||
// are running a peerapi server. This is the port they're
|
||||
// running the server on.
|
||||
PeerAPIPort uint16 `json:",omitempty"`
|
||||
|
||||
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
|
||||
}
|
||||
|
||||
|
||||
@@ -224,11 +224,14 @@ func (pp *Parsed) AsTSMPPing() (h TSMPPingRequest, ok bool) {
|
||||
}
|
||||
|
||||
type TSMPPongReply struct {
|
||||
IPHeader Header
|
||||
Data [8]byte
|
||||
IPHeader Header
|
||||
Data [8]byte
|
||||
PeerAPIPort uint16
|
||||
}
|
||||
|
||||
func (pp *Parsed) AsTSMPPong() (data [8]byte, ok bool) {
|
||||
// AsTSMPPong returns pp as a TSMPPongReply and whether it is one.
|
||||
// The pong.IPHeader field is not populated.
|
||||
func (pp *Parsed) AsTSMPPong() (pong TSMPPongReply, ok bool) {
|
||||
if pp.IPProto != ipproto.TSMP {
|
||||
return
|
||||
}
|
||||
@@ -236,12 +239,15 @@ func (pp *Parsed) AsTSMPPong() (data [8]byte, ok bool) {
|
||||
if len(p) < 9 || p[0] != byte(TSMPTypePong) {
|
||||
return
|
||||
}
|
||||
copy(data[:], p[1:])
|
||||
return data, true
|
||||
copy(pong.Data[:], p[1:])
|
||||
if len(p) >= 11 {
|
||||
pong.PeerAPIPort = binary.BigEndian.Uint16(p[9:])
|
||||
}
|
||||
return pong, true
|
||||
}
|
||||
|
||||
func (h TSMPPongReply) Len() int {
|
||||
return h.IPHeader.Len() + 9
|
||||
return h.IPHeader.Len() + 11
|
||||
}
|
||||
|
||||
func (h TSMPPongReply) Marshal(buf []byte) error {
|
||||
@@ -254,5 +260,6 @@ func (h TSMPPongReply) Marshal(buf []byte) error {
|
||||
buf = buf[h.IPHeader.Len():]
|
||||
buf[0] = byte(TSMPTypePong)
|
||||
copy(buf[1:], h.Data[:])
|
||||
binary.BigEndian.PutUint16(buf[9:11], h.PeerAPIPort)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -107,7 +107,11 @@ type Wrapper struct {
|
||||
PostFilterOut FilterFunc
|
||||
|
||||
// OnTSMPPongReceived, if non-nil, is called whenever a TSMP pong arrives.
|
||||
OnTSMPPongReceived func(data [8]byte)
|
||||
OnTSMPPongReceived func(packet.TSMPPongReply)
|
||||
|
||||
// PeerAPIPort, if non-nil, returns the peerapi port that's
|
||||
// running for the given IP address.
|
||||
PeerAPIPort func(netaddr.IP) (port uint16, ok bool)
|
||||
|
||||
// disableFilter disables all filtering when set. This should only be used in tests.
|
||||
disableFilter bool
|
||||
@@ -456,6 +460,9 @@ func (t *Wrapper) injectOutboundPong(pp *packet.Parsed, req packet.TSMPPingReque
|
||||
pong := packet.TSMPPongReply{
|
||||
Data: req.Data,
|
||||
}
|
||||
if t.PeerAPIPort != nil {
|
||||
pong.PeerAPIPort, _ = t.PeerAPIPort(pp.Dst.IP)
|
||||
}
|
||||
switch pp.IPVersion {
|
||||
case 4:
|
||||
h4 := pp.IP4Header()
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package paths
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
@@ -45,8 +46,17 @@ func stateFileUnix() string {
|
||||
try = filepath.Dir(try)
|
||||
}
|
||||
|
||||
// TODO: try some $HOME/.tailscale or XDG path? But will it
|
||||
// even work usefully enough as non-root? Probably not. Maybe
|
||||
// best to require it be explicit in that case.
|
||||
return ""
|
||||
if os.Getuid() == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// For non-root users, fall back to $XDG_DATA_HOME/tailscale/*.
|
||||
return filepath.Join(xdgDataHome(), "tailscale", "tailscaled.state")
|
||||
}
|
||||
|
||||
func xdgDataHome() string {
|
||||
if e := os.Getenv("XDG_DATA_HOME"); e != "" {
|
||||
return e
|
||||
}
|
||||
return filepath.Join(os.Getenv("HOME"), ".local/share")
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/paths"
|
||||
)
|
||||
|
||||
type closeable interface {
|
||||
@@ -31,11 +29,6 @@ func ConnCloseWrite(c net.Conn) error {
|
||||
return c.(closeable).CloseWrite()
|
||||
}
|
||||
|
||||
// ConnectDefault connects to the local Tailscale daemon.
|
||||
func ConnectDefault() (net.Conn, error) {
|
||||
return Connect(paths.DefaultTailscaledSocket(), 41112)
|
||||
}
|
||||
|
||||
// Connect connects to either path (on Unix) or the provided localhost port (on Windows).
|
||||
func Connect(path string, port uint16) (net.Conn, error) {
|
||||
return connect(path, port)
|
||||
|
||||
@@ -116,19 +116,19 @@ type userspaceEngine struct {
|
||||
pingers map[wgkey.Key]*pinger // legacy pingers for pre-discovery peers
|
||||
pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go
|
||||
networkMapCallbacks map[*someHandle]NetworkMapCallback
|
||||
tsIPByIPPort map[netaddr.IPPort]netaddr.IP // allows registration of IP:ports as belonging to a certain Tailscale IP for whois lookups
|
||||
pongCallback map[[8]byte]func() // for TSMP pong responses
|
||||
tsIPByIPPort map[netaddr.IPPort]netaddr.IP // allows registration of IP:ports as belonging to a certain Tailscale IP for whois lookups
|
||||
pongCallback map[[8]byte]func(packet.TSMPPongReply) // for TSMP pong responses
|
||||
|
||||
// Lock ordering: magicsock.Conn.mu, wgLock, then mu.
|
||||
}
|
||||
|
||||
// InternalsGetter is implemented by Engines that can export their internals.
|
||||
type InternalsGetter interface {
|
||||
GetInternals() (*tstun.Wrapper, *magicsock.Conn)
|
||||
GetInternals() (_ *tstun.Wrapper, _ *magicsock.Conn, ok bool)
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) GetInternals() (*tstun.Wrapper, *magicsock.Conn) {
|
||||
return e.tundev, e.magicConn
|
||||
func (e *userspaceEngine) GetInternals() (_ *tstun.Wrapper, _ *magicsock.Conn, ok bool) {
|
||||
return e.tundev, e.magicConn, true
|
||||
}
|
||||
|
||||
// Config is the engine configuration.
|
||||
@@ -310,13 +310,13 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
SkipBindUpdate: true,
|
||||
}
|
||||
|
||||
e.tundev.OnTSMPPongReceived = func(data [8]byte) {
|
||||
e.tundev.OnTSMPPongReceived = func(pong packet.TSMPPongReply) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
cb := e.pongCallback[data]
|
||||
e.logf("wgengine: got TSMP pong %02x; cb=%v", data, cb != nil)
|
||||
cb := e.pongCallback[pong.Data]
|
||||
e.logf("wgengine: got TSMP pong %02x, peerAPIPort=%v; cb=%v", pong.Data, pong.PeerAPIPort, cb != nil)
|
||||
if cb != nil {
|
||||
go cb()
|
||||
go cb(pong)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1389,12 +1389,13 @@ func (e *userspaceEngine) sendTSMPPing(ip netaddr.IP, peer *tailcfg.Node, res *i
|
||||
e.setTSMPPongCallback(data, nil)
|
||||
})
|
||||
t0 := time.Now()
|
||||
e.setTSMPPongCallback(data, func() {
|
||||
e.setTSMPPongCallback(data, func(pong packet.TSMPPongReply) {
|
||||
expireTimer.Stop()
|
||||
d := time.Since(t0)
|
||||
res.LatencySeconds = d.Seconds()
|
||||
res.NodeIP = ip.String()
|
||||
res.NodeName = peer.ComputedName
|
||||
res.PeerAPIPort = pong.PeerAPIPort
|
||||
cb(res)
|
||||
})
|
||||
|
||||
@@ -1406,11 +1407,11 @@ func (e *userspaceEngine) sendTSMPPing(ip netaddr.IP, peer *tailcfg.Node, res *i
|
||||
e.tundev.InjectOutbound(tsmpPing)
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) setTSMPPongCallback(data [8]byte, cb func()) {
|
||||
func (e *userspaceEngine) setTSMPPongCallback(data [8]byte, cb func(packet.TSMPPongReply)) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.pongCallback == nil {
|
||||
e.pongCallback = map[[8]byte]func(){}
|
||||
e.pongCallback = map[[8]byte]func(packet.TSMPPongReply){}
|
||||
}
|
||||
if cb == nil {
|
||||
delete(e.pongCallback, data)
|
||||
|
||||
@@ -15,9 +15,11 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
@@ -133,6 +135,12 @@ func (e *watchdogEngine) WhoIsIPPort(ipp netaddr.IPPort) (tsIP netaddr.IP, ok bo
|
||||
func (e *watchdogEngine) Close() {
|
||||
e.watchdog("Close", e.wrap.Close)
|
||||
}
|
||||
func (e *watchdogEngine) GetInternals() (tw *tstun.Wrapper, c *magicsock.Conn, ok bool) {
|
||||
if ig, ok := e.wrap.(InternalsGetter); ok {
|
||||
return ig.GetInternals()
|
||||
}
|
||||
return
|
||||
}
|
||||
func (e *watchdogEngine) Wait() {
|
||||
e.wrap.Wait()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user