diff --git a/client/wasm/cmd/main.go b/client/wasm/cmd/main.go index bff2ac94e..cd1c792aa 100644 --- a/client/wasm/cmd/main.go +++ b/client/wasm/cmd/main.go @@ -438,7 +438,7 @@ func createGenerateVNCSessionKeyMethod() js.Func { } // createVNCProxyMethod creates the VNC proxy method for raw TCP-over-WebSocket bridging. -// JS signature: createVNCProxy(hostname, port, mode?, username?, keySessionID?, sessionID?, width?, height?, peerPublicKey?, ipVersion?) +// JS signature: createVNCProxy(hostname, port, mode?, username?, keySessionID?, sessionID?, width?, height?, peerPublicKey?, ipVersion?, external?) // // mode: "attach" (default) or "session" // username: required when mode is "session" @@ -447,6 +447,9 @@ func createGenerateVNCSessionKeyMethod() js.Func { // width/height: requested viewport size for session mode (0 = server default) // peerPublicKey: base64 X25519 static pubkey of the destination peer (required for auth) // ipVersion: address family to dial: 4, 6, or 0/omitted for automatic +// external: true to carry plain RFB to a third-party VNC server on the +// peer, which performs no NetBird authentication and ignores +// every parameter above except port and ipVersion func createVNCProxyMethod(client *netbird.Client) js.Func { return js.FuncOf(func(_ js.Value, args []js.Value) any { params, err := parseVNCProxyArgs(args) @@ -470,6 +473,7 @@ func createVNCProxyMethod(client *netbird.Client) js.Func { PeerPublicKey: params.peerPublicKey, KeySessionID: params.keySessionID, IPVersion: params.ipVersion, + External: params.external, }) }) } @@ -485,6 +489,7 @@ type vncProxyParams struct { height uint16 peerPublicKey string ipVersion int + external bool rejectViaPromise bool } @@ -503,9 +508,16 @@ func parseVNCProxyArgs(args []js.Value) (vncProxyParams, error) { if err := parseVNCProxyOptionalNumbers(args, &p); err != nil { return p, err } + parseVNCProxyOptionalFlags(args, &p) return p, nil } +func parseVNCProxyOptionalFlags(args []js.Value, p *vncProxyParams) { + if len(args) > 10 && args[10].Type() == js.TypeBoolean { + p.external = args[10].Bool() + } +} + func parseVNCProxyRequiredArgs(args []js.Value, p *vncProxyParams) error { if len(args) < 2 { return fmt.Errorf("hostname and port required") diff --git a/client/wasm/internal/vnc/proxy.go b/client/wasm/internal/vnc/proxy.go index 5a9de8999..ff30816ab 100644 --- a/client/wasm/internal/vnc/proxy.go +++ b/client/wasm/internal/vnc/proxy.go @@ -144,8 +144,12 @@ type VNCProxy struct { } type vncDestination struct { - address string - network string + address string + network string + // external marks a destination that speaks plain RFB rather than + // NetBird's session protocol, so the connection header is not written + // and the stream is piped from the first byte the server sends. + external bool mode byte username string sessionPriv []byte @@ -205,6 +209,12 @@ type ProxyRequest struct { // 4, 6, or 0 for automatic selection. Mirrors the SSH proxy so the // dashboard can resolve a peer label to a specific family. IPVersion int + // External addresses a third-party VNC server on the peer instead of + // NetBird's embedded one. The proxy then carries plain RFB and performs + // no NetBird authentication, so the server's own security type decides + // who gets in, and Mode, Username, SessionID, Width, Height, + // PeerPublicKey and KeySessionID have no meaning. + External bool } // CreateProxy creates a new proxy endpoint for the given VNC destination. @@ -225,12 +235,26 @@ func (p *VNCProxy) CreateProxy(req ProxyRequest) js.Value { dest := vncDestination{ address: address, network: netutil.TCPNetwork(req.IPVersion), + external: req.External, mode: m, username: username, sessionID: sessionID, width: width, height: height, } + if req.External { + // Refusing rather than ignoring these: a caller that supplied a session + // key expects the connection to be authenticated, and one that asked + // for a virtual session expects a second desktop. Carrying on would + // hand them a plain attach session that looks like what they asked for. + if req.KeySessionID != "" { + return rejectedPromise("external VNC destination cannot use a NetBird session key") + } + if mode == "session" { + return rejectedPromise("external VNC destination cannot start a virtual session") + } + return p.newProxyPromise(address, mode, username, dest) + } if req.KeySessionID != "" { kp, ok := lookupSessionKey(req.KeySessionID) if !ok { @@ -434,14 +458,17 @@ func (p *VNCProxy) connectToVNC(conn *vncConnection) { conn.vncConn = vncConn conn.mu.Unlock() - // Send the NetBird VNC session header before the RFB handshake. - if err := p.sendSessionHeader(vncConn, conn.destination); err != nil { - log.Errorf("send VNC session header: %v", err) - if conn.wsHandlers.Get("close").Truthy() { - conn.wsHandlers.Call("close", wsCodeSessionSetup, fmt.Sprintf("send session header: %v", err)) + // An external server speaks RFB from its first byte, so anything written + // ahead of the version exchange would corrupt it. + if !conn.destination.external { + if err := p.sendSessionHeader(vncConn, conn.destination); err != nil { + log.Errorf("send VNC session header: %v", err) + if conn.wsHandlers.Get("close").Truthy() { + conn.wsHandlers.Call("close", wsCodeSessionSetup, fmt.Sprintf("send session header: %v", err)) + } + p.cleanupConnection(conn) + return } - p.cleanupConnection(conn) - return } // WS→TCP payloads are enqueued in arrival order by the onGoMessage handler