shadow1ng/fscan · error

NODE_RDP_PROTOCOL_T125_MCS_BAD_HEADER

NODE_RDP_PROTOCOL_T125_MCS_BAD_HEADER

Error message

NODE_RDP_PROTOCOL_T125_MCS_BAD_HEADER

What it means

recvAttachUserConfirm expects the first byte of the server reply to be an MCS PDU header whose opcode is ATTACH_USER_CONFIRM (11). readMCSPDUHeader checks (option>>2)==11; if not, this sentinel error is emitted. It means the server responded with a different MCS PDU than the Attach User Confirm the client was waiting for.

Source

Thrown at libs/grdp/protocol/t125/mcs.go:375

func (c *MCSClient) sendAttachUserRequest() {
	buff := &bytes.Buffer{}
	writeMCSPDUHeader(ATTACH_USER_REQUEST, 0, buff)
	c.transport.Write(buff.Bytes())
}

func (c *MCSClient) recvAttachUserConfirm(s []byte) {
	glog.Debug("mcs recvAttachUserConfirm", hex.EncodeToString(s))
	r := bytes.NewReader(s)

	option, err := core.ReadUInt8(r)
	if err != nil {
		c.Emit("error", err)
		return
	}

	if !readMCSPDUHeader(option, ATTACH_USER_CONFIRM) {
		c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_BAD_HEADER"))
		return
	}

	e, err := per.ReadEnumerates(r)
	if err != nil {
		c.Emit("error", err)
		return
	}
	if e != 0 {
		c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_SERVER_REJECT_USER'"))
		return
	}

	userId, _ := per.ReadInteger16(r)
	userId += MCS_USERCHANNEL_BASE
	c.userId = userId

	c.channels = append(c.channels, MCSChannelInfo{userId, "user"})

View on GitHub (pinned to 95cc12e753)

Solutions

  1. Hex-dump the received buffer and decode the actual MCS opcode from (option>>2) to see what the server sent instead.
  2. Check whether the opcode is DISCONNECT_PROVIDER_ULTIMATUM (8) — that usually indicates the server rejected the connection outright.
  3. Verify the preceding sendAttachUserRequest was actually written (check transport.Write error handling in sendAttachUserRequest).
  4. Retry the connection; transient server-side resets can put the session out of sequence.

Example fix

// before
if !readMCSPDUHeader(option, ATTACH_USER_CONFIRM) {
    c.Emit("error", errors.New("NODE_RDP_PROTOCOL_T125_MCS_BAD_HEADER"))
    return
}
// after
if readMCSPDUHeader(option, DISCONNECT_PROVIDER_ULTIMATUM) {
    c.Emit("error", errors.New("server sent DISCONNECT_PROVIDER_ULTIMATUM instead of ATTACH_USER_CONFIRM"))
    c.transport.Close()
    return
}
if !readMCSPDUHeader(option, ATTACH_USER_CONFIRM) {
    c.Emit("error", fmt.Errorf("NODE_RDP_PROTOCOL_T125_MCS_BAD_HEADER: got opcode %d", option>>2))
    return
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Decode the actual opcode before dispatching handlers:
op := (buf[0] >> 2)
// expect op == 11 (ATTACH_USER_CONFIRM); anything else will hit BAD_HEADER

Try / catch

mcs.On("error", func(err error) {
    if strings.Contains(err.Error(), "MCS_BAD_HEADER") {
        // server sent an unexpected PDU — close and retry or inspect dump
    }
})

Prevention

When it happens

Trigger: The 'data' event handled by recvAttachUserConfirm delivers a buffer whose first byte's high 6 bits are not ATTACH_USER_CONFIRM — e.g. a Disconnect Provider Ultimatum, an error PDU, or out-of-order data arrives.

Common situations: Server rejected the earlier Connect Initial/ Erect Domain phase and sends a disconnect PDU instead; TCP framing mismatch so the header byte read is not the PDU start; server under load sends data out of the expected request/confirm sequence.

Related errors


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