blob: 51e67270d22157ca6bbe9fe98b42d506c74e781e [file]
/** @file
*
* PROXY protocol definitions and parsers.
*
* @section license License
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "iocore/net/ProxyProtocol.h"
#include "tscore/Diags.h"
#include "tscore/ink_assert.h"
#include "tscore/ink_string.h"
#include "tscore/ink_inet.h"
#include "swoc/TextView.h"
#include "swoc/bwf_base.h"
#include "tsutil/DbgCtl.h"
#include <limits>
#include <optional>
#include <string_view>
namespace
{
using namespace std::literals;
constexpr swoc::TextView PPv1_CONNECTION_PREFACE = "PROXY"sv;
constexpr swoc::TextView PPv2_CONNECTION_PREFACE = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"sv;
constexpr size_t PPv1_CONNECTION_HEADER_LEN_MIN = 15;
constexpr swoc::TextView PPv1_PROTO_UNKNOWN = "UNKNOWN"sv;
constexpr swoc::TextView PPv1_PROTO_TCP4 = "TCP4"sv;
constexpr swoc::TextView PPv1_PROTO_TCP6 = "TCP6"sv;
constexpr std::string_view PPv1_DELIMITER = " "sv;
constexpr uint8_t PPv2_CMD_LOCAL = 0x20;
constexpr uint8_t PPv2_CMD_PROXY = 0x21;
constexpr uint8_t PPv2_PROTO_UNSPEC = 0x00;
constexpr uint8_t PPv2_PROTO_TCP4 = 0x11;
constexpr uint8_t PPv2_PROTO_UDP4 = 0x12;
constexpr uint8_t PPv2_PROTO_TCP6 = 0x21;
constexpr uint8_t PPv2_PROTO_UDP6 = 0x22;
constexpr uint8_t PPv2_PROTO_UNIX_STREAM = 0x31;
constexpr uint8_t PPv2_PROTO_UNIX_DATAGRAM = 0x32;
constexpr uint16_t PPv2_ADDR_LEN_INET = 4 + 4 + 2 + 2;
constexpr uint16_t PPv2_ADDR_LEN_INET6 = 16 + 16 + 2 + 2;
constexpr uint16_t PPv2_ADDR_LEN_UNIX = 108 + 108;
const swoc::bwf::Spec ADDR_ONLY_FMT{"::a"};
std::optional<in_port_t>
proxy_protocol_v1_parse_port(swoc::TextView token)
{
swoc::TextView parsed;
auto port = swoc::svtoi(token, &parsed, 10);
if (parsed != token || port <= 0 || port > std::numeric_limits<in_port_t>::max()) {
return std::nullopt;
}
return static_cast<in_port_t>(port);
}
DbgCtl dbg_ctl_proxyprotocol_v1{"proxyprotocol_v1"};
DbgCtl dbg_ctl_proxyprotocol_v2{"proxyprotocol_v2"};
DbgCtl dbg_ctl_proxyprotocol{"proxyprotocol"};
struct PPv2Hdr {
uint8_t sig[12]; ///< preface
uint8_t ver_cmd; ///< protocol version and command
uint8_t fam; ///< protocol family and transport
uint16_t len; ///< number of following bytes part of the header
union {
// for TCP/UDP over IPv4, len = 12 (PPv2_ADDR_LEN_INET)
struct {
uint32_t src_addr;
uint32_t dst_addr;
uint16_t src_port;
uint16_t dst_port;
} ip4;
// for TCP/UDP over IPv6, len = 36 (PPv2_ADDR_LEN_INET6)
struct {
uint8_t src_addr[16];
uint8_t dst_addr[16];
uint16_t src_port;
uint16_t dst_port;
} ip6;
// for AF_UNIX sockets, len = 216 (PPv2_ADDR_LEN_UNIX)
struct {
uint8_t src_addr[108];
uint8_t dst_addr[108];
} unix;
} addr;
};
/**
PROXY Protocol v1 Parser
@return read length
*/
size_t
proxy_protocol_v1_parse(ProxyProtocol *pp_info, swoc::TextView hdr)
{
ink_release_assert(hdr.size() >= PPv1_CONNECTION_HEADER_LEN_MIN);
// Find the terminating newline
swoc::TextView::size_type pos = hdr.find('\n');
if (pos == hdr.npos) {
Dbg(dbg_ctl_proxyprotocol_v1, "ssl_has_proxy_v1: LF not found");
return 0;
}
if (hdr[pos - 1] != '\r') {
Dbg(dbg_ctl_proxyprotocol_v1, "ssl_has_proxy_v1: CR not found");
return 0;
}
swoc::TextView token;
// All the cases are special and sequence, might as well unroll them.
// The header should begin with the PROXY preface
token = hdr.split_prefix_at(' ');
if (0 == token.size() || token != PPv1_CONNECTION_PREFACE) {
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: header [%.*s] does not start with preface [%.*s]",
static_cast<int>(hdr.size()), hdr.data(), static_cast<int>(PPv1_CONNECTION_PREFACE.size()), PPv1_CONNECTION_PREFACE.data());
return 0;
}
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: [%.*s] = PREFACE", static_cast<int>(token.size()), token.data());
// The INET protocol family - TCP4, TCP6 or UNKNOWN
if (hdr.starts_with(PPv1_PROTO_UNKNOWN)) {
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: [UNKNOWN] = INET Family");
// Ignore anything presented before the CRLF
pp_info->version = ProxyProtocolVersion::V1;
return pos + 1;
} else if (hdr.starts_with(PPv1_PROTO_TCP4)) {
token = hdr.split_prefix_at(' ');
if (0 == token.size()) {
return 0;
}
pp_info->ip_family = AF_INET;
pp_info->type = SOCK_STREAM;
} else if (hdr.starts_with(PPv1_PROTO_TCP6)) {
token = hdr.split_prefix_at(' ');
if (0 == token.size()) {
return 0;
}
pp_info->ip_family = AF_INET6;
pp_info->type = SOCK_STREAM;
} else {
return 0;
}
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: [%.*s] = INET Family", static_cast<int>(token.size()), token.data());
// Next up is the layer 3 source address
// - 255.255.255.255 or ffff:f...f:ffff ffff:f...f:fff
token = hdr.split_prefix_at(' ');
if (0 == token.size()) {
return 0;
}
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: [%.*s] = Source Address", static_cast<int>(token.size()), token.data());
if (0 != ats_ip_pton(token, &pp_info->src_addr)) {
return 0;
}
// Next is the layer3 destination address
// - 255.255.255.255 or ffff:f...f:ffff ffff:f...f:fff
token = hdr.split_prefix_at(' ');
if (0 == token.size()) {
return 0;
}
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: [%.*s] = Destination Address", static_cast<int>(token.size()), token.data());
if (0 != ats_ip_pton(token, &pp_info->dst_addr)) {
return 0;
}
// Next is the TCP source port represented as a decimal number in the range of [1..65535] inclusive.
token = hdr.split_prefix_at(' ');
if (0 == token.size()) {
return 0;
}
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: [%.*s] = Source Port", static_cast<int>(token.size()), token.data());
auto src_port = proxy_protocol_v1_parse_port(token);
if (!src_port) {
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: src port token [%.*s] failed to parse", static_cast<int>(token.size()),
token.data());
return 0;
}
pp_info->src_addr.network_order_port() = htons(*src_port);
// Next is the TCP destination port represented as a decimal number in the range of [1..65535] inclusive.
// Final trailer is CR LF so split at CR.
token = hdr.split_prefix_at('\r');
if (0 == token.size() || token.find(0x20) != token.npos) {
return 0;
}
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: [%.*s] = Destination Port", static_cast<int>(token.size()), token.data());
auto dst_port = proxy_protocol_v1_parse_port(token);
if (!dst_port) {
Dbg(dbg_ctl_proxyprotocol_v1, "proxy_protov1_parse: dst port token [%.*s] failed to parse", static_cast<int>(token.size()),
token.data());
return 0;
}
pp_info->dst_addr.network_order_port() = htons(*dst_port);
pp_info->version = ProxyProtocolVersion::V1;
return pos + 1;
}
/**
PROXY Protocol v2 Parser
@return read length
*/
size_t
proxy_protocol_v2_parse(ProxyProtocol *pp_info, const swoc::TextView &msg)
{
ink_release_assert(msg.size() >= PPv2_CONNECTION_HEADER_LEN);
const PPv2Hdr *hdr_v2 = reinterpret_cast<const PPv2Hdr *>(msg.data());
// Assuming PREFACE check is done
// length check
const uint16_t len = ntohs(hdr_v2->len);
const size_t total_len = PPv2_CONNECTION_HEADER_LEN + len;
uint16_t tlv_len = 0;
if (msg.size() < total_len) {
Error("The size of PP header received (%zu) is smaller than the expected size (%zu)", msg.size(), total_len);
return 0;
}
// protocol version and command
switch (hdr_v2->ver_cmd) {
case PPv2_CMD_LOCAL: {
// protocol byte should be UNSPEC (\x00) with LOCAL command
if (hdr_v2->fam != PPv2_PROTO_UNSPEC) {
Dbg(dbg_ctl_proxyprotocol_v2, "UNSPEC is unexpected");
return 0;
}
pp_info->version = ProxyProtocolVersion::V2;
pp_info->ip_family = AF_UNSPEC;
return total_len;
}
case PPv2_CMD_PROXY: {
switch (hdr_v2->fam) {
case PPv2_PROTO_TCP4:
case PPv2_PROTO_UDP4:
if (len < PPv2_ADDR_LEN_INET) {
Dbg(dbg_ctl_proxyprotocol_v2, "There is not enough data left for IPv4 info");
return 0;
}
tlv_len = len - PPv2_ADDR_LEN_INET;
pp_info->set_ipv4_addrs(reinterpret_cast<in_addr_t>(hdr_v2->addr.ip4.src_addr), hdr_v2->addr.ip4.src_port,
reinterpret_cast<in_addr_t>(hdr_v2->addr.ip4.dst_addr), hdr_v2->addr.ip4.dst_port);
pp_info->type = hdr_v2->fam == PPv2_PROTO_TCP4 ? SOCK_STREAM : SOCK_DGRAM;
pp_info->version = ProxyProtocolVersion::V2;
break;
case PPv2_PROTO_TCP6:
case PPv2_PROTO_UDP6:
if (len < PPv2_ADDR_LEN_INET6) {
Dbg(dbg_ctl_proxyprotocol_v2, "There is not enough data left for IPv6 info");
return 0;
}
tlv_len = len - PPv2_ADDR_LEN_INET6;
pp_info->set_ipv6_addrs(reinterpret_cast<in6_addr const &>(hdr_v2->addr.ip6.src_addr), hdr_v2->addr.ip6.src_port,
reinterpret_cast<in6_addr const &>(hdr_v2->addr.ip6.dst_addr), hdr_v2->addr.ip6.dst_port);
pp_info->type = hdr_v2->fam == PPv2_PROTO_TCP6 ? SOCK_STREAM : SOCK_DGRAM;
pp_info->version = ProxyProtocolVersion::V2;
break;
case PPv2_PROTO_UNIX_STREAM:
[[fallthrough]];
case PPv2_PROTO_UNIX_DATAGRAM:
[[fallthrough]];
case PPv2_PROTO_UNSPEC:
[[fallthrough]];
default:
// unsupported
Dbg(dbg_ctl_proxyprotocol_v2, "Unsupported protocol family (%d)", hdr_v2->fam);
return 0;
}
if (tlv_len > 0) {
if (pp_info->set_additional_data(msg.substr(total_len - tlv_len, tlv_len)) < 0) {
Dbg(dbg_ctl_proxyprotocol_v2, "Failed to parse additional fields");
return 0;
}
}
return total_len;
}
default:
Dbg(dbg_ctl_proxyprotocol_v2, "Unsupported command (%d)", hdr_v2->ver_cmd);
break;
}
return 0;
}
/**
Build PROXY Protocol v1
*/
size_t
proxy_protocol_v1_build(uint8_t *buf, size_t max_buf_len, const ProxyProtocol &pp_info)
{
if (max_buf_len < PPv1_CONNECTION_HEADER_LEN_MAX) {
return 0;
}
swoc::FixedBufferWriter bw{reinterpret_cast<char *>(buf), max_buf_len};
// preface
bw.write(PPv1_CONNECTION_PREFACE);
bw.write(PPv1_DELIMITER);
// the proxied INET protocol and family
if (pp_info.src_addr.isIp4()) {
bw.write(PPv1_PROTO_TCP4);
} else if (pp_info.src_addr.isIp6()) {
bw.write(PPv1_PROTO_TCP6);
} else {
bw.write(PPv1_PROTO_UNKNOWN);
}
bw.write(PPv1_DELIMITER);
// the layer 3 source address
bwformat(bw, ADDR_ONLY_FMT, pp_info.src_addr);
bw.write(PPv1_DELIMITER);
// the layer 3 destination address
bwformat(bw, ADDR_ONLY_FMT, pp_info.dst_addr);
bw.write(PPv1_DELIMITER);
// TCP source port
{
size_t len = ink_small_itoa(ats_ip_port_host_order(pp_info.src_addr), bw.aux_data(), bw.remaining());
bw.commit(len);
bw.write(PPv1_DELIMITER);
}
// TCP destination port
{
size_t len = ink_small_itoa(ats_ip_port_host_order(pp_info.dst_addr), bw.aux_data(), bw.remaining());
bw.commit(len);
}
Dbg(dbg_ctl_proxyprotocol_v1, "Proxy Protocol v1: %.*s", static_cast<int>(bw.size()), bw.data());
bw.write("\r\n");
return bw.size();
}
/**
Build PROXY Protocol v2
UDP, Unix Domain Socket, and TLV fields are not supported yet
*/
size_t
proxy_protocol_v2_build(uint8_t *buf, size_t max_buf_len, const ProxyProtocol &pp_info)
{
if (max_buf_len < PPv2_CONNECTION_HEADER_LEN) {
return 0;
}
swoc::FixedBufferWriter bw{reinterpret_cast<char *>(buf), max_buf_len};
// # proxy_hdr_v2
// ## preface
bw.write(PPv2_CONNECTION_PREFACE);
// ## version and command
// TODO: support PPv2_CMD_LOCAL for health check
bw.write(static_cast<char>(PPv2_CMD_PROXY));
// ## family & address
// TODO: support UDP
switch (pp_info.src_addr.family()) {
case AF_INET:
bw.write(static_cast<char>(PPv2_PROTO_TCP4));
break;
case AF_INET6:
bw.write(static_cast<char>(PPv2_PROTO_TCP6));
break;
case AF_UNIX:
bw.write(static_cast<char>(PPv2_PROTO_UNIX_STREAM));
break;
default:
bw.write(static_cast<char>(PPv2_PROTO_UNSPEC));
break;
}
// ## len field. this will be set at the end of this function
const size_t len_field_offset = bw.size();
bw.commit(2);
ink_release_assert(bw.size() == PPv2_CONNECTION_HEADER_LEN);
// # proxy_addr
// TODO: support UDP
switch (pp_info.src_addr.family()) {
case AF_INET: {
bw.write(&ats_ip4_addr_cast(pp_info.src_addr), TS_IP4_SIZE);
bw.write(&ats_ip4_addr_cast(pp_info.dst_addr), TS_IP4_SIZE);
bw.write(&ats_ip_port_cast(pp_info.src_addr), TS_PORT_SIZE);
bw.write(&ats_ip_port_cast(pp_info.dst_addr), TS_PORT_SIZE);
break;
}
case AF_INET6: {
bw.write(&ats_ip6_addr_cast(pp_info.src_addr), TS_IP6_SIZE);
bw.write(&ats_ip6_addr_cast(pp_info.dst_addr), TS_IP6_SIZE);
bw.write(&ats_ip_port_cast(pp_info.src_addr), TS_PORT_SIZE);
bw.write(&ats_ip_port_cast(pp_info.dst_addr), TS_PORT_SIZE);
break;
}
case AF_UNIX: {
// unsupported yet
bw.commit(PPv2_ADDR_LEN_UNIX);
break;
}
default:
// do nothing
break;
}
// # Additional TLVs (pp2_tlv)
// unsupported yet
// Set len field (number of following bytes part of the header) in the hdr
uint16_t len = htons(bw.size() - PPv2_CONNECTION_HEADER_LEN);
memcpy(buf + len_field_offset, &len, sizeof(uint16_t));
Dbg(dbg_ctl_proxyprotocol_v2, "Proxy Protocol v2 of %zu bytes", bw.size());
return bw.size();
}
} // namespace
bool
proxy_protocol_detect(swoc::TextView tv)
{
if (tv.size() >= PPv1_CONNECTION_HEADER_LEN_MIN && tv.starts_with(PPv1_CONNECTION_PREFACE)) {
return true;
} else if (tv.size() >= PPv2_CONNECTION_HEADER_LEN && tv.starts_with(PPv2_CONNECTION_PREFACE)) {
return true;
} else {
return false;
}
}
/**
PROXY Protocol Parser
*/
size_t
proxy_protocol_parse(ProxyProtocol *pp_info, swoc::TextView tv)
{
size_t len = 0;
// Parse the TextView before moving the bytes in the buffer
if (tv.size() >= PPv1_CONNECTION_HEADER_LEN_MIN && tv.starts_with(PPv1_CONNECTION_PREFACE)) {
// Client must send at least 15 bytes to get a reasonable match.
len = proxy_protocol_v1_parse(pp_info, tv);
} else if (tv.size() >= PPv2_CONNECTION_HEADER_LEN && tv.starts_with(PPv2_CONNECTION_PREFACE)) {
len = proxy_protocol_v2_parse(pp_info, tv);
} else {
// if we don't have the PROXY preface, we don't have a ProxyProtocol header
// TODO: print hexdump of buffer safely
Dbg(dbg_ctl_proxyprotocol, "failed to find ProxyProtocol preface in the first %zu bytes", tv.size());
}
return len;
}
/**
PROXY Protocol Builder
*/
size_t
proxy_protocol_build(uint8_t *buf, size_t max_buf_len, const ProxyProtocol &pp_info, ProxyProtocolVersion force_version)
{
ProxyProtocolVersion version = pp_info.version;
if (force_version != ProxyProtocolVersion::UNDEFINED) {
version = force_version;
}
size_t len = 0;
if (version == ProxyProtocolVersion::V1) {
len = proxy_protocol_v1_build(buf, max_buf_len, pp_info);
} else if (version == ProxyProtocolVersion::V2) {
len = proxy_protocol_v2_build(buf, max_buf_len, pp_info);
} else {
ink_abort("PROXY Protocol Version is undefined");
}
return len;
}
ProxyProtocolVersion
proxy_protocol_version_cast(int i)
{
switch (i) {
case 1:
case 2:
return static_cast<ProxyProtocolVersion>(i);
default:
return ProxyProtocolVersion::UNDEFINED;
}
}
void
ProxyProtocol::set_ipv4_addrs(in_addr_t src_addr, uint16_t src_port, in_addr_t dst_addr, uint16_t dst_port)
{
IpAddr src(src_addr);
IpAddr dst(dst_addr);
this->src_addr.assign(src, src_port);
this->dst_addr.assign(dst, dst_port);
this->ip_family = AF_INET;
}
void
ProxyProtocol::set_ipv6_addrs(const in6_addr &src_addr, uint16_t src_port, const in6_addr &dst_addr, uint16_t dst_port)
{
IpAddr src(src_addr);
IpAddr dst(dst_addr);
this->src_addr.assign(src, src_port);
this->dst_addr.assign(dst, dst_port);
this->ip_family = AF_INET6;
}
std::optional<std::string_view>
ProxyProtocol::get_tlv(const uint8_t tlvCode) const
{
if (version == ProxyProtocolVersion::V2) {
if (auto v = tlv.find(tlvCode); v != tlv.end()) {
return v->second;
}
}
return std::nullopt;
}
/*
* PP2_TYPE_SSL
* struct pp2_tlv_ssl {
* uint8_t client;
* uint32_t verify;
* struct pp2_tlv sub_tlv[0];
* };
*/
std::optional<std::string_view>
ProxyProtocol::_get_tlv_ssl_subtype(uint8_t subtype) const
{
if (auto v = tlv.find(PP2_TYPE_SSL); v != tlv.end() && v->second.length() != 0) {
auto ssl = v->second;
// Is the client connected over TLS
if ((ssl.data()[0] & 0x01) == 0) {
// Not over TLS
return std::nullopt;
}
if (ssl.length() < 5) {
return std::nullopt;
}
// Find the given subtype
uint16_t len = ssl.length();
const char *p = ssl.data() + 5; // Skip client (uint8_t) + verify (uint32_t)
const char *end = ssl.data() + len;
while (p != end) {
if (end - p < 3) {
// The size of a sub TLV entry must be 3 bytes or more
Dbg(dbg_ctl_proxyprotocol_v2, "Remaining data (%ld bytes) is not enough for a sub TLV field", end - p);
return std::nullopt;
}
// Type
uint8_t type = *p;
p += 1;
// Length
uint16_t length = ntohs(*reinterpret_cast<const uint16_t *>(p));
p += 2;
// Value
if (end - p < length) {
// Does not have enough data
Dbg(dbg_ctl_proxyprotocol_v2, "Remaining data (%ld bytes) is not enough for a TLV field (ID:%u LEN:%hu)", end - p, type,
length);
return std::nullopt;
}
// Found it?
if (type == subtype) {
Dbg(dbg_ctl_proxyprotocol, "TLV: ID=%u LEN=%hu", type, length);
return std::string_view(p, length);
}
p += length;
}
}
return std::nullopt;
}
std::optional<std::string_view>
ProxyProtocol::get_tlv_ssl_version() const
{
// The specification only says "the US-ASCII string representation of the TLS version".
// HAProxy sends a string returned by SSL_get_version.
return this->_get_tlv_ssl_subtype(PP2_SUBTYPE_SSL_VERSION);
}
std::optional<std::string_view>
ProxyProtocol::get_tlv_ssl_cipher() const
{
return this->_get_tlv_ssl_subtype(PP2_SUBTYPE_SSL_CIPHER);
}
std::optional<std::string_view>
ProxyProtocol::get_tlv_ssl_group() const
{
return this->_get_tlv_ssl_subtype(PP2_SUBTYPE_SSL_GROUP);
}
int
ProxyProtocol::set_additional_data(std::string_view data)
{
uint16_t len = data.length();
Dbg(dbg_ctl_proxyprotocol_v2, "Parsing %d byte additional data", len);
additional_data.assign(data);
const char *p = additional_data.data();
const char *end = p + len;
while (p != end) {
if (end - p < 3) {
// The size of a TLV entry must be 3 bytes or more
Dbg(dbg_ctl_proxyprotocol_v2, "Remaining data (%ld bytes) is not enough for a TLV field", end - p);
return -2;
}
// Type
uint8_t type = *p;
p += 1;
// Length
uint16_t length = ntohs(*reinterpret_cast<const uint16_t *>(p));
p += 2;
// Value
if (end - p < length) {
// Does not have enough data
Dbg(dbg_ctl_proxyprotocol_v2, "Remaining data (%ld bytes) is not enough for a TLV field (ID:%u LEN:%hu)", end - p, type,
length);
return -3;
}
Dbg(dbg_ctl_proxyprotocol, "TLV: ID=%u LEN=%hu", type, length);
tlv.emplace(type, std::string_view(p, length));
p += length;
}
return 0;
}