init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib

This commit is contained in:
w11
2026-09-19 18:55:43 +08:00
commit 1096fbce9d
94 changed files with 36790 additions and 0 deletions
+935
View File
@@ -0,0 +1,935 @@
package pdu
import (
"bytes"
"encoding/binary"
"fmt"
"log/slog"
"sync"
"sync/atomic"
"git.zeroonesoft.cn/golib/rdplib/core"
"git.zeroonesoft.cn/golib/rdplib/emission"
"git.zeroonesoft.cn/golib/rdplib/protocol/t125/gcc"
)
var readerPool = sync.Pool{
New: func() any { return new(bytes.Reader) },
}
// LastServerErrorInfo 记录服务器最近一次通过 SET_ERROR_INFO PDU 报告的错误码。
// 服务器断开前通常先发送该 PDU(如 0x0000112F = ERRINFO_GRAPHICS_SUBSYSTEM_FAILED),
// 上层据此判断断连原因并做降级重试。
var LastServerErrorInfo atomic.Uint32
// fastPathBufPool reuses byte slices for serializing fast-path input PDUs.
// Capacity 128 covers the maximum frame: 1 + 7*15 = 106 bytes.
var fastPathBufPool = sync.Pool{
New: func() any { return make([]byte, 0, 128) },
}
type PDULayer struct {
bitmapCachePersistKeys []uint64
emission.Emitter
transport core.Transport
sharedId uint32
userId uint16
channelId uint16
serverCapabilities map[CapsType]Capability
clientCapabilities map[CapsType]Capability
fastPathSender core.FastPathSender
// serverFastPathInput is set after capability exchange when both sides
// advertise INPUT_FLAG_FASTPATH_INPUT, allowing client input to be sent
// using the much shorter fast-path framing (MS-RDPBCGR §2.2.8.1.2).
serverFastPathInput bool
demandActivePDU *DemandActivePDU
mppc *core.MppcDecompressor
}
func NewPDULayer(t core.Transport) *PDULayer {
p := &PDULayer{
Emitter: *emission.NewEmitter(),
transport: t,
sharedId: 0x103EA,
serverCapabilities: map[CapsType]Capability{
CAPSTYPE_GENERAL: &GeneralCapability{
ProtocolVersion: 0x0200,
},
CAPSTYPE_BITMAP: &BitmapCapability{
Receive1BitPerPixel: 0x0001,
Receive4BitsPerPixel: 0x0001,
Receive8BitsPerPixel: 0x0001,
BitmapCompressionFlag: 0x0001,
MultipleRectangleSupport: 0x0001,
},
CAPSTYPE_ORDER: &OrderCapability{
DesktopSaveXGranularity: 1,
DesktopSaveYGranularity: 20,
MaximumOrderLevel: 1,
OrderFlags: NEGOTIATEORDERSUPPORT,
DesktopSaveSize: 480 * 480,
},
CAPSTYPE_POINTER: &PointerCapability{ColorPointerCacheSize: 20},
CAPSTYPE_INPUT: &InputCapability{},
CAPSTYPE_VIRTUALCHANNEL: &VirtualChannelCapability{},
CAPSTYPE_FONT: &FontCapability{SupportFlags: 0x0001},
CAPSTYPE_COLORCACHE: &ColorCacheCapability{CacheSize: 0x0006},
CAPSTYPE_SHARE: &ShareCapability{},
},
clientCapabilities: map[CapsType]Capability{
CAPSTYPE_GENERAL: &GeneralCapability{
ProtocolVersion: 0x0200,
},
CAPSTYPE_BITMAP: &BitmapCapability{
Receive1BitPerPixel: 0x0001,
Receive4BitsPerPixel: 0x0001,
Receive8BitsPerPixel: 0x0001,
BitmapCompressionFlag: 0x0001,
MultipleRectangleSupport: 0x0001,
},
CAPSTYPE_ORDER: &OrderCapability{
DesktopSaveXGranularity: 1,
DesktopSaveYGranularity: 20,
MaximumOrderLevel: 1,
OrderFlags: NEGOTIATEORDERSUPPORT,
DesktopSaveSize: 480 * 480,
TextANSICodePage: 0x4e4,
},
CAPSTYPE_CONTROL: &ControlCapability{0, 0, 2, 2},
CAPSTYPE_ACTIVATION: &WindowActivationCapability{},
CAPSTYPE_POINTER: &PointerCapability{1, 20, 20},
CAPSTYPE_SHARE: &ShareCapability{},
CAPSTYPE_COLORCACHE: &ColorCacheCapability{6, 0},
CAPSTYPE_SOUND: &SoundCapability{0x0001, 0},
CAPSTYPE_INPUT: &InputCapability{},
CAPSTYPE_FONT: &FontCapability{0x0001, 0},
CAPSTYPE_BRUSH: &BrushCapability{BRUSH_COLOR_8x8},
CAPSTYPE_GLYPHCACHE: &GlyphCapability{},
CAPSETTYPE_BITMAP_CODECS: newClientBitmapCodecsCapability(),
CAPSTYPE_BITMAPCACHE_REV2: &BitmapCache2Capability{
BitmapCachePersist: 2,
CachesNum: 5,
BmpC0Cells: 0x258,
BmpC1Cells: 0x258,
BmpC2Cells: 0x800,
BmpC3Cells: 0x1000,
BmpC4Cells: 0x800,
},
CAPSTYPE_VIRTUALCHANNEL: &VirtualChannelCapability{0, 1600},
CAPSETTYPE_MULTIFRAGMENTUPDATE: &MultiFragmentUpdate{0x3F0000},
CAPSTYPE_RAIL: &RemoteProgramsCapability{
RailSupportLevel: RAIL_LEVEL_SUPPORTED |
RAIL_LEVEL_SHELL_INTEGRATION_SUPPORTED |
RAIL_LEVEL_LANGUAGE_IME_SYNC_SUPPORTED |
RAIL_LEVEL_SERVER_TO_CLIENT_IME_SYNC_SUPPORTED |
RAIL_LEVEL_HIDE_MINIMIZED_APPS_SUPPORTED |
RAIL_LEVEL_WINDOW_CLOAKING_SUPPORTED |
RAIL_LEVEL_HANDSHAKE_EX_SUPPORTED |
RAIL_LEVEL_DOCKED_LANGBAR_SUPPORTED,
},
CAPSETTYPE_LARGE_POINTER: &LargePointerCapability{1},
CAPSETTYPE_COMPDESK: &DesktopCompositionCapability{
CompDeskSupportLevel: 1, // COMPDESK_SUPPORTED
},
CAPSETTYPE_SURFACE_COMMANDS: &SurfaceCommandsCapability{
CmdFlags: SURFCMDS_SET_SURFACE_BITS | SURFCMDS_STREAM_SURFACE_BITS | SURFCMDS_FRAME_MARKER,
},
CAPSSETTYPE_FRAME_ACKNOWLEDGE: &FrameAcknowledgeCapability{2},
},
mppc: core.NewMppcDecompressor(),
}
t.On("close", func() {
p.Emit("close")
}).On("error", func(err error) {
p.Emit("error", err)
})
return p
}
func (p *PDULayer) sendPDU(message PDUMessage) {
pdu := NewPDU(p.userId, message)
p.transport.Write(pdu.serialize())
}
func (p *PDULayer) sendDataPDU(message DataPDUData) {
dataPdu := NewDataPDU(message, p.sharedId)
p.sendPDU(dataPdu)
}
func (p *PDULayer) SetFastPathSender(f core.FastPathSender) {
p.fastPathSender = f
}
type Client struct {
*PDULayer
clientCoreData *gcc.ClientCoreData
buff *bytes.Buffer
// fragCode 记住分片更新首片的 updateCode:续片的 updateCode 无意义
//(Win10 发送的是载荷字节),重组完成后必须用它来解析。
fragCode uint8
// fragActive/fragPoisoned:分片重组进行中标记;解压失败的首片会把
// 整个更新标记为中毒,其剩余分片被吞掉(防止孤儿分片拼出垃圾)。
fragActive bool
fragPoisoned bool
// decErrLogs:MPPC 解压失败日志限次计数器。
decErrLogs int
// surfResetDumped:SURFCMDS 重置全量转储限次(离线分析用)。
surfResetDumped int
// bmpRectLogs:16bpp 矩形取证日志限次计数器。
bmpRectLogs int
// bitmapCachePersistKeys:客户端持久位图缓存键(6.4b M2),
// finalize 时经 maybeSendPersistentKeyList 上报服务器。
bitmapCachePersistKeys []uint64
// bmpFrame:fast-path 位图跨段累积缓冲(Win10 会把一个位图更新拆到
// 多个 fast-path PDU,按矩形结构完整解析后整体消费)。
bmpFrame []byte
// surfBuf:fast-path 表面命令跨段累积缓冲(按命令流自定界解析)。
surfBuf []byte
// surfHdrLen:SET_SURFACE_BITS 头部长度(21=经典 / 22=Win10),由本
// 连接第一条命令判定后锁定。服务器布局会话内恒定;逐帧启发式在
// inclusive/exclusive 双兼容下会偶发选错 ±1 字节,错位累积到批次
// 末尾即"未知命令类型→整段重置"——表现为周期性花屏+断流。
surfHdrLen int
}
func NewClient(t core.Transport) *Client {
c := &Client{
PDULayer: NewPDULayer(t),
buff: &bytes.Buffer{},
}
c.transport.Once("connect", c.connect)
return c
}
func (c *Client) connect(data *gcc.ClientCoreData, userId uint16, channelId uint16) {
slog.Debug("pdu connect", "userId", userId, "channelId", channelId)
c.clientCoreData = data
c.userId = userId
c.channelId = channelId
c.transport.Once("data", c.recvDemandActivePDU)
}
func (c *Client) recvDemandActivePDU(s []byte) {
r := readerPool.Get().(*bytes.Reader)
r.Reset(s)
defer readerPool.Put(r)
pdu, err := readPDU(r, c.mppc)
if err != nil {
slog.Error("recvDemandActivePDU", "err", err)
return
}
if pdu.ShareCtrlHeader.PDUType != PDUTYPE_DEMANDACTIVEPDU {
if pdu.ShareCtrlHeader.PDUType == PDUTYPE_DEACTIVATEALLPDU {
// Per [MS-RDPBCGR] the server may send DeactivateAllPDU before
// DemandActivePDU (e.g. GNOME RDP after RDPGFX capability
// exchange). Stay on the same connection and keep waiting,
// exactly as FreeRDP does.
slog.Debug("received DeactivateAllPDU while waiting for DemandActivePDU; continuing to wait")
c.transport.Once("data", c.recvDemandActivePDU)
return
}
if pdu.ShareCtrlHeader.PDUType == PDUTYPE_SERVER_REDIR_PKT {
if redir, ok := pdu.Message.(*ServerRedirectionPDU); ok {
c.Emit("redirect", redir)
}
return
}
slog.Debug("ignore message during connection sequence", "type", pdu.ShareCtrlHeader.PDUType)
c.transport.Once("data", c.recvDemandActivePDU)
return
}
c.sharedId = pdu.Message.(*DemandActivePDU).SharedId
c.demandActivePDU = pdu.Message.(*DemandActivePDU)
for _, caps := range c.demandActivePDU.CapabilitySets {
slog.Debug("serverCaps", "type", caps.Type(), "value", caps)
c.serverCapabilities[caps.Type()] = caps
}
if ic, ok := c.serverCapabilities[CAPSTYPE_INPUT].(*InputCapability); ok {
c.serverFastPathInput = ic.Flags&INPUT_FLAG_FASTPATH_INPUT != 0
}
c.sendConfirmActivePDU()
c.sendClientFinalizeSynchronizePDU()
c.transport.Once("data", c.recvServerSynchronizePDU)
}
func (c *Client) sendConfirmActivePDU() {
pdu := NewConfirmActivePDU()
generalCapa := c.clientCapabilities[CAPSTYPE_GENERAL].(*GeneralCapability)
generalCapa.OSMajorType = OSMAJORTYPE_WINDOWS
generalCapa.OSMinorType = OSMINORTYPE_WINDOWS_NT
generalCapa.GeneralCompressionTypes = 0x0002 // PACKET_COMPR_TYPE_64K: advertise MPPC-64K support
generalCapa.ExtraFlags = LONG_CREDENTIALS_SUPPORTED | NO_BITMAP_COMPRESSION_HDR |
FASTPATH_OUTPUT_SUPPORTED | AUTORECONNECT_SUPPORTED
generalCapa.RefreshRectSupport = 1
generalCapa.SuppressOutputSupport = 1
bitmapCapa := c.clientCapabilities[CAPSTYPE_BITMAP].(*BitmapCapability)
bitmapCapa.PreferredBitsPerPixel = 32
bitmapCapa.DesktopWidth = c.clientCoreData.DesktopWidth
bitmapCapa.DesktopHeight = c.clientCoreData.DesktopHeight
bitmapCapa.DesktopResizeFlag = 0x0001
orderCapa := c.clientCapabilities[CAPSTYPE_ORDER].(*OrderCapability)
orderCapa.OrderFlags = NEGOTIATEORDERSUPPORT | ZEROBOUNDSDELTASSUPPORT | COLORINDEXSUPPORT | ORDERFLAGS_EXTRA_FLAGS
orderCapa.OrderSupportExFlags |= ORDERFLAGS_EX_ALTSEC_FRAME_MARKER_SUPPORT
orderCapa.OrderSupport[TS_NEG_DSTBLT_INDEX] = 1
orderCapa.OrderSupport[TS_NEG_PATBLT_INDEX] = 1
orderCapa.OrderSupport[TS_NEG_SCRBLT_INDEX] = 1
// MEMBLT + 位图缓存 v2(stage6 6.4b M1 会话内缓存):服务器把重复
// 位图存入客户端缓存单元,后续以 MemBlt 引用回贴,替代全量重发。
orderCapa.OrderSupport[TS_NEG_MEMBLT_INDEX] = 1
//orderCapa.OrderSupport[TS_NEG_LINETO_INDEX] = 1
//orderCapa.OrderSupport[TS_NEG_MEM3BLT_INDEX] = 1
//orderCapa.OrderSupport[TS_NEG_POLYLINE_INDEX] = 1
/*orderCapa.OrderSupport[TS_NEG_MULTIOPAQUERECT_INDEX] = 1
orderCapa.OrderSupport[TS_NEG_GLYPH_INDEX_INDEX] = 1
//orderCapa.OrderSupport[TS_NEG_DRAWNINEGRID_INDEX] = 1
orderCapa.OrderSupport[TS_NEG_SAVEBITMAP_INDEX] = 1
orderCapa.OrderSupport[TS_NEG_POLYGON_SC_INDEX] = 1
orderCapa.OrderSupport[TS_NEG_POLYGON_CB_INDEX] = 1
orderCapa.OrderSupport[TS_NEG_ELLIPSE_SC_INDEX] = 1
orderCapa.OrderSupport[TS_NEG_ELLIPSE_CB_INDEX] = 1*/
//orderCapa.OrderSupport[TS_NEG_FAST_GLYPH_INDEX] = 1
inputCapa := c.clientCapabilities[CAPSTYPE_INPUT].(*InputCapability)
inputCapa.Flags = INPUT_FLAG_SCANCODES | INPUT_FLAG_MOUSEX | INPUT_FLAG_UNICODE |
INPUT_FLAG_FASTPATH_INPUT | INPUT_FLAG_FASTPATH_INPUT2
inputCapa.KeyboardLayout = c.clientCoreData.KbdLayout
inputCapa.KeyboardType = c.clientCoreData.KeyboardType
inputCapa.KeyboardSubType = c.clientCoreData.KeyboardSubType
inputCapa.KeyboardFunctionKey = c.clientCoreData.KeyboardFnKeys
inputCapa.ImeFileName = c.clientCoreData.ImeFileName
glyphCapa := c.clientCapabilities[CAPSTYPE_GLYPHCACHE].(*GlyphCapability)
/*glyphCapa.GlyphCache[0] = cacheEntry{254, 4}
glyphCapa.GlyphCache[1] = cacheEntry{254, 4}
glyphCapa.GlyphCache[2] = cacheEntry{254, 8}
glyphCapa.GlyphCache[3] = cacheEntry{254, 8}
glyphCapa.GlyphCache[4] = cacheEntry{254, 16}
glyphCapa.GlyphCache[5] = cacheEntry{254, 32}
glyphCapa.GlyphCache[6] = cacheEntry{254, 64}
glyphCapa.GlyphCache[7] = cacheEntry{254, 128}
glyphCapa.GlyphCache[8] = cacheEntry{254, 256}
glyphCapa.GlyphCache[9] = cacheEntry{64, 2048}
glyphCapa.FragCache = 0x01000100*/
glyphCapa.SupportLevel = GLYPH_SUPPORT_NONE
// 位图缓存 v2 单元(MS-RDPBCGR 2.2.7.1.8):单元尺寸取 mstsc 量级。
// CacheFlags 持久键支持位留待 6.4b M2(配合 PERSISTENT_KEY_LIST)。
rev2, ok := c.clientCapabilities[CAPSTYPE_BITMAPCACHE_REV2].(*BitmapCacheRev2Capability)
if !ok || rev2 == nil {
rev2 = &BitmapCacheRev2Capability{}
c.clientCapabilities[CAPSTYPE_BITMAPCACHE_REV2] = rev2
}
rev2.CacheCells = [5]BitmapCacheV2CellInfo{
{NumEntries: 600, MaxCellSize: 256},
{NumEntries: 1024, MaxCellSize: 512},
{NumEntries: 4096, MaxCellSize: 1024},
{}, {},
}
// v1 位图缓存 caps(0x0004)与 Rev2 一并广告——与 mstsc 行为对齐,
// 部分服务器以 v1 caps 存在与否决定是否启用缓存订单。
v1, ok := c.clientCapabilities[CAPSTYPE_BITMAPCACHE].(*BitmapCacheCapability)
if !ok || v1 == nil {
v1 = &BitmapCacheCapability{}
c.clientCapabilities[CAPSTYPE_BITMAPCACHE] = v1
}
pdu.SharedId = c.sharedId
for _, v := range c.clientCapabilities {
slog.Debug("clientCaps", "type", v.Type(), "value", v)
pdu.CapabilitySets = append(pdu.CapabilitySets, v)
}
pdu.NumberCapabilities = uint16(len(pdu.CapabilitySets))
pdu.LengthSourceDescriptor = c.demandActivePDU.LengthSourceDescriptor
pdu.SourceDescriptor = c.demandActivePDU.SourceDescriptor
pdu.LengthCombinedCapabilities = c.demandActivePDU.LengthCombinedCapabilities
c.sendPDU(pdu)
}
func (c *Client) sendClientFinalizeSynchronizePDU() {
c.sendDataPDU(NewSynchronizeDataPDU(c.channelId))
c.sendDataPDU(&ControlDataPDU{Action: CTRLACTION_COOPERATE})
c.sendDataPDU(&ControlDataPDU{Action: CTRLACTION_REQUEST_CONTROL})
c.maybeSendPersistentKeyList()
c.sendDataPDU(&FontListDataPDU{ListFlags: 0x0003, EntrySize: 0x0032})
}
func (c *Client) recvServerSynchronizePDU(s []byte) {
r := readerPool.Get().(*bytes.Reader)
r.Reset(s)
defer readerPool.Put(r)
pdu, err := readPDU(r, c.mppc)
if err != nil {
slog.Error("recvServerSynchronizePDU", "err", err)
return
}
dataPdu, ok := pdu.Message.(*DataPDU)
if !ok || dataPdu.Header.PDUType2 != PDUTYPE2_SYNCHRONIZE {
if ok {
slog.Error("recvServerSynchronizePDU ignore datapdu", "type2", dataPdu.Header.PDUType2)
} else {
slog.Error("recvServerSynchronizePDU ignore message", "type", pdu.ShareCtrlHeader.PDUType)
}
slog.Debug("recvServerSynchronizePDU dataPdu", "pdu", &dataPdu)
c.transport.Once("data", c.recvServerSynchronizePDU)
return
}
c.transport.Once("data", c.recvServerControlCooperatePDU)
}
func (c *Client) recvServerControlCooperatePDU(s []byte) {
r := readerPool.Get().(*bytes.Reader)
r.Reset(s)
defer readerPool.Put(r)
pdu, err := readPDU(r, c.mppc)
if err != nil {
slog.Error("recvServerControlCooperatePDU", "err", err)
return
}
dataPdu, ok := pdu.Message.(*DataPDU)
if !ok || dataPdu.Header.PDUType2 != PDUTYPE2_CONTROL {
if ok {
slog.Error("recvServerControlCooperatePDU ignore datapdu", "type2", dataPdu.Header.PDUType2)
} else {
slog.Error("recvServerControlCooperatePDU ignore message", "type", pdu.ShareCtrlHeader.PDUType)
}
c.transport.Once("data", c.recvServerControlCooperatePDU)
return
}
if dataPdu.Data.(*ControlDataPDU).Action != CTRLACTION_COOPERATE {
slog.Error("recvServerControlCooperatePDU ignore", "action", dataPdu.Data.(*ControlDataPDU).Action)
c.transport.Once("data", c.recvServerControlCooperatePDU)
return
}
c.transport.Once("data", c.recvServerControlGrantedPDU)
}
func (c *Client) recvServerControlGrantedPDU(s []byte) {
r := readerPool.Get().(*bytes.Reader)
r.Reset(s)
defer readerPool.Put(r)
pdu, err := readPDU(r, c.mppc)
if err != nil {
slog.Error("recvServerControlGrantedPDU", "err", err)
return
}
dataPdu, ok := pdu.Message.(*DataPDU)
if !ok || dataPdu.Header.PDUType2 != PDUTYPE2_CONTROL {
if ok {
slog.Error("recvServerControlGrantedPDU ignore datapdu", "type2", dataPdu.Header.PDUType2)
} else {
slog.Error("recvServerControlGrantedPDU ignore message", "type", pdu.ShareCtrlHeader.PDUType)
}
c.transport.Once("data", c.recvServerControlGrantedPDU)
return
}
if dataPdu.Data.(*ControlDataPDU).Action != CTRLACTION_GRANTED_CONTROL {
slog.Error("recvServerControlGrantedPDU ignore", "action", dataPdu.Data.(*ControlDataPDU).Action)
c.transport.Once("data", c.recvServerControlGrantedPDU)
return
}
c.transport.Once("data", c.recvServerFontMapPDU)
}
func (c *Client) recvServerFontMapPDU(s []byte) {
r := readerPool.Get().(*bytes.Reader)
r.Reset(s)
defer readerPool.Put(r)
pdu, err := readPDU(r, c.mppc)
if err != nil {
slog.Error("recvServerFontMapPDU", "err", err)
return
}
dataPdu, ok := pdu.Message.(*DataPDU)
if !ok || dataPdu.Header.PDUType2 != PDUTYPE2_FONTMAP {
if ok {
slog.Error("recvServerFontMapPDU ignore datapdu", "type2", dataPdu.Header.PDUType2)
} else {
slog.Error("recvServerFontMapPDU ignore message", "type", pdu.ShareCtrlHeader.PDUType)
}
return
}
c.transport.On("data", c.recvPDU)
// Tell the server we're ready to receive display updates (MS-RDPBCGR 2.2.11.3.1)
slog.Debug("Sending SuppressOutput (ALLOW_DISPLAY_UPDATES)")
c.sendDataPDU(&SuppressOutputPDU{
AllowDisplayUpdates: 1,
Right: c.clientCoreData.DesktopWidth - 1,
Bottom: c.clientCoreData.DesktopHeight - 1,
})
c.Emit("ready")
}
func (c *Client) recvPDU(s []byte) {
r := readerPool.Get().(*bytes.Reader)
r.Reset(s)
defer readerPool.Put(r)
if r.Len() > 0 {
p, err := readPDU(r, c.mppc)
if err != nil {
slog.Error("recvPDU", "err", err)
return
}
if p.ShareCtrlHeader.PDUType == PDUTYPE_DEACTIVATEALLPDU {
// Server is reactivating the session (e.g. desktop resize).
// Signal callers to pause input until "ready" fires again.
slog.Debug("received DeactivateAllPDU during active session, waiting for reactivation")
c.Emit("deactivateAll")
c.transport.Once("data", c.recvDemandActivePDU)
} else if p.ShareCtrlHeader.PDUType == PDUTYPE_SERVER_REDIR_PKT {
if redir, ok := p.Message.(*ServerRedirectionPDU); ok {
c.Emit("redirect", redir)
}
} else if p.ShareCtrlHeader.PDUType == PDUTYPE_DATAPDU {
d := p.Message.(*DataPDU)
if d.Header.PDUType2 == PDUTYPE2_UPDATE {
up := d.Data.(*UpdateDataPDU)
p := up.Udata
if up.UpdateType == FASTPATH_UPDATETYPE_BITMAP {
c.Emit("bitmap", p.(*BitmapUpdateDataPDU).Rectangles)
} else if up.UpdateType == FASTPATH_UPDATETYPE_ORDERS {
c.Emit("orders", p.(*FastPathOrdersPDU).OrderPdus)
}
} else if d.Header.PDUType2 == PDUTYPE2_POINTER {
pp := d.Data.(*PointerDataPDU)
if pp.Pdata != nil {
switch pp.MessageType {
case TS_PTRUPDATE_TYPE_CACHED:
c.Emit("pointer_cached", pp.Pdata.(*FastPathUpdateCachedPDU).CacheIdx)
case TS_PTRUPDATE_TYPE_POINTER:
c.Emit("pointer_update", pp.Pdata.(*FastPathUpdatePointerPDU))
}
}
if pp.MessageType == TS_PTRUPDATE_TYPE_SYSTEM {
c.Emit("pointer_hide")
}
} else if d.Header.PDUType2 == PDUTYPE2_SET_ERROR_INFO_PDU {
// 服务器在断开连接前会发送错误码,不解析就无法定位断连原因
ei := d.Data.(*ErrorInfoDataPDU)
LastServerErrorInfo.Store(ei.ErrorInfo)
slog.Warn("server SET_ERROR_INFO", "code", fmt.Sprintf("0x%08X", ei.ErrorInfo))
}
}
}
}
func (c *Client) RecvFastPath(secFlag byte, s []byte) {
r := readerPool.Get().(*bytes.Reader)
r.Reset(s)
defer readerPool.Put(r)
for r.Len() > 0 {
updateHeader, err := core.ReadUInt8(r)
if err != nil {
return
}
updateCode := updateHeader & 0x0f
fragmentation := updateHeader & 0x30
// compression 占 header 的高 2 位:字段值 0b10(左移后 0x80)表示
// 后随 1 字节 compressionFlags。此前误拿位掩码值与字段值 0x2 直接
// 比较,恒为假 → 压缩标志字节从未被消费,整条 fast-path 流错位。
compression := updateHeader & 0xC0
var compressionFlags uint8 = 0
if compression == FASTPATH_OUTPUT_COMPRESSION_USED<<6 {
compressionFlags, err = core.ReadUInt8(r)
if err != nil {
return
}
}
size, err := core.ReadUint16LE(r)
if err != nil {
return
}
// Read exactly `size` bytes for this update's payload.
payload, err := core.ReadBytes(int(size), r)
if err != nil {
return
}
slog.Debug("RecvFastPath", "Code", FastPathUpdateType(updateCode),
"compressionFlags", compressionFlags,
"fragmentation", fragmentation,
"size", size)
// 先解压再重组:压缩以每个 fast-path 分片为独立单位(各分片头部
// 携带各自的 compressionFlags),共享同一条 MPPC 历史流;拼接压缩
// 字节后一次性解压必然产生垃圾。
decompressed := payload
decFailed := false
if compressionFlags != 0 && c.mppc != nil {
out, err := c.mppc.Decompress(compressionFlags, payload)
if err != nil {
decFailed = true
if c.decErrLogs < 4 {
c.decErrLogs++
slog.Warn("RecvFastPath: MPPC decompression failed", "err", err,
"cf", fmt.Sprintf("%02X", compressionFlags),
"frag", fragmentation, "size", size)
}
} else {
decompressed = out
}
}
// 分片重组:续片的 updateCode 不可信(MS-RDPBCGR 2.2.9.1.1.3.1
// 规定忽略),解析时使用首片记住的 code。解压失败的分片会把所在
// 更新标记为中毒:吞掉其剩余分片,避免孤儿分片拼出垃圾更新。
if fragmentation != FASTPATH_FRAGMENT_SINGLE {
if fragmentation == FASTPATH_FRAGMENT_FIRST {
c.buff.Reset()
c.fragCode = updateCode
c.fragActive = true
c.fragPoisoned = decFailed
} else if !c.fragActive || c.fragPoisoned {
continue // 无首片的孤儿分片 / 中毒更新的剩余分片
}
c.buff.Write(decompressed)
if fragmentation != FASTPATH_FRAGMENT_LAST {
continue
}
c.fragActive = false
if c.fragPoisoned {
continue
}
payload = c.buff.Bytes()
updateCode = c.fragCode
} else if decFailed {
continue
} else {
// 单片更新:解析对象必须是解压后的结果(此前漏赋值导致
// 嗅探/解析拿到原始压缩数据,单片位图全部报废)。
payload = decompressed
}
// Surface Commands:Win10 会把一条表面命令拆到多个 fast-path PDU
//(实测 frag 位 FIRST/NEXT/LAST 使用规范),按命令流自定界累积解析,命令完整即发射。
if updateCode == FASTPATH_UPDATETYPE_SURFCMDS {
if decFailed {
continue
}
c.surfBuf = append(c.surfBuf, payload...)
for {
result, consumed, needMore, valid, dropped, hdrLen := parseSurfaceCommandsIncremental(c.surfBuf, c.surfHdrLen)
if c.surfHdrLen == 0 && hdrLen != 0 {
c.surfHdrLen = hdrLen // 首条命令锁定头部布局(会话内恒定)
}
if !valid {
if c.surfResetDumped < 2 {
c.surfResetDumped++
slog.Warn("SURFCMDS stream reset full dump",
"n", c.surfResetDumped, "bufLen", len(c.surfBuf),
"hdrLen", c.surfHdrLen,
"hex", fmt.Sprintf("% X", c.surfBuf))
}
slog.Warn("SURFCMDS stream reset",
"bufLen", len(c.surfBuf),
"hex", fmt.Sprintf("% X", c.surfBuf[:min(24, len(c.surfBuf))]))
c.surfBuf = c.surfBuf[:0]
c.surfHdrLen = 0 // 布局判定作废,下批重新锁定
break
}
if len(result.Rects) > 0 {
c.Emit("bitmap", result.Rects)
}
for _, fid := range result.FrameIDs {
c.sendDataPDU(&FrameAcknowledgeDataPDU{FrameID: fid})
}
c.surfBuf = c.surfBuf[consumed:]
if dropped {
// 尾部未知结构:已解析前缀照常上屏,仅丢弃尾巴。
// 实测 Win10 整屏重绘批次末尾带 11 字节未文档化尾巴,
// 整批报废会造成周期性花屏+断流。
slog.Debug("SURFCMDS tail dropped", "consumed", consumed, "bufLen", len(c.surfBuf))
c.surfBuf = c.surfBuf[:0]
break
}
if needMore || consumed == 0 || len(c.surfBuf) < 2 {
break
}
}
if len(c.surfBuf) > 8<<20 {
c.surfBuf = c.surfBuf[:0] // 防御:异常累积封顶
}
continue
}
// fast-path 位图:Win10 会把一个位图更新拆到多个 fast-path PDU
//(首段携带 updateType + numberRectangles + 前几个矩形,后续段
// 继续补充矩形数据)。将解压结果持续累积,一旦能完整解析出全部
// 矩形就发射。非位图更新仍走通用分片状态机。
// 同样必须追加 payload(重组缓冲)而非 decompressed,理由同上。
if updateCode == FASTPATH_UPDATETYPE_BITMAP {
if decFailed {
continue
}
c.bmpFrame = append(c.bmpFrame, payload...)
for {
rects, consumed, complete, valid := parseBitmapFrame(c.bmpFrame)
if !valid {
slog.Warn("BITMAP frame reset",
"bufLen", len(c.bmpFrame),
"hex", fmt.Sprintf("% X", c.bmpFrame[:min(24, len(c.bmpFrame))]))
c.bmpFrame = c.bmpFrame[:0]
break
}
if !complete {
break
}
if len(rects) > 0 {
if c.bmpRectLogs < 24 {
for i, rc := range rects {
if c.bmpRectLogs >= 24 {
break
}
c.bmpRectLogs++
slog.Debug("BITMAP rect", "i", i, "n", len(rects),
"dx", rc.DestLeft, "dy", rc.DestTop,
"dr", rc.DestRight, "db", rc.DestBottom,
"w", rc.Width, "h", rc.Height,
"bpp", rc.BitsPerPixel, "flags", fmt.Sprintf("0x%04X", rc.Flags),
"len", len(rc.BitmapDataStream))
}
}
c.Emit("bitmap", rects)
}
c.bmpFrame = c.bmpFrame[consumed:]
if len(c.bmpFrame) < 4 {
break
}
}
continue
}
if updateCode == FASTPATH_UPDATETYPE_POINTER {
// 取证:指针更新原始字节,核对 xorBpp 后各字段对齐。
slog.Debug("PTR raw", "hex", fmt.Sprintf("% X", payload[:min(32, len(payload))]))
}
pr := bytes.NewReader(payload)
p, err := readFastPathUpdatePDU(pr, updateCode)
if err != nil {
slog.Warn("readFastPathUpdatePDU:", "Code", FastPathUpdateType(updateCode), "err", err)
continue
}
if updateCode == FASTPATH_UPDATETYPE_BITMAP {
c.Emit("bitmap", p.Data.(*FastPathBitmapUpdateDataPDU).Rectangles)
} else if updateCode == FASTPATH_UPDATETYPE_COLOR {
c.Emit("color", p.Data.(*FastPathColorPdu))
} else if updateCode == FASTPATH_UPDATETYPE_ORDERS {
c.Emit("orders", p.Data.(*FastPathOrdersPDU).OrderPdus)
} else if updateCode == FASTPATH_UPDATETYPE_PTR_NULL {
c.Emit("pointer_hide")
} else if updateCode == FASTPATH_UPDATETYPE_PTR_DEFAULT {
c.Emit("pointer_default")
} else if updateCode == FASTPATH_UPDATETYPE_PTR_POSITION {
pp := p.Data.(*FastPathPointerPositionPDU)
c.Emit("pointer_position", pp.X, pp.Y)
} else if updateCode == FASTPATH_UPDATETYPE_CACHED {
c.Emit("pointer_cached", p.Data.(*FastPathUpdateCachedPDU).CacheIdx)
} else if updateCode == FASTPATH_UPDATETYPE_POINTER {
c.Emit("pointer_update", p.Data.(*FastPathUpdatePointerPDU))
}
}
}
type InputEventsInterface interface {
Serialize() []byte
}
// fastPathEncoder is implemented by input event types that know how to
// produce their Fast-Path Input wire encoding (MS-RDPBCGR §2.2.8.1.2.2).
type fastPathEncoder interface {
FastPathEncode(buf []byte) []byte
}
func (c *Client) SendInputEvents(msgType uint16, events []InputEventsInterface) {
if c.serverFastPathInput && c.fastPathSender != nil && c.canSendFastPathInput(events) {
if c.sendFastPathInputEvents(events) {
return
}
// Fall back to slow-path on send failure (e.g. legacy encryption).
}
p := &ClientInputEventPDU{}
p.NumEvents = uint16(len(events))
p.SlowPathInputEvents = make([]SlowPathInputEvent, 0, p.NumEvents)
for _, in := range events {
seria := in.Serialize()
s := SlowPathInputEvent{0, msgType, len(seria), seria}
p.SlowPathInputEvents = append(p.SlowPathInputEvents, s)
}
c.sendDataPDU(p)
}
// canSendFastPathInput reports whether every event in the batch implements
// the fast-path encoder. Falls back to slow-path if any event type doesn't
// (currently just SynchronizeEvent, which the client never sends).
func (c *Client) canSendFastPathInput(events []InputEventsInterface) bool {
if len(events) == 0 || len(events) > 15 {
return false
}
for _, e := range events {
if _, ok := e.(fastPathEncoder); !ok {
return false
}
}
return true
}
func (c *Client) sendFastPathInputEvents(events []InputEventsInterface) bool {
buf := fastPathBufPool.Get().([]byte)
buf = buf[:0]
buf = append(buf, byte(len(events)))
for _, e := range events {
buf = e.(fastPathEncoder).FastPathEncode(buf)
}
_, err := c.fastPathSender.SendFastPath(0, buf)
fastPathBufPool.Put(buf[:cap(buf)])
if err != nil {
// Disable for the rest of the session so we don't keep paying the
// failed-attempt cost on every input event.
c.serverFastPathInput = false
slog.Warn("fast-path input disabled, falling back to slow-path", "err", err)
return false
}
return true
}
// SendRefreshRect requests the server to redraw the given screen rectangle.
// This causes the server to send a full refresh (including a new H.264 IDR)
// for the specified region, which is useful after a decoder reset.
func (c *Client) SendRefreshRect(width, height uint16) {
slog.Debug("PDU: SendRefreshRect", "w", width, "h", height)
c.sendDataPDU(&RefreshRectPDU{
NumberOfAreas: 1,
Right: width - 1,
Bottom: height - 1,
})
}
// SendForceRefresh asks the server for a complete display repaint by toggling
// SuppressOutput off→on. Per MS-RDPBCGR 2.2.11.3.1, sending ALLOW_DISPLAY_UPDATES
// after SUPPRESS_DISPLAY_UPDATES forces the server to send a fresh full-screen
// update — for the RDPGFX H.264 pipeline this means a new IDR frame, which is
// what we need to recover after a hardware-decoder hard reset. Plain
// SendRefreshRect is sometimes silently ignored by Windows servers while a
// video stream is active; this is the reliable fallback used by mstsc/FreeRDP.
func (c *Client) SendForceRefresh(width, height uint16) {
slog.Debug("PDU: SendForceRefresh (suppress→allow)", "w", width, "h", height)
c.sendDataPDU(&SuppressOutputPDU{
AllowDisplayUpdates: 0x00, // SUPPRESS_DISPLAY_UPDATES
})
c.sendDataPDU(&SuppressOutputPDU{
AllowDisplayUpdates: 0x01, // ALLOW_DISPLAY_UPDATES
Right: width - 1,
Bottom: height - 1,
})
}
// bitmapDropLogs:垃圾帧丢弃诊断日志限次计数器。
var bitmapDropLogs = 0
// parseBitmapFrame 尝试把累积缓冲解析为完整的 fast-path 位图更新
// (updateType(2) + numberRectangles(2) + 矩形数组)。
// 返回 valid=false 表示缓冲开头不是合法位图结构(应丢弃);
// complete=false 表示数据不足(应继续累积);
// complete=true 时 consumed 为本次更新占用的字节数,尾部可能残留
// 下一更新的开头。
func parseBitmapFrame(buf []byte) (rects []BitmapData, consumed int, complete bool, valid bool) {
if len(buf) < 4 {
return nil, 0, false, true
}
if u16 := binary.LittleEndian.Uint16(buf[0:2]); u16 != 0x0001 { // UPDATE_TYPE_BITMAP
return nil, 0, false, false
}
nr := int(binary.LittleEndian.Uint16(buf[2:4]))
if nr > 4096 {
if bitmapDropLogs < 3 {
bitmapDropLogs++
slog.Warn("bitmap frame dropped: implausible rect count",
"nr", nr, "len", len(buf),
"prefix", fmt.Sprintf("% X", buf[:min(24, len(buf))]))
}
return nil, 0, false, false
}
rects = make([]BitmapData, 0, nr)
pos := 4
for i := 0; i < nr; i++ {
if pos+18 > len(buf) {
return nil, 0, false, true
}
r := BitmapData{}
r.DestLeft = binary.LittleEndian.Uint16(buf[pos:])
r.DestTop = binary.LittleEndian.Uint16(buf[pos+2:])
r.DestRight = binary.LittleEndian.Uint16(buf[pos+4:])
r.DestBottom = binary.LittleEndian.Uint16(buf[pos+6:])
r.Width = binary.LittleEndian.Uint16(buf[pos+8:])
r.Height = binary.LittleEndian.Uint16(buf[pos+10:])
r.BitsPerPixel = binary.LittleEndian.Uint16(buf[pos+12:])
r.Flags = binary.LittleEndian.Uint16(buf[pos+14:])
bl := int(binary.LittleEndian.Uint16(buf[pos+16:]))
hdr := 0
if r.Flags&BITMAP_COMPRESSION != 0 && r.Flags&NO_BITMAP_COMPRESSION_HDR == 0 {
if pos+18+8 > len(buf) {
return nil, 0, false, true
}
bl = int(binary.LittleEndian.Uint16(buf[pos+18+2 : pos+18+4]))
hdr = 8
}
if bl < 0 || pos+18+hdr+bl > len(buf) {
return nil, 0, false, true
}
r.BitmapDataStream = buf[pos+18+hdr : pos+18+hdr+bl]
pos += 18 + hdr + bl
rects = append(rects, r)
}
return rects, pos, true, true
}
// SetPersistentKeyList 注册客户端持久位图缓存持有的键(bitmap 管线)。
// 须在 DemandActive 之前调用;finalize 时仅当服务器广告了
// CAPSTYPE_BITMAPCACHE_HOSTSUPPORT 且键非空才实际发送。
func (p *PDULayer) SetPersistentKeyList(keys []uint64) {
p.bitmapCachePersistKeys = keys
}
// maybeSendPersistentKeyList 在 Connection Finalization 序列
// (REQUEST_CONTROL 之后、FONT_LIST 之前)发送持久缓存键列表,
// 位置与 FreeRDP/MS-RDPBCGR §2.2.1.17 一致。
func (p *PDULayer) maybeSendPersistentKeyList() {
if len(p.bitmapCachePersistKeys) == 0 {
return
}
hs, ok := p.serverCapabilities[CAPSTYPE_BITMAPCACHE_HOSTSUPPORT].(*BitmapCacheHostSupportCapability)
if !ok || hs.CacheVersion < 1 {
slog.Info("bmpcache: server lacks bitmap cache host support, skip persistent key list")
return
}
var cells [5]uint16
if rev2, ok := p.clientCapabilities[CAPSTYPE_BITMAPCACHE_REV2].(*BitmapCacheRev2Capability); ok {
for i := range rev2.CacheCells {
if i < len(cells) {
cells[i] = rev2.CacheCells[i].NumEntries
}
}
}
keys := p.bitmapCachePersistKeys
if len(keys) > 2042 {
// FreeRDP 同款上限:>2042 条会触发服务器错误
keys = keys[:2042]
slog.Info("bmpcache: truncating persistent key list", "kept", len(keys))
}
pdu := NewPDU(p.userId, NewPersistentKeyListPDU(p.sharedId, keys, cells))
p.transport.Write(pdu.serialize())
slog.Info("bmpcache: sent persistent key list", "keys", len(keys))
}