[Openvpn-devel,v15] Add DNS SRV remote host discovery support

Message ID 20221229072746.19169-1-themiron@yandex-team.ru
State New
Headers show
Series [Openvpn-devel,v15] Add DNS SRV remote host discovery support | expand

Commit Message

Vladislav Grishenko Dec. 29, 2022, 7:27 a.m. UTC
DNS SRV remote host discovery allows to have multiple OpenVPN servers for
a single domain w/o explicit profile enumeration, to move services from
host to host with little fuss, and to designate hosts as primary servers
for a service and others as backups.
Feature has been asked several times already, should be useful in case of
substantial number of clients & servers deployed.

Patch introduces "--remote-srv domain [service] [proto]" option.
The "service" and "proto" arguments are optional. Client will try
to resolve DNS SRV record "_service._proto.domain" and use returned
DNS SRV records as remote server list ordered by server selection
mechanism defined in RFC2782 (https://tools.ietf.org/html/rfc2782):

    A client MUST attempt to contact the target host with the
    lowest-numbered priority field value it can reach, target hosts
    with the same priority SHOULD be tried in an order defined by the
    weight field.
    The weight field specifies a relative weight for entries with the
    same priority. Larger weights SHOULD be given a proportionately
    higher probability of being selected.
    Domain administrators SHOULD use Weight 0 when there isn't any
    server selection to do. In the presence of records containing
    weights greater than 0, records with Weight 0 SHOULD have a very
    small chance of being selected.

Note: OpenVPN server selection mechanism implementation indeed will
give records with weight of zero a very small chance of being selected
first, but never skip them.

Example: instead of multiple --remote in order, now it's possible to
specify just one --remote-srv and configure DNS SRV records:

    remote-srv example.net

    name                 prio weight port target
    $ORIGIN example.net.
    _openvpn._udp IN SRV 10   60     1194 server1.example.net.
    _openvpn._udp IN SRV 10   40     1194 server2.example.net.
    _openvpn._udp IN SRV 10   0      1194 server3.example.net.
    _openvpn._tcp IN SRV 20   0       443 server4.example.net.

For "--remote-srv example.net" following will happen in order:
1. The client will first try to resolve "_openvpn._udp.example.net"
   and "_openvpn._tcp.example.net".
2. Records "server1.example.net:1194", "server2.example.net:1194"
   and "server3.example.net:1194" will be selected before record
   "server4.example.net:443" as their priority 10 is smaller than 20.
3. Records "server1.example.net:1194", "server2.example.net:1194"
   and "server3.example.net:1194" will be randomly selected with
   weight probability: first will be either "server1.example.net:1194"
   with 60% probability or "server2.example.net:1194" with 40% or
   "server3.example.net:1194" with almost zero probability.
4. If "server1.example.net:1194" was selected, the second record will
   be either "server2.example.net:1194" with almost 100% probability
   or "server3.example.net:1194" with almost 0%.
5. If "server2.example.net:1194" was selected, the third record will
   be the only last record of priority 10 - "server3.example.net:1194".
6. Record "server4.example.net:443" will be the last one selected as
   the only record with priority 20.
7. Each of the resulting "target:port" remote hosts will be resolved
   and accessed if its protocol has no conflict with the rest of the
   OpenVPN options.

  If DNS SRV name can't be resolved or no valid records were returned,
  client will move on to the next connection entry.

v15:
    rebase to master (Dec 2022)
    add optional port argument to --remote and --remote-srv usage message
    fix --proto option coexisting with --remote-srv
    fix --nobind option coexisting with --remote-srv
    fix options postprocess mutation lost in v13/v14
    recover --mtu-test handling with --remote-srv
    use explicit srv resolve stub for openbsd for the future
    fix comments

v14:
    disable SRV handling on OpenBSD due to resolver library differences

v13:
    rebase to master (Nov 2022)
    apply "'static inline', not 'inline static' style guide"
    include --explicit-exit-notify logic from commit 7953b07bf56c1df0
    formatting changes required by uncrustify
    replace last occurance of EAI_NODATA with EAI_NONAME
    OpenBSD does not have ns_c_in/ns_t_srv, need to use C_IN/T_SRV instead

v12:
    add get_cached_srv_entry() for servinfo vs addrinfo cache split
    add check for mixed --remote and --remote-srv
    add doxygen dns srv functions comments
    use query_servinfo() for both unix and windows
    fix undefined NS_MAXMSG issue on macOS
    fix undefined EAI_NODATA issue on FreeBSD
    fix man and msg() indents
    rebase against master

Signed-off-by: Vladislav Grishenko <themiron@yandex-team.ru>
---
 configure.ac                        |   2 +-
 doc/man-sections/client-options.rst | 121 +++-
 doc/management-notes.txt            |   6 +
 src/openvpn/Makefile.am             |   2 +-
 src/openvpn/buffer.h                |   5 -
 src/openvpn/errlevel.h              |   1 +
 src/openvpn/init.c                  |  79 ++-
 src/openvpn/openvpn.vcxproj         |  12 +-
 src/openvpn/options.c               | 249 +++++++-
 src/openvpn/options.h               |   4 +
 src/openvpn/socket.c                | 868 +++++++++++++++++++++++++++-
 src/openvpn/socket.h                |  67 ++-
 src/openvpn/syshead.h               |   5 +
 13 files changed, 1366 insertions(+), 55 deletions(-)

Comments

Vladislav Grishenko Dec. 29, 2022, 7:32 a.m. UTC | #1
Hi, please refer diff against v14 https://pastebin.com/XA0dWiih

--
Best Regards, Vladislav Grishenko
Frank Lichtenheld Dec. 29, 2022, 10:29 a.m. UTC | #2
On Thu, Dec 29, 2022 at 12:27:46PM +0500, Vladislav Grishenko wrote:
[...]
> v15:
>     rebase to master (Dec 2022)
>     add optional port argument to --remote and --remote-srv usage message
>     fix --proto option coexisting with --remote-srv
>     fix --nobind option coexisting with --remote-srv
>     fix options postprocess mutation lost in v13/v14
>     recover --mtu-test handling with --remote-srv
>     use explicit srv resolve stub for openbsd for the future
>     fix comments

I don't really have time today to review this completely, but a quick test
seems to confirm the issue I identified last time is indeed fixed. Tested
by replacing --remote in t_client tests 1 and 2 with --remote-srv lichtenheld.net

SRV setup for lichtenheld.net:
_openvpn._tcp	60	IN	SRV	0 0 51194 conn-test-server.openvpn.org.
_openvpn._udp	60	IN	SRV	0 0 51194 conn-test-server.openvpn.org.

Regards,
Frank Lichtenheld Dec. 29, 2022, 2:50 p.m. UTC | #3
On Thu, Dec 29, 2022 at 11:29:29AM +0100, Frank Lichtenheld wrote:
> On Thu, Dec 29, 2022 at 12:27:46PM +0500, Vladislav Grishenko wrote:
> [...]
> > v15:
> >     rebase to master (Dec 2022)
> >     add optional port argument to --remote and --remote-srv usage message
> >     fix --proto option coexisting with --remote-srv
> >     fix --nobind option coexisting with --remote-srv
> >     fix options postprocess mutation lost in v13/v14
> >     recover --mtu-test handling with --remote-srv
> >     use explicit srv resolve stub for openbsd for the future
> >     fix comments
> 
> I don't really have time today to review this completely, but a quick test
> seems to confirm the issue I identified last time is indeed fixed. Tested
> by replacing --remote in t_client tests 1 and 2 with --remote-srv lichtenheld.net
> 
> SRV setup for lichtenheld.net:
> _openvpn._tcp	60	IN	SRV	0 0 51194 conn-test-server.openvpn.org.
> _openvpn._udp	60	IN	SRV	0 0 51194 conn-test-server.openvpn.org.

Also put this through the buildbot tests. No build/test failures. 

Regards,
Gert Doering Jan. 10, 2023, 8:04 p.m. UTC | #4
Hi,

On Thu, Dec 29, 2022 at 12:27:46PM +0500, Vladislav Grishenko wrote:
>   client will move on to the next connection entry.
> 
> v15:
>     rebase to master (Dec 2022)
>     add optional port argument to --remote and --remote-srv usage message
>     fix --proto option coexisting with --remote-srv
>     fix --nobind option coexisting with --remote-srv
>     fix options postprocess mutation lost in v13/v14
>     recover --mtu-test handling with --remote-srv
>     use explicit srv resolve stub for openbsd for the future
>     fix comments

Getting close... but, alas, we need another rebase - the signal handling
fixes from Selva cause conflicts.  Can you do a v16, please?

Also, please have a very close look at the code now - it looks like
the previous rebase is now creating quite a bit of (undesired!) code
duplication.  For example, this new hunk:

+static bool
+options_postprocess_verify_ce_proto(const struct options *options,
+                                    const struct connection_entry *ce)
+{
+    int msglevel = M_WARN|M_NOPREFIX|M_OPTERR;
+
+    /*
+     * Sanity check on --local, --remote, and --ifconfig
+     */
+
+    if (proto_is_net(ce->proto)
+        && string_defined_equal(ce->local, ce->remote)
+        && string_defined_equal(ce->local_port, ce->remote_port))
+    {
+        msg(msglevel, "--remote and --local addresses are the same");
+        return false;
+    }
+
+    if (string_defined_equal(ce->remote, options->ifconfig_local)
+        || string_defined_equal(ce->remote, options->ifconfig_remote_netmask))
+    {
+        msg(msglevel,
+            "--local and --remote addresses must be distinct from --ifconfig "
+            "addresses");
+        return false;
+    }


... these checks are existing today, in options_postprocess_verify_ce(),
but your patch is not *moving* them to the new function, but *duplicating*
them.  This can happen if "git --rebase" gets confused by too many
unrelated changes, but is not the desired end state.

gert
Vladislav Grishenko Jan. 10, 2023, 8:42 p.m. UTC | #5
Hi, sure, will do.
Yes, I’ve noticed undesired code dup in v14 and have fixed everything found
in v15 rebase, same will be rechecked in v16 of course.
Thanks!

Ср, 11 янв. 2023 г. в 01:05, Gert Doering <gert@greenie.muc.de>:

> Hi,
>
> On Thu, Dec 29, 2022 at 12:27:46PM +0500, Vladislav Grishenko wrote:
> >   client will move on to the next connection entry.
> >
> > v15:
> >     rebase to master (Dec 2022)
> >     add optional port argument to --remote and --remote-srv usage message
> >     fix --proto option coexisting with --remote-srv
> >     fix --nobind option coexisting with --remote-srv
> >     fix options postprocess mutation lost in v13/v14
> >     recover --mtu-test handling with --remote-srv
> >     use explicit srv resolve stub for openbsd for the future
> >     fix comments
>
> Getting close... but, alas, we need another rebase - the signal handling
> fixes from Selva cause conflicts.  Can you do a v16, please?
>
> Also, please have a very close look at the code now - it looks like
> the previous rebase is now creating quite a bit of (undesired!) code
> duplication.  For example, this new hunk:
>
> +static bool
> +options_postprocess_verify_ce_proto(const struct options *options,
> +                                    const struct connection_entry *ce)
> +{
> +    int msglevel = M_WARN|M_NOPREFIX|M_OPTERR;
> +
> +    /*
> +     * Sanity check on --local, --remote, and --ifconfig
> +     */
> +
> +    if (proto_is_net(ce->proto)
> +        && string_defined_equal(ce->local, ce->remote)
> +        && string_defined_equal(ce->local_port, ce->remote_port))
> +    {
> +        msg(msglevel, "--remote and --local addresses are the same");
> +        return false;
> +    }
> +
> +    if (string_defined_equal(ce->remote, options->ifconfig_local)
> +        || string_defined_equal(ce->remote,
> options->ifconfig_remote_netmask))
> +    {
> +        msg(msglevel,
> +            "--local and --remote addresses must be distinct from
> --ifconfig "
> +            "addresses");
> +        return false;
> +    }
>
>
> ... these checks are existing today, in options_postprocess_verify_ce(),
> but your patch is not *moving* them to the new function, but *duplicating*
> them.  This can happen if "git --rebase" gets confused by too many
> unrelated changes, but is not the desired end state.
>
> gert
> --
> "If was one thing all people took for granted, was conviction that if you
>  feed honest figures into a computer, honest figures come out. Never
> doubted
>  it myself till I met a computer with a sense of humor."
>                              Robert A. Heinlein, The Moon is a Harsh
> Mistress
>
> Gert Doering - Munich, Germany
> gert@greenie.muc.de
> _______________________________________________
> Openvpn-devel mailing list
> Openvpn-devel@lists.sourceforge.net
> https://lists.sourceforge.net/lists/listinfo/openvpn-devel
>
Gert Doering Jan. 31, 2023, 4:16 p.m. UTC | #6
Hi,

On Wed, Jan 11, 2023 at 01:42:31AM +0500, Vladislav Grishenko wrote:
> Hi, sure, will do.
> Yes, I???ve noticed undesired code dup in v14 and have fixed everything found
> in v15 rebase, same will be rechecked in v16 of course.

Did you find time to have a look into this?

2.6.0 is out, but since there is demand for the SRV patch, we've agreed
to do an exceptional feature merge for 2.6.1 for SRV and dynamic tls-crypt
- but that means "we need to have a fully reviewed v16"...

gert

Patch

diff --git a/configure.ac b/configure.ac
index be31889e..27bd2eae 100644
--- a/configure.ac
+++ b/configure.ac
@@ -468,7 +468,7 @@  SOCKET_INCLUDES="
 "
 
 AC_CHECK_HEADERS(
-	[net/if.h netinet/ip.h resolv.h sys/un.h net/if_utun.h sys/kern_control.h],
+	[net/if.h netinet/ip.h arpa/nameser.h resolv.h sys/un.h net/if_utun.h sys/kern_control.h],
 	,
 	,
 	[[${SOCKET_INCLUDES}]]
diff --git a/doc/man-sections/client-options.rst b/doc/man-sections/client-options.rst
index 974cc992..980aa8bf 100644
--- a/doc/man-sections/client-options.rst
+++ b/doc/man-sections/client-options.rst
@@ -469,10 +469,121 @@  configuration.
   specification (4/6 suffix), OpenVPN will try both IPv4 and IPv6
   addresses, in the order getaddrinfo() returns them.
 
+--remote-srv args
+  Remote DNS SRV domain, service name and protocol.
+
+  Valid syntaxes:
+  ::
+
+     remote-srv domain
+     remote-srv domain service
+     remote-srv domain service proto
+
+  The ``service`` and ``proto`` arguments are optional. Client will try
+  to resolve DNS SRV record ``_service._proto.domain`` and use returned
+  DNS SRV records as remote server list ordered by server selection
+  mechanism defined in `RFC 2782 <https://tools.ietf.org/html/rfc2782>`_:
+  ::
+
+    A client MUST attempt to contact the target host with the
+    lowest-numbered priority field value it can reach, target hosts
+    with the same priority SHOULD be tried in an order defined by the
+    weight field.
+    The weight field specifies a relative weight for entries with the
+    same priority. Larger weights SHOULD be given a proportionately
+    higher probability of being selected.
+    Domain administrators SHOULD use Weight 0 when there isn't any
+    server selection to do. In the presence of records containing
+    weights greater than 0, records with Weight 0 SHOULD have a very
+    small chance of being selected.
+
+  *Note:*
+    OpenVPN server selection mechanism implementation indeed will give
+    records with weight of zero a very small chance of being selected
+    first, but never skip them.
+
+  The ``service`` argument indicates the symbolic name of the desired
+  service. By default service is the registered service name
+  :code:`openvpn`.
+
+  The ``proto`` argument indicates the symbolic name of the desired
+  protocol and the protocol to use when connecting with the remote, and
+  may either be :code:`tcp`, :code:`udp` or special value :code:`auto`
+  used by default to try both. In this case all the discovered remote
+  hosts will be ordered by server selection mechanism regardless their
+  protocol. To enforce IPv4 or IPv6 connections add a :code:`4` or
+  :code:`6` suffix; like :code:`udp4` / :code:`udp6` / :code:`tcp4` /
+  :code:`tcp6` / :code:`auto4` / :code:`auto6`.
+
+  On the client, multiple ``--remote-srv`` options may be specified for
+  redundancy, each referring to a different DNS SRV record name, in the
+  order specified by the list of ``--remote-srv`` options. Specifying
+  multiple ``--remote-srv`` options for this purpose is a special case
+  of the more general connection-profile feature. See the
+  ``<connection>`` documentation below.
+
+  The client will move on to the next DNS SRV record in the ordered list,
+  in the event of connection failure. Note that at any given time, the
+  OpenVPN client will at most be connected to one server.
+
+  If particular DNS SRV record next resolves to multiple IP addresses,
+  OpenVPN will try them in the order that the system getaddrinfo()
+  presents them, so prioritization and DNS randomization is done by the
+  system library. Unless an IP version is forced by the protocol
+  specification (4/6 suffix), OpenVPN will try both IPv4 and IPv6
+  addresses, in the order getaddrinfo() returns them.
+
+  Examples:
+  ::
+
+     remote-srv example.net
+     remote-srv example.net openvpn
+     remote-srv example.net openvpn tcp
+
+  Example of DNS SRV records:
+  ::
+
+     name                 prio weight port target
+     $ORIGIN example.net.
+     _openvpn._udp IN SRV 10   60     1194 server1.example.net.
+     _openvpn._udp IN SRV 10   40     1194 server2.example.net.
+     _openvpn._udp IN SRV 10   0      1194 server3.example.net.
+     _openvpn._tcp IN SRV 20   0       443 server4.example.net.
+
+  For ``--remote-srv example.net`` following will happen in order:
+
+  1. The client will first try to resolve ``_openvpn._udp.example.net``
+     and ``_openvpn._tcp.example.net``.
+  2. Records ``server1.example.net:1194``, ``server2.example.net:1194``
+     and ``server3.example.net:1194`` will be selected before record
+     ``server4.example.net:443`` as their priority 10 is smaller than 20.
+  3. Records ``server1.example.net:1194``, ``server2.example.net:1194``
+     and ``server3.example.net:1194`` will be randomly selected with
+     weight probability: first will be either ``server1.example.net:1194``
+     with 60% probability or ``server2.example.net:1194`` with 40% or
+     ``server3.example.net:1194`` with almost zero probability.
+  4. If ``server1.example.net:1194`` was selected, the second record will
+     be either ``server2.example.net:1194`` with almost 100% probability
+     or ``server3.example.net:1194`` with almost 0%.
+  5. If ``server2.example.net:1194`` was selected, the third record will
+     be the only last record of priority 10 - ``server3.example.net:1194``.
+  6. Record ``server4.example.net:443`` will be the last one selected as
+     the only record with priority 20.
+  7. Each of the resulting ```target:port`` remote hosts will be resolved
+     and accessed if its protocol has no conflict with the rest of the
+     OpenVPN options.
+
+  If a DNS SRV name cannot be resolved or no valid records are returned,
+  the client will move on to the next connection entry.
+
+  For more information on DNS SRV see https://tools.ietf.org/html/rfc2782
+
 --remote-random
-  When multiple ``--remote`` address/ports are specified, or if connection
-  profiles are being used, initially randomize the order of the list as a
-  kind of basic load-balancing measure.
+  Randomises the order of ``--remote-srv``, ``remote-srv`` and connection
+  profiles on start. This randomisation of these entries is a kind of basic
+  load-balancing measure. Note that implementing DNS SRV entries (with a
+  short TTL if it should be dynamic) and using ``--remote-srv`` is a better
+  alternative for client-side load balancing.
 
 --remote-random-hostname
   Prepend a random string (6 bytes, 12 hex characters) to hostname to
@@ -480,8 +591,8 @@  configuration.
   "<random-chars>.foo.bar.gov".
 
 --resolv-retry n
-  If hostname resolve fails for ``--remote``, retry resolve for ``n``
-  seconds before failing.
+  If hostname resolve fails for ``--remote`` or ``--remote-srv``, retry
+  resolve for ``n`` seconds before failing.
 
   Set ``n`` to :code:`infinite` to retry indefinitely.
 
diff --git a/doc/management-notes.txt b/doc/management-notes.txt
index 6daa811a..487d38b4 100644
--- a/doc/management-notes.txt
+++ b/doc/management-notes.txt
@@ -867,6 +867,12 @@  the above notification, use this command:
 
   remote MOD vpn.otherexample.com 1234
 
+DNS SRV discovery domain and service name with protocol code:`auto`
+can't be overriden, but either accepted or skipped only.
+Example of such notification:
+
+  >REMOTE:example.com,openvpn,auto
+
 To accept the same host and port as the client would ordinarily
 have connected to, use this command:
 
diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am
index e80a35ab..faf8fea0 100644
--- a/src/openvpn/Makefile.am
+++ b/src/openvpn/Makefile.am
@@ -155,5 +155,5 @@  openvpn_LDADD = \
 	$(OPTIONAL_INOTIFY_LIBS)
 if WIN32
 openvpn_SOURCES += openvpn_win32_resources.rc block_dns.c block_dns.h ring_buffer.h
-openvpn_LDADD += -lgdi32 -lws2_32 -lwininet -lcrypt32 -liphlpapi -lwinmm -lfwpuclnt -lrpcrt4 -lncrypt -lsetupapi -lbcrypt
+openvpn_LDADD += -lgdi32 -lws2_32 -lwininet -lcrypt32 -liphlpapi -lwinmm -lfwpuclnt -lrpcrt4 -lncrypt -lsetupapi -lbcrypt -ldnsapi
 endif
diff --git a/src/openvpn/buffer.h b/src/openvpn/buffer.h
index 185226f7..f4ea6ced 100644
--- a/src/openvpn/buffer.h
+++ b/src/openvpn/buffer.h
@@ -211,11 +211,6 @@  bool buf_init_debug(struct buffer *buf, int offset, const char *file, int line);
 
 
 /* inline functions */
-static inline void
-gc_freeaddrinfo_callback(void *addr)
-{
-    freeaddrinfo((struct addrinfo *) addr);
-}
 
 /** Return an empty struct buffer */
 static inline struct buffer
diff --git a/src/openvpn/errlevel.h b/src/openvpn/errlevel.h
index 5bb1e65e..3b8a7c30 100644
--- a/src/openvpn/errlevel.h
+++ b/src/openvpn/errlevel.h
@@ -90,6 +90,7 @@ 
 #define D_ROUTE_QUOTA        LOGLEV(3, 42, 0)        /* show route quota exceeded messages */
 #define D_OSBUF              LOGLEV(3, 43, 0)        /* show socket/tun/tap buffer sizes */
 #define D_PS_PROXY           LOGLEV(3, 44, 0)        /* messages related to --port-share option */
+#define D_RESOLVE            LOGLEV(3, 46, 0)        /* show hostname resolve info */
 #define D_IFCONFIG           LOGLEV(3, 0,  0)        /* show ifconfig info (don't mute) */
 #define D_DCO                LOGLEV(3, 0, 0)         /* show DCO related messages */
 
diff --git a/src/openvpn/init.c b/src/openvpn/init.c
index 2e95256c..b351530c 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -393,7 +393,12 @@  management_callback_remote_cmd(void *arg, const char **p)
         }
         else if (!strcmp(p[1], "MOD") && p[2] && p[3])
         {
-            if (strlen(p[2]) < RH_HOST_LEN && strlen(p[3]) < RH_PORT_LEN)
+            if (ce->remote_srv && ce->proto == PROTO_AUTO)
+            {
+                /* can't mutate --remote-srv into --remote without protocol */
+                ret = false;
+            }
+            else if (strlen(p[2]) < RH_HOST_LEN && strlen(p[3]) < RH_PORT_LEN)
             {
                 struct remote_host_store *rhs = c->options.rh_store;
                 if (!rhs)
@@ -406,6 +411,7 @@  management_callback_remote_cmd(void *arg, const char **p)
 
                 ce->remote = rhs->host;
                 ce->remote_port = rhs->port;
+                ce->remote_srv = false;
                 flags = CE_MAN_QUERY_REMOTE_MOD;
                 ret = true;
             }
@@ -505,6 +511,23 @@  clear_remote_addrlist(struct link_socket_addr *lsa, bool free)
     lsa->current_remote = NULL;
 }
 
+/*
+ * Clear the remote service list
+ */
+static void
+clear_remote_servlist(struct link_socket_addr *lsa, bool free)
+{
+    if (lsa->service_list && free)
+    {
+        freeservinfo(lsa->service_list);
+    }
+    lsa->service_list = NULL;
+    lsa->current_service = NULL;
+
+    /* clear addrinfo objects as well */
+    clear_remote_addrlist(lsa, free);
+}
+
 /*
  * Increment to next connection entry
  */
@@ -534,6 +557,24 @@  next_connection_entry(struct context *c)
                 c->c1.link_socket_addr.current_remote =
                     c->c1.link_socket_addr.current_remote->ai_next;
             }
+            /* Check if there is another resolved service to try for
+             * the current connection unless persist-remote-ip was
+             * requested and current service already has an address */
+            else if (c->c1.link_socket_addr.current_service
+                     && c->c1.link_socket_addr.current_service->next
+                     && !(c->options.persist_remote_ip
+                          && c->c1.link_socket_addr.remote_list))
+            {
+                c->c1.link_socket_addr.current_service =
+                    c->c1.link_socket_addr.current_service->next;
+
+                /* Clear addrinfo object of the previous service */
+                if (c->c1.link_socket_addr.remote_list)
+                {
+                    clear_remote_addrlist(&c->c1.link_socket_addr,
+                                          !c->options.resolve_in_advance);
+                }
+            }
             else
             {
                 c->options.advance_next_remote = false;
@@ -543,20 +584,24 @@  next_connection_entry(struct context *c)
                  */
                 if (!c->options.persist_remote_ip)
                 {
-                    /* Connection entry addrinfo objects might have been
+                    /* Connection entry addr/servinfo objects might have been
                      * resolved earlier but the entry itself might have been
-                     * skipped by management on the previous loop.
-                     * If so, clear the addrinfo objects as close_instance does
+                     * skipped on the previous loop either by management or
+                     * due inappropriate service protocol.
+                     * Clear the addr/servinfo objects as close_instance does.
                      */
-                    if (c->c1.link_socket_addr.remote_list)
+                    if (c->c1.link_socket_addr.remote_list
+                        || c->c1.link_socket_addr.service_list)
                     {
-                        clear_remote_addrlist(&c->c1.link_socket_addr,
+                        clear_remote_servlist(&c->c1.link_socket_addr,
                                               !c->options.resolve_in_advance);
                     }
 
                     /* close_instance should have cleared the addrinfo objects */
                     ASSERT(c->c1.link_socket_addr.current_remote == NULL);
                     ASSERT(c->c1.link_socket_addr.remote_list == NULL);
+                    ASSERT(c->c1.link_socket_addr.current_service == NULL);
+                    ASSERT(c->c1.link_socket_addr.service_list == NULL);
                 }
                 else
                 {
@@ -603,6 +648,12 @@  next_connection_entry(struct context *c)
         }
 
         c->options.ce = *ce;
+        if (ce_defined && c->c1.link_socket_addr.current_service)
+        {
+            /* map in current service */
+            struct servinfo *si = c->c1.link_socket_addr.current_service;
+            ce_defined = options_mutate_ce_servinfo(&c->options, si);
+        }
 #ifdef ENABLE_MANAGEMENT
         if (ce_defined && management && management_query_remote_enabled(management))
         {
@@ -3771,10 +3822,13 @@  do_close_link_socket(struct context *c)
                ( c->sig->source != SIG_SOURCE_HARD
                  && ((c->c1.link_socket_addr.current_remote
                       && c->c1.link_socket_addr.current_remote->ai_next)
+                     || (c->c1.link_socket_addr.current_service
+                         && c->c1.link_socket_addr.current_service->next)
                      || c->options.no_advance))
                )))
     {
-        clear_remote_addrlist(&c->c1.link_socket_addr, !c->options.resolve_in_advance);
+        clear_remote_servlist(&c->c1.link_socket_addr,
+                              !c->options.resolve_in_advance);
     }
 
     /* Clear the remote actual address when persist_remote_ip is not in use */
@@ -4319,6 +4373,17 @@  init_instance(struct context *c, const struct env_set *env, const unsigned int f
     /* map in current connection entry */
     next_connection_entry(c);
 
+    /* map in current remote service */
+    if (c->options.ce.remote_srv)
+    {
+        do_resolve_service(c);
+        if (IS_SIG(c))
+        {
+            goto sig;
+        }
+        update_options_ce_post(&c->options);
+    }
+
     /* link_socket_mode allows CM_CHILD_TCP
      * instances to inherit acceptable fds
      * from a top-level parent */
diff --git a/src/openvpn/openvpn.vcxproj b/src/openvpn/openvpn.vcxproj
index 13755dae..970de3f3 100644
--- a/src/openvpn/openvpn.vcxproj
+++ b/src/openvpn/openvpn.vcxproj
@@ -166,7 +166,7 @@ 
     </ClCompile>
     <ResourceCompile />
     <Link>
-      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;Advapi32.lib</AdditionalDependencies>
+      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;dnsapi.lib;Advapi32.lib</AdditionalDependencies>
       <AdditionalLibraryDirectories>$(OPENSSL_HOME)/lib;$(LZO_HOME)/lib;$(PKCS11H_HOME)/lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
       <SubSystem>Console</SubSystem>
     </Link>
@@ -183,7 +183,7 @@ 
     </ClCompile>
     <ResourceCompile />
     <Link>
-      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;Advapi32.lib</AdditionalDependencies>
+      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;dnsapi.lib;Advapi32.lib</AdditionalDependencies>
       <AdditionalLibraryDirectories>$(OPENSSL_HOME)/lib;$(LZO_HOME)/lib;$(PKCS11H_HOME)/lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
       <SubSystem>Console</SubSystem>
     </Link>
@@ -200,7 +200,7 @@ 
     </ClCompile>
     <ResourceCompile />
     <Link>
-      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;Advapi32.lib</AdditionalDependencies>
+      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;dnsapi.lib;Advapi32.lib</AdditionalDependencies>
       <AdditionalLibraryDirectories>$(OPENSSL_HOME)/lib;$(LZO_HOME)/lib;$(PKCS11H_HOME)/lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
       <SubSystem>Console</SubSystem>
     </Link>
@@ -218,7 +218,7 @@ 
     </ClCompile>
     <ResourceCompile />
     <Link>
-      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;Advapi32.lib</AdditionalDependencies>
+      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;dnsapi.lib;Advapi32.lib</AdditionalDependencies>
       <AdditionalLibraryDirectories>$(OPENSSL_HOME)/lib;$(LZO_HOME)/lib;$(PKCS11H_HOME)/lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
       <SubSystem>Console</SubSystem>
       <CETCompat>true</CETCompat>
@@ -237,7 +237,7 @@ 
     </ClCompile>
     <ResourceCompile />
     <Link>
-      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;Advapi32.lib</AdditionalDependencies>
+      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;dnsapi.lib;Advapi32.lib</AdditionalDependencies>
       <AdditionalLibraryDirectories>$(OPENSSL_HOME)/lib;$(LZO_HOME)/lib;$(PKCS11H_HOME)/lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
       <SubSystem>Console</SubSystem>
       <CETCompat>true</CETCompat>
@@ -256,7 +256,7 @@ 
     </ClCompile>
     <ResourceCompile />
     <Link>
-      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;Advapi32.lib</AdditionalDependencies>
+      <AdditionalDependencies>Ncrypt.lib;gdi32.lib;ws2_32.lib;wininet.lib;crypt32.lib;iphlpapi.lib;winmm.lib;Fwpuclnt.lib;Rpcrt4.lib;setupapi.lib;dnsapi.lib;Advapi32.lib</AdditionalDependencies>
       <AdditionalLibraryDirectories>$(OPENSSL_HOME)/lib;$(LZO_HOME)/lib;$(PKCS11H_HOME)/lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
       <SubSystem>Console</SubSystem>
     </Link>
diff --git a/src/openvpn/options.c b/src/openvpn/options.c
index e454b2ac..018aa89d 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -127,8 +127,12 @@  static const char usage_message[] =
     "\n"
     "Tunnel Options:\n"
     "--local host    : Local host name or ip address. Implies --bind.\n"
-    "--remote host [port] : Remote host name or ip address.\n"
-    "--remote-random : If multiple --remote options specified, choose one randomly.\n"
+    "--remote host [port] [proto] : Remote host name or ip address.\n"
+#if !defined(TARGET_OPENBSD)
+    "--remote-srv domain [service] [proto] : Perform DNS SRV remote host discovery.\n"
+#endif
+    "--remote-random : If multiple --remote or --remote-srv options specified,\n"
+    "                  choose one randomly.\n"
     "--remote-random-hostname : Add a random string to remote DNS name.\n"
     "--mode m        : Major mode, m = 'p2p' (default, point-to-point) or 'server'.\n"
     "--proto p       : Use protocol p for communicating with peer.\n"
@@ -161,7 +165,7 @@  static const char usage_message[] =
     "                  up is a file containing username/password on 2 lines, or\n"
     "                  'stdin' to prompt for console.\n"
     "--socks-proxy-retry : Retry indefinitely on Socks proxy errors.\n"
-    "--resolv-retry n: If hostname resolve fails for --remote, retry\n"
+    "--resolv-retry n: If hostname resolve fails for --remote or --remote-srv, retry\n"
     "                  resolve for n seconds before failing (disabled by default).\n"
     "                  Set n=\"infinite\" to retry indefinitely.\n"
     "--float         : Allow remote to change its IP address/port, such as through\n"
@@ -812,7 +816,7 @@  init_options(struct options *o, const bool init_gc)
     }
     o->mode = MODE_POINT_TO_POINT;
     o->topology = TOP_NET30;
-    o->ce.proto = PROTO_UDP;
+    o->ce.proto = -1;
     o->ce.af = AF_UNSPEC;
     o->ce.bind_ipv6_only = false;
     o->ce.connect_retry_seconds = 1;
@@ -1703,6 +1707,7 @@  show_connection_entry(const struct connection_entry *o)
     SHOW_STR(local_port);
     SHOW_STR(remote);
     SHOW_STR(remote_port);
+    SHOW_BOOL(remote_srv);
     SHOW_BOOL(remote_float);
     SHOW_BOOL(bind_defined);
     SHOW_BOOL(bind_local);
@@ -2268,6 +2273,70 @@  connection_entry_load_re(struct connection_entry *ce, const struct remote_entry
     {
         ce->af = re->af;
     }
+    ce->remote_srv = re->remote_srv;
+}
+
+/* Part of options_postprocess_verify_ce that can be used
+ * in runtime after connection entry proto/hostname change.
+ */
+static bool
+options_postprocess_verify_ce_proto(const struct options *options,
+                                    const struct connection_entry *ce)
+{
+    int msglevel = M_WARN|M_NOPREFIX|M_OPTERR;
+
+    /*
+     * Sanity check on --local, --remote, and --ifconfig
+     */
+
+    if (proto_is_net(ce->proto)
+        && string_defined_equal(ce->local, ce->remote)
+        && string_defined_equal(ce->local_port, ce->remote_port))
+    {
+        msg(msglevel, "--remote and --local addresses are the same");
+        return false;
+    }
+
+    if (string_defined_equal(ce->remote, options->ifconfig_local)
+        || string_defined_equal(ce->remote, options->ifconfig_remote_netmask))
+    {
+        msg(msglevel,
+            "--local and --remote addresses must be distinct from --ifconfig "
+            "addresses");
+        return false;
+    }
+
+    /*
+     * Sanity check on MTU parameters
+     */
+
+    if (!proto_is_udp(ce->proto) && options->mtu_test)
+    {
+        msg(msglevel, "--mtu-test only makes sense with --proto udp");
+        return false;
+    }
+
+    /*
+     * Check that protocol options make sense.
+     */
+
+#ifdef ENABLE_FRAGMENT
+    if (!proto_is_udp(ce->proto) && ce->fragment)
+    {
+        msg(msglevel, "--fragment can only be used with --proto udp");
+        return false;
+    }
+#endif
+
+    if ((ce->http_proxy_options) && ce->proto != PROTO_TCP_CLIENT)
+    {
+        msg(msglevel,
+            "--http-proxy MUST be used in TCP Client mode (i.e. --proto "
+            "tcp-client)");
+        return false;
+    }
+
+    return true;
 }
 
 static void
@@ -2344,6 +2413,11 @@  options_postprocess_verify_ce(const struct options *options,
             "--proto tcp-server or --proto tcp-client");
     }
 
+    if (ce->proto == PROTO_AUTO && !ce->remote_srv)
+    {
+        msg(M_USAGE, "--proto auto can be only used with --remote-srv");
+    }
+
     if (options->lladdr && dev != DEV_TYPE_TAP)
     {
         msg(M_USAGE, "--lladdr can only be used in --dev tap mode");
@@ -2357,7 +2431,9 @@  options_postprocess_verify_ce(const struct options *options,
         msg(M_USAGE, "only one of --tun-mtu or --link-mtu may be defined");
     }
 
-    if (!proto_is_udp(ce->proto) && options->mtu_test)
+    /* Defer validation for --remote-srv with auto protocol */
+    if (!proto_is_udp(ce->proto) && options->mtu_test
+        && !(ce->remote_srv && ce->proto == PROTO_AUTO))
     {
         msg(M_USAGE, "--mtu-test only makes sense with --proto udp");
     }
@@ -2369,6 +2445,11 @@  options_postprocess_verify_ce(const struct options *options,
      * Sanity check on --local, --remote, and --ifconfig
      */
 
+    if (ce->remote_srv && options->ip_remote_hint)
+    {
+        msg(M_USAGE, "--ip-remote-hint can't be used with --remote-srv");
+    }
+
     if (proto_is_net(ce->proto)
         && string_defined_equal(ce->local, ce->remote)
         && string_defined_equal(ce->local_port, ce->remote_port))
@@ -2415,9 +2496,10 @@  options_postprocess_verify_ce(const struct options *options,
             "--lport and --nobind don't make sense when used together");
     }
 
-    if (!ce->remote && !ce->bind_local)
+    if (!ce->remote && !ce->remote_srv && !ce->bind_local)
     {
-        msg(M_USAGE, "--nobind doesn't make sense unless used with --remote");
+        msg(M_USAGE, "--nobind doesn't make sense unless used with --remote "
+            "or --remote-srv");
     }
 
     /*
@@ -2492,18 +2574,22 @@  options_postprocess_verify_ce(const struct options *options,
      */
 
 #ifdef ENABLE_FRAGMENT
-    if (!proto_is_udp(ce->proto) && ce->fragment)
+    /* Defer validation for --remote-srv with auto protocol */
+    if (!proto_is_udp(ce->proto) && ce->fragment
+        && !(ce->remote_srv && ce->proto == PROTO_AUTO))
     {
         msg(M_USAGE, "--fragment can only be used with --proto udp");
     }
 #endif
 
-    if (!ce->remote && ce->proto == PROTO_TCP_CLIENT)
+    if (!ce->remote && !ce->remote_srv && ce->proto == PROTO_TCP_CLIENT)
     {
-        msg(M_USAGE, "--remote MUST be used in TCP Client mode");
+        msg(M_USAGE, "--remote or --remote-srv MUST be used in TCP Client mode");
     }
 
-    if ((ce->http_proxy_options) && ce->proto != PROTO_TCP_CLIENT)
+    /* Defer validation for --remote-srv with auto protocol */
+    if ((ce->http_proxy_options) && ce->proto != PROTO_TCP_CLIENT
+        && !(ce->remote_srv && ce->proto == PROTO_AUTO))
     {
         msg(M_USAGE,
             "--http-proxy MUST be used in TCP Client mode (i.e. --proto "
@@ -2574,6 +2660,10 @@  options_postprocess_verify_ce(const struct options *options,
         {
             msg(M_USAGE, "--remote cannot be used with --mode server");
         }
+        if (ce->remote_srv)
+        {
+            msg(M_USAGE, "--remote-srv cannot be used with --mode server");
+        }
         if (!ce->bind_local)
         {
             msg(M_USAGE, "--nobind cannot be used with --mode server");
@@ -3086,10 +3176,14 @@  options_postprocess_verify_ce(const struct options *options,
     uninit_options(&defaults);
 }
 
-static void
-options_postprocess_mutate_ce(struct options *o, struct connection_entry *ce)
+/* Part of options_postprocess_mutate_ce that can be used
+ * in runtime after connection entry proto/hostname change.
+ */
+static bool
+options_postprocess_mutate_ce_proto(struct options *o,
+                                    struct connection_entry *ce)
 {
-    const int dev = dev_type_enum(o->dev, o->dev_type);
+    bool result = true;
 
     if (o->server_defined || o->server_bridge_defined || o->server_bridge_proxy_dhcp)
     {
@@ -3129,9 +3223,10 @@  options_postprocess_mutate_ce(struct options *o, struct connection_entry *ce)
     /* if protocol forcing is enabled, disable all protocols
      * except for the forced one
      */
-    if (o->proto_force >= 0 && o->proto_force != ce->proto)
+    if (o->proto_force >= 0 && o->proto_force != ce->proto
+        && !(ce->remote_srv && ce->proto == PROTO_AUTO))
     {
-        ce->flags |= CE_DISABLED;
+        result = false;
     }
 
     /* our socks code is not fully IPv6 enabled yet (TCP works, UDP not)
@@ -3142,16 +3237,43 @@  options_postprocess_mutate_ce(struct options *o, struct connection_entry *ce)
         if (ce->af == AF_INET6)
         {
             msg(M_INFO, "WARNING: '--proto udp6' is not compatible with "
-                "'--socks-proxy' today.  Forcing IPv4 mode." );
+                "'--socks-proxy' today. Forcing IPv4 mode.");
         }
         else
         {
             msg(M_INFO, "NOTICE: dual-stack mode for '--proto udp' does not "
-                "work correctly with '--socks-proxy' today.  Forcing IPv4." );
+                "work correctly with '--socks-proxy' today. Forcing IPv4.");
         }
         ce->af = AF_INET;
     }
 
+    if (!proto_is_udp(ce->proto) && ce->explicit_exit_notification)
+    {
+        msg(M_WARN, "NOTICE: --explicit-exit-notify ignored for --proto tcp");
+        ce->explicit_exit_notification = 0;
+    }
+
+    return result;
+}
+
+static void
+options_postprocess_mutate_ce(struct options *o, struct connection_entry *ce)
+{
+    const int dev = dev_type_enum(o->dev, o->dev_type);
+
+    /*
+     * Set protocol defaults
+     */
+    if (ce->proto < 0)
+    {
+        ce->proto = ce->remote_srv ? PROTO_AUTO : PROTO_UDP;
+    }
+
+    if (!options_postprocess_mutate_ce_proto(o, ce))
+    {
+        ce->flags |= CE_DISABLED;
+    }
+
     /*
      * Set MTU defaults
      */
@@ -3233,12 +3355,32 @@  options_postprocess_mutate_ce(struct options *o, struct connection_entry *ce)
         connection_entry_preload_key(&ce->tls_crypt_v2_file,
                                      &ce->tls_crypt_v2_file_inline, &o->gc);
     }
+}
 
-    if (!proto_is_udp(ce->proto) && ce->explicit_exit_notification)
+/*
+ * Merges servinfo's hostname, servname and proto into current connection
+ * entry if possible and doesn't conflict with the options.
+ * Used by runtime DNS SRV discovery.
+ */
+bool
+options_mutate_ce_servinfo(struct options *o, struct servinfo *si)
+{
+    struct connection_entry ce = o->ce;
+
+    ASSERT(ce.remote_srv);
+
+    ce.remote = si->hostname;
+    ce.remote_port = si->servname;
+    ce.proto = si->proto;
+
+    if (options_postprocess_mutate_ce_proto(o, &ce)
+        && options_postprocess_verify_ce_proto(o, &ce))
     {
-        msg(M_WARN, "NOTICE: --explicit-exit-notify ignored for --proto tcp");
-        ce->explicit_exit_notification = 0;
+        o->ce = ce;
+        return true;
     }
+
+    return false;
 }
 
 #ifdef _WIN32
@@ -6086,7 +6228,8 @@  add_option(struct options *options,
                                OPT_P_CONNECTION, option_types_found, es);
             if (!sub.ce.remote)
             {
-                msg(msglevel, "Each 'connection' block must contain exactly one 'remote' directive");
+                msg(msglevel, "Each 'connection' block must contain exactly one 'remote' "
+                    "or 'remote-srv' directive");
                 uninit_options(&sub);
                 goto err;
             }
@@ -6164,8 +6307,9 @@  add_option(struct options *options,
     {
         struct remote_entry re;
         re.remote = re.remote_port = NULL;
+        re.remote_srv = false;
         re.proto = -1;
-        re.af = 0;
+        re.af = AF_UNSPEC;
 
         VERIFY_PERMISSION(OPT_P_GENERAL|OPT_P_CONNECTION);
         re.remote = p[1];
@@ -6198,9 +6342,66 @@  add_option(struct options *options,
         }
         else if (permission_mask & OPT_P_CONNECTION)
         {
+            if (options->ce.remote && options->ce.remote_srv)
+            {
+                msg(msglevel, "Each 'connection' block must contain exactly one 'remote' "
+                    "or 'remote-srv' directive");
+                goto err;
+            }
+            connection_entry_load_re(&options->ce, &re);
+        }
+    }
+#if !defined(TARGET_OPENBSD)
+    else if (streq(p[0], "remote-srv") && p[1] && !p[4])
+    {
+        struct remote_entry re;
+        re.remote = NULL;
+        re.remote_port = OPENVPN_SERVICE;
+        re.remote_srv = true;
+        re.proto = -1;
+        re.af = AF_UNSPEC;
+
+        VERIFY_PERMISSION(OPT_P_GENERAL|OPT_P_CONNECTION);
+        re.remote = p[1];
+        if (p[2])
+        {
+            re.remote_port = p[2];
+            if (p[3])
+            {
+                const int proto = ascii2proto(p[3]);
+                const sa_family_t af = ascii2af(p[3]);
+                if (proto < 0)
+                {
+                    msg(msglevel,
+                        "remote-srv: bad protocol associated with domain %s: "
+                        "'%s'", p[1], p[3]);
+                    goto err;
+                }
+                re.proto = proto;
+                re.af = af;
+            }
+        }
+        if (permission_mask & OPT_P_GENERAL)
+        {
+            struct remote_entry *e = alloc_remote_entry(options, msglevel);
+            if (!e)
+            {
+                goto err;
+            }
+            *e = re;
+        }
+        else if (permission_mask & OPT_P_CONNECTION)
+        {
+            if (options->ce.remote && !options->ce.remote_srv)
+            {
+                msg(msglevel, "Each 'connection' block must contain exactly one 'remote' "
+                    "or 'remote-srv' directive");
+                goto err;
+            }
             connection_entry_load_re(&options->ce, &re);
         }
     }
+#endif /* if !defined(TARGET_OPENBSD) */
     else if (streq(p[0], "resolv-retry") && p[1] && !p[2])
     {
         VERIFY_PERMISSION(OPT_P_GENERAL);
@@ -6715,7 +6916,7 @@  add_option(struct options *options,
         int proto_force;
         VERIFY_PERMISSION(OPT_P_GENERAL);
         proto_force = ascii2proto(p[1]);
-        if (proto_force < 0)
+        if (proto_force < 0 || proto_force == PROTO_AUTO)
         {
             msg(msglevel, "Bad --proto-force protocol: '%s'", p[1]);
             goto err;
diff --git a/src/openvpn/options.h b/src/openvpn/options.h
index fec1eace..10b59e13 100644
--- a/src/openvpn/options.h
+++ b/src/openvpn/options.h
@@ -105,6 +105,7 @@  struct connection_entry
     const char *remote_port;
     const char *local;
     const char *remote;
+    bool remote_srv;
     bool remote_float;
     bool bind_defined;
     bool bind_ipv6_only;
@@ -177,6 +178,7 @@  struct remote_entry
     const char *remote_port;
     int proto;
     sa_family_t af;
+    bool remote_srv;
 };
 
 #define CONNECTION_LIST_SIZE 64
@@ -836,6 +838,8 @@  void options_postprocess(struct options *options, struct env_set *es);
 
 bool options_postprocess_pull(struct options *o, struct env_set *es);
 
+bool options_mutate_ce_servinfo(struct options *o, struct servinfo *si);
+
 void pre_connect_restore(struct options *o, struct gc_arena *gc);
 
 bool apply_push_options(struct options *options,
diff --git a/src/openvpn/socket.c b/src/openvpn/socket.c
index 2230052f..54d51825 100644
--- a/src/openvpn/socket.c
+++ b/src/openvpn/socket.c
@@ -240,7 +240,7 @@  streqnull(const char *a, const char *b)
 }
 
 /*
- * get_cached_dns_entry return 0 on success and -1
+ * get_cached_dns_entry returns 0 on success and -1
  * otherwise. (like getaddrinfo)
  */
 static int
@@ -271,6 +271,37 @@  get_cached_dns_entry(struct cached_dns_entry *dns_cache,
     return -1;
 }
 
+/*
+ * get_cached_srv_entry returns 0 on success and -1
+ * otherwise. (like getservinfo)
+ */
+static int
+get_cached_srv_entry(struct cached_dns_entry *dns_cache,
+                     const char *hostname,
+                     const char *servname,
+                     int ai_family,
+                     int resolve_flags,
+                     struct servinfo **si)
+{
+    struct cached_dns_entry *ph;
+    int flags;
+
+    /* Only use flags that are relevant for the structure */
+    flags = resolve_flags & GETADDR_CACHE_SERVICE_MASK;
+    flags |= GETADDR_SERVICE;
+
+    for (ph = dns_cache; ph; ph = ph->next)
+    {
+        if (streqnull(ph->hostname, hostname)
+            && streqnull(ph->servname, servname)
+            && ph->flags == flags)
+        {
+            *si = ph->si;
+            return 0;
+        }
+    }
+    return -1;
+}
 
 static int
 do_preresolve_host(struct context *c,
@@ -326,6 +357,77 @@  do_preresolve_host(struct context *c,
     return status;
 }
 
+static int
+do_preresolve_service(struct context *c,
+                      const char *hostname,
+                      const char *servname,
+                      const int af,
+                      const int flags,
+                      bool preresolve_host)
+{
+    struct servinfo *si;
+    int status;
+
+    if (get_cached_srv_entry(c->c1.dns_cache,
+                             hostname,
+                             servname,
+                             af,
+                             flags,
+                             &si) == 0)
+    {
+        /* entry already cached, return success */
+        return 0;
+    }
+
+    status = openvpn_getservinfo(flags, hostname, servname,
+                                 c->options.resolve_retry_seconds, NULL,
+                                 af, &si);
+    if (status == 0)
+    {
+        struct cached_dns_entry *ph;
+
+        ALLOC_OBJ_CLEAR_GC(ph, struct cached_dns_entry, &c->gc);
+        ph->si = si;
+        ph->hostname = hostname;
+        ph->servname = servname;
+        ph->flags = flags & GETADDR_CACHE_SERVICE_MASK;
+
+        if (!c->c1.dns_cache)
+        {
+            c->c1.dns_cache = ph;
+        }
+        else
+        {
+            struct cached_dns_entry *prev = c->c1.dns_cache;
+            while (prev->next)
+            {
+                prev = prev->next;
+            }
+            prev->next = ph;
+        }
+
+        gc_addspecial(si, &gc_freeservinfo_callback, &c->gc);
+
+        /* preresolve service targets unless disabled */
+        if (preresolve_host)
+        {
+            while (si)
+            {
+                int host_flags = flags & ~GETADDR_PROTO_MASK;
+                if (proto_is_dgram(si->proto))
+                {
+                    host_flags |= GETADDR_DATAGRAM;
+                }
+                /* ignore errors */
+                do_preresolve_host(c, si->hostname, si->servname,
+                                   af, host_flags);
+                si = si->next;
+            }
+        }
+    }
+    return status;
+}
+
 void
 do_preresolve(struct context *c)
 {
@@ -364,8 +466,31 @@  do_preresolve(struct context *c)
             remote = ce->remote;
         }
 
+        /* Preresolve remote services */
+        if (ce->remote_srv)
+        {
+            int service_flags = flags|GETADDR_SERVICE;
+
+            if (proto_is_dgram(ce->proto) || ce->proto == PROTO_AUTO)
+            {
+                service_flags |= GETADDR_DATAGRAM;
+            }
+            if (proto_is_stream(ce->proto) || ce->proto == PROTO_AUTO)
+            {
+                service_flags |= GETADDR_STREAM;
+            }
+
+            /* HTTP remote hostnames does not need to be resolved */
+            status = do_preresolve_service(c, remote, ce->remote_port,
+                                           ce->af, service_flags,
+                                           !ce->http_proxy_options);
+            if (status != 0)
+            {
+                goto err;
+            }
+        }
         /* HTTP remote hostname does not need to be resolved */
-        if (!ce->http_proxy_options)
+        else if (!ce->http_proxy_options)
         {
             status = do_preresolve_host(c, remote, ce->remote_port,
                                         ce->af, flags);
@@ -423,6 +548,630 @@  err:
     throw_signal_soft(SIGHUP, "Preresolving failed");
 }
 
+/**
+ * Allocates new service structure on heap and stores
+ * its initial values.
+ *
+ * @param hostname - The peer host name or ip address
+ * @param port     - The peer TCP/UDP port
+ * @param proto    - Protocol for communicating with the peer
+ *
+ * @return A pointer to servinfo structure or NULL in case of
+ *     allocation error.
+ */
+static struct servinfo *
+alloc_servinfo(const char *hostname, uint16_t port, int proto)
+{
+    size_t namesize = hostname ? strlen(hostname) + 1 : 0;
+
+    char servname[sizeof("65535")];
+    openvpn_snprintf(servname, sizeof(servname), "%u", port);
+    size_t servsize = strlen(servname) + 1;
+
+    struct servinfo *si = calloc(1, sizeof(*si) + namesize + servsize);
+    if (si)
+    {
+        if (hostname)
+        {
+            si->hostname = (char *)(si + 1);
+            memcpy((char *)si->hostname, hostname, namesize);
+        }
+        si->servname = (char *)(si + 1) + namesize;
+        memcpy((char *)si->servname, servname, servsize);
+        si->proto = proto;
+    }
+    return si;
+}
+
+/**
+ * Queries DNS SRV records for specified DNS domain.
+ *
+ * @param domain - The DNS domain name
+ * @param proto  - TCP/UDP protocol for communicating with the peer
+ * @param next   - The servinfo structure list to be chained to
+ *                 the resulting servinfo list
+ * @param res    - The pointer to the resulting servinfo list
+ *
+ * @return 0 on success, a EAI-* status code otherwise
+ */
+#ifdef _WIN32
+static int
+query_servinfo(const char *domain, int proto,
+               struct servinfo *next, struct servinfo **res)
+{
+    PDNS_RECORD pDnsRecord;
+    int status;
+
+    ASSERT(res);
+
+    DNS_STATUS DnsStatus = DnsQuery(domain, DNS_TYPE_SRV, DNS_QUERY_STANDARD,
+                                    NULL, &pDnsRecord, NULL);
+    dmsg(D_SOCKET_DEBUG, "DNSQUERY type=%d domain=%s result=%d",
+         DNS_TYPE_SRV, domain, DnsStatus);
+    switch (DnsStatus)
+    {
+        case ERROR_SUCCESS:
+            break;
+
+        case DNS_ERROR_RCODE_NAME_ERROR:
+        case DNS_INFO_NO_RECORDS:
+            return EAI_NONAME; /* HOST_NOT_FOUND */
+
+        case DNS_ERROR_NO_DNS_SERVERS:
+        case DNS_ERROR_RCODE_FORMAT_ERROR:
+        case DNS_ERROR_RCODE_NOT_IMPLEMENTED:
+        case DNS_ERROR_RCODE_REFUSED:
+            return EAI_FAIL; /* NO_RECOVERY */
+
+        case ERROR_TIMEOUT:
+        case DNS_ERROR_RCODE_SERVER_FAILURE:
+        case DNS_ERROR_TRY_AGAIN_LATER:
+            return EAI_AGAIN; /* TRY_AGAIN */
+
+        default:
+            return EAI_FAIL;
+    }
+
+    struct servinfo *list = NULL, *first = NULL;
+    for (PDNS_RECORD rr = pDnsRecord; rr; rr = rr->pNext)
+    {
+        if (rr->wType == DNS_TYPE_SRV)
+        {
+            PDNS_SRV_DATA rdata = &rr->Data.Srv;
+
+            if (rr->wDataLength >= sizeof(DNS_SRV_DATA)
+                && *rdata->pNameTarget)
+            {
+                struct servinfo *si = alloc_servinfo(rdata->pNameTarget,
+                                                     rdata->wPort,
+                                                     proto);
+                if (!si)
+                {
+                    freeservinfo(list);
+                    status = EAI_MEMORY;
+                    goto done;
+                }
+                si->prio = rdata->wPriority;
+                si->weight = rdata->wWeight;
+                si->next = list, list = si;
+                if (!first)
+                {
+                    first = si;
+                }
+            }
+        }
+    }
+    if (list)
+    {
+        first->next = next;
+        *res = list;
+        status = 0;
+    }
+    else
+    {
+        status = EAI_FAIL;
+    }
+
+done:
+    DnsRecordListFree(pDnsRecord, DnsFreeRecordList);
+    return status;
+}
+
+#elif defined(TARGET_OPENBSD)
+static int
+query_servinfo(const char *domain, int proto,
+               struct servinfo *next, struct servinfo **res)
+{
+    /* OpenBSD's native resolver library has a better API - getrrsetbyname() -
+     * but this is not implemented yet
+     */
+    return EAI_FAIL;
+}
+
+#else /* if !defined(_WIN32) && !defined(TARGET_OPENBSD) */
+static int
+query_servinfo(const char *domain, int proto,
+               struct servinfo *next, struct servinfo **res)
+{
+    unsigned char answer[65535];
+    int status;
+
+    int n = res_query(domain, ns_c_in, ns_t_srv, answer, sizeof(answer));
+    dmsg(D_SOCKET_DEBUG, "RES_QUERY class=%d type=%d domain=%s result=%d",
+         ns_c_in, ns_t_srv, domain, n);
+    if (n < 0)
+    {
+        switch (h_errno)
+        {
+            case HOST_NOT_FOUND:
+            case NO_ADDRESS:
+#if NO_ADDRESS != NO_DATA
+            case NO_DATA:
+#endif
+                return EAI_NONAME;
+
+            case NO_RECOVERY:
+                return EAI_FAIL;
+
+            case TRY_AGAIN:
+                return EAI_AGAIN;
+        }
+        return EAI_SYSTEM;
+    }
+
+    ns_msg msg;
+    if (ns_initparse(answer, n, &msg) < 0)
+    {
+        return EAI_FAIL;
+    }
+
+    struct servinfo *list = NULL, *first = NULL;
+    for (int i = 0; i < ns_msg_count(msg, ns_s_an); i++)
+    {
+        ns_rr rr;
+
+        if (ns_parserr(&msg, ns_s_an, i, &rr) == 0
+            && ns_rr_type(rr) == ns_t_srv)
+        {
+            const unsigned char *rdata = ns_rr_rdata(rr);
+            char name[NS_MAXDNAME];
+
+            if (ns_rr_rdlen(rr) > 6
+                && dn_expand(ns_msg_base(msg), ns_msg_end(msg),
+                             rdata + 6, name, sizeof(name)) > 0 && *name)
+            {
+                struct servinfo *si = alloc_servinfo(name,
+                                                     ns_get16(rdata + 4),
+                                                     proto);
+                if (!si)
+                {
+                    freeservinfo(list);
+                    status = EAI_MEMORY;
+                    goto done;
+                }
+                si->prio = ns_get16(rdata);
+                si->weight = ns_get16(rdata + 2);
+                si->next = list, list = si;
+                if (!first)
+                {
+                    first = si;
+                }
+            }
+        }
+    }
+    if (list)
+    {
+        first->next = next;
+        *res = list;
+        status = 0;
+    }
+    else
+    {
+        status = EAI_FAIL;
+    }
+
+done:
+    return status;
+}
+#endif /* if !defined(_WIN32) && !defined(TARGET_OPENBSD) */
+
+/**
+ * Servinfo qsort compare function for the server selection
+ * mechanism, defined in RFC 2782.
+ *
+ * @param a - Pointer to first servinfo structure
+ * @param b - Pointer to second servinfo structure
+ *
+ * @return
+ * @li 0, if equal
+ * @li 1, if "a" should be ordered after "b"
+ * @li -1, if "a" should be ordered before "b"
+ */
+static int
+servinfo_cmp(const void *a, const void *b)
+{
+    const struct servinfo *ae = *(struct servinfo **)a;
+    const struct servinfo *be = *(struct servinfo **)b;
+
+    /* lowest-numbered priority first */
+    if (ae->prio != be->prio)
+    {
+        return ae->prio < be->prio ? -1 : 1;
+    }
+
+    /* zero-weighted first */
+    if ((ae->weight == 0 && be->weight)
+        || (ae->weight && be->weight == 0))
+    {
+        return ae->weight < be->weight ? -1 : 1;
+    }
+
+    /* else keep received order, can't be equal */
+    return ae->order > be->order ? -1 : 1;
+}
+
+/**
+ * Sorts service list according the server selection mechanism,
+ * defined in RFC 2782.
+ *
+ * @param list - The pointer to servinfo list
+ *
+ * @return A pointer to the sorted servinfo list
+ */
+static struct servinfo *
+sort_servinfo(struct servinfo *list)
+{
+    struct servinfo ordered, *tail = &ordered;
+    int count = 0;
+    struct gc_arena gc = gc_new();
+
+    ASSERT(list);
+
+    /* count and number entries in reverse order */
+    for (struct servinfo *si = list; si; si = si->next)
+    {
+        si->order = count++;
+    }
+
+    struct servinfo **sorted;
+    ALLOC_ARRAY_CLEAR_GC(sorted, struct servinfo *, count, &gc);
+    for (struct servinfo *si = list; si; si = si->next)
+    {
+        sorted[si->order] = si;
+    }
+
+    /* sort records by priority and zero weight */
+    qsort(sorted, count, sizeof(sorted[0]), servinfo_cmp);
+
+    /* apply weighted selection mechanism */
+    ordered.next = NULL;
+    for (int i = 0; i < count; )
+    {
+        struct servinfo unordered;
+
+        /* compute the sum of the weights of records of the same
+         * priority and put them in the unordered list */
+        unordered.prio = sorted[i]->prio;
+        unordered.weight = 0;
+        unordered.next = NULL;
+        for (struct servinfo *prev = &unordered;
+             i < count && sorted[i]->prio == unordered.prio; i++)
+        {
+            unordered.weight += sorted[i]->weight;
+
+            /* add entry to the tail of unordered list */
+            sorted[i]->next = NULL;
+            prev->next = sorted[i], prev = sorted[i];
+        }
+
+        /* process the unordered list */
+        while (unordered.next)
+        {
+            /* choose a uniform random number between 0 and the sum
+             * computed (inclusive) */
+            int weight = get_random() % (unordered.weight + 1);
+
+            /* select the entries whose running sum value is the first
+             * in the selected order which is greater than or equal
+             * to the random number selected */
+            for (struct servinfo *si = unordered.next, *prev = &unordered;
+                 si; prev = si, si = si->next)
+            {
+                /* selected entry is the next one to be contacted */
+                if (si->weight >= weight)
+                {
+                    unordered.weight -= si->weight;
+
+                    /* move entry to the ordered list */
+                    prev->next = si->next;
+                    si->next = NULL;
+                    tail->next = si, tail = si;
+
+                    /*
+                     * RFC 2782 is ambiguous, it says:
+                     *   In the presence of records containing weights greater
+                     *   than 0, records with weight 0 should have a very
+                     *   small chance of being selected.
+                     * According that, within the same priority, after all
+                     * records containing weights greater than 0 were selected,
+                     * the rest of records with weight 0 should be skipped.
+                     * At the same time, it says:
+                     *   The following algorithm SHOULD be used to order the
+                     *   SRV RRs of the same priority:
+                     *   ...
+                     *   Continue the ordering process until there are no
+                     *   unordered SRV RRs.
+                     * This means records with wight 0 should always be
+                     * selected, as last ones in worst case.
+                     *
+                     * We implement the second option and do not skip any of
+                     * the records with unordered.weight == 0 after the last
+                     * one with weight greater than 0.
+                     */
+                }
+                weight -= si->weight;
+            }
+        }
+    }
+
+    gc_free(&gc);
+    return ordered.next;
+}
+
+/**
+ * Resolves DNS SRV records for given domain and service names.
+ *
+ * @param domain  - The DNS SRV domain name
+ * @param service - The DNS SRV service name
+ * @param flags   - GETADDR_* DNS resultion flags
+ * @param res     - The pointer to the resulting servinfo list
+ *
+ * @return 0 on success, a EAI-* status code otherwise
+ */
+static int
+getservinfo(const char *domain,
+            const char *service,
+            int flags,
+            struct servinfo **res)
+{
+    static const struct {
+        int flags;
+        int proto;
+        const char *name;
+    } proto[] = {
+        { GETADDR_DATAGRAM, PROTO_UDP, "udp" },
+        { GETADDR_STREAM, PROTO_TCP_CLIENT, "tcp" }
+    };
+    struct servinfo *list = NULL;
+    int status = EAI_SOCKTYPE;
+
+    ASSERT(res);
+
+    if (!domain)
+    {
+        return EAI_NONAME;
+    }
+    if (!service)
+    {
+        return EAI_SERVICE;
+    }
+
+    int proto_flags = flags & GETADDR_PROTO_MASK;
+    for (int i = 0; i < SIZE(proto); i++)
+    {
+        if (proto_flags & proto[i].flags)
+        {
+            proto_flags &= ~proto[i].flags;
+
+            char qname[256];
+            if (!openvpn_snprintf(qname, sizeof(qname), "_%s._%s.%s",
+                                  service, proto[i].name, domain))
+            {
+                freeservinfo(list);
+                return EAI_MEMORY;
+            }
+
+            status = query_servinfo(qname, proto[i].proto, list, &list);
+        }
+    }
+
+    if (list)
+    {
+        *res = sort_servinfo(list);
+        status = 0;
+    }
+
+    return status;
+}
+
+void
+freeservinfo(struct servinfo *res)
+{
+    while (res)
+    {
+        struct servinfo *si = res;
+        res = res->next;
+        free(si);
+    }
+}
+
+/*
+ * Translate IPv4/IPv6 hostname and service name into struct servinfo
+ * If resolve error, try again for resolve_retry_seconds seconds.
+ */
+int
+openvpn_getservinfo(unsigned int flags,
+                    const char *hostname,
+                    const char *servname,
+                    int resolve_retry_seconds,
+                    volatile int *signal_received,
+                    int family,
+                    struct servinfo **res)
+{
+    int status;
+    int sigrec = 0;
+    int msglevel = (flags & GETADDR_FATAL) ? M_FATAL : D_RESOLVE_ERRORS;
+    struct gc_arena gc = gc_new();
+    const char *print_hostname;
+    const char *print_servname;
+
+    ASSERT(res);
+
+    ASSERT(hostname || servname);
+    ASSERT(!(flags & GETADDR_HOST_ORDER));
+
+    if (hostname)
+    {
+        print_hostname = hostname;
+    }
+    else
+    {
+        print_hostname = "undefined";
+    }
+
+    if (servname)
+    {
+        print_servname = servname;
+    }
+    else
+    {
+        print_servname = "";
+    }
+
+    if (flags & GETADDR_MSG_VIRT_OUT)
+    {
+        msglevel |= M_MSG_VIRT_OUT;
+    }
+
+    if ((flags & (GETADDR_FATAL_ON_SIGNAL|GETADDR_WARN_ON_SIGNAL))
+        && !signal_received)
+    {
+        signal_received = &sigrec;
+    }
+
+    const int fail_wait_interval = 5; /* seconds */
+    /* Add +4 to cause integer division rounding up (1 + 4) = 5, (0+4)/5=0 */
+    int resolve_retries = (flags & GETADDR_TRY_ONCE) ? 1 :
+                          ((resolve_retry_seconds + 4)/ fail_wait_interval);
+    const char *fmt;
+    int level = 0;
+
+    fmt = "RESOLVE: Cannot resolve remote service: %s:%s (%s)";
+    if ((flags & GETADDR_MENTION_RESOLVE_RETRY)
+        && !resolve_retry_seconds)
+    {
+        fmt = "RESOLVE: Cannot resolve remote service: %s:%s (%s) "
+              "(I would have retried this name query if you had "
+              "specified the --resolv-retry option.)";
+    }
+
+#ifdef ENABLE_MANAGEMENT
+    if (flags & GETADDR_UPDATE_MANAGEMENT_STATE)
+    {
+        if (management)
+        {
+            management_set_state(management,
+                                 OPENVPN_STATE_RESOLVE,
+                                 NULL,
+                                 NULL,
+                                 NULL,
+                                 NULL,
+                                 NULL);
+        }
+    }
+#endif
+
+    /*
+     * Resolve service
+     */
+    while (true)
+    {
+#ifndef _WIN32
+        /* force resolv.conf reload */
+        res_init();
+#endif
+        dmsg(D_SOCKET_DEBUG, "GETSERVINFO flags=0x%04x family=%d %s:%s",
+             flags, family, print_hostname, print_servname);
+        status = getservinfo(hostname, servname, flags, res);
+
+        if (signal_received)
+        {
+            get_signal(signal_received);
+            if (*signal_received) /* were we interrupted by a signal? */
+            {
+                if (*signal_received == SIGUSR1) /* ignore SIGUSR1 */
+                {
+                    msg(level, "RESOLVE: Ignored SIGUSR1 signal received "
+                        "during DNS resolution attempt");
+                    *signal_received = 0;
+                }
+                else
+                {
+                    /* turn success into failure (interrupted syscall) */
+                    if (0 == status)
+                    {
+                        ASSERT(res);
+                        freeservinfo(*res);
+                        *res = NULL;
+                        status = EAI_AGAIN; /* = temporary failure */
+                        errno = EINTR;
+                    }
+                    goto done;
+                }
+            }
+        }
+
+        /* success? */
+        if (0 == status)
+        {
+            break;
+        }
+
+        /* resolve lookup failed, should we
+         * continue or fail? */
+        level = msglevel;
+        if (resolve_retries > 0)
+        {
+            level = D_RESOLVE_ERRORS;
+        }
+
+        msg(level,
+            fmt,
+            print_hostname,
+            print_servname,
+            gai_strerror(status));
+
+        if (--resolve_retries <= 0)
+        {
+            goto done;
+        }
+
+        management_sleep(fail_wait_interval);
+    }
+
+    ASSERT(res);
+
+    /* service resolve succeeded */
+
+done:
+    if (signal_received && *signal_received)
+    {
+        int level = 0;
+        if (flags & GETADDR_FATAL_ON_SIGNAL)
+        {
+            level = M_FATAL;
+        }
+        else if (flags & GETADDR_WARN_ON_SIGNAL)
+        {
+            level = M_WARN;
+        }
+        msg(level, "RESOLVE: signal received during DNS resolution attempt");
+    }
+
+    gc_free(&gc);
+    return status;
+}
+
 /*
  * Translate IPv4/IPv6 addr or hostname into struct addrinfo
  * If resolve error, try again for resolve_retry_seconds seconds.
@@ -557,8 +1306,9 @@  openvpn_getaddrinfo(unsigned int flags,
             /* try hostname lookup */
             hints.ai_flags &= ~AI_NUMERICHOST;
             dmsg(D_SOCKET_DEBUG,
-                 "GETADDRINFO flags=0x%04x ai_family=%d ai_socktype=%d",
-                 flags, hints.ai_family, hints.ai_socktype);
+                 "GETADDRINFO flags=0x%04x ai_family=%d ai_socktype=%d %s:%s",
+                 flags, hints.ai_family, hints.ai_socktype,
+                 print_hostname, print_servname);
             status = getaddrinfo(hostname, servname, &hints, res);
 
             if (signal_received)
@@ -1816,7 +2566,110 @@  done:
     gc_free(&gc);
 }
 
+void
+do_resolve_service(struct context *c)
+{
+    struct connection_entry *ce = &c->options.ce;
 
+    if (ce->remote_srv
+        && !c->c1.link_socket_addr.service_list)
+    {
+        if (ce->remote)
+        {
+            unsigned int flags = sf2gaf(GETADDR_RESOLVE
+                                        |GETADDR_UPDATE_MANAGEMENT_STATE
+                                        |GETADDR_SERVICE,
+                                        c->options.sockflags);
+            int retry = 0;
+            int status = -1;
+            struct servinfo *si;
+
+            if (proto_is_dgram(ce->proto) || ce->proto == PROTO_AUTO)
+            {
+                flags |= GETADDR_DATAGRAM;
+            }
+            if (proto_is_stream(ce->proto) || ce->proto == PROTO_AUTO)
+            {
+                flags |= GETADDR_STREAM;
+            }
+
+            if (c->options.sockflags & SF_HOST_RANDOMIZE)
+            {
+                flags |= GETADDR_RANDOMIZE;
+            }
+
+            if (c->options.resolve_retry_seconds == RESOLV_RETRY_INFINITE)
+            {
+                flags |= (GETADDR_TRY_ONCE | GETADDR_FATAL);
+                retry = 0;
+            }
+            else if (c->options.resolve_retry_seconds)
+            {
+                flags |= GETADDR_FATAL;
+                retry = c->options.resolve_retry_seconds;
+            }
+            else
+            {
+                flags |= (GETADDR_FATAL | GETADDR_MENTION_RESOLVE_RETRY);
+                retry = 0;
+            }
+
+            status = get_cached_srv_entry(c->c1.dns_cache,
+                                          ce->remote,
+                                          ce->remote_port,
+                                          ce->af,
+                                          flags, &si);
+            if (status)
+            {
+                status = openvpn_getservinfo(flags, ce->remote, ce->remote_port,
+                                             retry, &c->sig->signal_received,
+                                             ce->af, &si);
+            }
+
+            if (status == 0)
+            {
+                c->c1.link_socket_addr.service_list = si;
+                c->c1.link_socket_addr.current_service = NULL;
+
+                /* advance to the first appropriate service */
+                while (si)
+                {
+                    /* map in current service */
+                    if (!c->c1.link_socket_addr.current_service
+                        && options_mutate_ce_servinfo(&c->options, si))
+                    {
+                        c->c1.link_socket_addr.current_service = si;
+                    }
+
+                    /* log discovered service hosts */
+                    msg(D_RESOLVE,
+                        "Resolved remote service host: %s:%s,%s prio %u weight %u",
+                        np(si->hostname), np(si->servname),
+                        proto2ascii(si->proto, ce->af, false),
+                        si->prio, si->weight);
+
+                    si = si->next;
+                }
+                if (!c->c1.link_socket_addr.current_service)
+                {
+                    status = EAI_NONAME;
+                }
+
+                dmsg(D_SOCKET_DEBUG,
+                     "RESOLVE_SERVICE flags=0x%04x rrs=%d sig=%d status=%d",
+                     flags,
+                     retry,
+                     c->sig->signal_received,
+                     status);
+            }
+
+            if (!c->sig->signal_received && status != 0)
+            {
+                c->sig->signal_received = SIGUSR1;
+            }
+        }
+    }
+}
 
 struct link_socket *
 link_socket_new(void)
@@ -3077,16 +3930,19 @@  static const struct proto_names proto_names[] = {
     {"tcp-server",  "TCP_SERVER", AF_UNSPEC, PROTO_TCP_SERVER},
     {"tcp-client",  "TCP_CLIENT", AF_UNSPEC, PROTO_TCP_CLIENT},
     {"tcp",         "TCP", AF_UNSPEC, PROTO_TCP},
+    {"auto",        "AUTO", AF_UNSPEC, PROTO_AUTO},
     /* force IPv4 */
     {"udp4",        "UDPv4", AF_INET, PROTO_UDP},
     {"tcp4-server", "TCPv4_SERVER", AF_INET, PROTO_TCP_SERVER},
     {"tcp4-client", "TCPv4_CLIENT", AF_INET, PROTO_TCP_CLIENT},
     {"tcp4",        "TCPv4", AF_INET, PROTO_TCP},
+    {"auto4",       "AUTOv4", AF_INET, PROTO_AUTO},
     /* force IPv6 */
     {"udp6",        "UDPv6", AF_INET6, PROTO_UDP},
     {"tcp6-server", "TCPv6_SERVER", AF_INET6, PROTO_TCP_SERVER},
     {"tcp6-client", "TCPv6_CLIENT", AF_INET6, PROTO_TCP_CLIENT},
     {"tcp6",        "TCPv6", AF_INET6, PROTO_TCP},
+    {"auto6",       "AUTOv6", AF_INET6, PROTO_AUTO},
 };
 
 int
@@ -3147,6 +4003,10 @@  proto2ascii_all(struct gc_arena *gc)
 
     for (i = 0; i < SIZE(proto_names); ++i)
     {
+        if (proto_names[i].proto == PROTO_NONE)
+        {
+            continue;
+        }
         if (i)
         {
             buf_printf(&out, " ");
diff --git a/src/openvpn/socket.h b/src/openvpn/socket.h
index 3b9c1ba3..e9618bc5 100644
--- a/src/openvpn/socket.h
+++ b/src/openvpn/socket.h
@@ -41,6 +41,11 @@ 
  */
 #define OPENVPN_PORT "1194"
 
+/*
+ * OpenVPN's default service name for DNS SRV discovery.
+ */
+#define OPENVPN_SERVICE "openvpn"
+
 /*
  * Number of seconds that "resolv-retry infinite"
  * represents.
@@ -72,6 +77,19 @@  struct openvpn_sockaddr
     } addr;
 };
 
+/* struct to hold resolved service targets */
+struct servinfo
+{
+    const char *hostname;
+    const char *servname;
+    int proto;
+    int order;
+    unsigned short prio;
+    unsigned short weight;
+    struct addrinfo *ai;
+    struct servinfo *next;
+};
+
 /* struct to hold preresolved host names */
 struct cached_dns_entry {
     const char *hostname;
@@ -79,6 +97,7 @@  struct cached_dns_entry {
     int ai_family;
     int flags;
     struct addrinfo *ai;
+    struct servinfo *si;
     struct cached_dns_entry *next;
 };
 
@@ -107,6 +126,9 @@  struct link_socket_addr
     struct addrinfo *remote_list; /* complete remote list */
     struct addrinfo *current_remote; /* remote used in the
                                       * current connection attempt */
+    struct servinfo *service_list; /* complete service list */
+    struct servinfo *current_service; /* service used in the
+                                       * current connection attempt */
     struct link_socket_actual actual; /* reply to this address */
 };
 
@@ -333,6 +355,8 @@  void link_socket_init_phase2(struct context *c);
 
 void do_preresolve(struct context *c);
 
+void do_resolve_service(struct context *c);
+
 void link_socket_close(struct link_socket *sock);
 
 void sd_close(socket_descriptor_t *sd);
@@ -513,8 +537,12 @@  bool unix_socket_get_peer_uid_gid(const socket_descriptor_t sd, int *uid, int *g
 #define GETADDR_RANDOMIZE             (1<<9)
 #define GETADDR_PASSIVE               (1<<10)
 #define GETADDR_DATAGRAM              (1<<11)
+#define GETADDR_STREAM                (1<<12)
+#define GETADDR_SERVICE               (1<<13)
 
+#define GETADDR_PROTO_MASK              (GETADDR_DATAGRAM|GETADDR_STREAM)
 #define GETADDR_CACHE_MASK              (GETADDR_DATAGRAM|GETADDR_PASSIVE)
+#define GETADDR_CACHE_SERVICE_MASK      (GETADDR_PROTO_MASK|GETADDR_SERVICE)
 
 /**
  * Translate an IPv4 addr or hostname from string form to in_addr_t
@@ -542,6 +570,30 @@  int openvpn_getaddrinfo(unsigned int flags,
                         int ai_family,
                         struct addrinfo **res);
 
+int openvpn_getservinfo(unsigned int flags,
+                        const char *hostname,
+                        const char *servname,
+                        int resolve_retry_seconds,
+                        volatile int *signal_received,
+                        int family,
+                        struct servinfo **res);
+
+void freeservinfo(struct servinfo *res);
+
+/* Inline functions */
+
+static inline void
+gc_freeaddrinfo_callback(void *addr)
+{
+    freeaddrinfo((struct addrinfo *) addr);
+}
+
+static inline void
+gc_freeservinfo_callback(void *addr)
+{
+    freeservinfo((struct servinfo *) addr);
+}
+
 /*
  * Transport protocol naming and other details.
  */
@@ -556,6 +608,7 @@  enum proto_num {
     PROTO_TCP,
     PROTO_TCP_SERVER,
     PROTO_TCP_CLIENT,
+    PROTO_AUTO,
     PROTO_N
 };
 
@@ -563,7 +616,7 @@  static inline bool
 proto_is_net(int proto)
 {
     ASSERT(proto >= 0 && proto < PROTO_N);
-    return proto != PROTO_NONE;
+    return proto != PROTO_NONE && proto != PROTO_AUTO;
 }
 
 /**
@@ -577,7 +630,7 @@  proto_is_udp(int proto)
 }
 
 /**
- * @brief Return if the protocol is datagram (UDP)
+ * @brief Returns if the protocol is datagram (UDP)
  *
  */
 static inline bool
@@ -596,6 +649,16 @@  proto_is_tcp(int proto)
     return proto == PROTO_TCP_CLIENT || proto == PROTO_TCP_SERVER;
 }
 
+/**
+ * @brief Returns if the protocol is stream (TCP)
+ *
+ */
+static inline bool
+proto_is_stream(int proto)
+{
+    return proto_is_tcp(proto);
+}
+
 
 int ascii2proto(const char *proto_name);
 
diff --git a/src/openvpn/syshead.h b/src/openvpn/syshead.h
index 5a673a7b..484c4474 100644
--- a/src/openvpn/syshead.h
+++ b/src/openvpn/syshead.h
@@ -38,6 +38,7 @@ 
 
 #ifdef _WIN32
 #include <windows.h>
+#include <windns.h>
 #include <winsock2.h>
 #include <tlhelp32.h>
 #define sleep(x) Sleep((x)*1000)
@@ -143,6 +144,10 @@ 
 #include <netinet/in.h>
 #endif
 
+#ifdef HAVE_ARPA_NAMESER_H
+#include <arpa/nameser.h>
+#endif
+
 #ifdef HAVE_RESOLV_H
 #include <resolv.h>
 #endif