rtdp

package
v0.0.0-...-ae481c8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 15 Imported by: 0

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

View Source
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.

View Source
const DefaultMaxClockSkew = 30 * time.Second

DefaultMaxClockSkew is the tolerance applied to a peer's PeerAuthHello timestamp when none is configured.

View Source
const EphemeralKeySize = 32

EphemeralKeySize is the length of an X25519 public key.

View Source
const MaxSequence = int32(1<<31 - 1)

MaxSequence is the largest usable sequence number (2^31 - 1).

View Source
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

View Source
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.

View Source
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

type DirectionKeys struct {
	AESKey   []byte // 32 bytes
	IVPrefix []byte // 8 bytes
}

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 PacketID

type PacketID int32

PacketID identifies an RTDP packet type.

const (
	IDPeerAuthHello   PacketID = 0x00
	IDPeerPing        PacketID = 0x01
	IDPeerPong        PacketID = 0x02
	IDPeerPayload     PacketID = 0x03
	IDPeerAcknowledge PacketID = 0x04
	IDPeerDisconnect  PacketID = 0x05
)

RTDP packet type identifiers.

type PeerAcknowledge

type PeerAcknowledge struct {
	TransactionID int32
	Error         string
}

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

type PeerDisconnect struct {
	Reason  int32
	Message string
}

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

type PeerPayload struct {
	Action        string
	Subject       []byte
	TransactionID int32
	Data          []byte
}

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.

func (*PeerPing) ID

func (*PeerPing) ID() PacketID

type PeerPong

type PeerPong struct{}

PeerPong (0x02) is the encrypted reply to a PeerPing.

func (*PeerPong) ID

func (*PeerPong) ID() PacketID

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

func (s *Session) Acknowledge(transactionID int32, errMsg string) error

Acknowledge sends a PeerAcknowledge for transactionID; errMsg is empty on success.

func (*Session) Close

func (s *Session) Close() error

Close closes the underlying connection without sending a PeerDisconnect.

func (*Session) Disconnect

func (s *Session) Disconnect(reason int32, message string) error

Disconnect sends a PeerDisconnect and then closes the underlying connection, as the specification requires.

func (*Session) IsClient

func (s *Session) IsClient() bool

IsClient reports whether this side initiated the connection.

func (*Session) Peer

func (s *Session) Peer() mfp.PublicKey

Peer returns the authenticated ed25519 public key of the remote server.

func (*Session) Ping

func (s *Session) Ping() error

Ping sends a PeerPing.

func (*Session) Pong

func (s *Session) Pong() error

Pong sends a PeerPong.

func (*Session) ReadPacket

func (s *Session) ReadPacket() (Packet, error)

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

func (s *Session) SendSequence() int32

SendSequence returns the sequence number that the next sent packet will use. Monitor it to renegotiate keys before it approaches MaxSequence.

func (*Session) WritePacket

func (s *Session) WritePacket(p Packet) error

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)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL