//go:build js package rdp import ( "context" "crypto/tls" "encoding/asn1" "io" "syscall/js" log "github.com/sirupsen/logrus" ) const ( // MS-RDPBCGR: confusingly named, actually means PROTOCOL_HYBRID (CredSSP) protocolSSL = 0x00000001 protocolHybridEx = 0x00000008 ) func (p *RDCleanPathProxy) processRDCleanPathPDU(conn *proxyConnection, pdu RDCleanPathPDU) { log.Infof("Processing RDCleanPath PDU: Version=%d, Destination=%s", pdu.Version, pdu.Destination) if pdu.Version != RDCleanPathVersion { p.sendRDCleanPathError(conn, newHTTPError(400)) return } destination := conn.destination if pdu.Destination != "" { destination = pdu.Destination } ctx, cancel := context.WithTimeout(conn.ctx, rdpDialTimeout) defer cancel() rdpConn, err := p.nbClient.Dial(ctx, "tcp", destination) if err != nil { log.Errorf("Failed to connect to %s: %v", destination, err) p.sendRDCleanPathError(conn, newWSAError(err)) p.cleanupConnection(conn) return } conn.rdpConn = rdpConn // RDP always starts with X.224 negotiation, then determines if TLS is needed // Modern RDP (since Windows Vista/2008) typically requires TLS // The X.224 Connection Confirm response will indicate if TLS is required // For now, we'll attempt TLS for all connections as it's the modern default p.setupTLSConnection(conn, pdu) } // detectCredSSPFromX224 checks if the X.224 response indicates NLA/CredSSP is required. // Per MS-RDPBCGR spec: byte 11 = TYPE_RDP_NEG_RSP (0x02), bytes 15-18 = selectedProtocol flags. // Returns (requiresTLS12, selectedProtocol, detectionSuccessful). func (p *RDCleanPathProxy) detectCredSSPFromX224(x224Response []byte) (bool, uint32, bool) { const minResponseLength = 19 if len(x224Response) < minResponseLength { return false, 0, false } // Per X.224 specification: // x224Response[0] == 0x03: Length of X.224 header (3 bytes) // x224Response[5] == 0xD0: X.224 Data TPDU code if x224Response[0] != 0x03 || x224Response[5] != 0xD0 { return false, 0, false } if x224Response[11] == 0x02 { flags := uint32(x224Response[15]) | uint32(x224Response[16])<<8 | uint32(x224Response[17])<<16 | uint32(x224Response[18])<<24 hasNLA := (flags & (protocolSSL | protocolHybridEx)) != 0 return hasNLA, flags, true } return false, 0, false } func (p *RDCleanPathProxy) setupTLSConnection(conn *proxyConnection, pdu RDCleanPathPDU) { var x224Response []byte if len(pdu.X224ConnectionPDU) > 0 { log.Debugf("Forwarding X.224 Connection Request (%d bytes)", len(pdu.X224ConnectionPDU)) _, err := conn.rdpConn.Write(pdu.X224ConnectionPDU) if err != nil { log.Errorf("Failed to write X.224 PDU: %v", err) p.sendRDCleanPathError(conn, newWSAError(err)) return } response := make([]byte, 1024) n, err := conn.rdpConn.Read(response) if err != nil { log.Errorf("Failed to read X.224 response: %v", err) p.sendRDCleanPathError(conn, newWSAError(err)) return } x224Response = response[:n] log.Debugf("Received X.224 Connection Confirm (%d bytes)", n) } requiresCredSSP, selectedProtocol, detected := p.detectCredSSPFromX224(x224Response) if detected { if requiresCredSSP { log.Warnf("Detected NLA/CredSSP (selectedProtocol: 0x%08X), forcing TLS 1.2 for compatibility", selectedProtocol) } else { log.Warnf("No NLA/CredSSP detected (selectedProtocol: 0x%08X), allowing up to TLS 1.3", selectedProtocol) } } else { log.Warnf("Could not detect RDP security protocol, allowing up to TLS 1.3") } tlsConfig := p.getTLSConfigWithValidation(conn, requiresCredSSP) tlsConn := tls.Client(conn.rdpConn, tlsConfig) conn.tlsConn = tlsConn if err := tlsConn.Handshake(); err != nil { log.Errorf("TLS handshake failed: %v", err) p.sendRDCleanPathError(conn, newWSAError(err)) return } log.Info("TLS handshake successful") // Certificate validation happens during handshake via VerifyConnection callback var certChain [][]byte connState := tlsConn.ConnectionState() if len(connState.PeerCertificates) > 0 { for _, cert := range connState.PeerCertificates { certChain = append(certChain, cert.Raw) } log.Debugf("Extracted %d certificates from TLS connection", len(certChain)) } responsePDU := RDCleanPathPDU{ Version: RDCleanPathVersion, ServerAddr: conn.destination, ServerCertChain: certChain, } if len(x224Response) > 0 { responsePDU.X224ConnectionPDU = x224Response } p.sendRDCleanPathPDU(conn, responsePDU) log.Debug("Starting TLS forwarding") go p.forwardConnToWS(conn, conn.tlsConn, "TLS") go p.forwardWSToConn(conn, conn.tlsConn, "TLS") <-conn.ctx.Done() log.Debug("TLS connection context done, cleaning up") p.cleanupConnection(conn) } func (p *RDCleanPathProxy) sendRDCleanPathPDU(conn *proxyConnection, pdu RDCleanPathPDU) { data, err := asn1.Marshal(pdu) if err != nil { log.Errorf("Failed to marshal RDCleanPath PDU: %v", err) return } log.Debugf("Sending RDCleanPath PDU response (%d bytes)", len(data)) p.sendToWebSocket(conn, data) } func (p *RDCleanPathProxy) readWebSocketMessage(conn *proxyConnection) ([]byte, error) { msgChan := make(chan []byte) errChan := make(chan error) handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { if len(args) < 1 { errChan <- io.EOF return nil } data := args[0] if data.InstanceOf(js.Global().Get("Uint8Array")) { length := data.Get("length").Int() bytes := make([]byte, length) js.CopyBytesToGo(bytes, data) msgChan <- bytes } return nil }) defer handler.Release() conn.wsHandlers.Set("onceGoMessage", handler) select { case msg := <-msgChan: return msg, nil case err := <-errChan: return nil, err case <-conn.ctx.Done(): return nil, conn.ctx.Err() } } func (p *RDCleanPathProxy) forwardWSToConn(conn *proxyConnection, dst io.Writer, connType string) { for { if conn.ctx.Err() != nil { return } msg, err := p.readWebSocketMessage(conn) if err != nil { if err != io.EOF { log.Errorf("Failed to read from WebSocket: %v", err) } return } _, err = dst.Write(msg) if err != nil { log.Errorf("Failed to write to %s: %v", connType, err) return } } } func (p *RDCleanPathProxy) forwardConnToWS(conn *proxyConnection, src io.Reader, connType string) { buffer := make([]byte, 32*1024) for { if conn.ctx.Err() != nil { return } n, err := src.Read(buffer) if err != nil { if err != io.EOF { log.Errorf("Failed to read from %s: %v", connType, err) } return } if n > 0 { p.sendToWebSocket(conn, buffer[:n]) } } }