diff --git a/include/openvpn-msg.h b/include/openvpn-msg.h index 7a993358..8b480537 100644 --- a/include/openvpn-msg.h +++ b/include/openvpn-msg.h @@ -35,6 +35,8 @@ typedef enum { msg_del_route, msg_add_dns_cfg, msg_del_dns_cfg, + msg_add_nrpt_cfg, + msg_del_nrpt_cfg, msg_add_nbt_cfg, msg_del_nbt_cfg, msg_flush_neighbors, @@ -96,6 +98,23 @@ typedef struct { inet_address_t addr[4]; /* support up to 4 dns addresses */ } dns_cfg_message_t; + +typedef enum { + nrpt_dnssec +} nrpt_flags_t; + +#define NRPT_ADDR_NUM 8 /* Max. number of addresses */ +#define NRPT_ADDR_SIZE 48 /* Max. address strlen + some */ +typedef char nrpt_address_t[NRPT_ADDR_SIZE]; +typedef struct { + message_header_t header; + interface_t iface; + nrpt_address_t addresses[NRPT_ADDR_NUM]; + char resolve_domains[512]; /* double \0 terminated */ + char search_domains[512]; + nrpt_flags_t flags; +} nrpt_dns_cfg_message_t; + typedef struct { message_header_t header; interface_t iface; diff --git a/src/openvpn/dns.c b/src/openvpn/dns.c index cf48c220..b6e524fa 100644 --- a/src/openvpn/dns.c +++ b/src/openvpn/dns.c @@ -29,6 +29,12 @@ #include "dns.h" #include "socket.h" +#include "options.h" + +#ifdef _WIN32 +#include "win32.h" +#include "openvpn-msg.h" +#endif /** * Parses a string as port and stores it @@ -428,6 +434,122 @@ setenv_dns_options(const struct dns_options *o, struct env_set *es) gc_free(&gc); } +#ifdef _WIN32 + +static void +make_domain_list(const char *what, const struct dns_domain *src, + bool nrpt_domains, char *dst, size_t dst_size) +{ + /* NRPT domains need two \0 at the end for REG_MULTI_SZ + * and a leading '.' added in front of the domain name */ + size_t term_size = nrpt_domains ? 2 : 1; + size_t leading_dot = nrpt_domains ? 1 : 0; + size_t offset = 0; + + memset(dst, 0, dst_size); + + while (src) + { + size_t len = strlen(src->name); + if (offset + leading_dot + len + term_size > dst_size) + { + msg(M_WARN, "WARNING: %s truncated", what); + if (offset) + { + /* Remove trailing comma */ + *(dst + offset - 1) = '\0'; + } + break; + } + + if (leading_dot) + { + *(dst + offset++) = '.'; + } + strncpy(dst + offset, src->name, len); + offset += len; + + src = src->next; + if (src) + { + *(dst + offset++) = ','; + } + } +} + +static void +run_up_down_service(bool add, const struct options *o, const struct tuntap *tt) +{ + const struct dns_server *server = o->dns_options.servers; + const struct dns_domain *search_domains = o->dns_options.search_domains; + + while (true) + { + if (!server) + { + if (add) + { + msg(M_WARN, "WARNING: setting DNS failed, no compatible server profile"); + } + return; + } + + bool only_standard_server_ports = true; + for (size_t i = 0; i < NRPT_ADDR_NUM; ++i) + { + if (server->addr[i].port && server->addr[i].port != 53) + { + only_standard_server_ports = false; + break; + } + } + if ((server->transport == DNS_TRANSPORT_UNSET || server->transport == DNS_TRANSPORT_PLAIN) + && only_standard_server_ports) + { + break; /* found compatible server */ + } + + server = server->next; + } + + ack_message_t ack; + nrpt_dns_cfg_message_t nrpt = { + .header = { + (add ? msg_add_nrpt_cfg : msg_del_nrpt_cfg), + sizeof(nrpt_dns_cfg_message_t), + 0 + }, + .iface = { .index = tt->adapter_index, .name = "" }, + .flags = server->dnssec == DNS_SECURITY_NO ? 0 : nrpt_dnssec, + }; + strncpynt(nrpt.iface.name, tt->actual_name, sizeof(nrpt.iface.name)); + + for (size_t i = 0; i < NRPT_ADDR_NUM; ++i) + { + if (server->addr[i].family == AF_UNSPEC) + { + /* No more addresses */ + break; + } + + if (inet_ntop(server->addr[i].family, &server->addr[i].in, + nrpt.addresses[i], NRPT_ADDR_SIZE) == NULL) + { + msg(M_WARN, "WARNING: could not convert dns server address"); + } + } + + make_domain_list("dns server resolve domains", server->domains, true, + nrpt.resolve_domains, sizeof(nrpt.resolve_domains)); + + make_domain_list("dns search domains", search_domains, false, + nrpt.search_domains, sizeof(nrpt.search_domains)); + + send_msg_iservice(o->msg_channel, &nrpt, sizeof(nrpt), &ack, "DNS"); +} + +#endif /* _WIN32 */ + void show_dns_options(const struct dns_options *o) { @@ -506,3 +628,43 @@ show_dns_options(const struct dns_options *o) gc_free(&gc); } + +void +run_dns_up_down(bool up, struct options *o, const struct tuntap *tt) +{ + if (!o->dns_options.servers) + { + return; + } + + /* Warn about adding servers of unsupported AF */ + const struct dns_server *s = o->dns_options.servers; + while (up && s) + { + size_t bad_count = 0; + for (size_t i = 0; i < s->addr_count; ++i) + { + if ((s->addr[i].family == AF_INET6 && !tt->did_ifconfig_ipv6_setup) + || (s->addr[i].family == AF_INET && !tt->did_ifconfig_setup)) + { + ++bad_count; + } + } + if (bad_count == s->addr_count) + { + msg(M_WARN, "DNS server %ld only has address(es) from a family " + "the tunnel is not configured for - it will not be reachable", + s->priority); + } + else if (bad_count) + { + msg(M_WARN, "DNS server %ld has address(es) from a family " + "the tunnel is not configured for", s->priority); + } + s = s->next; + } + +#ifdef _WIN32 + run_up_down_service(up, o, tt); +#endif /* ifdef _WIN32 */ +} diff --git a/src/openvpn/dns.h b/src/openvpn/dns.h index 838ebe15..f24e30b7 100644 --- a/src/openvpn/dns.h +++ b/src/openvpn/dns.h @@ -26,6 +26,7 @@ #include "buffer.h" #include "env_set.h" +#include "tun.h" enum dns_security { DNS_SECURITY_UNSET, @@ -146,6 +147,14 @@ void dns_options_preprocess_pull(struct dns_options *o); */ void dns_options_postprocess_pull(struct dns_options *o); +/** + * Invokes the action associated with bringing DNS up or down + * @param up Boolean to set this call to "up" when true + * @param o Pointer to the program options + * @param tt Pointer to the connection's tuntap struct + */ +void run_dns_up_down(bool up, struct options *o, const struct tuntap *tt); + /** * Puts the DNS options into an environment set. * diff --git a/src/openvpn/init.c b/src/openvpn/init.c index 1be205ba..9eb82904 100644 --- a/src/openvpn/init.c +++ b/src/openvpn/init.c @@ -2026,6 +2026,8 @@ do_open_tun(struct context *c, int *error_flags) c->c2.frame.tun_mtu, c->c2.es, &c->net_ctx); } + run_dns_up_down(true, &c->options, c->c1.tuntap); + /* run the up script */ run_up_down(c->options.up_script, c->plugins, @@ -2064,6 +2066,8 @@ do_open_tun(struct context *c, int *error_flags) /* explicitly set the ifconfig_* env vars */ do_ifconfig_setenv(c->c1.tuntap, c->c2.es); + run_dns_up_down(true, &c->options, c->c1.tuntap); + /* run the up script if user specified --up-restart */ if (c->options.up_restart) { @@ -2152,6 +2156,8 @@ do_close_tun(struct context *c, bool force) adapter_index = c->c1.tuntap->adapter_index; #endif + run_dns_up_down(false, &c->options, c->c1.tuntap); + if (force || !(c->sig->signal_received == SIGUSR1 && c->options.persist_tun)) { static_context = NULL; diff --git a/src/openvpnserv/interactive.c b/src/openvpnserv/interactive.c index c6963b36..3cefcc7b 100644 --- a/src/openvpnserv/interactive.c +++ b/src/openvpnserv/interactive.c @@ -88,6 +88,7 @@ typedef enum { wfp_block, undo_dns4, undo_dns6, + undo_nrpt, undo_domains, undo_ring_buffer, undo_wins, @@ -119,12 +120,20 @@ typedef union { flush_neighbors_message_t flush_neighbors; wfp_block_message_t wfp_block; dns_cfg_message_t dns; + nrpt_dns_cfg_message_t nrpt_dns; enable_dhcp_message_t dhcp; register_ring_buffers_message_t rrb; set_mtu_message_t mtu; wins_cfg_message_t wins; } pipe_message_t; +typedef struct { + CHAR addresses[NRPT_ADDR_NUM * NRPT_ADDR_SIZE]; + WCHAR domains[512]; /* MULTI_SZ string */ + DWORD domains_size; /* bytes in domains */ +} nrpt_exclude_data_t; + + static DWORD AddListItem(list_item_t **pfirst, LPVOID data) { @@ -1194,13 +1203,13 @@ ApplyDnsSettings(BOOL apply_gpol) if (apply_gpol && ApplyGpolSettings() == FALSE) { - MsgToEventLog(M_ERR, L"%s: sending GPOL notification failed", __func__); + MsgToEventLog(M_ERR, L"%S: sending GPOL notification failed", __func__); } scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if (scm == NULL) { - MsgToEventLog(M_ERR, L"%s: OpenSCManager call failed (%lu)", + MsgToEventLog(M_ERR, L"%S: OpenSCManager call failed (%lu)", __func__, GetLastError()); goto out; } @@ -1208,7 +1217,7 @@ ApplyDnsSettings(BOOL apply_gpol) dnssvc = OpenServiceA(scm, "Dnscache", SERVICE_PAUSE_CONTINUE); if (dnssvc == NULL) { - MsgToEventLog(M_ERR, L"%s: OpenService call failed (%lu)", + MsgToEventLog(M_ERR, L"%S: OpenService call failed (%lu)", __func__, GetLastError()); goto out; } @@ -1216,7 +1225,7 @@ ApplyDnsSettings(BOOL apply_gpol) SERVICE_STATUS status; if (ControlService(dnssvc, SERVICE_CONTROL_PARAMCHANGE, &status) == 0) { - MsgToEventLog(M_ERR, L"%s: ControlService call failed (%lu)", + MsgToEventLog(M_ERR, L"%S: ControlService call failed (%lu)", __func__, GetLastError()); goto out; } @@ -1255,19 +1264,19 @@ InterfaceIdString(PCSTR itf_name, PWSTR str, size_t len) err = InterfaceLuid(itf_name, &luid); if (err) { - MsgToEventLog(M_ERR, L"%s: failed to convert itf alias '%s'", __func__, itf_name); + MsgToEventLog(M_ERR, L"%S: failed to convert itf alias '%s'", __func__, itf_name); goto out; } err = ConvertInterfaceLuidToGuid(&luid, &guid); if (err) { - MsgToEventLog(M_ERR, L"%s: Failed to convert itf '%s' LUID", __func__, itf_name); + MsgToEventLog(M_ERR, L"%S: Failed to convert itf '%s' LUID", __func__, itf_name); goto out; } if (StringFromIID(&guid, &iid_str) != S_OK) { - MsgToEventLog(M_ERR, L"%s: Failed to convert itf '%s' IID", __func__, itf_name); + MsgToEventLog(M_ERR, L"%S: Failed to convert itf '%s' IID", __func__, itf_name); err = ERROR_OUTOFMEMORY; goto out; } @@ -1417,7 +1426,7 @@ InitialSearchListExists(HKEY key) { return FALSE; } - MsgToEventLog(M_ERR, L"%s: failed to get InitialSearchList (%lu)", + MsgToEventLog(M_ERR, L"%S: failed to get InitialSearchList (%lu)", __func__, err); } @@ -1439,7 +1448,7 @@ StoreInitialDnsSearchList(HKEY key, PCWSTR list) { if (!list || wcslen(list) == 0) { - MsgToEventLog(M_ERR, L"StoreInitialDnsSearchList: empty search list"); + MsgToEventLog(M_ERR, L"%S: empty search list", __func__); return FALSE; } @@ -1453,7 +1462,7 @@ StoreInitialDnsSearchList(HKEY key, PCWSTR list) LSTATUS err = RegSetValueExW(key, L"InitialSearchList", 0, REG_SZ, (PBYTE)list, size); if (err) { - MsgToEventLog(M_ERR, L"%s: failed to set InitialSearchList value (%lu)", + MsgToEventLog(M_ERR, L"%S: failed to set InitialSearchList value (%lu)", __func__, err); return FALSE; } @@ -1482,7 +1491,7 @@ AddDnsSearchDomains(HKEY key, BOOL have_list, PCWSTR domains) err = RegGetValueW(key, NULL, L"SearchList", RRF_RT_REG_SZ, NULL, list, &size); if (err) { - MsgToEventLog(M_SYSERR, L"%s: could not get SearchList from registry (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not get SearchList from registry (%lu)", __func__, err); return FALSE; } @@ -1496,7 +1505,7 @@ AddDnsSearchDomains(HKEY key, BOOL have_list, PCWSTR domains) size_t domlen = wcslen(domains); if (listlen + domlen + 2 > _countof(list)) { - MsgToEventLog(M_SYSERR, L"%s: not enough space in list for search domains (len=%lu)", + MsgToEventLog(M_SYSERR, L"%S: not enough space in list for search domains (len=%lu)", __func__, domlen); return FALSE; } @@ -1515,7 +1524,7 @@ AddDnsSearchDomains(HKEY key, BOOL have_list, PCWSTR domains) err = RegSetValueExW(key, L"SearchList", 0, REG_SZ, (PBYTE)list, size); if (err) { - MsgToEventLog(M_SYSERR, L"%s: could not set SearchList to registry (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not set SearchList to registry (%lu)", __func__, err); return FALSE; } @@ -1547,7 +1556,7 @@ ResetDnsSearchDomains(HKEY key) { if (err != ERROR_FILE_NOT_FOUND) { - MsgToEventLog(M_SYSERR, L"%s: could not get InitialSearchList from registry (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not get InitialSearchList from registry (%lu)", __func__, err); } goto out; @@ -1557,7 +1566,7 @@ ResetDnsSearchDomains(HKEY key) err = RegSetValueExW(key, L"SearchList", 0, REG_SZ, (PBYTE)list, size); if (err) { - MsgToEventLog(M_SYSERR, L"%s: could not set SearchList in registry (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not set SearchList in registry (%lu)", __func__, err); goto out; } @@ -1585,7 +1594,7 @@ RemoveDnsSearchDomains(HKEY key, PCWSTR domains) err = RegGetValueW(key, NULL, L"SearchList", RRF_RT_REG_SZ, NULL, list, &size); if (err) { - MsgToEventLog(M_SYSERR, L"%s: could not get SearchList from registry (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not get SearchList from registry (%lu)", __func__, err); return; } @@ -1593,7 +1602,7 @@ RemoveDnsSearchDomains(HKEY key, PCWSTR domains) PWSTR dst = wcsstr(list, domains); if (!dst) { - MsgToEventLog(M_ERR, L"%s: could not find domains in search list", __func__); + MsgToEventLog(M_ERR, L"%S: could not find domains in search list", __func__); return; } @@ -1613,7 +1622,7 @@ RemoveDnsSearchDomains(HKEY key, PCWSTR domains) err = RegGetValueW(key, NULL, L"InitialSearchList", RRF_RT_REG_SZ, NULL, initial, &size); if (err) { - MsgToEventLog(M_SYSERR, L"%s: could not get InitialSearchList from registry (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not get InitialSearchList from registry (%lu)", __func__, err); return; } @@ -1630,7 +1639,7 @@ RemoveDnsSearchDomains(HKEY key, PCWSTR domains) err = RegSetValueExW(key, L"SearchList", 0, REG_SZ, (PBYTE)list, size); if (err) { - MsgToEventLog(M_SYSERR, L"%s: could not set SearchList in registry (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not set SearchList in registry (%lu)", __func__, err); } } @@ -1687,7 +1696,7 @@ SetDnsSearchDomains(PCSTR itf_name, PCSTR domains, PBOOL gpol, undo_lists_t *lis BOOL have_list = GetDnsSearchListKey(itf_name, gpol, &list_key); if (list_key == INVALID_HANDLE_VALUE) { - MsgToEventLog(M_SYSERR, L"%s: could not get search list registry key", __func__); + MsgToEventLog(M_SYSERR, L"%S: could not get search list registry key", __func__); return ERROR_FILE_NOT_FOUND; } @@ -1756,7 +1765,7 @@ GetInterfacesKey(short family, PHKEY key) if (err) { *key = INVALID_HANDLE_VALUE; - MsgToEventLog(M_SYSERR, L"%s: could not open interfaces registry key for family %d (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not open interfaces registry key for family %d (%lu)", __func__, family, err); } @@ -1787,7 +1796,7 @@ SetNameServersValue(PCWSTR itf_id, short family, PCSTR value) err = RegOpenKeyExW(itfs, itf_id, 0, KEY_ALL_ACCESS, &itf); if (err) { - MsgToEventLog(M_SYSERR, L"%s: could not open interface key for %s family %d (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not open interface key for %s family %d (%lu)", __func__, itf_id, family, err); goto out; } @@ -1795,7 +1804,7 @@ SetNameServersValue(PCWSTR itf_id, short family, PCSTR value) err = RegSetValueExA(itf, "NameServer", 0, REG_SZ, (PBYTE)value, strlen(value) + 1); if (err) { - MsgToEventLog(M_SYSERR, L"%s: could not set name servers '%S' for %s family %d (%lu)", + MsgToEventLog(M_SYSERR, L"%S: could not set name servers '%S' for %s family %d (%lu)", __func__, value, itf_id, family, err); } @@ -1947,6 +1956,903 @@ HandleDNSConfigMessage(const dns_cfg_message_t *msg, undo_lists_t *lists) return err; } +/** + * Checks if DHCP is enabled for an interface + * + * @param key HKEY of the interface to check for + * + * @return BOOL set to TRUE if DHCP is enabled, or FALSE if + * disabled or an error occurred + */ +static BOOL +IsDhcpEnabled(HKEY key) +{ + DWORD dhcp; + DWORD size = sizeof(dhcp); + LSTATUS err; + + err = RegGetValueA(key, NULL, "EnableDHCP", RRF_RT_REG_DWORD, NULL, (PBYTE)&dhcp, &size); + if (err != NO_ERROR) + { + MsgToEventLog(M_SYSERR, L"%S: Could not read DHCP status (%lu)", __func__, err); + return FALSE; + } + + return dhcp ? TRUE : FALSE; +} + +/** + * Set name servers from a NRPT address list + * + * @param itf_id the VPN interface ID to set the name servers for + * @param addresses the list of NRPT addresses + * + * @return LSTATUS NO_ERROR in case of success, a Windows error code otherwise + */ +static LSTATUS +SetNameServerAddresses(PWSTR itf_id, const nrpt_address_t *addresses) +{ + const short families[] = { AF_INET, AF_INET6 }; + for (int i = 0; i < _countof(families); i++) + { + short family = families[i]; + + /* Create a comma sparated list of addresses of this family */ + int offset = 0; + char addr_list[NRPT_ADDR_SIZE * NRPT_ADDR_NUM]; + for (int j = 0; j < NRPT_ADDR_NUM && addresses[j][0]; j++) + { + if ((family == AF_INET6 && strchr(addresses[j], ':') == NULL) + || (family == AF_INET && strchr(addresses[j], ':') != NULL)) + { + /* Address family doesn't match, skip this one */ + continue; + } + if (offset) + { + addr_list[offset++] = ','; + } + strcpy(addr_list + offset, addresses[j]); + offset += strlen(addresses[j]); + } + + if (offset == 0) + { + /* No address for this family to set */ + continue; + } + + /* Set name server addresses */ + LSTATUS err = SetNameServers(itf_id, family, addr_list); + if (err) + { + return err; + } + } + return NO_ERROR; +} + +/** + * Get DNS server IPv4 addresses of an interface + * + * @param itf_key registry key of the IPv4 interface data + * @param addrs pointer to the buffer addresses are returned in + * @param size pointer to the size of the buffer, contains the + * size of the addresses on return + * + * @return LSTATUS NO_ERROR on success, a Windows error code otherwise + */ +static LSTATUS +GetItfDnsServersV4(HKEY itf_key, PSTR addrs, PDWORD size) +{ + addrs[*size - 1] = '\0'; + + LSTATUS err; + DWORD s = *size; + err = RegGetValueA(itf_key, NULL, "NameServer", RRF_RT_REG_SZ, NULL, (PBYTE)addrs, &s); + if (err && err != ERROR_FILE_NOT_FOUND) + { + *size = 0; + return err; + } + + /* Try DHCP addresses if we don't have some already */ + if (!strchr(addrs, '.') && IsDhcpEnabled(itf_key)) + { + s = *size; + RegGetValueA(itf_key, NULL, "DhcpNameServer", RRF_RT_REG_SZ, NULL, (PBYTE)addrs, &s); + if (err) + { + *size = 0; + return err; + } + } + + if (strchr(addrs, '.')) + { + *size = s; + return NO_ERROR; + } + + *size = 0; + return ERROR_FILE_NOT_FOUND; +} + +/** + * Get DNS server IPv6 addresses of an interface + * + * @param itf_key registry key of the IPv6 interface data + * @param addrs pointer to the buffer addresses are returned in + * @param size pointer to the size of the buffer + * + * @return LSTATUS NO_ERROR on success, a Windows error code otherwise + */ +static LSTATUS +GetItfDnsServersV6(HKEY itf_key, PSTR addrs, PDWORD size) +{ + addrs[*size - 1] = '\0'; + + LSTATUS err; + DWORD s = *size; + err = RegGetValueA(itf_key, NULL, "NameServer", RRF_RT_REG_SZ, NULL, (PBYTE)addrs, &s); + if (err && err != ERROR_FILE_NOT_FOUND) + { + *size = 0; + return err; + } + + /* Try DHCP addresses if we don't have some already */ + if (!strchr(addrs, ':') && IsDhcpEnabled(itf_key)) + { + IN6_ADDR in_addrs[8]; + DWORD in_addrs_size = sizeof(in_addrs); + err = RegGetValueA(itf_key, NULL, "Dhcpv6DNSServers", RRF_RT_REG_BINARY, NULL, + (PBYTE)in_addrs, &in_addrs_size); + if (err) + { + *size = 0; + return err; + } + + s = *size; + PSTR pos = addrs; + size_t in_addrs_read = in_addrs_size / sizeof(IN6_ADDR); + for (size_t i = 0; i < in_addrs_read; ++i) + { + if (i != 0) + { + /* Add separator */ + *pos++ = ','; + s--; + } + + if (inet_ntop(AF_INET6, &in_addrs[i], + pos, s) != NULL) + { + *size = 0; + return ERROR_MORE_DATA; + } + + size_t addr_len = strlen(pos); + pos += addr_len; + s -= addr_len; + } + s = strlen(addrs) + 1; + } + + if (strchr(addrs, ':')) + { + *size = s; + return NO_ERROR; + } + + *size = 0; + return ERROR_FILE_NOT_FOUND; +} + +/** + * Return interface specific domain suffix(es) + * + * The \p domains paramter will be set to a MULTI_SZ domains string. + * In case of an error or if no domains are found for the interface + * \p size is set to 0 and the contents of \p domains are invalid. + * Note that the domains could have been set by DHCP or manually. + * + * @param itf HKEY of the interface to read from + * @param domains PWSTR buffer to return the domain(s) in + * @param size pointer to size of the domains buffer in bytes. Will be + * set to the size of the string returned, including + * the terminating zeros or 0. + * + * @return LSTATUS NO_ERROR if the domain suffix(es) were read successfully, + * ERROR_FILE_NOT_FOUND if no domain was found for the interface, + * ERROR_MORE_DATA if the list did not fit into the buffer, + * any other error indicates an error while reading from the registry. + */ +static LSTATUS +GetItfDnsDomains(HKEY itf, PWSTR domains, PDWORD size) +{ + if (domains == NULL || size == 0) + { + return ERROR_INVALID_PARAMETER; + } + + LSTATUS err = ERROR_FILE_NOT_FOUND; + const DWORD buf_size = *size; + const size_t one_glyph = sizeof(*domains); + PWSTR values[] = { L"SearchList", L"Domain", L"DhcpDomainSearchList", L"DhcpDomain", NULL}; + + for (int i = 0; values[i]; i++) + { + *size = buf_size; + err = RegGetValueW(itf, NULL, values[i], RRF_RT_REG_SZ, NULL, (PBYTE)domains, size); + if (!err && *size > one_glyph && wcschr(domains, '.')) + { + /* + * Found domain(s), now convert them: + * - prefix each domain with a dot + * - convert comma separated list to MULTI_SZ + */ + PWCHAR pos = domains; + const DWORD buf_len = buf_size / one_glyph; + while (TRUE) + { + /* Terminate the domain at the next comma */ + PWCHAR comma = wcschr(pos, ','); + if (comma) + { + *comma = '\0'; + } + + /* Check for enough space to convert this domain */ + size_t converted_size = pos - domains; + size_t domain_len = wcslen(pos) + 1; + size_t domain_size = domain_len * one_glyph; + size_t extra_size = 2 * one_glyph; + if (converted_size + domain_size + extra_size > buf_size) + { + /* Domain doesn't fit, bad luck if it's the first one */ + *pos = '\0'; + *size = converted_size == 0 ? 0 : *size + 1; + return ERROR_MORE_DATA; + } + + /* Prefix domain at pos with the dot */ + memmove(pos + 1, pos, buf_size - converted_size - one_glyph); + domains[buf_len - 1] = '\0'; + *pos = '.'; + *size += 1; + + if (!comma) + { + /* Conversion is done */ + *(pos + domain_len) = '\0'; + *size += 1; + return NO_ERROR; + } + + pos = comma + 1; + } + } + } + + *size = 0; + return err; +} + +/** + * Check if an interface is connected and up + * + * @param iid_str the interface GUID as string + * + * @return TRUE if the interface is connected and up, FALSE otherwise or in + * case an error happened + */ +static BOOL +IsInterfaceConnected(PWSTR iid_str) +{ + GUID iid; + BOOL res = FALSE; + MIB_IF_ROW2 itf_row; + + /* Get GUID from string */ + if (IIDFromString(iid_str, &iid) != S_OK) + { + MsgToEventLog(M_SYSERR, L"%S: could not convert interface %s GUID string", __func__, iid_str); + goto out; + } + + /* Get LUID from GUID */ + if (ConvertInterfaceGuidToLuid(&iid, &itf_row.InterfaceLuid) != NO_ERROR) + { + goto out; + } + + /* Look up interface status */ + if (GetIfEntry2(&itf_row) != NO_ERROR) + { + MsgToEventLog(M_SYSERR, L"%S: could not get interface %s status", __func__, iid_str); + goto out; + } + + if (itf_row.MediaConnectState == MediaConnectStateConnected + && itf_row.OperStatus == IfOperStatusUp) + { + res = TRUE; + } + +out: + return res; +} + +/** + * Collect interface DNS settings to be used in excluding NRPT rules. This is + * needed so that local DNS keeps working even when a catch all NRPT rule is + * installed by a VPN connection. + * + * @param data pointer to the data structures the values are returned in + * @param data_size number of exclude data structures pointed to + */ +static void +GetNrptExcludeData(nrpt_exclude_data_t *data, size_t data_size) +{ + HKEY v4_itfs = INVALID_HANDLE_VALUE; + HKEY v6_itfs = INVALID_HANDLE_VALUE; + + if (!GetInterfacesKey(AF_INET, &v4_itfs) + || !GetInterfacesKey(AF_INET6, &v6_itfs)) + { + goto out; + } + + size_t i = 0; + DWORD enum_index = 0; + while (i < data_size) + { + WCHAR itf_guid[MAX_PATH]; + DWORD itf_guid_len = _countof(itf_guid); + LSTATUS err = RegEnumKeyExW(v4_itfs, enum_index++, itf_guid, &itf_guid_len, + NULL, NULL, NULL, NULL); + if (err) + { + if (err != ERROR_NO_MORE_ITEMS) + { + MsgToEventLog(M_SYSERR, L"%S: could not enumerate interfaces (%lu)", __func__, err); + } + goto out; + } + + /* Ignore interfaces that are not connected or disabled */ + if (!IsInterfaceConnected(itf_guid)) + { + continue; + } + + HKEY v4_itf; + if (RegOpenKeyExW(v4_itfs, itf_guid, 0, KEY_READ, &v4_itf) != NO_ERROR) + { + MsgToEventLog(M_SYSERR, L"%S: could not open interface %s v4 registry key", __func__, itf_guid); + goto out; + } + + /* Get the DNS domain(s) for exclude routing */ + data[i].domains_size = sizeof(data[0].domains); + memset(data[i].domains, 0, data[i].domains_size); + err = GetItfDnsDomains(v4_itf, data[i].domains, &data[i].domains_size); + if (err) + { + if (err != ERROR_FILE_NOT_FOUND) + { + MsgToEventLog(M_SYSERR, L"%S: could not read interface %s domain suffix", __func__, itf_guid); + } + goto next_itf; + } + + /* Get the IPv4 DNS servers */ + DWORD v4_addrs_size = sizeof(data[0].addresses); + err = GetItfDnsServersV4(v4_itf, data[i].addresses, &v4_addrs_size); + if (err && err != ERROR_FILE_NOT_FOUND) + { + MsgToEventLog(M_SYSERR, L"%S: could not read interface %s v4 name servers (%ld)", + __func__, itf_guid, err); + goto next_itf; + } + + /* Get the IPv6 DNS servers, if there's space left */ + PSTR v6_addrs = data[i].addresses + v4_addrs_size; + DWORD v6_addrs_size = sizeof(data[0].addresses) - v4_addrs_size; + if (v6_addrs_size > NRPT_ADDR_SIZE) + { + HKEY v6_itf; + if (RegOpenKeyExW(v6_itfs, itf_guid, 0, KEY_READ, &v6_itf) != NO_ERROR) + { + MsgToEventLog(M_SYSERR, L"%S: could not open interface %s v6 registry key", __func__, itf_guid); + goto next_itf; + } + err = GetItfDnsServersV6(v6_itf, v6_addrs, &v6_addrs_size); + RegCloseKey(v6_itf); + if (err && err != ERROR_FILE_NOT_FOUND) + { + MsgToEventLog(M_SYSERR, L"%S: could not read interface %s v6 name servers (%ld)", + __func__, itf_guid, err); + goto next_itf; + } + } + + if (v4_addrs_size || v6_addrs_size) + { + /* Replace comma-delimters with semicolons, as required by NRPT */ + for (int j = 0; j < sizeof(data[0].addresses) && data[i].addresses[j]; j++) + { + if (data[i].addresses[j] == ',') + { + data[i].addresses[j] = ';'; + } + } + ++i; + } + +next_itf: + RegCloseKey(v4_itf); + } + +out: + RegCloseKey(v6_itfs); + RegCloseKey(v4_itfs); +} + +/** + * Set a NRPT rule (subkey) and its values in the registry + * + * @param nrpt_key NRPT registry key handle + * @param subkey subkey string to create + * @param address name server address string + * @param domains domains to resolve by this server as MULTI_SZ + * @param dom_size size of domains in bytes including the terminators + * @param dnssec boolean to determine if DNSSEC is to be enabled + * + * @return NO_ERROR on success, or Windows error code + */ +static DWORD +SetNrptRule(HKEY nrpt_key, PCWSTR subkey, PCSTR address, + PCWSTR domains, DWORD dom_size, BOOL dnssec) +{ + /* Create rule subkey */ + DWORD err = NO_ERROR; + HKEY rule_key; + err = RegCreateKeyExW(nrpt_key, subkey, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &rule_key, NULL); + if (err) + { + return err; + } + + /* Set name(s) for DNS routing */ + err = RegSetValueExW(rule_key, L"Name", 0, REG_MULTI_SZ, (PBYTE)domains, dom_size); + if (err) + { + goto out; + } + + /* Set DNS Server address */ + err = RegSetValueExA(rule_key, "GenericDNSServers", 0, REG_SZ, (PBYTE)address, strlen(address) + 1); + if (err) + { + goto out; + } + + DWORD reg_val; + /* Set DNSSEC if required */ + if (dnssec) + { + reg_val = 1; + err = RegSetValueExA(rule_key, "DNSSECValidationRequired", 0, REG_DWORD, (PBYTE)®_val, sizeof(reg_val)); + if (err) + { + goto out; + } + + reg_val = 0; + err = RegSetValueExA(rule_key, "DNSSECQueryIPSECRequired", 0, REG_DWORD, (PBYTE)®_val, sizeof(reg_val)); + if (err) + { + goto out; + } + + reg_val = 0; + err = RegSetValueExA(rule_key, "DNSSECQueryIPSECEncryption", 0, REG_DWORD, (PBYTE)®_val, sizeof(reg_val)); + if (err) + { + goto out; + } + } + + /* Set NRPT config options */ + reg_val = dnssec ? 0x0000000A : 0x00000008; + err = RegSetValueExA(rule_key, "ConfigOptions", 0, REG_DWORD, (const PBYTE)®_val, sizeof(reg_val)); + if (err) + { + goto out; + } + + /* Mandatory NRPT version */ + reg_val = 2; + err = RegSetValueExA(rule_key, "Version", 0, REG_DWORD, (const PBYTE)®_val, sizeof(reg_val)); + if (err) + { + goto out; + } + +out: + if (err) + { + RegDeleteKeyW(nrpt_key, subkey); + } + RegCloseKey(rule_key); + return err; +} + +/** + * Set NRPT exclude rules to accompany a catch all rule. This is done so that + * local resolution of names is not interfered with in case the VPN resolves + * all names. + * + * @param nrpt_key the registry key to set the rules under + * @param ovpn_pid the PID of the openvpn process + */ +static void +SetNrptExcludeRules(HKEY nrpt_key, DWORD ovpn_pid) +{ + nrpt_exclude_data_t data[8]; /* data from up to 8 interfaces */ + memset(data, 0, sizeof(data)); + GetNrptExcludeData(data, _countof(data)); + + unsigned n = 0; + for (int i = 0; i < _countof(data); ++i) + { + nrpt_exclude_data_t *d = &data[i]; + if (d->domains_size == 0) + { + break; + } + + DWORD err; + WCHAR subkey[48]; + swprintf(subkey, _countof(subkey), L"OpenVPNDNSRoutingX-%02x-%lu", ++n, ovpn_pid); + err = SetNrptRule(nrpt_key, subkey, d->addresses, d->domains, d->domains_size, FALSE); + if (err) + { + MsgToEventLog(M_ERR, L"%S: failed to set rule %s (%lu)", __func__, subkey, err); + } + } +} + +/** + * Set NRPT rules for a openvpn process + * + * @param nrpt_key the registry key to set the rules under + * @param addresses name server addresses + * @param domains optional list of split routing domains + * @param dnssec boolean whether DNSSEC is to be used + * @param ovpn_pid the PID of the openvpn process + * + * @return NO_ERROR on success, or a Windows error code + */ +static DWORD +SetNrptRules(HKEY nrpt_key, const nrpt_address_t *addresses, + const char *domains, BOOL dnssec, DWORD ovpn_pid) +{ + DWORD err = NO_ERROR; + PWSTR wide_domains = L".\0"; /* DNS route everything by default */ + DWORD dom_size = 6; + + /* Prepare DNS routing domains / split DNS */ + if (domains[0]) + { + size_t domains_len = strlen(domains); + dom_size = domains_len + 2; /* len + the trailing NULs */ + + wide_domains = utf8to16_size(domains, dom_size); + dom_size *= sizeof(*wide_domains); + if (!wide_domains) + { + return ERROR_OUTOFMEMORY; + } + /* Make a MULTI_SZ from a comma separated list */ + for (size_t i = 0; i < domains_len; ++i) + { + if (wide_domains[i] == ',') + { + wide_domains[i] = 0; + } + } + } + else + { + SetNrptExcludeRules(nrpt_key, ovpn_pid); + } + + /* Create address string list */ + CHAR addr_list[NRPT_ADDR_NUM * NRPT_ADDR_SIZE]; + PSTR pos = addr_list; + for (int i = 0; i < NRPT_ADDR_NUM && addresses[i][0]; ++i) + { + if (i != 0) + { + *pos++ = ';'; + } + strcpy(pos, addresses[i]); + pos += strlen(pos); + } + + WCHAR subkey[MAX_PATH]; + swprintf(subkey, _countof(subkey), L"OpenVPNDNSRouting-%lu", ovpn_pid); + err = SetNrptRule(nrpt_key, subkey, addr_list, wide_domains, dom_size, dnssec); + if (err) + { + MsgToEventLog(M_ERR, L"%S: failed to set rule %s (%lu)", __func__, subkey, err); + } + + if (domains[0]) + { + free(wide_domains); + } + return err; +} + +/** + * Return the registry key where NRPT rules are stored + * + * @param key pointer to the HKEY it is returned in + * @param gpol pointer to BOOL the use of GPOL hive is returned in + * + * @return NO_ERROR on success, or a Windows error code + */ +static LSTATUS +OpenNrptBaseKey(PHKEY key, PBOOL gpol) +{ + /* + * Registry keys Name Service Policy Table (NRPT) rules can be stored at. + * When the group policy key exists, NRPT rules must be placed there. + * It is created when NRPT rules are pushed via group policy and it + * remains in the registry even if the last GP-NRPT rule is deleted. + */ + static PCSTR gpol_key = "SOFTWARE\\Policies\\Microsoft\\Windows NT\\DNSClient\\DnsPolicyConfig"; + static PCSTR sys_key = "SYSTEM\\CurrentControlSet\\Services\\Dnscache\\Parameters\\DnsPolicyConfig"; + + HKEY nrpt; + *gpol = TRUE; + LSTATUS err = RegOpenKeyExA(HKEY_LOCAL_MACHINE, gpol_key, 0, KEY_ALL_ACCESS, &nrpt); + if (err == ERROR_FILE_NOT_FOUND) + { + *gpol = FALSE; + err = RegOpenKeyExA(HKEY_LOCAL_MACHINE, sys_key, 0, KEY_ALL_ACCESS, &nrpt); + if (err) + { + nrpt = INVALID_HANDLE_VALUE; + } + } + *key = nrpt; + return err; +} + +/** + * Delete OpenVPN NRPT rules from the registry + * + * If the pid parameter is 0 all NRPT rules added by OpenVPN are deleted. + * In all other cases only rules matching the pid are deleted. + * + * @param pid PID of the process to delete the rules for or 0 + * @param gpol + * + * @return BOOL to indicate if rules were deleted + */ +static BOOL +DeleteNrptRules(DWORD pid, PBOOL gpol) +{ + HKEY key; + LSTATUS err = OpenNrptBaseKey(&key, gpol); + if (err) + { + MsgToEventLog(M_SYSERR, L"%S: could not open NRPT base key (%lu)", __func__, err); + return FALSE; + } + + /* PID suffix string to compare against later */ + WCHAR pid_str[16]; + size_t pidlen = 0; + if (pid) + { + swprintf(pid_str, _countof(pid_str), L"-%lu", pid); + pidlen = wcslen(pid_str); + } + + int deleted = 0; + DWORD enum_index = 0; + while (TRUE) + { + WCHAR name[MAX_PATH]; + DWORD namelen = _countof(name); + err = RegEnumKeyExW(key, enum_index++, name, &namelen, NULL, NULL, NULL, NULL); + if (err) + { + if (err != ERROR_NO_MORE_ITEMS) + { + MsgToEventLog(M_SYSERR, L"%S: could not enumerate NRPT rules (%lu)", __func__, err); + } + break; + } + + /* Keep rule if name doesn't match */ + if (wcsncmp(name, L"OpenVPNDNSRouting", 17) != 0 + || (pid && wcsncmp(name + namelen - pidlen, pid_str, pidlen) != 0)) + { + continue; + } + + if (RegDeleteKeyW(key, name) == NO_ERROR) + { + enum_index--; + deleted++; + } + } + + RegCloseKey(key); + return deleted ? TRUE : FALSE; +} + +/** + * Delete a process' NRPT rules and apply the reduced set of rules + * + * @param ovpn_pid OpenVPN process id to delete rules for + */ +static void +UndoNrptRules(DWORD ovpn_pid) +{ + BOOL gpol; + if (DeleteNrptRules(ovpn_pid, &gpol)) + { + ApplyDnsSettings(gpol); + } +} + +/** + * Add Name Resolution Policy Table (NRPT) rules as documented in + * https://msdn.microsoft.com/en-us/library/ff957356.aspx for DNS name + * resolution, as well as DNS search domain(s), if given. + * + * @param msg config messages sent by the openvpn process + * @param ovpn_pid process id of the sending openvpn process + * @param lists undo lists for this process + * + * @return NO_ERROR on success, or a Windows error code + */ +static DWORD +HandleDNSConfigNrptMessage(const nrpt_dns_cfg_message_t *msg, + DWORD ovpn_pid, undo_lists_t *lists) +{ + /* + * Use a non-const reference with limited scope to + * enforce null-termination of strings from client + */ + { + nrpt_dns_cfg_message_t *msgptr = (nrpt_dns_cfg_message_t *) msg; + msgptr->iface.name[_countof(msg->iface.name) - 1] = '\0'; + msgptr->search_domains[_countof(msg->search_domains) - 1] = '\0'; + msgptr->resolve_domains[_countof(msg->resolve_domains) - 1] = '\0'; + for (size_t i = 0; i < NRPT_ADDR_NUM; ++i) + { + msgptr->addresses[i][_countof(msg->addresses[0]) - 1] = '\0'; + } + } + + /* Make sure we have the VPN interface name */ + if (msg->iface.name[0] == 0) + { + return ERROR_MESSAGE_DATA; + } + + /* Some sanity checks on the add message data */ + if (msg->header.type == msg_add_nrpt_cfg) + { + /* At least one name server address is set */ + if (msg->addresses[0][0] == 0) + { + return ERROR_MESSAGE_DATA; + } + /* Resolve domains are double zero terminated (MULTI_SZ) */ + const char *rdom = msg->resolve_domains; + size_t rdom_size = sizeof(msg->resolve_domains); + size_t rdom_len = strlen(rdom); + if (rdom_len && (rdom_len + 1 >= rdom_size || rdom[rdom_len + 2] != 0)) + { + return ERROR_MESSAGE_DATA; + } + } + + BOOL gpol_nrpt = FALSE; + BOOL gpol_list = FALSE; + + WCHAR iid[64]; + DWORD iid_err = InterfaceIdString(msg->iface.name, iid, _countof(iid)); + if (iid_err) + { + return iid_err; + } + + /* Delete previously set values for this instance first, if any */ + PDWORD undo_pid = RemoveListItem(&(*lists)[undo_nrpt], CmpAny, NULL); + if (undo_pid) + { + if (*undo_pid != ovpn_pid) + { + MsgToEventLog(M_INFO, + L"%S: PID stored for undo doesn't match: %lu vs %lu. " + "This is likely an error. Cleaning up anyway.", + __func__, *undo_pid, ovpn_pid); + } + DeleteNrptRules(*undo_pid, &gpol_nrpt); + free(undo_pid); + + ResetNameServers(iid, AF_INET); + ResetNameServers(iid, AF_INET6); + } + SetDnsSearchDomains(msg->iface.name, NULL, &gpol_list, lists); + + if (msg->header.type == msg_del_nrpt_cfg) + { + ApplyDnsSettings(gpol_nrpt || gpol_list); + return NO_ERROR; /* Done dealing with del message */ + } + + HKEY key; + LSTATUS err = OpenNrptBaseKey(&key, &gpol_nrpt); + if (err) + { + goto out; + } + + /* Add undo information first in case there's no heap left */ + PDWORD pid = malloc(sizeof(ovpn_pid)); + if (!pid) + { + err = ERROR_OUTOFMEMORY; + goto out; + } + *pid = ovpn_pid; + if (AddListItem(&(*lists)[undo_nrpt], pid)) + { + err = ERROR_OUTOFMEMORY; + free(pid); + goto out; + } + + /* Set NRPT rules */ + BOOL dnssec = (msg->flags & nrpt_dnssec) != 0; + err = SetNrptRules(key, msg->addresses, msg->resolve_domains, dnssec, ovpn_pid); + if (err) + { + goto out; + } + + /* Set name servers */ + err = SetNameServerAddresses(iid, msg->addresses); + if (err) + { + goto out; + } + + /* Set search domains, if any */ + if (msg->search_domains[0]) + { + err = SetDnsSearchDomains(msg->iface.name, msg->search_domains, &gpol_list, lists); + } + + ApplyDnsSettings(gpol_nrpt || gpol_list); + +out: + return err; +} + static DWORD HandleWINSConfigMessage(const wins_cfg_message_t *msg, undo_lists_t *lists) { @@ -2202,7 +3108,7 @@ HandleMTUMessage(const set_mtu_message_t *mtu) } static VOID -HandleMessage(HANDLE pipe, HANDLE ovpn_proc, +HandleMessage(HANDLE pipe, PPROCESS_INFORMATION proc_info, DWORD bytes, DWORD count, LPHANDLE events, undo_lists_t *lists) { pipe_message_t msg; @@ -2265,6 +3171,14 @@ HandleMessage(HANDLE pipe, HANDLE ovpn_proc, ack.error_number = HandleDNSConfigMessage(&msg.dns, lists); break; + case msg_add_nrpt_cfg: + case msg_del_nrpt_cfg: + { + DWORD ovpn_pid = proc_info->dwProcessId; + ack.error_number = HandleDNSConfigNrptMessage(&msg.nrpt_dns, ovpn_pid, lists); + } + break; + case msg_add_wins_cfg: case msg_del_wins_cfg: ack.error_number = HandleWINSConfigMessage(&msg.wins, lists); @@ -2280,7 +3194,8 @@ HandleMessage(HANDLE pipe, HANDLE ovpn_proc, case msg_register_ring_buffers: if (msg.header.size == sizeof(msg.rrb)) { - ack.error_number = HandleRegisterRingBuffers(&msg.rrb, ovpn_proc, lists); + HANDLE ovpn_hnd = proc_info->hProcess; + ack.error_number = HandleRegisterRingBuffers(&msg.rrb, ovpn_hnd, lists); } break; @@ -2331,6 +3246,10 @@ Undo(undo_lists_t *lists) ResetNameServers(item->data, AF_INET6); break; + case undo_nrpt: + UndoNrptRules(*(PDWORD)item->data); + break; + case undo_domains: UndoDnsSearchDomains(item->data); break; @@ -2652,7 +3571,7 @@ RunOpenvpn(LPVOID p) break; } - HandleMessage(ovpn_pipe, proc_info.hProcess, bytes, 1, &exit_event, &undo_lists); + HandleMessage(ovpn_pipe, &proc_info, bytes, 1, &exit_event, &undo_lists); } WaitForSingleObject(proc_info.hProcess, IO_TIMEOUT); @@ -2848,24 +3767,28 @@ ServiceStartInteractiveOwn(DWORD dwArgc, LPWSTR *lpszArgv) static void CleanupRegistry(void) { - HKEY key; - DWORD changed = 0; + BOOL changed = FALSE; + + /* Clean up leftover NRPT rules */ + BOOL gpol_nrpt; + changed = DeleteNrptRules(0, &gpol_nrpt); /* Clean up leftover DNS search list fragments */ + HKEY key; BOOL gpol_list; GetDnsSearchListKey(NULL, &gpol_list, &key); if (key != INVALID_HANDLE_VALUE) { if (ResetDnsSearchDomains(key)) { - changed++; + changed = TRUE; } RegCloseKey(key); } if (changed) { - ApplyDnsSettings(gpol_list); + ApplyDnsSettings(gpol_nrpt || gpol_list); } }