shadow1ng/fscan · error
NODE_RDP_PROTOCOL_T125_MCS_WAIT_CHANNEL_JOIN_CONFIRM
Error message
NODE_RDP_PROTOCOL_T125_MCS_WAIT_CHANNEL_JOIN_CONFIRM
What it means
After sending MCS Channel Join Requests, the client reads the Channel Join Confirm PDU and validates its header via readMCSPDUHeader(option, CHANNEL_JOIN_CONFIRM). If the PDU received is not a Channel Join Confirm (wrong type or malformed header), the library emits this sentinel error and aborts the join sequence. It is a protocol-state error: the server responded out of the expected T.125 state machine order.
Source
Thrown at libs/grdp/protocol/t125/mcs.go:497
if err != nil {
c.Emit("error", errors.New(fmt.Sprintf("mcs recvData get data error %v", err)))
return
}
glog.Debugf("mcs emit channel<%s>:%v", channelName, left)
c.Emit("sec", channelName, left)
}
func (c *MCSClient) recvChannelJoinConfirm(s []byte) {
glog.Debug("mcs recvChannelJoinConfirm", hex.EncodeToString(s))
r := bytes.NewReader(s)
option, err := core.ReadUInt8(r)
if err != nil {
c.Emit("error", err)
return
}
if !readMCSPDUHeader(option, CHANNEL_JOIN_CONFIRM) {
c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_WAIT_CHANNEL_JOIN_CONFIRM"))
return
}
confirm, _ := per.ReadEnumerates(r)
userId, _ := per.ReadInteger16(r)
userId += MCS_USERCHANNEL_BASE
if c.userId != userId {
c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_INVALID_USER_ID"))
return
}
channelId, _ := per.ReadInteger16(r)
if (confirm != 0) && (channelId == uint16(MCS_GLOBAL_CHANNEL_ID) || channelId == c.userId) {
c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_SERVER_MUST_CONFIRM_STATIC_CHANNEL"))
return
}
glog.Debug("Confirm channelId:", channelId)View on GitHub (pinned to 95cc12e753)
Solutions
- Log the actual PDU type received to identify what the server sent instead of the join confirm.
- Retry the connection once — transient server-side disconnects are common under load.
- Verify server RDP protocol support; older/proprietary servers may not follow the standard MCS join flow.
- Check that no earlier parsing step (X224/SEC) left unread bytes causing stream desync.
Example fix
// before
if !readMCSPDUHeader(option, CHANNEL_JOIN_CONFIRM) {
c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_WAIT_CHANNEL_JOIN_CONFIRM"))
return
}
// after
if !readMCSPDUHeader(option, CHANNEL_JOIN_CONFIRM) {
c.Emit("error", fmt.Errorf("mcs: expected CHANNEL_JOIN_CONFIRM, got pdu type %d", option))
return
} Defensive patterns
Strategy: retry
Try / catch
c.On("error", func(err error) {
if strings.Contains(err.Error(), "WAIT_CHANNEL_JOIN_CONFIRM") {
log.Println("unexpected MCS PDU during join; retrying handshake")
retryConnect(1)
}
}) Prevention
- Test against a standard RDP server before deploying to exotic targets
- Log the received PDU type to spot protocol desync early
- Reuse a fresh client object per connection to avoid stale stream state
When it happens
Trigger: recvChannelJoinConfirm reads a PDU whose header does not match CHANNEL_JOIN_CONFIRM — e.g. the server sent a Disconnect Provider Ultimatum, a different MCS PDU type, or garbage bytes after the user channel join request.
Common situations: Connecting to a non-standard RDP server or gateway that interleaves MCS PDUs differently; server rejected the connection earlier and sent a disconnect PDU; desynced stream due to a preceding parsing bug leaving bytes in the buffer.
Related errors
- NODE_RDP_PROTOCOL_T125_MCS_INVALID_USER_ID
- failed to get server public key
- Not a valid license packet
- bad BER tags
- invalid expected BER tag
AI-assisted analysis of shadow1ng/fscan@95cc12e753 (2026-09-06).
Data as JSON: /api/errors/bfaf8d4cac9ea588.
Report an issue: GitHub.