[Openvpn-devel] add support for --dns option

Message ID 20220312135810.242028-1-heiko@ist.eigentlich.net
State Changes Requested
Headers show
Series [Openvpn-devel] add support for --dns option | expand

Commit Message

Heiko Hund March 12, 2022, 2:58 a.m. UTC
As a first step towards DNS configuration in openvpn and a unified way
to push DNS related settings to clients in v2 and v3, this commit adds
support for parsing the new --dns option. Later commits will add support
for setting up DNS on different platforms.

For now, --dns and DNS related --dhcp-option can be used together for
smoother transition. Settings from --dns will override ones --dhcp-option
where applicable.

For detailed information about the option consult the documentation in
this commit.

Signed-off-by: Heiko Hund <heiko@ist.eigentlich.net>
---
 doc/man-sections/client-options.rst |  59 ++++
 doc/man-sections/script-options.rst |  19 ++
 doc/man-sections/server-options.rst |   2 +-
 src/openvpn/Makefile.am             |   1 +
 src/openvpn/dns.c                   | 495 ++++++++++++++++++++++++++++
 src/openvpn/dns.h                   | 164 +++++++++
 src/openvpn/openvpn.vcxproj         |   4 +-
 src/openvpn/openvpn.vcxproj.filters |   8 +-
 src/openvpn/options.c               | 221 +++++++++++++
 src/openvpn/options.h               |   7 +
 src/openvpn/push.c                  |   4 +
 src/openvpn/socket.c                |  11 +
 src/openvpn/socket.h                |   2 +
 13 files changed, 994 insertions(+), 3 deletions(-)
 create mode 100644 src/openvpn/dns.c
 create mode 100644 src/openvpn/dns.h

Comments

Heiko Hund March 12, 2022, 3:13 a.m. UTC | #1
Sorry, this is [PATCH v2].

Somehow I managed for send-email to override my --subject.
Heiko Hund March 16, 2022, 1:28 a.m. UTC | #2
On Samstag, 12. März 2022 14:58:10 CET Heiko Hund wrote:
> +        name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_search_domain_%d", i) && name_ok; 

With some distance, I still like the &= version better from a readability standpoint.
Even though it's a bit unclean.

Heiko
Gert Doering March 16, 2022, 11:41 p.m. UTC | #3
Hi,

(this is about v2)

On Sat, Mar 12, 2022 at 02:58:10PM +0100, Heiko Hund wrote:
> As a first step towards DNS configuration in openvpn and a unified way
> to push DNS related settings to clients in v2 and v3, this commit adds
> support for parsing the new --dns option. Later commits will add support
> for setting up DNS on different platforms.
> 
> For now, --dns and DNS related --dhcp-option can be used together for
> smoother transition. Settings from --dns will override ones --dhcp-option
> where applicable.
> 
> For detailed information about the option consult the documentation in
> this commit.
[..]
> +    for (i = 1, d = o->search_domains; d != NULL; i++, d = d->next)
> +    {
> +        name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_search_domain_%d", i) && name_ok;
> +        setenv_str(es, env_name, d->name);
> +    }

With the "&& name_ok" this code does not really improve, even if it
was requested in the v1 review...

I wonder why we bother to actually *do* this?  As in "we already know
that this can never overflow here" (because all strings involved are 
known, and the max width of %d is known, too), but *if* it ever did, 
calling the subsequent setenv_str() wouldn't be a good idea to do - no?

Can we make transform these into a miniwrapper that does

 - openvpn_snprintf()
 - check result, warn if overflow
 - call setenv_str() otherwise

like

   for (i = 1, d = o->search_domains; d != NULL; i++, d = d->next)
   {
        _do_dns_env_thing( "dns_search_domain_%d", i, d->name );
   }

so, shorter lines, clearer code, actual error checking, and no && ugliness :-)

(As far as I could see, it's all "setenv_str()" anyway)

gert
Heiko Hund March 17, 2022, 1:06 a.m. UTC | #4
Hi Gert

On Donnerstag, 17. März 2022 11:41:22 CET Gert Doering wrote:
> I wonder why we bother to actually *do* this?  As in "we already know
> that this can never overflow here" (because all strings involved are
> known, and the max width of %d is known, too), but *if* it ever did,
> calling the subsequent setenv_str() wouldn't be a good idea to do - no?

It's in the spirit of defensive programming. It cannot overflow now, but will likely overflow some time. However I thought that getting the value into env might still be worthwhile in some scenarios, even if the name is not complete, e.g. if the env is logged somewhere. The error message in the openvpn log, will lead to cause then.
 
> Can we make transform these into a miniwrapper that does
> like
> 
>    for (i = 1, d = o->search_domains; d != NULL; i++, d = d->next)
>    {
>         _do_dns_env_thing( "dns_search_domain_%d", i, d->name );
>    }

I actually had such a wrapper in place, but dumped it because I didn't like the fact that the value would have to come before the name in the argument list, because of the var-args. Since we have a format string with two %d (and there might be more / other formatting specifiers in the future), the wrapper would have to deal with ..., or we'd end up with two wrappers (or more if this is extended in the future). No matter what:

>            const char* format = s->domain_type == DNS_RESOLVE_DOMAINS ?
>                "dns_server_%d_resolve_domain_%d" : "dns_server_%d_exclude_domain_%d";

requires it to be:

 _do_dns_env_thing(d->name, "dns_server_%d_resolve__domain_%d", i, j);

right now already. And that lead me to not making things wrapped.

However I could be talked into removing the checks for openvpn_snprintf()'s return value if there's a consensus, as it wouldn't introduce an actual issue. Just maybe a little harder to debug issues in future code possibly. Also not setting the env if sprintf fails sounds good, would do this rather:

if (openvpn_snprinf())
    setenv_str();
else
    name_ok = false;

Iff the value is really not useful in the env, when the name is incomplete. Bonus: no &=.

Heiko

Patch

diff --git a/doc/man-sections/client-options.rst b/doc/man-sections/client-options.rst
index 92a02e28..2d7293c6 100644
--- a/doc/man-sections/client-options.rst
+++ b/doc/man-sections/client-options.rst
@@ -154,6 +154,65 @@  configuration.
 --connect-timeout n
   See ``--server-poll-timeout``.
 
+--dns args
+  Client DNS configuration to be used with the connection.
+
+  Valid syntaxes:
+  ::
+
+     dns search-domains domain [domain ...]
+     dns server n address addr[:port] [addr[:port]]
+     dns server n resolve-domains|exclude-domains domain [domain ...]
+     dns server n dnssec yes|optional|no
+     dns server n transport DoH|DoT|plain
+     dns server n sni server-name
+
+  The ``--dns search-domains`` directive takes one or more domain names
+  to be added as DNS domain suffixes. If it is repeated multiple times within
+  a configuration the domains are appended, thus e.g. domain names pushed by
+  a server will amend locally defined ones.
+
+  The ``--dns server`` directive is used to configure DNS server ``n``.
+  The server id ``n`` must be a value between -128 and 127. For pushed
+  DNS server options it must be between 0 and 127. The server id is used
+  to group options and also for ordering the list of configured DNS servers;
+  lower numbers come first. DNS servers being pushed to a client replace
+  already configured DNS servers with the same server id.
+
+  The ``address`` option configures the IPv4 and / or IPv6 address of
+  the DNS server. Optionally a port can be appended after a colon. IPv6
+  addresses need to be enclosed in brackets if a port is appended.
+
+  The ``resolve-domains`` and ``exclude-domains`` options take one or
+  more DNS domains which are explicitly resolved or explicitly not resolved
+  by a server. Only one of the options can be configured for a server.
+  ``resolve-domains`` is used to define a split-dns setup, where only
+  given domains are resolved by a server. ``exclude-domains`` is used to
+  define domains which will never be resolved by a server (e.g. domains
+  which can only be resolved locally). Systems which do not support fine
+  grained DNS domain configuration, will ignore these settings.
+
+  The ``dnssec`` option is used to configure validation of DNSSEC records.
+  While the exact semantics may differ for resolvers on different systems,
+  ``yes`` likely makes validation mandatory, ``no`` disables it, and ``optional``
+  uses it opportunistically.
+
+  The ``transport`` option enables DNS-over-HTTPS (``DoH``) or DNS-over-TLS (``DoT``)
+  for a DNS server. The ``sni`` option can be used with them to specify the
+  ``server-name`` for TLS server name indication.
+
+  Each server has to have at least one address configured for a configuration
+  to be valid. All the other options can be omitted.
+
+  Note that not all options may be supported on all platforms. As soon support
+  for different systems is implemented, information will be added here how
+  unsupported options are treated.
+
+  The ``--dns`` option will eventually obsolete the ``--dhcp-option`` directive.
+  Until then it will replace configuration at the places ``--dhcp-option`` puts it,
+  so that ``--dns`` overrides ``--dhcp-option``. Thus, ``--dns`` can be used today
+  to migrate from ``--dhcp-option``.
+
 --explicit-exit-notify n
   In UDP client mode or point-to-point mode, send server/peer an exit
   notification if tunnel is restarted or OpenVPN process is exited. In
diff --git a/doc/man-sections/script-options.rst b/doc/man-sections/script-options.rst
index 22990f4f..c81efe9e 100644
--- a/doc/man-sections/script-options.rst
+++ b/doc/man-sections/script-options.rst
@@ -586,6 +586,25 @@  instances.
     netsh.exe calls which sometimes just do not work right with interface
     names). Set prior to ``--up`` or ``--down`` script execution.
 
+:code:`dns_*`
+    The ``--dns`` configuration options will be made available to script
+    execution through this set of environment variables. Variables appear
+    only if the corresponding option has a value assigned. For the semantics
+    of each individual variable, please refer to the documentation for ``--dns``.
+
+    ::
+
+       dns_search_domain_{n}
+       dns_server_{n}_address4
+       dns_server_{n}_port4
+       dns_server_{n}_address6
+       dns_server_{n}_port6
+       dns_server_{n}_resolve_domain_{m}
+       dns_server_{n}_exclude_domain_{m}
+       dns_server_{n}_dnssec
+       dns_server_{n}_transport
+       dns_server_{n}_sni
+
 :code:`foreign_option_{n}`
     An option pushed via ``--push`` to a client which does not natively
     support it, such as ``--dhcp-option`` on a non-Windows system, will be
diff --git a/doc/man-sections/server-options.rst b/doc/man-sections/server-options.rst
index 8a030294..08ee7bd3 100644
--- a/doc/man-sections/server-options.rst
+++ b/doc/man-sections/server-options.rst
@@ -412,7 +412,7 @@  fast hardware. SSL/TLS authentication must be used in this mode.
 
   This is a partial list of options which can currently be pushed:
   ``--route``, ``--route-gateway``, ``--route-delay``,
-  ``--redirect-gateway``, ``--ip-win32``, ``--dhcp-option``,
+  ``--redirect-gateway``, ``--ip-win32``, ``--dhcp-option``, ``--dns``,
   ``--inactive``, ``--ping``, ``--ping-exit``, ``--ping-restart``,
   ``--setenv``, ``--auth-token``, ``--persist-key``, ``--persist-tun``,
   ``--echo``, ``--comp-lzo``, ``--socket-flags``, ``--sndbuf``,
diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am
index 5883c291..bcaf93b4 100644
--- a/src/openvpn/Makefile.am
+++ b/src/openvpn/Makefile.am
@@ -54,6 +54,7 @@  openvpn_SOURCES = \
 	crypto_openssl.c crypto_openssl.h \
 	crypto_mbedtls.c crypto_mbedtls.h \
 	dhcp.c dhcp.h \
+	dns.c dns.h \
 	env_set.c env_set.h \
 	errlevel.h \
 	error.c error.h \
diff --git a/src/openvpn/dns.c b/src/openvpn/dns.c
new file mode 100644
index 00000000..cd13f8f6
--- /dev/null
+++ b/src/openvpn/dns.c
@@ -0,0 +1,495 @@ 
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2022 OpenVPN Inc <sales@openvpn.net>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2
+ *  as published by the Free Software Foundation.
+ *
+ *  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, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#elif defined(_MSC_VER)
+#include "config-msvc.h"
+#endif
+
+#include "syshead.h"
+
+#include "dns.h"
+#include "socket.h"
+
+/**
+ * Parses a string as port and stores it
+ *
+ * @param   port        Pointer to in_port_t where the port value is stored
+ * @param   addr        Port number as string
+ * @return              True if parsing was successful
+ */
+static bool
+dns_server_port_parse(in_port_t *port, char *port_str)
+{
+    char *endptr;
+    errno = 0;
+    unsigned long tmp = strtoul(port_str, &endptr, 10);
+    if (errno || *endptr != '\0' || tmp == 0 || tmp > UINT16_MAX)
+    {
+        return false;
+    }
+    *port = (in_port_t)tmp;
+    return true;
+}
+
+bool
+dns_server_addr_parse(struct dns_server *server, const char *addr)
+{
+    if (!addr)
+    {
+        return false;
+    }
+
+    char addrcopy[INET6_ADDRSTRLEN] = {0};
+    size_t copylen = 0;
+    in_port_t port = 0;
+    int af;
+
+    char *first_colon = strchr(addr, ':');
+    char *last_colon = strrchr(addr, ':');
+
+    if (!first_colon || first_colon == last_colon)
+    {
+        /* IPv4 address with optional port, e.g. 1.2.3.4 or 1.2.3.4:853 */
+        if (last_colon)
+        {
+            if (last_colon == addr || !dns_server_port_parse(&port, last_colon + 1))
+            {
+                return false;
+            }
+            copylen = first_colon - addr;
+        }
+        af = AF_INET;
+    }
+    else
+    {
+        /* IPv6 address with optional port, e.g. ab::cd or [ab::cd]:853 */
+        if (addr[0] == '[')
+        {
+            addr += 1;
+            char *bracket = last_colon - 1;
+            if (*bracket != ']' || bracket == addr || !dns_server_port_parse(&port, last_colon + 1))
+            {
+                return false;
+            }
+            copylen = bracket - addr;
+        }
+        af = AF_INET6;
+    }
+
+    /* Copy the address part into a temporary buffer and use that */
+    if (copylen)
+    {
+        if (copylen >= sizeof(addrcopy))
+        {
+            return false;
+        }
+        strncpy(addrcopy, addr, copylen);
+        addr = addrcopy;
+    }
+
+    struct addrinfo *ai = NULL;
+    if (openvpn_getaddrinfo(0, addr, NULL, 0, NULL, af, &ai) != 0)
+    {
+        return false;
+    }
+
+    if (ai->ai_family == AF_INET)
+    {
+        struct sockaddr_in *sin = (struct sockaddr_in *)ai->ai_addr;
+        server->addr4_defined = true;
+        server->addr4.s_addr = ntohl(sin->sin_addr.s_addr);
+        server->port4 = port;
+    }
+    else
+    {
+        struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ai->ai_addr;
+        server->addr6_defined = true;
+        server->addr6 = sin6->sin6_addr;
+        server->port6 = port;
+    }
+
+    freeaddrinfo(ai);
+    return true;
+}
+
+void
+dns_domain_list_append(struct dns_domain **entry, char **domains, struct gc_arena *gc)
+{
+    /* Fast forward to the end of the list */
+    while (*entry)
+    {
+        entry = &((*entry)->next);
+    }
+
+    /* Append all domains to the end of the list */
+    while (*domains)
+    {
+        ALLOC_OBJ_CLEAR_GC(*entry, struct dns_domain, gc);
+        struct dns_domain *new = *entry;
+        new->name = *domains++;
+        entry = &new->next;
+    }
+}
+
+bool
+dns_server_priority_parse(long *priority, const char *str, bool pulled)
+{
+    char *endptr;
+    const long min = pulled ? 0 : INT8_MIN;
+    const long max = INT8_MAX;
+    long prio = strtol(str, &endptr, 10);
+    if (*endptr != '\0' || prio < min || prio > max)
+    {
+        return false;
+    }
+    *priority = prio;
+    return true;
+}
+
+struct dns_server*
+dns_server_get(struct dns_server **entry, long priority, struct gc_arena *gc)
+{
+    struct dns_server *obj = *entry;
+    while (true)
+    {
+        if (!obj || obj->priority > priority)
+        {
+            ALLOC_OBJ_CLEAR_GC(*entry, struct dns_server, gc);
+            (*entry)->next = obj;
+            (*entry)->priority = priority;
+            return *entry;
+        }
+        else if (obj->priority == priority)
+        {
+            return obj;
+        }
+        entry = &obj->next;
+        obj = *entry;
+    }
+}
+
+bool
+dns_options_verify(int msglevel, const struct dns_options *o)
+{
+    const struct dns_server *server =
+        o->servers ? o->servers : o->servers_prepull;
+    while (server)
+    {
+        if (!server->addr4_defined && !server->addr6_defined)
+        {
+            msg(msglevel, "ERROR: dns server %ld does not have an address assigned", server->priority);
+            return false;
+        }
+        server = server->next;
+    }
+    return true;
+}
+
+static struct dns_domain*
+clone_dns_domains(const struct dns_domain *domain, struct gc_arena* gc)
+{
+    struct dns_domain *new_list = NULL;
+    struct dns_domain **new_entry = &new_list;
+
+    while (domain)
+    {
+        ALLOC_OBJ_CLEAR_GC(*new_entry, struct dns_domain, gc);
+        struct dns_domain *new_domain = *new_entry;
+        *new_domain = *domain;
+        new_entry = &new_domain->next;
+        domain = domain->next;
+    }
+
+    return new_list;
+}
+
+static struct dns_server*
+clone_dns_servers(const struct dns_server *server, struct gc_arena* gc)
+{
+    struct dns_server *new_list = NULL;
+    struct dns_server **new_entry = &new_list;
+
+    while (server)
+    {
+        ALLOC_OBJ_CLEAR_GC(*new_entry, struct dns_server, gc);
+        struct dns_server *new_server = *new_entry;
+        *new_server = *server;
+        new_server->domains = clone_dns_domains(server->domains, gc);
+        new_entry = &new_server->next;
+        server = server->next;
+    }
+
+    return new_list;
+}
+
+struct dns_options
+clone_dns_options(const struct dns_options o, struct gc_arena* gc)
+{
+    struct dns_options clone;
+    memset(&clone, 0, sizeof(clone));
+    clone.search_domains = clone_dns_domains(o.search_domains, gc);
+    clone.servers = clone_dns_servers(o.servers, gc);
+    clone.servers_prepull = clone_dns_servers(o.servers_prepull, gc);
+    return clone;
+}
+
+void
+dns_options_preprocess_pull(struct dns_options *o)
+{
+    o->servers_prepull = o->servers;
+    o->servers = NULL;
+}
+
+void
+dns_options_postprocess_pull(struct dns_options *o)
+{
+    struct dns_server **entry = &o->servers;
+    struct dns_server *server = *entry;
+    struct dns_server *server_pp = o->servers_prepull;
+
+    while (server && server_pp)
+    {
+        if (server->priority > server_pp->priority)
+        {
+            /* Merge static server in front of pulled one */
+            struct dns_server *next_pp = server_pp->next;
+            server_pp->next = server;
+            *entry = server_pp;
+            server = *entry;
+            server_pp = next_pp;
+        }
+        else if (server->priority == server_pp->priority)
+        {
+            /* Pulled server overrides static one */
+            server_pp = server_pp->next;
+        }
+        entry = &server->next;
+        server = *entry;
+    }
+
+    /* Append remaining local servers */
+    if (server_pp)
+    {
+        *entry = server_pp;
+    }
+
+    o->servers_prepull = NULL;
+}
+
+static const char *
+dnssec_value(const enum dns_security dnssec)
+{
+    switch (dnssec)
+    {
+    case DNS_SECURITY_YES:
+        return "yes";
+    case DNS_SECURITY_OPTIONAL:
+        return "optional";
+    case DNS_SECURITY_NO:
+        return "no";
+    default:
+        return "unset";
+    }
+}
+
+static const char *
+transport_value(const enum dns_server_transport transport)
+{
+    switch (transport)
+    {
+    case DNS_TRANSPORT_HTTPS:
+        return "DoH";
+    case DNS_TRANSPORT_TLS:
+        return "DoT";
+    case DNS_TRANSPORT_PLAIN:
+        return "plain";
+    default:
+        return "unset";
+    }
+}
+
+void
+setenv_dns_options(const struct dns_options *o, struct env_set *es)
+{
+    struct gc_arena gc = gc_new();
+    const struct dns_server *s;
+    const struct dns_domain *d;
+    bool name_ok = true;
+    char env_name[64];
+    int i, j;
+
+    for (i = 1, d = o->search_domains; d != NULL; i++, d = d->next)
+    {
+        name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_search_domain_%d", i) && name_ok;
+        setenv_str(es, env_name, d->name);
+    }
+
+    for (i = 1, s = o->servers; s != NULL; i++, s = s->next)
+    {
+        if (s->addr4_defined)
+        {
+            name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_server_%d_address4", i) && name_ok;
+            setenv_str(es, env_name, print_in_addr_t(s->addr4.s_addr, 0, &gc));
+        }
+        if (s->port4)
+        {
+            name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_server_%d_port4", i) && name_ok;
+            setenv_str(es, env_name, print_in_port_t(s->port4, &gc));
+        }
+
+        if (s->addr6_defined)
+        {
+            name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_server_%d_address6", i) && name_ok;
+            setenv_str(es, env_name, print_in6_addr(s->addr6, 0, &gc));
+        }
+        if (s->port6)
+        {
+            name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_server_%d_port6", i) && name_ok;
+            setenv_str(es, env_name, print_in_port_t(s->port6, &gc));
+        }
+
+        if (s->domains)
+        {
+            const char* format = s->domain_type == DNS_RESOLVE_DOMAINS ?
+                "dns_server_%d_resolve_domain_%d" : "dns_server_%d_exclude_domain_%d";
+            for (j = 1, d = s->domains; d != NULL; j++, d = d->next)
+            {
+                name_ok = openvpn_snprintf(env_name, sizeof(env_name), format, i, j) && name_ok;
+                setenv_str(es, env_name, d->name);
+            }
+        }
+
+        if (s->dnssec)
+        {
+            name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_server_%d_dnssec", i) && name_ok;
+            setenv_str(es, env_name, dnssec_value(s->dnssec));
+        }
+
+        if (s->transport)
+        {
+            name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_server_%d_transport", i) && name_ok;
+            setenv_str(es, env_name, transport_value(s->transport));
+        }
+        if (s->sni)
+        {
+            name_ok = openvpn_snprintf(env_name, sizeof(env_name), "dns_server_%d_sni", i) && name_ok;
+            setenv_str(es, env_name, s->sni);
+        }
+    }
+
+    if (!name_ok)
+    {
+        msg(M_WARN, "WARNING: dns option setenv name buffer overflow");
+    }
+
+    gc_free(&gc);
+}
+
+void
+show_dns_options(const struct dns_options *o)
+{
+    struct gc_arena gc = gc_new();
+
+    int i = 1;
+    struct dns_server *server = o->servers_prepull ? o->servers_prepull : o->servers;
+    while (server)
+    {
+        msg(D_SHOW_PARMS, "  DNS server #%d:", i++);
+
+        if (server->addr4_defined)
+        {
+            const char *addr = print_in_addr_t(server->addr4.s_addr, 0, &gc);
+            if (server->port4)
+            {
+                const char *port = print_in_port_t(server->port4, &gc);
+                msg(D_SHOW_PARMS, "    address4 = %s:%s", addr, port);
+            }
+            else
+            {
+                msg(D_SHOW_PARMS, "    address4 = %s", addr);
+            }
+        }
+        if (server->addr6_defined)
+        {
+            const char *addr = print_in6_addr(server->addr6, 0, &gc);
+            if (server->port6)
+            {
+                const char *port = print_in_port_t(server->port6, &gc);
+                msg(D_SHOW_PARMS, "    address6 = [%s]:%s", addr, port);
+            }
+            else
+            {
+                msg(D_SHOW_PARMS, "    address6 = %s", addr);
+            }
+        }
+
+        if (server->dnssec)
+        {
+            msg(D_SHOW_PARMS, "    dnssec = %s", dnssec_value(server->dnssec));
+        }
+
+        if (server->transport)
+        {
+            msg(D_SHOW_PARMS, "    transport = %s", transport_value(server->transport));
+        }
+        if (server->sni)
+        {
+            msg(D_SHOW_PARMS, "    sni = %s", server->sni);
+        }
+
+        struct dns_domain *domain = server->domains;
+        if (domain)
+        {
+            if (server->domain_type == DNS_RESOLVE_DOMAINS)
+            {
+                msg(D_SHOW_PARMS, "    resolve domains:");
+            }
+            else
+            {
+                msg(D_SHOW_PARMS, "    exclude domains:");
+            }
+            while(domain)
+            {
+                msg(D_SHOW_PARMS, "      %s", domain->name);
+                domain = domain->next;
+            }
+        }
+
+        server = server->next;
+    }
+
+    struct dns_domain *search_domain = o->search_domains;
+    if (search_domain)
+    {
+        msg(D_SHOW_PARMS, "  DNS search domains:");
+        while(search_domain)
+        {
+            msg(D_SHOW_PARMS, "    %s", search_domain->name);
+            search_domain = search_domain->next;
+        }
+    }
+
+    gc_free(&gc);
+}
diff --git a/src/openvpn/dns.h b/src/openvpn/dns.h
new file mode 100644
index 00000000..1c6bfc6f
--- /dev/null
+++ b/src/openvpn/dns.h
@@ -0,0 +1,164 @@ 
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2022 OpenVPN Inc <sales@openvpn.net>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2
+ *  as published by the Free Software Foundation.
+ *
+ *  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, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef DNS_H
+#define DNS_H
+
+#include "buffer.h"
+#include "env_set.h"
+
+enum dns_domain_type {
+  DNS_DOMAINS_UNSET,
+  DNS_RESOLVE_DOMAINS,
+  DNS_EXCLUDE_DOMAINS
+};
+
+enum dns_security {
+  DNS_SECURITY_UNSET,
+  DNS_SECURITY_NO,
+  DNS_SECURITY_YES,
+  DNS_SECURITY_OPTIONAL
+};
+
+enum dns_server_transport {
+  DNS_TRANSPORT_UNSET,
+  DNS_TRANSPORT_PLAIN,
+  DNS_TRANSPORT_HTTPS,
+  DNS_TRANSPORT_TLS
+};
+
+struct dns_domain {
+  struct dns_domain *next;
+  const char *name;
+};
+
+struct dns_server {
+  struct dns_server *next;
+  long priority;
+  bool addr4_defined;
+  bool addr6_defined;
+  struct in_addr addr4;
+  struct in6_addr addr6;
+  in_port_t port4;
+  in_port_t port6;
+  struct dns_domain *domains;
+  enum dns_domain_type domain_type;
+  enum dns_security dnssec;
+  enum dns_server_transport transport;
+  const char *sni;
+};
+
+struct dns_options {
+  struct dns_domain *search_domains;
+  struct dns_server *servers_prepull;
+  struct dns_server *servers;
+  struct gc_arena gc;
+};
+
+/**
+ * Parses a string DNS server priority and validates it.
+ *
+ * @param   priority    Pointer to where the priority should be stored
+ * @param   str         Priority string to parse
+ * @param   pulled      Whether this was pulled from a server
+ * @return              True if priority in string is valid
+ */
+bool dns_server_priority_parse(long *priority, const char *str, bool pulled);
+
+/**
+ * Find or create DNS server with priority in a linked list.
+ * The list is ordered by priority.
+ *
+ * @param   entry       Address of the first list entry pointer
+ * @param   priority    Priority of the DNS server to find / create
+ * @param   gc          The gc new list items should be allocated in
+ */
+struct dns_server* dns_server_get(struct dns_server **entry, long priority, struct gc_arena *gc);
+
+/**
+ * Appends DNS domain parameters to a linked list.
+ *
+ * @param   entry       Address of the first list entry pointer
+ * @param   domains     Address of the first domain parameter
+ * @param   gc          The gc the new list items should be allocated in
+ */
+void dns_domain_list_append(struct dns_domain **entry, char **domains, struct gc_arena *gc);
+
+/**
+ * Parses a string IPv4 or IPv6 address and optional colon separated port,
+ * into a in_addr or in6_addr respectively plus a in_port_t port.
+ *
+ * @param   server      Pointer to DNS server the address is parsed for
+ * @param   addr        Address as string
+ * @return              True if parsing was successful
+ */
+bool dns_server_addr_parse(struct dns_server *server, const char *addr);
+
+/**
+ * Checks validity of DNS options
+ *
+ * @param   msglevel    The message level to log errors with
+ * @param   o           Pointer to the DNS options to validate
+ * @return              True if no error was found
+ */
+bool dns_options_verify(int msglevel, const struct dns_options *o);
+
+/**
+ * Makes a deep copy of the passed DNS options.
+ *
+ * @param   o           Pointer to the DNS options to clone
+ * @param   gc          Pointer to the gc_arena to use for the clone
+ * @return              The dns_options clone
+  */
+struct dns_options clone_dns_options(const struct dns_options o, struct gc_arena *gc);
+
+/**
+ * Saves and resets the server options, so that pulled ones don't mix in.
+ *
+ * @param   o           Pointer to the DNS options to modify
+ */
+void dns_options_preprocess_pull(struct dns_options *o);
+
+/**
+ * Merges pulled DNS servers with static ones into an ordered list.
+ *
+ * @param   o           Pointer to the DNS options to modify
+ */
+void dns_options_postprocess_pull(struct dns_options *o);
+
+/**
+ * Puts the DNS options into an environment set.
+ *
+ * @param   o           Pointer to the DNS options to set
+ * @param   es          Pointer to the env_set to set the options into
+ */
+void setenv_dns_options(const struct dns_options *o, struct env_set *es);
+
+/**
+ * Prints configured DNS options.
+ *
+ * @param   o           Pointer to the DNS options to print
+ */
+void show_dns_options(const struct dns_options *o);
+
+#endif
diff --git a/src/openvpn/openvpn.vcxproj b/src/openvpn/openvpn.vcxproj
index 65ee6839..f6ec17bf 100644
--- a/src/openvpn/openvpn.vcxproj
+++ b/src/openvpn/openvpn.vcxproj
@@ -255,6 +255,7 @@ 
     <ClCompile Include="cryptoapi.c" />
     <ClCompile Include="env_set.c" />
     <ClCompile Include="dhcp.c" />
+    <ClCompile Include="dns.c" />
     <ClCompile Include="error.c" />
     <ClCompile Include="event.c" />
     <ClCompile Include="fdmisc.c" />
@@ -336,6 +337,7 @@ 
     <ClInclude Include="crypto_openssl.h" />
     <ClInclude Include="cryptoapi.h" />
     <ClInclude Include="dhcp.h" />
+    <ClInclude Include="dns.h" />
     <ClInclude Include="env_set.h" />
     <ClInclude Include="errlevel.h" />
     <ClInclude Include="error.h" />
@@ -427,4 +429,4 @@ 
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/src/openvpn/openvpn.vcxproj.filters b/src/openvpn/openvpn.vcxproj.filters
index f5fdfcd7..97bca54b 100644
--- a/src/openvpn/openvpn.vcxproj.filters
+++ b/src/openvpn/openvpn.vcxproj.filters
@@ -39,6 +39,9 @@ 
     <ClCompile Include="dhcp.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="dns.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="error.c">
       <Filter>Source Files</Filter>
     </ClCompile>
@@ -290,6 +293,9 @@ 
     <ClInclude Include="dhcp.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="dns.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
     <ClInclude Include="errlevel.h">
       <Filter>Header Files</Filter>
     </ClInclude>
@@ -526,4 +532,4 @@ 
       <Filter>Resource Files</Filter>
     </Manifest>
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/src/openvpn/options.c b/src/openvpn/options.c
index bf8e7759..7dd0dbb2 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -496,6 +496,16 @@  static const char usage_message[] =
     "                  ignore or reject causes the option to be allowed, removed or\n"
     "                  rejected with error. May be specified multiple times, and\n"
     "                  each filter is applied in the order of appearance.\n"
+    "--dns server <n> <option> <value> [value ...] : Configure option for DNS server #n\n"
+    "                  Valid options are :\n"
+    "                  address <addr[:port]> [addr[:port]] : server address 4/6\n"
+    "                  resolve-domains <domain> [domain ...] : split domains\n"
+    "                  exclude-domains <domain> [domain ...] : domains not to resolve\n"
+    "                  dnssec <yes|no|optional> : option to use DNSSEC\n"
+    "                  type <DoH|DoT> : query server over HTTPS / TLS\n"
+    "                  sni <domain> : DNS server name indication\n"
+    "--dns search-domains <domain> [domain ...]:\n"
+    "                  Add domains to DNS domain search list\n"
     "--auth-retry t  : How to handle auth failures.  Set t to\n"
     "                  none (default), interact, or nointeract.\n"
     "--static-challenge t e : Enable static challenge/response protocol using\n"
@@ -783,6 +793,7 @@  init_options(struct options *o, const bool init_gc)
     if (init_gc)
     {
         gc_init(&o->gc);
+        gc_init(&o->dns_options.gc);
         o->gc_owned = true;
     }
     o->mode = MODE_POINT_TO_POINT;
@@ -886,6 +897,7 @@  uninit_options(struct options *o)
     if (o->gc_owned)
     {
         gc_free(&o->gc);
+        gc_free(&o->dns_options.gc);
     }
 }
 
@@ -988,6 +1000,11 @@  setenv_settings(struct env_set *es, const struct options *o)
     {
         setenv_connection_entry(es, &o->ce, 1);
     }
+
+    if (!o->pull)
+    {
+        setenv_dns_options(&o->dns_options, es);
+    }
 }
 
 static in_addr_t
@@ -1262,6 +1279,64 @@  dhcp_option_address_parse(const char *name, const char *parm, in_addr_t *array,
     }
 }
 
+/*
+ * If DNS options are set use these for TUN/TAP options as well.
+ * Applies to DNS, DNS6 and DOMAIN-SEARCH.
+ * Existing options will be discarded.
+ */
+static void
+tuntap_options_copy_dns(struct options *o)
+{
+    struct tuntap_options *tt = &o->tuntap_options;
+    struct dns_options *dns = &o->dns_options;
+
+    if (dns->search_domains)
+    {
+        tt->domain_search_list_len = 0;
+        const struct dns_domain *domain = dns->search_domains;
+        while (domain && tt->domain_search_list_len < N_SEARCH_LIST_LEN)
+        {
+            tt->domain_search_list[tt->domain_search_list_len++] = domain->name;
+            domain = domain->next;
+        }
+        if (domain)
+        {
+            msg(M_WARN, "WARNING: couldn't copy all --dns search-domains to --dhcp-option");
+        }
+    }
+
+    if (dns->servers)
+    {
+        tt->dns_len = 0;
+        tt->dns6_len = 0;
+        bool overflow = false;
+        const struct dns_server *server = dns->servers;
+        while (server)
+        {
+            if (server->addr4_defined && tt->dns_len < N_DHCP_ADDR)
+            {
+                tt->dns[tt->dns_len++] = server->addr4.s_addr;
+            }
+            else
+            {
+                overflow = true;
+            }
+            if (server->addr6_defined && tt->dns6_len < N_DHCP_ADDR)
+            {
+                tt->dns6[tt->dns6_len++] = server->addr6;
+            }
+            else
+            {
+                overflow = true;
+            }
+            server = server->next;
+        }
+        if (overflow)
+        {
+            msg(M_WARN, "WARNING: couldn't copy all --dns server addresses to --dhcp-option");
+        }
+    }
+}
 #endif /* if defined(_WIN32) || defined(TARGET_ANDROID) */
 
 static const char *
@@ -1690,6 +1765,8 @@  show_settings(const struct options *o)
         print_client_nat_list(o->client_nat, D_SHOW_PARMS);
     }
 
+    show_dns_options(&o->dns_options);
+
 #ifdef ENABLE_MANAGEMENT
     SHOW_STR(management_addr);
     SHOW_STR(management_port);
@@ -3069,6 +3146,8 @@  options_postprocess_verify(const struct options *o)
     {
         options_postprocess_verify_ce(o, &o->ce);
     }
+
+    dns_options_verify(M_FATAL, &o->dns_options);
 }
 
 /**
@@ -3311,6 +3390,16 @@  options_postprocess_mutate(struct options *o)
      * Save certain parms before modifying options during connect, especially
      * when using --pull
      */
+    if (o->pull)
+    {
+        dns_options_preprocess_pull(&o->dns_options);
+    }
+#if defined(_WIN32) || defined(TARGET_ANDROID)
+    else
+    {
+        tuntap_options_copy_dns(o);
+    }
+#endif
     pre_connect_save(o);
 }
 
@@ -3655,6 +3744,25 @@  options_postprocess(struct options *options)
 #endif /* !ENABLE_SMALL */
 }
 
+/*
+ * Sanity check on options after more options were pulled from server.
+ * Also time to modify some options based on other options.
+ */
+bool
+options_postprocess_pull(struct options *o, struct env_set *es)
+{
+    bool success = dns_options_verify(D_PUSH_ERRORS, &o->dns_options);
+    if (success)
+    {
+        dns_options_postprocess_pull(&o->dns_options);
+        setenv_dns_options(&o->dns_options, es);
+#if defined(_WIN32) || defined(TARGET_ANDROID)
+        tuntap_options_copy_dns(o);
+#endif
+    }
+    return success;
+}
+
 /*
  * Save/Restore certain option defaults before --pull is applied.
  */
@@ -3686,6 +3794,8 @@  pre_connect_save(struct options *o)
     o->pre_connect->route_default_gateway = o->route_default_gateway;
     o->pre_connect->route_ipv6_default_gateway = o->route_ipv6_default_gateway;
 
+    o->pre_connect->dns_options = clone_dns_options(o->dns_options, &o->gc);
+
     /* NCP related options that can be overwritten by a push */
     o->pre_connect->ciphername = o->ciphername;
     o->pre_connect->authname = o->authname;
@@ -3736,6 +3846,12 @@  pre_connect_restore(struct options *o, struct gc_arena *gc)
         o->route_default_gateway = pp->route_default_gateway;
         o->route_ipv6_default_gateway = pp->route_ipv6_default_gateway;
 
+        /* Free DNS options and reset them to pre-pull state */
+        gc_free(&o->dns_options.gc);
+        struct gc_arena dns_gc = gc_new();
+        o->dns_options = clone_dns_options(pp->dns_options, &dns_gc);
+        o->dns_options.gc = dns_gc;
+
         if (pp->client_nat_defined)
         {
             cnol_check_alloc(o);
@@ -7485,6 +7601,111 @@  add_option(struct options *options,
         to->ip_win32_defined = true;
     }
 #endif /* ifdef _WIN32 */
+    else if (streq(p[0], "dns") && p[1])
+    {
+        VERIFY_PERMISSION(OPT_P_DEFAULT);
+
+        if (streq(p[1], "search-domains") && p[2])
+        {
+            dns_domain_list_append(&options->dns_options.search_domains, &p[2], &options->dns_options.gc);
+        }
+        else if (streq(p[1], "server") && p[2] && p[3] && p[4])
+        {
+            long priority;
+            if (!dns_server_priority_parse(&priority, p[2], pull_mode)) {
+                msg(msglevel, "--dns server: invalid priority value '%s'", p[2]);
+                goto err;
+            }
+
+            struct dns_server *server = dns_server_get(&options->dns_options.servers, priority, &options->dns_options.gc);
+
+            if (streq(p[3], "address") && !p[6])
+            {
+                for (int i = 4; p[i]; i++)
+                {
+                    if(!dns_server_addr_parse(server, p[i]))
+                    {
+                        msg(msglevel, "--dns server %ld: malformed or duplicate address '%s'", priority, p[i]);
+                        goto err;
+                    }
+                }
+            }
+            else if (streq(p[3], "resolve-domains"))
+            {
+                if (server->domain_type == DNS_EXCLUDE_DOMAINS)
+                {
+                    msg(msglevel, "--dns server %ld: cannot use resolve-domains and exclude-domains", priority);
+                    goto err;
+                }
+                server->domain_type = DNS_RESOLVE_DOMAINS;
+                dns_domain_list_append(&server->domains, &p[4], &options->dns_options.gc);
+            }
+            else if (streq(p[3], "exclude-domains"))
+            {
+                if (server->domain_type == DNS_RESOLVE_DOMAINS)
+                {
+                    msg(msglevel, "--dns server %ld: cannot use exclude-domains and resolve-domains", priority);
+                    goto err;
+                }
+                server->domain_type = DNS_EXCLUDE_DOMAINS;
+                dns_domain_list_append(&server->domains, &p[4], &options->dns_options.gc);
+            }
+            else if (streq(p[3], "dnssec") && !p[5])
+            {
+                if (streq(p[4], "yes"))
+                {
+                    server->dnssec = DNS_SECURITY_YES;
+                }
+                else if (streq(p[4], "no"))
+                {
+                    server->dnssec = DNS_SECURITY_NO;
+                }
+                else if (streq(p[4], "optional"))
+                {
+                    server->dnssec = DNS_SECURITY_OPTIONAL;
+                }
+                else
+                {
+                    msg(msglevel, "--dns server %ld: malformed dnssec value '%s'", priority, p[4]);
+                    goto err;
+                }
+            }
+            else if (streq(p[3], "transport") && !p[5])
+            {
+                if (streq(p[4], "plain"))
+                {
+                    server->transport = DNS_TRANSPORT_PLAIN;
+                }
+                else if (streq(p[4], "DoH"))
+                {
+                    server->transport = DNS_TRANSPORT_HTTPS;
+                }
+                else if (streq(p[4], "DoT"))
+                {
+                    server->transport = DNS_TRANSPORT_TLS;
+                }
+                else
+                {
+                    msg(msglevel, "--dns server %ld: malformed transport value '%s'", priority, p[4]);
+                    goto err;
+                }
+            }
+            else if (streq(p[3], "sni") && !p[5])
+            {
+                server->sni = p[4];
+            }
+            else
+            {
+                msg(msglevel, "--dns server %ld: unknown option type '%s' or missing or unknown parameter", priority, p[3]);
+                goto err;
+            }
+        }
+        else
+        {
+            msg(msglevel, "--dns: unknown option type '%s' or missing or unknown parameter", p[1]);
+            goto err;
+        }
+    }
 #if defined(_WIN32) || defined(TARGET_ANDROID)
     else if (streq(p[0], "dhcp-option") && p[1])
     {
diff --git a/src/openvpn/options.h b/src/openvpn/options.h
index d4f41cd7..c06fcbf1 100644
--- a/src/openvpn/options.h
+++ b/src/openvpn/options.h
@@ -42,6 +42,7 @@ 
 #include "pushlist.h"
 #include "clinat.h"
 #include "crypto_backend.h"
+#include "dns.h"
 
 
 /*
@@ -76,6 +77,8 @@  struct options_pre_connect
     bool client_nat_defined;
     struct client_nat_option_list *client_nat;
 
+    struct dns_options dns_options;
+
     const char* ciphername;
     const char* authname;
 
@@ -272,6 +275,8 @@  struct options
 
     struct remote_host_store *rh_store;
 
+    struct dns_options dns_options;
+
     bool remote_random;
     const char *ipchange;
     const char *dev;
@@ -802,6 +807,8 @@  char *options_string_extract_option(const char *options_string,
 
 void options_postprocess(struct options *options);
 
+bool options_postprocess_pull(struct options *o, struct env_set *es);
+
 void pre_connect_save(struct options *o);
 
 void pre_connect_restore(struct options *o, struct gc_arena *gc);
diff --git a/src/openvpn/push.c b/src/openvpn/push.c
index f9343b42..43716500 100644
--- a/src/openvpn/push.c
+++ b/src/openvpn/push.c
@@ -459,6 +459,10 @@  incoming_push_message(struct context *c, const struct buffer *buffer)
         /* delay bringing tun/tap up until --push parms received from remote */
         if (status == PUSH_MSG_REPLY)
         {
+            if (!options_postprocess_pull(&c->options, c->c2.es))
+            {
+                goto error;
+            }
             if (!do_up(c, true, c->options.push_option_types_found))
             {
                 msg(D_PUSH_ERRORS, "Failed to open tun/tap interface");
diff --git a/src/openvpn/socket.c b/src/openvpn/socket.c
index df736746..d00cd4ac 100644
--- a/src/openvpn/socket.c
+++ b/src/openvpn/socket.c
@@ -2906,6 +2906,17 @@  print_in6_addr(struct in6_addr a6, unsigned int flags, struct gc_arena *gc)
     return BSTR(&out);
 }
 
+/*
+ * Convert an in_port_t in host byte order to a string
+ */
+const char *
+print_in_port_t(in_port_t port, struct gc_arena *gc)
+{
+    struct buffer buffer = alloc_buf_gc(8, gc);
+    buf_printf(&buffer, "%hu", port);
+    return BSTR(&buffer);
+}
+
 #ifndef UINT8_MAX
 #define UINT8_MAX 0xff
 #endif
diff --git a/src/openvpn/socket.h b/src/openvpn/socket.h
index cc1e0c36..deaa41a2 100644
--- a/src/openvpn/socket.h
+++ b/src/openvpn/socket.h
@@ -360,6 +360,8 @@  const char *print_in_addr_t(in_addr_t addr, unsigned int flags, struct gc_arena
 
 const char *print_in6_addr(struct in6_addr addr6, unsigned int flags, struct gc_arena *gc);
 
+const char *print_in_port_t(in_port_t port, struct gc_arena *gc);
+
 struct in6_addr add_in6_addr( struct in6_addr base, uint32_t add );
 
 #define SA_IP_PORT        (1<<0)