Skip to content

Make it possible to accept PROXYv2 in BIND (and send it in dig)

This merge request contains the changes necessary to make it possible for BIND to accept the PROXYv2 protocol as well as make dig send them. You may want to take a look at the overall design description and principles in the issue #4388 (closed) and the PROXYv2 specification first.

It should be noted that the merge request is a work-in-progress one and, thus, is only of interest to the ones curious about PROXYv2 support in BIND and its complimentary tools. In particular, at the time of writing, proper integration into BIND is missing, and system tests are missing, but support in the dig is complete. However, all of the lower-level parts are ready and have been sufficiently covered with unit tests.

To give you some guidance, the central part of PROXYv2 is proxy2.c and proxy2.h files that contain the set of APIs for parsing incoming PROXYv2 headers as well as generating them - that is, these files contain a library to deal with PROXYv2 protocol, and it is complete. It follows the spec very closely (especially the incoming handler) and is very strict regarding what data to accept and build to detect ill-formed headers. While it meticulously verifies all data types currently defined in the spec, it will remain forward-compatible with future revisions of the PROXYv2 protocol.

As it is, in general, much easier to generate data than to parse, the API that is meant to deal with that (isc_proxy2_make_header() and isc_proxy2_header_append*() functions) is pretty straightforward.

The part responsible for handling the input headers (the isc_proxy2_handler_t object) in BIND is more interesting. It is structured as a state machine into which you push an arbitrary amount of received data, and it tries to assemble the header while doing iterative processing and verification of the data. When the header is completely assembled, a user-provided callback is called with all of the data extracted from the PROXYv2 header passed as arguments. It is also called in the case of an error (with a certain error code depending on the situation) or when more data is needed. It was designed with the following things in mind:

  1. It should be tolerant of the way the data is sent. It should be equally tolerable to the "torn-apart" headers as well as to the cases when the OS returns as a piece of data containing both the header and the piece of the data (e.g. a part or a complete DNS message). In fact, we have little control over how the data is being passed to us by the networking stack (especially in the case of stream protocols) or by the underlying layers (like TLS).
  2. It should be able to handle data being sent over both stream and datagram-based transports.
  3. It should be able to detect that the data transferred does not contain PROXYv2 data as soon as reasonably possible (port-scanners case).
  4. It should be able to detect ill-formed PROXYv2 headers (e.g. hand-crafted ones specifically made for the reasons of triggering security issues in PROXYv2 implementations). In particular, it should not wait for a whole PROXYv2 header to arrive to detect common errors in the already received parts of the header.
  5. It should be strict to the data and follow the TLVs structure defined in the spec closely. That would be particularly important when/if we implement PROXYv2 forwarding: we want to be good citizens and not send ill-formed data to the upstream servers (that might be less tolerable to ill-formed data than we are). At the same time, we should be reasonably tolerable to the non-defined data types for the sake of forward compatibility (while high-level structure verification is still possible and desirable) - in general, it is possible to add arbitrary data to the TLVs section of the PROXYv2 headers (and cloud service providers do so).

The next significant part of the merge request is the new transports that are meant to encapsulate PROXYv2 processing logic in themselves and act as either TCP or UDP to the higher-level code: PROXY Stream (proxystream.c) and PROXY over UDP (proxyudp.c). They reuse the existing unit test logic to ensure compatibility with TCP and UDP code. While these are the most complicated code to get right (as it is asynchronous code), there is nothing special about them: the transports are structured and work in a similar manner to other multilayer transports we have (like TLS Stream, Stream DNS, DNS over HTTP). They are based on the same PROXYv2 handling code and work in a similar manner - in two stages:

  1. In the first stage, they take care of the PROXYv2 headers.
  2. In the second stage, they work as mere wrappers on top of the underlying transports.

PROXY Stream is the foundation for the PROXYv2 support in all TCP-based transports (TLS Stream, Stream DNS (TCP DNS and TLS DNS), DNS-over-HTTP(S)). PROXY over UDP, being somewhat an exception (as a first multilayer datagram-based transport), is used directly.

With all low-level parts (mostly) ready, integrating them into the codebase should be easy (with integration into dig serving as an example).

This merge request, being in line with other new DNS transports, DOES NOT deal with making BIND forward PROXYv2 queries or making it able to connect to remote servers expecting PROXYv2 headers: that should be a part of a greater effort of making BIND treat transports, other than UDP and TCP, as the first class citizens (e.g. see here #2992 ). For now, we should have enough PROXYv2 support to make BIND usable as a backend behind PROXYv2-compatible proxies.

Example Configurations

Here you can find some configs that I used for interoperability testing (you may want to adapt them for your case - not guaranteed to work right away).

dnsdist

See here for more details.

-- listen om 53001 (UDP and TCP) for regular DNS queries
setLocal("0.0.0.0:53001")

-- Connect to 127.0.0.1:53000 via plain Do53 as a back-end with PROXYv2 enabled (both for UDP and TCP, use "tcpOnly=true" to force using TCP)
-- On the BIND's side 'proxy plain' must be used
newServer({address="127.0.0.1:53000", useProxyProtocol=true})
-- Connect to 127.0.0.1:44344 via TLS as a back-end with PROXYv2 enabled (both UDP and TCP, "tcpOnly=true" is implied)
-- On the BIND's side 'proxy encrypted' must be used
newServer({address="127.0.0.1:44344", useProxyProtocol=true, checkTCP=true, tls="openssl", validateCertificates=false})

-- one can make dnsdist PROXYv2 headers by setting the dedicated ACL
-- setProxyProtocolACL({"127.0.0.1/32"})

HAProxy

Please keep in mind that on the BIND's side only proxy plain must be used for interoperability with HAProxy.


## TCP-only mode
# (HAProxy accepts TCP connection and then passes the connection information to the DNS-server acting like a back-end)
frontend dns-front
  mode tcp
  #bind :53002 accept-proxy
  bind :53002
  default_backend dns-servers-plain

backend dns-servers-plain
  mode tcp
  server s1 127.0.0.1:53000 check send-proxy-v2

## Same for plain HTTP/2
frontend doh-front-plain
  mode http
  bind :8081 proto h2
  default_backend doh-servers-plain

backend doh-servers-plain
  mode http
  server s1 127.0.0.1:8080 check send-proxy-v2 proto h2

## TLS Termination (for DoT)
## (HAProxy takes care of encryption - it accepts a TLS connection and passes information
## about the client's endpoint via PROXYv2 and unencrypted data to the backend)
frontend dot-in-tls
    mode tcp
    timeout client 10s
    # Here we specify "dot" as the ALPN token to be selected.
    # HAProxy passes the negotiated ALPN via the TLVs section of the PROXYv2 header
    bind *:853 v4v6 tfo ssl crt /var/lib/acme/test.example.com/full.pem alpn dot
    default_backend dns-server-plain

backend dns-server-plain
    mode tcp
    timeout connect 10s
    timeout server 10s
    # Address where BIND listens for unencrypted TCP requests
    server doh-server 127.0.0.1:53000 check send-proxy-v2

## Same a above, but connection to the back-end is also encrypted
frontend dot-in-tls
    mode tcp
    timeout client 10s
    # Here we specify "dot" as the ALPN token to be selected.
    # HAProxy passes the negotiated ALPN via the TLVs section of the PROXYv2 header
    bind *:853 v4v6 tfo ssl crt /var/lib/acme/test.example.com/full.pem alpn dot
    default_backend dot-server

backend dot-server
    mode tcp
    timeout connect 10s
    timeout server 10s
    # Address where BIND listens for unencrypted TCP requests
    server doh-server 127.0.0.1:85353 check send-proxy-v2 ssl verify none

## TLS termination for plain HTTP/2
frontend doh-front-plain
  mode http
  bind :443 proto h2 ssl crt /var/lib/acme/test.example.com/full.pem alpn h2
  default_backend doh-servers-plain

backend doh-servers-plain
  mode http
  server s1 127.0.0.1:8080 check send-proxy-v2 proto h2

One can make HAProxy accept PROXYv2 headers by adding accept-proxy option to bind statement.

Closes #4388 (closed)

Edited by Artem Boldariev

Merge request reports