moon-proxyproto

Strict HAProxy PROXY protocol v1/v2 codec, streaming decoder, TLV parser, and CRC32C validator for MoonBit.

moon add P7001/moon-proxyproto@0.1.0
Download zip
Author
Version
0.1.0
License
Apache-2.0
Last updated
12 days ago
Downloads
2
README

#moon-proxyproto

CI

moon-proxyproto is a strict MoonBit codec for HAProxy PROXY Protocol v1 and v2. It is not an HTTP proxy, reverse proxy, VPN, or connection forwarder.

The library decodes one PROXY header and returns the untouched application payload. It supports v1 TCP4/TCP6/UNKNOWN, v2 LOCAL/PROXY, IPv4, IPv6, UNIX address fields, STREAM and DGRAM transports, TLV boundary parsing, SSL TLV parsing, and Castagnoli CRC32C validation.

Wire validation is deliberately strict: the v1 limit is 107 bytes including CRLF, PROXY v2 accepts only registered family/transport combinations, and LOCAL v2 consumes its declared body while ignoring and normalizing its address metadata.

#Security

Only enable PROXY parsing on a connection accepted from a trusted proxy. A source address carried in a PROXY header is metadata, not an authenticated identity. The decoder never silently treats a malformed PROXY header as HTTP, TLS, SMTP, or another protocol. Configure an explicit policy at the trust boundary.

#Examples

let policy = DecodePolicy::v2_only()
match decode_v2(input, policy) {
Ok(frame) => println("payload bytes: \{frame.remaining.length()}")
Err(error) => println(error.context)
}

For streams, use Decoder::new(policy) and call feed with each received chunk. For UDP, call decode_datagram; it never joins two datagrams.

Executable examples are available under examples/decode_v1, decode_v2, preserve_payload, stream_chunks, tlv_inspect, http_preamble, and tls_preamble. Run one with, for example, moon run examples/decode_v1.

#Command-line inspection

proxyproto-tool provides decode-v1, decode-v2, decode-auto, encode-v1, encode-v2, inspect-tlv, verify-crc32c, split-test, and vector-check. Output is stable JSON. decode-auto is only an offline diagnostic command and must not be used as automatic protocol detection on a public listener.

#Validation

The local suite contains 156 named tests, 3888 chunk-split and truncation cases, 1000 deterministic v2 roundtrips, and 71 generated vectors. Vector metadata is checked against actual MoonBit parser results, including precise failure kinds and payload boundaries. See docs/test-matrix.md, docs/protocol-matrix.md, and docs/security.md for the tested matrix and trust boundary.

#Project status

This repository contains the active development version of moon-proxyproto.

  • GitHub repository: P7001/moon-proxyproto
  • Module namespace: P7001/moon-proxyproto
  • Current status: pre-release
  • Current version: 0.1.0-dev

#
AddressBlock

pub(all) enum AddressBlock {
NoAddress
Ipv4(Ipv4Endpoints)
Ipv6(Ipv6Endpoints)
UnixAddress(UnixEndpoints)
RawUnsupported(Bytes)
} derive(
Debug
)

#
AddressFamily

pub(all) enum AddressFamily {
Unspec
Inet
Inet6
Unix
} derive(Eq,
Debug
)

#
ByteCursor

pub struct ByteCursor {
input : Bytes
position_value : Int
} derive(
Debug
)

#
ByteCursor::checkpoint

fn ByteCursor::checkpoint(self : ByteCursor) -> Int

#
ByteCursor::new

fn ByteCursor::new(input : Bytes) -> ByteCursor

#
ByteCursor::peek

fn ByteCursor::peek(self : ByteCursor) -> Byte?

#
ByteCursor::peek_n

fn ByteCursor::peek_n(self : ByteCursor, count : Int) -> Bytes?

#
ByteCursor::position

fn ByteCursor::position(self : ByteCursor) -> Int

#
ByteCursor::read_exact

fn ByteCursor::read_exact(self : ByteCursor, count : Int) -> Bytes?

#
ByteCursor::read_u16_be

fn ByteCursor::read_u16_be(self : ByteCursor) -> Int?

#
ByteCursor::read_u32_be

fn ByteCursor::read_u32_be(self : ByteCursor) -> UInt?

#
ByteCursor::read_u8

fn ByteCursor::read_u8(self : ByteCursor) -> Byte?

#
ByteCursor::remaining

fn ByteCursor::remaining(self : ByteCursor) -> Int

#
ByteCursor::restore

fn ByteCursor::restore(self : ByteCursor, checkpoint : Int) -> Unit

#
ByteCursor::skip

fn ByteCursor::skip(self : ByteCursor, count : Int) -> Bool

#
ChunkBuffer

pub struct ChunkBuffer {
chunks : Array[Bytes]
first_chunk : Int
first_offset : Int
available_bytes : Int
copied_bytes_value : Int
} derive(
Debug
)

A queue of immutable input chunks. Bytes are copied only by copy_exact.

#
ChunkBuffer::append

fn ChunkBuffer::append(self : ChunkBuffer, chunk : Bytes) -> Unit

#
ChunkBuffer::available

fn ChunkBuffer::available(self : ChunkBuffer) -> Int

#
ChunkBuffer::chunk_count

fn ChunkBuffer::chunk_count(self : ChunkBuffer) -> Int

#
ChunkBuffer::clear

fn ChunkBuffer::clear(self : ChunkBuffer) -> Unit

#
ChunkBuffer::copied_bytes

fn ChunkBuffer::copied_bytes(self : ChunkBuffer) -> Int

#
ChunkBuffer::copy_exact

fn ChunkBuffer::copy_exact(self : ChunkBuffer, length : Int) -> Bytes?

#
ChunkBuffer::discard

fn ChunkBuffer::discard(self : ChunkBuffer, length : Int) -> Bool

#
ChunkBuffer::new

#
ChunkBuffer::peek_byte

fn ChunkBuffer::peek_byte(self : ChunkBuffer, offset : Int) -> Byte?

#
Command

pub(all) enum Command {
Local
Proxy
} derive(Eq,
Debug
)

#
DecodePolicy

pub(all) struct DecodePolicy {
expected_protocol : ExpectedProtocol
max_v1_line_bytes : Int
max_v2_payload_bytes : Int
verify_crc32c : Bool
require_crc32c : Bool
allow_local_command : Bool
allow_unspec : Bool
preserve_unknown_tlvs : Bool
} derive(
Debug
)

#
DecodePolicy::strict

fn DecodePolicy::strict() -> DecodePolicy

#
DecodePolicy::v1_only

fn DecodePolicy::v1_only() -> DecodePolicy

#
DecodePolicy::v2_only

fn DecodePolicy::v2_only() -> DecodePolicy

#
DecodeProgress

pub(all) enum DecodeProgress {
NeedMore
Done(DecodedFrame)
Failed(ProxyError)
} derive(
Debug
)

#
DecodedFrame

pub(all) struct DecodedFrame {
header : ProxyHeader
remaining : Bytes
consumed : Int
} derive(
Debug
)

#
Decoder

pub struct Decoder {
policy : DecodePolicy
buffer : ChunkBuffer
state_value : DecoderState
failure : ProxyError?
} derive(
Debug
)

#
Decoder::buffered_len

fn Decoder::buffered_len(self : Decoder) -> Int

#
Decoder::feed

fn Decoder::feed(self : Decoder, chunk : Bytes) -> DecodeProgress

#
Decoder::new

fn Decoder::new(policy : DecodePolicy) -> Decoder

#
Decoder::reset

fn Decoder::reset(self : Decoder) -> Unit

#
Decoder::state

fn Decoder::state(self : Decoder) -> DecoderState

#
DecoderState

pub(all) enum DecoderState {
AwaitingPrefix
ReadingV1Line
ReadingV2Fixed
ReadingV2Body(Int)
Done
Failed
} derive(Eq,
Debug
)

#
ExpectedProtocol

pub(all) enum ExpectedProtocol {
V1Only
V2Only
V1OrV2
} derive(Eq,
Debug
)

#
GeneratedVector

pub(all) struct GeneratedVector {
name : String
parser : String
data : Bytes
expected_success : Bool
expected_error : String
expected_consumed : Int
expected_remaining_length : Int
expected_family : String
expected_transport : String
}

#
Ipv4Address

pub(all) struct Ipv4Address {
octets : FixedArray[Byte]
} derive(
Debug
)

#
Ipv4Address::new

fn Ipv4Address::new(octets : FixedArray[Byte]) -> Result[Ipv4Address, ProxyError]

#
Ipv4Endpoints

pub(all) struct Ipv4Endpoints {
source_address : Ipv4Address
destination_address : Ipv4Address
source_port : Int
destination_port : Int
} derive(
Debug
)

#
Ipv6Address

pub(all) struct Ipv6Address {
octets : FixedArray[Byte]
} derive(
Debug
)

#
Ipv6Address::new

fn Ipv6Address::new(octets : FixedArray[Byte]) -> Result[Ipv6Address, ProxyError]

#
Ipv6Endpoints

pub(all) struct Ipv6Endpoints {
source_address : Ipv6Address
destination_address : Ipv6Address
source_port : Int
destination_port : Int
} derive(
Debug
)

#
ParsedTlv

pub(all) enum ParsedTlv {
Alpn(Bytes)
Authority(String)
Crc32c(UInt)
Noop(Bytes)
UniqueId(Bytes)
Ssl(SslInfo)
Netns(String)
Custom(Byte, Bytes)
Experimental(Byte, Bytes)
FutureReserved(Byte, Bytes)
Unknown(Byte, Bytes)
} derive(
Debug
)

#
PropertyGenerator

pub struct PropertyGenerator {
state : UInt
} derive(
Debug
)

Deterministic xorshift32 generator for reproducible protocol tests.

#
PropertyGenerator::new

fn PropertyGenerator::new(seed : UInt) -> PropertyGenerator

#
PropertyGenerator::next_byte

fn PropertyGenerator::next_byte(self : PropertyGenerator) -> Byte

#
PropertyGenerator::next_bytes

fn PropertyGenerator::next_bytes(self : PropertyGenerator, length : Int) -> Bytes

#
PropertyGenerator::next_ipv4

#
PropertyGenerator::next_ipv6

#
PropertyGenerator::next_payload

fn PropertyGenerator::next_payload(self : PropertyGenerator) -> Bytes

#
PropertyGenerator::next_port

fn PropertyGenerator::next_port(self : PropertyGenerator) -> Int

#
PropertyGenerator::next_u32

fn PropertyGenerator::next_u32(self : PropertyGenerator) -> UInt

#
PropertyGenerator::next_unknown_tlv

fn PropertyGenerator::next_unknown_tlv(self : PropertyGenerator) -> RawTlv

#
PropertyGenerator::next_v2_header

fn PropertyGenerator::next_v2_header(self : PropertyGenerator) -> ProxyHeader

#
ProxyError

pub(all) struct ProxyError {
kind : ProxyErrorKind
offset : Int
context : String
} derive(
Debug
)

#
ProxyErrorKind

pub(all) enum ProxyErrorKind {
NeedMoreData
InvalidPrefix
UnsupportedVersion
InvalidCommand
InvalidFamily
InvalidTransport
InvalidFamilyTransport
InvalidV1Line
V1LineTooLong
MissingCrlf
InvalidIpv4
InvalidIpv6
InvalidPort
InvalidV2Signature
InvalidLength
HeaderTooLarge
TruncatedAddress
TruncatedTlv
InvalidTlv
DuplicateCrc32c
MissingCrc32c
Crc32cMismatch
InvalidSslTlv
PolicyViolation
DecoderAlreadyFinished
DecoderFailed
} derive(Eq,
Debug
)

#
ProxyHeader

pub(all) struct ProxyHeader {
version : ProxyVersion
command : Command
family : AddressFamily
transport : Transport
address : AddressBlock
tlvs : Array[RawTlv]
} derive(
Debug
)

#
ProxyHeader::new

fn ProxyHeader::new(version : ProxyVersion, command : Command, family : AddressFamily, transport : Transport, address : AddressBlock, tlvs : Array[RawTlv]) -> Result[ProxyHeader, ProxyError]

#
ProxyVersion

pub(all) enum ProxyVersion {
V1
V2
} derive(Eq,
Debug
)

#
RawTlv

pub(all) struct RawTlv {
type_code : Byte
value : Bytes
} derive(
Debug
)

#
SslInfo

pub(all) struct SslInfo {
client_flags : Byte
verify : UInt
sub_tlvs : Array[RawTlv]
} derive(
Debug
)

#
SslTlv

pub(all) struct SslTlv {
client_flags : Byte
verify : UInt
sub_tlvs : Array[RawTlv]
} derive(
Debug
)

#
TlvRange

pub(all) enum TlvRange {
Registered
CustomRange
ExperimentalRange
FutureReservedRange
UnknownRange
} derive(Eq,
Debug
)

#
Transport

pub(all) enum Transport {
Unspec
Stream
Datagram
} derive(Eq,
Debug
)

#
UnixEndpoints

pub(all) struct UnixEndpoints {
source : Bytes
destination : Bytes
} derive(
Debug
)

#
VectorMetadata

pub(all) struct VectorMetadata {
name : String
category : String
expected_success : Bool
expected_error : String
expected_consumed : Int
expected_remaining_length : Int
expected_family : String
expected_transport : String
} derive(
Debug
)

#
GENERATED_VECTOR_COUNT

let GENERATED_VECTOR_COUNT : Int

#
TLV_ALPN

let TLV_ALPN : Byte

#
TLV_AUTHORITY

let TLV_AUTHORITY : Byte

#
TLV_CRC32C

let TLV_CRC32C : Byte

#
TLV_NETNS

let TLV_NETNS : Byte

#
TLV_NOOP

let TLV_NOOP : Byte

#
TLV_SSL

let TLV_SSL : Byte

#
TLV_SSL_CIPHER

let TLV_SSL_CIPHER : Byte

#
TLV_SSL_CLIENT_CERT

let TLV_SSL_CLIENT_CERT : Byte

#
TLV_SSL_CN

let TLV_SSL_CN : Byte

#
TLV_SSL_GROUP

let TLV_SSL_GROUP : Byte

#
TLV_SSL_KEY_ALG

let TLV_SSL_KEY_ALG : Byte

#
TLV_SSL_SIG_ALG

let TLV_SSL_SIG_ALG : Byte

#
TLV_SSL_SIG_SCHEME

let TLV_SSL_SIG_SCHEME : Byte

#
TLV_SSL_VERSION

let TLV_SSL_VERSION : Byte

#
TLV_UNIQUE_ID

let TLV_UNIQUE_ID : Byte

#
assert_all_prefix_truncations

fn assert_all_prefix_truncations(header : Bytes, policy : DecodePolicy) -> Int

#
assert_all_single_splits

fn assert_all_single_splits(input : Bytes, policy : DecodePolicy) -> Int

#
assert_bytes_eq

fn assert_bytes_eq(actual : Bytes, expected : Bytes) -> Unit

#
assert_err_kind

fn assert_err_kind(result : Result[DecodedFrame, ProxyError], expected : ProxyErrorKind) -> Unit

#
assert_header_eq

fn assert_header_eq(actual : ProxyHeader, expected : ProxyHeader) -> Unit

#
assert_ok

fn assert_ok(result : Result[DecodedFrame, ProxyError]) -> DecodedFrame

#
assert_remaining_eq

fn assert_remaining_eq(frame : DecodedFrame, expected : Bytes) -> Unit

#
assert_roundtrip_v1

fn assert_roundtrip_v1(header : ProxyHeader) -> Unit

#
assert_roundtrip_v2

fn assert_roundtrip_v2(header : ProxyHeader) -> Unit

#
assert_selected_three_way_splits

fn assert_selected_three_way_splits(input : Bytes, cuts : Array[Int], policy : DecodePolicy) -> Int

#
bytes_concat

fn bytes_concat(left : Bytes, right : Bytes) -> Bytes

#
bytes_equal_constant_time

fn bytes_equal_constant_time(left : Bytes, right : Bytes) -> Bool

#
bytes_slice

fn bytes_slice(input : Bytes, start : Int, end : Int) -> Result[Bytes, ProxyError]

#
classify_tlv_range

fn classify_tlv_range(type_code : Byte) -> TlvRange

#
count_single_split_cases

fn count_single_split_cases(input : Bytes) -> Int

#
count_three_way_cases

fn count_three_way_cases(first_cuts : Array[Int], second_cuts : Array[Int]) -> Int

#
crc32c

fn crc32c(data : Bytes) -> UInt

#
crc32c_reference

fn crc32c_reference(data : Bytes) -> UInt

#
decode_datagram

fn decode_datagram(datagram : Bytes, policy : DecodePolicy) -> Result[DecodedFrame, ProxyError]

#
decode_v1

fn decode_v1(input : Bytes, policy : DecodePolicy) -> Result[DecodedFrame, ProxyError]

#
decode_v2

fn decode_v2(input : Bytes, policy : DecodePolicy) -> Result[DecodedFrame, ProxyError]

#
encode

fn encode(header : ProxyHeader) -> Result[Bytes, ProxyError]

#
encode_ssl_tlv

fn encode_ssl_tlv(ssl : SslTlv) -> Result[RawTlv, ProxyError]

#
encode_tlvs

fn encode_tlvs(tlvs : Array[RawTlv]) -> Result[Bytes, ProxyError]

#
encode_v1

fn encode_v1(header : ProxyHeader) -> Result[Bytes, ProxyError]

#
encode_v2

fn encode_v2(header : ProxyHeader) -> Result[Bytes, ProxyError]

#
encode_with_payload

fn encode_with_payload(header : ProxyHeader, payload : Bytes) -> Result[Bytes, ProxyError]

#
feed_single_split

fn feed_single_split(input : Bytes, split : Int, policy : DecodePolicy) -> DecodeProgress

#
feed_three_way

fn feed_three_way(input : Bytes, first_cut : Int, second_cut : Int, policy : DecodePolicy) -> DecodeProgress

#
find_all_tlvs

fn find_all_tlvs(tlvs : Array[RawTlv], type_code : Byte) -> Array[RawTlv]

#
find_tlv

fn find_tlv(tlvs : Array[RawTlv], type_code : Byte) -> RawTlv?

#
format_ipv4

fn format_ipv4(address : Ipv4Address) -> String

#
format_ipv6

fn format_ipv6(address : Ipv6Address) -> String

#
generated_vectors

fn generated_vectors() -> Array[GeneratedVector]

#
header_semantic_eq

fn header_semantic_eq(left : ProxyHeader, right : ProxyHeader) -> Bool

#
hex_decode

fn hex_decode(input : String) -> Result[Bytes, ProxyError]

#
hex_encode

fn hex_encode(input : Bytes) -> String

#
insert_proxy_crc32c

fn insert_proxy_crc32c(header_without_checksum : ProxyHeader) -> Result[Bytes, ProxyError]

#
interpret_all_tlvs

fn interpret_all_tlvs(tlvs : Array[RawTlv]) -> Result[Array[ParsedTlv], ProxyError]

#
interpret_tlv

fn interpret_tlv(tlv : RawTlv, offset? : Int) -> Result[ParsedTlv, ProxyError]

#
parse_ipv4_text

fn parse_ipv4_text(input : String) -> Result[Ipv4Address, ProxyError]

#
parse_ipv6_text

fn parse_ipv6_text(input : String) -> Result[Ipv6Address, ProxyError]

#
parse_port_text

fn parse_port_text(input : String) -> Result[Int, ProxyError]

#
parse_ssl_tlv

fn parse_ssl_tlv(value : Bytes, offset? : Int) -> Result[SslTlv, ProxyError]

#
parse_tlvs

fn parse_tlvs(input : Bytes, offset? : Int, preserve_unknown? : Bool) -> Result[Array[RawTlv], ProxyError]

#
proxy_error

fn proxy_error(kind : ProxyErrorKind, offset : Int, context : String) -> ProxyError

#
read_u16_be

fn read_u16_be(input : Bytes, offset : Int) -> Result[Int, ProxyError]

#
read_u32_be

fn read_u32_be(input : Bytes, offset : Int) -> Result[UInt, ProxyError]

#
tlv_type_name

fn tlv_type_name(type_code : Byte) -> String

#
unix_address_field

fn unix_address_field(input : Bytes) -> Result[Bytes, ProxyError]

#
unix_address_text_view

fn unix_address_text_view(raw : Bytes) -> String?

#
v2_signature

fn v2_signature() -> Bytes

#
vector_metadata

fn vector_metadata(name : String, category : String, expected_success : Bool, expected_error : String, expected_consumed : Int, expected_remaining_length : Int, expected_family : String, expected_transport : String) -> VectorMetadata

#
verify_generated_vectors

fn verify_generated_vectors() -> Result[Int, ProxyError]

#
verify_proxy_crc32c

fn verify_proxy_crc32c(complete_header : Bytes) -> Result[Unit, ProxyError]

#
write_u16_be

fn write_u16_be(value : Int) -> Bytes

#
write_u32_be

fn write_u32_be(value : UInt) -> Bytes

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io