Contents

mKCP

A reliable protocol over UDP that trades bandwidth for lower latency on bad links.

mKCP exists for one situation: a link that loses packets, where TCP’s polite congestion control makes everything slow. It reimplements reliable, ordered delivery in userspace on top of raw UDP datagrams — but where TCP backs off when packets vanish, mKCP retransmits early, retransmits often, and keeps pushing at whatever rate you configured. You pay for that in bandwidth; you get paid back in latency.

The ACK for #8 arrives while #7 is still missing — HandleFastAck cuts #7's retransmit timer by RTO/3 instead of waiting for the full timeout.
transport/internet/kcp/kcp.go transport/internet/kcp/connection.go

Reliability is rebuilt in userspace, segment by segment

Every mKCP packet is a plain UDP datagram carrying segments, and a 2-byte conversation id is the entire session layer. The parser (KCPPacketReader) accepts several segments per datagram, though this sender writes one at a time (SimpleSegmentWriter). A data segment costs 18 bytes of header — conversation, command, option, timestamp, sequence number, the sender’s first-unacknowledged number, and a length — so each datagram carries at most MTU − 18 bytes of payload (mss in NewConnection). The dialer picks the conversation id by incrementing a global counter seeded from dice.RollUint16(); the listener demultiplexes every incoming datagram into sessions keyed by ConnectionID{Remote, Port, Conv}, so one UDP socket on the server carries every client conversation at once.

There is no handshake. The first segment that arrives with an unknown (address, port, conv) triple — anything except a terminate command — simply creates the session on the spot (Listener.OnReceive). Teardown is a small state machine driven by CommandTerminate segments, and a connection that hears nothing for 30 seconds closes itself (Connection.flush).

transport/internet/kcp/segment.go transport/internet/kcp/output.go transport/internet/kcp/dialer.go transport/internet/kcp/listener.go

The retransmit clock is impatient by design

mKCP treats every hint of loss as a reason to resend now, not a reason to slow down. The retransmission timeout starts from the standard RFC 6298 smoothed-RTT formula, and RoundTripInfo.Update even pads it by 5/4 — a deliberately loose deadline, because the timeout is only the fallback. Three faster triggers run in front of it:

  • Every tti milliseconds (default 50) the sending window walks its unacknowledged segments and resends anything past its deadline (SendingWindow.Flush).
  • When an ACK arrives for a later segment, every earlier still-missing segment gets its deadline cut by rto/3 (HandleFastAck) — one out-of-order ACK is enough to pull retransmission forward.
  • The ACKs themselves are unreliable UDP, so the receiver re-sends each acknowledgment every max(rto/2, 20 ms) until the peer’s next segment proves it was heard (AckList.Flush / AckList.Clear).

The result is a protocol that recovers from a lost packet in a fraction of an RTT instead of TCP’s timeout-and-shrink dance — by sending many packets more than once.

transport/internet/kcp/sending.go transport/internet/kcp/receiving.go

You declare the bandwidth — congestion control is opt-in

mKCP does not discover how fast the link is; it believes you. uplinkCapacity and downlinkCapacity (in MB/s) are converted straight into window sizes — the number of MTU-sized segments your declared rate can move in one tti: capacity × 1024² / mtu / (1000 / tti), floored at 8 (GetSendingInFlightSize). With the defaults — mtu 1350, tti 50, uplink 5, downlink 20 — that is 194 segments in flight upstream and 776 downstream.

{
	"network": "mkcp",
	"kcpSettings": {
		"mtu": 1350,
		"tti": 20,
		"uplinkCapacity": 5,
		"downlinkCapacity": 20,
		"congestion": false,
		"writeBufferSize": 2
	}
}

Lowering tti to 20 ms makes retransmission checks 2.5× more frequent — lower recovery latency, more duplicate packets. congestion: true enables the only backoff mKCP has: at ≥15% observed loss the control window shrinks to 3/4, at ≤5% it grows by a quarter, clamped between 16 segments and twice the configured window (OnPacketLoss).

transport/internet/kcp/config.go

Obfuscation moved out of the transport in this fork

The classic header (utp, wechat-video, …) and seed options are gone from kcpSettings — packet disguise now lives in the finalmask layer. Setting either key makes config loading fail with a removed-feature error pointing at the replacements (KCPConfig.Build in infra/conf/transport_internet.go). The same disguises still exist, but as composable UDP masks that wrap the socket before mKCP writes to it (UdpmaskManager.WrapPacketConnClient in the dialer): header-utp, header-wechat, header-srtp, header-dtls, header-wireguard fake protocol headers (the uTP mask, for instance, prepends 4 bytes with a random connection id so datagrams look like BitTorrent uTP), while mkcp-original reproduces the legacy default obfuscation (a FNV-1a checksum plus XOR scramble — what old mKCP applied when no seed was set) and mkcp-aes128gcm, the seed replacement, encrypts every datagram with AES-128-GCM keyed from a pre-shared password:

{
	"network": "mkcp",
	"kcpSettings": { "tti": 20 },
	"finalmask": {
		"udp": [
			{ "type": "mkcp-aes128gcm", "settings": { "password": "example-psk" } },
			{ "type": "header-utp" }
		]
	}
}
transport/internet/kcp/io.go

Spend the overhead only where it pays

On a clean link mKCP is pure waste — the aggression only wins when packets are actually being lost. Every segment costs 18 bytes, ACK packets flow every tick and are themselves duplicated, and early retransmission re-sends packets that were merely late.

Don't

Use mKCP on a stable, low-loss line

You pay duplicate retransmits, redundant ACK traffic and header overhead for a problem you don’t have — and many ISPs throttle sustained UDP flows, making it slower than plain TCP.

Do

Reserve it for lossy last miles

On mobile networks or long intercontinental paths with real packet loss, set uplinkCapacity/downlinkCapacity to your honest line rate and drop tti to 20–30 ms. Recovery happens in milliseconds while TCP is still waiting for its timeout.