From 4c39454564edea61eb2df03cd3aa754113e652dd Mon Sep 17 00:00:00 2001 From: Robert Edmonds <edmondsfastly@users.noreply.github.com> Date: Thu, 26 Aug 2021 16:47:09 -0400 Subject: [PATCH] Support haproxy PROXY v2 protocol on incoming UDP packets This commit adds minimal support for the haproxy PROXY v2 protocol which is described at https://www.haproxy.org/download/2.5/doc/proxy-protocol.txt. Only the UDP-over-IPv4 and UDP-over-IPv6 PROXY v2 family/transports are supported, and only the original source address/port of the proxied client are recovered from the PROXY v2 payload. Only the PROXY command is supported. There is a hardcoded ACL check to verify that the query was sent from 127.0.0.0/8 before PROXY v2 decapsulation is attempted. This prevents spoofing of the PROXY v2 header and avoids exposing the PROXY v2 parsing code to the Internet. This should probably be converted to a real ACL check that can be configured. If a proxied client address/port was successfully extracted from the PROXY v2 payload, the 'remote' field in the knotd_qdata_params_t structure will be updated to represent the address of the real (proxied) client. This way query modules (e.g. whoami) don't need to be updated to continue to produce correct source address dependent behavior. The address of the proxy that actually sent the proxied packet will be saved in a new 'proxy' field in knotd_qdata_params_t in case this value needs to be processed. The 'sdig' utility that comes with PowerDNS supports generating queries with a PROXY v2 header, which is in the 'pdns-tools' package on Debian/Ubuntu systems. Example command-line invocations: * sdig 127.0.0.1 53053 example.net a proxy 0 192.0.2.1:49153 198.51.100.1:53 * sdig 127.0.0.1 53053 example.net a proxy 0 '[2001:db8::1]:49153' '[2001:db8::100:1]:53' --- src/knot/Makefile.inc | 2 + src/knot/include/module.h | 1 + src/knot/query/proxyv2.c | 267 ++++++++++++++++++++++++++++++++++ src/knot/query/proxyv2.h | 16 ++ src/knot/server/udp-handler.c | 16 +- 5 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 src/knot/query/proxyv2.c create mode 100644 src/knot/query/proxyv2.h diff --git a/src/knot/Makefile.inc b/src/knot/Makefile.inc index cd2945f9d8..f86d623710 100644 --- a/src/knot/Makefile.inc +++ b/src/knot/Makefile.inc @@ -115,6 +115,8 @@ libknotd_la_SOURCES = \ knot/query/capture.c \ knot/query/capture.h \ knot/query/layer.h \ + knot/query/proxyv2.c \ + knot/query/proxyv2.h \ knot/query/query.c \ knot/query/query.h \ knot/query/requestor.c \ diff --git a/src/knot/include/module.h b/src/knot/include/module.h index 423fa7d283..54fd4dab62 100644 --- a/src/knot/include/module.h +++ b/src/knot/include/module.h @@ -400,6 +400,7 @@ typedef enum { typedef struct { knotd_query_flag_t flags; /*!< Current query flags. */ const struct sockaddr_storage *remote; /*!< Current remote address. */ + const struct sockaddr_storage *proxy; /*!< Current proxy address. */ int socket; /*!< Current network socket. */ unsigned thread_id; /*!< Current thread id. */ void *server; /*!< Server object private item. */ diff --git a/src/knot/query/proxyv2.c b/src/knot/query/proxyv2.c new file mode 100644 index 0000000000..7c50df2c94 --- /dev/null +++ b/src/knot/query/proxyv2.c @@ -0,0 +1,267 @@ +/* Copyright (C) 2021 Fastly, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "knot/query/proxyv2.h" + +#include <arpa/inet.h> +#include <stdint.h> + +/* + * Minimal implementation of the haproxy PROXY v2 protocol. + * + * Supports extracting the original client address and client port number from + * the haproxy PROXY v2 protocol's address block. + * + * See https://www.haproxy.org/download/2.5/doc/proxy-protocol.txt for the + * protocol specification. + */ + +static const char PROXYV2_SIG[12] = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; + +/* + * The part of the PROXY v2 payload following the signature. + */ +struct proxyv2_hdr { + /* + * The protocol version and command. + * + * The upper four bits contain the version which must be \x2 and the + * receiver must only accept this value. + * + * The lower four bits represent the command, which is \x0 for LOCAL + * and \x1 for PROXY. + */ + uint8_t ver_cmd; + + /* + * The transport protocol and address family. The upper four bits + * contain the address family and the lower four bits contain the + * protocol. + * + * The relevant values for DNS are: + * \x11: TCP over IPv4 + * \x12: UDP over IPv4 + * \x21: TCP over IPv6 + * \x22: UDP over IPv6 + */ + uint8_t fam_addr; + + /* + * The number of PROXY v2 payload bytes following this header to skip + * to reach the proxied packet (i.e., start of the original DNS mesage). + */ + uint16_t len; +}; + +/* + * The PROXY v2 address block for IPv4. + */ +struct proxyv2_addr_ipv4 { + uint8_t src_addr[4]; + uint8_t dst_addr[4]; + uint16_t src_port; + uint16_t dst_port; +}; + +/* + * The PROXY v2 address block for IPv6. + */ +struct proxyv2_addr_ipv6 { + uint8_t src_addr[16]; + uint8_t dst_addr[16]; + uint16_t src_port; + uint16_t dst_port; +}; + +/* + * Make sure the C compiler lays out the PROXY v2 address block structs so that + * they can be memcpy()'d off the wire. + */ +#if (__STDC_VERSION__ >= 201112L) +_Static_assert(sizeof(struct proxyv2_hdr) == 4, + "struct proxyv2_hdr is correct size"); +_Static_assert(sizeof(struct proxyv2_addr_ipv4) == 12, + "struct proxyv2_addr_ipv4 is correct size"); +_Static_assert(sizeof(struct proxyv2_addr_ipv6) == 36, + "struct proxyv2_addr_ipv6 is correct size"); +#endif + +#define S_ADDR_IS_LOOPBACK(a) ((((long int) (a)) & 0xff000000) == 0x7f000000) + +int proxyv2_decapsulate(void *base, + size_t len_base, + knot_pkt_t **query, + knotd_qdata_params_t *params, + struct sockaddr_storage *client, + knot_mm_t *mm) +{ + /* + * Check if the query was sent from an IP address authorized to send + * proxied DNS traffic. This is a hardcoded ACL check for queries + * originated from 127.0.0.0/8. + * + * XXX: This should be a real ACL check. + */ + int ret = KNOT_EDENIED; + const struct sockaddr_storage *sock = params->remote; + if (sock != NULL && sock->ss_family == AF_INET) { + const struct sockaddr_in *sock4 = (const struct sockaddr_in *) sock; + if (S_ADDR_IS_LOOPBACK(ntohl(sock4->sin_addr.s_addr))) { + ret = KNOT_EOK; + } + } + if (ret != KNOT_EOK) { + /* Failure. */ + return ret; + } + + /* + * Check that 'base' has enough bytes to read the PROXY v2 signature + * and header, and if so whether the PROXY v2 signature is present. + */ + if (len_base < (sizeof(PROXYV2_SIG) + sizeof(struct proxyv2_hdr)) || + memcmp(base, PROXYV2_SIG, sizeof(PROXYV2_SIG)) != 0) + { + /* Failure. */ + return KNOT_EMALF; + } + + /* Read the PROXY v2 header. */ + struct proxyv2_hdr hdr; + memcpy(&hdr, base + sizeof(PROXYV2_SIG), sizeof(hdr)); + + /* + * Check that this is a version 2, command "PROXY" payload. + * + * XXX: The PROXY v2 spec mandates support for the "LOCAL" command + * (byte 0x20). + */ + if (hdr.ver_cmd != 0x21) { + /* Failure. */ + return KNOT_EMALF; + } + + /* + * Calculate the offset of the original DNS message inside the packet. + * This needs to account for the length of the PROXY v2 signature, + * PROXY v2 header, and the bytes of variable length PROXY v2 data + * following the PROXY v2 header. + */ + const size_t offset_dns = + sizeof(PROXYV2_SIG) + + sizeof(struct proxyv2_hdr) + + ntohs(hdr.len); + + /* + * Check if the calculated offset of the original DNS message is + * actually inside the packet received on the wire, and if so, parse + * the real DNS query message. + */ + if (offset_dns < len_base) { + /* Free the old, misparsed query message object. */ + knot_pkt_free(*query); + + /* + * Re-parse the query message using the data in the + * packet following the PROXY v2 payload. + */ + *query = knot_pkt_new(base + offset_dns, + len_base - offset_dns, + mm); + ret = knot_pkt_parse(*query, 0); + if (ret != KNOT_EOK) { + /* Failure. */ + return ret; + } + } + + /* + * Calculate the offset of the PROXY v2 address block. This is the data + * immediately following the PROXY v2 header. + */ + const size_t offset_proxy_addr = + sizeof(PROXYV2_SIG) + sizeof(struct proxyv2_hdr); + + /* + * Handle proxied UDP-over-IPv4 and UDP-over-IPv6 packets. + * + * XXX: What about TCP? + */ + if (hdr.fam_addr == 0x12) { + /* This is a proxied UDP-over-IPv4 packet. */ + struct proxyv2_addr_ipv4 addr; + + /* + * Check that the packet is large enough to contain the IPv4 + * address block. + */ + if (offset_proxy_addr + sizeof(addr) < len_base) { + /* Read the PROXY v2 address block. */ + memcpy(&addr, base + offset_proxy_addr, sizeof(addr)); + + /* Copy the client's IPv4 address to the caller. */ + sockaddr_set_raw(client, + AF_INET, + &addr.src_addr[0], + sizeof(addr.src_addr)); + + /* Copy the client's port to the caller. */ + sockaddr_port_set(client, ntohs(addr.src_port)); + + /* Save the address of the proxy. */ + params->proxy = params->remote; + + /* Expose the address of the proxied client. */ + params->remote = client; + + /* Success. */ + return KNOT_EOK; + } + } else if (hdr.fam_addr == 0x22) { + /* This is a proxied UDP-over-IPv6 packet. */ + struct proxyv2_addr_ipv6 addr; + + /* + * Check that the packet is large enough to contain the IPv6 + * address block. + */ + if (offset_proxy_addr + sizeof(addr) < len_base) { + /* Read the PROXY v2 address block. */ + memcpy(&addr, base + offset_proxy_addr, sizeof(addr)); + + /* Copy the client's IPv6 address to the caller. */ + sockaddr_set_raw(client, + AF_INET6, + &addr.src_addr[0], + sizeof(addr.src_addr)); + + /* Copy the client's port to the caller. */ + sockaddr_port_set(client, ntohs(addr.src_port)); + + /* Save the address of the proxy. */ + params->proxy = params->remote; + + /* Expose the address of the proxied client. */ + params->remote = client; + + /* Success. */ + return KNOT_EOK; + } + } + + /* Failure. */ + return KNOT_EMALF; +} diff --git a/src/knot/query/proxyv2.h b/src/knot/query/proxyv2.h new file mode 100644 index 0000000000..62a5597a92 --- /dev/null +++ b/src/knot/query/proxyv2.h @@ -0,0 +1,16 @@ +#pragma once + +#include <sys/socket.h> +#include <stddef.h> + +#include "libknot/mm_ctx.h" +#include "libknot/packet/pkt.h" +#include "knot/include/module.h" +#include "contrib/sockaddr.h" + +int proxyv2_decapsulate(void *base, + size_t len_base, + knot_pkt_t **query, + knotd_qdata_params_t *params, + struct sockaddr_storage *client, + knot_mm_t *mm); diff --git a/src/knot/server/udp-handler.c b/src/knot/server/udp-handler.c index 60b771c2eb..933064004c 100644 --- a/src/knot/server/udp-handler.c +++ b/src/knot/server/udp-handler.c @@ -37,6 +37,7 @@ #include "knot/common/fdset.h" #include "knot/nameserver/process_query.h" #include "knot/query/layer.h" +#include "knot/query/proxyv2.h" #include "knot/server/server.h" #include "knot/server/udp-handler.h" #include "knot/server/xdp-handler.h" @@ -73,6 +74,7 @@ static void udp_handle(udp_context_t *udp, int fd, struct sockaddr_storage *ss, .xdp_msg = xdp_msg, .thread_id = udp->thread_id }; + struct sockaddr_storage proxied_remote; /* Start query processing. */ knot_layer_begin(&udp->layer, ¶ms); @@ -84,7 +86,19 @@ static void udp_handle(udp_context_t *udp, int fd, struct sockaddr_storage *ss, /* Input packet. */ int ret = knot_pkt_parse(query, 0); if (ret != KNOT_EOK && query->parsed > 0) { // parsing failed (e.g. 2x OPT) - query->parsed--; // artificially decreasing "parsed" leads to FORMERR + /* + * DNS parsing failed, try re-parsing with a PROXY v2 header. + * XXX: This behavior should probably be controlled by a config + * option. + */ + ret = proxyv2_decapsulate(rx->iov_base, rx->iov_len, + &query, ¶ms, &proxied_remote, + udp->layer.mm); + + if (ret != KNOT_EOK && query->parsed > 0) { + // artificially decreasing "parsed" leads to FORMERR + query->parsed--; + } } knot_layer_consume(&udp->layer, query); -- GitLab