Compare commits
25 Commits
bradfitz/a
...
v1.18.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b04815c9cd | ||
|
|
e67182d446 | ||
|
|
d4fd80c5d7 | ||
|
|
71be839f03 | ||
|
|
32210788a2 | ||
|
|
3d9937513b | ||
|
|
972bcccc36 | ||
|
|
6c44133d8f | ||
|
|
2d6404609d | ||
|
|
7d6407a632 | ||
|
|
7dd677043d | ||
|
|
d79aad2b89 | ||
|
|
e055d10f5b | ||
|
|
e399b0bec8 | ||
|
|
21b1a44cd6 | ||
|
|
47975d373f | ||
|
|
caf0da3628 | ||
|
|
c005f67314 | ||
|
|
aee2387d6e | ||
|
|
6f99ad1630 | ||
|
|
c654662250 | ||
|
|
bcb979f8bf | ||
|
|
6db09061bc | ||
|
|
1b9fbdc057 | ||
|
|
71f1dd5aa2 |
@@ -56,6 +56,5 @@ RUN GOARCH=$TARGETARCH go install -tags=xversion -ldflags="\
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/tailscale ./cmd/tailscaled
|
||||
|
||||
FROM alpine:3.14
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
FROM ghcr.io/tailscale/alpine-base:3.14
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
|
||||
6
Dockerfile.base
Normal file
6
Dockerfile.base
Normal file
@@ -0,0 +1,6 @@
|
||||
# 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.
|
||||
|
||||
FROM alpine:3.14
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
3
Makefile
3
Makefile
@@ -23,9 +23,8 @@ build386:
|
||||
buildlinuxarm:
|
||||
GOOS=linux GOARCH=arm go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
|
||||
buildmultiarchimage:
|
||||
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t ${IMAGE_REPO}:latest --push -f Dockerfile .
|
||||
./build_docker.sh
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.17.0
|
||||
1.18.2
|
||||
|
||||
@@ -30,12 +30,14 @@ else
|
||||
fi
|
||||
|
||||
long_suffix="$change_suffix-t$short_hash"
|
||||
SHORT="$major.$minor.$patch"
|
||||
MINOR="$major.$minor"
|
||||
SHORT="$MINOR.$patch"
|
||||
LONG="${SHORT}$long_suffix"
|
||||
GIT_HASH="$git_hash"
|
||||
|
||||
if [ "$1" = "shellvars" ]; then
|
||||
cat <<EOF
|
||||
VERSION_MINOR="$MINOR"
|
||||
VERSION_SHORT="$SHORT"
|
||||
VERSION_LONG="$LONG"
|
||||
VERSION_GIT_HASH="$GIT_HASH"
|
||||
|
||||
@@ -21,8 +21,15 @@ set -eu
|
||||
|
||||
eval $(./build_dist.sh shellvars)
|
||||
|
||||
docker build \
|
||||
--build-arg VERSION_LONG=$VERSION_LONG \
|
||||
--build-arg VERSION_SHORT=$VERSION_SHORT \
|
||||
--build-arg VERSION_GIT_HASH=$VERSION_GIT_HASH \
|
||||
-t tailscale:$VERSION_SHORT -t tailscale:latest .
|
||||
go run github.com/tailscale/mkctr@latest \
|
||||
--base="ghcr.io/tailscale/alpine-base:3.14" \
|
||||
--gopaths="\
|
||||
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
|
||||
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \
|
||||
--ldflags="\
|
||||
-X tailscale.com/version.Long=${VERSION_LONG} \
|
||||
-X tailscale.com/version.Short=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
|
||||
--tags="v${VERSION_SHORT},v${VERSION_MINOR}" \
|
||||
--repos="tailscale/tailscale,ghcr.io/tailscale/tailscale" \
|
||||
--push
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// geese is a collection of gooses. It need not be complete.
|
||||
@@ -57,6 +58,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
curExitNodeIP netaddr.IP
|
||||
curUser string // os.Getenv("USER") on the client side
|
||||
goos string // empty means "linux"
|
||||
distro distro.Distro
|
||||
|
||||
want string
|
||||
}{
|
||||
@@ -427,6 +429,38 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
},
|
||||
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
|
||||
},
|
||||
{
|
||||
// Issue 3176: on Synology, don't require --accept-routes=false because user
|
||||
// migth've had old an install, and we don't support --accept-routes anyway.
|
||||
name: "synology_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
goos: "linux",
|
||||
distro: distro.Synology,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
// Same test case as "synology_permit_omit_accept_routes" above, but
|
||||
// on non-Synology distro.
|
||||
name: "not_synology_dont_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
goos: "linux",
|
||||
distro: "", // not Synology
|
||||
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -447,6 +481,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
goos: goos,
|
||||
flagSet: flagSet,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
distro: tt.distro,
|
||||
}); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
|
||||
@@ -46,8 +46,10 @@ down").
|
||||
|
||||
If flags are specified, the flags must be the complete set of desired
|
||||
settings. An error is returned if any setting would be changed as a
|
||||
result of an unspecified flag's default value, unless the --reset
|
||||
flag is also used.
|
||||
result of an unspecified flag's default value, unless the --reset flag
|
||||
is also used. (The flags --authkey, --force-reauth, and --qr are not
|
||||
considered settings that need to be re-specified when modifying
|
||||
settings.)
|
||||
`),
|
||||
FlagSet: upFlagSet,
|
||||
Exec: runUp,
|
||||
@@ -380,6 +382,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
env := upCheckEnv{
|
||||
goos: effectiveGOOS(),
|
||||
distro: distro.Get(),
|
||||
user: os.Getenv("USER"),
|
||||
flagSet: upFlagSet,
|
||||
upArgs: upArgs,
|
||||
@@ -622,6 +625,7 @@ type upCheckEnv struct {
|
||||
upArgs upArgsT
|
||||
backendState string
|
||||
curExitNodeIP netaddr.IP
|
||||
distro distro.Distro
|
||||
}
|
||||
|
||||
// checkForAccidentalSettingReverts (the "up checker") checks for
|
||||
@@ -672,6 +676,10 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
|
||||
if flagName == "login-server" && ipn.IsLoginServerSynonym(valCur) && ipn.IsLoginServerSynonym(valNew) {
|
||||
continue
|
||||
}
|
||||
if flagName == "accept-routes" && valNew == false && env.goos == "linux" && env.distro == distro.Synology {
|
||||
// Issue 3176. Old prefs had 'RouteAll: true' on disk, so ignore that.
|
||||
continue
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
|
||||
@@ -217,7 +217,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/pad32 from tailscale.com/derp+
|
||||
tailscale.com/types/pad32 from tailscale.com/derp
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
|
||||
@@ -440,14 +440,14 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
|
||||
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, false, fmt.Errorf("createBIRDClient: %w", err)
|
||||
}
|
||||
}
|
||||
if !useNetstack {
|
||||
dev, devName, err := tstun.New(logf, name)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name)
|
||||
return nil, false, err
|
||||
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
|
||||
}
|
||||
conf.Tun = dev
|
||||
if strings.HasPrefix(name, "tap:") {
|
||||
@@ -459,11 +459,11 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
r, err := router.New(logf, dev, linkMon)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, false, err
|
||||
return nil, false, fmt.Errorf("creating router: %w", err)
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
}
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
|
||||
@@ -78,7 +78,10 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
// Make a logger without a date prefix, as filelogger
|
||||
// and logtail both already add their own. All we really want
|
||||
// from the log package is the automatic newline.
|
||||
logger := log.New(os.Stderr, "", 0)
|
||||
// We start with log.Default().Writer(), which is the logtail
|
||||
// writer that logpolicy already installed as the global
|
||||
// output.
|
||||
logger := log.New(log.Default().Writer(), "", 0)
|
||||
ipnserver.BabysitProc(ctx, args, logger.Printf)
|
||||
}()
|
||||
|
||||
@@ -114,6 +117,9 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
logid := os.Args[2]
|
||||
|
||||
// Remove the date/time prefix; the logtail + file logggers add it.
|
||||
log.SetFlags(0)
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
|
||||
|
||||
@@ -339,11 +339,9 @@ func (c *Auto) authRoutine() {
|
||||
continue
|
||||
}
|
||||
if url != "" {
|
||||
if goal.url != "" {
|
||||
err = fmt.Errorf("[unexpected] server required a new URL?")
|
||||
report(err, "WaitLoginURL")
|
||||
}
|
||||
|
||||
// goal.url ought to be empty here.
|
||||
// However, not all control servers get this right,
|
||||
// and logging about it here just generates noise.
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: true,
|
||||
|
||||
@@ -139,7 +139,11 @@ type LocalBackend struct {
|
||||
// same as the Network Extension lifetime and we can thus avoid
|
||||
// double-copying files by writing them to the right location
|
||||
// immediately.
|
||||
directFileRoot string
|
||||
// It's also used on Synology, but in that case DoFinalRename is
|
||||
// also set true, which moves the *.partial file to its final
|
||||
// name on completion.
|
||||
directFileRoot string
|
||||
directFileDoFinalRename bool // false on macOS, true on Synology
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
@@ -219,6 +223,17 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
|
||||
b.directFileRoot = dir
|
||||
}
|
||||
|
||||
// SetDirectFileDoFinalRename sets whether the peerapi file server should rename
|
||||
// a received "name.partial" file to "name" when the download is complete.
|
||||
//
|
||||
// This only applies when SetDirectFileRoot is non-empty.
|
||||
// The default is false.
|
||||
func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.directFileDoFinalRename = v
|
||||
}
|
||||
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) maybePauseControlClientLocked() {
|
||||
if b.cc == nil {
|
||||
@@ -588,6 +603,11 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// findExitNodeIDLocked updates b.prefs to reference an exit node by ID,
|
||||
// rather than by IP. It returns whether prefs was mutated.
|
||||
func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
|
||||
if nm == nil {
|
||||
// No netmap, can't resolve anything.
|
||||
return false
|
||||
}
|
||||
|
||||
// If we have a desired IP on file, try to find the corresponding
|
||||
// node.
|
||||
if b.prefs.ExitNodeIP.IsZero() {
|
||||
@@ -1651,7 +1671,7 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||
}
|
||||
|
||||
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
|
||||
// unlocks b.mu when done.
|
||||
// unlocks b.mu when done. newp ownership passes to this function.
|
||||
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
netMap := b.netMap
|
||||
stateKey := b.stateKey
|
||||
@@ -1659,6 +1679,10 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
oldp := b.prefs
|
||||
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
||||
b.prefs = newp
|
||||
// findExitNodeIDLocked returns whether it updated b.prefs, but
|
||||
// everything in this function treats b.prefs as completely new
|
||||
// anyway. No-op if no exit node resolution is needed.
|
||||
b.findExitNodeIDLocked(netMap)
|
||||
b.inServerMode = newp.ForceDaemon
|
||||
// We do this to avoid holding the lock while doing everything else.
|
||||
newp = b.prefs.Clone()
|
||||
@@ -2136,11 +2160,12 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
}
|
||||
|
||||
ps := &peerAPIServer{
|
||||
b: b,
|
||||
rootDir: fileRoot,
|
||||
tunName: tunName,
|
||||
selfNode: selfNode,
|
||||
directFileMode: b.directFileRoot != "",
|
||||
b: b,
|
||||
rootDir: fileRoot,
|
||||
selfNode: selfNode,
|
||||
tunName: tunName,
|
||||
directFileMode: b.directFileRoot != "",
|
||||
directFileDoFinalRename: b.directFileDoFinalRename,
|
||||
}
|
||||
b.peerAPIServer = ps
|
||||
|
||||
|
||||
@@ -52,10 +52,17 @@ type peerAPIServer struct {
|
||||
// directFileMode is whether we're writing files directly to a
|
||||
// download directory (as *.partial files), rather than making
|
||||
// the frontend retrieve it over localapi HTTP and write it
|
||||
// somewhere itself. This is used on GUI macOS version.
|
||||
// somewhere itself. This is used on the GUI macOS versions
|
||||
// and on Synology.
|
||||
// In directFileMode, the peerapi doesn't do the final rename
|
||||
// from "foo.jpg.partial" to "foo.jpg".
|
||||
// from "foo.jpg.partial" to "foo.jpg" unless
|
||||
// directFileDoFinalRename is set.
|
||||
directFileMode bool
|
||||
|
||||
// directFileDoFinalRename is whether in directFileMode we
|
||||
// additionally move the *.direct file to its final name after
|
||||
// it's received.
|
||||
directFileDoFinalRename bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -671,7 +678,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if h.ps.directFileMode {
|
||||
if h.ps.directFileMode && !h.ps.directFileDoFinalRename {
|
||||
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
||||
inFile.markAndNotifyDone()
|
||||
}
|
||||
|
||||
@@ -757,6 +757,18 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
|
||||
b.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return smallzstd.NewDecoder(nil)
|
||||
})
|
||||
if distro.Get() == distro.Synology {
|
||||
// See if they have a "Taildrop" share.
|
||||
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
|
||||
path, err := findSynologyTaildropDir()
|
||||
if err != nil {
|
||||
logf("Synology Taildrop support: %v", err)
|
||||
} else {
|
||||
logf("Synology Taildrop: using %v", path)
|
||||
b.SetDirectFileRoot(path)
|
||||
b.SetDirectFileDoFinalRename(true)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.AutostartStateKey == "" {
|
||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
||||
@@ -1112,3 +1124,17 @@ func (ln *listenerWithReadyConn) Accept() (net.Conn, error) {
|
||||
}
|
||||
return ln.Listener.Accept()
|
||||
}
|
||||
|
||||
// findSynologyTaildropDir looks for the first volume containing a
|
||||
// "Taildrop" directory. We'd run "synoshare --get Taildrop" command
|
||||
// but on DSM7 at least, we lack permissions to run that.
|
||||
func findSynologyTaildropDir() (dir string, err error) {
|
||||
const name = "Taildrop"
|
||||
for i := 1; i <= 16; i++ {
|
||||
dir = fmt.Sprintf("/volume%v/%s", i, name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package dns
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -50,10 +52,17 @@ func readResolv(r io.Reader) (config OSConfig, err error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
i := strings.IndexByte(line, '#')
|
||||
if i >= 0 {
|
||||
line = line[:i]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "nameserver") {
|
||||
nameserver := strings.TrimPrefix(line, "nameserver")
|
||||
nameserver = strings.TrimSpace(nameserver)
|
||||
s := strings.TrimPrefix(line, "nameserver")
|
||||
nameserver := strings.TrimSpace(s)
|
||||
if len(nameserver) == len(s) {
|
||||
return OSConfig{}, fmt.Errorf("missing space after \"nameserver\" in %q", line)
|
||||
}
|
||||
ip, err := netaddr.ParseIP(nameserver)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
@@ -63,8 +72,12 @@ func readResolv(r io.Reader) (config OSConfig, err error) {
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "search") {
|
||||
domain := strings.TrimPrefix(line, "search")
|
||||
domain = strings.TrimSpace(domain)
|
||||
s := strings.TrimPrefix(line, "search")
|
||||
domain := strings.TrimSpace(s)
|
||||
if len(domain) == len(s) {
|
||||
// No leading space?!
|
||||
return OSConfig{}, fmt.Errorf("missing space after \"domain\" in %q", line)
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(domain)
|
||||
if err != nil {
|
||||
return OSConfig{}, fmt.Errorf("parsing search domains %q: %w", line, err)
|
||||
@@ -121,12 +134,20 @@ func isResolvedRunning() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
err = exec.Command("systemctl", "is-active", "systemd-resolved.service").Run()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
err = exec.CommandContext(ctx, "systemctl", "is-active", "systemd-resolved.service").Run()
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func restartResolved() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return exec.CommandContext(ctx, "systemctl", "restart", "systemd-resolved.service").Run()
|
||||
}
|
||||
|
||||
// directManager is an OSConfigurator which replaces /etc/resolv.conf with a file
|
||||
// generated from the given configuration, creating a backup of its old state.
|
||||
//
|
||||
@@ -383,7 +404,12 @@ func (m *directManager) Close() error {
|
||||
}
|
||||
|
||||
if isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
|
||||
m.logf("restarting systemd-resolved...")
|
||||
if err := restartResolved(); err != nil {
|
||||
m.logf("restart of systemd-resolved failed: %v", err)
|
||||
} else {
|
||||
m.logf("restarted systemd-resolved")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
@@ -138,3 +139,61 @@ func TestDirectBrokenRemove(t *testing.T) {
|
||||
}
|
||||
testDirect(t, brokenRemoveFS{directFS{prefix: tmp}})
|
||||
}
|
||||
|
||||
func TestReadResolve(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
tests := []struct {
|
||||
in string
|
||||
want OSConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{in: `nameserver 192.168.0.100`,
|
||||
want: OSConfig{
|
||||
Nameservers: []netaddr.IP{
|
||||
netaddr.MustParseIP("192.168.0.100"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{in: `nameserver 192.168.0.100 # comment`,
|
||||
want: OSConfig{
|
||||
Nameservers: []netaddr.IP{
|
||||
netaddr.MustParseIP("192.168.0.100"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{in: `nameserver 192.168.0.100#`,
|
||||
want: OSConfig{
|
||||
Nameservers: []netaddr.IP{
|
||||
netaddr.MustParseIP("192.168.0.100"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{in: `nameserver #192.168.0.100`, wantErr: true},
|
||||
{in: `nameserver`, wantErr: true},
|
||||
{in: `# nameserver 192.168.0.100`, want: OSConfig{}},
|
||||
{in: `nameserver192.168.0.100`, wantErr: true},
|
||||
|
||||
{in: `search tailsacle.com`,
|
||||
want: OSConfig{
|
||||
SearchDomains: []dnsname.FQDN{"tailsacle.com."},
|
||||
},
|
||||
},
|
||||
{in: `search tailsacle.com # typo`,
|
||||
want: OSConfig{
|
||||
SearchDomains: []dnsname.FQDN{"tailsacle.com."},
|
||||
},
|
||||
},
|
||||
{in: `searchtailsacle.com`, wantErr: true},
|
||||
{in: `search`, wantErr: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
cfg, err := readResolv(strings.NewReader(test.in))
|
||||
if test.wantErr {
|
||||
c.Assert(err, qt.IsNotNil)
|
||||
} else {
|
||||
c.Assert(err, qt.IsNil)
|
||||
}
|
||||
c.Assert(cfg, qt.DeepEquals, test.want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -193,7 +194,8 @@ func (fs wslFS) Truncate(name string) error { return fs.WriteFile(name, nil, 064
|
||||
|
||||
func (fs wslFS) ReadFile(name string) ([]byte, error) {
|
||||
b, err := wslCombinedOutput(fs.cmd("cat", "--", name))
|
||||
if ee, _ := err.(*exec.ExitError); ee != nil && ee.ExitCode() == 1 {
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) && ee.ExitCode() == 1 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return b, err
|
||||
|
||||
@@ -763,6 +763,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
meta, err := parseUPnPDiscoResponse(buf[:n])
|
||||
if err != nil {
|
||||
c.logf("unrecognized UPnP discovery response; ignoring")
|
||||
continue
|
||||
}
|
||||
c.logf("[v1] UPnP reply %+v, %q", meta, buf[:n])
|
||||
res.UPnP = true
|
||||
|
||||
@@ -190,7 +190,7 @@ type autoProxyOptions struct {
|
||||
AutoConfigUrl *uint16
|
||||
_ uintptr
|
||||
_ uint32
|
||||
FAutoLogonIfChallenged bool
|
||||
FAutoLogonIfChallenged int32 // BOOL
|
||||
}
|
||||
|
||||
// WINHTTP_PROXY_INFO
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/pad32"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
@@ -68,14 +67,17 @@ type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response
|
||||
|
||||
// Wrapper augments a tun.Device with packet filtering and injection.
|
||||
type Wrapper struct {
|
||||
logf logger.Logf
|
||||
logf logger.Logf
|
||||
limitedLogf logger.Logf // aggressively rate-limited logf used for potentially high volume errors
|
||||
// tdev is the underlying Wrapper device.
|
||||
tdev tun.Device
|
||||
isTAP bool // whether tdev is a TAP device
|
||||
|
||||
closeOnce sync.Once
|
||||
|
||||
_ pad32.Four
|
||||
// lastActivityAtomic is read/written atomically.
|
||||
// On 32 bit systems, if the fields above change,
|
||||
// you might need to add a pad32.Four field here.
|
||||
lastActivityAtomic mono.Time // time of last send or receive
|
||||
|
||||
destIPActivity atomic.Value // of map[netaddr.IP]func()
|
||||
@@ -168,10 +170,12 @@ func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper {
|
||||
}
|
||||
|
||||
func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper {
|
||||
logf = logger.WithPrefix(logf, "tstun: ")
|
||||
tun := &Wrapper{
|
||||
logf: logger.WithPrefix(logf, "tstun: "),
|
||||
isTAP: isTAP,
|
||||
tdev: tdev,
|
||||
logf: logf,
|
||||
limitedLogf: logger.RateLimitedFn(logf, 1*time.Minute, 2, 10),
|
||||
isTAP: isTAP,
|
||||
tdev: tdev,
|
||||
// bufferConsumed is conceptually a condition variable:
|
||||
// a goroutine should not block when setting it, even with no listeners.
|
||||
bufferConsumed: make(chan struct{}, 1),
|
||||
@@ -421,7 +425,7 @@ func (t *Wrapper) filterOut(p *packet.Parsed) filter.Response {
|
||||
// macOS in Network Extension mode might be.
|
||||
if p.IPProto == ipproto.UDP && // disco is over UDP; avoid isSelfDisco call for TCP/etc
|
||||
t.isSelfDisco(p) {
|
||||
t.logf("[unexpected] received self disco out packet over tstun; dropping")
|
||||
t.limitedLogf("[unexpected] received self disco out packet over tstun; dropping")
|
||||
metricPacketOutDropSelfDisco.Add(1)
|
||||
return filter.DropSilently
|
||||
}
|
||||
@@ -535,7 +539,7 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
|
||||
// macOS in Network Extension mode might be.
|
||||
if p.IPProto == ipproto.UDP && // disco is over UDP; avoid isSelfDisco call for TCP/etc
|
||||
t.isSelfDisco(p) {
|
||||
t.logf("[unexpected] received self disco in packet over tstun; dropping")
|
||||
t.limitedLogf("[unexpected] received self disco in packet over tstun; dropping")
|
||||
metricPacketInDropSelfDisco.Add(1)
|
||||
return filter.DropSilently
|
||||
}
|
||||
|
||||
@@ -495,7 +495,7 @@ func TestPeerAPIBypass(t *testing.T) {
|
||||
func TestFilterDiscoLoop(t *testing.T) {
|
||||
var memLog tstest.MemLogger
|
||||
discoPub := key.DiscoPublicFromRaw32(mem.B([]byte{1: 1, 2: 2, 31: 0}))
|
||||
tw := &Wrapper{logf: memLog.Logf}
|
||||
tw := &Wrapper{logf: memLog.Logf, limitedLogf: memLog.Logf}
|
||||
tw.SetDiscoKey(discoPub)
|
||||
uh := packet.UDP4Header{
|
||||
IP4Header: packet.IP4Header{
|
||||
|
||||
@@ -382,6 +382,9 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
||||
case ipproto.TSMP:
|
||||
return Accept, "tsmp ok"
|
||||
default:
|
||||
if f.matches4.matchProtoAndIPsOnlyIfAllPorts(q) {
|
||||
return Accept, "otherproto ok"
|
||||
}
|
||||
return Drop, "Unknown proto"
|
||||
}
|
||||
return Drop, "no rules matched"
|
||||
@@ -439,6 +442,9 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
|
||||
case ipproto.TSMP:
|
||||
return Accept, "tsmp ok"
|
||||
default:
|
||||
if f.matches6.matchProtoAndIPsOnlyIfAllPorts(q) {
|
||||
return Accept, "otherproto ok"
|
||||
}
|
||||
return Drop, "Unknown proto"
|
||||
}
|
||||
return Drop, "no rules matched"
|
||||
|
||||
@@ -23,17 +23,25 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newFilter(logf logger.Logf) *Filter {
|
||||
m := func(srcs []netaddr.IPPrefix, dsts []NetPortRange, protos ...ipproto.Proto) Match {
|
||||
if protos == nil {
|
||||
protos = defaultProtos
|
||||
}
|
||||
return Match{
|
||||
IPProto: protos,
|
||||
Srcs: srcs,
|
||||
Dsts: dsts,
|
||||
}
|
||||
// testAllowedProto is an IP protocol number we treat as allowed for
|
||||
// these tests.
|
||||
const (
|
||||
testAllowedProto ipproto.Proto = 116
|
||||
testDeniedProto ipproto.Proto = 127 // CRUDP, appropriately cruddy
|
||||
)
|
||||
|
||||
func m(srcs []netaddr.IPPrefix, dsts []NetPortRange, protos ...ipproto.Proto) Match {
|
||||
if protos == nil {
|
||||
protos = defaultProtos
|
||||
}
|
||||
return Match{
|
||||
IPProto: protos,
|
||||
Srcs: srcs,
|
||||
Dsts: dsts,
|
||||
}
|
||||
}
|
||||
|
||||
func newFilter(logf logger.Logf) *Filter {
|
||||
matches := []Match{
|
||||
m(nets("8.1.1.1", "8.2.2.2"), netports("1.2.3.4:22", "5.6.7.8:23-24")),
|
||||
m(nets("9.1.1.1", "9.2.2.2"), netports("1.2.3.4:22", "5.6.7.8:23-24"), ipproto.SCTP),
|
||||
@@ -44,6 +52,8 @@ func newFilter(logf logger.Logf) *Filter {
|
||||
m(nets("153.1.1.1", "153.1.1.2", "153.3.3.3"), netports("1.2.3.4:999")),
|
||||
m(nets("::1", "::2"), netports("2001::1:22", "2001::2:22")),
|
||||
m(nets("::/0"), netports("::/0:443")),
|
||||
m(nets("0.0.0.0/0"), netports("0.0.0.0/0:*"), testAllowedProto),
|
||||
m(nets("::/0"), netports("::/0:*"), testAllowedProto),
|
||||
}
|
||||
|
||||
// Expects traffic to 100.122.98.50, 1.2.3.4, 5.6.7.8,
|
||||
@@ -112,6 +122,12 @@ func TestFilter(t *testing.T) {
|
||||
{Drop, parsed(ipproto.SCTP, "8.1.1.1", "1.2.3.4", 999, 22)},
|
||||
// But SCTP is allowed for 9.1.1.1
|
||||
{Accept, parsed(ipproto.SCTP, "9.1.1.1", "1.2.3.4", 999, 22)},
|
||||
|
||||
// Unknown protocol is allowed if all its ports are allowed.
|
||||
{Accept, parsed(testAllowedProto, "1.2.3.4", "5.6.7.8", 0, 0)},
|
||||
{Accept, parsed(testAllowedProto, "2001::1", "2001::2", 0, 0)},
|
||||
{Drop, parsed(testDeniedProto, "1.2.3.4", "5.6.7.8", 0, 0)},
|
||||
{Drop, parsed(testDeniedProto, "2001::1", "2001::2", 0, 0)},
|
||||
}
|
||||
for i, test := range tests {
|
||||
aclFunc := acl.runIn4
|
||||
@@ -534,13 +550,7 @@ func TestLoggingPrivacy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func mustIP(s string) netaddr.IP {
|
||||
ip, err := netaddr.ParseIP(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ip
|
||||
}
|
||||
var mustIP = netaddr.MustParseIP
|
||||
|
||||
func parsed(proto ipproto.Proto, src, dst string, sport, dport uint16) packet.Parsed {
|
||||
sip, dip := mustIP(src), mustIP(dst)
|
||||
@@ -689,7 +699,7 @@ func nets(nets ...string) (ret []netaddr.IPPrefix) {
|
||||
|
||||
func ports(s string) PortRange {
|
||||
if s == "*" {
|
||||
return PortRange{First: 0, Last: 65535}
|
||||
return allPorts
|
||||
}
|
||||
|
||||
var fs, ls string
|
||||
@@ -815,3 +825,40 @@ func TestMatchesFromFilterRules(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesMatchProtoAndIPsOnlyIfAllPorts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
m Match
|
||||
p packet.Parsed
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "all_ports_okay",
|
||||
m: m(nets("0.0.0.0/0"), netports("0.0.0.0/0:*"), testAllowedProto),
|
||||
p: parsed(testAllowedProto, "1.2.3.4", "5.6.7.8", 0, 0),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "all_ports_match_but_packet_wrong_proto",
|
||||
m: m(nets("0.0.0.0/0"), netports("0.0.0.0/0:*"), testAllowedProto),
|
||||
p: parsed(testDeniedProto, "1.2.3.4", "5.6.7.8", 0, 0),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "ports_requirements_dont_match_unknown_proto",
|
||||
m: m(nets("0.0.0.0/0"), netports("0.0.0.0/0:12345"), testAllowedProto),
|
||||
p: parsed(testAllowedProto, "1.2.3.4", "5.6.7.8", 0, 0),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
matches := matches{tt.m}
|
||||
got := matches.matchProtoAndIPsOnlyIfAllPorts(&tt.p)
|
||||
if got != tt.want {
|
||||
t.Errorf("got = %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ type PortRange struct {
|
||||
First, Last uint16 // inclusive
|
||||
}
|
||||
|
||||
var allPorts = PortRange{0, 0xffff}
|
||||
|
||||
func (pr PortRange) String() string {
|
||||
if pr.First == 0 && pr.Last == 65535 {
|
||||
return "*"
|
||||
@@ -115,6 +117,29 @@ func (ms matches) matchIPsOnly(q *packet.Parsed) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// matchProtoAndIPsOnlyIfAllPorts reports q matches any Match in ms where the
|
||||
// Match if for the right IP Protocol and IP address, but ports are
|
||||
// ignored, as long as the match is for the entire uint16 port range.
|
||||
func (ms matches) matchProtoAndIPsOnlyIfAllPorts(q *packet.Parsed) bool {
|
||||
for _, m := range ms {
|
||||
if !protoInList(q.IPProto, m.IPProto) {
|
||||
continue
|
||||
}
|
||||
if !ipInList(q.Src.IP(), m.Srcs) {
|
||||
continue
|
||||
}
|
||||
for _, dst := range m.Dsts {
|
||||
if dst.Ports != allPorts {
|
||||
continue
|
||||
}
|
||||
if dst.Net.Contains(q.Dst.IP()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ipInList(ip netaddr.IP, netlist []netaddr.IPPrefix) bool {
|
||||
for _, net := range netlist {
|
||||
if net.Contains(ip) {
|
||||
|
||||
@@ -99,7 +99,7 @@ type linuxRouter struct {
|
||||
ipRuleFixLimiter *rate.Limiter
|
||||
|
||||
// Various feature checks for the network stack.
|
||||
ipRuleAvailable bool
|
||||
ipRuleAvailable bool // whether kernel was built with IP_MULTIPLE_TABLES
|
||||
v6Available bool
|
||||
v6NATAvailable bool
|
||||
|
||||
@@ -119,7 +119,7 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, linkMon *monitor.Mo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v6err := checkIPv6()
|
||||
v6err := checkIPv6(logf)
|
||||
if v6err != nil {
|
||||
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
|
||||
}
|
||||
@@ -165,8 +165,13 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, linkMon *monit
|
||||
if r.useIPCommand() {
|
||||
r.ipRuleAvailable = (cmd.run("ip", "rule") == nil)
|
||||
} else {
|
||||
// Pretend it is.
|
||||
r.ipRuleAvailable = true
|
||||
if rules, err := netlink.RuleList(netlink.FAMILY_V4); err != nil {
|
||||
r.logf("error querying IP rules (does kernel have IP_MULTIPLE_TABLES?): %v", err)
|
||||
r.logf("warning: running without policy routing")
|
||||
} else {
|
||||
r.logf("[v1] policy routing available; found %d rules", len(rules))
|
||||
r.ipRuleAvailable = true
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
@@ -245,13 +250,13 @@ func (r *linuxRouter) Up() error {
|
||||
return err
|
||||
}
|
||||
if err := r.addIPRules(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("adding IP rules: %w", err)
|
||||
}
|
||||
if err := r.setNetfilterMode(netfilterOff); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("setting netfilter mode: %w", err)
|
||||
}
|
||||
if err := r.upInterface(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("bringing interface up: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1487,7 +1492,7 @@ func cleanup(logf logger.Logf, interfaceName string) {
|
||||
// missing. It does not check that IPv6 is currently functional or
|
||||
// that there's a global address, just that the system would support
|
||||
// IPv6 if it were on an IPv6 network.
|
||||
func checkIPv6() error {
|
||||
func checkIPv6(logf logger.Logf) error {
|
||||
_, err := os.Stat("/proc/sys/net/ipv6")
|
||||
if os.IsNotExist(err) {
|
||||
return err
|
||||
@@ -1519,7 +1524,7 @@ func checkIPv6() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := checkIPRuleSupportsV6(); err != nil {
|
||||
if err := checkIPRuleSupportsV6(logf); err != nil {
|
||||
return fmt.Errorf("kernel doesn't support IPv6 policy routing: %w", err)
|
||||
}
|
||||
|
||||
@@ -1547,11 +1552,24 @@ func supportsV6NAT() bool {
|
||||
return bytes.Contains(bs, []byte("nat\n"))
|
||||
}
|
||||
|
||||
func checkIPRuleSupportsV6() error {
|
||||
func checkIPRuleSupportsV6(logf logger.Logf) error {
|
||||
// First try just a read-only operation to ideally avoid
|
||||
// having to modify any state.
|
||||
if rules, err := netlink.RuleList(netlink.FAMILY_V6); err != nil {
|
||||
return fmt.Errorf("querying IPv6 policy routing rules: %w", err)
|
||||
} else {
|
||||
if len(rules) > 0 {
|
||||
logf("[v1] kernel supports IPv6 policy routing (found %d rules)", len(rules))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try to actually create & delete one as a test.
|
||||
rule := netlink.NewRule()
|
||||
rule.Priority = 1234
|
||||
rule.Mark = tailscaleBypassMarkNum
|
||||
rule.Table = tailscaleRouteTable.num
|
||||
rule.Family = netlink.FAMILY_V6
|
||||
// First delete the rule unconditionally, and don't check for
|
||||
// errors. This is just cleaning up anything that might be already
|
||||
// there.
|
||||
|
||||
@@ -793,7 +793,7 @@ func TestDebugListRules(t *testing.T) {
|
||||
t.Run(famName[fam], func(t *testing.T) {
|
||||
rules, err := netlink.RuleList(fam)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Skipf("skip; RuleList fails with: %v", err)
|
||||
}
|
||||
for _, r := range rules {
|
||||
t.Logf("Rule: %+v", r)
|
||||
@@ -803,7 +803,7 @@ func TestDebugListRules(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCheckIPRuleSupportsV6(t *testing.T) {
|
||||
err := checkIPRuleSupportsV6()
|
||||
err := checkIPRuleSupportsV6(t.Logf)
|
||||
if err != nil && os.Getuid() != 0 {
|
||||
t.Skipf("skipping, error when not root: %v", err)
|
||||
}
|
||||
|
||||
@@ -183,7 +183,10 @@ func (ft *firewallTweaker) runFirewall(args ...string) (time.Duration, error) {
|
||||
args = append([]string{"advfirewall", "firewall"}, args...)
|
||||
cmd := exec.Command("netsh", args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
err := cmd.Run()
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w: %v", err, string(b))
|
||||
}
|
||||
return time.Since(t0).Round(time.Millisecond), err
|
||||
}
|
||||
|
||||
|
||||
@@ -389,11 +389,11 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
|
||||
e.logf("Bringing wireguard device up...")
|
||||
if err := e.wgdev.Up(); err != nil {
|
||||
return nil, fmt.Errorf("wgdev.Up(): %w", err)
|
||||
return nil, fmt.Errorf("wgdev.Up: %w", err)
|
||||
}
|
||||
e.logf("Bringing router up...")
|
||||
if err := e.router.Up(); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("router.Up: %w", err)
|
||||
}
|
||||
|
||||
// It's a little pointless to apply no-op settings here (they
|
||||
@@ -401,7 +401,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
// router implementation early on.
|
||||
e.logf("Clearing router settings...")
|
||||
if err := e.router.Set(nil); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("router.Set(nil): %w", err)
|
||||
}
|
||||
e.logf("Starting link monitor...")
|
||||
e.linkMon.Start()
|
||||
|
||||
Reference in New Issue
Block a user