Am 05.10.20 um 00:36 schrieb Vladislav Grishenko:
> 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:
>     remote server1.example.net 1194 udp
>     remote server2.example.net 1194 udp
>     remote server3.example.net 1194 udp
>     remote server4.example.net 443 tcp
> 
> now it's possible to specify just one --remote-srv:
>     remote-srv example.net
> 
> and configure following DNS SRV records:
>     name                             prio weight port target
>     _openvpn._udp.example.net IN SRV 10   60     1194 server1.example.net
>     _openvpn._udp.example.net IN SRV 10   40     1194 server2.example.net
>     _openvpn._udp.example.net IN SRV 10   0      1194 server3.example.net
>     _openvpn._tcp.example.net IN SRV 20   0       443 server4.example.net

Missing . at the end at least if it should be in a bind zone file:


_openvpn._udp.example.net. IN SRV 10   60     1194 server1.example.net.

or

_openvpn._udp            IN SRV 10   60     1194 server1.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.
> 
> v2:
>   - use DNS SRV for feature naming
>   - change option name to --remote-service-discovery
>   - add --remote-service-discovery [service] optional parameter support
>   - rewrite man, add algo/prio/weight description and examples
>   - fix random weight selection was not implicit
>   - remove pulled REMOTE_DISCOVERY support, not needed
>   - split windows/unix-specific parts into extra functions
>   - put functions into servinfo scope, add doxygen comments
>   - remove addrinfo hack, use servinfo containers of addrinfo list
>   - better proxy support (tcp mode not supported so far)
>   - log discovery attempts and results, if enabled
> 
> v3:
>   - complete logic rewrite
>   - use separate --remote-srv [service] [proto] option
>   - remove fallback, additional --remote/--remote-srv does the same
>   - add "auto" protocol support to allow both udp & tcp discoverery
>   - add support for tcp / http proxy (natively)
>   - man update
> 
> v4:
>   - due RFC 2782 ambiguity, prefer to use all resolved DNS SRV records,
>     even ones with weight 0 after the records containing weights greater
>     than 0 were all selected, keep related code disabled for historical
>     reasons.
>   - man update
> 
> v5: rebase against upstream with connection advancing fix
>   - allow management skip/accept for remote service hosts as for --remote
>   - improve compatibility with a way "--persist-remote-ip" is handled
>   - ensure max line length is 80
> 
> v6:
>   - pick out code-style conformant changes into separate patch
>   - add more options checks and comments
> 
> v7:
>   - prefer line breaks before long string parameters
>   - use win32/posix suffixes for query_servinfo
> 
> v8:
>   - rework compatibility with --preresolve and --persist-remote-ip
>   - fix dns data structures leak on wine/win32
>   - add priority and weight logging
> 
> Signed-off-by: Vladislav Grishenko <themi...@yandex-team.ru>
> ---
>  configure.ac                        |   2 +-
>  doc/man-sections/client-options.rst | 117 +++-
>  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         |   8 +-
>  src/openvpn/options.c               | 268 +++++++--
>  src/openvpn/options.h               |   4 +
>  src/openvpn/socket.c                | 854 +++++++++++++++++++++++++++-
>  src/openvpn/socket.h                |  58 +-
>  src/openvpn/syshead.h               |   5 +
>  13 files changed, 1328 insertions(+), 81 deletions(-)
> 
> diff --git a/configure.ac b/configure.ac
> index ebb32204..22088aaf 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -477,7 +477,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 af21fbcd..2ed9655d 100644
> --- a/doc/man-sections/client-options.rst
> +++ b/doc/man-sections/client-options.rst
> @@ -307,10 +307,117 @@ 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 it is :code:`openvpn` registered service name.

The last sentenced feels odd to me. But I am no native speaker I would
word that as:

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
> +     _openvpn._udp.example.net IN SRV 10   60     1194 server1.example.net
> +     _openvpn._udp.example.net IN SRV 10   40     1194 server2.example.net
> +     _openvpn._udp.example.net IN SRV 10   0      1194 server3.example.net
> +     _openvpn._tcp.example.net IN SRV 20   0       443 server4.example.net
> +

Same problem with missing . at the end of the domain. I ended up with:


2020-10-19 17:43:16 Resolved remote service host:
openpvn.blinkt.de.blinkt.de:1198,udp prio 10 weight 60

which is semi helpful.


> +  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,

If /a/ DNS SRV name.

Use cannot rather than the informal "can't".
Also are instead of were to keep tenses the same in both sentences.

> +  client will move on to the next connection entry.
> +

the client

> +  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.
> +  When multiple ``--remote`` address/ports or ``--remote-srv`` records are
> +  specified, or if connection profiles are being used, initially randomize
> +  the order of the list as a kind of basic load-balancing measure.

This is now even more confusing. I would completely rewrite it and not
that remote-srv is better.

--remote-random
  Randomises the order of ``--remote-srv``, ``remote-srv`` and
connection profiles on start. This randomisationof these entries as a
kind of basic load-balancing measure. Note that implementing DNS SRV
entries (with a short TTL if it should be dynamic) and remote-srv is a
better alternative for client-side load balancing.



> +}
> +
> +#else
> +/*
> + * Queries DNS SRV records for specified DNS domain.
> + * Returns EAI_* status and service list on success in order of receive.
> + */
> +static int
> +query_servinfo_posix(const char *domain, int proto,
> +                     struct servinfo *next, struct servinfo **res)
> +{
> +    unsigned char answer[NS_MAXMSG];
> +    int status;


on macOS:

../../../openvpn-git/src/openvpn/socket.c:672:26: error: use of undeclared
      identifier 'NS_MAXMSG'
    unsigned char answer[NS_MAXMSG];


I used

#ifndef NS_MAXMSG
#define NS_MAXMSG 65335
#endif

for my testing in that function


> +
> +    int n = res_query(domain, ns_c_in, ns_t_srv, answer, NS_MAXMSG);
> +    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:
> +                return EAI_NONAME;
> +
> +            case NO_ADDRESS:
> +#if NO_ADDRESS != NO_DATA
> +            case NO_DATA:
> +#endif
> +                return EAI_NODATA;
> +
> +            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_NODATA;
> +    }
> +
> +done:
> +    return status;
> +}
> +#endif
> +
> +/*
> + * Service structure compare function for server selection
> + * mechanism, defined in RFC 2782.
> + */
> +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;
> +}
> +
> +/*
> + * Sort & order service list according server selection mechanism,
> + * defined in RFC 2782.
> + * Returns service list. Although specific elements can be freed,
> + * non-empty list can never become empty.
> + */

The comment about freed elements only applies if the #if 0 block is
enabled, right? I would remove this. And in general it is better to have
the function comments as doxygen style comments.


> +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.


> +#if 0
> +                    /*
> +                     * Skip all the rest records with weight 0 after the last
> +                     * one with weight greater than 0.
> +                     */
> +                    if (unordered.weight == 0 && si->weight)
> +                    {
> +                        freeservinfo(unordered.next);
> +                        unordered.next = NULL;
> +                    }
> +                    break;
> +#endif

Lets not add a #if 0 here

rather use a comment here that notes that we never skip records or
something like that.




> +                }
> +                weight -= si->weight;
> +            }
> +        }
> +    }
> +
> +    gc_free(&gc);
> +    return ordered.next;
> +}
> +
> +
> +/*
> + * Resolves DNS SRV records for specified domain and service.
> + * Returns EAI_* status (like getaddrinfo) and service list, ordered
> + * according server selection mechanism, defined in RFC 2782.
> + */
> +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;
> +            }
> +
> +#ifdef _WIN32
> +            status = query_servinfo_win32(qname, proto[i].proto, list, 
> &list);
> +#else
> +            status = query_servinfo_posix(qname, proto[i].proto, list, 
> &list);
> +#endif

I think we should name those two function identical. They are doing
identical stuff, so need for this ifdef and different function names.



>  bool

> +/* 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;
>      const char *servname;
>      int ai_family;
>      int flags;
> -    struct addrinfo *ai;
> +    union {
> +        struct addrinfo *ai;
> +        struct servinfo *si;
> +    };

Is saving the space for one pointer really worth the potential danger of
accidentially using an addrinfo as servinfo or vice versa?


I tested the patch and it works as expected (apart from things mentioned
here).

But this should not be a legal config:

<connection>
remote foo.bar
remote-srv blinkt.de ovpn
</connection>

In a connection entry there should be only one of remote or remote-srv
be allowed.

Arne

Attachment: signature.asc
Description: OpenPGP digital signature

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

Reply via email to