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

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

Commit Message

Heiko Hund March 8, 2022, 11:06 p.m.
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 |  55 +++
 doc/man-sections/script-options.rst |  19 +
 doc/man-sections/server-options.rst |   2 +-
 src/openvpn/Makefile.am             |   1 +
 src/openvpn/dns.c                   | 561 ++++++++++++++++++++++++++++
 src/openvpn/dns.h                   |  89 +++++
 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, 981 insertions(+), 3 deletions(-)
 create mode 100644 src/openvpn/dns.c
 create mode 100644 src/openvpn/dns.h

Comments

Frank Lichtenheld March 9, 2022, 10:39 a.m. | #1
Acked-By: Frank Lichtenheld <frank@lichtenheld.com>

I have reviewed this on Github already and had no further issues.

> Heiko Hund <heiko@ist.eigentlich.net> hat am 09.03.2022 00:06 geschrieben:
> 
>  
> 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>

Grüße,
--
Frank Lichtenheld
Arne Schwabe March 9, 2022, 12:40 p.m. | #2
Am 09.03.22 um 00:06 schrieb Heiko Hund:
> +
> +bool dns_server_priority_parse(long *priority, const char *str, bool pulled);
> +struct dns_server* dns_server_get(struct dns_server **entry, long priority, struct gc_arena *gc);
> +void dns_domain_list_append(struct dns_domain **entry, char **domains, struct gc_arena *gc);
> +bool dns_server_addr_parse(struct dns_server *server, const char *addr);
> +bool dns_options_verify(int msglevel, const struct dns_options *o);
> +struct dns_options clone_dns_options(const struct dns_options o, struct gc_arena *gc);
> +void dns_options_preprocess_pull(struct dns_options *o);
> +void dns_options_postprocess_pull(struct dns_options *o);
> +void setenv_dns_options(const struct dns_options *o, struct env_set *es);
> +void show_dns_options(const struct dns_options *o);

These new functions are missing doxygen comments. We generally have the 
doxygen comments in the header files rather than in the c files.

> +--dns args
> +  Client DNS configuration to be used with the connection.

This man page section sounds like all options are supported on all 
platforms while currently that is not the case. An explicit warning etc 
that says that some options like port, dnssec, doh etc are not supported 
on certain platforms should be included and also what happens if they 
are not.

>  The ``--dns`` option will eventually obsolete the ``--dhcp-option`` directive.
> +  Until then it will add configuration at the places ``--dhcp-option`` puts it
> +  and will override it if both are given.

Is dhcp-option ignored when dns is present by newer client or are they 
mixed? This is a bit vague.


> +  long priority;

why a type that is 32 bit on some platforms and 64 bit on others? Either 
go with just int if 32 bit is enough or use long long or int64_t if we 
need 64 but long is a weird type.

> +            name_ok &= openvpn_snprintf(env_name, sizeof(env_name), "dns_server_%d_address4", i);

bitwise and with a bool feels wrong. We should use && instead of & for 
boolean logic.

> +  enum dns_security dnssec;
> +  enum dns_server_transport transport;
> +  char *sni;
> +};

That should be probably be a const char* instead of a modifable char*

>          gc_free(&o->gc);
> +        gc_free(&o->dns_options.gc);

having an extra gc in this way feels a bit strange. Since this is the 
only time it is initialised you can just as well just use o->gc instead.


> +        struct gc_arena dns_gc = gc_new();;

extra ;
> +        /* Free DNS options and reset them to pre-pull state */
> +        gc_free(&o->dns_options.gc);

There is an additional free for the gc here that has no matching 
gc_init. If your goal is to have a gc for the life time of prepull etc, 
then it is better to put it there as that is easier to see its lifetime 
than to have it only for DNS where the lifetime/ownership is not so clear.


I see also that this code makes no attempt to remove/refactor the old 
DHCP code. The only thing I see is the tuntap_options_copy_dns code. 
What the supposed way forward here? That also raises the question about 
platform specific code. Does that code now need two implementations? One 
for installing options from the dhcp structs and one from the DNS struct?

Some idea how this is going to go forward would be good.

Arne
Heiko Hund March 12, 2022, 12:50 p.m. | #3
On Mittwoch, 9. März 2022 13:40:32 CET Arne Schwabe wrote:
> Am 09.03.22 um 00:06 schrieb Heiko Hund:
> > +bool dns_server_priority_parse(long *priority, const char *str, bool
> > +[...]
> > +void show_dns_options(const struct dns_options *o);
> 
> These new functions are missing doxygen comments. We generally have the
> doxygen comments in the header files rather than in the c files.

Okay, I'll move the comments into the header file.

> > +--dns args
> > +  Client DNS configuration to be used with the connection.
> 
> This man page section sounds like all options are supported on all
> platforms while currently that is not the case. An explicit warning etc
> that says that some options like port, dnssec, doh etc are not supported
> on certain platforms should be included and also what happens if they
> are not.

Noted. What happens will likely be determined when we have the 
implementation(s) that actually take this option and try to apply it. Think it 
is too early to add this info to the manual, as it may change. Some ideas 
exist, but they have to prove themselves and survive public discussion. So far 
I have the idea that options will be ignored if they are not supported, and 
split DNS is turned into all DNS redirected to the pushed server(s) if they 
are unsupported. Again, this is very theoretical at this point and may change 
once facing reality.
 
> >  The ``--dns`` option will eventually obsolete the ``--dhcp-option``
> >  directive.> 
> > +  Until then it will add configuration at the places ``--dhcp-option``
> > puts it +  and will override it if both are given.
> 
> Is dhcp-option ignored when dns is present by newer client or are they
> mixed? This is a bit vague.

Will clarify.

> > +  long priority;
> 
> why a type that is 32 bit on some platforms and 64 bit on others? Either
> go with just int if 32 bit is enough or use long long or int64_t if we
> need 64 but long is a weird type.

int8_t would be enough, just need an extra few bits for the conversation from 
text. It is a long because strtol(3) returns a long, and I went with that. Not 
sure if casting after checks will gain much. If we decide to change this I'd 
rather use int8_t however, or a future somebody will ponder why the extra bits 
there, when the priority is really a 8 bit value. IMHO it is not worth the 
extra code, that's why I went with long. Compilers might align an 8 bit value 
anyways.

> > +            name_ok &= openvpn_snprintf(env_name, sizeof(env_name),
> > "dns_server_%d_address4", i);
> bitwise and with a bool feels wrong. We should use && instead of & for
> boolean logic.

Since there is no &&= operator, I thought it's more readable with &=. The 
result is the same because of the way bool is defined. I'll convert it to 
logical operators for the v2 and then we'll see if it looks and feels better.

> > +  enum dns_security dnssec;
> > +  enum dns_server_transport transport;
> > +  char *sni;
> > +};
> 
> That should be probably be a const char* instead of a modifable char*

Right.

> >          gc_free(&o->gc);
> > 
> > +        gc_free(&o->dns_options.gc);
> 
> having an extra gc in this way feels a bit strange. Since this is the
> only time it is initialised you can just as well just use o->gc instead.

Actually no. The --dns option is a bit special as it can partially replace 
local setting with pushed ones (i.e. override single dns servers and others 
not). The contained gc is used to allocate (and free) pulled options. If we'd 
use o->gc as well, the pulled options wouldn't be freed for a potentially long 
time, increasing the memory footprint over time.
 
> > +        struct gc_arena dns_gc = gc_new();;
> 
> extra ;

Eagle eyes! =)

> > +        /* Free DNS options and reset them to pre-pull state */
> > +        gc_free(&o->dns_options.gc);
> 
> There is an additional free for the gc here that has no matching
> gc_init. If your goal is to have a gc for the life time of prepull etc,
> then it is better to put it there as that is easier to see its lifetime
> than to have it only for DNS where the lifetime/ownership is not so clear.

The accompanying gc_init() is with the one for o->gc. Don't see how moving the 
gc into struct options_pre_connect would change anything substantially. The 
gc_init would still be done in init_options() then.

> I see also that this code makes no attempt to remove/refactor the old
> DHCP code. The only thing I see is the tuntap_options_copy_dns code.
> What the supposed way forward here? That also raises the question about
> platform specific code. Does that code now need two implementations? One
> for installing options from the dhcp structs and one from the DNS struct?
> 
> Some idea how this is going to go forward would be good.

There are several ideas. Instead of having a #ifdef hell, like for other 
platform specific code, I would prefer some sort of "dns plugin / script" way, 
where platform specific code could reside in individual units and linked / 
called on a on-demand basis. However I want the community to discuss and bless 
an approach before implementing it. Later. For now this is the first step to 
get openvpn 2 and 3 on a common basis for (pushing) dns options. dhcp-options 
will fade then, once we have proper --dns code in. If it turns out some of the 
dhcp-options are worthwhile keeping, I think it is best to introduce new 
specialized (Windows) options to handle them.

Cheers,
Heiko

Patch

diff --git a/doc/man-sections/client-options.rst b/doc/man-sections/client-options.rst
index 92a02e28..bdfe6aac 100644
--- a/doc/man-sections/client-options.rst
+++ b/doc/man-sections/client-options.rst
@@ -154,6 +154,61 @@  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.
+
+  The ``--dns`` option will eventually obsolete the ``--dhcp-option`` directive.
+  Until then it will add configuration at the places ``--dhcp-option`` puts it
+  and will override it if both are given. So, ``--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..41d7b0fa
--- /dev/null
+++ b/src/openvpn/dns.c
@@ -0,0 +1,561 @@ 
+/*
+ *  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;
+}
+
+/**
+ * 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)
+{
+    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;
+}
+
+/**
+ * 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)
+{
+    /* 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;
+    }
+}
+
+/**
+ * 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)
+{
+    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;
+}
+
+/**
+ * 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)
+{
+    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;
+    }
+}
+
+/**
+ * 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)
+{
+    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;
+}
+
+/**
+ * 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)
+{
+    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;
+}
+
+/**
+ * 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)
+{
+    o->servers_prepull = o->servers;
+    o->servers = NULL;
+}
+
+/**
+ * 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)
+{
+    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";
+    }
+}
+
+/**
+ * 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)
+{
+    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);
+        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);
+            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);
+            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);
+            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);
+            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);
+                setenv_str(es, env_name, d->name);
+            }
+        }
+
+        if (s->dnssec)
+        {
+            name_ok &= openvpn_snprintf(env_name, sizeof(env_name), "dns_server_%d_dnssec", i);
+            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);
+            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);
+            setenv_str(es, env_name, s->sni);
+        }
+    }
+
+    if (!name_ok)
+    {
+        msg(M_WARN, "WARNING: dns option setenv name buffer overflow");
+    }
+
+    gc_free(&gc);
+}
+
+/**
+ * Prints configured DNS options.
+ *
+ * @param   o           Pointer to the DNS options to print
+ */
+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..5248d345
--- /dev/null
+++ b/src/openvpn/dns.h
@@ -0,0 +1,89 @@ 
+/*
+ *  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;
+  char *sni;
+};
+
+struct dns_options {
+  struct dns_domain *search_domains;
+  struct dns_server *servers_prepull;
+  struct dns_server *servers;
+  struct gc_arena gc;
+};
+
+bool dns_server_priority_parse(long *priority, const char *str, bool pulled);
+struct dns_server* dns_server_get(struct dns_server **entry, long priority, struct gc_arena *gc);
+void dns_domain_list_append(struct dns_domain **entry, char **domains, struct gc_arena *gc);
+bool dns_server_addr_parse(struct dns_server *server, const char *addr);
+bool dns_options_verify(int msglevel, const struct dns_options *o);
+struct dns_options clone_dns_options(const struct dns_options o, struct gc_arena *gc);
+void dns_options_preprocess_pull(struct dns_options *o);
+void dns_options_postprocess_pull(struct dns_options *o);
+void setenv_dns_options(const struct dns_options *o, struct env_set *es);
+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..457c70c1 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)