Skip to content
Snippets Groups Projects
Commit 4c394545 authored by Robert Edmonds's avatar Robert Edmonds Committed by Daniel Salzman
Browse files

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'
parent 4fd0e18d
Branches
Tags
1 merge request!1468Support haproxy PROXY v2 protocol on incoming UDP packets
......@@ -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 \
......
......@@ -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. */
......
/* 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;
}
#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);
......@@ -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, &params);
......@@ -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, &params, &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);
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment