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.
319 lines
8.9 KiB
Go
319 lines
8.9 KiB
Go
10 months ago
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
|
||
|
package ipnlocal
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"net"
|
||
|
"net/netip"
|
||
|
"os"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"tailscale.com/ipn"
|
||
|
"tailscale.com/logtail/backoff"
|
||
|
"tailscale.com/tailcfg"
|
||
|
"tailscale.com/tailfs"
|
||
|
"tailscale.com/types/logger"
|
||
|
"tailscale.com/types/netmap"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// TailfsLocalPort is the port on which the Tailfs listens for location
|
||
|
// connections on quad 100.
|
||
|
TailfsLocalPort = 8080
|
||
|
|
||
|
tailfsSharesStateKey = ipn.StateKey("_tailfs-shares")
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
shareNameRegex = regexp.MustCompile(`^[a-z0-9_\(\) ]+$`)
|
||
|
errInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
|
||
|
)
|
||
|
|
||
|
// TailfsSharingEnabled reports whether sharing to remote nodes via tailfs is
|
||
|
// enabled. This is currently based on checking for the tailfs:share node
|
||
|
// attribute.
|
||
|
func (b *LocalBackend) TailfsSharingEnabled() bool {
|
||
|
b.mu.Lock()
|
||
|
defer b.mu.Unlock()
|
||
|
return b.tailfsSharingEnabledLocked()
|
||
|
}
|
||
|
|
||
|
func (b *LocalBackend) tailfsSharingEnabledLocked() bool {
|
||
|
return b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrsTailfsSharingEnabled)
|
||
|
}
|
||
|
|
||
|
// TailfsSetFileServerAddr tells tailfs to use the given address for connecting
|
||
|
// to the tailfs.FileServer that's exposing local files as an unprivileged
|
||
|
// user.
|
||
|
func (b *LocalBackend) TailfsSetFileServerAddr(addr string) error {
|
||
|
b.mu.Lock()
|
||
|
fs := b.tailfsForRemote
|
||
|
b.mu.Unlock()
|
||
|
if fs == nil {
|
||
|
return errors.New("tailfs not enabled")
|
||
|
}
|
||
|
|
||
|
fs.SetFileServerAddr(addr)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// TailfsAddShare adds the given share if no share with that name exists, or
|
||
|
// replaces the existing share if one with the same name already exists.
|
||
|
// To avoid potential incompatibilities across file systems, share names are
|
||
|
// limited to alphanumeric characters and the underscore _.
|
||
|
func (b *LocalBackend) TailfsAddShare(share *tailfs.Share) error {
|
||
|
var err error
|
||
|
share.Name, err = normalizeShareName(share.Name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
b.mu.Lock()
|
||
|
shares, err := b.tailfsAddShareLocked(share)
|
||
|
b.mu.Unlock()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
b.tailfsNotifyShares(shares)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// normalizeShareName normalizes the given share name and returns an error if
|
||
|
// it contains any disallowed characters.
|
||
|
func normalizeShareName(name string) (string, error) {
|
||
|
// Force all share names to lowercase to avoid potential incompatibilities
|
||
|
// with clients that don't support case-sensitive filenames.
|
||
|
name = strings.ToLower(name)
|
||
|
|
||
|
// Trim whitespace
|
||
|
name = strings.TrimSpace(name)
|
||
|
|
||
|
if !shareNameRegex.MatchString(name) {
|
||
|
return "", errInvalidShareName
|
||
|
}
|
||
|
|
||
|
return name, nil
|
||
|
}
|
||
|
|
||
|
func (b *LocalBackend) tailfsAddShareLocked(share *tailfs.Share) (map[string]string, error) {
|
||
|
if b.tailfsForRemote == nil {
|
||
|
return nil, errors.New("tailfs not enabled")
|
||
|
}
|
||
|
|
||
|
shares, err := b.tailfsGetSharesLocked()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
shares[share.Name] = share
|
||
|
data, err := json.Marshal(shares)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("marshal: %w", err)
|
||
|
}
|
||
|
err = b.store.WriteState(tailfsSharesStateKey, data)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("write state: %w", err)
|
||
|
}
|
||
|
b.tailfsForRemote.SetShares(shares)
|
||
|
|
||
|
return shareNameMap(shares), nil
|
||
|
}
|
||
|
|
||
|
// TailfsRemoveShare removes the named share. Share names are forced to
|
||
|
// lowercase.
|
||
|
func (b *LocalBackend) TailfsRemoveShare(name string) error {
|
||
|
// Force all share names to lowercase to avoid potential incompatibilities
|
||
|
// with clients that don't support case-sensitive filenames.
|
||
|
name = strings.ToLower(name)
|
||
|
|
||
|
b.mu.Lock()
|
||
|
shares, err := b.tailfsRemoveShareLocked(name)
|
||
|
b.mu.Unlock()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
b.tailfsNotifyShares(shares)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (b *LocalBackend) tailfsRemoveShareLocked(name string) (map[string]string, error) {
|
||
|
if b.tailfsForRemote == nil {
|
||
|
return nil, errors.New("tailfs not enabled")
|
||
|
}
|
||
|
|
||
|
shares, err := b.tailfsGetSharesLocked()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
_, shareExists := shares[name]
|
||
|
if !shareExists {
|
||
|
return nil, os.ErrNotExist
|
||
|
}
|
||
|
delete(shares, name)
|
||
|
data, err := json.Marshal(shares)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("marshal: %w", err)
|
||
|
}
|
||
|
err = b.store.WriteState(tailfsSharesStateKey, data)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("write state: %w", err)
|
||
|
}
|
||
|
b.tailfsForRemote.SetShares(shares)
|
||
|
|
||
|
return shareNameMap(shares), nil
|
||
|
}
|
||
|
|
||
|
func shareNameMap(sharesByName map[string]*tailfs.Share) map[string]string {
|
||
|
sharesMap := make(map[string]string, len(sharesByName))
|
||
|
for _, share := range sharesByName {
|
||
|
sharesMap[share.Name] = share.Path
|
||
|
}
|
||
|
return sharesMap
|
||
|
}
|
||
|
|
||
|
// tailfsNotifyShares notifies IPN bus listeners (e.g. Mac Application process)
|
||
|
// about the latest set of shares, supplied as a map of name -> directory.
|
||
|
func (b *LocalBackend) tailfsNotifyShares(shares map[string]string) {
|
||
|
b.send(ipn.Notify{TailfsShares: shares})
|
||
|
}
|
||
|
|
||
|
// tailfsNotifyCurrentSharesLocked sends an ipn.Notify with the current set of
|
||
|
// tailfs shares.
|
||
|
func (b *LocalBackend) tailfsNotifyCurrentSharesLocked() {
|
||
|
shares, err := b.tailfsGetSharesLocked()
|
||
|
if err != nil {
|
||
|
b.logf("error notifying current tailfs shares: %v", err)
|
||
|
return
|
||
|
}
|
||
|
// Do the below on a goroutine to avoid deadlocking on b.mu in b.send().
|
||
|
go b.tailfsNotifyShares(shareNameMap(shares))
|
||
|
}
|
||
|
|
||
|
// TailfsGetShares() returns the current set of shares from the state store.
|
||
|
func (b *LocalBackend) TailfsGetShares() (map[string]*tailfs.Share, error) {
|
||
|
b.mu.Lock()
|
||
|
defer b.mu.Unlock()
|
||
|
|
||
|
return b.tailfsGetSharesLocked()
|
||
|
}
|
||
|
|
||
|
func (b *LocalBackend) tailfsGetSharesLocked() (map[string]*tailfs.Share, error) {
|
||
|
data, err := b.store.ReadState(tailfsSharesStateKey)
|
||
|
if err != nil {
|
||
|
if errors.Is(err, ipn.ErrStateNotExist) {
|
||
|
return make(map[string]*tailfs.Share), nil
|
||
|
}
|
||
|
return nil, fmt.Errorf("read state: %w", err)
|
||
|
}
|
||
|
|
||
|
var shares map[string]*tailfs.Share
|
||
|
err = json.Unmarshal(data, &shares)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("unmarshal: %w", err)
|
||
|
}
|
||
|
|
||
|
return shares, nil
|
||
|
}
|
||
|
|
||
|
// updateTailfsListenersLocked creates listeners on the local Tailfs port.
|
||
|
// This is needed to properly route local traffic when using kernel networking
|
||
|
// mode.
|
||
|
func (b *LocalBackend) updateTailfsListenersLocked() {
|
||
|
if b.netMap == nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
addrs := b.netMap.GetAddresses()
|
||
|
oldListeners := b.tailfsListeners
|
||
|
newListeners := make(map[netip.AddrPort]*localListener, addrs.Len())
|
||
|
for i := range addrs.LenIter() {
|
||
|
if fs, ok := b.sys.TailfsForLocal.GetOK(); ok {
|
||
|
addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), TailfsLocalPort)
|
||
|
if sl, ok := b.tailfsListeners[addrPort]; ok {
|
||
|
newListeners[addrPort] = sl
|
||
|
delete(oldListeners, addrPort)
|
||
|
continue // already listening
|
||
|
}
|
||
|
|
||
|
sl := b.newTailfsListener(context.Background(), fs, addrPort, b.logf)
|
||
|
newListeners[addrPort] = sl
|
||
|
go sl.Run()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// At this point, anything left in oldListeners can be stopped.
|
||
|
for _, sl := range oldListeners {
|
||
|
sl.cancel()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// newTailfsListener returns a listener for local connections to a tailfs
|
||
|
// WebDAV FileSystem.
|
||
|
func (b *LocalBackend) newTailfsListener(ctx context.Context, fs *tailfs.FileSystemForLocal, ap netip.AddrPort, logf logger.Logf) *localListener {
|
||
|
ctx, cancel := context.WithCancel(ctx)
|
||
|
return &localListener{
|
||
|
b: b,
|
||
|
ap: ap,
|
||
|
ctx: ctx,
|
||
|
cancel: cancel,
|
||
|
logf: logf,
|
||
|
|
||
|
handler: func(conn net.Conn) error {
|
||
|
return fs.HandleConn(conn, conn.RemoteAddr())
|
||
|
},
|
||
|
bo: backoff.NewBackoff(fmt.Sprintf("tailfs-listener-%d", ap.Port()), logf, 30*time.Second),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// updateTailfsPeersLocked sets all applicable peers from the netmap as tailfs
|
||
|
// remotes.
|
||
|
func (b *LocalBackend) updateTailfsPeersLocked(nm *netmap.NetworkMap) {
|
||
|
fs, ok := b.sys.TailfsForLocal.GetOK()
|
||
|
if !ok {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
tailfsRemotes := make([]*tailfs.Remote, 0, len(nm.Peers))
|
||
|
for _, p := range nm.Peers {
|
||
|
peerID := p.ID()
|
||
|
url := fmt.Sprintf("%s/%s", peerAPIBase(nm, p), tailfsPrefix[1:])
|
||
|
tailfsRemotes = append(tailfsRemotes, &tailfs.Remote{
|
||
|
Name: p.DisplayName(false),
|
||
|
URL: url,
|
||
|
Available: func() bool {
|
||
|
// TODO(oxtoacart): need to figure out a performant and reliable way to only
|
||
|
// show the peers that have shares to which we have access
|
||
|
// This will require work on the control server to transmit the inverse
|
||
|
// of the "tailscale.com/cap/tailfs" capability.
|
||
|
// For now, at least limit it only to nodes that are online.
|
||
|
// Note, we have to iterate the latest netmap because the peer we got from the first iteration may not be it
|
||
|
b.mu.Lock()
|
||
|
latestNetMap := b.netMap
|
||
|
b.mu.Unlock()
|
||
|
|
||
|
for _, candidate := range latestNetMap.Peers {
|
||
|
if candidate.ID() == peerID {
|
||
|
online := candidate.Online()
|
||
|
// TODO(oxtoacart): for some reason, this correctly
|
||
|
// catches when a node goes from offline to online,
|
||
|
// but not the other way around...
|
||
|
return online != nil && *online
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// peer not found, must not be available
|
||
|
return false
|
||
|
},
|
||
|
})
|
||
|
}
|
||
|
fs.SetRemotes(b.netMap.Domain, tailfsRemotes, &tailfsTransport{b: b})
|
||
|
}
|