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

  1. Log the actual PDU type received to identify what the server sent instead of the join confirm.
  2. Retry the connection once — transient server-side disconnects are common under load.
  3. Verify server RDP protocol support; older/proprietary servers may not follow the standard MCS join flow.
  4. 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

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


AI-assisted analysis of shadow1ng/fscan@95cc12e753 (2026-09-06). Data as JSON: /api/errors/bfaf8d4cac9ea588. Report an issue: GitHub.