Skip to content

Commit 000e99e

Browse files
authored
[client] Force TLS1.2 for RDP with Win11/Server2025 for CredSSP compatibility (netbirdio#4617)
1 parent 0d2e679 commit 000e99e

3 files changed

Lines changed: 152 additions & 75 deletions

File tree

client/wasm/internal/rdp/cert_validation.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, cert
7373
}
7474
}
7575

76-
func (p *RDCleanPathProxy) getTLSConfigWithValidation(conn *proxyConnection) *tls.Config {
77-
return &tls.Config{
76+
func (p *RDCleanPathProxy) getTLSConfigWithValidation(conn *proxyConnection, requiresCredSSP bool) *tls.Config {
77+
config := &tls.Config{
7878
InsecureSkipVerify: true, // We'll validate manually after handshake
7979
VerifyConnection: func(cs tls.ConnectionState) error {
8080
var certChain [][]byte
@@ -93,4 +93,15 @@ func (p *RDCleanPathProxy) getTLSConfigWithValidation(conn *proxyConnection) *tl
9393
return nil
9494
},
9595
}
96+
97+
// CredSSP (NLA) requires TLS 1.2 - it's incompatible with TLS 1.3
98+
if requiresCredSSP {
99+
config.MinVersion = tls.VersionTLS12
100+
config.MaxVersion = tls.VersionTLS12
101+
} else {
102+
config.MinVersion = tls.VersionTLS12
103+
config.MaxVersion = tls.VersionTLS13
104+
}
105+
106+
return config
96107
}

client/wasm/internal/rdp/rdcleanpath.go

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import (
66
"context"
77
"crypto/tls"
88
"encoding/asn1"
9+
"errors"
910
"fmt"
1011
"io"
1112
"net"
1213
"sync"
1314
"syscall/js"
15+
"time"
1416

1517
log "github.com/sirupsen/logrus"
1618
)
@@ -19,18 +21,34 @@ const (
1921
RDCleanPathVersion = 3390
2022
RDCleanPathProxyHost = "rdcleanpath.proxy.local"
2123
RDCleanPathProxyScheme = "ws"
24+
25+
rdpDialTimeout = 15 * time.Second
26+
27+
GeneralErrorCode = 1
28+
WSAETimedOut = 10060
29+
WSAEConnRefused = 10061
30+
WSAEConnAborted = 10053
31+
WSAEConnReset = 10054
32+
WSAEGenericError = 10050
2233
)
2334

2435
type RDCleanPathPDU struct {
25-
Version int64 `asn1:"tag:0,explicit"`
26-
Error []byte `asn1:"tag:1,explicit,optional"`
27-
Destination string `asn1:"utf8,tag:2,explicit,optional"`
28-
ProxyAuth string `asn1:"utf8,tag:3,explicit,optional"`
29-
ServerAuth string `asn1:"utf8,tag:4,explicit,optional"`
30-
PreconnectionBlob string `asn1:"utf8,tag:5,explicit,optional"`
31-
X224ConnectionPDU []byte `asn1:"tag:6,explicit,optional"`
32-
ServerCertChain [][]byte `asn1:"tag:7,explicit,optional"`
33-
ServerAddr string `asn1:"utf8,tag:9,explicit,optional"`
36+
Version int64 `asn1:"tag:0,explicit"`
37+
Error RDCleanPathErr `asn1:"tag:1,explicit,optional"`
38+
Destination string `asn1:"utf8,tag:2,explicit,optional"`
39+
ProxyAuth string `asn1:"utf8,tag:3,explicit,optional"`
40+
ServerAuth string `asn1:"utf8,tag:4,explicit,optional"`
41+
PreconnectionBlob string `asn1:"utf8,tag:5,explicit,optional"`
42+
X224ConnectionPDU []byte `asn1:"tag:6,explicit,optional"`
43+
ServerCertChain [][]byte `asn1:"tag:7,explicit,optional"`
44+
ServerAddr string `asn1:"utf8,tag:9,explicit,optional"`
45+
}
46+
47+
type RDCleanPathErr struct {
48+
ErrorCode int16 `asn1:"tag:0,explicit"`
49+
HTTPStatusCode int16 `asn1:"tag:1,explicit,optional"`
50+
WSALastError int16 `asn1:"tag:2,explicit,optional"`
51+
TLSAlertCode int8 `asn1:"tag:3,explicit,optional"`
3452
}
3553

3654
type RDCleanPathProxy struct {
@@ -210,23 +228,29 @@ func (p *RDCleanPathProxy) handleDirectRDP(conn *proxyConnection, firstPacket []
210228
destination := conn.destination
211229
log.Infof("Direct RDP mode: Connecting to %s via NetBird", destination)
212230

213-
rdpConn, err := p.nbClient.Dial(conn.ctx, "tcp", destination)
231+
ctx, cancel := context.WithTimeout(conn.ctx, rdpDialTimeout)
232+
defer cancel()
233+
234+
rdpConn, err := p.nbClient.Dial(ctx, "tcp", destination)
214235
if err != nil {
215236
log.Errorf("Failed to connect to %s: %v", destination, err)
237+
p.sendRDCleanPathError(conn, newWSAError(err))
216238
return
217239
}
218240
conn.rdpConn = rdpConn
219241

220242
_, err = rdpConn.Write(firstPacket)
221243
if err != nil {
222244
log.Errorf("Failed to write first packet: %v", err)
245+
p.sendRDCleanPathError(conn, newWSAError(err))
223246
return
224247
}
225248

226249
response := make([]byte, 1024)
227250
n, err := rdpConn.Read(response)
228251
if err != nil {
229252
log.Errorf("Failed to read X.224 response: %v", err)
253+
p.sendRDCleanPathError(conn, newWSAError(err))
230254
return
231255
}
232256

@@ -269,3 +293,52 @@ func (p *RDCleanPathProxy) sendToWebSocket(conn *proxyConnection, data []byte) {
269293
conn.wsHandlers.Call("send", uint8Array.Get("buffer"))
270294
}
271295
}
296+
297+
func (p *RDCleanPathProxy) sendRDCleanPathError(conn *proxyConnection, pdu RDCleanPathPDU) {
298+
data, err := asn1.Marshal(pdu)
299+
if err != nil {
300+
log.Errorf("Failed to marshal error PDU: %v", err)
301+
return
302+
}
303+
p.sendToWebSocket(conn, data)
304+
}
305+
306+
func errorToWSACode(err error) int16 {
307+
if err == nil {
308+
return WSAEGenericError
309+
}
310+
var netErr *net.OpError
311+
if errors.As(err, &netErr) && netErr.Timeout() {
312+
return WSAETimedOut
313+
}
314+
if errors.Is(err, context.DeadlineExceeded) {
315+
return WSAETimedOut
316+
}
317+
if errors.Is(err, context.Canceled) {
318+
return WSAEConnAborted
319+
}
320+
if errors.Is(err, io.EOF) {
321+
return WSAEConnReset
322+
}
323+
return WSAEGenericError
324+
}
325+
326+
func newWSAError(err error) RDCleanPathPDU {
327+
return RDCleanPathPDU{
328+
Version: RDCleanPathVersion,
329+
Error: RDCleanPathErr{
330+
ErrorCode: GeneralErrorCode,
331+
WSALastError: errorToWSACode(err),
332+
},
333+
}
334+
}
335+
336+
func newHTTPError(statusCode int16) RDCleanPathPDU {
337+
return RDCleanPathPDU{
338+
Version: RDCleanPathVersion,
339+
Error: RDCleanPathErr{
340+
ErrorCode: GeneralErrorCode,
341+
HTTPStatusCode: statusCode,
342+
},
343+
}
344+
}

client/wasm/internal/rdp/rdcleanpath_handlers.go

Lines changed: 56 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package rdp
44

55
import (
6+
"context"
67
"crypto/tls"
78
"encoding/asn1"
89
"io"
@@ -11,11 +12,17 @@ import (
1112
log "github.com/sirupsen/logrus"
1213
)
1314

15+
const (
16+
// MS-RDPBCGR: confusingly named, actually means PROTOCOL_HYBRID (CredSSP)
17+
protocolSSL = 0x00000001
18+
protocolHybridEx = 0x00000008
19+
)
20+
1421
func (p *RDCleanPathProxy) processRDCleanPathPDU(conn *proxyConnection, pdu RDCleanPathPDU) {
1522
log.Infof("Processing RDCleanPath PDU: Version=%d, Destination=%s", pdu.Version, pdu.Destination)
1623

1724
if pdu.Version != RDCleanPathVersion {
18-
p.sendRDCleanPathError(conn, "Unsupported version")
25+
p.sendRDCleanPathError(conn, newHTTPError(400))
1926
return
2027
}
2128

@@ -24,10 +31,13 @@ func (p *RDCleanPathProxy) processRDCleanPathPDU(conn *proxyConnection, pdu RDCl
2431
destination = pdu.Destination
2532
}
2633

27-
rdpConn, err := p.nbClient.Dial(conn.ctx, "tcp", destination)
34+
ctx, cancel := context.WithTimeout(conn.ctx, rdpDialTimeout)
35+
defer cancel()
36+
37+
rdpConn, err := p.nbClient.Dial(ctx, "tcp", destination)
2838
if err != nil {
2939
log.Errorf("Failed to connect to %s: %v", destination, err)
30-
p.sendRDCleanPathError(conn, "Connection failed")
40+
p.sendRDCleanPathError(conn, newWSAError(err))
3141
p.cleanupConnection(conn)
3242
return
3343
}
@@ -40,36 +50,75 @@ func (p *RDCleanPathProxy) processRDCleanPathPDU(conn *proxyConnection, pdu RDCl
4050
p.setupTLSConnection(conn, pdu)
4151
}
4252

53+
// detectCredSSPFromX224 checks if the X.224 response indicates NLA/CredSSP is required.
54+
// Per MS-RDPBCGR spec: byte 11 = TYPE_RDP_NEG_RSP (0x02), bytes 15-18 = selectedProtocol flags.
55+
// Returns (requiresTLS12, selectedProtocol, detectionSuccessful).
56+
func (p *RDCleanPathProxy) detectCredSSPFromX224(x224Response []byte) (bool, uint32, bool) {
57+
const minResponseLength = 19
58+
59+
if len(x224Response) < minResponseLength {
60+
return false, 0, false
61+
}
62+
63+
// Per X.224 specification:
64+
// x224Response[0] == 0x03: Length of X.224 header (3 bytes)
65+
// x224Response[5] == 0xD0: X.224 Data TPDU code
66+
if x224Response[0] != 0x03 || x224Response[5] != 0xD0 {
67+
return false, 0, false
68+
}
69+
70+
if x224Response[11] == 0x02 {
71+
flags := uint32(x224Response[15]) | uint32(x224Response[16])<<8 |
72+
uint32(x224Response[17])<<16 | uint32(x224Response[18])<<24
73+
74+
hasNLA := (flags & (protocolSSL | protocolHybridEx)) != 0
75+
return hasNLA, flags, true
76+
}
77+
78+
return false, 0, false
79+
}
80+
4381
func (p *RDCleanPathProxy) setupTLSConnection(conn *proxyConnection, pdu RDCleanPathPDU) {
4482
var x224Response []byte
4583
if len(pdu.X224ConnectionPDU) > 0 {
4684
log.Debugf("Forwarding X.224 Connection Request (%d bytes)", len(pdu.X224ConnectionPDU))
4785
_, err := conn.rdpConn.Write(pdu.X224ConnectionPDU)
4886
if err != nil {
4987
log.Errorf("Failed to write X.224 PDU: %v", err)
50-
p.sendRDCleanPathError(conn, "Failed to forward X.224")
88+
p.sendRDCleanPathError(conn, newWSAError(err))
5189
return
5290
}
5391

5492
response := make([]byte, 1024)
5593
n, err := conn.rdpConn.Read(response)
5694
if err != nil {
5795
log.Errorf("Failed to read X.224 response: %v", err)
58-
p.sendRDCleanPathError(conn, "Failed to read X.224 response")
96+
p.sendRDCleanPathError(conn, newWSAError(err))
5997
return
6098
}
6199
x224Response = response[:n]
62100
log.Debugf("Received X.224 Connection Confirm (%d bytes)", n)
63101
}
64102

65-
tlsConfig := p.getTLSConfigWithValidation(conn)
103+
requiresCredSSP, selectedProtocol, detected := p.detectCredSSPFromX224(x224Response)
104+
if detected {
105+
if requiresCredSSP {
106+
log.Warnf("Detected NLA/CredSSP (selectedProtocol: 0x%08X), forcing TLS 1.2 for compatibility", selectedProtocol)
107+
} else {
108+
log.Warnf("No NLA/CredSSP detected (selectedProtocol: 0x%08X), allowing up to TLS 1.3", selectedProtocol)
109+
}
110+
} else {
111+
log.Warnf("Could not detect RDP security protocol, allowing up to TLS 1.3")
112+
}
113+
114+
tlsConfig := p.getTLSConfigWithValidation(conn, requiresCredSSP)
66115

67116
tlsConn := tls.Client(conn.rdpConn, tlsConfig)
68117
conn.tlsConn = tlsConn
69118

70119
if err := tlsConn.Handshake(); err != nil {
71120
log.Errorf("TLS handshake failed: %v", err)
72-
p.sendRDCleanPathError(conn, "TLS handshake failed")
121+
p.sendRDCleanPathError(conn, newWSAError(err))
73122
return
74123
}
75124

@@ -106,47 +155,6 @@ func (p *RDCleanPathProxy) setupTLSConnection(conn *proxyConnection, pdu RDClean
106155
p.cleanupConnection(conn)
107156
}
108157

109-
func (p *RDCleanPathProxy) setupPlainConnection(conn *proxyConnection, pdu RDCleanPathPDU) {
110-
if len(pdu.X224ConnectionPDU) > 0 {
111-
log.Debugf("Forwarding X.224 Connection Request (%d bytes)", len(pdu.X224ConnectionPDU))
112-
_, err := conn.rdpConn.Write(pdu.X224ConnectionPDU)
113-
if err != nil {
114-
log.Errorf("Failed to write X.224 PDU: %v", err)
115-
p.sendRDCleanPathError(conn, "Failed to forward X.224")
116-
return
117-
}
118-
119-
response := make([]byte, 1024)
120-
n, err := conn.rdpConn.Read(response)
121-
if err != nil {
122-
log.Errorf("Failed to read X.224 response: %v", err)
123-
p.sendRDCleanPathError(conn, "Failed to read X.224 response")
124-
return
125-
}
126-
127-
responsePDU := RDCleanPathPDU{
128-
Version: RDCleanPathVersion,
129-
X224ConnectionPDU: response[:n],
130-
ServerAddr: conn.destination,
131-
}
132-
133-
p.sendRDCleanPathPDU(conn, responsePDU)
134-
} else {
135-
responsePDU := RDCleanPathPDU{
136-
Version: RDCleanPathVersion,
137-
ServerAddr: conn.destination,
138-
}
139-
p.sendRDCleanPathPDU(conn, responsePDU)
140-
}
141-
142-
go p.forwardConnToWS(conn, conn.rdpConn, "TCP")
143-
go p.forwardWSToConn(conn, conn.rdpConn, "TCP")
144-
145-
<-conn.ctx.Done()
146-
log.Debug("TCP connection context done, cleaning up")
147-
p.cleanupConnection(conn)
148-
}
149-
150158
func (p *RDCleanPathProxy) sendRDCleanPathPDU(conn *proxyConnection, pdu RDCleanPathPDU) {
151159
data, err := asn1.Marshal(pdu)
152160
if err != nil {
@@ -158,21 +166,6 @@ func (p *RDCleanPathProxy) sendRDCleanPathPDU(conn *proxyConnection, pdu RDClean
158166
p.sendToWebSocket(conn, data)
159167
}
160168

161-
func (p *RDCleanPathProxy) sendRDCleanPathError(conn *proxyConnection, errorMsg string) {
162-
pdu := RDCleanPathPDU{
163-
Version: RDCleanPathVersion,
164-
Error: []byte(errorMsg),
165-
}
166-
167-
data, err := asn1.Marshal(pdu)
168-
if err != nil {
169-
log.Errorf("Failed to marshal error PDU: %v", err)
170-
return
171-
}
172-
173-
p.sendToWebSocket(conn, data)
174-
}
175-
176169
func (p *RDCleanPathProxy) readWebSocketMessage(conn *proxyConnection) ([]byte, error) {
177170
msgChan := make(chan []byte)
178171
errChan := make(chan error)

0 commit comments

Comments
 (0)