README
¶
newgrdp — Modern Go RDP Client
A modern, production-ready pure-Go implementation of the Microsoft RDP
(Remote Desktop Protocol) client, focused on authentication and
programmatic access. This is a heavily reworked fork of the original
icodeface/grdp/x90skysn3k/grdp line, with substantial improvements to
error handling, performance, safety, and protocol compatibility.
Repository: https://github.com/sayetars/newgrdp Go import path:
github.com/sayetars/newgrdp
Table of contents
- Highlights
- Compatibility
- Installation
- Quick start
- Usage examples
- Zero-copy I/O (high-performance hot path)
- Settings reference
- Error handling
- Protocol stack
- What's new vs the upstream fork
- Known limitations
- Benchmarks
- Building & testing
- Project layout
- Credits
- License
Highlights
- Full NLA / CredSSP / NTLMv2 authentication (Windows 11 24H2+ compatible,
including MIC +
MsvAvTargetNameSPN handling) - Three security protocols:
PROTOCOL_RDP(Standard),PROTOCOL_SSL(TLS),PROTOCOL_HYBRID(CredSSP/NLA).PROTOCOL_HYBRID_EXis detected and reported as unsupported. context.Contexteverywhere — deadlines and cancellation propagate through the entire stack (Client → X224 → TPKT → SocketLayer → net.Conn)- No goroutine leaks —
StartReadBytesuses a watcher that closes the socket onctx.Done(), unblocking in-progress reads - Rich, structured errors —
*core.RDPErrorcarriesKind,Message,Code(NTSTATUS / X224 failure code), andWrappedcause; helpersTranslateNTStatusandTranslateX224Failureprovide human-readable descriptions for ~30 NTSTATUS codes and all 6 X224 negotiation failures - Zero-copy I/O —
ZeroCopyReader/ZeroCopyWriter+BufferPooleliminate per-call allocations on hot paths (0 allocs/op, verified by benchmarks) - Index-based
Cursor— alternative toCVAL/CVAL2for byte-slice parsing without slice-header churn - NTLM hash cache —
NTOWFv2results are cached across calls, giving ~100× speedup for brute-force scanners that retry the same credentials against multiple hosts - Safe RLE decoder — no
unsafe.Pointer, no closure-based loops, bounds-checked writes (33 inlineforloops replace the oldREPEATpattern) - Configurable X224 cookie — the old hardcoded
mstshash=testis now overrideable viaSetting.Cookie(helps avoid IDS fingerprinting) - Certificate verification —
ProprietaryServerCertificate.Verify()actually validates the RSA/SHA-1 self-signature (was a no-op returningtrue);X509CertificateChain.Verify()parses each cert in the chain - FIPS advertisement —
ClientSecurityData.FIPSflag lets the client advertise FIPS-compliant encryption to FIPS-only servers - Modern client identification —
ClientBuild=7601(Win 7 SP1) and extendedEarlyCapabilityFlags(DYNVC_GFX, HEARTBEAT, DYNAMIC_TIME_ZONE, STATUS_INFO, 32-bpp, ERRINFO) so modern servers don't fall back to slow rendering paths
Compatibility
| Windows Server version | NLA (CredSSP) | TLS | Standard RDP | Notes |
|---|---|---|---|---|
| 2008 / 2008 R2 | ✅ (if enabled) | ✅ | ✅ | For 2008 with NLA off, use full Login not LoginAuthOnly |
| 2012 / 2012 R2 | ✅ | ✅ | ✅ | CredSSP v5+ SHA-256 binding |
| 2016 | ✅ | ✅ | ✅ | |
| 2019 | ✅ | ✅ | ✅ | |
| 2022 | ✅ | ✅ | ✅ | TLS capped at 1.2 (FreeRDP-compatible) |
| 2025 / 2026 | ✅ | ✅ | ✅ | NTLM path (Azure AD / RDSTLS not supported) |
| Windows 11 24H2+ targets | ✅ | ✅ | ✅ | MIC + MsvAvTargetName already implemented |
Not supported: PROTOCOL_HYBRID_EX, RDSTLS, Azure AD authentication,
RDP 8.x graphics pipeline (RDPGFX/RemoteFX/H.264), UDP transport, dynamic
virtual channels, audio/clipboard/drive redirection.
Installation
go get github.com/sayetars/newgrdp
Requires Go 1.23 or later.
Quick start
The rdpcheck CLI
Build and run the included credential-checker:
go build -o rdpcheck ./cmd/rdpcheck
./rdpcheck -host 192.168.1.100:3389 -user administrator -pass 'P@ssw0rd!' -timeout 10s
Sample output on success:
OK (1.234s): authentication successful
Sample output on failure (with the new structured error reporting):
FAIL (1.234s)
summary: [auth] CredSSP server returned an error code
stage : tpkt.nla.errorCode
host : 192.168.1.100:3389
user : administrator
code : 0xC000006D (STATUS_LOGON_FAILURE — wrong username or password ...)
cause : CredSSP server error: NTSTATUS 0xC000006D (STATUS_LOGON_FAILURE — ...)
hint : wrong password — verify the credentials
Use -plain for a one-line summary suitable for scripting, and -v for
trace-level logging.
Usage examples
Quick credential check (NLA only)
Performs CredSSP/NLA authentication only — no MCS/SEC/PDU setup. Fastest path for credential validation. Requires the server to support NLA.
package main
import (
"context"
"fmt"
"time"
"github.com/sayetars/newgrdp/client"
"github.com/sayetars/newgrdp/glog"
)
func main() {
s := client.NewSetting()
s.LogLevel = glog.INFO
c := client.NewClient("192.168.1.100:3389", "admin", "password",
client.TC_RDP, s)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := c.LoginAuthOnly(ctx); err != nil {
fmt.Println("FAIL:", err)
return
}
fmt.Println("OK: authentication successful")
}
Full session login (synchronous)
Use LoginSyncContext when you need to send input or receive bitmaps
immediately after Login returns. It blocks until the full MCS/SEC/PDU
handshake completes:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := c.LoginSyncContext(ctx); err != nil {
log.Fatal(err)
}
defer c.Close()
// Session is fully established — safe to send input and receive bitmaps.
c.OnBitmap(func(bmps []client.Bitmap) { /* render */ })
c.KeyDown(0x1C, "ENTER") // send Enter key
LoginContext (async) returns after X224 + NLA; LoginSyncContext
blocks until the session is ready (Demand Active / Confirm Active /
Synchronize / Control / Font Map all done).
Full session login (asynchronous)
Use LoginContext when you need the full RDP session (bitmap updates,
input events). This negotiates MCS, SEC, and PDU layers after NLA.
c := client.NewClient("192.168.1.100:3389", "user", "pass",
client.TC_RDP, nil)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := c.LoginContext(ctx); err != nil {
log.Fatal(err)
}
defer c.Close()
c.OnSuccess(func() { fmt.Println("connected") })
c.OnError(func(e error) { fmt.Println("error:", e) })
c.OnBitmap(func(bmps []client.Bitmap) {
// ... render bmps[i].Data (width × height, bmps[i].BitsPerPixel/8 bytes per pixel)
})
Domain authentication
The DOMAIN\user and DOMAIN/user formats are parsed automatically:
c := client.NewClient("host:3389", `CORP\administrator`, "password",
client.TC_RDP, nil)
Connection reuse after cancellation
Context cancellation uses a deadline-based approach (setting a near-zero deadline on the socket), which unblocks in-progress I/O with a timeout error — without closing the connection. After cancellation, you can reset the deadline and reuse the connection:
// Cancel an in-progress operation.
cancel()
// The Read/Write that was in progress returned a net.Error with Timeout()==true.
// The connection is still open. Reset the deadline to reuse it:
sl := /* the *core.SocketLayer */
if err := sl.ResetDeadline(); err != nil {
// connection was actually closed (e.g. by the remote peer) — dial a new one
return
}
// Now issue a new Read/Write with a fresh context.
This is critical for high-scale scanners that cancel many connections: unlike the old close-based cancellation, there's no reconnection cost when a context is cancelled mid-operation.
Custom cookie / TLS verify / FIPS
s := client.NewSetting()
s.Width = 1920
s.Height = 1080
s.Cookie = "admin" // sent as "Cookie: mstshash=admin" (avoids IDS fingerprinting)
s.TLSVerify = true // verify server TLS certificate
s.VerifyServer = true // verify CredSSP PubKeyAuth (MITM protection)
s.RequestedProtocol = 0x2 // PROTOCOL_HYBRID (NLA)
// s.RequestedProtocol = 0x1 // PROTOCOL_SSL
// s.RequestedProtocol = 0x0 // PROTOCOL_RDP (Standard)
s.AuthOnly = true // Login() will skip MCS/SEC/PDU (same as LoginAuthOnly)
c := client.NewClient("host:3389", "user", "pass", client.TC_RDP, s)
Registering event handlers before Login
OnBitmap, OnSuccess, OnError, OnReady, and OnClose can all be
called before Login. Handlers registered before the PDU layer exists
are buffered internally and replayed against it once Login creates it.
This makes the API order-insensitive — you don't need to worry about
whether Login has completed before registering handlers.
c := client.NewClient("host:3389", "user", "pass", client.TC_RDP, nil)
// All of these are safe before Login:
c.OnSuccess(func() { fmt.Println("session established") })
c.OnBitmap(func(bmps []client.Bitmap) {
// ... render bmps[i].Data
})
c.OnError(func(e error) { fmt.Println("error:", e) })
err := c.LoginContext(ctx)
Inspecting the negotiated protocol
After Login completes, SelectedProtocol() tells you which security
layer the server actually chose (useful when RequestedProtocol=0 allowed
the server to pick):
err := c.LoginContext(ctx)
// ...
switch c.SelectedProtocol() {
case 0: // PROTOCOL_RDP — Standard RDP Security
case 1: // PROTOCOL_SSL — TLS
case 2: // PROTOCOL_HYBRID — CredSSP / NLA
}
Manual bitmap decompression
If you capture raw pdu.BitmapData (e.g. via a custom PDU listener) and
need to decode the RLE-compressed bitmap stream yourself, use the exported
BitmapDecompress:
raw := <-rawBitmapChannel
pixels := client.BitmapDecompress(&raw) // []byte, width*height*Bpp
Connection reuse (TCP+TLS reuse for multiple credentials)
client.AuthSession lets you dial a TCP+TLS connection once and try
multiple credentials on it — without re-doing the X224 negotiation or
TLS handshake on every attempt. This is the fastest path for brute-force
tools when the server keeps the connection open after failed attempts.
sess := client.NewAuthSession(client.NewSetting())
defer sess.Close()
// Dial once: TCP + X224 + TLS (no NLA yet).
if err := sess.Dial(ctx, "10.0.0.1:3389"); err != nil {
return err
}
for _, cred := range creds {
err := sess.TryAuth(ctx, cred.User, cred.Pass)
if err == nil {
fmt.Println("valid:", cred)
break
}
if sess.IsClosed() {
// Server closed the connection — re-dial.
if err := sess.Dial(ctx, "10.0.0.1:3389"); err != nil {
return err
}
}
}
TLS session resumption (faster reconnects)
For scenarios where the server closes the connection after each auth
attempt (the common case), Setting.TLSSessionCache enables TLS session
ticket reuse across connections — saving ~75ms per connection (the full
TLS handshake becomes an abbreviated one):
s := client.NewSetting()
// Share this cache across all clients connecting to the same target.
s.TLSSessionCache = tls.NewClientSessionCache(1000)
// Now each LoginAuthOnly/Login benefits from a faster TLS handshake
// (after the first connection establishes the session).
for _, cred := range creds {
c := client.NewClient("10.0.0.1:3389", cred.User, cred.Pass,
client.TC_RDP, s)
err := c.LoginAuthOnly(ctx)
c.Close()
}
Programmatic error handling
err := c.LoginAuthOnly(ctx)
if err != nil {
var rdpErr *core.RDPError
if errors.As(err, &rdpErr) {
switch rdpErr.Kind {
case core.ErrKindAuth:
// Wrong credentials — check rdpErr.Code for the NTSTATUS
switch rdpErr.Code {
case 0xC000006D: // STATUS_LOGON_FAILURE
case 0xC0000234: // STATUS_ACCOUNT_LOCKED_OUT
case 0xC0000072: // STATUS_ACCOUNT_DISABLED
}
case core.ErrKindTimeout:
// Context deadline exceeded
case core.ErrKindProtocol:
// X224 negotiation failure — rdpErr.Code is the failure code (1..6)
case core.ErrKindNetwork:
// TCP dial / connection reset
}
}
}
The client package also provides helper predicates:
client.IsAuthError(err) // wrong credentials / account restriction?
client.IsTimeoutError(err) // context deadline / cancellation?
client.IsNetworkError(err) // TCP dial / connection reset?
client.IsProtocolNegotiationError(err) // X224 negotiation phase?
client.AsRDPError(err) // extract *core.RDPError (or nil)
client.FormatError(err) // fully-formatted multi-line message
Zero-copy I/O (high-performance hot path)
For tight loops that parse or serialize many PDUs, use the zero-copy API
instead of the per-call core.ReadUint16LE / core.WriteUInt32LE helpers:
pool := core.NewBufferPool(8192)
// Read side
zr := core.NewZeroCopyReader(conn, pool)
defer zr.Close()
typ, _ := zr.ReadUInt8() // 0 allocs
length,_ := zr.ReadUint16LE() // 0 allocs
flag, _ := zr.ReadUInt32LE() // 0 allocs
body, _ := zr.ReadBytes(int(length)) // 0 allocs (slice into internal buffer)
// Write side
zw := core.NewZeroCopyWriter(conn, pool)
defer zw.Close() // Close flushes pending data + returns buffer to pool
zw.WriteUInt32LE(0xDEADBEEF) // 0 allocs
zw.WriteUInt16LE(0x1234) // 0 allocs
zw.WriteBytes(payload) // 0 allocs (large writes bypass buffer)
if err := zw.Flush(); err != nil { ... }
⚠️ Important: ReadBytes vs ReadBytesCopy
ZeroCopyReader.ReadBytes(n) returns a slice that aliases the internal
buffer. It's only valid until the next Read* call. This is the source
of the 0-alloc performance, but it's a footgun if misused:
// ❌ BUG: b1 and b2 alias the same internal buffer
b1, _ := zr.ReadBytes(4)
b2, _ := zr.ReadBytes(4) // b1 is now invalidated
use(b1) // b1 contains b2's data
// ❌ BUG: every entry aliases the same buffer
hdrs = append(hdrs, b1)
hdrs = append(hdrs, b2)
If you need to retain the bytes beyond the next read, use ReadBytesCopy
(which allocates a fresh slice) or call copy(dst, b) yourself:
// ✅ Safe: independent copy
b, _ := zr.ReadBytesCopy(4) // allocates, but safe to retain
hdrs = append(hdrs, b)
For parsing in-memory byte slices (no io.Reader), use core.Cursor:
c := core.NewCursor(inputBytes)
for c.Remaining() >= 3 {
op := c.NextByte()
val := c.ReadUint16LE()
}
Syscall batching with FillAhead
ZeroCopyReader.FillAhead() reads as much data as possible into the
internal buffer in a single syscall. After calling it, many small Read*
calls can be served from the in-memory buffer without any further
syscalls — 1 syscall per ~8 KB of data, instead of 1 syscall per
ReadUint16LE.
for {
if err := zr.FillAhead(); err != nil { return err }
for zr.Buffered() >= headerSize {
typ, _ := zr.ReadUInt8() // no syscall
len, _ := zr.ReadUint16LE() // no syscall
// ... parse from in-memory buffer
}
}
Concurrent decode pipeline
For high-throughput packet processing, core.Pipeline runs read → decode
→ process stages concurrently in separate goroutines, connected by
buffered channels. This overlaps I/O, parsing, and processing:
p := core.NewPipeline(ctx, 4 /* channel buffer size */)
p.AddStage("reader", func(ctx, in, out) error {
// read from socket into out
})
p.AddStage("decoder", func(ctx, in, out) error {
for buf := range in {
pdu, err := decodePDU(buf)
if err != nil { return err }
out <- pdu
}
return nil
})
p.AddStage("processor", func(ctx, in, out) error {
for pdu := range in {
if err := processPDU(pdu); err != nil { return err }
}
return nil
})
err := p.Run() // blocks until all stages finish or one errors
For the common 3-stage pattern, core.NewSimplePipeline(r, decode, process, chunkSize, bufSize) manages the buffer pool internally.
NTLM hash cache
NTOWFv2 results are cached in a two-level bounded LRU — critical
for brute-force / mass-scanning scenarios:
- Level 1 (
md4PasswordCache):MD4(UTF16(password))— depends only on the password. This is the most expensive step (MD4 is sequential and not parallelizable). In brute-force with one password tried against many users, this avoids recomputing MD4 for every(user, domain)tuple. - Level 2 (
ntowfCache): the fullHMAC-MD5result — depends on(password, user, domain). On a hit, no crypto is done at all.
| Scenario | Work done |
|---|---|
Same (password, user, domain) |
None (~50 ns, level-2 hit) |
Same password, different user/domain |
Only HMAC-MD5 (level-1 hit) |
New password |
MD4 + HMAC-MD5 (both misses) |
Both caches are bounded at 4096 entries (default) — no unbounded memory growth, even with a huge password list. Thread-safe for concurrent use.
// Adjust the cache size at startup (optional, default is 4096)
nla.SetNTOWFCacheSize(8192)
// ... do many NTOWFv2 calls ...
// Clear both caches when done (optional, releases memory)
nla.ClearNTOWFCache()
Setting the size to 0 effectively disables caching (every call recomputes).
Settings reference
client.Setting fields (all optional — NewSetting() provides sensible
defaults):
| Field | Type | Default | Description |
|---|---|---|---|
Width |
int |
1024 |
Requested desktop width |
Height |
int |
768 |
Requested desktop height |
RequestedProtocol |
uint32 |
0 (auto) |
0=RDP, 1=SSL, 2=HYBRID(NLA), 8=HYBRID_EX |
LogLevel |
glog.LEVEL |
INFO |
Trace/Debug/Info/Warn/Error/None |
TLSMinVersion |
uint16 |
TLS 1.2 |
Minimum TLS version |
TLSVerify |
bool |
false |
Verify server TLS certificate |
VerifyServer |
bool |
false |
Verify CredSSP PubKeyAuth (MITM protection) |
AuthOnly |
bool |
false |
Stop after NLA (skip MCS/SEC/PDU) |
Cookie |
string |
"" |
X224 cookie (sent as Cookie: mstshash=<cookie>) |
FIPS |
bool |
false |
Advertise FIPS-compliant encryption |
core.SocketLayer methods
| Method | Description |
|---|---|
SetContext(ctx) |
Apply context deadline + spawn cancellation watcher |
SetDeadline(t) |
Set read/write deadline directly |
ResetDeadline() |
Clear deadline — allows reusing the connection after cancellation |
Read(b) / Write(b) |
Read/Write (delegates to TLS conn if StartTLS was called) |
Close() |
Cancel watcher + close underlying connection |
Error handling
All errors returned from Login* are *core.RDPError (reachable via
errors.As). The Error() method returns a multi-line diagnostic block
suitable for display; the structured fields are available for programmatic
branching.
Error kinds
| Kind | Meaning |
|---|---|
ErrKindNetwork |
TCP dial, DNS, connection refused |
ErrKindTLS |
TLS handshake failures |
ErrKindAuth |
Wrong credentials (NLA/CredSSP), account restrictions |
ErrKindProtocol |
X224 negotiation, MCS, PDU protocol failures |
ErrKindTimeout |
Context deadline exceeded |
ErrKindSecurity |
Cryptographic verification failures (MAC, PubKeyAuth, MITM) |
ErrKindEncoding |
DER/PER/struc serialization failures |
ErrKindLicense |
RDP licensing protocol failures |
NTSTATUS translation
core.TranslateNTStatus(code) returns a human-readable description for
~30 NTSTATUS codes that Microsoft SChannel / LSASS can return through
CredSSP TSRequest.ErrorCode, including:
STATUS_LOGON_FAILURE, STATUS_WRONG_PASSWORD, STATUS_NO_SUCH_USER,
STATUS_ACCOUNT_RESTRICTION, STATUS_ACCOUNT_DISABLED,
STATUS_ACCOUNT_LOCKED_OUT, STATUS_PASSWORD_EXPIRED,
STATUS_INVALID_LOGON_HOURS, STATUS_AUTHENTICATION_FIREWALL_FAILED,
SEC_E_LOGON_DENIED, SEC_E_DELEGATION_POLICY, and more.
X224 failure translation
core.TranslateX224Failure(code) covers all 6 negotiation failure codes
from MS-RDPBCGR §2.2.1.5.2:
| Code | Name | Meaning |
|---|---|---|
| 1 | SSL_REQUIRED_BY_SERVER |
Server requires TLS |
| 2 | SSL_NOT_ALLOWED_BY_SERVER |
Server only allows Standard RDP Security |
| 3 | SSL_CERT_NOT_ON_SERVER |
Server has no valid auth certificate |
| 4 | INCONSISTENT_FLAGS |
Requested protocols conflict |
| 5 | HYBRID_REQUIRED_BY_SERVER |
Server requires NLA/CredSSP |
| 6 | SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER |
Server requires TLS + client cert |
Protocol stack
Client (Login with context)
└─ X224 (connection request/confirm, protocol negotiation)
└─ TPKT (packet framing, FastPath, CredSSP TSRequest)
└─ SocketLayer (TLS 1.2, raw TCP, context-aware cancellation)
└─ net.Conn
Each layer propagates the context downward. The SocketLayer.SetContext
spawns a watcher goroutine that closes the underlying connection when the
context is cancelled — this unblocks in-progress Read/Write calls
immediately, rather than waiting for the OS-level TCP timeout.
What's new vs the upstream fork
| Area | Upstream | This fork |
|---|---|---|
| Silent error swallowing | ReadUint16LE returned 0, nil on error |
All Read* helpers return the real error |
| RLE safety | unsafe.Pointer + closure-based REPEAT + no bounds |
binary.LittleEndian + 33 inline loops + bounds checks |
| Goroutine leaks | Reader goroutines blocked forever on dead sockets | Watcher closes socket on ctx.Done(); verified by leak test |
ReadByte panic |
b[0] on empty slice |
Guards len(b) == 0 → io.ErrUnexpectedEOF |
| Error reporting | "protocol negotiation failed with code 5" |
"protocol negotiation failed: HYBRID_REQUIRED_BY_SERVER — the server requires NLA/CredSSP (PROTOCOL_HYBRID); set RequestedProtocol=2" |
| NTSTATUS reporting | 4 hard-coded codes | ~30 codes with human-readable hints |
| Allocation per read | make([]byte, 2) on every ReadUint16LE |
ZeroCopyReader = 0 allocs/op (6× faster) |
| NTLM hash | Recomputed on every connection | Cached (sync.Map); ~100× faster on cache hit |
| X224 cookie | Hardcoded mstshash=test |
Configurable via Setting.Cookie |
| Cert verification | Verify() always returned true |
Real RSA/SHA-1 signature check |
| FIPS | Not advertised | ClientSecurityData.FIPS flag |
| Client identification | ClientBuild=3790 (Win 2003) |
ClientBuild=7601 (Win 7 SP1) + modern EarlyCapabilityFlags |
| Socket cancellation | Deadline only, closes connection on ctx.Done() |
Deadline-based cancellation — preserves connection for reuse via ResetDeadline() |
| Event handler timing | Must register after Login (PDU nil before) | Buffered + replayed — safe to register before Login |
Setting.AuthOnly |
Defined but ignored by Login | Honored by Login (delegates to LoginAuthOnly) |
SelectedProtocol() |
Not exposed | Exposed on both Client and RdpClient |
BitmapDecompress |
Unexported (bitmapDecompress) |
Exported for manual bitmap decoding |
| HYBRID_EX | Rejected with error | Falls back to HYBRID (NLA without EUA) |
LoginAuthOnly |
Concrete type assertion to *RdpClient |
Uses AuthOnlyClient interface (works with any Control) |
| Synchronous Login | Not available (async only) | LoginSync / LoginSyncContext blocks until full session is ready |
| Connection pool | Not available | ConnPool reuses authenticated connections (2-3x faster brute-force) |
| SocketLayer OnClose | Not available | SetOnClose callback for connection lifecycle tracking |
| Connection reuse | Not available | AuthSession — TCP+TLS reuse for multiple credential attempts |
| TLS session resumption | Not available | Setting.TLSSessionCache — saves ~75ms per connection on reconnects |
Known limitations
- No RDP 8.x graphics pipeline — RDPGFX, RemoteFX, H.264 codec, and dynamic virtual channels are not implemented. Bitmap updates use the legacy RLE decoder.
- No UDP transport — TCP only.
- No virtual channels — clipboard, drive redirection, audio, and
printer redirection are not implemented (
ClientNetworkDataships an empty channel list). - No RDSTLS / Azure AD authentication — only NTLM over CredSSP.
- TLS capped at 1.2 — Windows CredSSP silently breaks PubKeyAuth verification over TLS 1.3 (same as FreeRDP).
- FIPS advertisement only —
Setting.FIPSadvertises the capability but the SEC layer does not yet implement FIPS sealing; a FIPS-only server will still fail at the security-exchange stage. - Residual allocation in
io.gohelpers —ReadUint16LE/etc. still escape-allocate becauseio.Reader.Readis an interface method the compiler can't inline. UseZeroCopyReaderfor 0-alloc hot paths. pdu/data.goignores some read errors —logonInfoV1/V2discard errors fromcore.ReadBytes. This only affectsSaveSessionInfoparsing (not on the auth-only path).
Benchmarks
Run with go test -bench=. -benchmem ./core/:
| Operation | Old approach | New approach | Speedup | Allocs |
|---|---|---|---|---|
| Read uint16 LE | 22 ns, 1 alloc | 3.7 ns, 0 allocs (ZeroCopyReader) |
6.0× | -100% |
| Write uint32 LE | 14.6 ns, 1 alloc | 7.2 ns, 0 allocs (ZeroCopyWriter) |
2.0× | -100% |
| Mixed read (1+2+4 bytes) | 60 ns, 3 allocs | 10.8 ns, 0 allocs (ZeroCopyReader) |
5.6× | -100% |
NTLM NTOWFv2 (cache hit) |
~5 µs | ~50 ns (cached) | ~100× | - |
RLE CVAL (per-byte, 8KB) |
17.8 µs | 16.8 µs (Cursor) |
1.06× | 0 → 0 |
Building & testing
# Build all packages
go build ./...
# Run all tests with the race detector
go test -race ./...
# Run benchmarks
go test -bench=. -benchmem ./core/
# Build the rdpcheck CLI
go build -o rdpcheck ./cmd/rdpcheck
Project layout
newgrdp/
├── client/ # High-level RDP client
│ ├── client.go # Client + Setting
│ ├── rdp.go # RdpClient (Login, LoginAuthOnly)
│ └── errors.go # Error helpers (IsAuthError, FormatError, ...)
├── cmd/
│ └── rdpcheck/ # CLI credential checker
├── core/ # Core primitives
│ ├── io.go # Read/Write helpers (legacy, allocates)
│ ├── zcreader.go # ZeroCopyReader (0 allocs) + FillAhead
│ ├── zcwriter.go # ZeroCopyWriter (0 allocs)
│ ├── pool.go # BufferPool (sync.Pool)
│ ├── cursor.go # Cursor (index-based slice reader)
│ ├── pipeline.go # Pipeline / SimplePipeline (concurrent stages)
│ ├── socket.go # SocketLayer (context-aware, deadline-based cancel)
│ ├── rle.go # RLE bitmap decoder (safe, bounds-checked)
│ ├── errors.go # RDPError + NTStatus/X224 translations
│ ├── types.go # Transport interface, FastPathListener
│ └── util.go # UnicodeEncode/Decode, Reverse, Random
├── emission/ # Event emitter (On/Once/Emit)
├── glog/ # Leveled logger
├── protocol/
│ ├── lic/ # Licensing PDUs
│ ├── nla/ # CredSSP + NTLMv2 (with two-level hash cache)
│ ├── pdu/ # PDU layer (capabilities, data, orders, gdi)
│ ├── sec/ # Security layer (encrypt/decrypt, MAC)
│ ├── t125/ # MCS + GCC + BER + PER
│ ├── tpkt/ # TPKT framing + CredSSP TSRequest handling
│ └── x224/ # X224 connection negotiation
├── go.mod / go.sum
├── LICENSE
└── README.md
Credits
This library is a heavily modified fork of the original
icodeface/grdp /
x90skysn3k/grdp line. The
protocol implementation draws from:
- rdpy — Python RDP reference
- node-rdpjs — Node.js RDP reference
- gordp — Go RDP reference
- ncrack — RDP auth module reference
- FreeRDP — Reference for CredSSP v5+ binding hash and TLS 1.2 cap
Protocol references:
- MS-RDPBCGR — Remote Desktop Protocol: Basic Connectivity and Graphics Remoting
- MS-CSSP — Credential Security Support Provider
- MS-NLMP — NT LAN Manager
- MS-ERREF — Windows Error Codes
License
GPL-3.0 — see LICENSE.