Documentation
¶
Overview ¶
Package rtdp implements the RealTime Delivery Protocol (RTDP) from INTERCONNECTION.md.
RTDP turns any ordered, reliable byte stream (TCP by default) into an authenticated, confidential channel between two federation servers. After a short handshake the peers derive per-direction AES-256-GCM keys and exchange framed packets without re-signing every message.
Handshake ¶
The dialer and acceptor exchange a signed PeerAuthHello carrying an ephemeral X25519 public key, perform an X25519 ECDH, and derive keys with HKDF (see DeriveKeys). They then exchange an encrypted PeerPing / PeerPong. Dial and Accept drive the whole flow and return a ready Session.
Framing ¶
Every packet is framed as VarInt length || VarInt packet-ID || data. PeerAuthHello (0x00) is sent in the clear; every other packet type is encrypted, with data laid out as Int32 sequence || Prefixed AEAD-tag || ciphertext. Sequence numbers increase per direction modulo 2^31; the peer rejects any out-of-order packet.
Specification ambiguities and interoperability ¶
The specification leaves a few details under-specified. Where it does, this package makes an explicit, documented choice; any interoperating implementation must make the same one.
- HKDF hash and salt. The spec pins X25519, HKDF and AES-256-GCM but does not name the HKDF hash or the exact Extract salt ("salt=0"). This package uses HKDF-SHA-256 with a zero-length salt — equivalent to RFC 5869's "no salt", i.e. HashLen zero bytes — and the info strings "rdp client" / "rdp server". See DeriveKeys.
- Nonce and tag. Each direction's 12-byte GCM nonce is IV_prefix(8) || sequence(Int32, big-endian, 4); the AEAD tag is 16 bytes; no additional authenticated data is used. These follow directly from the spec and are fixed here for completeness.
- Packet framing of encrypted packets. The packet-ID travels in the clear ahead of the encrypted section (Int32 sequence || Prefixed AEAD-tag || ciphertext), matching the spec's "the data differs between plaintext and ciphertext packets" wording.
Index ¶
- Constants
- Variables
- type Config
- type DirectionKeys
- type Packet
- type PacketID
- type PeerAcknowledge
- type PeerAuthHello
- type PeerDisconnect
- type PeerPayload
- type PeerPing
- type PeerPong
- type STARTRTDOptions
- type Session
- func (s *Session) Acknowledge(transactionID int32, errMsg string) error
- func (s *Session) Close() error
- func (s *Session) Disconnect(reason int32, message string) error
- func (s *Session) IsClient() bool
- func (s *Session) Peer() mfp.PublicKey
- func (s *Session) Ping() error
- func (s *Session) Pong() error
- func (s *Session) ReadPacket() (Packet, error)
- func (s *Session) SendPayload(p *PeerPayload) error
- func (s *Session) SendSequence() int32
- func (s *Session) WritePacket(p Packet) error
- type SessionKeys
Constants ¶
const ( // DisconnectGeneric is any reason other than a rekey. DisconnectGeneric int32 = 0 // DisconnectRekey signals that the sender is renegotiating keys; the // acceptor should await a fresh connection. DisconnectRekey int32 = 1 )
DisconnectReason values carried by PeerDisconnect.
const DefaultMaxClockSkew = 30 * time.Second
DefaultMaxClockSkew is the tolerance applied to a peer's PeerAuthHello timestamp when none is configured.
const EphemeralKeySize = 32
EphemeralKeySize is the length of an X25519 public key.
const MaxSequence = int32(1<<31 - 1)
MaxSequence is the largest usable sequence number (2^31 - 1).
const STARTRTDIntent int32 = 127
STARTRTDIntent is the Minecraft handshake Intent value that signals the start of an RTDP session over a shared game port.
Variables ¶
var ( // ErrConfig indicates a missing or invalid [Config] field. ErrConfig = errors.New("rtdp: invalid config") // ErrHelloTime means the peer's handshake timestamp was non-positive or // outside the allowed clock skew. ErrHelloTime = errors.New("rtdp: handshake timestamp out of range") // ErrHelloTarget means the peer addressed the handshake to a different key. ErrHelloTarget = errors.New("rtdp: handshake target is not our key") // ErrHelloUnknownPeer means the peer's identity key is not in the key set. ErrHelloUnknownPeer = errors.New("rtdp: handshake from unknown peer") // ErrHelloSignature means the handshake signature did not verify. ErrHelloSignature = errors.New("rtdp: handshake signature invalid") // ErrPeerMismatch means the authenticated peer differs from the expected // Config.Peer. ErrPeerMismatch = errors.New("rtdp: peer key does not match expected") // ErrHandshake is returned when an unexpected packet arrives during the // ping/pong handshake exchange. ErrHandshake = errors.New("rtdp: unexpected packet during handshake") )
Handshake errors.
var ( // ErrSequence means a received packet's sequence number was not the // expected successor; per spec the connection must be dropped. ErrSequence = errors.New("rtdp: out-of-order packet sequence") // ErrDecrypt means AEAD decryption or authentication failed. ErrDecrypt = errors.New("rtdp: AEAD open failed") // ErrRekeyRequired means the send sequence space is exhausted; the caller // must renegotiate keys (send a rekey [PeerDisconnect] and reconnect) // instead of letting the sequence wrap. ErrRekeyRequired = errors.New("rtdp: sequence space exhausted, rekey required") // ErrPlaintextAfterHandshake means a clear-text packet arrived on an // established session. ErrPlaintextAfterHandshake = errors.New("rtdp: unexpected plaintext packet") // ErrCannotEncrypt means [PeerAuthHello] was passed to WritePacket; it is // only ever sent in the clear during the handshake. ErrCannotEncrypt = errors.New("rtdp: PeerAuthHello cannot be sent encrypted") )
Session and transport errors.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Identity is this server's long-term ed25519 key pair.
Identity *mfp.Identity
// Known is the registry of authorized federation peer keys.
Known *mfp.KeySet
// Peer is the expected remote identity key. Required by Dial; when set on
// Accept, the incoming peer must match it.
Peer mfp.PublicKey
// MaxClockSkew bounds the accepted difference between the peer's handshake
// timestamp and local time. Defaults to [DefaultMaxClockSkew].
MaxClockSkew time.Duration
// Now overrides the clock. Defaults to time.Now.
Now func() time.Time
// STARTRTD, when non-nil, enables the STARTRTD port-multiplexing extension:
// Dial sends a Minecraft handshake (Intent 127) first, and Accept expects
// and consumes one before the RTDP handshake.
STARTRTD *STARTRTDOptions
}
Config configures a handshake. Identity and Known are always required; Peer is required for Dial (the target's key) and optional for Accept (an allow-list of one).
type DirectionKeys ¶
DirectionKeys holds the AES-256 key and IV prefix for one direction of a session. The client encrypts with the client keys and decrypts the server keys; the server does the reverse.
type Packet ¶
type Packet interface {
// ID returns the packet's type identifier.
ID() PacketID
// contains filtered or unexported methods
}
Packet is implemented by every RTDP packet type.
type PeerAcknowledge ¶
PeerAcknowledge (0x04) acknowledges a PeerPayload. TransactionID echoes the payload's; Error is empty on success.
func (*PeerAcknowledge) ID ¶
func (*PeerAcknowledge) ID() PacketID
type PeerAuthHello ¶
type PeerAuthHello struct {
LocalKey mfp.PublicKey // sender's ed25519 identity key (32 bytes)
TargetKey mfp.PublicKey // intended recipient's ed25519 key (32 bytes)
LocalEphemeral []byte // sender's ephemeral X25519 public key (32 bytes)
UnixTime int64 // sender's current UNIX time in seconds (> 0)
Signature []byte // ed25519 signature over the signing message (64 bytes)
}
PeerAuthHello (0x00) is the clear-text handshake packet. It carries the sender's signed ephemeral X25519 public key.
func (*PeerAuthHello) ID ¶
func (*PeerAuthHello) ID() PacketID
type PeerDisconnect ¶
PeerDisconnect (0x05) is the encrypted goodbye. After sending it, close the connection. Reason DisconnectRekey means the sender is renegotiating keys.
func (*PeerDisconnect) ID ¶
func (*PeerDisconnect) ID() PacketID
type PeerPayload ¶
PeerPayload (0x03) is the encrypted data-carrying packet. Action and Subject carry the same meaning as in ODP. A non-zero TransactionID obliges the recipient to reply with a PeerAcknowledge.
func (*PeerPayload) ID ¶
func (*PeerPayload) ID() PacketID
type PeerPing ¶
type PeerPing struct{}
PeerPing (0x01) is an encrypted keep-alive request. The recipient must reply with a PeerPong.
type STARTRTDOptions ¶
type STARTRTDOptions struct {
// ProtocolVersion is the Minecraft protocol version field (unused by RTDP).
ProtocolVersion int32
// Host is the server address field. Defaults to "localhost".
Host string
// Port is the server port field. Defaults to 25565.
Port uint16
}
STARTRTDOptions configures the Minecraft handshake ("ServerboundHandshake", packet 0x00) that the STARTRTD extension sends before the RTDP handshake. The zero value is valid; empty fields take sensible defaults.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is an established, encrypted RTDP channel. It is safe to call Session.ReadPacket and Session.WritePacket from different goroutines concurrently; each direction is independently serialized.
func Accept ¶
func Accept(conn io.ReadWriteCloser, cfg Config) (sess *Session, err error)
Accept performs the server side of the RTDP handshake over conn and returns a ready Session. On any error the connection is closed.
func Dial ¶
func Dial(conn io.ReadWriteCloser, cfg Config) (sess *Session, err error)
Dial performs the client side of the RTDP handshake over conn and returns a ready Session. Config.Peer must be the target server's ed25519 key. On any error the connection is closed.
func (*Session) Acknowledge ¶
Acknowledge sends a PeerAcknowledge for transactionID; errMsg is empty on success.
func (*Session) Close ¶
Close closes the underlying connection without sending a PeerDisconnect.
func (*Session) Disconnect ¶
Disconnect sends a PeerDisconnect and then closes the underlying connection, as the specification requires.
func (*Session) ReadPacket ¶
ReadPacket reads, decrypts and parses the next packet. A sequence-number mismatch returns ErrSequence and an AEAD failure returns ErrDecrypt; per the specification the caller must then disconnect (see Session.Disconnect).
func (*Session) SendPayload ¶
func (s *Session) SendPayload(p *PeerPayload) error
SendPayload sends a PeerPayload.
func (*Session) SendSequence ¶
SendSequence returns the sequence number that the next sent packet will use. Monitor it to renegotiate keys before it approaches MaxSequence.
func (*Session) WritePacket ¶
WritePacket encrypts and sends any packet except PeerAuthHello. It advances the send sequence. When the sequence space is exhausted it returns ErrRekeyRequired for every packet other than a PeerDisconnect (so the rekey signal can still be delivered).
type SessionKeys ¶
type SessionKeys struct {
Client DirectionKeys
Server DirectionKeys
}
SessionKeys is the full set of keys derived from a shared secret.
func DeriveKeys ¶
func DeriveKeys(sharedSecret []byte) (SessionKeys, error)
DeriveKeys derives the per-direction AES-256-GCM keys from an X25519 shared secret using HKDF-SHA-256, as documented in the package overview:
PRK = HKDF-Extract(salt=0, sharedSecret) client_AES_key(32) || client_IV_prefix(8) = HKDF-Expand(PRK, "rdp client", 40) server_AES_key(32) || server_IV_prefix(8) = HKDF-Expand(PRK, "rdp server", 40)