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 <he...@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

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 <sa...@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 <sa...@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)
-- 
2.32.0



_______________________________________________
Openvpn-devel mailing list
Openvpn-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/openvpn-devel

Reply via email to