Compare commits
54 Commits
bradfitz/u
...
josh/tsweb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b89db4dff | ||
|
|
1dc4151f8b | ||
|
|
8d6cf14456 | ||
|
|
b4947be0c8 | ||
|
|
01e8a152f7 | ||
|
|
2448c000b3 | ||
|
|
903988b392 | ||
|
|
8267ea0f80 | ||
|
|
8fe503057d | ||
|
|
5d9ab502f3 | ||
|
|
a19c110dd3 | ||
|
|
2db6cd1025 | ||
|
|
be9d564c29 | ||
|
|
3a94ece30c | ||
|
|
86a902b201 | ||
|
|
adda2d2a51 | ||
|
|
a80cef0c13 | ||
|
|
84046d6f7c | ||
|
|
ec62217f52 | ||
|
|
21358cf2f5 | ||
|
|
37e7a387ff | ||
|
|
15599323a1 | ||
|
|
60abeb027b | ||
|
|
b9c92b90db | ||
|
|
e206a3663f | ||
|
|
0173a50bf0 | ||
|
|
dbea8217ac | ||
|
|
82cd98609f | ||
|
|
39d173e5fc | ||
|
|
c8551c8a67 | ||
|
|
3a74f2d2d7 | ||
|
|
24c9dbd129 | ||
|
|
62db629227 | ||
|
|
3c481d6b18 | ||
|
|
b3d268c5a1 | ||
|
|
df8f02db3f | ||
|
|
16652ae52c | ||
|
|
aaba49ca10 | ||
|
|
e64cecac8e | ||
|
|
2a67beaacf | ||
|
|
0626cf4183 | ||
|
|
d7962e3bcf | ||
|
|
6eed2811b2 | ||
|
|
e3dccfd7ff | ||
|
|
fa612c28cf | ||
|
|
e5cd765e00 | ||
|
|
bd90781b34 | ||
|
|
e45d51b060 | ||
|
|
730aa1c89c | ||
|
|
f5ec916214 | ||
|
|
69392411d9 | ||
|
|
02bdc654d5 | ||
|
|
70d71ba1e7 | ||
|
|
1af26222b6 |
6
.github/workflows/cross-darwin.yml
vendored
6
.github/workflows/cross-darwin.yml
vendored
@@ -37,6 +37,12 @@ jobs:
|
||||
GOARCH: amd64
|
||||
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -c ); done
|
||||
|
||||
- name: iOS build most
|
||||
env:
|
||||
GOOS: ios
|
||||
GOARCH: arm64
|
||||
run: go install ./ipn/... ./wgengine/ ./types/... ./control/controlclient
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
|
||||
66
api.md
66
api.md
@@ -15,6 +15,10 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [POST device routes](#device-routes-post)
|
||||
- Authorize machine
|
||||
- [POST device authorized](#device-authorized-post)
|
||||
- Tags
|
||||
- [POST device tags](#device-tags-post)
|
||||
- Key
|
||||
- [POST device key](#device-key-post)
|
||||
* **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
@@ -268,6 +272,68 @@ curl 'https://api.tailscale.com/api/v2/device/11055/authorized' \
|
||||
The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
<a name=device-tags-post></a>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/tags` - update tags on a device
|
||||
|
||||
Updates the tags set on a device.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`tags` - The new list of tags for the device.
|
||||
|
||||
```
|
||||
{
|
||||
"tags": ["tag:foo", "tag:bar"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/tags' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"tags": ["tag:foo", "tag:bar"]}'
|
||||
```
|
||||
|
||||
The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
<a name=device-key-post></a>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/key` - update device key
|
||||
|
||||
Allows for updating properties on the device key.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
`keyExpiryDisabled`
|
||||
|
||||
- Provide `true` to disable the device's key expiry. The original key expiry time is still maintained. Upon re-enabling, the key will expire at that original time.
|
||||
- Provide `false` to enable the device's key expiry. Sets the key to expire at the original expiry time prior to disabling. The key may already have expired. In that case, the device must be re-authenticated.
|
||||
- Empty value will not change the key expiry.
|
||||
|
||||
```
|
||||
{
|
||||
"keyExpiryDisabled": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/key' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"keyExpiryDisabled": true}'
|
||||
```
|
||||
|
||||
The response is 2xx on success. The response body is currently an empty JSON
|
||||
object.
|
||||
|
||||
## Tailnet
|
||||
A tailnet is the name of your Tailscale network.
|
||||
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
|
||||
|
||||
@@ -19,9 +19,9 @@ func New(socket string) (*BIRDClient, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
|
||||
}
|
||||
b := &BIRDClient{socket: socket, conn: conn, bs: bufio.NewScanner(conn)}
|
||||
b := &BIRDClient{socket: socket, conn: conn, scanner: bufio.NewScanner(conn)}
|
||||
// Read and discard the first line as that is the welcome message.
|
||||
if _, err := b.readLine(); err != nil {
|
||||
if _, err := b.readResponse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
@@ -29,9 +29,9 @@ func New(socket string) (*BIRDClient, error) {
|
||||
|
||||
// BIRDClient handles communication with the BIRD Internet Routing Daemon.
|
||||
type BIRDClient struct {
|
||||
socket string
|
||||
conn net.Conn
|
||||
bs *bufio.Scanner
|
||||
socket string
|
||||
conn net.Conn
|
||||
scanner *bufio.Scanner
|
||||
}
|
||||
|
||||
// Close closes the underlying connection to BIRD.
|
||||
@@ -39,7 +39,7 @@ func (b *BIRDClient) Close() error { return b.conn.Close() }
|
||||
|
||||
// DisableProtocol disables the provided protocol.
|
||||
func (b *BIRDClient) DisableProtocol(protocol string) error {
|
||||
out, err := b.exec("disable %s\n", protocol)
|
||||
out, err := b.exec("disable %s", protocol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (b *BIRDClient) DisableProtocol(protocol string) error {
|
||||
|
||||
// EnableProtocol enables the provided protocol.
|
||||
func (b *BIRDClient) EnableProtocol(protocol string) error {
|
||||
out, err := b.exec("enable %s\n", protocol)
|
||||
out, err := b.exec("enable %s", protocol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -65,19 +65,65 @@ func (b *BIRDClient) EnableProtocol(protocol string) error {
|
||||
return fmt.Errorf("failed to enable %s: %v", protocol, out)
|
||||
}
|
||||
|
||||
// BIRD CLI docs from https://bird.network.cz/?get_doc&v=20&f=prog-2.html#ss2.9
|
||||
|
||||
// Each session of the CLI consists of a sequence of request and replies,
|
||||
// slightly resembling the FTP and SMTP protocols.
|
||||
// Requests are commands encoded as a single line of text,
|
||||
// replies are sequences of lines starting with a four-digit code
|
||||
// followed by either a space (if it's the last line of the reply) or
|
||||
// a minus sign (when the reply is going to continue with the next line),
|
||||
// the rest of the line contains a textual message semantics of which depends on the numeric code.
|
||||
// If a reply line has the same code as the previous one and it's a continuation line,
|
||||
// the whole prefix can be replaced by a single white space character.
|
||||
//
|
||||
// Reply codes starting with 0 stand for ‘action successfully completed’ messages,
|
||||
// 1 means ‘table entry’, 8 ‘runtime error’ and 9 ‘syntax error’.
|
||||
|
||||
func (b *BIRDClient) exec(cmd string, args ...interface{}) (string, error) {
|
||||
if _, err := fmt.Fprintf(b.conn, cmd, args...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.readLine()
|
||||
fmt.Fprintln(b.conn)
|
||||
return b.readResponse()
|
||||
}
|
||||
|
||||
func (b *BIRDClient) readLine() (string, error) {
|
||||
if !b.bs.Scan() {
|
||||
return "", fmt.Errorf("reading response from bird failed")
|
||||
// hasResponseCode reports whether the provided byte slice is
|
||||
// prefixed with a BIRD response code.
|
||||
// Equivalent regex: `^\d{4}[ -]`.
|
||||
func hasResponseCode(s []byte) bool {
|
||||
if len(s) < 5 {
|
||||
return false
|
||||
}
|
||||
if err := b.bs.Err(); err != nil {
|
||||
return "", err
|
||||
for _, b := range s[:4] {
|
||||
if '0' <= b && b <= '9' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return b.bs.Text(), nil
|
||||
return s[4] == ' ' || s[4] == '-'
|
||||
}
|
||||
|
||||
func (b *BIRDClient) readResponse() (string, error) {
|
||||
var resp strings.Builder
|
||||
var done bool
|
||||
for !done {
|
||||
if !b.scanner.Scan() {
|
||||
return "", fmt.Errorf("reading response from bird failed: %q", resp.String())
|
||||
}
|
||||
if err := b.scanner.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
out := b.scanner.Bytes()
|
||||
if _, err := resp.Write(out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if hasResponseCode(out) {
|
||||
done = out[4] == ' '
|
||||
}
|
||||
if !done {
|
||||
resp.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
return resp.String(), nil
|
||||
}
|
||||
|
||||
111
chirp/chirp_test.go
Normal file
111
chirp/chirp_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2022 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 chirp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeBIRD struct {
|
||||
net.Listener
|
||||
protocolsEnabled map[string]bool
|
||||
sock string
|
||||
}
|
||||
|
||||
func newFakeBIRD(t *testing.T, protocols ...string) *fakeBIRD {
|
||||
sock := filepath.Join(t.TempDir(), "sock")
|
||||
l, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pe := make(map[string]bool)
|
||||
for _, p := range protocols {
|
||||
pe[p] = false
|
||||
}
|
||||
return &fakeBIRD{
|
||||
Listener: l,
|
||||
protocolsEnabled: pe,
|
||||
sock: sock,
|
||||
}
|
||||
}
|
||||
|
||||
func (fb *fakeBIRD) listen() error {
|
||||
for {
|
||||
c, err := fb.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
go fb.handle(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (fb *fakeBIRD) handle(c net.Conn) {
|
||||
fmt.Fprintln(c, "0001 BIRD 2.0.8 ready.")
|
||||
sc := bufio.NewScanner(c)
|
||||
for sc.Scan() {
|
||||
cmd := sc.Text()
|
||||
args := strings.Split(cmd, " ")
|
||||
switch args[0] {
|
||||
case "enable":
|
||||
en, ok := fb.protocolsEnabled[args[1]]
|
||||
if !ok {
|
||||
fmt.Fprintln(c, "9001 syntax error, unexpected CF_SYM_UNDEFINED, expecting CF_SYM_KNOWN or TEXT or ALL")
|
||||
} else if en {
|
||||
fmt.Fprintf(c, "0010-%s: already enabled\n", args[1])
|
||||
} else {
|
||||
fmt.Fprintf(c, "0011-%s: enabled\n", args[1])
|
||||
}
|
||||
fmt.Fprintln(c, "0000 ")
|
||||
fb.protocolsEnabled[args[1]] = true
|
||||
case "disable":
|
||||
en, ok := fb.protocolsEnabled[args[1]]
|
||||
if !ok {
|
||||
fmt.Fprintln(c, "9001 syntax error, unexpected CF_SYM_UNDEFINED, expecting CF_SYM_KNOWN or TEXT or ALL")
|
||||
} else if !en {
|
||||
fmt.Fprintf(c, "0008-%s: already disabled\n", args[1])
|
||||
} else {
|
||||
fmt.Fprintf(c, "0009-%s: disabled\n", args[1])
|
||||
}
|
||||
fmt.Fprintln(c, "0000 ")
|
||||
fb.protocolsEnabled[args[1]] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChirp(t *testing.T) {
|
||||
fb := newFakeBIRD(t, "tailscale")
|
||||
defer fb.Close()
|
||||
go fb.listen()
|
||||
c, err := New(fb.sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.EnableProtocol("tailscale"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.EnableProtocol("tailscale"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.DisableProtocol("tailscale"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.DisableProtocol("tailscale"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.EnableProtocol("rando"); err == nil {
|
||||
t.Fatalf("enabling %q succeded", "rando")
|
||||
}
|
||||
if err := c.DisableProtocol("rando"); err == nil {
|
||||
t.Fatalf("disabling %q succeded", "rando")
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,11 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dnsMu sync.Mutex
|
||||
dnsCache = map[string][]net.IP{}
|
||||
)
|
||||
var dnsCache atomic.Value // of []byte
|
||||
|
||||
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
|
||||
@@ -37,6 +34,7 @@ func refreshBootstrapDNS() {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
dnsEntries := make(map[string][]net.IP)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
names := strings.Split(*bootstrapDNS, ",")
|
||||
@@ -47,23 +45,23 @@ func refreshBootstrapDNS() {
|
||||
log.Printf("bootstrap DNS lookup %q: %v", name, err)
|
||||
continue
|
||||
}
|
||||
dnsMu.Lock()
|
||||
dnsCache[name] = addrs
|
||||
dnsMu.Unlock()
|
||||
dnsEntries[name] = addrs
|
||||
}
|
||||
j, err := json.MarshalIndent(dnsEntries, "", "\t")
|
||||
if err != nil {
|
||||
// leave the old values in place
|
||||
return
|
||||
}
|
||||
dnsCache.Store(j)
|
||||
}
|
||||
|
||||
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
bootstrapDNSRequests.Add(1)
|
||||
dnsMu.Lock()
|
||||
j, err := json.MarshalIndent(dnsCache, "", "\t")
|
||||
dnsMu.Unlock()
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS JSON: %v", err)
|
||||
http.Error(w, "JSON marshal error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
j, _ := dnsCache.Load().([]byte)
|
||||
// Bootstrap DNS requests occur cross-regions,
|
||||
// and are randomized per request,
|
||||
// so keeping a connection open is pointlessly expensive.
|
||||
w.Header().Set("Connection", "close")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
35
cmd/derper/bootstrap_dns_test.go
Normal file
35
cmd/derper/bootstrap_dns_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2022 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 (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkHandleBootstrapDNS(b *testing.B) {
|
||||
prev := *bootstrapDNS
|
||||
*bootstrapDNS = "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com"
|
||||
defer func() {
|
||||
*bootstrapDNS = prev
|
||||
}()
|
||||
refreshBootstrapDNS()
|
||||
w := new(bitbucketResponseWriter)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(b *testing.PB) {
|
||||
for b.Next() {
|
||||
handleBootstrapDNS(w, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type bitbucketResponseWriter struct{}
|
||||
|
||||
func (b *bitbucketResponseWriter) Header() http.Header { return make(http.Header) }
|
||||
|
||||
func (b *bitbucketResponseWriter) Write(p []byte) (int, error) { return len(p), nil }
|
||||
|
||||
func (b *bitbucketResponseWriter) WriteHeader(statusCode int) {}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
@@ -36,18 +38,22 @@ import (
|
||||
|
||||
var (
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode")
|
||||
addr = flag.String("a", ":443", "server address")
|
||||
addr = flag.String("a", ":443", "server HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces.")
|
||||
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
|
||||
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||
runSTUN = flag.Bool("stun", false, "also run a STUN server")
|
||||
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
|
||||
|
||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
|
||||
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
|
||||
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
|
||||
|
||||
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
|
||||
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -241,6 +247,8 @@ func main() {
|
||||
cert.Certificate = append(cert.Certificate, s.MetaCert())
|
||||
return cert, nil
|
||||
}
|
||||
// Disable TLS 1.0 and 1.1, which are obsolete and have security issues.
|
||||
httpsrv.TLSConfig.MinVersion = tls.VersionTLS12
|
||||
httpsrv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS != nil {
|
||||
label := "unknown"
|
||||
@@ -293,7 +301,7 @@ func main() {
|
||||
}
|
||||
}()
|
||||
}
|
||||
err = httpsrv.ListenAndServeTLS("", "")
|
||||
err = rateLimitedListenAndServeTLS(httpsrv)
|
||||
} else {
|
||||
log.Printf("derper: serving on %s", *addr)
|
||||
err = httpsrv.ListenAndServe()
|
||||
@@ -387,3 +395,63 @@ func defaultMeshPSKFile() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func rateLimitedListenAndServeTLS(srv *http.Server) error {
|
||||
addr := srv.Addr
|
||||
if addr == "" {
|
||||
addr = ":https"
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rln := newRateLimitedListener(ln, rate.Limit(*acceptConnLimit), *acceptConnBurst)
|
||||
expvar.Publish("tls_listener", rln.ExpVar())
|
||||
defer rln.Close()
|
||||
return srv.ServeTLS(rln, "", "")
|
||||
}
|
||||
|
||||
type rateLimitedListener struct {
|
||||
// These are at the start of the struct to ensure 64-bit alignment
|
||||
// on 32-bit architecture regardless of what other fields may exist
|
||||
// in this package.
|
||||
numAccepts expvar.Int // does not include number of rejects
|
||||
numRejects expvar.Int
|
||||
|
||||
net.Listener
|
||||
|
||||
lim *rate.Limiter
|
||||
}
|
||||
|
||||
func newRateLimitedListener(ln net.Listener, limit rate.Limit, burst int) *rateLimitedListener {
|
||||
return &rateLimitedListener{Listener: ln, lim: rate.NewLimiter(limit, burst)}
|
||||
}
|
||||
|
||||
func (l *rateLimitedListener) ExpVar() expvar.Var {
|
||||
m := new(metrics.Set)
|
||||
m.Set("counter_accepted_connections", &l.numAccepts)
|
||||
m.Set("counter_rejected_connections", &l.numRejects)
|
||||
return m
|
||||
}
|
||||
|
||||
var errLimitedConn = errors.New("cannot accept connection; rate limited")
|
||||
|
||||
func (l *rateLimitedListener) Accept() (net.Conn, error) {
|
||||
// Even under a rate limited situation, we accept the connection immediately
|
||||
// and close it, rather than being slow at accepting new connections.
|
||||
// This provides two benefits: 1) it signals to the client that something
|
||||
// is going on on the server, and 2) it prevents new connections from
|
||||
// piling up and occupying resources in the OS kernel.
|
||||
// The client will retry as needing (with backoffs in place).
|
||||
cn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !l.lim.Allow() {
|
||||
l.numRejects.Add(1)
|
||||
cn.Close()
|
||||
return nil, errLimitedConn
|
||||
}
|
||||
l.numAccepts.Add(1)
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
@@ -33,11 +35,21 @@ var (
|
||||
listen = flag.String("listen", ":8030", "HTTP listen address")
|
||||
)
|
||||
|
||||
// certReissueAfter is the time after which we expect all certs to be
|
||||
// reissued, at minimum.
|
||||
//
|
||||
// This is currently set to the date of the LetsEncrypt ALPN revocation event of Jan 2022:
|
||||
// https://community.letsencrypt.org/t/questions-about-renewing-before-tls-alpn-01-revocations/170449
|
||||
//
|
||||
// If there's another revocation event, bump this again.
|
||||
var certReissueAfter = time.Unix(1643226768, 0)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
state = map[nodePair]pairStatus{}
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastDERPMapAt time.Time
|
||||
certs = map[string]*x509.Certificate{}
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -46,6 +58,12 @@ func main() {
|
||||
log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(serve)))
|
||||
}
|
||||
|
||||
func setCert(name string, cert *x509.Certificate) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
certs[name] = cert
|
||||
}
|
||||
|
||||
type overallStatus struct {
|
||||
good, bad []string
|
||||
}
|
||||
@@ -93,6 +111,27 @@ func getOverallStatus() (o overallStatus) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var subjs []string
|
||||
for k := range certs {
|
||||
subjs = append(subjs, k)
|
||||
}
|
||||
sort.Strings(subjs)
|
||||
|
||||
soon := time.Now().Add(14 * 24 * time.Hour) // in 2 weeks; autocert does 30 days by default
|
||||
for _, s := range subjs {
|
||||
cert := certs[s]
|
||||
if cert.NotBefore.Before(certReissueAfter) {
|
||||
o.addBadf("cert %q needs reissuing; NotBefore=%v", s, cert.NotBefore.Format(time.RFC3339))
|
||||
continue
|
||||
}
|
||||
if cert.NotAfter.Before(soon) {
|
||||
o.addBadf("cert %q expiring soon (%v); wasn't auto-refreshed", s, cert.NotAfter.Format(time.RFC3339))
|
||||
continue
|
||||
}
|
||||
o.addGoodf("cert %q good %v - %v", s, cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -359,6 +398,21 @@ func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*de
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cs, ok := dc.TLSConnectionState()
|
||||
if !ok {
|
||||
dc.Close()
|
||||
return nil, errors.New("no TLS state")
|
||||
}
|
||||
if len(cs.PeerCertificates) == 0 {
|
||||
dc.Close()
|
||||
return nil, errors.New("no peer certificates")
|
||||
}
|
||||
if cs.ServerName != n.HostName {
|
||||
dc.Close()
|
||||
return nil, fmt.Errorf("TLS server name %q != derp hostname %q", cs.ServerName, n.HostName)
|
||||
}
|
||||
setCert(cs.ServerName, cs.PeerCertificates[0])
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
m, err := dc.Recv()
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var Stderr io.Writer = os.Stderr
|
||||
@@ -155,6 +156,9 @@ change in the future.
|
||||
if strSliceContains(args, "debug") {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
}
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
|
||||
@@ -793,8 +793,25 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
AdvertiseRoutesSet: true,
|
||||
AdvertiseTagsSet: true,
|
||||
AllowSingleHostsSet: true,
|
||||
ControlURLSet: true,
|
||||
CorpDNSSet: true,
|
||||
ExitNodeAllowLANAccessSet: true,
|
||||
ExitNodeIDSet: true,
|
||||
ExitNodeIPSet: true,
|
||||
HostnameSet: true,
|
||||
NetfilterModeSet: true,
|
||||
NoSNATSet: true,
|
||||
OperatorUserSet: true,
|
||||
RouteAllSet: true,
|
||||
RunSSHSet: true,
|
||||
ShieldsUpSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "control_synonym",
|
||||
@@ -860,16 +877,23 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
if simpleUp != tt.wantSimpleUp {
|
||||
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
|
||||
}
|
||||
var oldEditPrefs ipn.Prefs
|
||||
if justEditMP != nil {
|
||||
oldEditPrefs = justEditMP.Prefs
|
||||
justEditMP.Prefs = ipn.Prefs{} // uninteresting
|
||||
}
|
||||
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
|
||||
t.Fatalf("justEditMP: %v", cmp.Diff(justEditMP, tt.wantJustEditMP))
|
||||
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was %+v", oldEditPrefs)
|
||||
t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var cmpIP = cmp.Comparer(func(a, b netaddr.IP) bool {
|
||||
return a == b
|
||||
})
|
||||
|
||||
func TestExitNodeIPOfArg(t *testing.T) {
|
||||
mustIP := netaddr.MustParseIP
|
||||
tests := []struct {
|
||||
|
||||
85
cmd/tailscale/cli/configure-host.go
Normal file
85
cmd/tailscale/cli/configure-host.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2022 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var configureHostCmd = &ffcli.Command{
|
||||
Name: "configure-host",
|
||||
Exec: runConfigureHost,
|
||||
ShortHelp: "Configure Synology to enable more Tailscale features",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The 'configure-host' command is intended to run at boot as root
|
||||
to create the /dev/net/tun device and give the tailscaled binary
|
||||
permission to use it.
|
||||
|
||||
See: https://tailscale.com/kb/1152/synology-outbound/
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("configure-host")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var configureHostArgs struct{}
|
||||
|
||||
func runConfigureHost(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
|
||||
return errors.New("only implemented on Synology")
|
||||
}
|
||||
if uid := os.Getuid(); uid != 0 {
|
||||
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
|
||||
}
|
||||
osVer := hostinfo.GetOSVersion()
|
||||
isDSM6 := strings.HasPrefix(osVer, "Synology 6")
|
||||
isDSM7 := strings.HasPrefix(osVer, "Synology 7")
|
||||
if !isDSM6 && !isDSM7 {
|
||||
return fmt.Errorf("unsupported DSM version %q", osVer)
|
||||
}
|
||||
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll("/dev/net", 0755); err != nil {
|
||||
return fmt.Errorf("creating /dev/net: %v", err)
|
||||
}
|
||||
if out, err := exec.Command("/bin/mknod", "/dev/net/tun", "c", "10", "200").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("creating /dev/net/tun: %v, %s", err, out)
|
||||
}
|
||||
}
|
||||
if err := os.Chmod("/dev/net/tun", 0666); err != nil {
|
||||
return err
|
||||
}
|
||||
if isDSM6 {
|
||||
fmt.Printf("/dev/net/tun exists and has permissions 0666. Skipping setcap on DSM6.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
const daemonBin = "/var/packages/Tailscale/target/bin/tailscaled"
|
||||
if _, err := os.Stat(daemonBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("tailscaled binary not found at %s. Is the Tailscale *.spk package installed?", daemonBin)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if out, err := exec.Command("/bin/setcap", "cap_net_admin+eip", daemonBin).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("setcap: %v, %s", err, out)
|
||||
}
|
||||
fmt.Printf("Done. To restart Tailscale to use the new permissions, run:\n\n sudo synosystemctl restart pkgctl-Tailscale.service\n\n")
|
||||
return nil
|
||||
}
|
||||
@@ -387,7 +387,8 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
return prefs, nil
|
||||
}
|
||||
|
||||
// updatePrefs updates prefs based on curPrefs
|
||||
// updatePrefs returns how to edit preferences based on the
|
||||
// flag-provided 'prefs' and the currently active 'curPrefs'.
|
||||
//
|
||||
// It returns a non-nil justEditMP if we're already running and none of
|
||||
// the flags require a restart, so we can just do an EditPrefs call and
|
||||
@@ -424,11 +425,16 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
env.upArgs.authKeyOrFile == "" &&
|
||||
!controlURLChanged &&
|
||||
!tagsChanged
|
||||
|
||||
if justEdit {
|
||||
justEditMP = new(ipn.MaskedPrefs)
|
||||
justEditMP.WantRunningSet = true
|
||||
justEditMP.Prefs = *prefs
|
||||
env.flagSet.Visit(func(f *flag.Flag) {
|
||||
visitFlags := env.flagSet.Visit
|
||||
if env.upArgs.reset {
|
||||
visitFlags = env.flagSet.VisitAll
|
||||
}
|
||||
visitFlags(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(justEditMP, f.Name)
|
||||
})
|
||||
}
|
||||
@@ -520,7 +526,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(pumpCtx, bc, c) }()
|
||||
|
||||
printed := !simpleUp
|
||||
var printed bool // whether we've yet printed anything to stdout or stderr
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
|
||||
@@ -546,7 +552,6 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
|
||||
@@ -39,7 +39,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces
|
||||
tailscale.com/hostinfo from tailscale.com/net/interfaces+
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/kube from tailscale.com/ipn
|
||||
|
||||
@@ -67,7 +67,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -118,46 +118,46 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.zx2c4.com/wireguard/tai64n from golang.zx2c4.com/wireguard/device
|
||||
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
|
||||
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs+
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
|
||||
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/refsvfs2
|
||||
gvisor.dev/gvisor/pkg/refsvfs2 from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/net/tstun+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/noop from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
inet.af/netaddr from inet.af/wf+
|
||||
inet.af/netstack/atomicbitops from inet.af/netstack/tcpip+
|
||||
💣 inet.af/netstack/buffer from inet.af/netstack/tcpip/stack
|
||||
inet.af/netstack/context from inet.af/netstack/refs+
|
||||
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
|
||||
inet.af/netstack/linewriter from inet.af/netstack/log
|
||||
inet.af/netstack/log from inet.af/netstack/state+
|
||||
inet.af/netstack/rand from inet.af/netstack/tcpip/network/hash+
|
||||
inet.af/netstack/refs from inet.af/netstack/refsvfs2
|
||||
inet.af/netstack/refsvfs2 from inet.af/netstack/tcpip/stack
|
||||
💣 inet.af/netstack/sleep from inet.af/netstack/tcpip/transport/tcp
|
||||
💣 inet.af/netstack/state from inet.af/netstack/atomicbitops+
|
||||
inet.af/netstack/state/wire from inet.af/netstack/state
|
||||
💣 inet.af/netstack/sync from inet.af/netstack/linewriter+
|
||||
inet.af/netstack/tcpip from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 inet.af/netstack/tcpip/buffer from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/hash/jenkins from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/header from inet.af/netstack/tcpip/header/parse+
|
||||
inet.af/netstack/tcpip/header/parse from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/internal/tcp from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/ip from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/net/tstun+
|
||||
inet.af/netstack/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+
|
||||
💣 inet.af/netstack/tcpip/stack from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport from inet.af/netstack/tcpip/transport/icmp+
|
||||
inet.af/netstack/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/transport/internal/network from inet.af/netstack/tcpip/transport/icmp+
|
||||
inet.af/netstack/tcpip/transport/internal/noop from inet.af/netstack/tcpip/transport/raw
|
||||
inet.af/netstack/tcpip/transport/packet from inet.af/netstack/tcpip/transport/raw
|
||||
inet.af/netstack/tcpip/transport/raw from inet.af/netstack/tcpip/transport/icmp+
|
||||
💣 inet.af/netstack/tcpip/transport/tcp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport/tcpconntrack from inet.af/netstack/tcpip/stack
|
||||
inet.af/netstack/tcpip/transport/udp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
@@ -203,6 +203,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||
tailscale.com/net/portmapper from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
@@ -306,12 +307,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
|
||||
golang.org/x/time/rate from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
container/heap from inet.af/netstack/tcpip/transport/tcp
|
||||
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -349,9 +350,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from inet.af/netstack/tcpip/network/ipv6+
|
||||
hash/fnv from gvisor.dev/gvisor/pkg/tcpip/network/ipv6+
|
||||
hash/maphash from go4.org/mem
|
||||
html from net/http/pprof+
|
||||
html/template from tailscale.com/tsweb
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
@@ -390,6 +392,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from runtime/pprof
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from crypto/x509+
|
||||
|
||||
@@ -74,7 +74,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
|
||||
svcAccepts := svc.AcceptStop
|
||||
if winutil.GetRegInteger("FlushDNSOnSessionUnlock", 0) != 0 {
|
||||
if winutil.GetPolicyInteger("FlushDNSOnSessionUnlock", 0) != 0 {
|
||||
svcAccepts |= svc.AcceptSessionChange
|
||||
}
|
||||
|
||||
|
||||
@@ -266,9 +266,9 @@ func (c *Auto) authRoutine() {
|
||||
goal := c.loginGoal
|
||||
ctx := c.authCtx
|
||||
if goal != nil {
|
||||
c.logf("authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
|
||||
c.logf("[v1] authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
|
||||
} else {
|
||||
c.logf("authRoutine: %s; goal=nil paused=%v", c.state, c.paused)
|
||||
c.logf("[v1] authRoutine: %s; goal=nil paused=%v", c.state, c.paused)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -414,7 +414,7 @@ func (c *Auto) mapRoutine() {
|
||||
}
|
||||
continue
|
||||
}
|
||||
c.logf("mapRoutine: %s", c.state)
|
||||
c.logf("[v1] mapRoutine: %s", c.state)
|
||||
loggedIn := c.loggedIn
|
||||
ctx := c.mapCtx
|
||||
c.mu.Unlock()
|
||||
@@ -445,9 +445,9 @@ func (c *Auto) mapRoutine() {
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.logf("mapRoutine: context done.")
|
||||
c.logf("[v1] mapRoutine: context done.")
|
||||
case <-c.newMapCh:
|
||||
c.logf("mapRoutine: new map needed while idle.")
|
||||
c.logf("[v1] mapRoutine: new map needed while idle.")
|
||||
}
|
||||
} else {
|
||||
// Be sure this is false when we're not inside
|
||||
|
||||
@@ -168,6 +168,10 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
// Disable implicit gzip compression; the various
|
||||
// handlers (register, map, set-dns, etc) do their own
|
||||
// zstd compression per naclbox.
|
||||
tr.DisableCompression = true
|
||||
httpc = &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
@@ -210,7 +214,7 @@ func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
|
||||
}
|
||||
c.hostinfo = hi.Clone()
|
||||
j, _ := json.Marshal(c.hostinfo)
|
||||
c.logf("HostInfo: %s", j)
|
||||
c.logf("[v1] HostInfo: %s", j)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -241,10 +245,10 @@ func (c *Direct) GetPersist() persist.Persist {
|
||||
}
|
||||
|
||||
func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
c.logf("direct.TryLogout()")
|
||||
c.logf("[v1] direct.TryLogout()")
|
||||
|
||||
mustRegen, newURL, err := c.doLogin(ctx, loginOpt{Logout: true})
|
||||
c.logf("TryLogout control response: mustRegen=%v, newURL=%v, err=%v", mustRegen, newURL, err)
|
||||
c.logf("[v1] TryLogout control response: mustRegen=%v, newURL=%v, err=%v", mustRegen, newURL, err)
|
||||
|
||||
c.mu.Lock()
|
||||
c.persist = persist.Persist{}
|
||||
@@ -254,7 +258,7 @@ func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) {
|
||||
c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
|
||||
c.logf("[v1] direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
|
||||
return c.doLoginOrRegen(ctx, loginOpt{Token: t, Flags: flags})
|
||||
}
|
||||
|
||||
@@ -262,7 +266,7 @@ func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Log
|
||||
//
|
||||
// On success, newURL and err will both be nil.
|
||||
func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newURL string, err error) {
|
||||
c.logf("direct.WaitLoginURL")
|
||||
c.logf("[v1] direct.WaitLoginURL")
|
||||
return c.doLoginOrRegen(ctx, loginOpt{URL: url})
|
||||
}
|
||||
|
||||
@@ -465,7 +469,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if resp.AuthURL != "" {
|
||||
c.logf("AuthURL is %v", resp.AuthURL)
|
||||
} else {
|
||||
c.logf("No AuthURL")
|
||||
c.logf("[v1] No AuthURL")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -516,7 +520,7 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (c
|
||||
for _, ep := range endpoints {
|
||||
epStrs = append(epStrs, ep.Addr.String())
|
||||
}
|
||||
c.logf("client.newEndpoints(%v, %v)", localPort, epStrs)
|
||||
c.logf("[v2] client.newEndpoints(%v, %v)", localPort, epStrs)
|
||||
c.localPort = localPort
|
||||
c.endpoints = append(c.endpoints[:0], endpoints...)
|
||||
if len(endpoints) > 0 {
|
||||
@@ -821,10 +825,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
// We need at least one endpoint here for now else
|
||||
// other code doesn't even create the discoEndpoint.
|
||||
// TODO(bradfitz): fix that and then just nil this out.
|
||||
p.Endpoints = []string{"127.9.9.9:456"}
|
||||
p.Endpoints = nil
|
||||
}
|
||||
}
|
||||
if Debug.StripCaps {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/certstore"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -73,23 +74,46 @@ func isSubjectInChain(subject string, chain []*x509.Certificate) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func selectIdentityFromSlice(subject string, ids []certstore.Identity) (certstore.Identity, []*x509.Certificate) {
|
||||
func selectIdentityFromSlice(subject string, ids []certstore.Identity, now time.Time) (certstore.Identity, []*x509.Certificate) {
|
||||
var bestCandidate struct {
|
||||
id certstore.Identity
|
||||
chain []*x509.Certificate
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
chain, err := id.CertificateChain()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(chain) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isSupportedCertificate(chain[0]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isSubjectInChain(subject, chain) {
|
||||
return id, chain
|
||||
if now.Before(chain[0].NotBefore) || now.After(chain[0].NotAfter) {
|
||||
// Certificate is not valid at this time
|
||||
continue
|
||||
}
|
||||
|
||||
if !isSubjectInChain(subject, chain) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Select the most recently issued certificate. If there is a tie, pick
|
||||
// one arbitrarily.
|
||||
if len(bestCandidate.chain) > 0 && bestCandidate.chain[0].NotBefore.After(chain[0].NotBefore) {
|
||||
continue
|
||||
}
|
||||
|
||||
bestCandidate.id = id
|
||||
bestCandidate.chain = chain
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return bestCandidate.id, bestCandidate.chain
|
||||
}
|
||||
|
||||
// findIdentity locates an identity from the Windows or Darwin certificate
|
||||
@@ -105,7 +129,7 @@ func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x5
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
selected, chain := selectIdentityFromSlice(subject, ids)
|
||||
selected, chain := selectIdentityFromSlice(subject, ids, time.Now())
|
||||
|
||||
for _, id := range ids {
|
||||
if id != selected {
|
||||
|
||||
238
control/controlclient/sign_supported_test.go
Normal file
238
control/controlclient/sign_supported_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright (c) 2022 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.
|
||||
|
||||
//go:build windows && cgo
|
||||
// +build windows,cgo
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/certstore"
|
||||
)
|
||||
|
||||
const (
|
||||
testRootCommonName = "testroot"
|
||||
testRootSubject = "CN=testroot"
|
||||
)
|
||||
|
||||
type testIdentity struct {
|
||||
chain []*x509.Certificate
|
||||
}
|
||||
|
||||
func makeChain(rootCommonName string, notBefore, notAfter time.Time) []*x509.Certificate {
|
||||
return []*x509.Certificate{
|
||||
{
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
PublicKeyAlgorithm: x509.RSA,
|
||||
},
|
||||
{
|
||||
Subject: pkix.Name{
|
||||
CommonName: rootCommonName,
|
||||
},
|
||||
PublicKeyAlgorithm: x509.RSA,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *testIdentity) Certificate() (*x509.Certificate, error) {
|
||||
return t.chain[0], nil
|
||||
}
|
||||
|
||||
func (t *testIdentity) CertificateChain() ([]*x509.Certificate, error) {
|
||||
return t.chain, nil
|
||||
}
|
||||
|
||||
func (t *testIdentity) Signer() (crypto.Signer, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (t *testIdentity) Delete() error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (t *testIdentity) Close() {}
|
||||
|
||||
func TestSelectIdentityFromSlice(t *testing.T) {
|
||||
var times []time.Time
|
||||
for _, ts := range []string{
|
||||
"2000-01-01T00:00:00Z",
|
||||
"2001-01-01T00:00:00Z",
|
||||
"2002-01-01T00:00:00Z",
|
||||
"2003-01-01T00:00:00Z",
|
||||
} {
|
||||
tm, err := time.Parse(time.RFC3339, ts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
times = append(times, tm)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
subject string
|
||||
ids []certstore.Identity
|
||||
now time.Time
|
||||
// wantIndex is an index into ids, or -1 for nil.
|
||||
wantIndex int
|
||||
}{
|
||||
{
|
||||
name: "single unexpired identity",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[2]),
|
||||
},
|
||||
},
|
||||
now: times[1],
|
||||
wantIndex: 0,
|
||||
},
|
||||
{
|
||||
name: "single expired identity",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[1]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: -1,
|
||||
},
|
||||
{
|
||||
name: "unrelated ids",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain("something", times[0], times[2]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[2]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain("else", times[0], times[2]),
|
||||
},
|
||||
},
|
||||
now: times[1],
|
||||
wantIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "expired with unrelated ids",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain("something", times[0], times[3]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[1]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain("else", times[0], times[3]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: -1,
|
||||
},
|
||||
{
|
||||
name: "one expired",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[1]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[1], times[3]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "two certs both unexpired",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[3]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[1], times[3]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "two unexpired one expired",
|
||||
subject: testRootSubject,
|
||||
ids: []certstore.Identity{
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[3]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[1], times[3]),
|
||||
},
|
||||
&testIdentity{
|
||||
chain: makeChain(testRootCommonName, times[0], times[1]),
|
||||
},
|
||||
},
|
||||
now: times[2],
|
||||
wantIndex: 1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotId, gotChain := selectIdentityFromSlice(tt.subject, tt.ids, tt.now)
|
||||
|
||||
if gotId == nil && gotChain != nil {
|
||||
t.Error("id is nil: got non-nil chain, want nil chain")
|
||||
return
|
||||
}
|
||||
if gotId != nil && gotChain == nil {
|
||||
t.Error("id is not nil: got nil chain, want non-nil chain")
|
||||
return
|
||||
}
|
||||
if tt.wantIndex == -1 {
|
||||
if gotId != nil {
|
||||
t.Error("got non-nil id, want nil id")
|
||||
}
|
||||
return
|
||||
}
|
||||
if gotId == nil {
|
||||
t.Error("got nil id, want non-nil id")
|
||||
return
|
||||
}
|
||||
if gotId != tt.ids[tt.wantIndex] {
|
||||
found := -1
|
||||
for i := range tt.ids {
|
||||
if tt.ids[i] == gotId {
|
||||
found = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == -1 {
|
||||
t.Errorf("got unknown id, want id at index %v", tt.wantIndex)
|
||||
} else {
|
||||
t.Errorf("got id at index %v, want id at index %v", found, tt.wantIndex)
|
||||
}
|
||||
}
|
||||
|
||||
tid, ok := tt.ids[tt.wantIndex].(*testIdentity)
|
||||
if !ok {
|
||||
t.Error("got non-testIdentity, want testIdentity")
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tid.chain, gotChain) {
|
||||
t.Errorf("got unknown chain, want chain from id at index %v", tt.wantIndex)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
@@ -64,6 +65,12 @@ type Client struct {
|
||||
ctx context.Context // closed via cancelCtx in Client.Close
|
||||
cancelCtx context.CancelFunc
|
||||
|
||||
// addrFamSelAtomic is the last AddressFamilySelector set
|
||||
// by SetAddressFamilySelector. It's an atomic because it needs
|
||||
// to be accessed by multiple racing routines started while
|
||||
// Client.conn holds mu.
|
||||
addrFamSelAtomic atomic.Value // of AddressFamilySelector
|
||||
|
||||
mu sync.Mutex
|
||||
preferred bool
|
||||
canAckPings bool
|
||||
@@ -72,6 +79,7 @@ type Client struct {
|
||||
client *derp.Client
|
||||
connGen int // incremented once per new connection; valid values are >0
|
||||
serverPubKey key.NodePublic
|
||||
tlsState *tls.ConnectionState
|
||||
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
|
||||
}
|
||||
|
||||
@@ -124,6 +132,17 @@ func (c *Client) Connect(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// TLSConnectionState returns the last TLS connection state, if any.
|
||||
// The client must already be connected.
|
||||
func (c *Client) TLSConnectionState() (_ *tls.ConnectionState, ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.closed || c.client == nil {
|
||||
return nil, false
|
||||
}
|
||||
return c.tlsState, c.tlsState != nil
|
||||
}
|
||||
|
||||
// ServerPublicKey returns the server's public key.
|
||||
//
|
||||
// It only returns a non-zero value once a connection has succeeded
|
||||
@@ -181,6 +200,32 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
|
||||
return fmt.Sprintf("https://%s/derp", node.HostName)
|
||||
}
|
||||
|
||||
// AddressFamilySelector decides whethers IPv6 is preferred for
|
||||
// outbound dials.
|
||||
type AddressFamilySelector interface {
|
||||
// PreferIPv6 reports whether IPv4 dials should be slightly
|
||||
// delayed to give IPv6 a better chance of winning dial races.
|
||||
// Implementations should only return true if IPv6 is expected
|
||||
// to succeed. (otherwise delaying IPv4 will delay the
|
||||
// connection overall)
|
||||
PreferIPv6() bool
|
||||
}
|
||||
|
||||
// SetAddressFamilySelector sets the AddressFamilySelector that this
|
||||
// connection will use. It should be called before any dials.
|
||||
// The value must not be nil. If called more than once, s must
|
||||
// be the same concrete type as any prior calls.
|
||||
func (c *Client) SetAddressFamilySelector(s AddressFamilySelector) {
|
||||
c.addrFamSelAtomic.Store(s)
|
||||
}
|
||||
|
||||
func (c *Client) preferIPv6() bool {
|
||||
if s, ok := c.addrFamSelAtomic.Load().(AddressFamilySelector); ok {
|
||||
return s.PreferIPv6()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// dialWebsocketFunc is non-nil (set by websocket.go's init) when compiled in.
|
||||
var dialWebsocketFunc func(ctx context.Context, urlStr string) (net.Conn, error)
|
||||
|
||||
@@ -318,6 +363,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
|
||||
var serverPub key.NodePublic // or zero if unknown (if not using TLS or TLS middlebox eats it)
|
||||
var serverProtoVersion int
|
||||
var tlsState *tls.ConnectionState
|
||||
if c.useHTTPS() {
|
||||
tlsConn := c.tlsClient(tcpConn, node)
|
||||
httpConn = tlsConn
|
||||
@@ -340,9 +386,10 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
// Note that we're not specifically concerned about TLS downgrade
|
||||
// attacks. TLS handles that fine:
|
||||
// https://blog.gypsyengineer.com/en/security/how-does-tls-1-3-protect-against-downgrade-attacks.html
|
||||
connState := tlsConn.ConnectionState()
|
||||
if connState.Version >= tls.VersionTLS13 {
|
||||
serverPub, serverProtoVersion = parseMetaCert(connState.PeerCertificates)
|
||||
cs := tlsConn.ConnectionState()
|
||||
tlsState = &cs
|
||||
if cs.Version >= tls.VersionTLS13 {
|
||||
serverPub, serverProtoVersion = parseMetaCert(cs.PeerCertificates)
|
||||
}
|
||||
} else {
|
||||
httpConn = tcpConn
|
||||
@@ -409,6 +456,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
c.serverPubKey = derpClient.ServerPublicKey()
|
||||
c.client = derpClient
|
||||
c.netConn = tcpConn
|
||||
c.tlsState = tlsState
|
||||
c.connGen++
|
||||
return c.client, c.connGen, nil
|
||||
}
|
||||
@@ -568,6 +616,18 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
startDial := func(dstPrimary, proto string) {
|
||||
nwait++
|
||||
go func() {
|
||||
if proto == "tcp4" && c.preferIPv6() {
|
||||
t := time.NewTimer(200 * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Either user canceled original context,
|
||||
// it timed out, or the v6 dial succeeded.
|
||||
t.Stop()
|
||||
return
|
||||
case <-t.C:
|
||||
// Start v4 dial
|
||||
}
|
||||
}
|
||||
dst := dstPrimary
|
||||
if dst == "" {
|
||||
dst = n.HostName
|
||||
|
||||
@@ -20,15 +20,51 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
set = map[string]string{}
|
||||
list []string
|
||||
)
|
||||
|
||||
func noteEnv(k, v string) {
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if _, ok := set[v]; !ok {
|
||||
list = append(list, k)
|
||||
}
|
||||
set[k] = v
|
||||
}
|
||||
|
||||
// logf is logger.Logf, but logger depends on envknob, so for circular
|
||||
// dependency reasons, make a type alias (so it's still assignable,
|
||||
// but has nice docs here).
|
||||
type logf = func(format string, args ...interface{})
|
||||
|
||||
// LogCurrent logs the currently set environment knobs.
|
||||
func LogCurrent(logf logf) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for _, k := range list {
|
||||
logf("envknob: %s=%q", k, set[k])
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the named environment variable, using os.Getenv.
|
||||
//
|
||||
// In the future it will also track usage for reporting on debug pages.
|
||||
// If the variable is non-empty, it's also tracked & logged as being
|
||||
// an in-use knob.
|
||||
func String(envVar string) string {
|
||||
return os.Getenv(envVar)
|
||||
v := os.Getenv(envVar)
|
||||
noteEnv(envVar, v)
|
||||
return v
|
||||
}
|
||||
|
||||
// Bool returns the boolean value of the named environment variable.
|
||||
@@ -51,9 +87,10 @@ func boolOr(envVar string, implicitValue bool) bool {
|
||||
}
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err == nil {
|
||||
noteEnv(envVar, strconv.FormatBool(b)) // canonicalize
|
||||
return b
|
||||
}
|
||||
log.Fatalf("invalid environment variable %s value %q: %v", envVar, val, err)
|
||||
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
@@ -69,7 +106,7 @@ func LookupBool(envVar string) (v bool, ok bool) {
|
||||
if err == nil {
|
||||
return b, true
|
||||
}
|
||||
log.Fatalf("invalid environment variable %s value %q: %v", envVar, val, err)
|
||||
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
@@ -95,9 +132,10 @@ func LookupInt(envVar string) (v int, ok bool) {
|
||||
}
|
||||
v, err := strconv.Atoi(val)
|
||||
if err == nil {
|
||||
noteEnv(envVar, val)
|
||||
return v, true
|
||||
}
|
||||
log.Fatalf("invalid environment variable %s value %q: %v", envVar, val, err)
|
||||
log.Fatalf("invalid integer environment variable %s: %v", envVar, val)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -56,9 +56,9 @@ require (
|
||||
golang.org/x/tools v0.1.8
|
||||
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10
|
||||
gvisor.dev/gvisor v0.0.0-20220126021142-d8aa030b2591
|
||||
honnef.co/go/tools v0.2.2
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310
|
||||
nhooyr.io/websocket v1.8.7
|
||||
|
||||
12
go.sum
12
go.sum
@@ -189,7 +189,6 @@ github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacM
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
@@ -469,7 +468,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4=
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@@ -1113,12 +1111,10 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tomarrell/wrapcheck v0.0.0-20200807122107-df9e8bcb914d/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0=
|
||||
github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756 h1:zV5mu0ESwb+WnzqVaW2z1DdbAP0S46UtjY8DHQupQP4=
|
||||
github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.4.0 h1:mU4H9KsqqPZUALOUbVOpjy8qNQbWLoLI9fV68/1tq30=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.4.0/go.mod h1:68bQ/eJg55BROaRTbMjC7vuhL2OgfoG8bLp9ZyoBfyY=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa h1:RC4maTWLKKwb7p1cnoygsbKIgNlJqSYBeAFON3Ar8As=
|
||||
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.4.0 h1:1t0f8Uiaq+fqKteUR4N9Umr6E99R+lDnLnq7PwX2PPE=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
||||
@@ -1178,7 +1174,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b/go.mod h1:IZpXDfkJ6tWD3PhBK5YzgQT+xJWh7OsdwiG8hA2MkO4=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
@@ -1229,7 +1224,6 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
@@ -1351,7 +1345,6 @@ golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211101193420-4a448f8816b3/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211205041911-012df41ee64c h1:7SfqwP5fxEtl/P02w5IhKc86ziJ+A25yFrkVgoy2FT8=
|
||||
@@ -1497,7 +1490,6 @@ golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
|
||||
@@ -1825,6 +1817,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20220126021142-d8aa030b2591 h1:acuXPUADpJMtawdLCUje9xKlQN/8utegCB/Hr/ZgEuY=
|
||||
gvisor.dev/gvisor v0.0.0-20220126021142-d8aa030b2591/go.mod h1:vmN0Pug/s8TJmpnt30DvrEfZ5vDl52psGLU04tFuK2U=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1842,8 +1836,6 @@ howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8=
|
||||
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c h1:nr31qYr+91rWD8klUkPx3eGTZzumCC414UJG1QRKZTc=
|
||||
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c/go.mod h1:KOJdAzQzMLKzwFEdOOnrnSrLIhaFVB+NQoME/e5wllA=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310 h1:0jKHTf+W75kYRyg5bto1UT+r18QmAz2u/5pAs/fx4zo=
|
||||
|
||||
@@ -170,6 +170,10 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
if e == nil {
|
||||
panic("ipn.NewLocalBackend: engine must not be nil")
|
||||
}
|
||||
|
||||
hi := hostinfo.New()
|
||||
logf("Host: %s/%s, %s", hi.OS, hi.GoArch, hi.OSVersion)
|
||||
envknob.LogCurrent(logf)
|
||||
if dialer == nil {
|
||||
dialer = new(tsdial.Dialer)
|
||||
}
|
||||
@@ -215,7 +219,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
wiredPeerAPIPort := false
|
||||
if ig, ok := e.(wgengine.InternalsGetter); ok {
|
||||
if tunWrap, _, ok := ig.GetInternals(); ok {
|
||||
tunWrap.PeerAPIPort = b.getPeerAPIPortForTSMPPing
|
||||
tunWrap.PeerAPIPort = b.GetPeerAPIPort
|
||||
wiredPeerAPIPort = true
|
||||
}
|
||||
}
|
||||
@@ -607,7 +611,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
b.logf("[v1] netmap diff: (none)")
|
||||
} else {
|
||||
b.logf("netmap diff:\n%v", diff)
|
||||
b.logf("[v1] netmap diff:\n%v", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -906,7 +910,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
timer := time.NewTimer(time.Second)
|
||||
select {
|
||||
case <-b.gotPortPollRes:
|
||||
b.logf("got initial portlist info in %v", time.Since(t0).Round(time.Millisecond))
|
||||
b.logf("[v1] got initial portlist info in %v", time.Since(t0).Round(time.Millisecond))
|
||||
timer.Stop()
|
||||
case <-timer.C:
|
||||
b.logf("timeout waiting for initial portlist")
|
||||
@@ -1054,17 +1058,17 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
}
|
||||
|
||||
if !haveNetmap {
|
||||
b.logf("netmap packet filter: (not ready yet)")
|
||||
b.logf("[v1] netmap packet filter: (not ready yet)")
|
||||
b.setFilter(filter.NewAllowNone(b.logf, logNets))
|
||||
return
|
||||
}
|
||||
|
||||
oldFilter := b.e.GetFilter()
|
||||
if shieldsUp {
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
b.logf("[v1] netmap packet filter: (shields up)")
|
||||
b.setFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v filters", len(packetFilter))
|
||||
b.logf("[v1] netmap packet filter: %v filters", len(packetFilter))
|
||||
b.setFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
|
||||
}
|
||||
}
|
||||
@@ -1503,19 +1507,19 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
|
||||
}
|
||||
}
|
||||
|
||||
b.logf("using backend prefs")
|
||||
bs, err := b.store.ReadState(key)
|
||||
switch {
|
||||
case errors.Is(err, ipn.ErrStateNotExist):
|
||||
b.prefs = ipn.NewPrefs()
|
||||
b.prefs.WantRunning = false
|
||||
b.logf("created empty state for %q: %s", key, b.prefs.Pretty())
|
||||
b.logf("using backend prefs; created empty state for %q: %s", key, b.prefs.Pretty())
|
||||
return nil
|
||||
case err != nil:
|
||||
return fmt.Errorf("store.ReadState(%q): %v", key, err)
|
||||
return fmt.Errorf("backend prefs: store.ReadState(%q): %v", key, err)
|
||||
}
|
||||
b.prefs, err = ipn.PrefsFromBytes(bs, false)
|
||||
if err != nil {
|
||||
b.logf("using backend prefs for %q", key)
|
||||
return fmt.Errorf("PrefsFromBytes: %v", err)
|
||||
}
|
||||
|
||||
@@ -1538,7 +1542,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
|
||||
}
|
||||
}
|
||||
|
||||
b.logf("backend prefs for %q: %s", key, b.prefs.Pretty())
|
||||
b.logf("using backend prefs for %q: %s", key, b.prefs.Pretty())
|
||||
|
||||
b.sshAtomicBool.Set(b.prefs != nil && b.prefs.RunSSH)
|
||||
|
||||
@@ -1788,7 +1792,9 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
b.send(ipn.Notify{Prefs: newp})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok bool) {
|
||||
// GetPeerAPIPort returns the port number for the peerapi server
|
||||
// running on the provided IP.
|
||||
func (b *LocalBackend) GetPeerAPIPort(ip netaddr.IP) (port uint16, ok bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
@@ -1799,6 +1805,27 @@ func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// ServePeerAPIConnection serves an already-accepted connection c.
|
||||
//
|
||||
// The remote parameter is the remote address.
|
||||
// The local paramater is the local address (either a Tailscale IPv4
|
||||
// or IPv6 IP and the peerapi port for that address).
|
||||
//
|
||||
// The connection will be closed by ServePeerAPIConnection.
|
||||
func (b *LocalBackend) ServePeerAPIConnection(remote, local netaddr.IPPort, c net.Conn) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
if pln.ip == local.IP() {
|
||||
go pln.ServeConn(remote, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
b.logf("[unexpected] no peerAPI listener found for %v", local)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
proto := tailcfg.PeerAPI4
|
||||
@@ -1893,15 +1920,15 @@ func (b *LocalBackend) authReconfig() {
|
||||
b.mu.Unlock()
|
||||
|
||||
if blocked {
|
||||
b.logf("authReconfig: blocked, skipping.")
|
||||
b.logf("[v1] authReconfig: blocked, skipping.")
|
||||
return
|
||||
}
|
||||
if nm == nil {
|
||||
b.logf("authReconfig: netmap not yet valid. Skipping.")
|
||||
b.logf("[v1] authReconfig: netmap not yet valid. Skipping.")
|
||||
return
|
||||
}
|
||||
if !prefs.WantRunning {
|
||||
b.logf("authReconfig: skipping because !WantRunning.")
|
||||
b.logf("[v1] authReconfig: skipping because !WantRunning.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -481,47 +482,34 @@ func (pln *peerAPIListener) serve() {
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
peerNode, peerUser, ok := pln.lb.WhoIs(ipp)
|
||||
if !ok {
|
||||
logf("peerapi: unknown peer %v", ipp)
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
h := &peerAPIHandler{
|
||||
ps: pln.ps,
|
||||
isSelf: pln.ps.selfNode.User == peerNode.User,
|
||||
remoteAddr: ipp,
|
||||
peerNode: peerNode,
|
||||
peerUser: peerUser,
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
if addH2C != nil {
|
||||
addH2C(httpServer)
|
||||
}
|
||||
go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c})
|
||||
pln.ServeConn(ipp, c)
|
||||
}
|
||||
}
|
||||
|
||||
type oneConnListener struct {
|
||||
net.Listener
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
func (pln *peerAPIListener) ServeConn(src netaddr.IPPort, c net.Conn) {
|
||||
logf := pln.lb.logf
|
||||
peerNode, peerUser, ok := pln.lb.WhoIs(src)
|
||||
if !ok {
|
||||
logf("peerapi: unknown peer %v", src)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
h := &peerAPIHandler{
|
||||
ps: pln.ps,
|
||||
isSelf: pln.ps.selfNode.User == peerNode.User,
|
||||
remoteAddr: src,
|
||||
peerNode: peerNode,
|
||||
peerUser: peerUser,
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
if addH2C != nil {
|
||||
addH2C(httpServer)
|
||||
}
|
||||
go httpServer.Serve(netutil.NewOneConnListenerFrom(c, pln.ln))
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Close() error { return nil }
|
||||
|
||||
// peerAPIHandler serves the Peer API for a source specific client.
|
||||
type peerAPIHandler struct {
|
||||
ps *peerAPIServer
|
||||
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"tailscale.com/ipn/store/aws"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
@@ -47,6 +48,7 @@ import (
|
||||
"tailscale.com/util/groupmember"
|
||||
"tailscale.com/util/pidowner"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
@@ -182,6 +184,13 @@ func (s *Server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
func lookupUserFromID(logf logger.Logf, uid string) (*user.User, error) {
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil && runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(0x534)) {
|
||||
// The below workaround is only applicable when uid represents a
|
||||
// valid security principal. Omitting this check causes us to succeed
|
||||
// even when uid represents a deleted user.
|
||||
if !winutil.IsSIDValidPrincipal(uid) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
||||
// Work around https://github.com/tailscale/tailscale/issues/869 for
|
||||
// now. We don't strictly need the username. It's just a nice-to-have.
|
||||
@@ -300,7 +309,7 @@ func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
ErrorLog: logger.StdLogger(logf),
|
||||
Handler: s.localhostHandler(ci),
|
||||
}
|
||||
httpServer.Serve(&oneConnListener{&protoSwitchConn{s: s, br: br, Conn: c}})
|
||||
httpServer.Serve(netutil.NewOneConnListener(&protoSwitchConn{s: s, br: br, Conn: c}))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1053,29 +1062,6 @@ func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, error)) fun
|
||||
}
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
type oneConnListener struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Close() error { return nil }
|
||||
|
||||
func (l *oneConnListener) Addr() net.Addr { return dummyAddr("unused-address") }
|
||||
|
||||
func (a dummyAddr) Network() string { return string(a) }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
|
||||
// protoSwitchConn is a net.Conn that's we want to speak HTTP to but
|
||||
// it's already had a few bytes read from it to determine that it's
|
||||
// HTTP. So we Read from its bufio.Reader. On Close, we we tell the
|
||||
|
||||
@@ -571,6 +571,16 @@ func New(collection string) *Policy {
|
||||
}
|
||||
}
|
||||
|
||||
// dialLog is used by NewLogtailTransport to log the happy path of its
|
||||
// own dialing.
|
||||
//
|
||||
// By default it goes nowhere and is only enabled when
|
||||
// tailscaled's in verbose mode.
|
||||
//
|
||||
// log.Printf isn't used so its own logs don't loop back into logtail
|
||||
// in the happy path, thus generating more logs.
|
||||
var dialLog = log.New(io.Discard, "logtail: ", log.LstdFlags|log.Lmsgprefix)
|
||||
|
||||
// SetVerbosityLevel controls the verbosity level that should be
|
||||
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
|
||||
// are increasingly verbose.
|
||||
@@ -578,6 +588,9 @@ func New(collection string) *Policy {
|
||||
// It should not be changed concurrently with log writes.
|
||||
func (p *Policy) SetVerbosityLevel(level int) {
|
||||
p.Logtail.SetVerbosityLevel(level)
|
||||
if level > 0 {
|
||||
dialLog.SetOutput(os.Stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// Close immediately shuts down the logger.
|
||||
@@ -624,7 +637,7 @@ func NewLogtailTransport(host string) *http.Transport {
|
||||
c, err := nd.DialContext(ctx, netw, addr)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err == nil {
|
||||
log.Printf("logtail: dialed %q in %v", addr, d)
|
||||
dialLog.Printf("dialed %q in %v", addr, d)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -637,10 +650,10 @@ func NewLogtailTransport(host string) *http.Transport {
|
||||
err = errors.New(res.Status)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("logtail: CONNECT response from tailscaled: %v", err)
|
||||
log.Printf("logtail: CONNECT response error from tailscaled: %v", err)
|
||||
c.Close()
|
||||
} else {
|
||||
log.Printf("logtail: connected via tailscaled")
|
||||
dialLog.Printf("connected via tailscaled")
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (b *Backoff) BackOff(ctx context.Context, err error) {
|
||||
d = time.Duration(float64(d) * (rand.Float64() + 0.5))
|
||||
|
||||
if d >= b.LogLongerThan {
|
||||
b.logf("%s: backoff: %d msec", b.name, d.Milliseconds())
|
||||
b.logf("%s: [v1] backoff: %d msec", b.name, d.Milliseconds())
|
||||
}
|
||||
t := b.NewTimer(d)
|
||||
select {
|
||||
|
||||
@@ -7,6 +7,7 @@ package logtail
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -112,6 +113,16 @@ func ParsePublicID(s string) (PublicID, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// MustParsePublicID calls ParsePublicID and panics in case of an error.
|
||||
// It is intended for use with constant strings, typically in tests.
|
||||
func MustParsePublicID(s string) PublicID {
|
||||
id, err := ParsePublicID(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (id PublicID) MarshalText() ([]byte, error) {
|
||||
b := make([]byte, hex.EncodedLen(len(id)))
|
||||
if i := hex.Encode(b, id[:]); i != len(b) {
|
||||
@@ -156,3 +167,21 @@ func fromHexChar(c byte) (byte, bool) {
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (id1 PublicID) Less(id2 PublicID) bool {
|
||||
for i, c1 := range id1[:] {
|
||||
c2 := id2[i]
|
||||
if c1 != c2 {
|
||||
return c1 < c2
|
||||
}
|
||||
}
|
||||
return false // equal
|
||||
}
|
||||
|
||||
func (id PublicID) IsZero() bool {
|
||||
return id == PublicID{}
|
||||
}
|
||||
|
||||
func (id PublicID) Prefix64() uint64 {
|
||||
return binary.BigEndian.Uint64(id[:8])
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ func (l *Logger) drainPending(scratch []byte) (res []byte) {
|
||||
l.explainedRaw = true
|
||||
}
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: %s", b)
|
||||
b = l.encodeText(b, true)
|
||||
b = l.encodeText(b, true, 0)
|
||||
}
|
||||
|
||||
if entries > 0 {
|
||||
@@ -418,7 +418,7 @@ func (l *Logger) send(jsonBlob []byte) (int, error) {
|
||||
|
||||
// TODO: instead of allocating, this should probably just append
|
||||
// directly into the output log buffer.
|
||||
func (l *Logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
func (l *Logger) encodeText(buf []byte, skipClientTime bool, level int) []byte {
|
||||
now := l.timeNow()
|
||||
|
||||
// Factor in JSON encoding overhead to try to only do one alloc
|
||||
@@ -463,6 +463,14 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the log level, if non-zero. Note that we only use log
|
||||
// levels 1 and 2 currently. It's unlikely we'll ever make it
|
||||
// past 9.
|
||||
if level > 0 && level < 10 {
|
||||
b = append(b, `"v":`...)
|
||||
b = append(b, '0'+byte(level))
|
||||
b = append(b, ',')
|
||||
}
|
||||
b = append(b, "\"text\": \""...)
|
||||
for _, c := range buf {
|
||||
switch c {
|
||||
@@ -493,9 +501,9 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func (l *Logger) encode(buf []byte) []byte {
|
||||
func (l *Logger) encode(buf []byte, level int) []byte {
|
||||
if buf[0] != '{' {
|
||||
return l.encodeText(buf, l.skipClientTime) // text fast-path
|
||||
return l.encodeText(buf, l.skipClientTime, level) // text fast-path
|
||||
}
|
||||
|
||||
now := l.timeNow()
|
||||
@@ -560,7 +568,7 @@ func (l *Logger) Write(buf []byte) (int, error) {
|
||||
l.stderr.Write(withNL)
|
||||
}
|
||||
}
|
||||
b := l.encode(buf)
|
||||
b := l.encode(buf, level)
|
||||
_, err := l.send(b)
|
||||
return len(buf), err
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func TestLoggerEncodeTextAllocs(t *testing.T) {
|
||||
lg := &Logger{timeNow: time.Now}
|
||||
inBuf := []byte("some text to encode")
|
||||
err := tstest.MinAllocsPerRun(t, 1, func() {
|
||||
sink = lg.encodeText(inBuf, false)
|
||||
sink = lg.encodeText(inBuf, false, 0)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -328,10 +328,60 @@ func unmarshalOne(t *testing.T, body []byte) map[string]interface{} {
|
||||
func TestEncodeTextTruncation(t *testing.T) {
|
||||
lg := &Logger{timeNow: time.Now, lowMem: true}
|
||||
in := bytes.Repeat([]byte("a"), 300)
|
||||
b := lg.encodeText(in, true)
|
||||
b := lg.encodeText(in, true, 0)
|
||||
got := string(b)
|
||||
want := `{"text": "` + strings.Repeat("a", 255) + `…+45"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("got:\n%qwant:\n%q\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type simpleMemBuf struct {
|
||||
Buffer
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *simpleMemBuf) Write(p []byte) (n int, err error) { return b.buf.Write(p) }
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"normal",
|
||||
`{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z"}, "text": "normal"}` + "\n",
|
||||
},
|
||||
{
|
||||
"and a [v1] level one",
|
||||
`{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z"}, "v":1,"text": "and a level one"}` + "\n",
|
||||
},
|
||||
{
|
||||
"[v2] some verbose two",
|
||||
`{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z"}, "v":2,"text": "some verbose two"}` + "\n",
|
||||
},
|
||||
{
|
||||
"{}",
|
||||
`{"logtail":{"client_time":"1970-01-01T00:02:03.000000456Z"}}` + "\n",
|
||||
},
|
||||
{
|
||||
`{"foo":"bar"}`,
|
||||
`{"foo":"bar","logtail":{"client_time":"1970-01-01T00:02:03.000000456Z"}}` + "\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
buf := new(simpleMemBuf)
|
||||
lg := &Logger{
|
||||
timeNow: func() time.Time { return time.Unix(123, 456).UTC() },
|
||||
buffer: buf,
|
||||
}
|
||||
io.WriteString(lg, tt.in)
|
||||
got := buf.buf.String()
|
||||
if got != tt.want {
|
||||
t.Errorf("for %q,\n got: %#q\nwant: %#q\n", tt.in, got, tt.want)
|
||||
}
|
||||
if err := json.Compact(new(bytes.Buffer), buf.buf.Bytes()); err != nil {
|
||||
t.Errorf("invalid output JSON for %q: %s", tt.in, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,18 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
logf("dns: %v", debug)
|
||||
}()
|
||||
|
||||
// Before we read /etc/resolv.conf (which might be in a broken
|
||||
// or symlink-dangling state), try to ping the D-Bus service
|
||||
// for systemd-resolved. If it's active on the machine, this
|
||||
// will make it start up and write the /etc/resolv.conf file
|
||||
// before it replies to the ping. (see how systemd's
|
||||
// src/resolve/resolved.c calls manager_write_resolv_conf
|
||||
// before the sd_event_loop starts)
|
||||
resolvedUp := env.dbusPing("org.freedesktop.resolve1", "/org/freedesktop/resolve1") == nil
|
||||
if resolvedUp {
|
||||
dbg("resolved-ping", "yes")
|
||||
}
|
||||
|
||||
bs, err := env.fs.ReadFile(resolvConf)
|
||||
if os.IsNotExist(err) {
|
||||
dbg("rc", "missing")
|
||||
@@ -99,7 +111,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
dbg("resolved", "not-in-use")
|
||||
return "direct", nil
|
||||
}
|
||||
if err := env.dbusPing("org.freedesktop.resolve1", "/org/freedesktop/resolve1"); err != nil {
|
||||
if !resolvedUp {
|
||||
dbg("resolved", "no")
|
||||
return "direct", nil
|
||||
}
|
||||
@@ -309,15 +321,15 @@ func resolvedIsActuallyResolver(bs []byte) error {
|
||||
}
|
||||
|
||||
func dbusPing(name, objectPath string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
obj := conn.Object(name, dbus.ObjectPath(objectPath))
|
||||
call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0)
|
||||
return call.Err
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [rc=resolved nm=no ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -88,7 +88,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.2.3", false)),
|
||||
wantLog: "dns: [rc=resolved nm=yes nm-resolved=no ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -97,7 +97,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.26.2", true)),
|
||||
wantLog: "dns: [rc=resolved nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
want: "network-manager",
|
||||
},
|
||||
{
|
||||
@@ -106,7 +106,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.27.0", true)),
|
||||
wantLog: "dns: [rc=resolved nm=yes nm-resolved=yes nm-safe=no ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -115,7 +115,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.22.0", true)),
|
||||
wantLog: "dns: [rc=resolved nm=yes nm-resolved=yes nm-safe=no ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
// Regression tests for extreme corner cases below.
|
||||
@@ -141,7 +141,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"nameserver 127.0.0.53",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [rc=resolved nm=no ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -156,7 +156,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"# run \"systemd-resolve --status\" to see details about the actual nameservers.",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [rc=resolved nm=no ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -183,7 +183,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.32.12", true)),
|
||||
wantLog: "dns: [rc=nm nm-resolved=yes nm-safe=no ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -206,7 +206,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.26.3", true)),
|
||||
wantLog: "dns: [rc=nm nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
want: "network-manager",
|
||||
},
|
||||
{
|
||||
@@ -217,7 +217,27 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"nameserver 127.0.0.53",
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [rc=nm nm-resolved=yes nm=no ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/3531
|
||||
name: "networkmanager_but_systemd-resolved_with_search_domain",
|
||||
env: env(resolvDotConf(
|
||||
"# Generated by NetworkManager",
|
||||
"search lan",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
// Make sure that we ping systemd-resolved to let it start up and write its resolv.conf
|
||||
// before we read its file.
|
||||
env: env(resolvedStartOnPingAndThen(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
}
|
||||
@@ -281,9 +301,14 @@ func (m memFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbusService struct {
|
||||
name, path string
|
||||
hook func() // if non-nil, run on ping
|
||||
}
|
||||
|
||||
type envBuilder struct {
|
||||
fs memFS
|
||||
dbus []struct{ name, path string }
|
||||
dbus []dbusService
|
||||
nmUsingResolved bool
|
||||
nmVersion string
|
||||
resolvconfStyle string
|
||||
@@ -312,6 +337,9 @@ func env(opts ...envOption) newOSConfigEnv {
|
||||
dbusPing: func(name, path string) error {
|
||||
for _, svc := range b.dbus {
|
||||
if svc.name == name && svc.path == path {
|
||||
if svc.hook != nil {
|
||||
svc.hook()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -337,9 +365,25 @@ func resolvDotConf(ss ...string) envOption {
|
||||
})
|
||||
}
|
||||
|
||||
// resolvedRunning returns an option that makes resolved reply to a dbusPing.
|
||||
func resolvedRunning() envOption {
|
||||
return resolvedStartOnPingAndThen( /* nothing */ )
|
||||
}
|
||||
|
||||
// resolvedStartOnPingAndThen returns an option that makes resolved be
|
||||
// active but not yet running. On a dbus ping, it then applies the
|
||||
// provided options.
|
||||
func resolvedStartOnPingAndThen(opts ...envOption) envOption {
|
||||
return envOpt(func(b *envBuilder) {
|
||||
b.dbus = append(b.dbus, struct{ name, path string }{"org.freedesktop.resolve1", "/org/freedesktop/resolve1"})
|
||||
b.dbus = append(b.dbus, dbusService{
|
||||
name: "org.freedesktop.resolve1",
|
||||
path: "/org/freedesktop/resolve1",
|
||||
hook: func() {
|
||||
for _, opt := range opts {
|
||||
opt.apply(b)
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -347,7 +391,7 @@ func nmRunning(version string, usingResolved bool) envOption {
|
||||
return envOpt(func(b *envBuilder) {
|
||||
b.nmUsingResolved = usingResolved
|
||||
b.nmVersion = version
|
||||
b.dbus = append(b.dbus, struct{ name, path string }{"org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"})
|
||||
b.dbus = append(b.dbus, dbusService{name: "org.freedesktop.NetworkManager", path: "/org/freedesktop/NetworkManager/DnsManager"})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,9 @@ func (m *resolvdManager) SetDNS(config OSConfig) error {
|
||||
newSearch = append(newSearch, s.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
newResolvConf = append(newResolvConf, []byte(strings.Join(newSearch, " "))...)
|
||||
if len(newSearch) > 1 {
|
||||
newResolvConf = append(newResolvConf, []byte(strings.Join(newSearch, " "))...)
|
||||
}
|
||||
|
||||
err = m.fs.WriteFile(resolvConf, newResolvConf, 0644)
|
||||
if err != nil {
|
||||
|
||||
@@ -74,38 +74,6 @@ type resolvedLinkDomain struct {
|
||||
RoutingOnly bool
|
||||
}
|
||||
|
||||
// isResolvedActive determines if resolved is currently managing system DNS settings.
|
||||
func isResolvedActive() bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// Probably no DBus on the system, or we're not allowed to use
|
||||
// it. Cannot control resolved.
|
||||
return false
|
||||
}
|
||||
|
||||
rd := conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1"))
|
||||
call := rd.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0)
|
||||
if call.Err != nil {
|
||||
// Can't talk to resolved.
|
||||
return false
|
||||
}
|
||||
|
||||
config, err := newDirectManager(logger.Discard).readResolvFile(resolvConf)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// The sole nameserver must be the systemd-resolved stub.
|
||||
if len(config.Nameservers) == 1 && config.Nameservers[0] == resolvedListenAddr {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// resolvedManager is an OSConfigurator which uses the systemd-resolved DBus API.
|
||||
type resolvedManager struct {
|
||||
logf logger.Logf
|
||||
|
||||
@@ -278,53 +278,160 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con
|
||||
|
||||
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
|
||||
func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
return func(ctx context.Context, network, address string) (retConn net.Conn, ret error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// Bogus. But just let the real dialer return an error rather than
|
||||
// inventing a similar one.
|
||||
return fwd(ctx, network, address)
|
||||
}
|
||||
defer func() {
|
||||
// On any failure, assume our DNS is wrong and try our fallback, if any.
|
||||
if ret == nil || dnsCache.LookupIPFallback == nil {
|
||||
return
|
||||
}
|
||||
ips, err := dnsCache.LookupIPFallback(ctx, host)
|
||||
if err != nil {
|
||||
// Return with original error
|
||||
return
|
||||
}
|
||||
if c, err := raceDial(ctx, fwd, network, ips, port); err == nil {
|
||||
retConn = c
|
||||
ret = nil
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
ip, ip6, allIPs, err := dnsCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
|
||||
}
|
||||
i4s := v4addrs(allIPs)
|
||||
if len(i4s) < 2 {
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if debug {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, dst, address)
|
||||
}
|
||||
c, err := fwd(ctx, network, dst)
|
||||
if err == nil || ctx.Err() != nil || ip6 == nil {
|
||||
return c, err
|
||||
}
|
||||
// Fall back to trying IPv6.
|
||||
dst = net.JoinHostPort(ip6.String(), port)
|
||||
return fwd(ctx, network, dst)
|
||||
}
|
||||
|
||||
// Multiple IPv4 candidates, and 0+ IPv6.
|
||||
ipsToTry := append(i4s, v6addrs(allIPs)...)
|
||||
return raceDial(ctx, fwd, network, ipsToTry, port)
|
||||
d := &dialer{
|
||||
fwd: fwd,
|
||||
dnsCache: dnsCache,
|
||||
pastConnect: map[netaddr.IP]time.Time{},
|
||||
}
|
||||
return d.DialContext
|
||||
}
|
||||
|
||||
// dialer is the config and accumulated state for a dial func returned by Dialer.
|
||||
type dialer struct {
|
||||
fwd DialContextFunc
|
||||
dnsCache *Resolver
|
||||
|
||||
mu sync.Mutex
|
||||
pastConnect map[netaddr.IP]time.Time
|
||||
}
|
||||
|
||||
func (d *dialer) DialContext(ctx context.Context, network, address string) (retConn net.Conn, ret error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// Bogus. But just let the real dialer return an error rather than
|
||||
// inventing a similar one.
|
||||
return d.fwd(ctx, network, address)
|
||||
}
|
||||
dc := &dialCall{
|
||||
d: d,
|
||||
network: network,
|
||||
address: address,
|
||||
host: host,
|
||||
port: port,
|
||||
}
|
||||
defer func() {
|
||||
// On failure, consider that our DNS might be wrong and ask the DNS fallback mechanism for
|
||||
// some other IPs to try.
|
||||
if ret == nil || d.dnsCache.LookupIPFallback == nil || dc.dnsWasTrustworthy() {
|
||||
return
|
||||
}
|
||||
ips, err := d.dnsCache.LookupIPFallback(ctx, host)
|
||||
if err != nil {
|
||||
// Return with original error
|
||||
return
|
||||
}
|
||||
if c, err := dc.raceDial(ctx, ips); err == nil {
|
||||
retConn = c
|
||||
ret = nil
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
ip, ip6, allIPs, err := d.dnsCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
|
||||
}
|
||||
i4s := v4addrs(allIPs)
|
||||
if len(i4s) < 2 {
|
||||
if debug {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, ip, address)
|
||||
}
|
||||
ipNA, ok := netaddr.FromStdIP(ip)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid IP %q", ip)
|
||||
}
|
||||
c, err := dc.dialOne(ctx, ipNA)
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return c, err
|
||||
}
|
||||
// Fall back to trying IPv6, if any.
|
||||
ip6NA, ok := netaddr.FromStdIP(ip6)
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
return dc.dialOne(ctx, ip6NA)
|
||||
}
|
||||
|
||||
// Multiple IPv4 candidates, and 0+ IPv6.
|
||||
ipsToTry := append(i4s, v6addrs(allIPs)...)
|
||||
return dc.raceDial(ctx, ipsToTry)
|
||||
}
|
||||
|
||||
// dialCall is the state around a single call to dial.
|
||||
type dialCall struct {
|
||||
d *dialer
|
||||
network, address, host, port string
|
||||
|
||||
mu sync.Mutex // lock ordering: dialer.mu, then dialCall.mu
|
||||
fails map[netaddr.IP]error // set of IPs that failed to dial thus far
|
||||
}
|
||||
|
||||
// dnsWasTrustworthy reports whether we think the IP address(es) we
|
||||
// tried (and failed) to dial were probably the correct IPs. Currently
|
||||
// the heuristic is whether they ever worked previously.
|
||||
func (dc *dialCall) dnsWasTrustworthy() bool {
|
||||
dc.d.mu.Lock()
|
||||
defer dc.d.mu.Unlock()
|
||||
dc.mu.Lock()
|
||||
defer dc.mu.Unlock()
|
||||
|
||||
if len(dc.fails) == 0 {
|
||||
// No information.
|
||||
return false
|
||||
}
|
||||
|
||||
// If any of the IPs we failed to dial worked previously in
|
||||
// this dialer, assume the DNS is fine.
|
||||
for ip := range dc.fails {
|
||||
if _, ok := dc.d.pastConnect[ip]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (dc *dialCall) dialOne(ctx context.Context, ip netaddr.IP) (net.Conn, error) {
|
||||
c, err := dc.d.fwd(ctx, dc.network, net.JoinHostPort(ip.String(), dc.port))
|
||||
dc.noteDialResult(ip, err)
|
||||
return c, err
|
||||
}
|
||||
|
||||
// noteDialResult records that a dial to ip either succeeded or
|
||||
// failed.
|
||||
func (dc *dialCall) noteDialResult(ip netaddr.IP, err error) {
|
||||
if err == nil {
|
||||
d := dc.d
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.pastConnect[ip] = time.Now()
|
||||
return
|
||||
}
|
||||
dc.mu.Lock()
|
||||
defer dc.mu.Unlock()
|
||||
if dc.fails == nil {
|
||||
dc.fails = map[netaddr.IP]error{}
|
||||
}
|
||||
dc.fails[ip] = err
|
||||
}
|
||||
|
||||
// uniqueIPs returns a possibly-mutated subslice of ips, filtering out
|
||||
// dups and ones that have already failed previously.
|
||||
func (dc *dialCall) uniqueIPs(ips []netaddr.IP) (ret []netaddr.IP) {
|
||||
dc.mu.Lock()
|
||||
defer dc.mu.Unlock()
|
||||
seen := map[netaddr.IP]bool{}
|
||||
ret = ips[:0]
|
||||
for _, ip := range ips {
|
||||
if seen[ip] {
|
||||
continue
|
||||
}
|
||||
seen[ip] = true
|
||||
if dc.fails[ip] != nil {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, ip)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// fallbackDelay is how long to wait between trying subsequent
|
||||
@@ -334,7 +441,7 @@ const fallbackDelay = 300 * time.Millisecond
|
||||
|
||||
// raceDial tries to dial port on each ip in ips, starting a new race
|
||||
// dial every fallbackDelay apart, returning whichever completes first.
|
||||
func raceDial(ctx context.Context, fwd DialContextFunc, network string, ips []netaddr.IP, port string) (net.Conn, error) {
|
||||
func (dc *dialCall) raceDial(ctx context.Context, ips []netaddr.IP) (net.Conn, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
@@ -345,6 +452,14 @@ func raceDial(ctx context.Context, fwd DialContextFunc, network string, ips []ne
|
||||
resc := make(chan res) // must be unbuffered
|
||||
failBoost := make(chan struct{}) // best effort send on dial failure
|
||||
|
||||
// Remove IPs that we tried & failed to dial previously
|
||||
// (such as when we're being called after a dnsfallback lookup and get
|
||||
// the same results)
|
||||
ips = dc.uniqueIPs(ips)
|
||||
if len(ips) == 0 {
|
||||
return nil, errors.New("no IPs")
|
||||
}
|
||||
|
||||
go func() {
|
||||
for i, ip := range ips {
|
||||
if i != 0 {
|
||||
@@ -359,7 +474,7 @@ func raceDial(ctx context.Context, fwd DialContextFunc, network string, ips []ne
|
||||
}
|
||||
}
|
||||
go func(ip netaddr.IP) {
|
||||
c, err := fwd(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||
c, err := dc.dialOne(ctx, ip)
|
||||
if err != nil {
|
||||
// Best effort wake-up a pending dial.
|
||||
// e.g. IPv4 dials failing quickly on an IPv6-only system.
|
||||
|
||||
@@ -6,10 +6,14 @@ package dnscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
var dialTest = flag.String("dial-test", "", "if non-empty, addr:port to test dial")
|
||||
@@ -31,3 +35,78 @@ func TestDialer(t *testing.T) {
|
||||
t.Logf("dialed in %v", time.Since(t0))
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func TestDialCall_DNSWasTrustworthy(t *testing.T) {
|
||||
type step struct {
|
||||
ip netaddr.IP // IP we pretended to dial
|
||||
err error // the dial error or nil for success
|
||||
}
|
||||
mustIP := netaddr.MustParseIP
|
||||
errFail := errors.New("some connect failure")
|
||||
tests := []struct {
|
||||
name string
|
||||
steps []step
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "no-info",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "previous-dial",
|
||||
steps: []step{
|
||||
{mustIP("2003::1"), nil},
|
||||
{mustIP("2003::1"), errFail},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no-previous-dial",
|
||||
steps: []step{
|
||||
{mustIP("2003::1"), errFail},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d := &dialer{
|
||||
pastConnect: map[netaddr.IP]time.Time{},
|
||||
}
|
||||
dc := &dialCall{
|
||||
d: d,
|
||||
}
|
||||
for _, st := range tt.steps {
|
||||
dc.noteDialResult(st.ip, st.err)
|
||||
}
|
||||
got := dc.dnsWasTrustworthy()
|
||||
if got != tt.want {
|
||||
t.Errorf("got %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialCall_uniqueIPs(t *testing.T) {
|
||||
dc := &dialCall{}
|
||||
mustIP := netaddr.MustParseIP
|
||||
errFail := errors.New("some connect failure")
|
||||
dc.noteDialResult(mustIP("2003::1"), errFail)
|
||||
dc.noteDialResult(mustIP("2003::2"), errFail)
|
||||
got := dc.uniqueIPs([]netaddr.IP{
|
||||
mustIP("2003::1"),
|
||||
mustIP("2003::2"),
|
||||
mustIP("2003::2"),
|
||||
mustIP("2003::3"),
|
||||
mustIP("2003::3"),
|
||||
mustIP("2003::4"),
|
||||
mustIP("2003::4"),
|
||||
})
|
||||
want := []netaddr.IP{
|
||||
mustIP("2003::3"),
|
||||
mustIP("2003::4"),
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ func init() {
|
||||
|
||||
var procNetRouteErr syncs.AtomicBool
|
||||
|
||||
// errStopReading is a sentinel error value used internally by
|
||||
// lineread.File callers to stop reading. It doesn't escape to
|
||||
// callers/users.
|
||||
var errStopReading = errors.New("stop reading")
|
||||
|
||||
/*
|
||||
Parse 10.0.0.1 out of:
|
||||
|
||||
@@ -47,12 +52,15 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
|
||||
}
|
||||
lineNum := 0
|
||||
var f []mem.RO
|
||||
err := lineread.File("/proc/net/route", func(line []byte) error {
|
||||
err := lineread.File(procNetRoutePath, func(line []byte) error {
|
||||
lineNum++
|
||||
if lineNum == 1 {
|
||||
// Skip header line.
|
||||
return nil
|
||||
}
|
||||
if lineNum > maxProcNetRouteRead {
|
||||
return errStopReading
|
||||
}
|
||||
f = mem.AppendFields(f[:0], mem.B(line))
|
||||
if len(f) < 4 {
|
||||
return nil
|
||||
@@ -74,9 +82,13 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
|
||||
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
|
||||
if ip.IsPrivate() {
|
||||
ret = ip
|
||||
return errStopReading
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if errors.Is(err, errStopReading) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
procNetRouteErr.Set(true)
|
||||
if runtime.GOOS == "android" {
|
||||
@@ -139,6 +151,10 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
var zeroRouteBytes = []byte("00000000")
|
||||
var procNetRoutePath = "/proc/net/route"
|
||||
|
||||
// maxProcNetRouteRead is the max number of lines to read from
|
||||
// /proc/net/route looking for a default route.
|
||||
const maxProcNetRouteRead = 1000
|
||||
|
||||
func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) {
|
||||
f, err := os.Open(procNetRoutePath)
|
||||
if err != nil {
|
||||
@@ -147,9 +163,11 @@ func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) {
|
||||
defer f.Close()
|
||||
|
||||
br := bufio.NewReaderSize(f, bufsize)
|
||||
lineNum := 0
|
||||
for {
|
||||
lineNum++
|
||||
line, err := br.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
if err == io.EOF || lineNum > maxProcNetRouteRead {
|
||||
return "", fmt.Errorf("no default routes found: %w", err)
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestExtremelyLongProcNetRoute(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for n := 0; n <= 1000; n++ {
|
||||
for n := 0; n <= 900; n++ {
|
||||
line := fmt.Sprintf("eth%d\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n", n)
|
||||
_, err := f.Write([]byte(line))
|
||||
if err != nil {
|
||||
|
||||
50
net/netutil/netutil.go
Normal file
50
net/netutil/netutil.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2022 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 netutil contains misc shared networking code & types.
|
||||
package netutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// NewOneConnListener returns a net.Listener that returns c on its first
|
||||
// Accept and EOF thereafter. The Listener's Addr is a dummy address.
|
||||
func NewOneConnListener(c net.Conn) net.Listener {
|
||||
return NewOneConnListenerFrom(c, dummyListener{})
|
||||
}
|
||||
|
||||
// NewOneConnListenerFrom returns a net.Listener wrapping ln where
|
||||
// its Accept returns c on the first call and io.EOF thereafter.
|
||||
func NewOneConnListenerFrom(c net.Conn, ln net.Listener) net.Listener {
|
||||
return &oneConnListener{c, ln}
|
||||
}
|
||||
|
||||
type oneConnListener struct {
|
||||
conn net.Conn
|
||||
net.Listener
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
}
|
||||
|
||||
type dummyListener struct{}
|
||||
|
||||
func (dummyListener) Close() error { return nil }
|
||||
func (dummyListener) Addr() net.Addr { return dummyAddr("unused-address") }
|
||||
func (dummyListener) Accept() (c net.Conn, err error) { return nil, io.EOF }
|
||||
|
||||
type dummyAddr string
|
||||
|
||||
func (a dummyAddr) Network() string { return string(a) }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
"inet.af/netaddr"
|
||||
"inet.af/netstack/tcpip"
|
||||
"inet.af/netstack/tcpip/buffer"
|
||||
"inet.af/netstack/tcpip/header"
|
||||
"inet.af/netstack/tcpip/network/ipv4"
|
||||
"inet.af/netstack/tcpip/transport/udp"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
|
||||
@@ -431,7 +431,6 @@ func (t *Wrapper) filterOut(p *packet.Parsed) filter.Response {
|
||||
return filter.DropSilently // don't pass on to OS; already handled
|
||||
}
|
||||
}
|
||||
// TODO(bradfitz): support pinging TailscaleServiceIPv6 too.
|
||||
|
||||
// Issue 1526 workaround: if we sent disco packets over
|
||||
// Tailscale from ourselves, then drop them, as that shouldn't
|
||||
|
||||
@@ -41,11 +41,16 @@ main() {
|
||||
# - ID: the short name of the OS (e.g. "debian", "freebsd")
|
||||
# - VERSION_ID: the numeric release version for the OS, if any (e.g. "18.04")
|
||||
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
|
||||
# - UBUNTU_CODENAME: if it exists, as in linuxmint, use instead of VERSION_CODENAME
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|pop|neon|zorin|elementary|linuxmint)
|
||||
OS="ubuntu"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
if [ "${UBUNTU_CODENAME:-}" != "" ]; then
|
||||
VERSION="$UBUNTU_CODENAME"
|
||||
else
|
||||
VERSION="$VERSION_CODENAME"
|
||||
fi
|
||||
PACKAGETYPE="apt"
|
||||
# Third-party keyrings became the preferred method of
|
||||
# installation in Ubuntu 20.04.
|
||||
@@ -402,7 +407,8 @@ main() {
|
||||
fi
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
if ! type gpg >/dev/null; then
|
||||
apt-get install -y gnupg
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install -y gnupg
|
||||
fi
|
||||
|
||||
set -x
|
||||
@@ -462,7 +468,7 @@ main() {
|
||||
;;
|
||||
xbps)
|
||||
set -x
|
||||
$SUDO xbps-install tailscale
|
||||
$SUDO xbps-install tailscale -y
|
||||
set +x
|
||||
;;
|
||||
emerge)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"inet.af/netstack/atomicbitops"
|
||||
"gvisor.dev/gvisor/pkg/atomicbitops"
|
||||
)
|
||||
|
||||
// tests netstack's AlignedAtomicInt64.
|
||||
|
||||
@@ -376,6 +376,7 @@ func (h *Harness) testDistro(t *testing.T, d Distro, ipm ipMapping) {
|
||||
t.Run("login", func(t *testing.T) {
|
||||
runTestCommands(t, timeout, cli, []expect.Batcher{
|
||||
&expect.BSnd{S: fmt.Sprintf("tailscale up --login-server=%s\n", loginServer)},
|
||||
&expect.BSnd{S: "echo Success.\n"},
|
||||
&expect.BExp{R: `Success.`},
|
||||
})
|
||||
})
|
||||
|
||||
167
tsweb/debug.go
167
tsweb/debug.go
@@ -6,14 +6,20 @@ package tsweb
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/version"
|
||||
)
|
||||
@@ -33,6 +39,9 @@ type DebugHandler struct {
|
||||
kvs []func(io.Writer) // output one <li>...</li> each, see KV()
|
||||
urls []string // one <li>...</li> block with link each
|
||||
sections []func(io.Writer, *http.Request) // invoked in registration order prior to outputting </body>
|
||||
flagmu sync.Mutex // flagmu protects access to flagset and flagc
|
||||
flagset *flag.FlagSet // runtime-modifiable flags, may be nil
|
||||
flagc chan map[string]interface{} // DebugHandler sends new flag values on flagc when the flags have been modified
|
||||
}
|
||||
|
||||
// Debugger returns the DebugHandler registered on mux at /debug/,
|
||||
@@ -139,3 +148,161 @@ func gcHandler(w http.ResponseWriter, r *http.Request) {
|
||||
runtime.GC()
|
||||
w.Write([]byte("Done.\n"))
|
||||
}
|
||||
|
||||
// FlagSet returns a FlagSet that can be used to add runtime-modifiable flags to d.
|
||||
// Calling code should add flags to fs, but not retain the values directly.
|
||||
// Modifications to fs will be delivered via c.
|
||||
// Maps sent to c will be keyed on the flag name, and contain the new value.
|
||||
// Only modified values will be sent on c.
|
||||
//
|
||||
// Sample usage:
|
||||
// flagset, flagc := debug.FlagSet()
|
||||
// flagset.Int("max", 0, "maximum number of bars")
|
||||
// flagset.String("s", "qux", "default name for new foos")
|
||||
// go func() {
|
||||
// for change := range flagc {
|
||||
// // TODO: handle change, which will contain values for keys "max" and/or "s"
|
||||
// }
|
||||
// }()
|
||||
func (d *DebugHandler) FlagSet() (fs *flag.FlagSet, c chan map[string]interface{}) {
|
||||
d.flagmu.Lock()
|
||||
defer d.flagmu.Unlock()
|
||||
if d.flagset == nil {
|
||||
d.flagset = flag.NewFlagSet("debug", flag.ContinueOnError)
|
||||
d.flagc = make(chan map[string]interface{})
|
||||
d.Handle("flags", "Runtime flags", http.HandlerFunc(d.handleFlags))
|
||||
}
|
||||
return d.flagset, d.flagc
|
||||
}
|
||||
|
||||
type copiedFlag struct {
|
||||
Name string
|
||||
Value string
|
||||
Usage string
|
||||
}
|
||||
|
||||
func copyFlags(fs *flag.FlagSet) []copiedFlag {
|
||||
var all []copiedFlag
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
all = append(all, copiedFlag{Name: f.Name, Value: f.Value.String(), Usage: f.Usage})
|
||||
})
|
||||
return all
|
||||
}
|
||||
|
||||
func (d *DebugHandler) handleFlags(w http.ResponseWriter, r *http.Request) {
|
||||
d.flagmu.Lock()
|
||||
defer d.flagmu.Unlock()
|
||||
|
||||
var userError string
|
||||
var modified string
|
||||
if r.Method == http.MethodPost {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Make a copy of existing values, in case we need to roll back.
|
||||
all := copyFlags(d.flagset)
|
||||
// Set inbound values.
|
||||
changed := make(map[string][2]string)
|
||||
rollback := false
|
||||
for k, v := range r.PostForm {
|
||||
if len(v) != 1 {
|
||||
userError = fmt.Sprintf("multiple values for name %q: %q", k, v)
|
||||
rollback = true
|
||||
break
|
||||
}
|
||||
f := d.flagset.Lookup(k)
|
||||
if f == nil {
|
||||
userError = fmt.Sprintf("unknown name %q", k)
|
||||
rollback = true
|
||||
break
|
||||
}
|
||||
prev := f.Value.String()
|
||||
new := strings.TrimSpace(v[0])
|
||||
if prev == new {
|
||||
continue
|
||||
}
|
||||
err := d.flagset.Set(k, new)
|
||||
if err != nil {
|
||||
userError = fmt.Sprintf("parsing value %q for name %q: %v", new, k, err)
|
||||
rollback = true
|
||||
break
|
||||
}
|
||||
changed[k] = [2]string{prev, new}
|
||||
}
|
||||
if rollback {
|
||||
for _, f := range all {
|
||||
d.flagset.Set(f.Name, f.Value)
|
||||
}
|
||||
} else {
|
||||
// Generate description of modifications.
|
||||
var names []string
|
||||
for k := range changed {
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
buf := new(strings.Builder)
|
||||
for i, k := range names {
|
||||
if i != 0 {
|
||||
buf.WriteString("; ")
|
||||
}
|
||||
pn := changed[k]
|
||||
fmt.Fprintf(buf, "%q: %v → %v", k, pn[0], pn[1])
|
||||
}
|
||||
modified = buf.String()
|
||||
vals := make(map[string]interface{})
|
||||
for _, k := range names {
|
||||
vals[k] = d.flagset.Lookup(k).Value.(flag.Getter).Get()
|
||||
}
|
||||
|
||||
d.flagc <- vals
|
||||
// TODO: post modifications to Slack, along with attribution
|
||||
}
|
||||
}
|
||||
|
||||
dot := &struct {
|
||||
Error string
|
||||
Modified string
|
||||
Flags []copiedFlag
|
||||
}{
|
||||
Error: userError,
|
||||
Modified: modified,
|
||||
Flags: copyFlags(d.flagset),
|
||||
}
|
||||
err := flagsTemplate.Execute(w, dot)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
flagsTemplate = template.Must(template.New("flags").Parse(`
|
||||
<html>
|
||||
<body>
|
||||
|
||||
{{if .Error}}
|
||||
<h2>Error: <mark>{{.Error}}</mark></h2>
|
||||
{{end}}
|
||||
|
||||
{{if .Modified}}
|
||||
<h3>Modified: <mark>{{.Modified}}</mark></h3>
|
||||
{{end}}
|
||||
|
||||
<h3>Modifiable runtime flags</h3>
|
||||
|
||||
<p>Warning! Modifying these values will take effect immediately and impact the running service</p>
|
||||
|
||||
<form method="POST">
|
||||
<table>
|
||||
<tr> <th>Name</th> <th>Value</th> <th>Usage</th> </tr>
|
||||
{{range .Flags}}
|
||||
<tr> <td>{{.Name}}</td> <td><input type="text" value="{{.Value}}" name="{{.Name}}"/></td> <td>{{.Usage}}</td> </tr>
|
||||
{{end}}
|
||||
</table>
|
||||
<input type="submit"></input>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
)
|
||||
|
||||
@@ -5,9 +5,17 @@
|
||||
package tsweb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go4.org/mem"
|
||||
)
|
||||
|
||||
type response struct {
|
||||
@@ -85,7 +93,74 @@ func (fn JSONHandlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Request
|
||||
return jerr
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write(b)
|
||||
if AcceptsEncoding(r, "gzip") {
|
||||
encb, err := gzipBytes(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(encb)))
|
||||
w.WriteHeader(status)
|
||||
w.Write(encb)
|
||||
} else {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
|
||||
w.WriteHeader(status)
|
||||
w.Write(b)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var gzWriterPool sync.Pool // of *gzip.Writer
|
||||
|
||||
// gzipBytes returns the gzipped encoding of b.
|
||||
func gzipBytes(b []byte) (zb []byte, err error) {
|
||||
var buf bytes.Buffer
|
||||
zw, ok := gzWriterPool.Get().(*gzip.Writer)
|
||||
if ok {
|
||||
zw.Reset(&buf)
|
||||
} else {
|
||||
zw = gzip.NewWriter(&buf)
|
||||
}
|
||||
defer gzWriterPool.Put(zw)
|
||||
if _, err := zw.Write(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zb = buf.Bytes()
|
||||
zw.Reset(ioutil.Discard)
|
||||
return zb, nil
|
||||
}
|
||||
|
||||
// AcceptsEncoding reports whether r accepts the named encoding
|
||||
// ("gzip", "br", etc).
|
||||
func AcceptsEncoding(r *http.Request, enc string) bool {
|
||||
h := r.Header.Get("Accept-Encoding")
|
||||
if h == "" {
|
||||
return false
|
||||
}
|
||||
if !strings.Contains(h, enc) && !mem.ContainsFold(mem.S(h), mem.S(enc)) {
|
||||
return false
|
||||
}
|
||||
remain := h
|
||||
for len(remain) > 0 {
|
||||
comma := strings.Index(remain, ",")
|
||||
var part string
|
||||
if comma == -1 {
|
||||
part = remain
|
||||
remain = ""
|
||||
} else {
|
||||
part = remain[:comma]
|
||||
remain = remain[comma+1:]
|
||||
}
|
||||
part = strings.TrimSpace(part)
|
||||
if i := strings.Index(part, ";"); i != -1 {
|
||||
part = part[:i]
|
||||
}
|
||||
if part == enc {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
package tsweb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -27,13 +30,25 @@ type Response struct {
|
||||
}
|
||||
|
||||
func TestNewJSONHandler(t *testing.T) {
|
||||
checkStatus := func(w *httptest.ResponseRecorder, status string, code int) *Response {
|
||||
checkStatus := func(t *testing.T, w *httptest.ResponseRecorder, status string, code int) *Response {
|
||||
d := &Response{
|
||||
Data: &Data{},
|
||||
}
|
||||
|
||||
t.Logf("%s", w.Body.Bytes())
|
||||
err := json.Unmarshal(w.Body.Bytes(), d)
|
||||
bodyBytes := w.Body.Bytes()
|
||||
if w.Result().Header.Get("Content-Encoding") == "gzip" {
|
||||
zr, err := gzip.NewReader(bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
t.Fatalf("gzip read error at start: %v", err)
|
||||
}
|
||||
bodyBytes, err = io.ReadAll(zr)
|
||||
if err != nil {
|
||||
t.Fatalf("gzip read error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("%s", bodyBytes)
|
||||
err := json.Unmarshal(bodyBytes, d)
|
||||
if err != nil {
|
||||
t.Logf(err.Error())
|
||||
return nil
|
||||
@@ -64,7 +79,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h21.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
checkStatus(t, w, "success", http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("403 HTTPError", func(t *testing.T) {
|
||||
@@ -75,7 +90,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "error", http.StatusForbidden)
|
||||
checkStatus(t, w, "error", http.StatusForbidden)
|
||||
})
|
||||
|
||||
h22 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
@@ -86,7 +101,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h22.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
checkStatus(t, w, "success", http.StatusOK)
|
||||
})
|
||||
|
||||
h31 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
@@ -105,21 +120,21 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Name": "tailscale"}`))
|
||||
h31.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
checkStatus(t, w, "success", http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("400 bad json", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{`))
|
||||
h31.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "error", http.StatusBadRequest)
|
||||
checkStatus(t, w, "error", http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("400 post data error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{}`))
|
||||
h31.ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusBadRequest)
|
||||
resp := checkStatus(t, w, "error", http.StatusBadRequest)
|
||||
if resp.Error != "name is empty" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
@@ -144,18 +159,51 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Price": 10}`))
|
||||
h32.ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "success", http.StatusOK)
|
||||
resp := checkStatus(t, w, "success", http.StatusOK)
|
||||
t.Log(resp.Data)
|
||||
if resp.Data.Price != 20 {
|
||||
t.Fatalf("wrong price: %d %d", resp.Data.Price, 10)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gzipped", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Price": 10}`))
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
h32.ServeHTTPReturn(w, r)
|
||||
res := w.Result()
|
||||
if ct := res.Header.Get("Content-Encoding"); ct != "gzip" {
|
||||
t.Fatalf("encoding = %q; want gzip", ct)
|
||||
}
|
||||
resp := checkStatus(t, w, "success", http.StatusOK)
|
||||
t.Log(resp.Data)
|
||||
if resp.Data.Price != 20 {
|
||||
t.Fatalf("wrong price: %d %d", resp.Data.Price, 10)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gzipped_400", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Price": 10}`))
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
value := []string{"foo", "foo", "foo"}
|
||||
JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return 400, value, nil
|
||||
}).ServeHTTPReturn(w, r)
|
||||
res := w.Result()
|
||||
if ct := res.Header.Get("Content-Encoding"); ct != "gzip" {
|
||||
t.Fatalf("encoding = %q; want gzip", ct)
|
||||
}
|
||||
if res.StatusCode != 400 {
|
||||
t.Errorf("Status = %v; want 400", res.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("400 post data error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{}`))
|
||||
h32.ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusBadRequest)
|
||||
resp := checkStatus(t, w, "error", http.StatusBadRequest)
|
||||
if resp.Error != "price is empty" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
@@ -165,7 +213,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Name": "root"}`))
|
||||
h32.ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusInternalServerError)
|
||||
resp := checkStatus(t, w, "error", http.StatusInternalServerError)
|
||||
if resp.Error != "internal server error" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
@@ -177,7 +225,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusOK, make(chan int), nil
|
||||
}).ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusInternalServerError)
|
||||
resp := checkStatus(t, w, "error", http.StatusInternalServerError)
|
||||
if resp.Error != "json marshal error" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
@@ -189,7 +237,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
JSONHandlerFunc(func(r *http.Request) (status int, data interface{}, err error) {
|
||||
return
|
||||
}).ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "error", http.StatusInternalServerError)
|
||||
checkStatus(t, w, "error", http.StatusInternalServerError)
|
||||
})
|
||||
|
||||
t.Run("403 forbidden, status returned by JSONHandlerFunc and HTTPError agree", func(t *testing.T) {
|
||||
@@ -203,7 +251,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
Data: &Data{},
|
||||
Error: "403 forbidden",
|
||||
}
|
||||
got := checkStatus(w, "error", http.StatusForbidden)
|
||||
got := checkStatus(t, w, "error", http.StatusForbidden)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatalf(diff)
|
||||
}
|
||||
@@ -223,9 +271,37 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
Data: &Data{},
|
||||
Error: "403 forbidden",
|
||||
}
|
||||
got := checkStatus(w, "error", http.StatusForbidden)
|
||||
got := checkStatus(t, w, "error", http.StatusForbidden)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatalf("(-want,+got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAcceptsEncoding(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, enc string
|
||||
want bool
|
||||
}{
|
||||
{"", "gzip", false},
|
||||
{"gzip", "gzip", true},
|
||||
{"foo,gzip", "gzip", true},
|
||||
{"foo, gzip", "gzip", true},
|
||||
{"foo, gzip ", "gzip", true},
|
||||
{"gzip, foo ", "gzip", true},
|
||||
{"gzip, foo ", "br", false},
|
||||
{"gzip, foo ", "fo", false},
|
||||
{"gzip;q=1.2, foo ", "gzip", true},
|
||||
{" gzip;q=1.2, foo ", "gzip", true},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
h := make(http.Header)
|
||||
if tt.in != "" {
|
||||
h.Set("Accept-Encoding", tt.in)
|
||||
}
|
||||
got := AcceptsEncoding(&http.Request{Header: h}, tt.enc)
|
||||
if got != tt.want {
|
||||
t.Errorf("%d. got %v; want %v", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,88 +2,63 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Package winuntil contains misc Windows/win32 helper functions.
|
||||
// Package winutil contains misc Windows/Win32 helper functions.
|
||||
package winutil
|
||||
|
||||
import (
|
||||
"log"
|
||||
"syscall"
|
||||
// RegBase is the registry path inside HKEY_LOCAL_MACHINE where registry settings
|
||||
// are stored. This constant is a non-empty string only when GOOS=windows.
|
||||
const RegBase = regBase
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
const RegBase = `SOFTWARE\Tailscale IPN`
|
||||
|
||||
// GetDesktopPID searches the PID of the process that's running the
|
||||
// currently active desktop and whether it was found.
|
||||
// Usually the PID will be for explorer.exe.
|
||||
func GetDesktopPID() (pid uint32, ok bool) {
|
||||
hwnd := windows.GetShellWindow()
|
||||
if hwnd == 0 {
|
||||
return 0, false
|
||||
}
|
||||
windows.GetWindowThreadProcessId(hwnd, &pid)
|
||||
return pid, pid != 0
|
||||
// GetPolicyString looks up a registry value in the local machine's path for
|
||||
// system policies, or returns the given default if it can't.
|
||||
// Use this function to read values that may be set by sysadmins via the MSI
|
||||
// installer or via GPO. For registry settings that you do *not* want to be
|
||||
// visible to sysadmin tools, use GetRegString instead.
|
||||
//
|
||||
// This function will only work on GOOS=windows. Trying to run it on any other
|
||||
// OS will always return the default value.
|
||||
func GetPolicyString(name, defval string) string {
|
||||
return getPolicyString(name, defval)
|
||||
}
|
||||
|
||||
// GetRegString looks up a registry path in our local machine path, or returns
|
||||
// GetPolicyInteger looks up a registry value in the local machine's path for
|
||||
// system policies, or returns the given default if it can't.
|
||||
// Use this function to read values that may be set by sysadmins via the MSI
|
||||
// installer or via GPO. For registry settings that you do *not* want to be
|
||||
// visible to sysadmin tools, use GetRegInteger instead.
|
||||
//
|
||||
// This function will only work on GOOS=windows. Trying to run it on any other
|
||||
// OS will always return the default value.
|
||||
func GetPolicyInteger(name string, defval uint64) uint64 {
|
||||
return getPolicyInteger(name, defval)
|
||||
}
|
||||
|
||||
// GetRegString looks up a registry path in the local machine path, or returns
|
||||
// the given default if it can't.
|
||||
//
|
||||
// This function will only work on GOOS=windows. Trying to run it on any other
|
||||
// OS will always return the default value.
|
||||
func GetRegString(name, defval string) string {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, RegBase, registry.READ)
|
||||
if err != nil {
|
||||
log.Printf("registry.OpenKey(%v): %v", RegBase, err)
|
||||
return defval
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
val, _, err := key.GetStringValue(name)
|
||||
if err != nil {
|
||||
if err != registry.ErrNotExist {
|
||||
log.Printf("registry.GetStringValue(%v): %v", name, err)
|
||||
}
|
||||
return defval
|
||||
}
|
||||
return val
|
||||
return getRegString(name, defval)
|
||||
}
|
||||
|
||||
// GetRegInteger looks up a registry path in our local machine path, or returns
|
||||
// GetRegInteger looks up a registry path in the local machine path, or returns
|
||||
// the given default if it can't.
|
||||
//
|
||||
// This function will only work on GOOS=windows. Trying to run it on any other
|
||||
// OS will always return the default value.
|
||||
func GetRegInteger(name string, defval uint64) uint64 {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, RegBase, registry.READ)
|
||||
if err != nil {
|
||||
log.Printf("registry.OpenKey(%v): %v", RegBase, err)
|
||||
return defval
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
val, _, err := key.GetIntegerValue(name)
|
||||
if err != nil {
|
||||
if err != registry.ErrNotExist {
|
||||
log.Printf("registry.GetIntegerValue(%v): %v", name, err)
|
||||
}
|
||||
return defval
|
||||
}
|
||||
return val
|
||||
return getRegInteger(name, defval)
|
||||
}
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procWTSGetActiveConsoleSessionId = kernel32.NewProc("WTSGetActiveConsoleSessionId")
|
||||
)
|
||||
|
||||
// TODO(crawshaw): replace with x/sys/windows... one day.
|
||||
// https://go-review.googlesource.com/c/sys/+/331909
|
||||
func WTSGetActiveConsoleSessionId() uint32 {
|
||||
r1, _, _ := procWTSGetActiveConsoleSessionId.Call()
|
||||
return uint32(r1)
|
||||
// IsSIDValidPrincipal determines whether the SID contained in uid represents a
|
||||
// type that is a valid security principal under Windows. This check helps us
|
||||
// work around a bug in the standard library's Windows implementation of
|
||||
// LookupId in os/user.
|
||||
// See https://github.com/tailscale/tailscale/issues/869
|
||||
//
|
||||
// This function will only work on GOOS=windows. Trying to run it on any other
|
||||
// OS will always return false.
|
||||
func IsSIDValidPrincipal(uid string) bool {
|
||||
return isSIDValidPrincipal(uid)
|
||||
}
|
||||
|
||||
@@ -7,18 +7,14 @@
|
||||
|
||||
package winutil
|
||||
|
||||
const RegBase = ``
|
||||
const regBase = ``
|
||||
|
||||
// GetRegString looks up a registry path in our local machine path, or returns
|
||||
// the given default if it can't.
|
||||
//
|
||||
// This function will only work on GOOS=windows. Trying to run it on any other
|
||||
// OS will always return the default value.
|
||||
func GetRegString(name, defval string) string { return defval }
|
||||
func getPolicyString(name, defval string) string { return defval }
|
||||
|
||||
// GetRegInteger looks up a registry path in our local machine path, or returns
|
||||
// the given default if it can't.
|
||||
//
|
||||
// This function will only work on GOOS=windows. Trying to run it on any other
|
||||
// OS will always return the default value.
|
||||
func GetRegInteger(name string, defval uint64) uint64 { return defval }
|
||||
func getPolicyInteger(name string, defval uint64) uint64 { return defval }
|
||||
|
||||
func getRegString(name, defval string) string { return defval }
|
||||
|
||||
func getRegInteger(name string, defval uint64) uint64 { return defval }
|
||||
|
||||
func isSIDValidPrincipal(uid string) bool { return false }
|
||||
|
||||
256
util/winutil/winutil_windows.go
Normal file
256
util/winutil/winutil_windows.go
Normal file
@@ -0,0 +1,256 @@
|
||||
// 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 winutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
regBase = `SOFTWARE\Tailscale IPN`
|
||||
regPolicyBase = `SOFTWARE\Policies\Tailscale`
|
||||
)
|
||||
|
||||
// ErrNoShell is returned when the shell process is not found.
|
||||
var ErrNoShell = errors.New("no Shell process is present")
|
||||
|
||||
// GetDesktopPID searches the PID of the process that's running the
|
||||
// currently active desktop. Returns ErrNoShell if the shell is not present.
|
||||
// Usually the PID will be for explorer.exe.
|
||||
func GetDesktopPID() (uint32, error) {
|
||||
hwnd := windows.GetShellWindow()
|
||||
if hwnd == 0 {
|
||||
return 0, ErrNoShell
|
||||
}
|
||||
|
||||
var pid uint32
|
||||
windows.GetWindowThreadProcessId(hwnd, &pid)
|
||||
if pid == 0 {
|
||||
return 0, fmt.Errorf("invalid PID for HWND %v", hwnd)
|
||||
}
|
||||
|
||||
return pid, nil
|
||||
}
|
||||
|
||||
func getPolicyString(name, defval string) string {
|
||||
s, err := getRegStringInternal(regPolicyBase, name)
|
||||
if err != nil {
|
||||
// Fall back to the legacy path
|
||||
return getRegString(name, defval)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func getPolicyInteger(name string, defval uint64) uint64 {
|
||||
i, err := getRegIntegerInternal(regPolicyBase, name)
|
||||
if err != nil {
|
||||
// Fall back to the legacy path
|
||||
return getRegInteger(name, defval)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func getRegString(name, defval string) string {
|
||||
s, err := getRegStringInternal(regBase, name)
|
||||
if err != nil {
|
||||
return defval
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func getRegInteger(name string, defval uint64) uint64 {
|
||||
i, err := getRegIntegerInternal(regBase, name)
|
||||
if err != nil {
|
||||
return defval
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func getRegStringInternal(subKey, name string) (string, error) {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ)
|
||||
if err != nil {
|
||||
log.Printf("registry.OpenKey(%v): %v", subKey, err)
|
||||
return "", err
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
val, _, err := key.GetStringValue(name)
|
||||
if err != nil {
|
||||
if err != registry.ErrNotExist {
|
||||
log.Printf("registry.GetStringValue(%v): %v", name, err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func getRegIntegerInternal(subKey, name string) (uint64, error) {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ)
|
||||
if err != nil {
|
||||
log.Printf("registry.OpenKey(%v): %v", subKey, err)
|
||||
return 0, err
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
val, _, err := key.GetIntegerValue(name)
|
||||
if err != nil {
|
||||
if err != registry.ErrNotExist {
|
||||
log.Printf("registry.GetIntegerValue(%v): %v", name, err)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procWTSGetActiveConsoleSessionId = kernel32.NewProc("WTSGetActiveConsoleSessionId")
|
||||
)
|
||||
|
||||
// TODO(crawshaw): replace with x/sys/windows... one day.
|
||||
// https://go-review.googlesource.com/c/sys/+/331909
|
||||
func WTSGetActiveConsoleSessionId() uint32 {
|
||||
r1, _, _ := procWTSGetActiveConsoleSessionId.Call()
|
||||
return uint32(r1)
|
||||
}
|
||||
|
||||
func isSIDValidPrincipal(uid string) bool {
|
||||
usid, err := syscall.StringToSid(uid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, _, accType, err := usid.LookupAccount("")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch accType {
|
||||
case syscall.SidTypeUser, syscall.SidTypeGroup, syscall.SidTypeDomain, syscall.SidTypeAlias, syscall.SidTypeWellKnownGroup, syscall.SidTypeComputer:
|
||||
return true
|
||||
default:
|
||||
// Reject deleted users, invalid SIDs, unknown SIDs, mandatory label SIDs, etc.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// EnableCurrentThreadPrivilege enables the named privilege
|
||||
// in the current thread access token.
|
||||
func EnableCurrentThreadPrivilege(name string) error {
|
||||
var t windows.Token
|
||||
err := windows.OpenThreadToken(windows.CurrentThread(),
|
||||
windows.TOKEN_QUERY|windows.TOKEN_ADJUST_PRIVILEGES, false, &t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer t.Close()
|
||||
|
||||
var tp windows.Tokenprivileges
|
||||
|
||||
privStr, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = windows.LookupPrivilegeValue(nil, privStr, &tp.Privileges[0].Luid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tp.PrivilegeCount = 1
|
||||
tp.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED
|
||||
return windows.AdjustTokenPrivileges(t, false, &tp, 0, nil, nil)
|
||||
}
|
||||
|
||||
// StartProcessAsChild starts exePath process as a child of parentPID.
|
||||
// StartProcessAsChild copies parentPID's environment variables into
|
||||
// the new process, along with any optional environment variables in extraEnv.
|
||||
func StartProcessAsChild(parentPID uint32, exePath string, extraEnv []string) error {
|
||||
// The rest of this function requires SeDebugPrivilege to be held.
|
||||
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
err := windows.ImpersonateSelf(windows.SecurityImpersonation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer windows.RevertToSelf()
|
||||
|
||||
// According to https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights
|
||||
//
|
||||
// ... To open a handle to another process and obtain full access rights,
|
||||
// you must enable the SeDebugPrivilege privilege. ...
|
||||
//
|
||||
// But we only need PROCESS_CREATE_PROCESS. So perhaps SeDebugPrivilege is too much.
|
||||
//
|
||||
// https://devblogs.microsoft.com/oldnewthing/20080314-00/?p=23113
|
||||
//
|
||||
// TODO: try look for something less than SeDebugPrivilege
|
||||
|
||||
err = EnableCurrentThreadPrivilege("SeDebugPrivilege")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ph, err := windows.OpenProcess(
|
||||
windows.PROCESS_CREATE_PROCESS|windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_DUP_HANDLE,
|
||||
false, parentPID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer windows.CloseHandle(ph)
|
||||
|
||||
var pt windows.Token
|
||||
err = windows.OpenProcessToken(ph, windows.TOKEN_QUERY, &pt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pt.Close()
|
||||
|
||||
env, err := pt.Environ(false)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
}
|
||||
env = append(env, extraEnv...)
|
||||
|
||||
sys := &syscall.SysProcAttr{ParentProcess: syscall.Handle(ph)}
|
||||
|
||||
cmd := exec.Command(exePath)
|
||||
cmd.Env = env
|
||||
cmd.SysProcAttr = sys
|
||||
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// StartProcessAsCurrentGUIUser is like StartProcessAsChild, but if finds
|
||||
// current logged in user desktop process (normally explorer.exe),
|
||||
// and passes found PID to StartProcessAsChild.
|
||||
func StartProcessAsCurrentGUIUser(exePath string, extraEnv []string) error {
|
||||
// as described in https://devblogs.microsoft.com/oldnewthing/20190425-00/?p=102443
|
||||
desktop, err := GetDesktopPID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find desktop: %v", err)
|
||||
}
|
||||
err = StartProcessAsChild(desktop, exePath, extraEnv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start executable: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAppMutex creates a named Windows mutex, returning nil if the mutex
|
||||
// is created successfully or an error if the mutex already exists or could not
|
||||
// be created for some other reason.
|
||||
func CreateAppMutex(name string) (windows.Handle, error) {
|
||||
return windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(name))
|
||||
}
|
||||
@@ -305,6 +305,8 @@ type Conn struct {
|
||||
// lock ordering deadlocks. See issue 3726 and mu field docs.
|
||||
derpMapAtomic atomic.Value // of *tailcfg.DERPMap
|
||||
|
||||
lastNetCheckReport atomic.Value // of *netcheck.Report
|
||||
|
||||
// port is the preferred port from opts.Port; 0 means auto.
|
||||
port syncs.AtomicUint32
|
||||
|
||||
@@ -741,6 +743,7 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.lastNetCheckReport.Store(report)
|
||||
c.noV4.Set(!report.IPv4)
|
||||
c.noV6.Set(!report.IPv6)
|
||||
c.noV4Send.Set(!report.IPv4CanSend)
|
||||
@@ -1380,6 +1383,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.NodePublic) cha
|
||||
|
||||
dc.SetCanAckPings(true)
|
||||
dc.NotePreferred(c.myDerp == regionID)
|
||||
dc.SetAddressFamilySelector(derpAddrFamSelector{c})
|
||||
dc.DNSCache = dnscache.Get()
|
||||
|
||||
ctx, cancel := context.WithCancel(c.connCtx)
|
||||
@@ -2107,12 +2111,12 @@ func (c *Conn) enqueueCallMeMaybe(derpAddr netaddr.IPPort, de *endpoint) {
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if !c.lastEndpointsTime.After(time.Now().Add(-endpointsFreshEnoughDuration)) {
|
||||
c.logf("magicsock: want call-me-maybe but endpoints stale; restunning")
|
||||
c.logf("[v1] magicsock: want call-me-maybe but endpoints stale; restunning")
|
||||
if c.onEndpointRefreshed == nil {
|
||||
c.onEndpointRefreshed = map[*endpoint]func(){}
|
||||
}
|
||||
c.onEndpointRefreshed[de] = func() {
|
||||
c.logf("magicsock: STUN done; sending call-me-maybe to %v %v", de.discoShort, de.publicKey.ShortString())
|
||||
c.logf("[v1] magicsock: STUN done; sending call-me-maybe to %v %v", de.discoShort, de.publicKey.ShortString())
|
||||
c.enqueueCallMeMaybe(derpAddr, de)
|
||||
}
|
||||
// TODO(bradfitz): make a new 'reSTUNQuickly' method
|
||||
@@ -4125,6 +4129,22 @@ func (di *discoInfo) setNodeKey(nk key.NodePublic) {
|
||||
di.lastNodeKeyTime = time.Now()
|
||||
}
|
||||
|
||||
// derpAddrFamSelector is the derphttp.AddressFamilySelector we pass
|
||||
// to derphttp.Client.SetAddressFamilySelector.
|
||||
//
|
||||
// It provides the hint as to whether in an IPv4-vs-IPv6 race that
|
||||
// IPv4 should be held back a bit to give IPv6 a better-than-50/50
|
||||
// chance of winning. We only return true when we believe IPv6 will
|
||||
// work anyway, so we don't artificially delay the connection speed.
|
||||
type derpAddrFamSelector struct{ c *Conn }
|
||||
|
||||
func (s derpAddrFamSelector) PreferIPv6() bool {
|
||||
if r, ok := s.c.lastNetCheckReport.Load().(*netcheck.Report); ok {
|
||||
return r.IPv6
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
metricNumPeers = clientmetric.NewGauge("magicsock_netmap_num_peers")
|
||||
metricNumDERPConns = clientmetric.NewGauge("magicsock_num_derp_conns")
|
||||
|
||||
@@ -101,9 +101,21 @@ func (c *nlConn) Receive() (message, error) {
|
||||
dst := netaddrIPPrefix(rmsg.Attributes.Dst, rmsg.DstLength)
|
||||
gw := netaddrIP(rmsg.Attributes.Gateway)
|
||||
|
||||
if msg.Header.Type == unix.RTM_NEWROUTE &&
|
||||
(rmsg.Attributes.Table == 255 || rmsg.Attributes.Table == 254) &&
|
||||
(dst.IP().IsMulticast() || dst.IP().IsLinkLocalUnicast()) {
|
||||
// Normal Linux route changes on new interface coming up; don't log or react.
|
||||
return ignoreMessage{}, nil
|
||||
}
|
||||
|
||||
if rmsg.Table == tsTable && dst.IsSingleIP() {
|
||||
// Don't log. Spammy and normal to see a bunch of these on start-up,
|
||||
// which we make ourselves.
|
||||
} else if tsaddr.IsTailscaleIP(dst.IP()) {
|
||||
// Verbose only.
|
||||
c.logf("%s: [v1] src=%v, dst=%v, gw=%v, outif=%v, table=%v", typeStr,
|
||||
condNetAddrPrefix(src), condNetAddrPrefix(dst), condNetAddrIP(gw),
|
||||
rmsg.Attributes.OutIface, rmsg.Attributes.Table)
|
||||
} else {
|
||||
c.logf("%s: src=%v, dst=%v, gw=%v, outif=%v, table=%v", typeStr,
|
||||
condNetAddrPrefix(src), condNetAddrPrefix(dst), condNetAddrIP(gw),
|
||||
|
||||
@@ -21,19 +21,19 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
"gvisor.dev/gvisor/pkg/waiter"
|
||||
"inet.af/netaddr"
|
||||
"inet.af/netstack/tcpip"
|
||||
"inet.af/netstack/tcpip/adapters/gonet"
|
||||
"inet.af/netstack/tcpip/buffer"
|
||||
"inet.af/netstack/tcpip/header"
|
||||
"inet.af/netstack/tcpip/link/channel"
|
||||
"inet.af/netstack/tcpip/network/ipv4"
|
||||
"inet.af/netstack/tcpip/network/ipv6"
|
||||
"inet.af/netstack/tcpip/stack"
|
||||
"inet.af/netstack/tcpip/transport/icmp"
|
||||
"inet.af/netstack/tcpip/transport/tcp"
|
||||
"inet.af/netstack/tcpip/transport/udp"
|
||||
"inet.af/netstack/waiter"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/net/packet"
|
||||
@@ -82,9 +82,12 @@ type Impl struct {
|
||||
mc *magicsock.Conn
|
||||
logf logger.Logf
|
||||
dialer *tsdial.Dialer
|
||||
ctx context.Context // alive until Close
|
||||
ctxCancel context.CancelFunc // called on Close
|
||||
lb *ipnlocal.LocalBackend
|
||||
ctx context.Context // alive until Close
|
||||
ctxCancel context.CancelFunc // called on Close
|
||||
lb *ipnlocal.LocalBackend // or nil
|
||||
|
||||
peerapiPort4Atomic uint32 // uint16 port number for IPv4 peerapi
|
||||
peerapiPort6Atomic uint32 // uint16 port number for IPv6 peerapi
|
||||
|
||||
// atomicIsLocalIPFunc holds a func that reports whether an IP
|
||||
// is a local (non-subnet) Tailscale IP address of this
|
||||
@@ -370,8 +373,8 @@ func (ns *Impl) DialContextUDP(ctx context.Context, ipp netaddr.IPPort) (*gonet.
|
||||
|
||||
func (ns *Impl) injectOutbound() {
|
||||
for {
|
||||
packetInfo, ok := ns.linkEP.ReadContext(ns.ctx)
|
||||
if !ok {
|
||||
pkt := ns.linkEP.ReadContext(ns.ctx)
|
||||
if pkt == nil {
|
||||
if ns.ctx.Err() != nil {
|
||||
// Return without logging.
|
||||
return
|
||||
@@ -379,7 +382,6 @@ func (ns *Impl) injectOutbound() {
|
||||
ns.logf("[v2] ReadContext-for-write = ok=false")
|
||||
continue
|
||||
}
|
||||
pkt := packetInfo.Pkt
|
||||
hdrNetwork := pkt.NetworkHeader()
|
||||
hdrTransport := pkt.TransportHeader()
|
||||
|
||||
@@ -408,9 +410,33 @@ func (ns *Impl) processSSH() bool {
|
||||
return ns.lb != nil && ns.lb.ShouldRunSSH()
|
||||
}
|
||||
|
||||
func (ns *Impl) peerAPIPortAtomic(ip netaddr.IP) *uint32 {
|
||||
if ip.Is4() {
|
||||
return &ns.peerapiPort4Atomic
|
||||
} else {
|
||||
return &ns.peerapiPort6Atomic
|
||||
}
|
||||
}
|
||||
|
||||
// shouldProcessInbound reports whether an inbound packet should be
|
||||
// handled by netstack.
|
||||
func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
||||
// Handle incoming peerapi connections in netstack.
|
||||
if ns.lb != nil && p.IPProto == ipproto.TCP {
|
||||
var peerAPIPort uint16
|
||||
dstIP := p.Dst.IP()
|
||||
if p.TCPFlags&packet.TCPSynAck == packet.TCPSyn && ns.isLocalIP(dstIP) {
|
||||
if port, ok := ns.lb.GetPeerAPIPort(p.Dst.IP()); ok {
|
||||
peerAPIPort = port
|
||||
atomic.StoreUint32(ns.peerAPIPortAtomic(dstIP), uint32(port))
|
||||
}
|
||||
} else {
|
||||
peerAPIPort = uint16(atomic.LoadUint32(ns.peerAPIPortAtomic(dstIP)))
|
||||
}
|
||||
if p.IPProto == ipproto.TCP && p.Dst.Port() == peerAPIPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if ns.isInboundTSSH(p) && ns.processSSH() {
|
||||
return true
|
||||
}
|
||||
@@ -622,6 +648,16 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if ns.lb != nil {
|
||||
if port, ok := ns.lb.GetPeerAPIPort(dialIP); ok {
|
||||
if reqDetails.LocalPort == port && ns.isLocalIP(dialIP) {
|
||||
src := netaddr.IPPortFrom(clientRemoteIP, reqDetails.RemotePort)
|
||||
dst := netaddr.IPPortFrom(dialIP, port)
|
||||
ns.lb.ServePeerAPIConnection(src, dst, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if ns.ForwardTCPIn != nil {
|
||||
ns.ForwardTCPIn(c, reqDetails.LocalPort)
|
||||
return
|
||||
|
||||
@@ -107,7 +107,13 @@ func (ns *Impl) handleSSH(s ssh.Session) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("/bin/bash")
|
||||
var cmd *exec.Cmd
|
||||
sshUser := s.User()
|
||||
if os.Getuid() != 0 || sshUser == "root" {
|
||||
cmd = exec.Command("/bin/bash")
|
||||
} else {
|
||||
cmd = exec.Command("/usr/bin/env", "su", "-", sshUser)
|
||||
}
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
|
||||
f, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
|
||||
@@ -132,7 +132,7 @@ feist
|
||||
spitz
|
||||
squirrel
|
||||
gerbil
|
||||
hampster
|
||||
hamster
|
||||
panda
|
||||
gibbon
|
||||
flyingfox
|
||||
@@ -210,3 +210,5 @@ tyrannosaurus
|
||||
velociraptor
|
||||
siren
|
||||
mudpuppy
|
||||
ferret
|
||||
roborovski
|
||||
|
||||
Reference in New Issue
Block a user