Am 25.08.20 um 00:15 schrieb Vladislav Grishenko:
> DNS SRV (rfc2782) support allows to use several OpenVPN servers for a single
> domain w/o explicit profile enumerating, to move services from host to host
> with little fuss, and to designate some hosts as primary servers for a service
> and others as backups.
> OpenVPN client ask for a specific service/protocol for a specific domain and
> get back the names & ports of available servers to connect to, ordered by
> priority and relative weight. Note, implemented server selection is intended
> to express static information such as "this server has a faster CPU than that
> one" or "this server has a much better network connection than that one" at
> the moment client asks for a records.
> Feature has been asked several times already, can be useful in case of for 
> huge
> ammounts of client & servers deployed. 

Hu? You are talking here about loading balancing by a weight, right?

> For example, instead of multiple remotes in profile:
>     remote server.example.com 1194 udp
>     remote server1.example.com 1195 udp
>     remote server2.example.org 1196 udp
>     remote server.example.com 443 tcp
>     remote server1.example.tld 8443 tcp
> 
> Now it's possible to specify just static domain(s) and enable discovery:
>     remote server.example.com 1194 udp
>     remote server.example.com 443 tcp
>     remote-discovery
> 
> When discovery is enabled, 

I wonder if we should enable this by default. But this still fixes the
order of UDP and TCP in the config. Ideally I want just to say:

remote vpn.mycorp.com and then it should resolve to all UDP+TCP options.
And it should be possible to point the client to try
udp/1194/primary.vpn.corp.com and then 443/tcp/primary.vpn.corp.com and
only after that try 1194/backup.vpn.corp.com. With this syntax it looks
like that is not possible.


> OpenVPN will first try to resolve
> _openvpn._udp.server.example.com SRV records and use resolved targets (see
> example below) as servers for the subsequent connection attempts. If there's
> no records or resolve error, OpenVPN will fallback to direct connection
> using specified server.example.com host and 1194 port. Next connection
> attempt will be done with next configures connection/remote entry - using
> _openvpn._tcp.server.example.com records (if any) and server.example.com:443
> as last resort.
> 
> DNS zone can look like below (arbitrary prio & weight values):

What/how are prio and weight used?

>     _openvpn._udp.server.example.com IN SRV 10 70 1194 server.example.com
>     _openvpn._udp.server.example.com IN SRV 10 30 1195 server1.example.com
>     _openvpn._udp.server.example.com IN SRV 20 0 1196 server2.example.org
>     _openvpn._tcp.server.example.com IN SRV 10 10 443 server.example.com
>     _openvpn._tcp.server.example.com IN SRV 10 200 443 server.example.com
> 
> Currently only "openvpn" service is supported, usage of per-connection
> service names is up to syntax decision (either intead of fallback port, or
                                                 ^^^^ typo

> as remote-discovery argument, etc), almost all the required mechanics
> is implemented for that.

Almost all? What is missing?

> 
> References:
>  https://tools.ietf.org/html/rfc2782
>  https://en.wikipedia.org/wiki/SRV_record
>  https://sourceforge.net/p/openvpn/mailman/message/34364911/
>  https://forums.openvpn.net/viewtopic.php?f=10&t=13660
> 
> Signed-off-by: Vladislav Grishenko <themi...@yandex-team.ru>
> ---
>  configure.ac                        |   2 +-
>  doc/man-sections/client-options.rst |  26 ++
>  src/openvpn/Makefile.am             |   2 +-
>  src/openvpn/buffer.h                |   5 -
>  src/openvpn/init.c                  |   9 +-
>  src/openvpn/manage.c                |   2 +-
>  src/openvpn/openvpn.vcxproj         |   8 +-
>  src/openvpn/options.c               |  11 +
>  src/openvpn/options.h               |   1 +
>  src/openvpn/ps.c                    |   2 +-
>  src/openvpn/socket.c                | 530 ++++++++++++++++++++++++++--
>  src/openvpn/socket.h                |  15 +
>  src/openvpn/syshead.h               |   5 +
>  13 files changed, 580 insertions(+), 38 deletions(-)
> 
> diff --git a/configure.ac b/configure.ac
> index f8279924..67251bed 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 ec1e3b11..53e6580e 100644
> --- a/doc/man-sections/client-options.rst
> +++ b/doc/man-sections/client-options.rst
> @@ -299,6 +299,32 @@ configuration.
>    specification (4/6 suffix), OpenVPN will try both IPv4 and IPv6
>    addresses, in the order getaddrinfo() returns them.
>  
> +--remote-discovery

discovery is a bit ambigious. Maybe rather use --remote-service-discovery?

> +  Perform rfc2782 remote host discovery using specified ``host`` and
> +  ``proto`` values.


Spelling, RFC 2782. But RFC numbers are not user friendly when used
alone. I would at least add DNS SRV somewhere in there.

> +  OpenVPN will try to resolve `` _openvpn._proto.host`` name via DNS
> +  and use returned DNS SRV target and port records as OpenVPN servers
> +  addresses in the order specified by Priority and Weight of that
> +  records. If one record resolves to multiple IP addresses, OpenVPN
> +  will try them in the order that the system getaddrinfo() presents
> +  them. If discovery is failed, usual ``host`` and ``port`` connection
> +  will be tried as a fallback.
> +
> +  Example:
> +  ::
> +
> +     remote example.net 1194 udp
> +     remote example.net 443 tcp
> +     remote-discovery
> +
> +  DNS zone:
> +  ::
> +
> +     _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 20  0 1194 server3.example.net
> +     _openvpn._tcp.example.net IN SRV 10  0  443 server4.example.net
> +


The man page is completely missing how the weight/priority is
interpreted by OpenVPN.


> +++ b/src/openvpn/options.c
> @@ -123,6 +123,7 @@ static const char usage_message[] =
>      "--remote host [port] : Remote host name or ip address.\n"
>      "--remote-random : If multiple --remote options specified, choose one 
> randomly.\n"
>      "--remote-random-hostname : Add a random string to remote DNS name.\n"
> +    "--remote-discovery : Perform rfc2782 remote host discovery.\n"

See other comment.


>      else if (streq(p[0], "setenv") && p[1] && !p[3])
>      {
>          VERIFY_PERMISSION(OPT_P_GENERAL);
> @@ -6679,6 +6686,10 @@ add_option(struct options *options,
>          {
>              options->sockflags |= SF_HOST_RANDOMIZE;
>          }
> +        if (streq(p[1], "REMOTE_DISCOVERY"))
> +        {
> +            options->ce.remote_discovery = true;
> +        }

I don't think we should add this. We already have ignore-unknown-options
and setenv opt to write backwards compatible configs.


> @@ -416,7 +424,7 @@ do_preresolve(struct context *c)
>          if (ce->bind_local)
>          {
>              flags |= GETADDR_PASSIVE;
> -            flags &= ~GETADDR_RANDOMIZE;
> +            flags &= ~(GETADDR_RANDOMIZE|GETADDR_DISCOVERY);
>              status = do_preresolve_host(c, ce->local, ce->local_port, 
> ce->af, flags);


Hm does this do a DNS SRV discovery for the local bind address? That
seems odd.

>              if (status != 0)
>              {
> @@ -432,6 +440,363 @@ err:
>      throw_signal_soft(SIGHUP, "Preresolving failed");
>  }
>  
> +/* Struct to hold resolved srv records */
> +struct srvinfo
> +{
> +    const char *hostname;
> +    const char *servname;
> +    unsigned short prio;
> +    unsigned short weight;
> +    int order;
> +    struct srvinfo *next;
> +};



> +static int
> +rfc2782_cmp(const void *a, const void *b)

It does compare srvinfo structs and should be named accordingly. Also
please add a doxygen comment, say how it compares them.

> +{
> +    const struct srvinfo *ae = *(struct srvinfo **)a;
> +    const struct srvinfo *be = *(struct srvinfo **)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;
> +    }
> +
> +    /* keep received order */
> +    return ae->order < be->order ? -1 : 1;
> +}


> +static struct srvinfo *
> +rfc2782_sort(struct srvinfo *list, int count)
> +{

Doxygen, sorting by what etc.

> +    struct srvinfo head, *tail, **sorted;
> +    struct gc_arena gc = gc_new();
> +
> +    ASSERT(list);
> +    ASSERT(count);
> +
> +    head.next = NULL;
> +    tail = &head;
> +
> +    /* sort records by priority and zero weight */
> +    ALLOC_ARRAY_CLEAR_GC(sorted, struct srvinfo *, count, &gc);
> +    for (struct srvinfo *e = list; e; e = e->next)
> +    {
> +        ASSERT(e->order < count);
> +        sorted[e->order] = e;
> +    }
> +    qsort(sorted, count, sizeof(sorted[0]), rfc2782_cmp);
> +
> +    /* apply weighted selection mechanism */
> +    for (int i = 0; i < count;)
> +    {
> +        struct srvinfo 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;

unordered.weight, unordered.prio seems just used as variables in this
algorithm.

> +        for (struct srvinfo *prev = &unordered; i < count && sorted[i]->prio 
> == unordered.prio; i++)
> +        {
> +            struct srvinfo *e = sorted[i];
> +
> +            unordered.weight += e->weight;
> +
> +            /* add entry to the tail of unordered list */
> +            e->next = NULL;
> +            prev->next = e, prev = e;
> +        }
> +
> +        /* process the unordered list */
> +        while (unordered.next)
> +        {
> +            /* choose a uniform random number between 0 and the sum
> +             * computed (inclusive) */
> +            int weight = unordered.weight ? get_random() % unordered.weight 
> : 0;

This line is hard to parse.

> +
> +            /* 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 srvinfo *e = unordered.next, *prev = &unordered; e; 
> prev = e, e = e->next)
> +            {
> +                /* selected entry is the next one to be contacted */
> +                if (e->weight >= weight)
> +                {
> +                    unordered.weight -= e->weight;
> +
> +                    /* move entry to the ordered list */
> +                    prev->next = e->next;
> +                    e->next = NULL;
> +                    tail->next = e, tail = e;
> +
> +                    /* in the presence of records containing weights greater
> +                     * than 0, records with weight 0 should have a very small
> +                     * chance of being selected, so skip them all after the
> +                     * last weighted */
> +                    if (unordered.weight == 0 && e->weight)
> +                    {
> +                        unordered.next = NULL;
> +                    }
> +                    break;

This behaviour needs to be documented in the man page.



> +/*
> + * Translated hostname, service and protocol into struct srvinfo.
> + * Multiple records are ordered per rfc2782.
> + */
> +static int
> +openvpn_getsrvinfo(unsigned int flags,
> +                   const char *hostname,
> +                   const char *servname,
> +                   struct srvinfo **res,
> +                   struct gc_arena *gc)
> +{
> +    struct srvinfo *list = NULL;
> +#ifdef _WIN32
> +    char qname[DNS_MAX_NAME_BUFFER_LENGTH];
> +#else
> +    char qname[NS_MAXDNAME];
> +#endif
> +    int status;
> +    int count = 0;
> +
> +    ASSERT(res);
> +
> +    ASSERT(hostname && servname);
> +    ASSERT(!(flags & GETADDR_HOST_ORDER));
> +
> +    if (!openvpn_snprintf(qname, sizeof(qname), "_%s._%s.%s", servname,
> +                          (flags & GETADDR_DATAGRAM) ? "udp" : "tcp", 
> hostname))
> +    {
> +        return EAI_MEMORY;
> +    }
> +
> +#ifdef _WIN32
> +    PDNS_RECORD pDnsRecord;
> +    DNS_STATUS DnsStatus = DnsQuery(qname, DNS_TYPE_SRV, DNS_QUERY_STANDARD, 
> NULL, &pDnsRecord, NULL);
> +    dmsg(D_SOCKET_DEBUG, "DNSQUERY type=%d dname=%s result=%d",
> +         DNS_TYPE_SRV, qname, DnsStatus);
> +    switch (DnsStatus)
> +    {
> +        case ERROR_SUCCESS:
> +            break;
> +
> +        case DNS_ERROR_RCODE_NAME_ERROR:
> +            return EAI_NONAME; /* HOST_NOT_FOUND */
> +
> +        case DNS_INFO_NO_RECORDS:
> +            return EAI_NODATA; /* NO_DATA */
> +
> +        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_NODATA;
> +    }
> +
> +    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)
> +            {
> +                char port[sizeof("65535")];
> +                openvpn_snprintf(port, sizeof(port), "%u", rdata->wPort);
> +
> +                struct srvinfo *e;
> +                ALLOC_OBJ_CLEAR_GC(e, struct srvinfo, gc);
> +                e->hostname = string_alloc(rdata->pNameTarget, gc);
> +                e->servname = string_alloc(port, gc);
> +                e->prio = rdata->wPriority;
> +                e->weight = rdata->wWeight;
> +                e->order = count++;
> +                e->next = list, list = e;
> +            }
> +        }
> +    }
> +    DnsRecordListFree(pDnsRecord, DnsFreeParsedMessageFields);
> +#else
> +    unsigned char answer[NS_MAXMSG];
> +    int n = res_query(qname, ns_c_in, ns_t_srv, answer, NS_MAXMSG);
> +    dmsg(D_SOCKET_DEBUG, "RES_QUERY class=%d type=%d dname=%s result=%d",
> +         ns_c_in, ns_t_srv, qname, 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;
> +    }
> +
> +    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);
> +
> +            if (ns_rr_rdlen(rr) > 6
> +                && dn_expand(ns_msg_base(msg), ns_msg_end(msg),
> +                             rdata + 6, qname, sizeof(qname)) > 0 && *qname)
> +            {
> +                char port[sizeof("65535")];
> +                openvpn_snprintf(port, sizeof(port), "%u", ns_get16(rdata + 
> 4));
> +
> +                struct srvinfo *e;
> +                ALLOC_OBJ_CLEAR_GC(e, struct srvinfo, gc);
> +                e->hostname = string_alloc(qname, gc);
> +                e->servname = string_alloc(port, gc);
> +                e->prio = ns_get16(rdata);
> +                e->weight = ns_get16(rdata + 2);
> +                e->order = count++;
> +                e->next = list, list = e;
> +            }
> +        }
> +    }
> +#endif

Can we have the platform specifc part in an extra function? This ifdef
makes the function hard to read.



> +/* 
> + * Struct to hold chainable copy of struct addinfo.
> + * Must be binary compatible (except size) with original.
> + */

What is binary compatible expect size? The first member needs to be a
addrinfo since this might be cast to addrinfo? And if yes, why are we
doing a hack like that?

> +struct addrinfo_chained
> +{
> +    struct addrinfo ai;
> +    struct openvpn_sockaddr addr;
> +    char canonname[0];
> +};
> +
> +/*
> + * System getaddrinfo() returns one or more addrinfo structures.
> + * openvpn_getaddinfo() chains them into one in case DNS SRV
> + * resolve is performed, but unfortunately it can't be safely done
> + * (freed) with musl libc, refer
> + * 
> https://git.musl-libc.org/cgit/musl/commit/?id=d1395c43c019aec6b855cf3c656bf47c8a719e7f
> + * Use own copy of each addrinfo to workaround this and possibly
> + * other getaddrinfo()/freeaddrinfo() implementations.
> + */

This looks like a comment describing the function but it does not
desribe the function.

Also wouldn't it be easier to just add a gc special free function for
your srvinfo struct that also free the associated addrinfo record
instead of doing this strange dance?

> +int
> +getaddrinfo_chained(const char *node, const char *service,
> +                    const struct addrinfo *hints,
> +                    struct addrinfo **res)
> +{
> +    int status = getaddrinfo(node, service, hints, res);
> +    if (status == 0 && res && *res)
> +    {
> +        struct addrinfo_chained head, *tail = &head;
> +
> +        head.ai.ai_next = NULL;
> +        for (struct addrinfo *ai = *res; ai; ai = ai->ai_next)
> +        {
> +            socklen_t addrlen = ai->ai_addr ? ai->ai_addrlen : 0;
> +            size_t namelen = ai->ai_canonname ? strlen(ai->ai_canonname) + 1 
> : 0;
> +
> +            /* allocate self-contained struct with enough space for optional 
> data */
> +            struct addrinfo_chained *ac = calloc(1, sizeof(*ac) + addrlen + 
> namelen);
> +            if (!ac)
> +            {
> +                openvpn_freeaddrinfo(head.ai.ai_next);
> +                head.ai.ai_next = NULL;
> +                status = EAI_MEMORY;
> +                break;
> +            }
> +
> +            /* deep clone the structure and its members */
> +            memcpy(&ac->ai, ai, sizeof(ac->ai));
> +            if (ai->ai_addr)
> +            {
> +                memcpy(&ac->addr.addr.sa, ai->ai_addr, addrlen);
> +                ac->ai.ai_addr = &ac->addr.addr.sa;
> +            }
> +            if (ai->ai_canonname)
> +            {
> +                memcpy(ac->canonname, ai->ai_canonname, namelen);
> +                ac->ai.ai_canonname = ac->canonname;
> +            }
> +
> +            /* add clone to the tail */
> +            ac->ai.ai_next = NULL;
> +            tail->ai.ai_next = &ac->ai, tail = ac;
> +        }
> +
> +        /* original list is cloned, can be freed now */
> +        freeaddrinfo(*res);
> +        *res = head.ai.ai_next;
> +    }
> +    return status;
> +}
> +
> +void
> +openvpn_freeaddrinfo(struct addrinfo *res)
> +{
> +    while (res)
> +    {
> +        struct addrinfo_chained *ac = (struct addrinfo_chained *)res;
> +        res = res->ai_next;
> +        free(ac);
> +    }
> +}
> +
>  /*
>   * Translate IPv4/IPv6 addr or hostname into struct addrinfo
>   * If resolve error, try again for resolve_retry_seconds seconds.
> @@ -497,7 +862,7 @@ openvpn_getaddrinfo(unsigned int flags,
>          hints.ai_socktype = SOCK_STREAM;
>      }
>  
> -    status = getaddrinfo(hostname, servname, &hints, res);
> +    status = getaddrinfo_chained(hostname, servname, &hints, res);
>  
>      if (status != 0) /* parse as numeric address failed? */
>      {
> @@ -555,16 +920,17 @@ openvpn_getaddrinfo(unsigned int flags,
>          /*
>           * Resolve hostname
>           */
> -        while (true)
> -        {
> +        struct srvinfo *targets = NULL;
>  #ifndef _WIN32
> -            res_init();
> +        res_init();
>  #endif
> -            /* 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);
> -            status = getaddrinfo(hostname, servname, &hints, res);
> +
> +        /* try srv lookup */
> +        while (flags & GETADDR_DISCOVERY)
> +        {
> +            dmsg(D_SOCKET_DEBUG, "GETSRVINFO flags=0x%04x hostname=%s",
> +                 flags, np(hostname));
> +            status = openvpn_getsrvinfo(flags, hostname, OPENVPN_SERVICE, 
> &targets, &gc);
>  
>              if (signal_received)
>              {
> @@ -581,9 +947,6 @@ openvpn_getaddrinfo(unsigned int flags,
>                          /* turn success into failure (interrupted syscall) */
>                          if (0 == status)
>                          {
> -                            ASSERT(res);
> -                            freeaddrinfo(*res);
> -                            *res = NULL;
>                              status = EAI_AGAIN; /* = temporary failure */
>                              errno = EINTR;
>                          }
> @@ -592,8 +955,8 @@ openvpn_getaddrinfo(unsigned int flags,
>                  }
>              }
>  
> -            /* success? */
> -            if (0 == status)
> +            /* success and fails are expected, fallback to usual resolve */
> +            if (EAI_AGAIN != status)
>              {
>                  break;
>              }
> @@ -614,10 +977,127 @@ openvpn_getaddrinfo(unsigned int flags,
>  
>              if (--resolve_retries <= 0)
>              {
> -                goto done;
> +                /* can't retry, fallback to usual resolve */
> +                break;
>              }
>  
>              management_sleep(fail_wait_interval);
> +#ifndef _WIN32
> +            res_init();
> +#endif

Add a comment why we do that res_init again. It is not obvious for me.


This is not full review. But I feel this emulating addrinfo for remote
is a bit hacky. I feel refactoring the current_remote code to be a bit
more agnostic and then always use the srvinfo meta struct and iterates
through that one to set addrinfo or something in that is a much cleaner
solution.

(This might be already in patch) Also if srv records are returned and
verb level is high enough, something like 3 or 4, we should log
something like whole result of the service discovery.

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