Files
netbird/client/cmd/vnc_agent.go
T

139 lines
4.6 KiB
Go

//go:build windows || (darwin && !ios)
package cmd
import (
"bufio"
"errors"
"fmt"
"io"
"net/netip"
"os"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
vncserver "github.com/netbirdio/netbird/client/vnc/server"
)
var (
vncAgentSocket string
vncAgentTargetUID uint32
vncAgentTokenStdin bool
)
// maxAgentTokenLine bounds the token read from stdin. The token is a short hex
// string; anything longer is not one.
const maxAgentTokenLine = 1024
func init() {
vncAgentCmd.Flags().StringVar(&vncAgentSocket, "socket", "", "socket the agent listens on: a Unix-domain socket path on darwin, a named pipe path on Windows (required)")
vncAgentCmd.Flags().Uint32Var(&vncAgentTargetUID, "target-uid", 0, "uid the agent drops privileges to before listening (darwin only; required there, and must not be 0)")
// Must match agentTokenStdinFlag in client/vnc/server/agent_ipc.go.
vncAgentCmd.Flags().BoolVar(&vncAgentTokenStdin, "token-stdin", false, "read the per-spawn token from stdin instead of the environment")
rootCmd.AddCommand(vncAgentCmd)
}
// vncAgentCmd runs a VNC server inside the user's interactive session,
// listening on a Unix-domain socket (a named pipe on Windows). The NetBird
// service spawns it: on Windows via CreateProcessAsUser into the console
// session, on macOS via launchctl asuser into the Aqua session.
var vncAgentCmd = &cobra.Command{
Use: "vnc-agent",
Short: "Run VNC capture agent (internal, spawned by service)",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
log.SetReportCaller(true)
log.SetFormatter(&log.JSONFormatter{})
log.SetOutput(os.Stderr)
if vncAgentSocket == "" {
return fmt.Errorf("--socket is required")
}
token, err := readAgentToken()
if err != nil {
return err
}
// Drop root privileges to the target console user BEFORE creating
// the listening socket: keeps a post-auth bug in the encoder /
// input / capture paths confined to the user's own privileges
// rather than escalating to host root, and makes the daemon's
// LOCAL_PEERCRED check see the right uid. No-op on Windows, where
// both processes run as SYSTEM.
//
// Called unconditionally: a missing or zero --target-uid is exactly
// the case the Darwin implementation refuses, and skipping the call
// for it would leave the agent running as root instead.
if err := dropAgentPrivileges(vncAgentTargetUID); err != nil {
return fmt.Errorf("drop privileges to uid %d: %w", vncAgentTargetUID, err)
}
// Before the socket exists, not after: the daemon treats the socket
// appearing as "this agent is ready to serve". Building the capturer
// and injector can take a while and can fail — on macOS it raises the
// Screen Recording prompt and waits on the user — so binding first
// publishes an agent that is not serving yet, and the first connection
// stalls or fails against it.
capturer, injector, err := newAgentResources()
if err != nil {
return err
}
ln, err := vncserver.ListenAgentSocket(vncAgentSocket)
if err != nil {
return fmt.Errorf("listen on %s: %w", vncAgentSocket, err)
}
ctx := cmd.Context()
srv := vncserver.New(vncserver.Config{
Capturer: capturer,
Injector: injector,
DisableAuth: true,
AgentTokenHex: token,
Listener: ln,
})
if err := srv.Start(ctx, netip.AddrPort{}, netip.Prefix{}); err != nil {
return fmt.Errorf("start vnc server: %w", err)
}
log.Infof("vnc-agent listening on %s, ready", vncAgentSocket)
<-ctx.Done()
log.Info("vnc-agent context cancelled, shutting down")
return srv.Stop()
},
SilenceUsage: true,
}
// readAgentToken returns the per-spawn token the service handed over, from
// stdin when --token-stdin is set and from the environment otherwise. Missing
// or empty is an error: the agent must never serve without one.
func readAgentToken() (string, error) {
if vncAgentTokenStdin {
line, err := bufio.NewReader(io.LimitReader(os.Stdin, maxAgentTokenLine)).ReadString('\n')
if err != nil && !errors.Is(err, io.EOF) {
return "", fmt.Errorf("read agent token from stdin: %w", err)
}
_ = os.Stdin.Close()
token := strings.TrimSpace(line)
if token == "" {
return "", fmt.Errorf("no agent token on stdin; agent requires a token from the service")
}
return token, nil
}
token := os.Getenv("NB_VNC_AGENT_TOKEN")
if token == "" {
return "", fmt.Errorf("NB_VNC_AGENT_TOKEN not set; agent requires a token from the service")
}
// Purge the token from env so later reads in this process do not see it.
if err := os.Unsetenv("NB_VNC_AGENT_TOKEN"); err != nil {
log.Debugf("unset NB_VNC_AGENT_TOKEN: %v", err)
}
return token, nil
}