Move Linux client & common packages into a public repo.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine"
|
||||
"time"
|
||||
)
|
||||
|
||||
type State int
|
||||
|
||||
const (
|
||||
NoState = State(iota)
|
||||
NeedsLogin
|
||||
NeedsMachineAuth
|
||||
Stopped
|
||||
Starting
|
||||
Running
|
||||
)
|
||||
|
||||
func (s State) String() string {
|
||||
return [...]string{"NoState", "NeedsLogin", "NeedsMachineAuth",
|
||||
"Stopped", "Starting", "Running"}[s]
|
||||
}
|
||||
|
||||
type EngineStatus struct {
|
||||
RBytes, WBytes wgengine.ByteCount
|
||||
NumLive int
|
||||
LivePeers map[tailcfg.NodeKey]wgengine.PeerStatus
|
||||
}
|
||||
|
||||
type NetworkMap = controlclient.NetworkMap
|
||||
|
||||
// In any given notification, any or all of these may be nil, meaning
|
||||
// that they have not changed.
|
||||
type Notify struct {
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any
|
||||
LoginFinished *struct{} // event: login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
FrontendLogID string // public logtail id used by frontend
|
||||
ServerURL string
|
||||
Prefs Prefs
|
||||
LoginFlags controlclient.LoginFlags
|
||||
Notify func(n Notify) `json:"-"`
|
||||
}
|
||||
|
||||
type Backend interface {
|
||||
// Start or restart the backend, because a new Handle has connected.
|
||||
Start(opts Options) error
|
||||
// Start a new interactive login. This should trigger a new
|
||||
// BrowseToURL notification eventually.
|
||||
StartLoginInteractive()
|
||||
// Terminate the current login session and stop the wireguard engine.
|
||||
Logout()
|
||||
// Install a new set of user preferences, including WantRunning.
|
||||
// This may cause the wireguard engine to reconfigure or stop.
|
||||
SetPrefs(new Prefs)
|
||||
// Poll for an update from the wireguard engine. Only needed if
|
||||
// you want to display byte counts. Connection events are emitted
|
||||
// automatically without polling.
|
||||
RequestEngineStatus()
|
||||
// Pretend the current key is going to expire after duration x.
|
||||
// This is useful for testing GUIs to make sure they react properly
|
||||
// with keys that are going to expire.
|
||||
FakeExpireAfter(x time.Duration)
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package ipn implements the interactions between the Tailscale cloud
|
||||
// control plane and the local network stack.
|
||||
//
|
||||
// IPN is the abbreviated name for a Tailscale network. What's less
|
||||
// clear is what it's an abbreviation for: Identified Private Network?
|
||||
// IP Network? Internet Private Network? I Privately Network?
|
||||
package ipn
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
// 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.
|
||||
|
||||
// +build depends_on_currently_unreleased
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun/tuntest"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/testy"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.io/control" // not yet released
|
||||
)
|
||||
|
||||
func TestIPN(t *testing.T) {
|
||||
testy.FixLogs(t)
|
||||
defer testy.UnfixLogs(t)
|
||||
|
||||
// Turn off STUN for the test to make it hermitic.
|
||||
// TODO(crawshaw): add a test that runs against a local STUN server.
|
||||
origDefaultSTUN := magicsock.DefaultSTUN
|
||||
magicsock.DefaultSTUN = nil
|
||||
defer func() {
|
||||
magicsock.DefaultSTUN = origDefaultSTUN
|
||||
}()
|
||||
|
||||
// TODO(apenwarr): Make resource checks actually pass.
|
||||
// They don't right now, because (at least) wgengine doesn't fully
|
||||
// shut down.
|
||||
// rc := testy.NewResourceCheck()
|
||||
// defer rc.Assert(t)
|
||||
|
||||
var ctl *control.Server
|
||||
|
||||
ctlHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctl.ServeHTTP(w, r)
|
||||
}
|
||||
https := httptest.NewServer(http.HandlerFunc(ctlHandler))
|
||||
serverURL := https.URL
|
||||
defer https.Close()
|
||||
defer https.CloseClientConnections()
|
||||
|
||||
tmpdir, err := ioutil.TempDir("", "ipntest")
|
||||
if err != nil {
|
||||
t.Fatalf("create tempdir: %v\n", err)
|
||||
}
|
||||
ctl, err = control.New(tmpdir, serverURL, true)
|
||||
if err != nil {
|
||||
t.Fatalf("create control server: %v\n", ctl)
|
||||
}
|
||||
|
||||
n1 := newNode(t, "n1", https)
|
||||
defer n1.Backend.Shutdown()
|
||||
n1.Backend.StartLoginInteractive()
|
||||
|
||||
n2 := newNode(t, "n2", https)
|
||||
defer n2.Backend.Shutdown()
|
||||
n2.Backend.StartLoginInteractive()
|
||||
|
||||
var s1, s2 State
|
||||
for {
|
||||
t.Logf("\n\nn1.state=%v n2.state=%v\n\n", s1, s2)
|
||||
|
||||
// TODO(crawshaw): switch from || to &&. To do this we need to
|
||||
// transmit some data so that the handshake completes on both
|
||||
// sides. (Beacuse handshakes are 1RTT, it is the data
|
||||
// transmission that completes the handshake.)
|
||||
if s1 == Running || s2 == Running {
|
||||
// TODO(apenwarr): ensure state sequence.
|
||||
// Right now we'll just exit as soon as
|
||||
// state==Running, even if the backend is lying or
|
||||
// something. Not a great test.
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case n := <-n1.NotifyCh:
|
||||
t.Logf("n1n: %v\n", n)
|
||||
if n.State != nil {
|
||||
s1 = *n.State
|
||||
if s1 == NeedsMachineAuth {
|
||||
authNode(t, ctl, n1.Backend)
|
||||
}
|
||||
}
|
||||
case n := <-n2.NotifyCh:
|
||||
t.Logf("n2n: %v\n", n)
|
||||
if n.State != nil {
|
||||
s2 = *n.State
|
||||
if s2 == NeedsMachineAuth {
|
||||
authNode(t, ctl, n2.Backend)
|
||||
}
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatalf("\n\n\nFATAL: timed out waiting for notifications.\n\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
t.Skip("skipping ping tests, they are flaky") // TODO(crawshaw): this exposes a real bug!
|
||||
|
||||
n1addr := n1.Backend.NetMap().Addresses[0].IP
|
||||
n2addr := n2.Backend.NetMap().Addresses[0].IP
|
||||
t.Run("ping n2", func(t *testing.T) {
|
||||
msg := tuntest.Ping(n2addr.IP(), n1addr.IP())
|
||||
n1.ChannelTUN.Outbound <- msg
|
||||
select {
|
||||
case msgRecv := <-n2.ChannelTUN.Inbound:
|
||||
if !bytes.Equal(msg, msgRecv) {
|
||||
t.Error("bad ping")
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Error("no ping seen")
|
||||
}
|
||||
})
|
||||
t.Run("ping n1", func(t *testing.T) {
|
||||
msg := tuntest.Ping(n1addr.IP(), n2addr.IP())
|
||||
n2.ChannelTUN.Outbound <- msg
|
||||
select {
|
||||
case msgRecv := <-n1.ChannelTUN.Inbound:
|
||||
if !bytes.Equal(msg, msgRecv) {
|
||||
t.Error("bad ping")
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Error("no ping seen")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type testNode struct {
|
||||
Backend *LocalBackend
|
||||
ChannelTUN *tuntest.ChannelTUN
|
||||
NotifyCh <-chan Notify
|
||||
}
|
||||
|
||||
// Create a new IPN node.
|
||||
func newNode(t *testing.T, prefix string, https *httptest.Server) testNode {
|
||||
t.Helper()
|
||||
logfe := func(fmt string, args ...interface{}) {
|
||||
t.Logf(prefix+".e: "+fmt, args...)
|
||||
}
|
||||
logf := func(fmt string, args ...interface{}) {
|
||||
t.Logf(prefix+": "+fmt, args...)
|
||||
}
|
||||
|
||||
derp := false
|
||||
tun := tuntest.NewChannelTUN()
|
||||
e1, err := wgengine.NewUserspaceEngineAdvanced(logfe, tun.TUN(), wgengine.NewFakeRouter, 0, derp)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeEngine: %v\n", err)
|
||||
}
|
||||
n, err := NewLocalBackend(logf, prefix, e1)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v\n", err)
|
||||
}
|
||||
nch := make(chan Notify, 1000)
|
||||
c := controlclient.Persist{
|
||||
Provider: "google",
|
||||
LoginName: "test1@tailscale.com",
|
||||
}
|
||||
n.Start(Options{
|
||||
FrontendLogID: prefix + "-f",
|
||||
ServerURL: https.URL,
|
||||
Prefs: Prefs{
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
WantRunning: true,
|
||||
Persist: &c,
|
||||
},
|
||||
LoginFlags: controlclient.LoginDefault,
|
||||
Notify: func(n Notify) {
|
||||
// Automatically visit auth URLs
|
||||
if n.BrowseToURL != nil {
|
||||
t.Logf("\n\n\nURL! %vv\n", *n.BrowseToURL)
|
||||
hc := https.Client()
|
||||
_, err := hc.Get(*n.BrowseToURL)
|
||||
if err != nil {
|
||||
t.Logf("BrowseToURL: %v\n", err)
|
||||
}
|
||||
}
|
||||
nch <- n
|
||||
},
|
||||
})
|
||||
|
||||
return testNode{
|
||||
Backend: n,
|
||||
ChannelTUN: tun,
|
||||
NotifyCh: nch,
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the control server to authorize the given node.
|
||||
func authNode(t *testing.T, ctl *control.Server, n *LocalBackend) {
|
||||
mk := *n.prefs.Persist.PrivateMachineKey.Public()
|
||||
nk := *n.prefs.Persist.PrivateNodeKey.Public()
|
||||
ctl.AuthorizeMachine(tailcfg.MachineKey(mk), tailcfg.NodeKey(nk))
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FakeBackend struct {
|
||||
serverURL string
|
||||
notify func(n Notify)
|
||||
live bool
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Start(opts Options) error {
|
||||
b.serverURL = opts.ServerURL
|
||||
if opts.Notify == nil {
|
||||
log.Fatalf("FakeBackend.Start: opts.Notify is nil\n")
|
||||
}
|
||||
b.notify = opts.Notify
|
||||
b.notify(Notify{Prefs: &opts.Prefs})
|
||||
nl := NeedsLogin
|
||||
b.notify(Notify{State: &nl})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *FakeBackend) newState(s State) {
|
||||
b.notify(Notify{State: &s})
|
||||
if s == Running {
|
||||
b.live = true
|
||||
} else {
|
||||
b.live = false
|
||||
}
|
||||
}
|
||||
|
||||
func (b *FakeBackend) StartLoginInteractive() {
|
||||
u := b.serverURL + "/this/is/fake"
|
||||
b.notify(Notify{BrowseToURL: &u})
|
||||
b.newState(NeedsMachineAuth)
|
||||
b.newState(Stopped)
|
||||
// TODO(apenwarr): Fill in a more interesting netmap here.
|
||||
b.notify(Notify{NetMap: &NetworkMap{}})
|
||||
b.newState(Starting)
|
||||
// TODO(apenwarr): Fill in a more interesting status.
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
b.newState(Running)
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Logout() {
|
||||
b.newState(NeedsLogin)
|
||||
}
|
||||
|
||||
func (b *FakeBackend) SetPrefs(new Prefs) {
|
||||
b.notify(Notify{Prefs: &new})
|
||||
if new.WantRunning && !b.live {
|
||||
b.newState(Starting)
|
||||
b.newState(Running)
|
||||
} else if !new.WantRunning && b.live {
|
||||
b.newState(Stopped)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *FakeBackend) RequestEngineStatus() {
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.notify(Notify{NetMap: &NetworkMap{}})
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/logger"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
serverURL string
|
||||
frontendLogID string
|
||||
b Backend
|
||||
xnotify func(n Notify)
|
||||
logf logger.Logf
|
||||
|
||||
// Mutex protects everything below
|
||||
mu sync.Mutex
|
||||
netmapCache *NetworkMap
|
||||
engineStatusCache EngineStatus
|
||||
stateCache State
|
||||
prefsCache Prefs
|
||||
}
|
||||
|
||||
func NewHandle(b Backend, logf logger.Logf, opts Options) (*Handle, error) {
|
||||
h := &Handle{
|
||||
b: b,
|
||||
logf: logf,
|
||||
}
|
||||
|
||||
err := h.Start(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *Handle) Start(opts Options) error {
|
||||
h.serverURL = strings.TrimRight(opts.ServerURL, "/")
|
||||
h.frontendLogID = opts.FrontendLogID
|
||||
h.xnotify = opts.Notify
|
||||
h.netmapCache = nil
|
||||
h.engineStatusCache = EngineStatus{}
|
||||
h.stateCache = NoState
|
||||
h.prefsCache = opts.Prefs
|
||||
xopts := opts
|
||||
xopts.Notify = h.notify
|
||||
return h.b.Start(xopts)
|
||||
}
|
||||
|
||||
func (h *Handle) Reset() {
|
||||
st := NoState
|
||||
h.notify(Notify{State: &st})
|
||||
}
|
||||
|
||||
func (h *Handle) notify(n Notify) {
|
||||
h.mu.Lock()
|
||||
if n.BackendLogID != nil {
|
||||
h.logf("Handle: logs: be:%v fe:%v\n",
|
||||
*n.BackendLogID, h.frontendLogID)
|
||||
}
|
||||
if n.State != nil {
|
||||
h.stateCache = *n.State
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
h.prefsCache = *n.Prefs
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
h.netmapCache = n.NetMap
|
||||
}
|
||||
if n.Engine != nil {
|
||||
h.engineStatusCache = *n.Engine
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
if h.xnotify != nil {
|
||||
// Forward onward to our parent's notifier
|
||||
h.xnotify(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handle) Prefs() Prefs {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.prefsCache
|
||||
}
|
||||
|
||||
func (h *Handle) UpdatePrefs(updateFn func(old Prefs) (new Prefs)) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
new := updateFn(h.prefsCache)
|
||||
h.prefsCache = new
|
||||
h.b.SetPrefs(new)
|
||||
}
|
||||
|
||||
func (h *Handle) State() State {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.stateCache
|
||||
}
|
||||
|
||||
func (h *Handle) EngineStatus() EngineStatus {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.engineStatusCache
|
||||
}
|
||||
|
||||
func (h *Handle) LocalAddrs() []wgcfg.CIDR {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
nm := h.netmapCache
|
||||
if nm != nil {
|
||||
return nm.Addresses
|
||||
}
|
||||
return []wgcfg.CIDR{}
|
||||
}
|
||||
|
||||
func (h *Handle) NetMap() *NetworkMap {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.netmapCache
|
||||
}
|
||||
|
||||
func (h *Handle) Expiry() time.Time {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
nm := h.netmapCache
|
||||
if nm != nil {
|
||||
return nm.Expiry
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (h *Handle) AdminPageURL() string {
|
||||
return h.serverURL + "/admin/machines"
|
||||
}
|
||||
|
||||
func (h *Handle) StartLoginInteractive() {
|
||||
h.b.StartLoginInteractive()
|
||||
}
|
||||
|
||||
func (h *Handle) Logout() {
|
||||
h.b.Logout()
|
||||
}
|
||||
|
||||
func (h *Handle) RequestEngineStatus() {
|
||||
h.b.RequestEngineStatus()
|
||||
}
|
||||
|
||||
func (h *Handle) FakeExpireAfter(x time.Duration) {
|
||||
h.b.FakeExpireAfter(x)
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
SurviveDisconnects bool
|
||||
AllowQuit bool
|
||||
}
|
||||
|
||||
func pump(logf logger.Logf, ctx context.Context, bs *ipn.BackendServer, s net.Conn) {
|
||||
defer logf("Control connection done.\n")
|
||||
|
||||
for ctx.Err() == nil && !bs.GotQuit {
|
||||
msg, err := ipn.ReadMsg(s)
|
||||
if err != nil {
|
||||
logf("ReadMsg: %v\n", err)
|
||||
break
|
||||
}
|
||||
err = bs.GotCommandMsg(msg)
|
||||
if err != nil {
|
||||
logf("GotCommandMsg: %v\n", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e wgengine.Engine) error {
|
||||
bo := backoff.Backoff{Name: "ipnserver"}
|
||||
|
||||
listen, _, err := safesocket.Listen("", "Tailscale", "tailscaled", 41112)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
b, err := ipn.NewLocalBackend(logf, logid, e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
b.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return zstd.NewReader(nil)
|
||||
})
|
||||
b.SetCmpDiff(func(x, y interface{}) string { return cmp.Diff(x, y) })
|
||||
|
||||
var s net.Conn
|
||||
serverToClient := func(b []byte) {
|
||||
if s != nil {
|
||||
ipn.WriteMsg(s, b)
|
||||
}
|
||||
}
|
||||
|
||||
bs := ipn.NewBackendServer(logf, b, serverToClient)
|
||||
|
||||
logf("Listening on %v\n", listen.Addr())
|
||||
|
||||
// Go listeners can't take a context, close it instead.
|
||||
go func() {
|
||||
<-rctx.Done()
|
||||
listen.Close()
|
||||
}()
|
||||
|
||||
var oldS net.Conn
|
||||
ctx, cancel := context.WithCancel(rctx)
|
||||
|
||||
stopAll := func() {
|
||||
// Currently we only support one client connection at a time.
|
||||
// Theoretically we could allow multiple clients, by passing
|
||||
// notifications to all of them and accepting commands from
|
||||
// any of them, but there doesn't seem to be much need for
|
||||
// that right now.
|
||||
if oldS != nil {
|
||||
cancel()
|
||||
safesocket.ConnCloseRead(oldS)
|
||||
safesocket.ConnCloseWrite(oldS)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 1; rctx.Err() == nil; i++ {
|
||||
s, err = listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v\n", i, err)
|
||||
bo.BackOff(rctx, err)
|
||||
continue
|
||||
}
|
||||
logf("%d: Incoming control connection.\n", i)
|
||||
stopAll()
|
||||
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
oldS = s
|
||||
|
||||
go func(ctx context.Context, bs *ipn.BackendServer, s net.Conn, i int) {
|
||||
si := fmt.Sprintf("%d: ", i)
|
||||
pump(func(fmt string, args ...interface{}) {
|
||||
logf(si+fmt, args...)
|
||||
}, ctx, bs, s)
|
||||
if !opts.SurviveDisconnects || bs.GotQuit {
|
||||
bs.Reset()
|
||||
s.Close()
|
||||
}
|
||||
if opts.AllowQuit {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
bs.GotQuit = false
|
||||
}
|
||||
}(ctx, bs, s, i)
|
||||
|
||||
bo.BackOff(ctx, nil)
|
||||
}
|
||||
stopAll()
|
||||
|
||||
return rctx.Err()
|
||||
}
|
||||
|
||||
func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
panic("cannot determine executable: " + err.Error())
|
||||
}
|
||||
|
||||
var proc struct {
|
||||
mu sync.Mutex
|
||||
p *os.Process
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
var sig os.Signal
|
||||
select {
|
||||
case sig = <-interrupt:
|
||||
logf("BabysitProc: got signal: %v\n", sig)
|
||||
close(done)
|
||||
case <-ctx.Done():
|
||||
logf("BabysitProc: context done\n")
|
||||
sig = os.Kill
|
||||
close(done)
|
||||
}
|
||||
|
||||
proc.mu.Lock()
|
||||
proc.p.Signal(sig)
|
||||
proc.mu.Unlock()
|
||||
}()
|
||||
|
||||
bo := backoff.Backoff{Name: "BabysitProc"}
|
||||
|
||||
for {
|
||||
startTime := time.Now()
|
||||
log.Printf("exec: %#v %v\n", executable, args)
|
||||
cmd := exec.Command(executable, args...)
|
||||
|
||||
// Create a pipe object to use as the subproc's stdin.
|
||||
// When the writer goes away, the reader gets EOF.
|
||||
// A subproc can watch its stdin and exit when it gets EOF;
|
||||
// this is a very reliable way to have a subproc die when
|
||||
// its parent (us) disappears.
|
||||
// We never need to actually write to wStdin.
|
||||
rStdin, wStdin, err := os.Pipe()
|
||||
if err != nil {
|
||||
log.Printf("os.Pipe 1: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a pipe object to use as the subproc's stdout/stderr.
|
||||
// We'll read from this pipe and send it to logf, line by line.
|
||||
// We can't use os.exec's io.Writer for this because it
|
||||
// doesn't care about lines, and thus ends up merging multiple
|
||||
// log lines into one or splitting one line into multiple
|
||||
// logf() calls. bufio is more appropriate.
|
||||
rStdout, wStdout, err := os.Pipe()
|
||||
if err != nil {
|
||||
log.Printf("os.Pipe 2: %v\n", err)
|
||||
}
|
||||
go func(r *os.File) {
|
||||
defer r.Close()
|
||||
rb := bufio.NewReader(r)
|
||||
for {
|
||||
s, err := rb.ReadString('\n')
|
||||
if s != "" {
|
||||
logf("%s\n", strings.TrimSuffix(s, "\n"))
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}(rStdout)
|
||||
|
||||
cmd.Stdin = rStdin
|
||||
cmd.Stdout = wStdout
|
||||
cmd.Stderr = wStdout
|
||||
err = cmd.Start()
|
||||
|
||||
// Now that the subproc is started, get rid of our copy of the
|
||||
// pipe reader. Bad things happen on Windows if more than one
|
||||
// process owns the read side of a pipe.
|
||||
rStdin.Close()
|
||||
wStdout.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("starting subprocess failed: %v", err)
|
||||
} else {
|
||||
proc.mu.Lock()
|
||||
proc.p = cmd.Process
|
||||
proc.mu.Unlock()
|
||||
|
||||
err = cmd.Wait()
|
||||
log.Printf("subprocess exited: %v", err)
|
||||
}
|
||||
|
||||
// If the process finishes, clean up the write side of the
|
||||
// pipe. We'll make a new one when we restart the subproc.
|
||||
wStdin.Close()
|
||||
|
||||
if time.Since(startTime) < 60*time.Second {
|
||||
bo.BackOff(ctx, fmt.Errorf("subproc early exit: %v", err))
|
||||
} else {
|
||||
// Reset the timeout, since the process ran for a while.
|
||||
bo.BackOff(ctx, nil)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
+635
@@ -0,0 +1,635 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/logger"
|
||||
"tailscale.com/portlist"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// LocalBackend is the scaffolding between the Tailscale cloud control
|
||||
// plane and the local network stack.
|
||||
type LocalBackend struct {
|
||||
logf logger.Logf
|
||||
notify func(n Notify)
|
||||
c *controlclient.Client
|
||||
e wgengine.Engine
|
||||
serverURL string
|
||||
backendLogID string
|
||||
portpoll *portlist.Poller // may be nil
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
cmpDiff func(x, y interface{}) string
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
prefs Prefs
|
||||
state State
|
||||
hiCache tailcfg.Hostinfo
|
||||
netMapCache *controlclient.NetworkMap
|
||||
engineStatus EngineStatus
|
||||
endPoints []string
|
||||
blocked bool
|
||||
authURL string
|
||||
interact int
|
||||
|
||||
// statusLock must be held before calling statusChanged.Lock() or
|
||||
// statusChanged.Broadcast().
|
||||
statusLock sync.Mutex
|
||||
statusChanged *sync.Cond
|
||||
}
|
||||
|
||||
func NewLocalBackend(logf logger.Logf, logid string, e wgengine.Engine) (*LocalBackend, error) {
|
||||
|
||||
if e == nil {
|
||||
panic("ipn.NewLocalBackend: wgengine must not be nil")
|
||||
}
|
||||
|
||||
// Default filter blocks everything, until Start() is called.
|
||||
e.SetFilter(filter.NewAllowNone())
|
||||
|
||||
portpoll, err := portlist.NewPoller()
|
||||
if err != nil {
|
||||
logf("skipping portlist: %s\n", err)
|
||||
}
|
||||
|
||||
b := LocalBackend{
|
||||
logf: logf,
|
||||
e: e,
|
||||
backendLogID: logid,
|
||||
state: NoState,
|
||||
portpoll: portpoll,
|
||||
}
|
||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||
|
||||
if b.portpoll != nil {
|
||||
go b.portpoll.Run()
|
||||
go b.runPoller()
|
||||
}
|
||||
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Shutdown() {
|
||||
if b.portpoll != nil {
|
||||
b.portpoll.Close()
|
||||
}
|
||||
b.c.Shutdown()
|
||||
b.e.Close()
|
||||
b.e.Wait()
|
||||
}
|
||||
|
||||
// SetDecompressor sets a decompression function, which must be a zstd
|
||||
// reader.
|
||||
//
|
||||
// This exists because the iOS/Mac NetworkExtension is very resource
|
||||
// constrained, and the zstd package is too heavy to fit in the
|
||||
// constrained RSS limit.
|
||||
func (b *LocalBackend) SetDecompressor(fn func() (controlclient.Decompressor, error)) {
|
||||
b.newDecompressor = fn
|
||||
}
|
||||
|
||||
// SetCmpDiff sets a comparison function used to generate logs of what
|
||||
// has changed in the network map.
|
||||
//
|
||||
// Typically the comparison function comes from go-cmp.
|
||||
// We don't wire it in directly here because the go-cmp package adds
|
||||
// 1.77mb to the binary size of the iOS NetworkExtension, which takes
|
||||
// away from its precious RSS limit.
|
||||
func (b *LocalBackend) SetCmpDiff(cmpDiff func(x, y interface{}) string) {
|
||||
b.cmpDiff = cmpDiff
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Start(opts Options) error {
|
||||
if b.c != nil {
|
||||
// TODO(apenwarr): avoid the need to reinit controlclient.
|
||||
// This will trigger a full relogin/reconfigure cycle every
|
||||
// time a Handle reconnects to the backend. Ideally, we
|
||||
// would send the new Prefs and everything would get back
|
||||
// into sync with the minimal changes. But that's not how it
|
||||
// is right now, which is a sign that the code is still too
|
||||
// complicated.
|
||||
b.c.Shutdown()
|
||||
}
|
||||
|
||||
b.logf("Start: %v\n", opts.Prefs.Pretty())
|
||||
|
||||
hi := controlclient.NewHostinfo()
|
||||
hi.BackendLogID = b.backendLogID
|
||||
hi.FrontendLogID = opts.FrontendLogID
|
||||
|
||||
b.mu.Lock()
|
||||
hi.Services = b.hiCache.Services // keep any previous session
|
||||
b.hiCache = hi
|
||||
b.state = NoState
|
||||
b.serverURL = opts.ServerURL
|
||||
b.prefs = opts.Prefs
|
||||
b.notify = opts.Notify
|
||||
b.netMapCache = nil
|
||||
b.mu.Unlock()
|
||||
|
||||
b.updateFilter()
|
||||
|
||||
var err error
|
||||
persist := b.prefs.Persist
|
||||
if persist == nil {
|
||||
// let controlclient initialize it
|
||||
persist = &controlclient.Persist{}
|
||||
}
|
||||
cli, err := controlclient.New(controlclient.Options{
|
||||
Logf: func(fmt string, args ...interface{}) {
|
||||
b.logf("control: "+fmt, args...)
|
||||
},
|
||||
Persist: *persist,
|
||||
ServerURL: b.serverURL,
|
||||
Hostinfo: &hi,
|
||||
KeepAlive: true,
|
||||
NewDecompressor: b.newDecompressor,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
b.c = cli
|
||||
b.mu.Unlock()
|
||||
|
||||
if b.endPoints != nil {
|
||||
cli.UpdateEndpoints(0, b.endPoints)
|
||||
}
|
||||
|
||||
cli.SetStatusFunc(func(new controlclient.Status) {
|
||||
if new.LoginFinished != nil {
|
||||
// Auth completed, unblock the engine
|
||||
b.blockEngineUpdates(false)
|
||||
b.authReconfig()
|
||||
noargs := struct{}{}
|
||||
b.send(Notify{LoginFinished: &noargs})
|
||||
}
|
||||
if new.Persist != nil {
|
||||
persist := *new.Persist // copy
|
||||
b.prefs.Persist = &persist
|
||||
np := b.prefs
|
||||
b.send(Notify{Prefs: &np})
|
||||
}
|
||||
if new.NetMap != nil {
|
||||
if b.netMapCache != nil && b.cmpDiff != nil {
|
||||
s1 := strings.Split(b.netMapCache.Concise(), "\n")
|
||||
s2 := strings.Split(new.NetMap.Concise(), "\n")
|
||||
b.logf("netmap diff:\n%v\n", b.cmpDiff(s1, s2))
|
||||
}
|
||||
b.netMapCache = new.NetMap
|
||||
b.send(Notify{NetMap: new.NetMap})
|
||||
b.updateFilter()
|
||||
}
|
||||
if new.URL != "" {
|
||||
b.logf("Received auth URL: %.20v...\n", new.URL)
|
||||
|
||||
b.mu.Lock()
|
||||
interact := b.interact
|
||||
b.authURL = new.URL
|
||||
b.mu.Unlock()
|
||||
|
||||
if interact > 0 {
|
||||
b.popBrowserAuthNow()
|
||||
}
|
||||
}
|
||||
if new.Err != "" {
|
||||
// TODO(crawshaw): display in the UI.
|
||||
log.Print(new.Err)
|
||||
return
|
||||
}
|
||||
if new.NetMap != nil {
|
||||
if b.prefs.WantRunning || b.State() == NeedsLogin {
|
||||
b.prefs.WantRunning = true
|
||||
}
|
||||
b.SetPrefs(b.prefs)
|
||||
}
|
||||
b.stateMachine()
|
||||
})
|
||||
|
||||
b.e.SetStatusCallback(func(s *wgengine.Status, err error) {
|
||||
if err != nil {
|
||||
b.logf("wgengine status error: %#v", err)
|
||||
return
|
||||
}
|
||||
if s == nil {
|
||||
log.Fatalf("weird: non-error wgengine update with status=nil\n")
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
es := b.parseWgStatus(s)
|
||||
b.mu.Unlock()
|
||||
|
||||
b.engineStatus = es
|
||||
|
||||
if b.c != nil {
|
||||
b.c.UpdateEndpoints(0, s.LocalAddrs)
|
||||
}
|
||||
b.endPoints = append([]string{}, s.LocalAddrs...)
|
||||
b.stateMachine()
|
||||
|
||||
b.statusLock.Lock()
|
||||
b.statusChanged.Broadcast()
|
||||
b.statusLock.Unlock()
|
||||
|
||||
b.send(Notify{Engine: &es})
|
||||
})
|
||||
|
||||
blid := b.backendLogID
|
||||
b.logf("Backend: logs: be:%v fe:%v\n", blid, opts.FrontendLogID)
|
||||
b.send(Notify{BackendLogID: &blid})
|
||||
|
||||
cli.Login(nil, opts.LoginFlags)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) updateFilter() {
|
||||
if !b.Prefs().UsePacketFilter {
|
||||
b.e.SetFilter(filter.NewAllowAll())
|
||||
} else if b.netMapCache == nil {
|
||||
// Not configured yet, block everything
|
||||
b.e.SetFilter(filter.NewAllowNone())
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v\n", b.netMapCache.PacketFilter)
|
||||
b.e.SetFilter(filter.New(b.netMapCache.PacketFilter))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) runPoller() {
|
||||
for {
|
||||
ports := <-b.portpoll.C
|
||||
if ports == nil {
|
||||
break
|
||||
}
|
||||
sl := []tailcfg.Service{}
|
||||
for _, p := range ports {
|
||||
var proto tailcfg.ServiceProto
|
||||
if p.Proto == "tcp" {
|
||||
proto = tailcfg.TCP
|
||||
} else if p.Proto == "udp" {
|
||||
proto = tailcfg.UDP
|
||||
}
|
||||
if p.Port == 53 || p.Port == 68 ||
|
||||
p.Port == 5353 || p.Port == 5355 {
|
||||
// uninteresting system services
|
||||
continue
|
||||
}
|
||||
s := tailcfg.Service{
|
||||
Proto: proto,
|
||||
Port: p.Port,
|
||||
Description: p.Process,
|
||||
}
|
||||
sl = append(sl, s)
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
hi := b.hiCache
|
||||
hi.Services = sl
|
||||
b.hiCache = hi
|
||||
cli := b.c
|
||||
b.mu.Unlock()
|
||||
|
||||
// b.c might not be started yet
|
||||
if cli != nil {
|
||||
cli.SetHostinfo(hi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) send(n Notify) {
|
||||
if b.notify != nil {
|
||||
n.Version = version.LONG
|
||||
b.notify(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) popBrowserAuthNow() {
|
||||
b.mu.Lock()
|
||||
url := b.authURL
|
||||
b.interact = 0
|
||||
b.authURL = ""
|
||||
b.mu.Unlock()
|
||||
b.logf("popBrowserAuthNow: url=%v\n", url != "")
|
||||
|
||||
b.blockEngineUpdates(true)
|
||||
b.stopEngineAndWait()
|
||||
b.send(Notify{BrowseToURL: &url})
|
||||
if b.State() == Running {
|
||||
b.enterState(Starting)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) State() State {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
return b.state
|
||||
}
|
||||
|
||||
func (b *LocalBackend) EngineStatus() EngineStatus {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
return b.engineStatus
|
||||
}
|
||||
|
||||
func (b *LocalBackend) StartLoginInteractive() {
|
||||
b.assertClient()
|
||||
b.mu.Lock()
|
||||
b.interact++
|
||||
url := b.authURL
|
||||
b.mu.Unlock()
|
||||
b.logf("StartLoginInteractive: url=%v\n", url != "")
|
||||
|
||||
if url != "" {
|
||||
b.popBrowserAuthNow()
|
||||
} else {
|
||||
b.c.Login(nil, controlclient.LoginInteractive)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.logf("FakeExpireAfter: %v\n", x)
|
||||
if b.netMapCache != nil {
|
||||
e := b.netMapCache.Expiry
|
||||
if e.IsZero() || time.Until(e) > x {
|
||||
b.netMapCache.Expiry = time.Now().Add(x)
|
||||
}
|
||||
b.send(Notify{NetMap: b.netMapCache})
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) LocalAddrs() []wgcfg.CIDR {
|
||||
if b.netMapCache != nil {
|
||||
return b.netMapCache.Addresses
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Expiry() time.Time {
|
||||
if b.netMapCache != nil {
|
||||
return b.netMapCache.Expiry
|
||||
} else {
|
||||
return time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) parseWgStatus(s *wgengine.Status) EngineStatus {
|
||||
var ss []string
|
||||
var rx, tx wgengine.ByteCount
|
||||
peers := make(map[tailcfg.NodeKey]wgengine.PeerStatus)
|
||||
|
||||
live := 0
|
||||
for _, p := range s.Peers {
|
||||
if p.LastHandshake.IsZero() {
|
||||
ss = append(ss, "x")
|
||||
} else {
|
||||
ss = append(ss, fmt.Sprintf("%d/%d", p.RxBytes, p.TxBytes))
|
||||
live++
|
||||
peers[p.NodeKey] = p
|
||||
}
|
||||
rx += p.RxBytes
|
||||
tx += p.TxBytes
|
||||
}
|
||||
b.logf("v%v peers: %v\n", version.LONG, strings.Join(ss, " "))
|
||||
return EngineStatus{
|
||||
RBytes: rx,
|
||||
WBytes: tx,
|
||||
NumLive: live,
|
||||
LivePeers: peers,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) AdminPageURL() string {
|
||||
return b.serverURL + "/admin/machines"
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Prefs() Prefs {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
return b.prefs
|
||||
}
|
||||
|
||||
func (b *LocalBackend) SetPrefs(new Prefs) {
|
||||
b.mu.Lock()
|
||||
old := b.prefs
|
||||
new.Persist = old.Persist // caller isn't allowed to override this
|
||||
b.prefs = new
|
||||
b.mu.Unlock()
|
||||
|
||||
if old.WantRunning != new.WantRunning {
|
||||
b.stateMachine()
|
||||
} else {
|
||||
b.authReconfig()
|
||||
}
|
||||
|
||||
b.logf("SetPrefs: %v\n", new.Pretty())
|
||||
b.send(Notify{Prefs: &new})
|
||||
}
|
||||
|
||||
// Note: return value may be nil, if we haven't received a netmap yet.
|
||||
func (b *LocalBackend) NetMap() *controlclient.NetworkMap {
|
||||
return b.netMapCache
|
||||
}
|
||||
|
||||
func (b *LocalBackend) blockEngineUpdates(block bool) {
|
||||
// TODO(apenwarr): probably need mutex here (and several other places)
|
||||
b.logf("blockEngineUpdates(%v)\n", block)
|
||||
|
||||
b.mu.Lock()
|
||||
b.blocked = block
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) authReconfig() {
|
||||
b.mu.Lock()
|
||||
blocked := b.blocked
|
||||
uc := b.prefs
|
||||
nm := b.netMapCache
|
||||
b.mu.Unlock()
|
||||
|
||||
if blocked {
|
||||
b.logf("authReconfig: blocked, skipping.\n")
|
||||
return
|
||||
}
|
||||
if nm == nil {
|
||||
b.logf("authReconfig: netmap not yet valid. Skipping.\n")
|
||||
return
|
||||
}
|
||||
if !uc.WantRunning {
|
||||
b.logf("authReconfig: skipping because !WantRunning.\n")
|
||||
return
|
||||
}
|
||||
b.logf("Configuring wireguard connection.\n")
|
||||
|
||||
uflags := controlclient.UDefault
|
||||
if uc.RouteAll {
|
||||
uflags |= controlclient.UAllowDefaultRoute
|
||||
// TODO(apenwarr): Make subnet routes a different pref?
|
||||
uflags |= controlclient.UAllowSubnetRoutes
|
||||
// TODO(apenwarr): Remove this once we sort out subnet routes.
|
||||
// Right now default routes are broken in Windows, but
|
||||
// controlclient doesn't properly send subnet routes. So
|
||||
// let's convert a default route into a subnet route in order
|
||||
// to allow experimentation.
|
||||
uflags |= controlclient.UHackDefaultRoute
|
||||
}
|
||||
if uc.AllowSingleHosts {
|
||||
uflags |= controlclient.UAllowSingleHosts
|
||||
}
|
||||
b.logf("reconfig: ra=%v dns=%v 0x%02x\n", uc.RouteAll, uc.CorpDNS, uflags)
|
||||
|
||||
if nm != nil {
|
||||
dns := nm.DNS
|
||||
dom := nm.DNSDomains
|
||||
if !uc.CorpDNS {
|
||||
dns = []wgcfg.IP{}
|
||||
dom = []string{}
|
||||
}
|
||||
cfg, err := nm.WGCfg(uflags, dns)
|
||||
if err != nil {
|
||||
log.Fatalf("WGCfg: %v\n", err)
|
||||
}
|
||||
|
||||
err = b.e.Reconfig(cfg, dom)
|
||||
if err != nil {
|
||||
b.logf("reconfig: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) enterState(newState State) {
|
||||
b.mu.Lock()
|
||||
state := b.state
|
||||
prefs := b.prefs
|
||||
b.mu.Unlock()
|
||||
|
||||
if state == newState {
|
||||
return
|
||||
}
|
||||
b.logf("Switching ipn state %v -> %v (WantRunning=%v)\n",
|
||||
state, newState, prefs.WantRunning)
|
||||
if b.notify != nil {
|
||||
b.send(Notify{State: &newState})
|
||||
}
|
||||
|
||||
b.state = newState
|
||||
switch newState {
|
||||
case NeedsLogin:
|
||||
b.blockEngineUpdates(true)
|
||||
fallthrough
|
||||
case Stopped:
|
||||
err := b.e.Reconfig(&wgcfg.Config{}, nil)
|
||||
if err != nil {
|
||||
b.logf("Reconfig(down): %v\n", err)
|
||||
}
|
||||
case Starting, NeedsMachineAuth:
|
||||
b.authReconfig()
|
||||
// Needed so that UpdateEndpoints can run
|
||||
b.e.RequestStatus()
|
||||
case Running:
|
||||
break
|
||||
default:
|
||||
b.logf("Weird: unknown newState %#v\n", newState)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (b *LocalBackend) nextState() State {
|
||||
b.assertClient()
|
||||
state := b.State()
|
||||
|
||||
if b.netMapCache == nil {
|
||||
if b.c.AuthCantContinue() {
|
||||
// Auth was interrupted or waiting for URL visit,
|
||||
// so it won't proceed without human help.
|
||||
return NeedsLogin
|
||||
} else {
|
||||
// Auth or map request needs to finish
|
||||
return state
|
||||
}
|
||||
} else if !b.prefs.WantRunning {
|
||||
return Stopped
|
||||
} else if e := b.netMapCache.Expiry; !e.IsZero() && time.Until(e) <= 0 {
|
||||
return NeedsLogin
|
||||
} else if b.netMapCache.MachineStatus != tailcfg.MachineAuthorized {
|
||||
// TODO(crawshaw): handle tailcfg.MachineInvalid
|
||||
return NeedsMachineAuth
|
||||
} else if state == NeedsMachineAuth {
|
||||
// (if we get here, we know MachineAuthorized == true)
|
||||
return Starting
|
||||
} else if state == Starting {
|
||||
if b.EngineStatus().NumLive > 0 {
|
||||
return Running
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
} else if state == Running {
|
||||
return Running
|
||||
} else {
|
||||
return Starting
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) RequestEngineStatus() {
|
||||
b.e.RequestStatus()
|
||||
}
|
||||
|
||||
// TODO(apenwarr): use a channel or something to prevent re-entrancy?
|
||||
// Or maybe just call the state machine from fewer places.
|
||||
func (b *LocalBackend) stateMachine() {
|
||||
b.enterState(b.nextState())
|
||||
}
|
||||
|
||||
func (b *LocalBackend) stopEngineAndWait() {
|
||||
b.logf("stopEngineAndWait...\n")
|
||||
b.e.Reconfig(&wgcfg.Config{}, nil)
|
||||
b.requestEngineStatusAndWait()
|
||||
b.logf("stopEngineAndWait: done.\n")
|
||||
}
|
||||
|
||||
// Requests the wgengine status, and does not return until the status
|
||||
// was delivered (to the usual callback).
|
||||
func (b *LocalBackend) requestEngineStatusAndWait() {
|
||||
b.logf("requestEngineStatusAndWait\n")
|
||||
|
||||
b.statusLock.Lock()
|
||||
go b.e.RequestStatus()
|
||||
b.logf("requestEngineStatusAndWait: waiting...\n")
|
||||
b.statusChanged.Wait() // temporarily releases lock while waiting
|
||||
b.logf("requestEngineStatusAndWait: got status update.\n")
|
||||
b.statusLock.Unlock()
|
||||
}
|
||||
|
||||
// NOTE(apenwarr): No easy way to persist logged-out status.
|
||||
// Maybe that's for the better; if someone logs out accidentally,
|
||||
// rebooting will fix it.
|
||||
func (b *LocalBackend) Logout() {
|
||||
b.assertClient()
|
||||
b.netMapCache = nil
|
||||
b.c.Logout()
|
||||
b.netMapCache = nil
|
||||
b.stateMachine()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) assertClient() {
|
||||
if b.c == nil {
|
||||
panic("LocalBackend.assertClient: b.c == nil")
|
||||
}
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logger"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
type NoArgs struct{}
|
||||
|
||||
type StartArgs struct {
|
||||
Opts Options
|
||||
}
|
||||
|
||||
type SetPrefsArgs struct {
|
||||
New Prefs
|
||||
}
|
||||
|
||||
type FakeExpireAfterArgs struct {
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// A command message sent to the server. Exactly one of these must be non-nil.
|
||||
type Command struct {
|
||||
Version string
|
||||
Quit *NoArgs
|
||||
Start *StartArgs
|
||||
StartLoginInteractive *NoArgs
|
||||
Logout *NoArgs
|
||||
SetPrefs *SetPrefsArgs
|
||||
RequestEngineStatus *NoArgs
|
||||
FakeExpireAfter *FakeExpireAfterArgs
|
||||
}
|
||||
|
||||
type BackendServer struct {
|
||||
logf logger.Logf
|
||||
b Backend // the Backend we are serving up
|
||||
sendNotifyMsg func(b []byte) // send a notification message
|
||||
GotQuit bool // a Quit command was received
|
||||
}
|
||||
|
||||
func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte)) *BackendServer {
|
||||
return &BackendServer{
|
||||
logf: logf,
|
||||
b: b,
|
||||
sendNotifyMsg: sendNotifyMsg,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BackendServer) send(n Notify) {
|
||||
n.Version = version.LONG
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(notify): %v\n%#v\n", err, n)
|
||||
}
|
||||
bs.sendNotifyMsg(b)
|
||||
}
|
||||
|
||||
// Inform the BackendServer of an incoming message.
|
||||
func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
cmd := Command{}
|
||||
if err := json.Unmarshal(b, &cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
return bs.GotCommand(&cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
if cmd.Version != version.LONG {
|
||||
vs := fmt.Sprintf("Version mismatch! frontend=%#v backend=%#v\n",
|
||||
cmd.Version, version.LONG)
|
||||
bs.logf("%s\n", vs)
|
||||
// ignore the command, but send a message back to the
|
||||
// caller so it can realize the version mismatch too.
|
||||
// We don't want to exit because it might cause a crash
|
||||
// loop, and restarting won't fix the problem.
|
||||
bs.send(Notify{
|
||||
ErrMessage: &vs,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
if cmd.Quit != nil {
|
||||
bs.GotQuit = true
|
||||
return errors.New("Quit command received")
|
||||
}
|
||||
|
||||
if c := cmd.Start; c != nil {
|
||||
opts := c.Opts
|
||||
opts.Notify = bs.send
|
||||
return bs.b.Start(opts)
|
||||
} else if c := cmd.StartLoginInteractive; c != nil {
|
||||
bs.b.StartLoginInteractive()
|
||||
return nil
|
||||
} else if c := cmd.Logout; c != nil {
|
||||
bs.b.Logout()
|
||||
return nil
|
||||
} else if c := cmd.SetPrefs; c != nil {
|
||||
bs.b.SetPrefs(c.New)
|
||||
return nil
|
||||
} else if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
} else if c := cmd.FakeExpireAfter; c != nil {
|
||||
bs.b.FakeExpireAfter(c.Duration)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *BackendServer) Reset() error {
|
||||
// Tell the backend we got a Logout command, which will cause it
|
||||
// to forget all its authentication information.
|
||||
return bs.GotCommand(&Command{Logout: &NoArgs{}})
|
||||
}
|
||||
|
||||
type BackendClient struct {
|
||||
logf logger.Logf
|
||||
sendCommandMsg func(b []byte)
|
||||
notify func(n Notify)
|
||||
}
|
||||
|
||||
func NewBackendClient(logf logger.Logf, sendCommandMsg func(b []byte)) *BackendClient {
|
||||
return &BackendClient{
|
||||
logf: logf,
|
||||
sendCommandMsg: sendCommandMsg,
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BackendClient) GotNotifyMsg(b []byte) {
|
||||
n := Notify{}
|
||||
if err := json.Unmarshal(b, &n); err != nil {
|
||||
log.Fatalf("BackendClient.Notify: cannot decode message")
|
||||
}
|
||||
if n.Version != version.LONG {
|
||||
vs := fmt.Sprintf("Version mismatch! frontend=%#v backend=%#v",
|
||||
version.LONG, n.Version)
|
||||
bc.logf("%s\n", vs)
|
||||
// delete anything in the notification except the version,
|
||||
// to prevent incorrect operation.
|
||||
n = Notify{
|
||||
Version: n.Version,
|
||||
ErrMessage: &vs,
|
||||
}
|
||||
}
|
||||
if bc.notify != nil {
|
||||
bc.notify(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BackendClient) send(cmd Command) {
|
||||
cmd.Version = version.LONG
|
||||
b, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(cmd): %v\n%#v\n", err, cmd)
|
||||
}
|
||||
bc.sendCommandMsg(b)
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Quit() error {
|
||||
bc.send(Command{Quit: &NoArgs{}})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Start(opts Options) error {
|
||||
bc.notify = opts.Notify
|
||||
opts.Notify = nil // server can't call our function pointer
|
||||
bc.send(Command{Start: &StartArgs{Opts: opts}})
|
||||
return nil // remote Start() errors must be handled remotely
|
||||
}
|
||||
|
||||
func (bc *BackendClient) StartLoginInteractive() {
|
||||
bc.send(Command{StartLoginInteractive: &NoArgs{}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Logout() {
|
||||
bc.send(Command{Logout: &NoArgs{}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) SetPrefs(new Prefs) {
|
||||
bc.send(Command{SetPrefs: &SetPrefsArgs{New: new}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) RequestEngineStatus() {
|
||||
bc.send(Command{RequestEngineStatus: &NoArgs{}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
|
||||
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
|
||||
}
|
||||
|
||||
const MSG_MAX = 1024 * 1024
|
||||
|
||||
// TODO(apenwarr): incremental json decode?
|
||||
// That would let us avoid storing the whole byte array uselessly in RAM.
|
||||
func ReadMsg(r io.Reader) ([]byte, error) {
|
||||
cb := make([]byte, 4)
|
||||
_, err := io.ReadFull(r, cb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := binary.LittleEndian.Uint32(cb)
|
||||
if n > 1024*1024 {
|
||||
return nil, fmt.Errorf("ipn.Read: message too large: %v bytes", n)
|
||||
}
|
||||
b := make([]byte, n)
|
||||
_, err = io.ReadFull(r, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// TODO(apenwarr): incremental json encode?
|
||||
// That would save RAM, at the expense of having to encode once so that
|
||||
// we can produce the initial byte count.
|
||||
func WriteMsg(w io.Writer, b []byte) error {
|
||||
cb := make([]byte, 4)
|
||||
if len(b) > MSG_MAX {
|
||||
return fmt.Errorf("ipn.Write: message too large: %v bytes", len(b))
|
||||
}
|
||||
binary.LittleEndian.PutUint32(cb, uint32(len(b)))
|
||||
n, err := w.Write(cb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 4 {
|
||||
return fmt.Errorf("ipn.Write: short write: %v bytes (wanted 4)", n)
|
||||
}
|
||||
n, err = w.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != len(b) {
|
||||
return fmt.Errorf("ipn.Write: short write: %v bytes (wanted %v)", n, len(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"tailscale.com/testy"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestReadWrite(t *testing.T) {
|
||||
testy.FixLogs(t)
|
||||
defer testy.UnfixLogs(t)
|
||||
|
||||
rc := testy.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
err := WriteMsg(&buf, []byte("Test string1"))
|
||||
if err != nil {
|
||||
t.Fatalf("write1: %v\n", err)
|
||||
}
|
||||
err = WriteMsg(&buf, []byte(""))
|
||||
if err != nil {
|
||||
t.Fatalf("write2: %v\n", err)
|
||||
}
|
||||
err = WriteMsg(&buf, []byte("Test3"))
|
||||
if err != nil {
|
||||
t.Fatalf("write3: %v\n", err)
|
||||
}
|
||||
|
||||
b, err := ReadMsg(&buf)
|
||||
if want, got := "Test string1", string(b); want != got {
|
||||
t.Fatalf("read1: %#v != %#v\n", want, got)
|
||||
}
|
||||
b, err = ReadMsg(&buf)
|
||||
if want, got := "", string(b); want != got {
|
||||
t.Fatalf("read2: %#v != %#v\n", want, got)
|
||||
}
|
||||
b, err = ReadMsg(&buf)
|
||||
if want, got := "Test3", string(b); want != got {
|
||||
t.Fatalf("read3: %#v != %#v\n", want, got)
|
||||
}
|
||||
|
||||
b, err = ReadMsg(&buf)
|
||||
if err == nil {
|
||||
t.Fatalf("read4: expected error, got %#v\n", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientServer(t *testing.T) {
|
||||
testy.FixLogs(t)
|
||||
defer testy.UnfixLogs(t)
|
||||
|
||||
rc := testy.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
|
||||
b := &FakeBackend{}
|
||||
var bs *BackendServer
|
||||
var bc *BackendClient
|
||||
serverToClientCh := make(chan []byte, 16)
|
||||
defer close(serverToClientCh)
|
||||
go func() {
|
||||
for b := range serverToClientCh {
|
||||
bc.GotNotifyMsg(b)
|
||||
}
|
||||
}()
|
||||
serverToClient := func(b []byte) {
|
||||
serverToClientCh <- append([]byte{}, b...)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
bs.GotCommandMsg(b)
|
||||
}
|
||||
slogf := func(fmt string, args ...interface{}) {
|
||||
t.Logf("s: "+fmt, args...)
|
||||
}
|
||||
clogf := func(fmt string, args ...interface{}) {
|
||||
t.Logf("c: "+fmt, args...)
|
||||
}
|
||||
bs = NewBackendServer(slogf, b, serverToClient)
|
||||
bc = NewBackendClient(clogf, clientToServer)
|
||||
|
||||
ch := make(chan Notify, 256)
|
||||
h, err := NewHandle(bc, clogf, Options{
|
||||
ServerURL: "http://example.com/fake",
|
||||
Notify: func(n Notify) {
|
||||
ch <- n
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewHandle error: %v\n", err)
|
||||
}
|
||||
|
||||
notes := Notify{}
|
||||
nn := []Notify{}
|
||||
processNote := func(n Notify) {
|
||||
nn = append(nn, n)
|
||||
if n.State != nil {
|
||||
t.Logf("state change: %v", *n.State)
|
||||
notes.State = n.State
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
notes.Prefs = n.Prefs
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
notes.NetMap = n.NetMap
|
||||
}
|
||||
if n.Engine != nil {
|
||||
notes.Engine = n.Engine
|
||||
}
|
||||
if n.BrowseToURL != nil {
|
||||
notes.BrowseToURL = n.BrowseToURL
|
||||
}
|
||||
}
|
||||
notesState := func() State {
|
||||
if notes.State != nil {
|
||||
return *notes.State
|
||||
}
|
||||
return NoState
|
||||
}
|
||||
|
||||
flushUntil := func(wantFlush State) {
|
||||
t.Helper()
|
||||
timer := time.NewTimer(1 * time.Second)
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case n := <-ch:
|
||||
processNote(n)
|
||||
if notesState() == wantFlush {
|
||||
break loop
|
||||
}
|
||||
case <-timer.C:
|
||||
t.Fatalf("timeout waiting for state %v, got %v", wantFlush, notes.State)
|
||||
}
|
||||
}
|
||||
timer.Stop()
|
||||
loop2:
|
||||
for {
|
||||
select {
|
||||
case n := <-ch:
|
||||
processNote(n)
|
||||
default:
|
||||
break loop2
|
||||
}
|
||||
}
|
||||
if got, want := h.State(), notesState(); got != want {
|
||||
t.Errorf("h.State()=%v, notes.State=%v (on flush until %v)\n", got, want, wantFlush)
|
||||
}
|
||||
}
|
||||
|
||||
flushUntil(NeedsLogin)
|
||||
|
||||
h.StartLoginInteractive()
|
||||
flushUntil(Running)
|
||||
if notes.NetMap == nil && h.NetMap() != nil {
|
||||
t.Errorf("notes.NetMap == nil while h.NetMap != nil\nnotes:\n%v", nn)
|
||||
}
|
||||
|
||||
h.UpdatePrefs(func(p Prefs) Prefs {
|
||||
p.WantRunning = false
|
||||
return p
|
||||
})
|
||||
flushUntil(Stopped)
|
||||
|
||||
h.Logout()
|
||||
flushUntil(NeedsLogin)
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
)
|
||||
|
||||
type Prefs struct {
|
||||
RouteAll bool
|
||||
AllowSingleHosts bool
|
||||
CorpDNS bool
|
||||
WantRunning bool
|
||||
NotepadURLs bool
|
||||
UsePacketFilter bool
|
||||
|
||||
// The Persist field is named 'Config' in the file for backward
|
||||
// compatibility with earlier versions.
|
||||
// TODO(apenwarr): We should move this out of here, it's not a pref.
|
||||
// We can maybe do that once we're sure which module should persist
|
||||
// it (backend or frontend?)
|
||||
Persist *controlclient.Persist `json:"Config"`
|
||||
}
|
||||
|
||||
func (uc *Prefs) Pretty() string {
|
||||
var ucp string
|
||||
if uc.Persist != nil {
|
||||
ucp = uc.Persist.Pretty()
|
||||
} else {
|
||||
ucp = "Persist=nil"
|
||||
}
|
||||
return fmt.Sprintf("Prefs{ra=%v mesh=%v dns=%v want=%v notepad=%v %v}",
|
||||
uc.RouteAll, uc.AllowSingleHosts, uc.CorpDNS, uc.WantRunning,
|
||||
uc.NotepadURLs, ucp)
|
||||
}
|
||||
|
||||
func (uc *Prefs) ToBytes() []byte {
|
||||
data, err := json.MarshalIndent(uc, "", "\t")
|
||||
if err != nil {
|
||||
log.Fatalf("Prefs marshal: %v\n", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (uc *Prefs) Equals(uc2 *Prefs) bool {
|
||||
b1 := uc.ToBytes()
|
||||
b2 := uc2.ToBytes()
|
||||
return bytes.Equal(b1, b2)
|
||||
}
|
||||
|
||||
func NewPrefs() Prefs {
|
||||
return Prefs{
|
||||
// Provide default values for options which are normally
|
||||
// true, but might be missing from the json data for any
|
||||
// reason. The json can still override them to false.
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
WantRunning: true,
|
||||
UsePacketFilter: true,
|
||||
}
|
||||
}
|
||||
|
||||
func PrefsFromBytes(b []byte, enforceDefaults bool) (Prefs, error) {
|
||||
uc := NewPrefs()
|
||||
if len(b) == 0 {
|
||||
return uc, nil
|
||||
}
|
||||
persist := &controlclient.Persist{}
|
||||
err := json.Unmarshal(b, persist)
|
||||
if err == nil && (persist.Provider != "" || persist.LoginName != "") {
|
||||
// old-style relaynode config; import it
|
||||
uc.Persist = persist
|
||||
} else {
|
||||
err = json.Unmarshal(b, &uc)
|
||||
if err != nil {
|
||||
log.Printf("Prefs parse: %v: %v\n", err, b)
|
||||
}
|
||||
}
|
||||
if enforceDefaults {
|
||||
uc.RouteAll = true
|
||||
uc.AllowSingleHosts = true
|
||||
}
|
||||
return uc, err
|
||||
}
|
||||
|
||||
func (uc *Prefs) Copy() *Prefs {
|
||||
uc2, err := PrefsFromBytes(uc.ToBytes(), false)
|
||||
if err != nil {
|
||||
log.Fatalf("Prefs was uncopyable: %v\n", err)
|
||||
}
|
||||
return &uc2
|
||||
}
|
||||
|
||||
func LoadPrefs(filename string, enforceDefaults bool) Prefs {
|
||||
log.Printf("Loading prefs %v\n", filename)
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
uc := NewPrefs()
|
||||
if err != nil {
|
||||
log.Printf("Read: %v: %v\n", filename, err)
|
||||
goto fail
|
||||
}
|
||||
uc, err = PrefsFromBytes(data, enforceDefaults)
|
||||
if err != nil {
|
||||
log.Printf("Parse: %v: %v\n", filename, err)
|
||||
goto fail
|
||||
}
|
||||
goto post
|
||||
fail:
|
||||
log.Printf("failed to load config. Generating a new one.\n")
|
||||
uc = NewPrefs()
|
||||
uc.WantRunning = true
|
||||
post:
|
||||
// Update: we changed our minds :)
|
||||
// Versabank would like to persist the setting across reboots, for now,
|
||||
// because they don't fully trust the system and want to be able to
|
||||
// leave it turned off when not in use. Eventually we need to make
|
||||
// all motivation for this go away.
|
||||
if false {
|
||||
// Usability note: we always want WantRunning = true on startup.
|
||||
// That way, if someone accidentally disables their VPN and doesn't
|
||||
// know how, rebooting will fix it.
|
||||
// We still persist WantRunning just in case we change our minds on
|
||||
// this topic.
|
||||
uc.WantRunning = true
|
||||
}
|
||||
log.Printf("Loaded prefs %v %v\n", filename, uc.Pretty())
|
||||
return uc
|
||||
}
|
||||
|
||||
func SavePrefs(filename string, uc *Prefs) {
|
||||
log.Printf("Saving prefs %v %v\n", filename, uc.Pretty())
|
||||
data := uc.ToBytes()
|
||||
os.MkdirAll(filepath.Dir(filename), 0700)
|
||||
if err := atomicfile.WriteFile(filename, data, 0666); err != nil {
|
||||
log.Printf("SavePrefs: %v\n", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
)
|
||||
|
||||
func checkPrefs(t *testing.T, p Prefs) {
|
||||
var err error
|
||||
var p2, p2c Prefs
|
||||
var p2b Prefs
|
||||
|
||||
pp := p.Pretty()
|
||||
if pp == "" {
|
||||
t.Fatalf("default p.Pretty() failed\n")
|
||||
}
|
||||
t.Logf("\npp: %#v\n", pp)
|
||||
b := p.ToBytes()
|
||||
if len(b) == 0 {
|
||||
t.Fatalf("default p.ToBytes() failed\n")
|
||||
}
|
||||
if p != p {
|
||||
t.Fatalf("p != p\n")
|
||||
}
|
||||
p2 = p
|
||||
p2.RouteAll = true
|
||||
if p == p2 {
|
||||
t.Fatalf("p == p2\n")
|
||||
}
|
||||
p2b, err = PrefsFromBytes(p2.ToBytes(), false)
|
||||
if err != nil {
|
||||
t.Fatalf("PrefsFromBytes(p2) failed\n")
|
||||
}
|
||||
p2p := p2.Pretty()
|
||||
p2bp := p2b.Pretty()
|
||||
t.Logf("\np2p: %#v\np2bp: %#v\n", p2p, p2bp)
|
||||
if p2p != p2bp {
|
||||
t.Fatalf("p2p != p2bp\n%#v\n%#v\n", p2p, p2bp)
|
||||
}
|
||||
if !p2.Equals(&p2b) {
|
||||
t.Fatalf("p2 != p2b\n%#v\n%#v\n", p2, p2b)
|
||||
}
|
||||
p2c = *p2.Copy()
|
||||
if !p2b.Equals(&p2c) {
|
||||
t.Fatalf("p2b != p2c\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicPrefs(t *testing.T) {
|
||||
p := Prefs{}
|
||||
checkPrefs(t, p)
|
||||
}
|
||||
|
||||
func TestPrefsPersist(t *testing.T) {
|
||||
c := controlclient.Persist{
|
||||
LoginName: "test@example.com",
|
||||
}
|
||||
p := Prefs{
|
||||
CorpDNS: true,
|
||||
Persist: &c,
|
||||
}
|
||||
checkPrefs(t, p)
|
||||
}
|
||||
Reference in New Issue
Block a user