mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
388 lines
10 KiB
Go
388 lines
10 KiB
Go
// 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"
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"time"
|
|
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/types/structs"
|
|
"tailscale.com/version"
|
|
)
|
|
|
|
type readOnlyContextKey struct{}
|
|
|
|
// IsReadonlyContext reports whether ctx is a read-only context, as currently used
|
|
// by Unix non-root users running the "tailscale" CLI command. They can run "status",
|
|
// but not much else.
|
|
func IsReadonlyContext(ctx context.Context) bool {
|
|
return ctx.Value(readOnlyContextKey{}) != nil
|
|
}
|
|
|
|
// ReadonlyContextOf returns ctx wrapped with a context value that
|
|
// will make IsReadonlyContext reports true.
|
|
func ReadonlyContextOf(ctx context.Context) context.Context {
|
|
if IsReadonlyContext(ctx) {
|
|
return ctx
|
|
}
|
|
return context.WithValue(ctx, readOnlyContextKey{}, readOnlyContextKey{})
|
|
}
|
|
|
|
var jsonEscapedZero = []byte(`\u0000`)
|
|
|
|
type NoArgs struct{}
|
|
|
|
type StartArgs struct {
|
|
Opts Options
|
|
}
|
|
|
|
type SetPrefsArgs struct {
|
|
New *Prefs
|
|
}
|
|
|
|
type FakeExpireAfterArgs struct {
|
|
Duration time.Duration
|
|
}
|
|
|
|
type PingArgs struct {
|
|
IP string
|
|
UseTSMP bool
|
|
}
|
|
|
|
// Command is a command message that is JSON encoded and sent by a
|
|
// frontend to a backend.
|
|
type Command struct {
|
|
_ structs.Incomparable
|
|
|
|
// Version is the binary version of the frontend (the client).
|
|
Version string
|
|
|
|
// AllowVersionSkew controls whether it's permitted for the
|
|
// client and server to have a different version. The default
|
|
// (false) means to be strict.
|
|
AllowVersionSkew bool
|
|
|
|
// Exactly one of the following must be non-nil.
|
|
Quit *NoArgs
|
|
Start *StartArgs
|
|
StartLoginInteractive *NoArgs
|
|
Login *tailcfg.Oauth2Token
|
|
Logout *NoArgs
|
|
SetPrefs *SetPrefsArgs
|
|
EditPrefs *MaskedPrefs
|
|
RequestEngineStatus *NoArgs
|
|
RequestStatus *NoArgs
|
|
FakeExpireAfter *FakeExpireAfterArgs
|
|
Ping *PingArgs
|
|
}
|
|
|
|
type BackendServer struct {
|
|
logf logger.Logf
|
|
b Backend // the Backend we are serving up
|
|
sendNotifyMsg func(jsonMsg []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", err, n)
|
|
}
|
|
if bytes.Contains(b, jsonEscapedZero) {
|
|
log.Printf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
|
|
}
|
|
bs.sendNotifyMsg(b)
|
|
}
|
|
|
|
func (bs *BackendServer) SendErrorMessage(msg string) {
|
|
bs.send(Notify{ErrMessage: &msg})
|
|
}
|
|
|
|
// SendInUseOtherUserErrorMessage sends a Notify message to the client that
|
|
// both sets the state to 'InUseOtherUser' and sets the associated reason
|
|
// to msg.
|
|
func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) {
|
|
inUse := InUseOtherUser
|
|
bs.send(Notify{
|
|
State: &inUse,
|
|
ErrMessage: &msg,
|
|
})
|
|
}
|
|
|
|
// GotCommandMsg parses the incoming message b as a JSON Command and
|
|
// calls GotCommand with it.
|
|
func (bs *BackendServer) GotCommandMsg(ctx context.Context, b []byte) error {
|
|
cmd := &Command{}
|
|
if len(b) == 0 {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(b, cmd); err != nil {
|
|
return err
|
|
}
|
|
return bs.GotCommand(ctx, cmd)
|
|
}
|
|
|
|
func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error {
|
|
cmd.Version = version.Long
|
|
return bs.GotCommand(ctx, cmd)
|
|
}
|
|
|
|
// ErrMsgPermissionDenied is the Notify.ErrMessage value used an
|
|
// operation was done from a user/context that didn't have permission.
|
|
const ErrMsgPermissionDenied = "permission denied"
|
|
|
|
func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
|
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
|
|
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
|
|
cmd.Version, version.Long)
|
|
bs.logf("%s", 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
|
|
}
|
|
|
|
// TODO(bradfitz): finish plumbing context down to all the methods below;
|
|
// currently we just check for read-only contexts in this method and
|
|
// then never use contexts again.
|
|
|
|
// Actions permitted with a read-only context:
|
|
if c := cmd.RequestEngineStatus; c != nil {
|
|
bs.b.RequestEngineStatus()
|
|
return nil
|
|
} else if c := cmd.Ping; c != nil {
|
|
bs.b.Ping(c.IP, c.UseTSMP)
|
|
return nil
|
|
}
|
|
|
|
if IsReadonlyContext(ctx) {
|
|
msg := ErrMsgPermissionDenied
|
|
bs.send(Notify{ErrMessage: &msg})
|
|
return nil
|
|
}
|
|
|
|
if cmd.Quit != nil {
|
|
bs.GotQuit = true
|
|
return errors.New("Quit command received")
|
|
} else if c := cmd.Start; c != nil {
|
|
bs.b.SetNotifyCallback(bs.send)
|
|
opts := c.Opts
|
|
return bs.b.Start(opts)
|
|
} else if c := cmd.StartLoginInteractive; c != nil {
|
|
bs.b.StartLoginInteractive()
|
|
return nil
|
|
} else if c := cmd.Login; c != nil {
|
|
bs.b.Login(c)
|
|
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.EditPrefs; c != nil {
|
|
bs.b.EditPrefs(c)
|
|
return nil
|
|
} else if c := cmd.FakeExpireAfter; c != nil {
|
|
bs.b.FakeExpireAfter(c.Duration)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("BackendServer.Do: no command specified")
|
|
}
|
|
|
|
func (bs *BackendServer) Reset(ctx context.Context) error {
|
|
// Tell the backend we got a Logout command, which will cause it
|
|
// to forget all its authentication information.
|
|
return bs.GotFakeCommand(ctx, &Command{Logout: &NoArgs{}})
|
|
}
|
|
|
|
type BackendClient struct {
|
|
logf logger.Logf
|
|
sendCommandMsg func(jsonb []byte)
|
|
notify func(Notify)
|
|
|
|
// AllowVersionSkew controls whether to allow mismatched
|
|
// frontend & backend versions.
|
|
AllowVersionSkew bool
|
|
}
|
|
|
|
func NewBackendClient(logf logger.Logf, sendCommandMsg func(jsonb []byte)) *BackendClient {
|
|
return &BackendClient{
|
|
logf: logf,
|
|
sendCommandMsg: sendCommandMsg,
|
|
}
|
|
}
|
|
|
|
func (bc *BackendClient) GotNotifyMsg(b []byte) {
|
|
if len(b) == 0 {
|
|
// not interesting
|
|
return
|
|
}
|
|
if bytes.Contains(b, jsonEscapedZero) {
|
|
log.Printf("[unexpected] zero byte in BackendClient.GotNotifyMsg message: %q", b)
|
|
}
|
|
n := Notify{}
|
|
if err := json.Unmarshal(b, &n); err != nil {
|
|
log.Fatalf("BackendClient.Notify: cannot decode message (length=%d)\n%#v", len(b), string(b))
|
|
}
|
|
if n.Version != version.Long && !bc.AllowVersionSkew {
|
|
vs := fmt.Sprintf("GotNotify: Version mismatch! frontend=%#v backend=%#v",
|
|
version.Long, n.Version)
|
|
bc.logf("%s", 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)
|
|
}
|
|
if bytes.Contains(b, jsonEscapedZero) {
|
|
log.Printf("[unexpected] zero byte in BackendClient.send command: %q", b)
|
|
}
|
|
bc.sendCommandMsg(b)
|
|
}
|
|
|
|
func (bc *BackendClient) SetNotifyCallback(fn func(Notify)) {
|
|
bc.notify = fn
|
|
}
|
|
|
|
func (bc *BackendClient) Quit() error {
|
|
bc.send(Command{Quit: &NoArgs{}})
|
|
return nil
|
|
}
|
|
|
|
func (bc *BackendClient) Start(opts Options) error {
|
|
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) Login(token *tailcfg.Oauth2Token) {
|
|
bc.send(Command{Login: token})
|
|
}
|
|
|
|
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) EditPrefs(mp *MaskedPrefs) {
|
|
bc.send(Command{EditPrefs: mp})
|
|
}
|
|
|
|
func (bc *BackendClient) RequestEngineStatus() {
|
|
bc.send(Command{RequestEngineStatus: &NoArgs{}})
|
|
}
|
|
|
|
func (bc *BackendClient) RequestStatus() {
|
|
bc.send(Command{AllowVersionSkew: true, RequestStatus: &NoArgs{}})
|
|
}
|
|
|
|
func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
|
|
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
|
|
}
|
|
|
|
func (bc *BackendClient) Ping(ip string, useTSMP bool) {
|
|
bc.send(Command{Ping: &PingArgs{
|
|
IP: ip,
|
|
UseTSMP: useTSMP,
|
|
}})
|
|
}
|
|
|
|
// MaxMessageSize is the maximum message size, in bytes.
|
|
const MaxMessageSize = 10 << 20
|
|
|
|
// 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 > MaxMessageSize {
|
|
return nil, fmt.Errorf("ipn.Read: message too large: %v bytes", n)
|
|
}
|
|
b := make([]byte, n)
|
|
nn, err := io.ReadFull(r, b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if nn != int(n) {
|
|
return nil, fmt.Errorf("ipn.Read: expected %v bytes, got %v", n, nn)
|
|
}
|
|
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 {
|
|
// TODO(bradfitz): this does two writes to w, which likely
|
|
// does two writes on the wire, two frame generations, etc. We
|
|
// should take a concrete buffered type, or use a sync.Pool to
|
|
// allocate a buf and do one write.
|
|
cb := make([]byte, 4)
|
|
if len(b) > MaxMessageSize {
|
|
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
|
|
}
|