mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
272 lines
7.4 KiB
Go
272 lines
7.4 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"os/user"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
var (
|
|
vncUsername string
|
|
vncHost string
|
|
vncMode string
|
|
vncListen string
|
|
vncNoBrowser bool
|
|
vncNoCache bool
|
|
)
|
|
|
|
func init() {
|
|
vncCmd.PersistentFlags().StringVar(&vncUsername, "user", "", "OS username for session mode")
|
|
vncCmd.PersistentFlags().StringVar(&vncMode, "mode", "attach", "Connection mode: attach (view current display) or session (virtual desktop)")
|
|
vncCmd.PersistentFlags().StringVar(&vncListen, "listen", "", "Start local VNC proxy on this address (e.g., :5900) for external VNC viewers")
|
|
vncCmd.PersistentFlags().BoolVar(&vncNoBrowser, noBrowserFlag, false, noBrowserDesc)
|
|
vncCmd.PersistentFlags().BoolVar(&vncNoCache, "no-cache", false, "Skip cached JWT token and force fresh authentication")
|
|
}
|
|
|
|
var vncCmd = &cobra.Command{
|
|
Use: "vnc [flags] [user@]host",
|
|
Short: "Connect to a NetBird peer via VNC",
|
|
Long: `Connect to a NetBird peer using VNC with JWT-based authentication.
|
|
The target peer must have the VNC server enabled.
|
|
|
|
Two modes are available:
|
|
- attach: view the current physical display (remote support)
|
|
- session: start a virtual desktop as the specified user (passwordless login)
|
|
|
|
Use --listen to start a local proxy for external VNC viewers:
|
|
netbird vnc --listen :5900 peer-hostname
|
|
vncviewer localhost:5900
|
|
|
|
Examples:
|
|
netbird vnc peer-hostname
|
|
netbird vnc --mode session --user alice peer-hostname
|
|
netbird vnc --listen :5900 peer-hostname`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: vncFn,
|
|
}
|
|
|
|
func vncFn(cmd *cobra.Command, args []string) error {
|
|
SetFlagsFromEnvVars(rootCmd)
|
|
SetFlagsFromEnvVars(cmd)
|
|
cmd.SetOut(cmd.OutOrStdout())
|
|
|
|
logOutput := "console"
|
|
if firstLogFile := util.FindFirstLogPath(logFiles); firstLogFile != "" && firstLogFile != defaultLogFile {
|
|
logOutput = firstLogFile
|
|
}
|
|
if err := util.InitLog(logLevel, logOutput); err != nil {
|
|
return fmt.Errorf("init log: %w", err)
|
|
}
|
|
|
|
if err := parseVNCHostArg(args[0]); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := internal.CtxInitState(cmd.Context())
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
|
|
vncCtx, cancel := context.WithCancel(ctx)
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
if err := runVNC(vncCtx, cmd); err != nil {
|
|
errCh <- err
|
|
}
|
|
cancel()
|
|
}()
|
|
|
|
select {
|
|
case <-sig:
|
|
cancel()
|
|
<-vncCtx.Done()
|
|
return nil
|
|
case err := <-errCh:
|
|
return err
|
|
case <-vncCtx.Done():
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseVNCHostArg(arg string) error {
|
|
if strings.Contains(arg, "@") {
|
|
parts := strings.SplitN(arg, "@", 2)
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
return fmt.Errorf("invalid user@host format")
|
|
}
|
|
if vncUsername == "" {
|
|
vncUsername = parts[0]
|
|
}
|
|
vncHost = parts[1]
|
|
if vncMode == "attach" {
|
|
vncMode = "session"
|
|
}
|
|
} else {
|
|
vncHost = arg
|
|
}
|
|
|
|
if vncMode == "session" && vncUsername == "" {
|
|
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
|
|
vncUsername = sudoUser
|
|
} else if currentUser, err := user.Current(); err == nil {
|
|
vncUsername = currentUser.Username
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runVNC(ctx context.Context, cmd *cobra.Command) error {
|
|
grpcAddr := strings.TrimPrefix(daemonAddr, "tcp://")
|
|
grpcConn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
return fmt.Errorf("connect to daemon: %w", err)
|
|
}
|
|
defer func() { _ = grpcConn.Close() }()
|
|
|
|
daemonClient := proto.NewDaemonServiceClient(grpcConn)
|
|
|
|
if vncMode == "session" {
|
|
cmd.Printf("Connecting to %s@%s [session mode]...\n", vncUsername, vncHost)
|
|
} else {
|
|
cmd.Printf("Connecting to %s [attach mode]...\n", vncHost)
|
|
}
|
|
|
|
// Obtain JWT token. If the daemon has no SSO configured, proceed without one
|
|
// (the server will accept unauthenticated connections if --disable-vnc-auth is set).
|
|
var jwtToken string
|
|
hint := profilemanager.GetLoginHint()
|
|
var browserOpener func(string) error
|
|
if !vncNoBrowser {
|
|
browserOpener = util.OpenBrowser
|
|
}
|
|
|
|
token, err := nbssh.RequestJWTToken(ctx, daemonClient, nil, cmd.ErrOrStderr(), !vncNoCache, hint, browserOpener)
|
|
if err != nil {
|
|
log.Debugf("JWT authentication unavailable, connecting without token: %v", err)
|
|
} else {
|
|
jwtToken = token
|
|
log.Debug("JWT authentication successful")
|
|
}
|
|
|
|
// Connect to the VNC server on the standard port (5900). The peer's firewall
|
|
// DNATs 5900 -> 25900 (internal), so both ports work on the overlay network.
|
|
vncAddr := net.JoinHostPort(vncHost, "5900")
|
|
vncConn, err := net.DialTimeout("tcp", vncAddr, vncDialTimeout)
|
|
if err != nil {
|
|
return fmt.Errorf("connect to VNC at %s: %w", vncAddr, err)
|
|
}
|
|
defer vncConn.Close()
|
|
|
|
// Send session header with mode, username, and JWT.
|
|
if err := sendVNCHeader(vncConn, vncMode, vncUsername, jwtToken); err != nil {
|
|
return fmt.Errorf("send VNC header: %w", err)
|
|
}
|
|
|
|
cmd.Printf("VNC connected to %s\n", vncHost)
|
|
|
|
if vncListen != "" {
|
|
return runVNCLocalProxy(ctx, cmd, vncConn)
|
|
}
|
|
|
|
// No --listen flag: inform the user they need to use --listen for external viewers.
|
|
cmd.Printf("VNC tunnel established. Use --listen :5900 to proxy for local VNC viewers.\n")
|
|
cmd.Printf("Press Ctrl+C to disconnect.\n")
|
|
<-ctx.Done()
|
|
return nil
|
|
}
|
|
|
|
const vncDialTimeout = 15 * time.Second
|
|
|
|
// sendVNCHeader writes the NetBird VNC session header.
|
|
func sendVNCHeader(conn net.Conn, mode, username, jwt string) error {
|
|
var modeByte byte
|
|
if mode == "session" {
|
|
modeByte = 1
|
|
}
|
|
|
|
usernameBytes := []byte(username)
|
|
jwtBytes := []byte(jwt)
|
|
hdr := make([]byte, 3+len(usernameBytes)+2+len(jwtBytes))
|
|
hdr[0] = modeByte
|
|
binary.BigEndian.PutUint16(hdr[1:3], uint16(len(usernameBytes)))
|
|
off := 3
|
|
copy(hdr[off:], usernameBytes)
|
|
off += len(usernameBytes)
|
|
binary.BigEndian.PutUint16(hdr[off:off+2], uint16(len(jwtBytes)))
|
|
off += 2
|
|
copy(hdr[off:], jwtBytes)
|
|
|
|
_, err := conn.Write(hdr)
|
|
return err
|
|
}
|
|
|
|
// runVNCLocalProxy listens on the given address and proxies incoming
|
|
// connections to the already-established VNC tunnel.
|
|
func runVNCLocalProxy(ctx context.Context, cmd *cobra.Command, vncConn net.Conn) error {
|
|
listener, err := net.Listen("tcp", vncListen)
|
|
if err != nil {
|
|
return fmt.Errorf("listen on %s: %w", vncListen, err)
|
|
}
|
|
defer listener.Close()
|
|
|
|
cmd.Printf("VNC proxy listening on %s - connect with your VNC viewer\n", listener.Addr())
|
|
cmd.Printf("Press Ctrl+C to stop.\n")
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
listener.Close()
|
|
}()
|
|
|
|
// Accept a single viewer connection. VNC is single-session: the RFB
|
|
// handshake completes on vncConn for the first viewer, so subsequent
|
|
// viewers would get a mid-stream connection. The loop handles transient
|
|
// accept errors until a valid connection arrives.
|
|
for {
|
|
clientConn, err := listener.Accept()
|
|
if err != nil {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
default:
|
|
}
|
|
log.Debugf("accept VNC proxy client: %v", err)
|
|
continue
|
|
}
|
|
|
|
cmd.Printf("VNC viewer connected from %s\n", clientConn.RemoteAddr())
|
|
|
|
// Bidirectional copy.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
io.Copy(vncConn, clientConn)
|
|
close(done)
|
|
}()
|
|
io.Copy(clientConn, vncConn)
|
|
<-done
|
|
clientConn.Close()
|
|
|
|
cmd.Printf("VNC viewer disconnected\n")
|
|
return nil
|
|
}
|
|
}
|