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 <[email protected]>
> ---
> 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
signature.asc
Description: OpenPGP digital signature
_______________________________________________ Openvpn-devel mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/openvpn-devel
