Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e32002cf3 | ||
|
|
2a482848d8 | ||
|
|
9bb601ebe8 | ||
|
|
a7e118052f | ||
|
|
df5d9aa4ef | ||
|
|
6a949d39e0 | ||
|
|
d6fb860206 | ||
|
|
d8c78df2e6 | ||
|
|
ae33622c71 | ||
|
|
8e643357dc | ||
|
|
59a1b849f0 | ||
|
|
296d10a05d | ||
|
|
f37cdaefa7 | ||
|
|
1e6ca50e3b | ||
|
|
dbb6597381 | ||
|
|
2e80227276 | ||
|
|
96b76e4cc6 | ||
|
|
ec04759c41 | ||
|
|
88c4bde778 | ||
|
|
7052c6fe25 | ||
|
|
e510abc8d0 | ||
|
|
958917dce8 | ||
|
|
7c1a1aa5d9 | ||
|
|
90423bf3de | ||
|
|
0028a8d4d5 | ||
|
|
8519cab83d | ||
|
|
4cd062071c | ||
|
|
04a7f5066d | ||
|
|
a14d445fc7 | ||
|
|
1a4293c15c |
@@ -1 +1 @@
|
||||
1.19.0
|
||||
1.20.4
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -786,6 +786,32 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
wantSimpleUp: true,
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
},
|
||||
{
|
||||
name: "just_edit_reset",
|
||||
flags: []string{"--reset"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
},
|
||||
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,
|
||||
ShieldsUpSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "control_synonym",
|
||||
flags: []string{},
|
||||
@@ -850,16 +876,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 {
|
||||
|
||||
@@ -379,7 +379,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
|
||||
@@ -413,15 +414,19 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
|
||||
justEdit := env.backendState == ipn.Running.String() &&
|
||||
!env.upArgs.forceReauth &&
|
||||
!env.upArgs.reset &&
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -513,7 +518,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() }) }
|
||||
|
||||
@@ -539,7 +544,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
|
||||
|
||||
@@ -375,7 +375,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data.AdvertiseExitNode = true
|
||||
} else {
|
||||
if data.AdvertiseRoutes != "" {
|
||||
data.AdvertiseRoutes = ","
|
||||
data.AdvertiseRoutes += ","
|
||||
}
|
||||
data.AdvertiseRoutes += r.String()
|
||||
}
|
||||
|
||||
@@ -328,9 +328,6 @@ func run() error {
|
||||
}
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
|
||||
if useNetstack {
|
||||
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
|
||||
@@ -391,6 +388,10 @@ func run() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipnserver.New: %w", err)
|
||||
}
|
||||
ns.SetLocalBackend(srv.LocalBackend())
|
||||
if err := ns.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
|
||||
if debugMux != nil {
|
||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
||||
|
||||
@@ -212,7 +212,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
|
||||
}
|
||||
}
|
||||
@@ -1779,7 +1779,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 {
|
||||
@@ -1790,6 +1792,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
|
||||
|
||||
@@ -481,29 +481,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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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(&oneConnListener{Listener: pln.ln, conn: c})
|
||||
}
|
||||
|
||||
type oneConnListener struct {
|
||||
net.Listener
|
||||
conn net.Conn
|
||||
|
||||
@@ -4,8 +4,71 @@
|
||||
|
||||
package dns
|
||||
|
||||
import "tailscale.com/types/logger"
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type kv struct {
|
||||
k, v string
|
||||
}
|
||||
|
||||
func (kv kv) String() string {
|
||||
return fmt.Sprintf("%s=%s", kv.k, kv.v)
|
||||
}
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) {
|
||||
return newOSConfigurator(logf, interfaceName,
|
||||
newOSConfigEnv{
|
||||
rcIsResolvd: rcIsResolvd,
|
||||
fs: directFS{},
|
||||
})
|
||||
}
|
||||
|
||||
// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing.
|
||||
type newOSConfigEnv struct {
|
||||
fs directFS
|
||||
rcIsResolvd func(resolvConfContents []byte) bool
|
||||
}
|
||||
|
||||
func newOSConfigurator(logf logger.Logf, interfaceName string, env newOSConfigEnv) (ret OSConfigurator, err error) {
|
||||
var debug []kv
|
||||
dbg := func(k, v string) {
|
||||
debug = append(debug, kv{k, v})
|
||||
}
|
||||
defer func() {
|
||||
if ret != nil {
|
||||
dbg("ret", fmt.Sprintf("%T", ret))
|
||||
}
|
||||
logf("dns: %v", debug)
|
||||
}()
|
||||
|
||||
bs, err := env.fs.ReadFile(resolvConf)
|
||||
if os.IsNotExist(err) {
|
||||
dbg("rc", "missing")
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
}
|
||||
|
||||
if env.rcIsResolvd(bs) {
|
||||
dbg("resolvd", "yes")
|
||||
return newResolvdManager(logf, interfaceName)
|
||||
}
|
||||
|
||||
dbg("resolvd", "missing")
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
|
||||
func rcIsResolvd(resolvConfContents []byte) bool {
|
||||
// If we have the string "# resolvd:" in resolv.conf resolvd(8) is
|
||||
// managing things.
|
||||
if bytes.Contains(resolvConfContents, []byte("# resolvd:")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ package dns
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -34,6 +36,8 @@ const (
|
||||
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
|
||||
)
|
||||
|
||||
var configureWSL, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_CONFIGURE_WSL"))
|
||||
|
||||
type windowsManager struct {
|
||||
logf logger.Logf
|
||||
guid string
|
||||
@@ -307,13 +311,15 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
|
||||
|
||||
// On initial setup of WSL, the restart caused by --shutdown is slow,
|
||||
// so we do it out-of-line.
|
||||
go func() {
|
||||
if err := m.wslManager.SetDNS(cfg); err != nil {
|
||||
m.logf("WSL SetDNS: %v", err) // continue
|
||||
} else {
|
||||
m.logf("WSL SetDNS: success")
|
||||
}
|
||||
}()
|
||||
if configureWSL {
|
||||
go func() {
|
||||
if err := m.wslManager.SetDNS(cfg); err != nil {
|
||||
m.logf("WSL SetDNS: %v", err) // continue
|
||||
} else {
|
||||
m.logf("WSL SetDNS: success")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
163
net/dns/resolvd.go
Normal file
163
net/dns/resolvd.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build openbsd
|
||||
// +build openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
func newResolvdManager(logf logger.Logf, interfaceName string) (*resolvdManager, error) {
|
||||
return &resolvdManager{
|
||||
logf: logf,
|
||||
ifName: interfaceName,
|
||||
fs: directFS{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolvdManager is an OSConfigurator which uses route(1) to teach OpenBSD's
|
||||
// resolvd(8) about DNS servers.
|
||||
type resolvdManager struct {
|
||||
logf logger.Logf
|
||||
ifName string
|
||||
fs directFS
|
||||
}
|
||||
|
||||
func (m *resolvdManager) SetDNS(config OSConfig) error {
|
||||
args := []string{
|
||||
"nameserver",
|
||||
m.ifName,
|
||||
}
|
||||
|
||||
origResolv, err := m.readAndCopy(resolvConf, backupConf, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newResolvConf := removeSearchLines(origResolv)
|
||||
|
||||
for _, ns := range config.Nameservers {
|
||||
args = append(args, ns.String())
|
||||
}
|
||||
|
||||
var newSearch = []string{
|
||||
"search",
|
||||
}
|
||||
for _, s := range config.SearchDomains {
|
||||
newSearch = append(newSearch, s.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
if len(newSearch) > 1 {
|
||||
newResolvConf = append(newResolvConf, []byte(strings.Join(newSearch, " "))...)
|
||||
}
|
||||
|
||||
err = m.fs.WriteFile(resolvConf, newResolvConf, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("/sbin/route", args...)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (m *resolvdManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *resolvdManager) GetBaseConfig() (OSConfig, error) {
|
||||
cfg, err := m.readResolvConf()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *resolvdManager) Close() error {
|
||||
// resolvd handles teardown of nameservers so we only need to write back the original
|
||||
// config and be done.
|
||||
|
||||
_, err := m.readAndCopy(backupConf, resolvConf, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.fs.Remove(backupConf)
|
||||
}
|
||||
|
||||
func (m *resolvdManager) readAndCopy(a, b string, mode os.FileMode) ([]byte, error) {
|
||||
orig, err := m.fs.ReadFile(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = m.fs.WriteFile(b, orig, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orig, nil
|
||||
}
|
||||
|
||||
func (m resolvdManager) readResolvConf() (config OSConfig, err error) {
|
||||
b, err := m.fs.ReadFile(resolvConf)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// resolvd manages "nameserver" lines, we only need to handle
|
||||
// "search".
|
||||
if strings.HasPrefix(line, "search") {
|
||||
domain := strings.TrimPrefix(line, "search")
|
||||
domain = strings.TrimSpace(domain)
|
||||
fqdn, err := dnsname.ToFQDN(domain)
|
||||
if err != nil {
|
||||
return OSConfig{}, fmt.Errorf("parsing search domains %q: %w", line, err)
|
||||
}
|
||||
config.SearchDomains = append(config.SearchDomains, fqdn)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "nameserver") {
|
||||
s := strings.TrimPrefix(line, "nameserver")
|
||||
parts := strings.Split(s, " # ")
|
||||
if len(parts) == 0 {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
nameserver := strings.TrimSpace(parts[0])
|
||||
ip, err := netaddr.ParseIP(nameserver)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
config.Nameservers = append(config.Nameservers, ip)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func removeSearchLines(orig []byte) []byte {
|
||||
re := regexp.MustCompile(`(?m)^search\s+.+$`)
|
||||
return re.ReplaceAll(orig, []byte(""))
|
||||
}
|
||||
@@ -529,6 +529,10 @@ func stubResolverForOS() (ip netaddr.IP, err error) {
|
||||
if c, ok := resolvConfCacheValue.Load().(resolvConfCache); ok && c.mod == cur.mod && c.size == cur.size {
|
||||
return c.ip, nil
|
||||
}
|
||||
// TODO(bradfitz): unify this /etc/resolv.conf parsing code with readResolv
|
||||
// in net/dns, which we can't use due to circular dependency reasons.
|
||||
// Move it to a leaf, including the OSConfig type (perhaps in its own dnstype
|
||||
// package?)
|
||||
err = lineread.File("/etc/resolv.conf", func(line []byte) error {
|
||||
if !ip.IsZero() {
|
||||
return nil
|
||||
@@ -537,6 +541,12 @@ func stubResolverForOS() (ip netaddr.IP, err error) {
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
return nil
|
||||
}
|
||||
// Normalize tabs to spaces to simplify parsing code later.
|
||||
for i, b := range line {
|
||||
if b == '\t' {
|
||||
line[i] = ' '
|
||||
}
|
||||
}
|
||||
if mem.HasPrefix(mem.B(line), mem.S("nameserver ")) {
|
||||
s := strings.TrimSpace(strings.TrimPrefix(string(line), "nameserver "))
|
||||
ip, err = netaddr.ParseIP(s)
|
||||
|
||||
@@ -457,9 +457,7 @@ func TLSDialer(fwd DialContextFunc, dnsCache *Resolver, tlsConfigBase *tls.Confi
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
err := tlsConn.Handshake()
|
||||
handshakeTimeoutCancel()
|
||||
errc <- err
|
||||
errc <- tlsConn.Handshake()
|
||||
}()
|
||||
if err := <-errc; err != nil {
|
||||
tcpConn.Close()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
@@ -67,8 +66,7 @@ func socketMarkWorks() bool {
|
||||
// If it doesn't, we have to use SO_BINDTODEVICE on our sockets instead.
|
||||
func useSocketMark() bool {
|
||||
socketMarkWorksOnce.Do(func() {
|
||||
ipRuleWorks := exec.Command("ip", "rule").Run() == nil
|
||||
socketMarkWorksOnce.v = ipRuleWorks && socketMarkWorks()
|
||||
socketMarkWorksOnce.v = socketMarkWorks()
|
||||
})
|
||||
return socketMarkWorksOnce.v
|
||||
}
|
||||
|
||||
@@ -22,12 +22,16 @@ const minFrag = 60 + 20 // max IPv4 header + basic TCP header
|
||||
type TCPFlag uint8
|
||||
|
||||
const (
|
||||
TCPFin TCPFlag = 0x01
|
||||
TCPSyn TCPFlag = 0x02
|
||||
TCPRst TCPFlag = 0x04
|
||||
TCPPsh TCPFlag = 0x08
|
||||
TCPAck TCPFlag = 0x10
|
||||
TCPSynAck TCPFlag = TCPSyn | TCPAck
|
||||
TCPFin TCPFlag = 0x01
|
||||
TCPSyn TCPFlag = 0x02
|
||||
TCPRst TCPFlag = 0x04
|
||||
TCPPsh TCPFlag = 0x08
|
||||
TCPAck TCPFlag = 0x10
|
||||
TCPUrg TCPFlag = 0x20
|
||||
TCPECNEcho TCPFlag = 0x40
|
||||
TCPCWR TCPFlag = 0x80
|
||||
TCPSynAck TCPFlag = TCPSyn | TCPAck
|
||||
TCPECNBits TCPFlag = TCPECNEcho | TCPCWR
|
||||
)
|
||||
|
||||
// Parsed is a minimal decoding of a packet suitable for use in filters.
|
||||
@@ -52,7 +56,7 @@ type Parsed struct {
|
||||
Src netaddr.IPPort
|
||||
// DstIP4 is the destination address. Family matches IPVersion.
|
||||
Dst netaddr.IPPort
|
||||
// TCPFlags is the packet's TCP flag bigs. Valid iff IPProto == TCP.
|
||||
// TCPFlags is the packet's TCP flag bits. Valid iff IPProto == TCP.
|
||||
TCPFlags TCPFlag
|
||||
}
|
||||
|
||||
@@ -180,7 +184,7 @@ func (q *Parsed) decode4(b []byte) {
|
||||
}
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
q.TCPFlags = TCPFlag(sub[13])
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
@@ -282,7 +286,7 @@ func (q *Parsed) decode6(b []byte) {
|
||||
}
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
q.TCPFlags = TCPFlag(sub[13])
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
@@ -374,8 +378,14 @@ func (q *Parsed) Payload() []byte {
|
||||
return q.b[q.dataofs:q.length]
|
||||
}
|
||||
|
||||
// IsTCPSyn reports whether q is a TCP SYN packet
|
||||
// (i.e. the first packet in a new connection).
|
||||
// Transport returns the transport header and payload (IP subprotocol, such as TCP or UDP).
|
||||
// This is a read-only view; that is, p retains the ownership of the buffer.
|
||||
func (p *Parsed) Transport() []byte {
|
||||
return p.b[p.subofs:]
|
||||
}
|
||||
|
||||
// IsTCPSyn reports whether q is a TCP SYN packet,
|
||||
// without ACK set. (i.e. the first packet in a new connection)
|
||||
func (q *Parsed) IsTCPSyn() bool {
|
||||
return (q.TCPFlags & TCPSynAck) == TCPSyn
|
||||
}
|
||||
@@ -424,6 +434,40 @@ func (q *Parsed) IsEchoResponse() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveECNBits modifies p and its underlying memory buffer to remove
|
||||
// ECN bits, if any. It reports whether it did so.
|
||||
//
|
||||
// It currently only does the TCP flags.
|
||||
func (p *Parsed) RemoveECNBits() bool {
|
||||
if p.IPVersion == 0 {
|
||||
return false
|
||||
}
|
||||
if p.IPProto != ipproto.TCP {
|
||||
// TODO(bradfitz): handle non-TCP too? for now only trying to
|
||||
// fix the Issue 2642 problem.
|
||||
return false
|
||||
}
|
||||
if p.TCPFlags&TCPECNBits == 0 {
|
||||
// Nothing to do.
|
||||
return false
|
||||
}
|
||||
|
||||
// Clear flags.
|
||||
|
||||
// First in the parsed output.
|
||||
p.TCPFlags = p.TCPFlags & ^TCPECNBits
|
||||
|
||||
// Then in the underlying memory.
|
||||
tcp := p.Transport()
|
||||
old := binary.BigEndian.Uint16(tcp[12:14])
|
||||
tcp[13] = byte(p.TCPFlags)
|
||||
new := binary.BigEndian.Uint16(tcp[12:14])
|
||||
oldSum := binary.BigEndian.Uint16(tcp[16:18])
|
||||
newSum := ^checksumUpdate2ByteAlignedUint16(^oldSum, old, new)
|
||||
binary.BigEndian.PutUint16(tcp[16:18], newSum)
|
||||
return true
|
||||
}
|
||||
|
||||
func Hexdump(b []byte) string {
|
||||
out := new(strings.Builder)
|
||||
for i := 0; i < len(b); i += 16 {
|
||||
@@ -455,3 +499,26 @@ func Hexdump(b []byte) string {
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// From gVisor's unexported API:
|
||||
|
||||
// checksumUpdate2ByteAlignedUint16 updates a uint16 value in a calculated
|
||||
// checksum.
|
||||
//
|
||||
// The value MUST begin at a 2-byte boundary in the original buffer.
|
||||
func checksumUpdate2ByteAlignedUint16(xsum, old, new uint16) uint16 {
|
||||
// As per RFC 1071 page 4,
|
||||
//(4) Incremental Update
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// To update the checksum, simply add the differences of the
|
||||
// sixteen bit integers that have been changed. To see why this
|
||||
// works, observe that every 16-bit integer has an additive inverse
|
||||
// and that addition is associative. From this it follows that
|
||||
// given the original value m, the new value m', and the old
|
||||
// checksum C, the new checksum C' is:
|
||||
//
|
||||
// C' = C + (-m) + m' = C + (m' - m)
|
||||
return checksumCombine(xsum, checksumCombine(new, ^old))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ package packet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -561,3 +563,57 @@ func BenchmarkString(b *testing.B) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveECNBits(t *testing.T) {
|
||||
// withECNHex is a TCP SYN packet with ECN bits set in the TCP
|
||||
// header as captured by Wireshark on macOS against the
|
||||
// Tailscale interface. In this packet (because it's a SYN
|
||||
// control packet), the ECN bits are not set in the IP header.
|
||||
const withECNHex = `45 00 00 40 00 00 40 00
|
||||
40 06 0c 66 64 7b 65 28 64 7f 00 30 f1 ab 00 16
|
||||
5a 7a 63 e8 00 00 00 00 b0 c2 ff ff 97 76 00 00
|
||||
02 04 04 d8 01 03 03 06 01 01 08 0a 03 e1 bd 49
|
||||
00 00 00 00 04 02 00 00`
|
||||
|
||||
// Generated by hand-editing a pcap file in hexl-mode to set
|
||||
// the TCP flags to just SYN (0x02), then loading that pcap
|
||||
// file in wireshark to get the expected checksum value, then
|
||||
// putting that checksum value (0x9836) in the file.
|
||||
const wantStrippedHex = `45 00 00 40 00 00 40 00
|
||||
40 06 0c 66 64 7b 65 28 64 7f 00 30 f1 ab 00 16
|
||||
5a 7a 63 e8 00 00 00 00 b0 02 ff ff 98 36 00 00
|
||||
02 04 04 d8 01 03 03 06 01 01 08 0a 03 e1 bd 49
|
||||
00 00 00 00 04 02 00 00`
|
||||
|
||||
var p Parsed
|
||||
pktBuf := bytesOfHex(withECNHex)
|
||||
p.Decode(pktBuf)
|
||||
if want := TCPCWR | TCPECNEcho | TCPSyn; p.TCPFlags != want {
|
||||
t.Fatalf("pre flags = %v; want %v", p.TCPFlags, want)
|
||||
}
|
||||
|
||||
if !p.RemoveECNBits() {
|
||||
t.Fatal("didn't remove bits")
|
||||
}
|
||||
if want := TCPSyn; p.TCPFlags != want {
|
||||
t.Fatalf("post flags = %v; want %v", p.TCPFlags, want)
|
||||
}
|
||||
wantPkt := bytesOfHex(wantStrippedHex)
|
||||
if !bytes.Equal(pktBuf, wantPkt) {
|
||||
t.Fatalf("wrong result.\n got: % 2x\nwant: % 2x\n", pktBuf, wantPkt)
|
||||
}
|
||||
|
||||
if p.RemoveECNBits() {
|
||||
t.Fatal("unexpected true return value on second call")
|
||||
}
|
||||
}
|
||||
|
||||
var nonHex = regexp.MustCompile(`[^0-9a-fA-F]+`)
|
||||
|
||||
func bytesOfHex(s string) []byte {
|
||||
b, err := hex.DecodeString(nonHex.ReplaceAllString(s, ""))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ import (
|
||||
// 23: 2021-08-25: DNSConfig.Routes values may be empty (for ExtraRecords support in 1.14.1+)
|
||||
// 24: 2021-09-18: MapResponse.Health from control to node; node shows in "tailscale status"
|
||||
// 25: 2021-11-01: MapResponse.Debug.Exit
|
||||
const CurrentMapRequestVersion = 25
|
||||
// 26: 2022-01-12: (nothing, just bumping for 1.20.0)
|
||||
const CurrentMapRequestVersion = 26
|
||||
|
||||
type StableID string
|
||||
|
||||
|
||||
@@ -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.`},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -265,6 +265,7 @@ type Conn struct {
|
||||
stunReceiveFunc atomic.Value // of func(p []byte, fromAddr *net.UDPAddr)
|
||||
|
||||
// derpRecvCh is used by receiveDERP to read DERP messages.
|
||||
// It must have buffer size > 0; see issue 3736.
|
||||
derpRecvCh chan derpReadResult
|
||||
|
||||
// bind is the wireguard-go conn.Bind for Conn.
|
||||
@@ -299,11 +300,18 @@ type Conn struct {
|
||||
havePrivateKey syncs.AtomicBool
|
||||
publicKeyAtomic atomic.Value // of key.NodePublic (or NodeKey zero value if !havePrivateKey)
|
||||
|
||||
// derpMapAtomic is the same as derpMap, but without requiring
|
||||
// sync.Mutex. For use with NewRegionClient's callback, to avoid
|
||||
// lock ordering deadlocks. See issue 3726 and mu field docs.
|
||||
derpMapAtomic atomic.Value // of *tailcfg.DERPMap
|
||||
|
||||
// port is the preferred port from opts.Port; 0 means auto.
|
||||
port syncs.AtomicUint32
|
||||
|
||||
// ============================================================
|
||||
// mu guards all following fields; see userspaceEngine lock ordering rules
|
||||
// mu guards all following fields; see userspaceEngine lock
|
||||
// ordering rules against the engine. For derphttp, mu must
|
||||
// be held before derphttp.Client.mu.
|
||||
mu sync.Mutex
|
||||
muCond *sync.Cond
|
||||
|
||||
@@ -522,7 +530,7 @@ func (o *Options) derpActiveFunc() func() {
|
||||
// of NewConn. Mostly for tests.
|
||||
func newConn() *Conn {
|
||||
c := &Conn{
|
||||
derpRecvCh: make(chan derpReadResult),
|
||||
derpRecvCh: make(chan derpReadResult, 1), // must be buffered, see issue 3736
|
||||
derpStarted: make(chan struct{}),
|
||||
peerLastDerp: make(map[key.NodePublic]int),
|
||||
peerMap: newPeerMap(),
|
||||
@@ -1351,19 +1359,23 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.NodePublic) cha
|
||||
}
|
||||
|
||||
// Note that derphttp.NewRegionClient does not dial the server
|
||||
// so it is safe to do under the mu lock.
|
||||
// (it doesn't block) so it is safe to do under the c.mu lock.
|
||||
dc := derphttp.NewRegionClient(c.privateKey, c.logf, func() *tailcfg.DERPRegion {
|
||||
// Warning: it is not legal to acquire
|
||||
// magicsock.Conn.mu from this callback.
|
||||
// It's run from derphttp.Client.connect (via Send, etc)
|
||||
// and the lock ordering rules are that magicsock.Conn.mu
|
||||
// must be acquired before derphttp.Client.mu.
|
||||
// See https://github.com/tailscale/tailscale/issues/3726
|
||||
if c.connCtx.Err() != nil {
|
||||
// If we're closing, don't try to acquire the lock.
|
||||
// We might already be in Conn.Close and the Lock would deadlock.
|
||||
// We're closing anyway; return nil to stop dialing.
|
||||
return nil
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.derpMap == nil {
|
||||
derpMap, _ := c.derpMapAtomic.Load().(*tailcfg.DERPMap)
|
||||
if derpMap == nil {
|
||||
return nil
|
||||
}
|
||||
return c.derpMap.Regions[regionID]
|
||||
return derpMap.Regions[regionID]
|
||||
})
|
||||
|
||||
dc.SetCanAckPings(true)
|
||||
@@ -2252,6 +2264,7 @@ func (c *Conn) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
return
|
||||
}
|
||||
|
||||
c.derpMapAtomic.Store(dm)
|
||||
old := c.derpMap
|
||||
c.derpMap = dm
|
||||
if dm == nil {
|
||||
@@ -2629,6 +2642,7 @@ func (c *connBind) Close() error {
|
||||
}
|
||||
// Send an empty read result to unblock receiveDERP,
|
||||
// which will then check connBind.Closed.
|
||||
// connBind.Closed takes c.mu, but c.derpRecvCh is buffered.
|
||||
c.derpRecvCh <- derpReadResult{}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,13 +34,16 @@ import (
|
||||
"inet.af/netstack/tcpip/transport/tcp"
|
||||
"inet.af/netstack/tcpip/transport/udp"
|
||||
"inet.af/netstack/waiter"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
@@ -71,13 +74,19 @@ type Impl struct {
|
||||
// It can only be set before calling Start.
|
||||
ProcessSubnets bool
|
||||
|
||||
ipstack *stack.Stack
|
||||
linkEP *channel.Endpoint
|
||||
tundev *tstun.Wrapper
|
||||
e wgengine.Engine
|
||||
mc *magicsock.Conn
|
||||
logf logger.Logf
|
||||
dialer *tsdial.Dialer
|
||||
ipstack *stack.Stack
|
||||
linkEP *channel.Endpoint
|
||||
tundev *tstun.Wrapper
|
||||
e wgengine.Engine
|
||||
mc *magicsock.Conn
|
||||
logf logger.Logf
|
||||
dialer *tsdial.Dialer
|
||||
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
|
||||
@@ -151,10 +160,22 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
|
||||
dialer: dialer,
|
||||
connsOpenBySubnetIP: make(map[netaddr.IP]int),
|
||||
}
|
||||
ns.ctx, ns.ctxCancel = context.WithCancel(context.Background())
|
||||
ns.atomicIsLocalIPFunc.Store(tsaddr.NewContainsIPFunc(nil))
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func (ns *Impl) Close() error {
|
||||
ns.ctxCancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLocalBackend sets the LocalBackend; it should only be run before
|
||||
// the Start method is called.
|
||||
func (ns *Impl) SetLocalBackend(lb *ipnlocal.LocalBackend) {
|
||||
ns.lb = lb
|
||||
}
|
||||
|
||||
// wrapProtoHandler returns protocol handler h wrapped in a version
|
||||
// that dynamically reconfigures ns's subnet addresses as needed for
|
||||
// outbound traffic.
|
||||
@@ -242,8 +263,9 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
|
||||
ap := protocolAddr.AddressWithPrefix
|
||||
ip := netaddrIPFromNetstackIP(ap.Address)
|
||||
if ip == v4broadcast && ap.PrefixLen == 32 {
|
||||
// Don't delete this one later. It seems to be important.
|
||||
// Related to Issue 2642? Likely.
|
||||
// Don't add 255.255.255.255/32 to oldIPs so we don't
|
||||
// delete it later. We didn't install it, so it's not
|
||||
// ours to delete.
|
||||
continue
|
||||
}
|
||||
oldIPs[ap] = true
|
||||
@@ -254,10 +276,10 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
|
||||
if nm.SelfNode != nil {
|
||||
for _, ipp := range nm.SelfNode.Addresses {
|
||||
isAddr[ipp] = true
|
||||
newIPs[ipPrefixToAddressWithPrefix(ipp)] = true
|
||||
}
|
||||
for _, ipp := range nm.SelfNode.AllowedIPs {
|
||||
local := isAddr[ipp]
|
||||
if local && ns.ProcessLocalIPs || !local && ns.ProcessSubnets {
|
||||
if !isAddr[ipp] && ns.ProcessSubnets {
|
||||
newIPs[ipPrefixToAddressWithPrefix(ipp)] = true
|
||||
}
|
||||
}
|
||||
@@ -346,8 +368,12 @@ func (ns *Impl) DialContextUDP(ctx context.Context, ipp netaddr.IPPort) (*gonet.
|
||||
|
||||
func (ns *Impl) injectOutbound() {
|
||||
for {
|
||||
packetInfo, ok := ns.linkEP.ReadContext(context.Background())
|
||||
packetInfo, ok := ns.linkEP.ReadContext(ns.ctx)
|
||||
if !ok {
|
||||
if ns.ctx.Err() != nil {
|
||||
// Return without logging.
|
||||
return
|
||||
}
|
||||
ns.logf("[v2] ReadContext-for-write = ok=false")
|
||||
continue
|
||||
}
|
||||
@@ -376,9 +402,33 @@ func (ns *Impl) isLocalIP(ip netaddr.IP) bool {
|
||||
return ns.atomicIsLocalIPFunc.Load().(func(netaddr.IP) bool)(ip)
|
||||
}
|
||||
|
||||
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.ProcessLocalIPs && !ns.ProcessSubnets {
|
||||
// Fast path for common case (e.g. Linux server in TUN mode) where
|
||||
// netstack isn't used at all; don't even do an isLocalIP lookup.
|
||||
@@ -394,8 +444,14 @@ func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// setAmbientCapsRaw is non-nil on Linux for Synology, to run ping with
|
||||
// CAP_NET_RAW from tailscaled's binary.
|
||||
var setAmbientCapsRaw func(*exec.Cmd)
|
||||
|
||||
var userPingSem = syncs.NewSemaphore(20) // 20 child ping processes at once
|
||||
|
||||
var isSynology = runtime.GOOS == "linux" && distro.Get() == distro.Synology
|
||||
|
||||
// userPing tried to ping dstIP and if it succeeds, injects pingResPkt
|
||||
// into the tundev.
|
||||
//
|
||||
@@ -419,6 +475,11 @@ func (ns *Impl) userPing(dstIP netaddr.IP, pingResPkt []byte) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
err = exec.Command("ping", "-n", "1", "-w", "3000", dstIP.String()).Run()
|
||||
case "darwin":
|
||||
// Note: 2000 ms is actually 1 second + 2,000
|
||||
// milliseconds extra for 3 seconds total.
|
||||
// See https://github.com/tailscale/tailscale/pull/3753 for details.
|
||||
err = exec.Command("ping", "-c", "1", "-W", "2000", dstIP.String()).Run()
|
||||
case "android":
|
||||
ping := "/system/bin/ping"
|
||||
if dstIP.Is6() {
|
||||
@@ -426,11 +487,29 @@ func (ns *Impl) userPing(dstIP netaddr.IP, pingResPkt []byte) {
|
||||
}
|
||||
err = exec.Command(ping, "-c", "1", "-w", "3", dstIP.String()).Run()
|
||||
default:
|
||||
err = exec.Command("ping", "-c", "1", "-W", "3", dstIP.String()).Run()
|
||||
ping := "ping"
|
||||
if isSynology {
|
||||
ping = "/bin/ping"
|
||||
}
|
||||
cmd := exec.Command(ping, "-c", "1", "-W", "3", dstIP.String())
|
||||
if isSynology && os.Getuid() != 0 {
|
||||
// On DSM7 we run as non-root and need to pass
|
||||
// CAP_NET_RAW if our binary has it.
|
||||
setAmbientCapsRaw(cmd)
|
||||
}
|
||||
err = cmd.Run()
|
||||
}
|
||||
d := time.Since(t0)
|
||||
if err != nil {
|
||||
ns.logf("exec ping of %v failed in %v", dstIP, d)
|
||||
if d < time.Second/2 {
|
||||
// If it failed quicker than the 3 second
|
||||
// timeout we gave above (500 ms is a
|
||||
// reasonable threshold), then assume the ping
|
||||
// failed for problems finding/running
|
||||
// ping. We don't want to log if the host is
|
||||
// just down.
|
||||
ns.logf("exec ping of %v failed in %v: %v", dstIP, d, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if debugNetstack {
|
||||
@@ -470,6 +549,7 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
|
||||
case 6:
|
||||
pn = header.IPv6ProtocolNumber
|
||||
}
|
||||
p.RemoveECNBits() // Issue 2642
|
||||
if debugPackets {
|
||||
ns.logf("[v2] packet in (from %v): % x", p.Src, p.Buffer())
|
||||
}
|
||||
@@ -478,6 +558,7 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
|
||||
Data: vv,
|
||||
})
|
||||
ns.linkEP.InjectInbound(pn, packetBuf)
|
||||
packetBuf.DecRef()
|
||||
|
||||
// We've now delivered this to netstack, so we're done.
|
||||
// Instead of returning a filter.Accept here (which would also
|
||||
@@ -508,7 +589,7 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
clientRemoteIP := netaddrIPFromNetstackIP(reqDetails.RemoteAddress)
|
||||
if !clientRemoteIP.IsValid() {
|
||||
ns.logf("invalid RemoteAddress in TCP ForwarderRequest: %s", stringifyTEI(reqDetails))
|
||||
r.Complete(true)
|
||||
r.Complete(true) // sends a RST
|
||||
return
|
||||
}
|
||||
|
||||
@@ -524,7 +605,8 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
var wq waiter.Queue
|
||||
ep, err := r.CreateEndpoint(&wq)
|
||||
if err != nil {
|
||||
r.Complete(true)
|
||||
ns.logf("CreateEndpoint error for %s: %v", stringifyTEI(reqDetails), err)
|
||||
r.Complete(true) // sends a RST
|
||||
return
|
||||
}
|
||||
r.Complete(false)
|
||||
@@ -539,6 +621,16 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
// block until the TCP handshake is complete.
|
||||
c := gonet.NewTCPConn(&wq, ep)
|
||||
|
||||
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
|
||||
|
||||
20
wgengine/netstack/netstack_linux.go
Normal file
20
wgengine/netstack/netstack_linux.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 netstack
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func init() {
|
||||
setAmbientCapsRaw = func(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
AmbientCaps: []uintptr{unix.CAP_NET_RAW},
|
||||
}
|
||||
}
|
||||
}
|
||||
76
wgengine/netstack/netstack_test.go
Normal file
76
wgengine/netstack/netstack_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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 netstack
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// TestInjectInboundLeak tests that injectInbound doesn't leak memory.
|
||||
// See https://github.com/tailscale/tailscale/issues/3762
|
||||
func TestInjectInboundLeak(t *testing.T) {
|
||||
tunDev := tstun.NewFake()
|
||||
dialer := new(tsdial.Dialer)
|
||||
logf := func(format string, args ...interface{}) {
|
||||
if !t.Failed() {
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: tunDev,
|
||||
Dialer: dialer,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer eng.Close()
|
||||
ig, ok := eng.(wgengine.InternalsGetter)
|
||||
if !ok {
|
||||
t.Fatal("not an InternalsGetter")
|
||||
}
|
||||
tunWrap, magicSock, ok := ig.GetInternals()
|
||||
if !ok {
|
||||
t.Fatal("failed to get internals")
|
||||
}
|
||||
|
||||
ns, err := Create(logf, tunWrap, eng, magicSock, dialer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ns.Close()
|
||||
ns.ProcessLocalIPs = true
|
||||
if err := ns.Start(); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
ns.atomicIsLocalIPFunc.Store(func(netaddr.IP) bool { return true })
|
||||
|
||||
pkt := &packet.Parsed{}
|
||||
const N = 10_000
|
||||
ms0 := getMemStats()
|
||||
for i := 0; i < N; i++ {
|
||||
outcome := ns.injectInbound(pkt, tunWrap)
|
||||
if outcome != filter.DropSilently {
|
||||
t.Fatalf("got outcome %v; want DropSilently", outcome)
|
||||
}
|
||||
}
|
||||
ms1 := getMemStats()
|
||||
if grew := int64(ms1.HeapObjects) - int64(ms0.HeapObjects); grew >= N {
|
||||
t.Fatalf("grew by %v (which is too much and >= the %v packets we sent)", grew, N)
|
||||
}
|
||||
}
|
||||
|
||||
func getMemStats() (ms runtime.MemStats) {
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&ms)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user