@ -5,6 +5,7 @@ package tailfsimpl
import (
import (
"bufio"
"bufio"
"context"
"encoding/hex"
"encoding/hex"
"fmt"
"fmt"
"log"
"log"
@ -15,6 +16,7 @@ import (
"net/url"
"net/url"
"os"
"os"
"os/exec"
"os/exec"
"os/user"
"strings"
"strings"
"sync"
"sync"
"time"
"time"
@ -66,12 +68,21 @@ func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
func ( s * FileSystemForRemote ) SetShares ( shares map [ string ] * tailfs . Share ) {
func ( s * FileSystemForRemote ) SetShares ( shares map [ string ] * tailfs . Share ) {
userServers := make ( map [ string ] * userServer )
userServers := make ( map [ string ] * userServer )
if tailfs . AllowShareAs ( ) {
if tailfs . AllowShareAs ( ) {
// set up per-user server
// Set up per-user server by running the current executable as an
// unprivileged user in order to avoid privilege escalation.
executable , err := os . Executable ( )
if err != nil {
s . logf ( "can't find executable: %v" , err )
return
}
for _ , share := range shares {
for _ , share := range shares {
p , found := userServers [ share . As ]
p , found := userServers [ share . As ]
if ! found {
if ! found {
p = & userServer {
p = & userServer {
logf : s . logf ,
logf : s . logf ,
username : share . As ,
executable : executable ,
}
}
userServers [ share . As ] = p
userServers [ share . As ] = p
}
}
@ -229,6 +240,8 @@ func (s *FileSystemForRemote) Close() error {
type userServer struct {
type userServer struct {
logf logger . Logf
logf logger . Logf
shares [ ] * tailfs . Share
shares [ ] * tailfs . Share
username string
executable string
// mu guards the below values. Acquire a write lock before updating any of
// mu guards the below values. Acquire a write lock before updating any of
// them, acquire a read lock before reading any of them.
// them, acquire a read lock before reading any of them.
@ -251,11 +264,6 @@ func (s *userServer) Close() error {
}
}
func ( s * userServer ) runLoop ( ) {
func ( s * userServer ) runLoop ( ) {
executable , err := os . Executable ( )
if err != nil {
s . logf ( "can't find executable: %v" , err )
return
}
maxSleepTime := 30 * time . Second
maxSleepTime := 30 * time . Second
consecutiveFailures := float64 ( 0 )
consecutiveFailures := float64 ( 0 )
var timeOfLastFailure time . Time
var timeOfLastFailure time . Time
@ -267,7 +275,7 @@ func (s *userServer) runLoop() {
return
return
}
}
err := s . run ( executable )
err := s . run ( )
now := time . Now ( )
now := time . Now ( )
timeSinceLastFailure := now . Sub ( timeOfLastFailure )
timeSinceLastFailure := now . Sub ( timeOfLastFailure )
timeOfLastFailure = now
timeOfLastFailure = now
@ -280,22 +288,37 @@ func (s *userServer) runLoop() {
if sleepTime > maxSleepTime {
if sleepTime > maxSleepTime {
sleepTime = maxSleepTime
sleepTime = maxSleepTime
}
}
s . logf ( "user server % v stopped with error %v, will try again in %v" , executable, err , sleepTime )
s . logf ( "user server % v stopped with error %v, will try again in %v" , s. executable, err , sleepTime )
time . Sleep ( sleepTime )
time . Sleep ( sleepTime )
}
}
}
}
// Run runs the executable (tailscaled). This function only works on UNIX systems,
// Run runs the user server using the configured executable. This function only
// but those are the only ones on which we use userServers anyway.
// works on UNIX systems, but those are the only ones on which we use
func ( s * userServer ) run ( executable string ) error {
// userServers anyway.
func ( s * userServer ) run ( ) error {
// set up the command
// set up the command
args := [ ] string { "serve-tailfs" }
args := [ ] string { "serve-tailfs" }
for _ , s := range s . shares {
for _ , s := range s . shares {
args = append ( args , s . Name , s . Path )
args = append ( args , s . Name , s . Path )
}
}
allArgs := [ ] string { "-u" , s . shares [ 0 ] . As , executable }
var cmd * exec . Cmd
if s . canSudo ( ) {
s . logf ( "starting TailFS file server as user %q" , s . username )
allArgs := [ ] string { "-n" , "-u" , s . username , s . executable }
allArgs = append ( allArgs , args ... )
allArgs = append ( allArgs , args ... )
cmd := exec . Command ( "sudo" , allArgs ... )
cmd = exec . Command ( "sudo" , allArgs ... )
} else {
// If we were root, we should have been able to sudo as a specific
// user, but let's check just to make sure, since we never want to
// access shared folders as root.
err := s . assertNotRoot ( )
if err != nil {
return err
}
s . logf ( "starting TailFS file server as ourselves" )
cmd = exec . Command ( s . executable , args ... )
}
stdout , err := cmd . StdoutPipe ( )
stdout , err := cmd . StdoutPipe ( )
if err != nil {
if err != nil {
return fmt . Errorf ( "stdout pipe: %w" , err )
return fmt . Errorf ( "stdout pipe: %w" , err )
@ -350,3 +373,32 @@ var writeMethods = map[string]bool{
"MOVE" : true ,
"MOVE" : true ,
"PROPPATCH" : true ,
"PROPPATCH" : true ,
}
}
// canSudo checks wether we can sudo -u the configured executable as the
// configured user by attempting to call the executable with the '-h' flag to
// print help.
func ( s * userServer ) canSudo ( ) bool {
ctx , cancel := context . WithTimeout ( context . Background ( ) , 3 * time . Second )
defer cancel ( )
if err := exec . CommandContext ( ctx , "sudo" , "-n" , "-u" , s . username , s . executable , "-h" ) . Run ( ) ; err != nil {
return false
}
return true
}
// assertNotRoot returns an error if the current user has UID 0 or if we cannot
// determine the current user.
//
// On Linux, root users will always have UID 0.
//
// On BSD, root users should always have UID 0.
func ( s * userServer ) assertNotRoot ( ) error {
u , err := user . Current ( )
if err != nil {
return fmt . Errorf ( "assertNotRoot failed to find current user: %s" , err )
}
if u . Uid == "0" {
return fmt . Errorf ( "%q is root" , u . Name )
}
return nil
}