Sovite.ProxyProtocol (sovite v0.6.0)

Copy Markdown View Source

The HAProxy PROXY protocol, versions 1 (text) and 2 (binary), which a proxy or load balancer sends first on a connection to pass on the original client and server addresses.

parse/2 decodes a header at the start of a binary and read/3 reads one from a socket; encode_v1/1 and encode_v2/1 build headers for clients. A parsed header is a Sovite.ProxyProtocol.Header.

{:ok, socket} = :gen_tcp.accept(listen_socket)
{:ok, %Header{command: :proxy, source: {client_ip, client_port}}} =
  Sovite.ProxyProtocol.read(socket, 10_000)

Parsing is strict, as the specification requires: anything that is not exactly a valid header is an error, and the connection should be closed. Only accept headers from trusted proxies: a header is just what its sender claims.

Version 1

A line such as PROXY TCP4 192.0.2.1 198.51.100.1 56324 25\r\n of at most 107 bytes. Addresses and ports are decimal without leading zeros. PROXY UNKNOWN is parsed as :unspec, with the rest of its line ignored. Version 1 has no LOCAL command and no TLVs.

Version 2

A 16-byte prefix (signature, version and command, family and protocol, length) followed by the addresses and TLVs. A LOCAL header is parsed without its address block, which the receiver must ignore. With AF_UNSPEC the whole block is TLVs. Headers longer than :max_length are rejected. A PP2_TYPE_CRC32C checksum is verified; the well-known TLVs must be well-formed and appear at most once.

Errors

  • :invalid_signature - neither a version 1 nor a version 2 header.
  • Version 1: :line_too_long, :invalid_line (bad syntax or characters, a lone CR or LF), :invalid_transport (not TCP4, TCP6, or UNKNOWN), :invalid_address, :invalid_port.
  • Version 2: :invalid_version, :invalid_command, :invalid_family (an undefined family or protocol), :invalid_length (too short for the family's addresses), :header_too_long, :invalid_tlv (truncated, malformed, or repeated), :crc32c_mismatch.

Summary

Functions

Encodes a version 1 header. A :proxy header over :tcp4 or :tcp6 carries its addresses; anything else becomes PROXY UNKNOWN. TLVs are not sent. Raises ArgumentError on addresses that do not match the transport.

Encodes a version 2 header with the TLVs in :tlvs (the decoded TLV fields are ignored). A PP2_TYPE_CRC32C TLV among them, such as tlv(:crc32c), gets the header's checksum. A :local header is sent with no addresses and no TLVs. Raises ArgumentError on addresses that do not match the transport, or a header over 64 KiB.

Parses a header at the start of data. Returns the header and the bytes after it, which belong to the application protocol.

Reads a header from socket, a passive (active: false) :gen_tcp socket in binary mode with packet: :raw. Fails unless the whole header arrives within timeout milliseconds.

Returns a PP2_TYPE_CRC32C placeholder, see tlv/2.

Builds a well-known version 2 TLV for Header :tlvs.

Types

error_reason()

@type error_reason() ::
  :invalid_signature
  | :line_too_long
  | :invalid_line
  | :invalid_transport
  | :invalid_address
  | :invalid_port
  | :invalid_version
  | :invalid_command
  | :invalid_family
  | :invalid_length
  | :header_too_long
  | :invalid_tlv
  | :crc32c_mismatch

Functions

encode_v1(header)

@spec encode_v1(Sovite.ProxyProtocol.Header.t()) :: binary()

Encodes a version 1 header. A :proxy header over :tcp4 or :tcp6 carries its addresses; anything else becomes PROXY UNKNOWN. TLVs are not sent. Raises ArgumentError on addresses that do not match the transport.

iex> Sovite.ProxyProtocol.encode_v1(%Sovite.ProxyProtocol.Header{
...>   transport: :tcp4,
...>   source: {{192, 0, 2, 1}, 56324},
...>   destination: {{198, 51, 100, 1}, 25}
...> })
"PROXY TCP4 192.0.2.1 198.51.100.1 56324 25\r\n"
iex> Sovite.ProxyProtocol.encode_v1(%Sovite.ProxyProtocol.Header{command: :local})
"PROXY UNKNOWN\r\n"

encode_v2(header)

@spec encode_v2(Sovite.ProxyProtocol.Header.t()) :: binary()

Encodes a version 2 header with the TLVs in :tlvs (the decoded TLV fields are ignored). A PP2_TYPE_CRC32C TLV among them, such as tlv(:crc32c), gets the header's checksum. A :local header is sent with no addresses and no TLVs. Raises ArgumentError on addresses that do not match the transport, or a header over 64 KiB.

parse(data, opts \\ [])

@spec parse(binary(), keyword()) ::
  {:ok, Sovite.ProxyProtocol.Header.t(), rest :: binary()}
  | {:more, pos_integer() | :unknown}
  | {:error, error_reason()}

Parses a header at the start of data. Returns the header and the bytes after it, which belong to the application protocol.

On incomplete input, returns {:more, n} when at least n more bytes are needed (version 2), or {:more, :unknown} when the end cannot be known yet (version 1, or too little input to tell the version).

Options:

  • :max_length - the longest version 2 header accepted, in bytes. Defaults to 4096.

Examples:

iex> {:ok, header, rest} =
...>   Sovite.ProxyProtocol.parse("PROXY TCP4 192.0.2.1 198.51.100.1 56324 25\r\nEHLO")
iex> {header.source, header.destination, rest}
{{{192, 0, 2, 1}, 56324}, {{198, 51, 100, 1}, 25}, "EHLO"}
iex> Sovite.ProxyProtocol.parse("PROXY TCP4 192.0.2.1")
{:more, :unknown}
iex> Sovite.ProxyProtocol.parse("EHLO client.example\r\n")
{:error, :invalid_signature}

read(socket, timeout, opts \\ [])

@spec read(:gen_tcp.socket(), non_neg_integer(), keyword()) ::
  {:ok, Sovite.ProxyProtocol.Header.t()}
  | {:error, error_reason() | :timeout | :closed | :inet.posix()}

Reads a header from socket, a passive (active: false) :gen_tcp socket in binary mode with packet: :raw. Fails unless the whole header arrives within timeout milliseconds.

Exactly the header is read: what the client sent after it is left on the socket for the application protocol. A version 1 line is read a byte at a time past its first 15 bytes (from the socket's buffer, not a system call each), a version 2 header by its length.

Takes the options of parse/2. Returns {:error, reason} with a reason from parse/2, :timeout, :closed, or another :inet.posix() error.

tlv(atom)

@spec tlv(:crc32c) :: Sovite.ProxyProtocol.Header.tlv()

Returns a PP2_TYPE_CRC32C placeholder, see tlv/2.

tlv(type, ssl)

@spec tlv(:alpn | :authority | :unique_id | :netns | :noop | :ssl, binary() | map()) ::
  Sovite.ProxyProtocol.Header.tlv()

Builds a well-known version 2 TLV for Header :tlvs.

  • :alpn, :authority, :unique_id, :netns, :noop - value is the binary to send.
  • :ssl - value is a map like Sovite.ProxyProtocol.Header.ssl/0, every key optional (:client defaults to [], :verify to 0, :tlvs is ignored).

tlv(:crc32c) is a placeholder that encode_v2/1 fills in.

iex> Sovite.ProxyProtocol.tlv(:alpn, "h2")
{0x01, "h2"}
iex> Sovite.ProxyProtocol.tlv(:ssl, %{client: [:ssl], version: "TLSv1.3"})
{0x20, <<1, 0, 0, 0, 0, 0x21, 0, 7, "TLSv1.3">>}