mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 16:26:38 +00:00
[client] Force TLS1.2 for RDP with Win11/Server2025 for CredSSP compatibility (#4617)
This commit is contained in:
@@ -73,8 +73,8 @@ func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, cert
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) getTLSConfigWithValidation(conn *proxyConnection) *tls.Config {
|
func (p *RDCleanPathProxy) getTLSConfigWithValidation(conn *proxyConnection, requiresCredSSP bool) *tls.Config {
|
||||||
return &tls.Config{
|
config := &tls.Config{
|
||||||
InsecureSkipVerify: true, // We'll validate manually after handshake
|
InsecureSkipVerify: true, // We'll validate manually after handshake
|
||||||
VerifyConnection: func(cs tls.ConnectionState) error {
|
VerifyConnection: func(cs tls.ConnectionState) error {
|
||||||
var certChain [][]byte
|
var certChain [][]byte
|
||||||
@@ -93,4 +93,15 @@ func (p *RDCleanPathProxy) getTLSConfigWithValidation(conn *proxyConnection) *tl
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CredSSP (NLA) requires TLS 1.2 - it's incompatible with TLS 1.3
|
||||||
|
if requiresCredSSP {
|
||||||
|
config.MinVersion = tls.VersionTLS12
|
||||||
|
config.MaxVersion = tls.VersionTLS12
|
||||||
|
} else {
|
||||||
|
config.MinVersion = tls.VersionTLS12
|
||||||
|
config.MaxVersion = tls.VersionTLS13
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -19,18 +21,34 @@ const (
|
|||||||
RDCleanPathVersion = 3390
|
RDCleanPathVersion = 3390
|
||||||
RDCleanPathProxyHost = "rdcleanpath.proxy.local"
|
RDCleanPathProxyHost = "rdcleanpath.proxy.local"
|
||||||
RDCleanPathProxyScheme = "ws"
|
RDCleanPathProxyScheme = "ws"
|
||||||
|
|
||||||
|
rdpDialTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
GeneralErrorCode = 1
|
||||||
|
WSAETimedOut = 10060
|
||||||
|
WSAEConnRefused = 10061
|
||||||
|
WSAEConnAborted = 10053
|
||||||
|
WSAEConnReset = 10054
|
||||||
|
WSAEGenericError = 10050
|
||||||
)
|
)
|
||||||
|
|
||||||
type RDCleanPathPDU struct {
|
type RDCleanPathPDU struct {
|
||||||
Version int64 `asn1:"tag:0,explicit"`
|
Version int64 `asn1:"tag:0,explicit"`
|
||||||
Error []byte `asn1:"tag:1,explicit,optional"`
|
Error RDCleanPathErr `asn1:"tag:1,explicit,optional"`
|
||||||
Destination string `asn1:"utf8,tag:2,explicit,optional"`
|
Destination string `asn1:"utf8,tag:2,explicit,optional"`
|
||||||
ProxyAuth string `asn1:"utf8,tag:3,explicit,optional"`
|
ProxyAuth string `asn1:"utf8,tag:3,explicit,optional"`
|
||||||
ServerAuth string `asn1:"utf8,tag:4,explicit,optional"`
|
ServerAuth string `asn1:"utf8,tag:4,explicit,optional"`
|
||||||
PreconnectionBlob string `asn1:"utf8,tag:5,explicit,optional"`
|
PreconnectionBlob string `asn1:"utf8,tag:5,explicit,optional"`
|
||||||
X224ConnectionPDU []byte `asn1:"tag:6,explicit,optional"`
|
X224ConnectionPDU []byte `asn1:"tag:6,explicit,optional"`
|
||||||
ServerCertChain [][]byte `asn1:"tag:7,explicit,optional"`
|
ServerCertChain [][]byte `asn1:"tag:7,explicit,optional"`
|
||||||
ServerAddr string `asn1:"utf8,tag:9,explicit,optional"`
|
ServerAddr string `asn1:"utf8,tag:9,explicit,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RDCleanPathErr struct {
|
||||||
|
ErrorCode int16 `asn1:"tag:0,explicit"`
|
||||||
|
HTTPStatusCode int16 `asn1:"tag:1,explicit,optional"`
|
||||||
|
WSALastError int16 `asn1:"tag:2,explicit,optional"`
|
||||||
|
TLSAlertCode int8 `asn1:"tag:3,explicit,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RDCleanPathProxy struct {
|
type RDCleanPathProxy struct {
|
||||||
@@ -210,9 +228,13 @@ func (p *RDCleanPathProxy) handleDirectRDP(conn *proxyConnection, firstPacket []
|
|||||||
destination := conn.destination
|
destination := conn.destination
|
||||||
log.Infof("Direct RDP mode: Connecting to %s via NetBird", destination)
|
log.Infof("Direct RDP mode: Connecting to %s via NetBird", destination)
|
||||||
|
|
||||||
rdpConn, err := p.nbClient.Dial(conn.ctx, "tcp", destination)
|
ctx, cancel := context.WithTimeout(conn.ctx, rdpDialTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
rdpConn, err := p.nbClient.Dial(ctx, "tcp", destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to connect to %s: %v", destination, err)
|
log.Errorf("Failed to connect to %s: %v", destination, err)
|
||||||
|
p.sendRDCleanPathError(conn, newWSAError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conn.rdpConn = rdpConn
|
conn.rdpConn = rdpConn
|
||||||
@@ -220,6 +242,7 @@ func (p *RDCleanPathProxy) handleDirectRDP(conn *proxyConnection, firstPacket []
|
|||||||
_, err = rdpConn.Write(firstPacket)
|
_, err = rdpConn.Write(firstPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to write first packet: %v", err)
|
log.Errorf("Failed to write first packet: %v", err)
|
||||||
|
p.sendRDCleanPathError(conn, newWSAError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +250,7 @@ func (p *RDCleanPathProxy) handleDirectRDP(conn *proxyConnection, firstPacket []
|
|||||||
n, err := rdpConn.Read(response)
|
n, err := rdpConn.Read(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to read X.224 response: %v", err)
|
log.Errorf("Failed to read X.224 response: %v", err)
|
||||||
|
p.sendRDCleanPathError(conn, newWSAError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,3 +293,52 @@ func (p *RDCleanPathProxy) sendToWebSocket(conn *proxyConnection, data []byte) {
|
|||||||
conn.wsHandlers.Call("send", uint8Array.Get("buffer"))
|
conn.wsHandlers.Call("send", uint8Array.Get("buffer"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *RDCleanPathProxy) sendRDCleanPathError(conn *proxyConnection, pdu RDCleanPathPDU) {
|
||||||
|
data, err := asn1.Marshal(pdu)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to marshal error PDU: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.sendToWebSocket(conn, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorToWSACode(err error) int16 {
|
||||||
|
if err == nil {
|
||||||
|
return WSAEGenericError
|
||||||
|
}
|
||||||
|
var netErr *net.OpError
|
||||||
|
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||||
|
return WSAETimedOut
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return WSAETimedOut
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return WSAEConnAborted
|
||||||
|
}
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return WSAEConnReset
|
||||||
|
}
|
||||||
|
return WSAEGenericError
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWSAError(err error) RDCleanPathPDU {
|
||||||
|
return RDCleanPathPDU{
|
||||||
|
Version: RDCleanPathVersion,
|
||||||
|
Error: RDCleanPathErr{
|
||||||
|
ErrorCode: GeneralErrorCode,
|
||||||
|
WSALastError: errorToWSACode(err),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPError(statusCode int16) RDCleanPathPDU {
|
||||||
|
return RDCleanPathPDU{
|
||||||
|
Version: RDCleanPathVersion,
|
||||||
|
Error: RDCleanPathErr{
|
||||||
|
ErrorCode: GeneralErrorCode,
|
||||||
|
HTTPStatusCode: statusCode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package rdp
|
package rdp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"io"
|
"io"
|
||||||
@@ -11,11 +12,17 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
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) {
|
func (p *RDCleanPathProxy) processRDCleanPathPDU(conn *proxyConnection, pdu RDCleanPathPDU) {
|
||||||
log.Infof("Processing RDCleanPath PDU: Version=%d, Destination=%s", pdu.Version, pdu.Destination)
|
log.Infof("Processing RDCleanPath PDU: Version=%d, Destination=%s", pdu.Version, pdu.Destination)
|
||||||
|
|
||||||
if pdu.Version != RDCleanPathVersion {
|
if pdu.Version != RDCleanPathVersion {
|
||||||
p.sendRDCleanPathError(conn, "Unsupported version")
|
p.sendRDCleanPathError(conn, newHTTPError(400))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,10 +31,13 @@ func (p *RDCleanPathProxy) processRDCleanPathPDU(conn *proxyConnection, pdu RDCl
|
|||||||
destination = pdu.Destination
|
destination = pdu.Destination
|
||||||
}
|
}
|
||||||
|
|
||||||
rdpConn, err := p.nbClient.Dial(conn.ctx, "tcp", destination)
|
ctx, cancel := context.WithTimeout(conn.ctx, rdpDialTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
rdpConn, err := p.nbClient.Dial(ctx, "tcp", destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to connect to %s: %v", destination, err)
|
log.Errorf("Failed to connect to %s: %v", destination, err)
|
||||||
p.sendRDCleanPathError(conn, "Connection failed")
|
p.sendRDCleanPathError(conn, newWSAError(err))
|
||||||
p.cleanupConnection(conn)
|
p.cleanupConnection(conn)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -40,6 +50,34 @@ func (p *RDCleanPathProxy) processRDCleanPathPDU(conn *proxyConnection, pdu RDCl
|
|||||||
p.setupTLSConnection(conn, pdu)
|
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) {
|
func (p *RDCleanPathProxy) setupTLSConnection(conn *proxyConnection, pdu RDCleanPathPDU) {
|
||||||
var x224Response []byte
|
var x224Response []byte
|
||||||
if len(pdu.X224ConnectionPDU) > 0 {
|
if len(pdu.X224ConnectionPDU) > 0 {
|
||||||
@@ -47,7 +85,7 @@ func (p *RDCleanPathProxy) setupTLSConnection(conn *proxyConnection, pdu RDClean
|
|||||||
_, err := conn.rdpConn.Write(pdu.X224ConnectionPDU)
|
_, err := conn.rdpConn.Write(pdu.X224ConnectionPDU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to write X.224 PDU: %v", err)
|
log.Errorf("Failed to write X.224 PDU: %v", err)
|
||||||
p.sendRDCleanPathError(conn, "Failed to forward X.224")
|
p.sendRDCleanPathError(conn, newWSAError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,21 +93,32 @@ func (p *RDCleanPathProxy) setupTLSConnection(conn *proxyConnection, pdu RDClean
|
|||||||
n, err := conn.rdpConn.Read(response)
|
n, err := conn.rdpConn.Read(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to read X.224 response: %v", err)
|
log.Errorf("Failed to read X.224 response: %v", err)
|
||||||
p.sendRDCleanPathError(conn, "Failed to read X.224 response")
|
p.sendRDCleanPathError(conn, newWSAError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
x224Response = response[:n]
|
x224Response = response[:n]
|
||||||
log.Debugf("Received X.224 Connection Confirm (%d bytes)", n)
|
log.Debugf("Received X.224 Connection Confirm (%d bytes)", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConfig := p.getTLSConfigWithValidation(conn)
|
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)
|
tlsConn := tls.Client(conn.rdpConn, tlsConfig)
|
||||||
conn.tlsConn = tlsConn
|
conn.tlsConn = tlsConn
|
||||||
|
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
log.Errorf("TLS handshake failed: %v", err)
|
log.Errorf("TLS handshake failed: %v", err)
|
||||||
p.sendRDCleanPathError(conn, "TLS handshake failed")
|
p.sendRDCleanPathError(conn, newWSAError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,47 +155,6 @@ func (p *RDCleanPathProxy) setupTLSConnection(conn *proxyConnection, pdu RDClean
|
|||||||
p.cleanupConnection(conn)
|
p.cleanupConnection(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) setupPlainConnection(conn *proxyConnection, pdu RDCleanPathPDU) {
|
|
||||||
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, "Failed to forward X.224")
|
|
||||||
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, "Failed to read X.224 response")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
responsePDU := RDCleanPathPDU{
|
|
||||||
Version: RDCleanPathVersion,
|
|
||||||
X224ConnectionPDU: response[:n],
|
|
||||||
ServerAddr: conn.destination,
|
|
||||||
}
|
|
||||||
|
|
||||||
p.sendRDCleanPathPDU(conn, responsePDU)
|
|
||||||
} else {
|
|
||||||
responsePDU := RDCleanPathPDU{
|
|
||||||
Version: RDCleanPathVersion,
|
|
||||||
ServerAddr: conn.destination,
|
|
||||||
}
|
|
||||||
p.sendRDCleanPathPDU(conn, responsePDU)
|
|
||||||
}
|
|
||||||
|
|
||||||
go p.forwardConnToWS(conn, conn.rdpConn, "TCP")
|
|
||||||
go p.forwardWSToConn(conn, conn.rdpConn, "TCP")
|
|
||||||
|
|
||||||
<-conn.ctx.Done()
|
|
||||||
log.Debug("TCP connection context done, cleaning up")
|
|
||||||
p.cleanupConnection(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) sendRDCleanPathPDU(conn *proxyConnection, pdu RDCleanPathPDU) {
|
func (p *RDCleanPathProxy) sendRDCleanPathPDU(conn *proxyConnection, pdu RDCleanPathPDU) {
|
||||||
data, err := asn1.Marshal(pdu)
|
data, err := asn1.Marshal(pdu)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -158,21 +166,6 @@ func (p *RDCleanPathProxy) sendRDCleanPathPDU(conn *proxyConnection, pdu RDClean
|
|||||||
p.sendToWebSocket(conn, data)
|
p.sendToWebSocket(conn, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) sendRDCleanPathError(conn *proxyConnection, errorMsg string) {
|
|
||||||
pdu := RDCleanPathPDU{
|
|
||||||
Version: RDCleanPathVersion,
|
|
||||||
Error: []byte(errorMsg),
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := asn1.Marshal(pdu)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to marshal error PDU: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.sendToWebSocket(conn, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *RDCleanPathProxy) readWebSocketMessage(conn *proxyConnection) ([]byte, error) {
|
func (p *RDCleanPathProxy) readWebSocketMessage(conn *proxyConnection) ([]byte, error) {
|
||||||
msgChan := make(chan []byte)
|
msgChan := make(chan []byte)
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
|
|||||||
Reference in New Issue
Block a user