apply --dns options the new way

Previous to this --dns and DNS related --dhcp-options shared the same
code to apply the settings to Windows and macOS systems. So, both
options were pretty much just aliases, with --dns offering more and
finer grained settings that were mostly ignored.

Now --dhcp-options are applied the way they have always been and --dns
does it its own - the new - way. Reason for this behavioral change is
foremost that we want it to be the same between openvpn version 2 and
version 3. But there are also a few new features (e.g. DNSSEC), previously
not present with the --dhcp-options.

The name server and split-domain configuration is exclusively set via
NRPT on Windows, since it overrules any other resolver setting. If there
is no split DNS configured and all domains are resolved using the pushed
name server, we make sure that local domain names are still resolvable by
adding so called exclude NRPT rules, that make sure local domains get
resolved by their local DNS resolvers.

Since Windows does not know about alternative secure transports, the
'transport' and 'sni' settings are ignored.

For macOS the 'dnssec' setting is ignored in addition to that. Besides
that not much does change on that platform. In case of --dns options the
explicit values are used now. The API in use may be changed at a later time.

Signed-off-by: Heiko Hund <heiko@openvpn.net>
This commit is contained in:
Heiko Hund
2024-04-24 01:03:12 +02:00
parent 9bc6986873
commit d7606f4cfb
11 changed files with 1903 additions and 385 deletions

View File

@@ -21,26 +21,132 @@
#pragma once
#include <openvpn/options/continuation.hpp>
#include <openvpn/common/hostport.hpp>
#include <openvpn/common/number.hpp>
#include <openvpn/addr/ip.hpp>
#include <map>
#include <vector>
#include <cstdint>
#include <algorithm>
#include <sstream>
#include <openvpn/options/continuation.hpp>
#include <openvpn/common/hostport.hpp>
#include <openvpn/common/number.hpp>
#include <openvpn/common/jsonlib.hpp>
#include <openvpn/addr/ip.hpp>
#ifdef HAVE_JSON
#include <openvpn/common/jsonhelper.hpp>
#endif
namespace openvpn {
/**
* @class A name server address and optional port
*/
struct DnsAddress
{
/**
* @brief Return string representation of the DnsAddress object
*
* @return std::string the string representation generated
*/
std::string to_string() const
{
std::ostringstream os;
os << address.to_string();
if (port)
{
os << " " << port;
}
return os.str();
}
void validate(const std::string &title) const
{
IP::Addr::validate(address.to_string(), title);
}
#ifdef HAVE_JSON
Json::Value to_json() const
{
Json::Value root(Json::objectValue);
root["address"] = Json::Value(address.to_string());
if (port)
{
root["port"] = Json::Value(port);
}
return root;
}
void from_json(const Json::Value &root, const std::string &title)
{
json::assert_dict(root, title);
json::to_uint_optional(root, port, "port", 0u, title);
std::string addr_str;
json::to_string(root, addr_str, "address", title);
address = IP::Addr::from_string(addr_str);
}
#endif
IP::Addr address;
unsigned int port = 0;
};
/**
* @class A DNS domain name
*/
struct DnsDomain
{
/**
* @brief Return string representation of the DnsDomain object
*
* @return std::string the string representation generated
*/
std::string to_string() const
{
return domain;
}
void validate(const std::string &title) const
{
HostPort::validate_host(domain, title);
}
#ifdef HAVE_JSON
Json::Value to_json() const
{
return Json::Value(domain);
}
void from_json(const Json::Value &value, const std::string &title)
{
if (!value.isString())
{
throw json::json_parse("string " + title + " is of incorrect type");
}
domain = value.asString();
}
#endif
std::string domain;
};
/**
* @class DNS settings for a name server
*/
struct DnsServer
{
enum class DomainType
static std::int32_t parse_priority(const std::string &prio_str)
{
Unset,
Resolve,
Exclude
};
const auto min_prio = std::numeric_limits<std::int8_t>::min();
const auto max_prio = std::numeric_limits<std::int8_t>::max();
std::int32_t priority;
if (!parse_number_validate<std::int32_t>(prio_str, 4, min_prio, max_prio, &priority))
OPENVPN_THROW_ARG1(option_error, ERR_INVALID_OPTION_DNS, "dns server priority '" << prio_str << "' invalid");
return priority;
}
enum class Security
{
Unset,
@@ -48,6 +154,22 @@ struct DnsServer
Yes,
Optional
};
std::string dnssec_string(const Security dnssec) const
{
switch (dnssec)
{
case Security::No:
return "No";
case Security::Yes:
return "Yes";
case Security::Optional:
return "Optional";
default:
return "Unset";
}
}
enum class Transport
{
Unset,
@@ -56,26 +178,135 @@ struct DnsServer
TLS
};
IPv4::Addr address4;
IPv6::Addr address6;
unsigned int port4 = 0;
unsigned int port6 = 0;
std::vector<std::string> domains;
DomainType domain_type = DomainType::Unset;
std::string transport_string(const Transport transport) const
{
switch (transport)
{
case Transport::Plain:
return "Plain";
case Transport::HTTPS:
return "HTTPS";
case Transport::TLS:
return "TLS";
default:
return "Unset";
}
}
std::string to_string(const char *prefix = "") const
{
std::ostringstream os;
os << prefix << "Addresses:" << std::endl;
for (const auto &address : addresses)
{
os << prefix << " " << address.to_string() << std::endl;
}
if (!domains.empty())
{
os << prefix << "Domains:" << std::endl;
for (const auto &domain : domains)
{
os << prefix << " " << domain.to_string() << std::endl;
}
}
if (dnssec != Security::Unset)
{
os << prefix << "DNSSEC: " << dnssec_string(dnssec) << std::endl;
}
if (transport != Transport::Unset)
{
os << prefix << "Transport: " << transport_string(transport) << std::endl;
}
if (!sni.empty())
{
os << prefix << "SNI: " << sni << std::endl;
}
return os.str();
}
#ifdef HAVE_JSON
Json::Value to_json() const
{
Json::Value server(Json::objectValue);
json::from_vector(server, addresses, "addresses");
if (!domains.empty())
{
json::from_vector(server, domains, "domains");
}
if (dnssec != Security::Unset)
{
server["dnssec"] = Json::Value(dnssec_string(dnssec));
}
if (transport != Transport::Unset)
{
server["transport"] = Json::Value(transport_string(transport));
}
if (!sni.empty())
{
server["sni"] = Json::Value(sni);
}
return server;
}
void from_json(const Json::Value &root, const std::string &title)
{
json::assert_dict(root, title);
json::to_vector(root, addresses, "addresses", title);
if (json::exists(root, "domains"))
{
json::to_vector(root, domains, "domains", title);
}
if (json::exists(root, "dnssec"))
{
std::string dnssec_str;
json::to_string(root, dnssec_str, "dnssec", title);
if (dnssec_str == "Optional")
{
dnssec = Security::Optional;
}
else if (dnssec_str == "Yes")
{
dnssec = Security::Yes;
}
else if (dnssec_str == "No")
{
dnssec = Security::No;
}
else
{
throw json::json_parse("dnssec value " + dnssec_str + "is unknown");
}
}
if (json::exists(root, "transport"))
{
std::string transport_str;
json::to_string(root, transport_str, "transport", title);
if (transport_str == "Plain")
{
transport = Transport::Plain;
}
else if (transport_str == "HTTPS")
{
transport = Transport::HTTPS;
}
else if (transport_str == "TLS")
{
transport = Transport::TLS;
}
else
{
throw json::json_parse("transport value " + transport_str + "is unknown");
}
}
json::to_string_optional(root, sni, "sni", "", title);
}
#endif
std::vector<DnsAddress> addresses;
std::vector<DnsDomain> domains;
Security dnssec = Security::Unset;
Transport transport = Transport::Unset;
std::string sni;
static std::int32_t parse_priority(const std::string &prio_str)
{
const auto min = std::numeric_limits<std::int8_t>::min();
const auto max = std::numeric_limits<std::int8_t>::max();
std::int32_t priority;
if (!parse_number_validate<std::int32_t>(prio_str, 4, min, max, &priority))
OPENVPN_THROW_ARG1(option_error, ERR_INVALID_OPTION_DNS, "dns server priority '" << prio_str << "' invalid");
return priority;
}
};
struct DnsOptionsMerger : public PushOptionsMerger
@@ -130,13 +361,20 @@ struct DnsOptionsMerger : public PushOptionsMerger
}
};
/**
* @class All options set with the --dns directive
*/
struct DnsOptions
{
DnsOptions(const OptionList &opt)
DnsOptions() = default;
explicit DnsOptions(const OptionList &opt)
{
auto indices = opt.get_index_ptr("dns");
if (indices == nullptr)
{
return;
}
for (const auto i : *indices)
{
@@ -144,14 +382,16 @@ struct DnsOptions
if (o.size() >= 3 && o.ref(1) == "search-domains")
{
for (std::size_t j = 2; j < o.size(); j++)
search_domains.push_back(o.ref(j));
{
search_domains.push_back({o.ref(j)});
}
}
else if (o.size() >= 5 && o.ref(1) == "server")
{
auto priority = DnsServer::parse_priority(o.ref(2));
auto &server = get_server(priority);
if (o.ref(3) == "address" && o.size() <= 6)
if (o.ref(3) == "address" && o.size() <= 12)
{
for (std::size_t j = 4; j < o.size(); j++)
{
@@ -166,7 +406,9 @@ struct DnsOptions
{
std::string port_str;
if (!HostPort::split_host_port(o.ref(j), addr_str, port_str, "", false, &port))
{
OPENVPN_THROW_ARG1(option_error, ERR_INVALID_OPTION_DNS, "dns server " << priority << " invalid address: " << o.ref(j));
}
}
try
@@ -178,48 +420,30 @@ struct DnsOptions
OPENVPN_THROW_ARG1(option_error, ERR_INVALID_OPTION_DNS, "dns server " << priority << " invalid address: " << o.ref(j));
}
if (addr.is_ipv6())
{
server.address6 = addr.to_ipv6();
server.port6 = port;
}
else
{
server.address4 = addr.to_ipv4();
server.port4 = port;
}
server.addresses.push_back({addr, port});
}
}
else if (o.ref(3) == "resolve-domains")
{
if (server.domain_type == DnsServer::DomainType::Exclude)
{
OPENVPN_THROW_ARG1(option_error, ERR_INVALID_OPTION_DNS, "dns server " << priority << " cannot use resolve-domains and exclude-domains together");
}
server.domain_type = DnsServer::DomainType::Resolve;
for (std::size_t j = 4; j < o.size(); j++)
server.domains.push_back(o.ref(j));
}
else if (o.ref(3) == "exclude-domains")
{
if (server.domain_type == DnsServer::DomainType::Resolve)
{
OPENVPN_THROW_ARG1(option_error, ERR_INVALID_OPTION_DNS, "dns server " << priority << " cannot use exclude-domains and resolve-domains together");
server.domains.push_back({o.ref(j)});
}
server.domain_type = DnsServer::DomainType::Exclude;
for (std::size_t j = 4; j < o.size(); j++)
server.domains.push_back(o.ref(j));
}
else if (o.ref(3) == "dnssec" && o.size() == 5)
{
if (o.ref(4) == "yes")
{
server.dnssec = DnsServer::Security::Yes;
}
else if (o.ref(4) == "no")
{
server.dnssec = DnsServer::Security::No;
}
else if (o.ref(4) == "optional")
{
server.dnssec = DnsServer::Security::Optional;
}
else
{
OPENVPN_THROW_ARG1(option_error, ERR_INVALID_OPTION_DNS, "dns server " << priority << " dnssec setting '" << o.ref(4) << "' invalid");
@@ -228,11 +452,17 @@ struct DnsOptions
else if (o.ref(3) == "transport" && o.size() == 5)
{
if (o.ref(4) == "plain")
{
server.transport = DnsServer::Transport::Plain;
}
else if (o.ref(4) == "DoH")
{
server.transport = DnsServer::Transport::HTTPS;
}
else if (o.ref(4) == "DoT")
{
server.transport = DnsServer::Transport::TLS;
}
else
{
OPENVPN_THROW_ARG1(option_error, ERR_INVALID_OPTION_DNS, "dns server " << priority << " transport '" << o.ref(4) << "' invalid");
@@ -253,16 +483,67 @@ struct DnsOptions
}
}
for (const auto &keyval : servers)
for (const auto &[priority, server] : servers)
{
const auto priority = keyval.first;
const auto &server = keyval.second;
if (server.address4.unspecified() && server.address6.unspecified())
if (server.addresses.empty())
{
OPENVPN_THROW_ARG1(option_error, ERR_INVALID_OPTION_DNS, "dns server " << priority << " does not have an address assigned");
}
}
}
std::vector<std::string> search_domains;
std::string to_string() const
{
std::ostringstream os;
if (!servers.empty())
{
os << "DNS Servers:" << std::endl;
for (const auto &elem : servers)
{
os << " Priority: " << elem.first << std::endl;
os << elem.second.to_string(" ");
}
}
if (!search_domains.empty())
{
os << "DNS Search Domains:" << std::endl;
for (const auto &domain : search_domains)
{
os << " " << domain.to_string() << std::endl;
}
}
return os.str();
}
#ifdef HAVE_JSON
Json::Value to_json() const
{
Json::Value root(Json::objectValue);
Json::Value servers_json(Json::objectValue);
for (const auto &[prio, server] : servers)
{
servers_json[std::to_string(prio)] = server.to_json();
}
root["servers"] = std::move(servers_json);
json::from_vector(root, search_domains, "search_domains");
return root;
}
void from_json(const Json::Value &root, const std::string &title)
{
json::assert_dict(root, title);
json::assert_dict(root["servers"], title);
for (const auto &prio : root["servers"].getMemberNames())
{
DnsServer server;
server.from_json(root["servers"][prio], title);
servers[std::stoi(prio)] = std::move(server);
}
json::to_vector(root, search_domains, "search_domains", title);
}
#endif
std::vector<DnsDomain> search_domains;
std::map<std::int32_t, DnsServer> servers;
protected:

View File

@@ -1059,7 +1059,7 @@ int run(OptionList opt)
try
{
TunWin::NRPT::delete_rule(); // delete stale NRPT rules
TunWin::NRPT::delete_rules(0); // delete stale NRPT rules
omi.reset(new OMI(io_context, std::move(opt)));
omi->start();
io_context_run_called = true;

View File

@@ -148,6 +148,7 @@ class MyListener : public WS::Server::Listener
Win::ScopedHANDLE establish_tun(const TunBuilderCapture &tbc,
const std::wstring &openvpn_app_path,
DWORD client_process_id,
Stop *stop,
std::ostream &os,
TunWin::Type tun_type,
@@ -160,6 +161,7 @@ class MyListener : public WS::Server::Listener
if ((tun_type == TunWin::OvpnDco) && (tap.index != DWORD(-1)))
tun->set_adapter_state(tap);
tun->set_process_id(client_process_id);
auto th = tun->establish(tbc, openvpn_app_path, stop, os, ring_buffer);
// store VPN interface index to be able to exclude it
// when next time adding bypass route
@@ -576,6 +578,7 @@ class MyClientInstance : public WS::Server::Listener::Client
try
{
const HANDLE client_pipe = get_client_pipe();
const DWORD client_pid = get_client_pid(client_pipe);
const std::wstring client_exe = get_client_exe(client_pipe);
const HTTP::Request &req = request();
@@ -714,7 +717,7 @@ class MyClientInstance : public WS::Server::Listener::Client
}
// establish the tun setup object
Win::ScopedHANDLE tap_handle(parent()->establish_tun(*tbc, client_exe, nullptr, os, tun_type, allow_local_dns_resolvers, tap));
Win::ScopedHANDLE tap_handle(parent()->establish_tun(*tbc, client_exe, client_pid, nullptr, os, tun_type, allow_local_dns_resolvers, tap));
// post-establish impersonation
{
@@ -848,6 +851,17 @@ class MyClientInstance : public WS::Server::Listener::Client
return np->handle.native_handle();
}
/**
* @brief Get the named pipe client process id
*
* @param client_pipe The handle to the named pipe
* @return DWORD The client process id
*/
DWORD get_client_pid(const HANDLE client_pipe)
{
return NamedPipePeerInfo::get_pid(client_pipe, true);
}
std::wstring get_client_exe(const HANDLE client_pipe)
{
Win::NamedPipePeerInfoClient npinfo(client_pipe);
@@ -913,7 +927,7 @@ class MyService : public Win::Service
MyConfig conf;
Win::NamedPipePeerInfo::allow_client_query();
TunWin::NRPT::delete_rule(); // remove stale NRPT rules
TunWin::NRPT::delete_rules(0); // remove stale NRPT rules
WS::Server::Config::Ptr hconf = new WS::Server::Config();
hconf->http_server_id = OVPNAGENT_NAME_STRING "/" HTTP_SERVER_VERSION;

View File

@@ -31,6 +31,9 @@
#include <openvpn/addr/ip.hpp>
namespace openvpn {
struct DnsOptions;
class TunBuilderBase
{
public:
@@ -123,6 +126,16 @@ class TunBuilderBase
return false;
}
/**
* Callback to add --dns options to VPN interface
* May be called more than once to override previous options
*/
virtual bool tun_builder_add_dns_options(const DnsOptions &dns)
{
return false;
}
// Callback to add DNS server to VPN interface
// May be called more than once per tun_builder session
// If reroute_dns is true, all DNS traffic should be routed over the

View File

@@ -37,6 +37,7 @@
#include <openvpn/common/jsonlib.hpp>
#include <openvpn/tun/builder/base.hpp>
#include <openvpn/client/rgopt.hpp>
#include <openvpn/client/dns.hpp>
#include <openvpn/addr/ip.hpp>
#include <openvpn/addr/route.hpp>
#include <openvpn/http/urlparse.hpp>
@@ -506,6 +507,23 @@ class TunBuilderCapture : public TunBuilderBase, public RC<thread_unsafe_refcoun
return true;
}
/**
* @brief Add --dns options for use with tun builder
*
* Calling this invalidates any DNS related --dhcp-options previously added.
*
* @param dns The --dns options to be set
* @return true unconditionally
*/
virtual bool tun_builder_add_dns_options(const DnsOptions &dns) override
{
reset_dns_servers();
reset_search_domains();
reset_adapter_domain_suffix();
dns_options = dns;
return true;
}
virtual bool tun_builder_add_dns_server(const std::string &address, bool ipv6) override
{
DNSServer dns;
@@ -604,6 +622,16 @@ class TunBuilderCapture : public TunBuilderBase, public RC<thread_unsafe_refcoun
dns_servers.clear();
}
void reset_search_domains()
{
search_domains.clear();
}
void reset_adapter_domain_suffix()
{
adapter_domain_suffix.clear();
}
const RouteAddress *vpn_ipv4() const
{
if (tunnel_address_index_ipv4 >= 0)
@@ -667,10 +695,16 @@ class TunBuilderCapture : public TunBuilderBase, public RC<thread_unsafe_refcoun
os << "Route Metric Default: " << route_metric_default << std::endl;
render_list(os, "Add Routes", add_routes);
render_list(os, "Exclude Routes", exclude_routes);
render_list(os, "DNS Servers", dns_servers);
render_list(os, "Search Domains", search_domains);
if (!dns_servers.empty())
render_list(os, "DNS Servers", dns_servers);
if (!search_domains.empty())
render_list(os, "Search Domains", search_domains);
if (!adapter_domain_suffix.empty())
os << "Adapter Domain Suffix: " << adapter_domain_suffix << std::endl;
if (!dns_options.servers.empty())
{
os << dns_options.to_string() << std::endl;
}
if (!proxy_bypass.empty())
render_list(os, "Proxy Bypass", proxy_bypass);
if (proxy_auto_config_url.defined())
@@ -702,6 +736,7 @@ class TunBuilderCapture : public TunBuilderBase, public RC<thread_unsafe_refcoun
root["route_metric_default"] = Json::Value(route_metric_default);
json::from_vector(root, add_routes, "add_routes");
json::from_vector(root, exclude_routes, "exclude_routes");
root["dns_options"] = dns_options.to_json();
json::from_vector(root, dns_servers, "dns_servers");
json::from_vector(root, wins_servers, "wins_servers");
json::from_vector(root, search_domains, "search_domains");
@@ -733,6 +768,7 @@ class TunBuilderCapture : public TunBuilderBase, public RC<thread_unsafe_refcoun
json::to_int(root, tbc->route_metric_default, "route_metric_default", title);
json::to_vector(root, tbc->add_routes, "add_routes", title);
json::to_vector(root, tbc->exclude_routes, "exclude_routes", title);
tbc->dns_options.from_json(root["dns_options"], "dns_options");
json::to_vector(root, tbc->dns_servers, "dns_servers", title);
json::to_vector(root, tbc->wins_servers, "wins_servers", title);
json::to_vector(root, tbc->search_domains, "search_domains", title);
@@ -760,7 +796,8 @@ class TunBuilderCapture : public TunBuilderBase, public RC<thread_unsafe_refcoun
int route_metric_default = -1; // route-metric directive
std::vector<Route> add_routes; // routes that should be added to tunnel
std::vector<Route> exclude_routes; // routes that should be excluded from tunnel
std::vector<DNSServer> dns_servers; // VPN DNS servers
DnsOptions dns_options; // VPN DNS related settings from --dns option
std::vector<DNSServer> dns_servers; // VPN DNS servers from --dhcp-option(s)
std::vector<SearchDomain> search_domains; // domain suffixes whose DNS requests should be tunnel-routed
std::string adapter_domain_suffix; // domain suffix on tun/tap adapter (currently Windows only)

View File

@@ -174,7 +174,8 @@ class TunProp
}
// add DNS servers and domain prefixes
const unsigned int dhcp_option_flags = add_dhcp_options(tb, opt, quiet);
unsigned int dhcp_option_flags = add_dhcp_options(tb, opt, quiet);
dhcp_option_flags |= add_dns_options(tb, opt);
// Allow protocols unless explicitly blocked
tb->tun_builder_set_allow_family(AF_INET, !opt.exists("block-ipv4"));
@@ -527,6 +528,25 @@ class TunProp
}
}
/**
* @brief Configure tun builder to use --dns options if defined
*
* @param tb pointer to the tun builder
* @param opt the --dns options object
* @return unsigned int F_ADD_DNS flag when there were servers defined in the options
*/
static unsigned int add_dns_options(TunBuilderBase *tb, const OptionList &opt)
{
DnsOptions dns_options(opt);
if (dns_options.servers.empty())
return 0;
if (!tb->tun_builder_add_dns_options(dns_options))
throw tun_prop_dhcp_option_error("tun_builder_add_dns_options failed");
return F_ADD_DNS;
}
static unsigned int add_dhcp_options(TunBuilderBase *tb, const OptionList &opt, const bool quiet)
{
// Example:
@@ -543,35 +563,6 @@ class TunProp
// [dhcp-option] [PROXY_AUTO_CONFIG_URL] [http://...]
unsigned int flags = 0;
DnsOptions dns_options(opt);
for (const auto &domain : dns_options.search_domains)
{
if (!tb->tun_builder_set_adapter_domain_suffix(domain))
throw tun_prop_dhcp_option_error(ERR_INVALID_OPTION_PUSHED, "tun_builder_set_adapter_domain_suffix");
break; // use only the first domain for now
}
for (const auto &keyval : dns_options.servers)
{
const auto &server = keyval.second;
if (server.address4.specified())
{
if (!tb->tun_builder_add_dns_server(server.address4.to_string(), false))
throw tun_prop_dhcp_option_error(ERR_INVALID_OPTION_PUSHED, "tun_builder_add_dns_server failed");
flags |= F_ADD_DNS;
}
if (server.address6.specified())
{
if (!tb->tun_builder_add_dns_server(server.address6.to_string(), true))
throw tun_prop_dhcp_option_error(ERR_INVALID_OPTION_PUSHED, "tun_builder_add_dns_server failed");
flags |= F_ADD_DNS;
}
for (const auto &domain : server.domains)
{
if (!tb->tun_builder_add_search_domain(domain))
throw tun_prop_dhcp_option_error(ERR_INVALID_OPTION_PUSHED, "tun_builder_add_search_domain failed");
}
}
OptionList::IndexMap::const_iterator dopt = opt.map().find("dhcp-option"); // DIRECTIVE
if (dopt != opt.map().end())
{
@@ -586,7 +577,7 @@ class TunProp
try
{
const std::string &type = o.get(1, 64);
if ((type == "DNS" || type == "DNS6") && dns_options.servers.empty())
if (type == "DNS" || type == "DNS6")
{
o.exact_args(3);
const IP::Addr ip = IP::Addr::from_string(o.get(2, 256), "dns-server-ip");
@@ -595,7 +586,7 @@ class TunProp
throw tun_prop_dhcp_option_error(ERR_INVALID_OPTION_PUSHED, "tun_builder_add_dns_server failed");
flags |= F_ADD_DNS;
}
else if ((type == "DOMAIN" || type == "DOMAIN-SEARCH") && dns_options.servers.empty())
else if (type == "DOMAIN" || type == "DOMAIN-SEARCH")
{
o.min_args(3);
for (size_t j = 2; j < o.size(); ++j)
@@ -609,7 +600,7 @@ class TunProp
}
}
}
else if (type == "ADAPTER_DOMAIN_SUFFIX" && dns_options.search_domains.empty())
else if (type == "ADAPTER_DOMAIN_SUFFIX")
{
o.exact_args(3);
const std::string &adapter_domain_suffix = o.get(2, 256);

View File

@@ -58,14 +58,24 @@ class MacDNS : public RC<thread_unsafe_refcount>
}
Config(const TunBuilderCapture &settings)
: dns_servers(get_dns_servers(settings)),
: have_dns_options(!settings.dns_options.servers.empty()),
dns_servers(get_dns_servers(settings)),
resolve_domains(get_resolve_domains(settings)),
search_domains(get_search_domains(settings)),
adapter_domain_suffix(settings.adapter_domain_suffix)
{
// We redirect DNS if either of the following is true:
// 1. redirect-gateway (IPv4) is pushed, or
// 2. DNS servers are pushed but no search domains are pushed
redirect_dns = settings.reroute_gw.ipv4 || (CF::array_len(dns_servers) && !CF::array_len(search_domains));
if (have_dns_options)
{
// With --dns options we redirect when there are no split domains
redirect_dns = CF::array_len(resolve_domains);
}
else
{
// We redirect DNS if either of the following is true:
// 1. redirect-gateway (IPv4) is pushed, or
// 2. DNS servers are pushed but no search domains are pushed
redirect_dns = settings.reroute_gw.ipv4 || (CF::array_len(dns_servers) && !CF::array_len(search_domains));
}
}
std::string to_string() const
@@ -74,14 +84,17 @@ class MacDNS : public RC<thread_unsafe_refcount>
os << "RD=" << redirect_dns;
os << " SO=" << search_order;
os << " DNS=" << CF::array_to_string(dns_servers);
os << " RSD=" << CF::array_to_string(resolve_domains);
os << " DOM=" << CF::array_to_string(search_domains);
os << " ADS=" << adapter_domain_suffix;
return os.str();
}
bool have_dns_options;
bool redirect_dns = false;
int search_order = 5000;
CF::Array dns_servers;
CF::Array resolve_domains;
CF::Array search_domains;
std::string adapter_domain_suffix;
@@ -89,12 +102,34 @@ class MacDNS : public RC<thread_unsafe_refcount>
static CF::Array get_dns_servers(const TunBuilderCapture &settings)
{
CF::MutableArray ret(CF::mutable_array());
for (std::vector<TunBuilderCapture::DNSServer>::const_iterator i = settings.dns_servers.begin();
i != settings.dns_servers.end();
++i)
if (!settings.dns_options.servers.empty())
{
const TunBuilderCapture::DNSServer &ds = *i;
CF::array_append_str(ret, ds.address);
const auto &server = settings.dns_options.servers.begin()->second;
for (const auto &ip : server.addresses)
{
CF::array_append_str(ret, ip.address.to_string());
}
}
else
{
for (const auto &server : settings.dns_servers)
{
CF::array_append_str(ret, server.address);
}
}
return CF::const_array(ret);
}
static CF::Array get_resolve_domains(const TunBuilderCapture &settings)
{
CF::MutableArray ret(CF::mutable_array());
if (!settings.dns_options.servers.empty())
{
const auto &server = settings.dns_options.servers.begin()->second;
for (const auto &rd : server.domains)
{
CF::array_append_str(ret, rd.domain);
}
}
return CF::const_array(ret);
}
@@ -102,12 +137,19 @@ class MacDNS : public RC<thread_unsafe_refcount>
static CF::Array get_search_domains(const TunBuilderCapture &settings)
{
CF::MutableArray ret(CF::mutable_array());
for (std::vector<TunBuilderCapture::SearchDomain>::const_iterator i = settings.search_domains.begin();
i != settings.search_domains.end();
++i)
if (!settings.dns_options.servers.empty())
{
const TunBuilderCapture::SearchDomain &sd = *i;
CF::array_append_str(ret, sd.domain);
for (const auto &sd : settings.dns_options.search_domains)
{
CF::array_append_str(ret, sd.domain);
}
}
else
{
for (const auto &sd : settings.search_domains)
{
CF::array_append_str(ret, sd.domain);
}
}
return CF::const_array(ret);
}
@@ -173,14 +215,23 @@ class MacDNS : public RC<thread_unsafe_refcount>
// set search domains
info->dns.backup_orig("SearchDomains");
CF::MutableArray search_domains(CF::mutable_array());
// add adapter_domain_suffix to SearchDomains for domain autocompletion
if (config.adapter_domain_suffix.length() > 0)
CF::array_append_str(search_domains, config.adapter_domain_suffix);
if (config.have_dns_options)
{
if (CF::array_len(config.search_domains))
CF::dict_set_obj(info->dns.mod, "SearchDomains", config.search_domains());
}
else
{
// add adapter_domain_suffix to SearchDomains for domain autocompletion
CF::MutableArray search_domains(CF::mutable_array());
if (CF::array_len(search_domains))
CF::dict_set_obj(info->dns.mod, "SearchDomains", search_domains());
if (config.adapter_domain_suffix.length() > 0)
CF::array_append_str(search_domains, config.adapter_domain_suffix);
if (CF::array_len(search_domains))
CF::dict_set_obj(info->dns.mod, "SearchDomains", search_domains());
}
// set search order
info->dns.backup_orig("SearchOrder");
@@ -193,16 +244,38 @@ class MacDNS : public RC<thread_unsafe_refcount>
{
// split-DNS - resolve only specific domains
info->ovpn.mod_reset();
if (CF::array_len(config.dns_servers) && CF::array_len(config.search_domains))
if (config.have_dns_options)
{
// set DNS servers
CF::dict_set_obj(info->ovpn.mod, "ServerAddresses", config.dns_servers());
// DNS will be used only for those domains
CF::dict_set_obj(info->ovpn.mod, "SupplementalMatchDomains", config.search_domains());
CF::dict_set_obj(info->ovpn.mod, "SupplementalMatchDomains", config.resolve_domains());
// do not use those domains in autocompletion
CF::dict_set_int(info->ovpn.mod, "SupplementalMatchDomainsNoSearch", 1);
// set search domains if there are any
if (CF::array_len(config.search_domains))
{
info->dns.backup_orig("SearchDomains");
CF::dict_set_obj(info->dns.mod, "SearchDomains", config.search_domains());
mod |= info->dns.push_to_store();
}
}
else
{
if (CF::array_len(config.dns_servers) && CF::array_len(config.search_domains))
{
// set DNS servers
CF::dict_set_obj(info->ovpn.mod, "ServerAddresses", config.dns_servers());
// DNS will be used only for those domains
CF::dict_set_obj(info->ovpn.mod, "SupplementalMatchDomains", config.search_domains());
// do not use those domains in autocompletion
CF::dict_set_int(info->ovpn.mod, "SupplementalMatchDomainsNoSearch", 1);
}
}
// in case of split-DNS macOS uses domain suffix of network adapter,

View File

@@ -48,6 +48,7 @@
#if _WIN32_WINNT >= 0x0600 // Vista+
#include <openvpn/tun/win/nrpt.hpp>
#include <openvpn/tun/win/dns.hpp>
#include <openvpn/tun/win/wfp.hpp>
#endif
@@ -84,6 +85,21 @@ class Setup : public SetupBase
tap_ = tap;
}
/**
* @brief Set the process id to be used with the NPRT rules
*
* The NRPT c'tor expects a process id parameter, which is used
* internally. This function can be used if you want that pid to
* be different from the current process id, e.g. if you are doing
* the setup for a different process, like in the agent.
*
* @param process_id The process id used with the NRPT class
*/
void set_process_id(DWORD process_id)
{
process_id_ = process_id;
}
HANDLE get_handle(std::ostream &os) override
{
if (tap_.index_defined())
@@ -596,130 +612,168 @@ class Setup : public SetupBase
destroy.add(new WFP::ActionUnblock(openvpn_app_path, tap.index, false, wfp));
}
// Process DNS Servers
//
// Usage: set dnsservers [name=]<string> [source=]dhcp|static
// [[address=]<IP address>|none]
// [[register=]none|primary|both]
// [[validate=]yes|no]
// Usage: add dnsservers [name=]<string> [address=]<IPv4 address>
// [[index=]<integer>] [[validate=]yes|no]
// Usage: delete dnsservers [name=]<string> [[address=]<IP address>|all] [[validate=]yes|no]
//
// Usage: set dnsservers [name=]<string> [source=]dhcp|static
// [[address=]<IPv6 address>|none]
// [[register=]none|primary|both]
// [[validate=]yes|no]
// Usage: add dnsservers [name=]<string> [address=]<IPv6 address>
// [[index=]<integer>] [[validate=]yes|no]
// Usage: delete dnsservers [name=]<string> [[address=]<IPv6 address>|all] [[validate=]yes|no]
// The process id for NRPT rules
DWORD pid = process_id_ ? process_id_ : ::GetCurrentProcessId();
// Process DNS related settings
{
// fix for vista and dnsserver vs win7+ dnsservers
std::string dns_servers_cmd = "dnsservers";
std::string validate_cmd = " validate=no";
if (IsWindowsVistaOrGreater() && !IsWindows7OrGreater())
if (!pull.dns_options.servers.empty())
{
dns_servers_cmd = "dnsserver";
validate_cmd = "";
}
// apply DNS settings from --dns options
const DnsServer &server = pull.dns_options.servers.begin()->second;
#if 1
// normal production setting
const bool use_nrpt = IsWindows8OrGreater();
const bool add_netsh_rules = true;
#else
// test NRPT registry settings on pre-Win8
const bool use_nrpt = true;
const bool use_wfp = true;
const bool add_netsh_rules = true;
#endif
// determine IPv4/IPv6 DNS redirection
const UseDNS dns(pull);
// will DNS requests be split between VPN DNS server and local?
const bool split_dns = (!pull.search_domains.empty()
&& !(pull.reroute_gw.ipv4 && dns.ipv4())
&& !(pull.reroute_gw.ipv6 && dns.ipv6()));
// add DNS servers via netsh
if (add_netsh_rules && !(use_nrpt && split_dns) && !l2_post)
{
UseDNS dc;
for (auto &ds : pull.dns_servers)
// DNS server address(es)
std::vector<std::string> addresses;
for (const auto &addr : server.addresses)
{
// 0-based index for specific IPv4/IPv6 protocol, or -1 if disabled
const int count = dc.add(ds, pull);
if (count >= 0)
addresses.push_back(addr.address.to_string());
}
// DNS server split domain(s)
std::vector<std::string> domains;
for (const auto &dom : server.domains)
{
domains.push_back("." + dom.domain);
}
const bool dnssec = server.dnssec == DnsServer::Security::Yes;
std::string delimiter;
std::string search_domains;
for (const auto &domain : pull.dns_options.search_domains)
{
search_domains.append(delimiter + domain.to_string());
delimiter = ",";
}
create.add(new NRPT::ActionCreate(pid, domains, addresses, dnssec));
create.add(new DNS::ActionCreate(tap.name, search_domains));
destroy.add(new NRPT::ActionDelete(pid));
destroy.add(new DNS::ActionDelete(tap.name, search_domains));
}
else
{
// apply DNS settings from --dhcp-options
const bool use_nrpt = IsWindows8OrGreater();
// count IPv4/IPv6 DNS servers
const UseDNS dns(pull);
// will DNS requests be split between VPN DNS server and local?
const bool split_dns = (!pull.search_domains.empty()
&& !(pull.reroute_gw.ipv4 && dns.ipv4())
&& !(pull.reroute_gw.ipv6 && dns.ipv6()));
// add DNS servers via netsh
if (!(use_nrpt && split_dns) && !l2_post)
{
// Usage: set dnsservers [name=]<string> [source=]dhcp|static
// [[address=]<IP address>|none]
// [[register=]none|primary|both]
// [[validate=]yes|no]
// Usage: add dnsservers [name=]<string> [address=]<IPv4 address>
// [[index=]<integer>] [[validate=]yes|no]
// Usage: delete dnsservers [name=]<string> [[address=]<IP address>|all] [[validate=]yes|no]
//
// Usage: set dnsservers [name=]<string> [source=]dhcp|static
// [[address=]<IPv6 address>|none]
// [[register=]none|primary|both]
// [[validate=]yes|no]
// Usage: add dnsservers [name=]<string> [address=]<IPv6 address>
// [[index=]<integer>] [[validate=]yes|no]
// Usage: delete dnsservers [name=]<string> [[address=]<IPv6 address>|all] [[validate=]yes|no]
// fix for vista and dnsserver vs win7+ dnsservers
std::string dns_servers_cmd = "dnsservers";
std::string validate_cmd = " validate=no";
if (IsWindowsVistaOrGreater() && !IsWindows7OrGreater())
{
const std::string proto = ds.ipv6 ? "ipv6" : "ip";
if (count)
create.add(new WinCmd("netsh interface " + proto + " add " + dns_servers_cmd + " " + tap_index_name + ' ' + ds.address + " " + to_string(count + 1) + validate_cmd));
else
dns_servers_cmd = "dnsserver";
validate_cmd = "";
}
UseDNS dc;
for (auto &ds : pull.dns_servers)
{
// 0-based index for specific IPv4/IPv6 protocol, or -1 if disabled
const int count = dc.add(ds, pull);
if (count >= 0)
{
create.add(new WinCmd("netsh interface " + proto + " set " + dns_servers_cmd + " " + tap_index_name + " static " + ds.address + " register=primary" + validate_cmd));
destroy.add(new WinCmd("netsh interface " + proto + " delete " + dns_servers_cmd + " " + tap_index_name + " all" + validate_cmd));
const std::string proto = ds.ipv6 ? "ipv6" : "ip";
if (count)
create.add(new WinCmd("netsh interface " + proto + " add " + dns_servers_cmd + " " + tap_index_name + ' ' + ds.address + " " + to_string(count + 1) + validate_cmd));
else
{
create.add(new WinCmd("netsh interface " + proto + " set " + dns_servers_cmd + " " + tap_index_name + " static " + ds.address + " register=primary" + validate_cmd));
destroy.add(new WinCmd("netsh interface " + proto + " delete " + dns_servers_cmd + " " + tap_index_name + " all" + validate_cmd));
}
}
}
}
}
// If NRPT enabled and at least one IPv4 or IPv6 DNS
// server was added, add NRPT registry entries to
// route DNS through the tunnel.
// Also consider selective DNS routing using domain
// suffix list from pull.search_domains as set by
// "dhcp-option DOMAIN ..." directives.
if (use_nrpt && (dns.ipv4() || dns.ipv6()))
{
// domain suffix list
std::vector<std::string> dsfx;
// Only add DNS routing suffixes if not rerouting gateway.
// Otherwise, route all DNS requests with wildcard (".").
if (split_dns)
// If NRPT enabled and at least one IPv4 or IPv6 DNS
// server was added, add NRPT registry entries to
// route DNS through the tunnel.
// Also consider selective DNS routing using domain
// suffix list from pull.search_domains as set by
// "dhcp-option DOMAIN ..." directives.
if (use_nrpt && (dns.ipv4() || dns.ipv6()))
{
for (const auto &sd : pull.search_domains)
// domain suffix list
std::vector<std::string> dsfx;
// Only add DNS routing suffixes if not rerouting gateway.
// Otherwise, route all DNS requests with wildcard (".").
if (split_dns)
{
std::string dom = sd.domain;
if (!dom.empty())
for (const auto &sd : pull.search_domains)
{
// each DNS suffix must begin with '.'
if (dom[0] != '.')
dom = "." + dom;
dsfx.push_back(std::move(dom));
std::string dom = sd.domain;
if (!dom.empty())
{
// each DNS suffix must begin with '.'
if (dom[0] != '.')
dom = "." + dom;
dsfx.push_back(std::move(dom));
}
}
}
if (dsfx.empty() && !allow_local_dns_resolvers)
dsfx.emplace_back(".");
// DNS server list
std::vector<std::string> dserv;
for (const auto &ds : pull.dns_servers)
dserv.push_back(ds.address);
create.add(new NRPT::ActionCreate(pid, dsfx, dserv, false));
destroy.add(new NRPT::ActionDelete(pid));
}
if (dsfx.empty() && !allow_local_dns_resolvers)
dsfx.emplace_back(".");
// DNS server list
std::vector<std::string> dserv;
for (const auto &ds : pull.dns_servers)
dserv.push_back(ds.address);
// Set a default TAP-adapter domain suffix using
// "dhcp-option ADAPTER_DOMAIN_SUFFIX mycompany.com" directive.
if (!pull.adapter_domain_suffix.empty())
{
// Only the first search domain is used
create.add(new Util::ActionSetAdapterDomainSuffix(pull.adapter_domain_suffix, tap.guid));
destroy.add(new Util::ActionSetAdapterDomainSuffix("", tap.guid));
}
create.add(new NRPT::ActionCreate(dsfx, dserv));
destroy.add(new NRPT::ActionDelete);
// Use WFP for DNS leak protection unless local traffic is blocked already.
// If we added DNS servers, block DNS on all interfaces except
// the TAP adapter and loopback.
if (use_wfp && !block_local_traffic
&& !split_dns && !openvpn_app_path.empty() && (dns.ipv4() || dns.ipv6()))
{
create.add(new WFP::ActionBlock(openvpn_app_path, tap.index, true, wfp));
destroy.add(new WFP::ActionUnblock(openvpn_app_path, tap.index, true, wfp));
}
// flush DNS cache
create.add(new WinCmd("ipconfig /flushdns"));
destroy.add(new WinCmd("ipconfig /flushdns"));
}
// Use WFP for DNS leak protection unless local traffic is blocked already.
// If we added DNS servers, block DNS on all interfaces except
// the TAP adapter and loopback.
if (use_wfp && !block_local_traffic
&& !split_dns && !openvpn_app_path.empty() && (dns.ipv4() || dns.ipv6()))
{
create.add(new WFP::ActionBlock(openvpn_app_path, tap.index, true, wfp));
destroy.add(new WFP::ActionUnblock(openvpn_app_path, tap.index, true, wfp));
}
}
// Set a default TAP-adapter domain suffix using
// "dhcp-option ADAPTER_DOMAIN_SUFFIX mycompany.com" directive.
if (!pull.adapter_domain_suffix.empty())
{
// Only the first search domain is used
create.add(new Util::ActionSetAdapterDomainSuffix(pull.adapter_domain_suffix, tap.guid));
destroy.add(new Util::ActionSetAdapterDomainSuffix("", tap.guid));
}
// Process WINS Servers
@@ -745,10 +799,6 @@ class Setup : public SetupBase
OPENVPN_LOG("proxy_auto_config_url " << pull.proxy_auto_config_url.url);
if (pull.proxy_auto_config_url.defined())
ProxySettings::add_actions<WinProxySettings>(pull, create, destroy);
// flush DNS cache
create.add(new WinCmd("ipconfig /flushdns"));
destroy.add(new WinCmd("ipconfig /flushdns"));
}
#else
// Configure TAP adapter for pre-Vista
@@ -1003,6 +1053,7 @@ class Setup : public SetupBase
const Type tun_type_;
Util::TapNameGuidPair tap_;
bool allow_local_dns_resolvers = false;
DWORD process_id_ = 0;
};
} // namespace TunWin
} // namespace openvpn

550
openvpn/tun/win/dns.hpp Normal file
View File

@@ -0,0 +1,550 @@
// OpenVPN -- An application to securely tunnel IP networks
// over a single port, with support for SSL/TLS-based
// session authentication and key exchange,
// packet encryption, packet authentication, and
// packet compression.
//
// Copyright (C) 2024 OpenVPN Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License Version 3
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program in the COPYING file.
// If not, see <http://www.gnu.org/licenses/>.
//
/**
* @brief DNS utilities for Windows
*
* DNS search suffixes are applied to not fully qualified domain names
* before lookup, e.g. you try to resolve 'host' and Windows completes
* this to host.searchdomain1.in and host.searchdomain-n.com and looks
* up these two FQDNs.
*
* The domain suffixes for completion can be configured in various ways
* in Windows. There are so called adapter domain suffixes which can be
* specified with each network adapter configuration. However, these are
* overridden by a so called search list, which is shared between all
* adapters. If you want to have more than one search suffix defined for
* an adapter you have to use a search list, otherwise the primary suffix
* is enough. In addition to that a search list can also be defined by
* a group policy, which overrides both previous settings. The local and
* group polixy search lists a located in different subkeys in the Registry.
* There's also a primary domain suffix, which is for the Windows AD Domain.
*
* OpenVPN clients will apply pushed search domains this way:
* - If it is a single domain it will be added as primary domain suffix,
* unless there is a search list defined already. In that case the
* domain is added to the search list.
* - If there are multiple domains pushed and there already is a search
* list defined, the pushed domains will be added to the list. Otherwise
* a new serach list will be created. This newly created search list
* will also include the primary domain and all adapter domains, so that
* lookup of unqualified names continues to work when the VPN is
* connected.
*/
#pragma once
#include <array>
#include <openvpn/common/wstring.hpp>
#include <openvpn/common/action.hpp>
#include <openvpn/win/reg.hpp>
#include <openvpn/win/netutil.hpp>
namespace openvpn {
namespace TunWin {
/**
* @brief Manage DNS search suffixes for Windows
*
* @tparam REG Registry abstraction class to use
* @tparam NETAPI Network related Win32 API class to use
*/
template <typename REG, typename NETAPI>
class Dns
{
/**
* Registry locations the DNS search domains list can be stored in.
* When the first key exists and it has domains in the "SearchList" value,
* then these GPO provided domains will be used as suffixes, otherwise the
* manually created ones in the second key will be used (if they exist).
*/
static constexpr std::array<PCWSTR, 2> searchlist_subkeys{
LR"(SOFTWARE\Policies\Microsoft\WindowsNT\DNSClient)",
LR"(System\CurrentControlSet\Services\TCPIP\Parameters)"};
/**
* @brief Return the Key for the DNS domain "SearchList" value
*
* It also returns a boolean value, telling whether a SearchList already exists
* under the returned registry key.
*
* @return std::pair<REG::Key, bool> the search list key and a
* boolean telling if a list exists
*/
static std::pair<typename REG::Key, bool> open_searchlist_key()
{
for (const auto subkey : searchlist_subkeys)
{
typename REG::Key key(subkey);
if (key.defined())
{
auto [list, error] = REG::get_string(key, L"SearchList");
if (!error && !list.empty())
{
return {std::move(key), true};
}
else if (subkey == searchlist_subkeys.back())
{
// Return the local subkey (last in the list) as default
return {std::move(key), false};
}
}
}
return {typename REG::Key{}, false};
}
/**
* @brief Check if a initial list had already been created
*
* @param key key to look under for the initial list
* @return bool to indicate if the initial list is already present
*/
static bool initial_searchlist_exists(typename REG::Key &key)
{
auto [value, error] = REG::get_string(key, L"InitialSearchList");
return error ? false : true;
}
/**
* Prepare DNS domain "SearchList" registry value, so additional
* VPN domains can be added and its original state can be restored
* when the VPN disconnects.
*
* @param key the registry key to modify values under
* @param searchlist string of comma separated domains to use as the list
*
* @return bool indicates whether the list is stored successfully
*/
static bool set_initial_searchlist(typename REG::Key &key, const std::wstring &searchlist)
{
LSTATUS err;
err = REG::set_string(key, L"InitialSearchList", searchlist);
if (err)
{
return false;
}
err = REG::set_string(key, L"SearchList", searchlist);
if (err)
{
return false;
}
return true;
}
/**
* @brief Set the initial searchlist from the existing search list
*
* @param key the registry key to modify values under
* @return bool indicates whether the list is stored successfully
*/
static bool set_initial_searchlist_from_existing(typename REG::Key &key)
{
auto [searchlist, error] = REG::get_string(key, L"SearchList");
if (error)
{
return false;
}
/* Store a copy of the original list */
LSTATUS err = REG::set_string(key, L"OriginalSearchList", searchlist);
if (err)
{
return false;
}
return set_initial_searchlist(key, searchlist);
}
/**
* Create a initial DNS search list if it does not exist already
*
* @param key the registry key to modify values under
* @return bool to indicate creation success or failure
*/
static bool set_initial_searchlist_from_domains(typename REG::Key &key)
{
std::wstring list;
{
// Add primary domain to the list, if exists
typename REG::Key tcpip_params(LR"(SYSTEM\CurrentControlSet\Services\Tcpip\Parameters)");
auto [domain, error] = REG::get_string(tcpip_params, L"Domain");
if (!error && !domain.empty())
{
list.append(domain);
}
}
typename REG::Key itfs(REG::subkey_ipv4_itfs);
typename REG::KeyEnumerator itf_guids(itfs);
for (const auto &itf_guid : itf_guids)
{
// Ignore interfaces that are not connected or disabled
if (!NETAPI::interface_connected(itf_guid))
{
continue;
}
// Get the DNS domain for routing
std::wstring domain = interface_dns_domain<REG>(itf_guid);
if (domain.empty())
{
continue;
}
// FIXME: expand domain if "UseDomainNameDevolution" is set?
if (list.size())
{
list.append(L",");
}
list.append(domain);
}
return set_initial_searchlist(key, list);
}
/**
* @brief Set interface specific domain suffix
*
* @param itf_name interface alias name to set the domain suffix for
* @param domain domain suffix to set as wide character string
*
* @return bool to indicate success or failure setting the domain suffix
*/
static bool set_itf_domain_suffix(const std::string &itf_name, const std::wstring &domain)
{
const std::wstring iid = NETAPI::get_itf_id(itf_name);
if (iid.empty())
{
return false;
}
typename REG::Key itf_key(std::wstring(REG::subkey_ipv4_itfs) + L"\\" + iid);
PCWSTR name = dhcp_enabled_on_itf<REG>(itf_key) ? L"DhcpDomain" : L"Domain";
LSTATUS err = REG::set_string(itf_key, name, domain);
if (err)
{
return false;
}
return true;
}
/**
* @brief Append domain suffixes to an existing search list
*
* @param key the registry key to modify values under
* @param domains domain suffixes as comma separated string
*
* @return bool to indicate success or failure
*/
static bool add_to_searchlist(typename REG::Key &key, const std::wstring &domains)
{
auto [list, error] = REG::get_string(key, L"SearchList");
if (error)
{
return false;
}
if (list.size())
{
list.append(L",");
}
list.append(domains);
LSTATUS err = REG::set_string(key, L"SearchList", list);
return err ? false : true;
}
public:
OPENVPN_EXCEPTION(dns_error);
/**
* @brief Add DNS search domain(s)
*
* Extend the list of DNS search domains present on the system.
* If domains is only a single domain (no comma) and there currently is
* no search list defined on the system, a interface specific domain
* suffix is used instead of generating a new search list.
*
* @param itf_name alias name of the interface a single domain is set for
* @param domains a comma separated list of domain names
*
* @return NO_ERROR on success, an error code otherwise
*/
static void set_search_domains(const std::string &itf_name, const std::string &domains)
{
if (domains.empty())
{
return;
}
auto [list_key, list_exists] = open_searchlist_key();
bool initial_list_exists = initial_searchlist_exists(list_key);
bool single_domain = domains.find(',') == domains.npos;
if (!initial_list_exists)
{
if (list_exists)
{
if (!set_initial_searchlist_from_existing(list_key))
{
return;
}
}
else if (!single_domain)
{
if (!set_initial_searchlist_from_domains(list_key))
{
return;
}
}
}
std::wstring wide_domains = wstring::from_utf8(domains);
bool success_adding = single_domain && !list_exists
? set_itf_domain_suffix(itf_name, wide_domains)
: add_to_searchlist(list_key, wide_domains);
if (!success_adding)
{
remove_search_domains(itf_name, domains);
}
}
/**
* @brief Reset the DNS "SearchList" to its original value
*
* Looks for "OriginalSearchList" value as the one to reset to. If it doesn't
* exists resets to the empty value, which is interpreted as no search list.
*
* @param list_key key in the registry to reset the values under
*/
static void reset_search_domains(typename REG::Key &list_key)
{
auto [originallist, error] = REG::get_string(list_key, L"OriginalSearchList");
if (!error || error == ERROR_FILE_NOT_FOUND)
{
// Restore the original search list or set a empty list
REG::set_string(list_key, L"SearchList", originallist);
}
REG::delete_value(list_key, L"InitialSearchList");
REG::delete_value(list_key, L"OriginalSearchList");
}
/**
* @brief Remove domain suffix(es) from the system
*
* If a search list exists, it is restored to the previous state.
* The adapter domain suffix is also emptied. And temporary values
* from the registry are removed if they are no longer needed.
*
* @param itf_name alias name of the interface the suffix is removed from
* @param domains a comma separated list of domain names to be removed
*/
static void remove_search_domains(const std::string &itf_name, const std::string &domains)
{
if (domains.empty())
{
return;
}
set_itf_domain_suffix(itf_name, {});
auto [list_key, list_exists] = open_searchlist_key();
if (list_exists)
{
auto [searchlist, error] = REG::get_string(list_key, L"SearchList");
if (error)
{
return;
}
// Remove domains from list
const std::wstring wdomains = wstring::from_utf8(domains);
auto pos = searchlist.find(wdomains);
if (pos != searchlist.npos)
{
// Domains are in the search list, remove them
if (searchlist.size() == wdomains.size())
{
// No other domains in the list
searchlist.clear();
}
else if (pos == 0)
{
// Also remove trailing comma
searchlist.erase(pos, wdomains.size() + 1);
}
else
{
// Also remove leading comma
searchlist.erase(pos - 1, wdomains.size() + 1);
}
}
auto [initiallist, err] = REG::get_string(list_key, L"InitialSearchList");
if (err)
{
return;
}
// Compare shortened list with initial list
if (searchlist == initiallist)
{
// Reset everything to the original state
reset_search_domains(list_key);
}
else
{
// Store the shortened search list
REG::set_string(list_key, L"SearchList", searchlist);
}
}
}
/**
* @brief Signal the DNS resolver to reload its settings
*
* @return bool to indicate if the reload was initiated
*/
static bool apply_dns_settings()
{
bool res = false;
SC_HANDLE scm = static_cast<SC_HANDLE>(INVALID_HANDLE_VALUE);
SC_HANDLE dnssvc = static_cast<SC_HANDLE>(INVALID_HANDLE_VALUE);
scm = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (scm == NULL)
{
goto out;
}
dnssvc = ::OpenServiceA(scm, "Dnscache", SERVICE_PAUSE_CONTINUE);
if (dnssvc == NULL)
{
goto out;
}
SERVICE_STATUS status;
if (::ControlService(dnssvc, SERVICE_CONTROL_PARAMCHANGE, &status) == 0)
{
goto out;
}
res = true;
out:
if (dnssvc != INVALID_HANDLE_VALUE)
{
::CloseServiceHandle(dnssvc);
}
if (scm != INVALID_HANDLE_VALUE)
{
::CloseServiceHandle(scm);
}
return res;
}
class ActionCreate : public Action
{
public:
ActionCreate(const std::string &itf_name, const std::string &search_domains)
: itf_name_(itf_name), search_domains_(search_domains)
{
}
/**
* @brief Apply DNS data to the system
*
* @param log where the rules will be logged to
*/
void execute(std::ostream &log) override
{
log << to_string() << std::endl;
set_search_domains(itf_name_, search_domains_);
apply_dns_settings();
}
/**
* @brief Produce a textual representating of the DNS data
*
* @return std::string the data as string
*/
std::string to_string() const override
{
std::ostringstream os;
os << "DNS::ActionCreate"
<< " interface name=[" << itf_name_ << "]"
<< " search domains=[" << search_domains_ << "]";
return os.str();
}
private:
const std::string itf_name_;
const std::string search_domains_;
};
class ActionDelete : public Action
{
public:
ActionDelete(const std::string &itf_name, const std::string &search_domains)
: itf_name_(itf_name), search_domains_(search_domains)
{
}
/**
* @brief Undo any modification to the DNS settings.
*
* @param log where the log message goes
*/
void execute(std::ostream &log) override
{
log << to_string() << std::endl;
remove_search_domains(itf_name_, search_domains_);
apply_dns_settings();
}
/**
* @brief Return the log message
*
* @return std::string
*/
std::string to_string() const override
{
std::ostringstream ss;
ss << "DNS::ActionDelete"
<< " interface name=[" << itf_name_ << "]"
<< " search domains=[" << search_domains_ << "]";
return ss.str();
}
private:
const std::string itf_name_;
const std::string search_domains_;
};
};
using DNS = Dns<Win::Reg, Win::NetApi>;
} // namespace TunWin
} // namespace openvpn

View File

@@ -4,7 +4,7 @@
// packet encryption, packet authentication, and
// packet compression.
//
// Copyright (C) 2012-2022 OpenVPN Inc.
// Copyright (C) 2012-2024 OpenVPN Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License Version 3
@@ -20,179 +20,494 @@
// If not, see <http://www.gnu.org/licenses/>.
//
// Name Resolution Policy Table (NRPT) utilities for Windows
/**
* @brief Name Resolution Policy Table (NRPT) utilities for Windows
*
* NRPT rules define how DNS loop-ups are done on Windows systems. They
* override the traditional settings, that are done with the network adapters,
* so having NRPT rules in place, only those will define how DNS works.
*
* There are two subkey in the Registry where NRPT rules can be defined. One
* for rules coming in via group policies and the other for locally defined rules.
* Group policy rules are preferred and if they exist, local rules will be ignored.
*
* OpenVPN will find the right subkey to add its rules to. In case there is no
* split DNS rule defined it will also add so called bypass rules, which make sure
* local name resolution will still work while the VPN is connected. This is done
* by collecting the name server addresses from the adapter configurations and
* adding them as NRPT rules for the adapter's domain suffix.
*
* NRPT rules described here: https://msdn.microsoft.com/en-us/library/ff957356.aspx
*/
#ifndef OPENVPN_TUN_WIN_NRPT_H
#define OPENVPN_TUN_WIN_NRPT_H
#pragma once
#include <string>
#include <sstream>
#include <vector>
#include <array>
#include <openvpn/common/exception.hpp>
#include <openvpn/common/string.hpp>
#include <openvpn/common/wstring.hpp>
#include <openvpn/common/action.hpp>
#include <openvpn/win/reg.hpp>
#include <openvpn/win/netutil.hpp>
#include <openvpn/win/winerr.hpp>
namespace openvpn {
namespace TunWin {
// NRPT rules described here: https://msdn.microsoft.com/en-us/library/ff957356.aspx
class NRPT
/**
* @brief Manage NRPT rules for Windows
*
* @tparam REG Registry abstraction class to use
* @tparam NETAPI Network related Win32 API class to use
*/
template <typename REG, typename NETAPI>
class Nrpt
{
public:
OPENVPN_EXCEPTION(nrpt_error);
static void create_rule(const std::vector<std::string> names, const std::vector<std::string> dns_servers)
/**
* @brief Create a NRPT rule in the registry
*
* The exact location of the rule depends on whether there are alredy rules
* rules defined. If so the rule is stored with them, either in the place
* where group policy based ones are, or the local one.
*
* @param rule_id the unique rule id
* @param domains domains the rule applies to as wide MULTI_SZ
* @param dns_servers list of name server addresses, separated by semicolon
* @param dnssec whether DNSSEC should be enabled for the rule
*/
static void create_rule(const std::string &rule_id,
const std::wstring &domains,
const std::wstring &servers,
bool dnssec)
{
Win::Reg::Key key;
LSTATUS status;
for (size_t i = 0; i < names.size(); ++i)
// Open / create the key
typename REG::Key nrpt = open_nrpt_base_key();
if (!nrpt.defined())
{
// open/create the key
{
std::ostringstream ss;
ss << dnsPolicyConfig() << "\\" << policyPrefix() << "-" << i;
auto key_name = ss.str();
throw nrpt_error("cannot open NRPT base key");
}
typename REG::Key rule_key(nrpt(), wstring::from_utf8(rule_id), true);
if (!rule_key.defined())
{
throw nrpt_error("cannot create NRPT rule subkey");
}
const LONG status = ::RegCreateKeyA(HKEY_LOCAL_MACHINE, key_name.c_str(), key.ref());
check_reg_error<nrpt_error>(status, key_name);
}
// Name
status = REG::set_multi_string(rule_key, L"Name", domains);
check_reg_error<nrpt_error>(status, "Name");
// Name
{
std::wstring name(wstring::from_utf8(names[i]));
name += L'\0';
const LONG status = ::RegSetValueExW(key(),
L"Name",
0,
REG_MULTI_SZ,
reinterpret_cast<const BYTE *>(name.c_str()),
static_cast<DWORD>((name.length() + 1) * sizeof(wchar_t)));
check_reg_error<nrpt_error>(status, "Name");
}
// GenericDNSServers
status = REG::set_string(rule_key, L"GenericDNSServers", servers);
check_reg_error<nrpt_error>(status, "GenericDNSServers");
// GenericDNSServers
{
const std::wstring dns_servers_joined = wstring::from_utf8(string::join(dns_servers, ";"));
const LONG status = ::RegSetValueExW(key(),
L"GenericDNSServers",
0,
REG_SZ,
reinterpret_cast<const BYTE *>(dns_servers_joined.c_str()),
static_cast<DWORD>((dns_servers_joined.length() + 1) * sizeof(wchar_t)));
check_reg_error<nrpt_error>(status, "GenericDNSServers");
}
// DNSSEC
if (dnssec)
{
status = REG::set_dword(rule_key, L"DNSSECValidationRequired", 1);
check_reg_error<nrpt_error>(status, "DNSSECValidationRequired");
// ConfigOptions
{
const DWORD value = 0x8; // Only the Generic DNS server option (that is, the option defined in section 2.2.2.13) is specified.
const LONG status = ::RegSetValueExW(key(),
L"ConfigOptions",
0,
REG_DWORD,
reinterpret_cast<const BYTE *>(&value),
sizeof(value));
check_reg_error<nrpt_error>(status, "ConfigOptions");
}
status = REG::set_dword(rule_key, L"DNSSECQueryIPSECRequired", 0);
check_reg_error<nrpt_error>(status, "DNSSECQueryIPSECRequired");
// Version
{
const DWORD value = 0x2;
const LONG status = ::RegSetValueExW(key(),
L"Version",
0,
REG_DWORD,
reinterpret_cast<const BYTE *>(&value),
sizeof(value));
check_reg_error<nrpt_error>(status, "Version");
}
status = REG::set_dword(rule_key, L"DNSSECQueryIPSECEncryption", 0);
check_reg_error<nrpt_error>(status, "DNSSECQueryIPSECEncryption");
}
// ConfigOptions
// 0x8: Only the Generic DNS server option is specified.
// 0xA: The Generic DNS server option and the DNSSEC options are specified
status = REG::set_dword(rule_key, L"ConfigOptions", dnssec ? 0xA : 0x8);
check_reg_error<nrpt_error>(status, "ConfigOptions");
// Version
status = REG::set_dword(rule_key, L"Version", 2);
check_reg_error<nrpt_error>(status, "Version");
}
/**
* 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. Exclude rules are only installed if the DNS settings came in via
* --dns options, to keep backwards compatibility.
*
* @param process_id the process id used for the rules
*/
static void create_exclude_rules(DWORD process_id)
{
std::uint32_t n = 0;
const auto data = collect_exclude_rule_data();
for (const auto &exclude : data)
{
const auto id = exclude_rule_id(process_id, n++);
create_rule(id, exclude.domains, string::join(exclude.addresses, L";"), false);
}
}
static bool delete_rule()
/**
* @brief Remove our NRPT rules from the registry
*
* Iterate over the rules in the two know subkeys where NRPT rules can be located
* in the Windows registry and remove those rules, which we identify as ours. This
* is done by comparing the process id we add to the end of each rule we add. If
* the process id is zero all NRPT rules are deleted, regardless of the actual pid.
*
* @param process_id the process id used for the rule deletion
*/
static void delete_rules(DWORD process_id)
{
Reg::Key policies(wstring::from_utf8(dnsPolicyConfig()));
Win::Reg::KeyEnumerator keys(policies);
for (const auto &key : keys)
std::vector<std::wstring> del_subkeys;
// Only find rules to delete, so that the iterator stays valid
for (const auto &nrpt_subkey : nrpt_subkeys)
{
// remove only own policies
if (key.find(wstring::from_utf8(policyPrefix())) == std::wstring::npos)
continue;
const auto pid = L"-" + std::to_wstring(process_id);
typename REG::Key nrpt_key(nrpt_subkey);
typename REG::KeyEnumerator nrpt_rules(nrpt_key);
std::ostringstream ss;
ss << dnsPolicyConfig() << "\\" << wstring::to_utf8(key);
auto path = ss.str();
::RegDeleteTreeA(HKEY_LOCAL_MACHINE, path.c_str());
for (const auto &nrpt_rule_id : nrpt_rules)
{
// remove only own policies
if (nrpt_rule_id.find(wstring::from_utf8(id_prefix())) != 0)
continue;
if (process_id && nrpt_rule_id.rfind(pid) != (nrpt_rule_id.size() - pid.size()))
continue;
std::wostringstream rule_subkey;
rule_subkey << nrpt_subkey << L"\\" << nrpt_rule_id;
del_subkeys.push_back(rule_subkey.str());
}
}
// Now delete the rules
for (const auto &subkey : del_subkeys)
{
REG::delete_subkey(subkey);
}
return true;
}
private:
static const char *dnsPolicyConfig()
/**
* Holds the information for one NRPT exclude rule, i.e. data from
* local DNS configuration. Note that 'domains' is a MULTI_SZ string.
*/
struct ExcludeRuleData
{
static const char subkey[] = "SYSTEM\\CurrentControlSet\\Services\\Dnscache\\Parameters\\DnsPolicyConfig";
return subkey;
std::wstring domains;
std::vector<std::wstring> addresses;
};
/**
* @brief Get IPv4 DNS server addresses of an interface
*
* @param itf_guid The interface GUID string
* @return std::vector<std::wstring> IPv4 server addresses found
*/
static std::vector<std::wstring> interface_ipv4_dns_servers(const std::wstring &itf_guid)
{
typename REG::Key itf_key(std::wstring(REG::subkey_ipv4_itfs) + L"\\" + itf_guid);
auto [servers, error] = REG::get_string(itf_key, L"NameServer");
if (!error && !servers.empty())
{
return string::split(servers, ',');
}
if (dhcp_enabled_on_itf<REG>(itf_key))
{
auto [servers, error] = REG::get_string(itf_key, L"DhcpNameServer");
if (!error && !servers.empty())
{
return string::split(servers, ' ');
}
}
return {};
}
static const char *policyPrefix()
/**
* @brief Get IPv6 DNS server addresses of an interface
*
* @param itf_guid The interface GUID string
* @return std::vector<std::string> IPv6 server addresses found
*/
static std::vector<std::wstring> interface_ipv6_dns_servers(const std::wstring &itf_guid)
{
typename REG::Key itf_key(std::wstring(REG::subkey_ipv6_itfs) + L"\\" + itf_guid);
auto [servers, error] = REG::get_string(itf_key, L"NameServer");
if (!error && !servers.empty())
{
return string::split(servers, ',');
}
if (dhcp_enabled_on_itf<REG>(itf_key))
{
auto [in6_addrs, error] = REG::get_binary(itf_key, L"Dhcpv6DNSServers");
if (!error)
{
std::vector<std::wstring> addresses;
size_t in6_addr_count = in6_addrs.size() / sizeof(IN6_ADDR);
for (size_t i = 0; i < in6_addr_count; ++i)
{
WCHAR ipv6[64];
IN6_ADDR *in6_addr = reinterpret_cast<IN6_ADDR *>(in6_addrs.data()) + i;
if (::InetNtopW(AF_INET6, in6_addr, ipv6, _countof(ipv6)))
{
addresses.emplace_back(ipv6);
}
}
return addresses;
}
}
return {};
}
/**
* @brief Get all the data necessary for excluding local domains from the tunnel
*
* This data is only necessary if all the domains are to be resolved through
* the VPN. To not break resolving local DNS names, we add so called exclude rules
* to the NRPT for as long as the tunnel persists.
*
* @return std::vector<ExcludeRuleData> The data collected to create exclude rules from.
*/
static std::vector<ExcludeRuleData> collect_exclude_rule_data()
{
std::vector<ExcludeRuleData> data;
typename REG::Key itfs(REG::subkey_ipv4_itfs);
typename REG::KeyEnumerator itf_guids(itfs);
for (const auto &itf_guid : itf_guids)
{
// Ignore interfaces that are not connected or disabled
if (!NETAPI::interface_connected(itf_guid))
{
continue;
}
std::wstring domain = interface_dns_domain<REG>(itf_guid);
if (domain.empty())
{
continue;
}
// Get the DNS server addresses for the interface domain
auto addresses = interface_ipv4_dns_servers(itf_guid);
const auto addr6 = interface_ipv6_dns_servers(itf_guid);
addresses.insert(addresses.end(), addr6.begin(), addr6.end());
if (addresses.empty())
{
continue;
}
// Add a leading '.' to the domain and convert it to MULTI_SZ
domain.resize(domain.size() + 3);
domain.insert(domain.begin(), L'.');
domain.push_back(L'\0');
domain.push_back(L'\0');
data.push_back({domain, addresses});
}
return data;
}
/**
* Registry subkeys where NRPT rules can be found
*/
static constexpr std::array<PCWSTR, 2> nrpt_subkeys{
LR"(SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig)",
LR"(SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig)"};
/**
* @brief Open the NRPT key to store our rules at
*
* There are two places in the registry where NRPT rules can be found, depending
* on whether group policy rules are used or not. This function tries for the
* group policy place first and returns the key for the local rules in case it
* does not exist.
*
* @return REG::Key the opened Registry handle
*/
static typename REG::Key open_nrpt_base_key()
{
typename REG::Key key(nrpt_subkeys[0]);
if (key.defined())
{
return key;
}
return typename REG::Key(nrpt_subkeys[1]);
}
/**
* @brief Return the rule id prefix any rule starts with
*
* @return const char* the prefix string
*/
static const char *id_prefix()
{
static const char prefix[] = "OpenVPNDNSRouting";
return prefix;
}
/**
* @brief Generate a rule id string
*
* @param process_id the process id used for the rule
* @param exclude_rule whether the rule is for an exclude rule
* @param n the number of the exclude rule
* @return std::string the rule id string
*/
static std::string gen_rule_id(DWORD process_id, bool exclude_rule, std::uint32_t n)
{
std::ostringstream ss;
ss << id_prefix();
if (exclude_rule)
{
ss << "X-" << n;
}
ss << "-" << process_id;
return ss.str();
}
public:
/**
* @brief Return a NRPT rule id
*
* @param process_id the process id used for the rule
* @return std::string the rule is string
*/
static inline std::string rule_id(DWORD process_id)
{
return gen_rule_id(process_id, false, 0u);
}
/**
* @brief Return a NRPT exclude rule id
*
* @param process_id the process id used for the rule
* @param n the number of this rule
* @return std::string the rule id string
*/
static inline std::string exclude_rule_id(DWORD process_id, std::uint32_t n)
{
return gen_rule_id(process_id, true, n);
}
class ActionCreate : public Action
{
public:
ActionCreate(const std::vector<std::string> &names_arg,
const std::vector<std::string> &dns_servers_arg)
: names(names_arg),
dns_servers(dns_servers_arg)
ActionCreate(DWORD process_id,
const std::vector<std::string> &domains,
const std::vector<std::string> &dns_servers,
bool dnssec)
: process_id_(process_id),
domains_(domains),
dns_servers_(dns_servers),
dnssec_(dnssec)
{
}
virtual void execute(std::ostream &log) override
/**
* @brief Apply NRPT data to the registry
*
* In case a --dns server has no domains, we fall back to resolving
* "all domains" with it and install rules excluding the domains
* found on the system, so local domain names keep working.
*
* @param log where the rules will be logged to
*/
void execute(std::ostream &log) override
{
log << to_string() << std::endl;
create_rule(names, dns_servers);
// Convert domains into a wide MULTI_SZ string
std::wstring domains;
if (domains_.empty())
{
// --dns options did not specify any domains to resolve.
domains = L".";
domains.push_back(L'\0');
domains.push_back(L'\0');
create_exclude_rules(process_id_);
}
else
{
domains = wstring::pack_string_vector(domains_);
}
const std::string id = rule_id(process_id_);
const std::wstring servers = wstring::from_utf8(string::join(dns_servers_, ";"));
log << to_string() << " id=[" << id << "]" << std::endl;
create_rule(id, domains, servers, dnssec_);
}
virtual std::string to_string() const override
/**
* @brief Produce a textual representating of the NRPT data
*
* @return std::string the data as string
*/
std::string to_string() const override
{
std::ostringstream os;
os << "NRPT::ActionCreate"
<< " names=[" << string::join(names, ",") << "]"
<< " dns_servers=[" << string::join(dns_servers, ",") << "]";
<< " pid=[" << process_id_ << "]"
<< " domains=[" << string::join(domains_, ",") << "]"
<< " dns_servers=[" << string::join(dns_servers_, ",") << "]"
<< " dnssec=[" << dnssec_ << "]";
return os.str();
}
private:
const std::vector<std::string> names;
const std::vector<std::string> dns_servers;
DWORD process_id_;
const std::vector<std::string> domains_;
const std::vector<std::string> dns_servers_;
const bool dnssec_;
};
class ActionDelete : public Action
{
public:
virtual void execute(std::ostream &log) override
ActionDelete(DWORD process_id)
: process_id_(process_id)
{
log << to_string() << std::endl;
delete_rule();
}
virtual std::string to_string() const override
/**
* @brief Delete all rules this process has set.
*
* Note that the ActionCreate and ActionDelete must be
* executed from the same process for this to work reliably
*
* @param log where the log message goes
*/
void execute(std::ostream &log) override
{
return "NRPT::ActionDelete";
log << to_string() << std::endl;
delete_rules(process_id_);
}
/**
* @brief Return the log message
*
* @return std::string
*/
std::string to_string() const override
{
std::ostringstream ss;
ss << "NRPT::ActionDelete pid=[" << process_id_ << "]";
return ss.str();
}
protected:
DWORD process_id_;
};
};
using NRPT = Nrpt<Win::Reg, Win::NetApi>;
} // namespace TunWin
} // namespace openvpn
#endif

View File

@@ -20,6 +20,7 @@
#include "test_common.h"
#include <json/value.h>
#include <openvpn/client/dns.hpp>
using namespace openvpn;
@@ -34,13 +35,12 @@ TEST(Dns, Options)
"dns server -2 address [2.2.2.2]:5353\n"
"dns server -2 resolve-domains rdom0\n"
"dns server 1 address [1::1]:5353\n"
"dns server 1 exclude-domains xdom0\n"
"dns search-domains domain2\n"
"dns server -2 resolve-domains rdom1\n"
"dns server -2 dnssec optional\n"
"dns server -2 transport DoT\n"
"dns server -2 sni hostname\n"
"dns server 3 address 3::3 3.2.1.0:4242\n"
"dns server 3 address 3::3 3.2.1.0:4242 [3:3::3:3]:3333\n"
"dns server 3 dnssec no\n"
"dns server 3 transport DoH\n",
nullptr);
@@ -49,9 +49,9 @@ TEST(Dns, Options)
DnsOptions dns(config);
ASSERT_EQ(dns.search_domains.size(), 3u);
ASSERT_EQ(dns.search_domains[0], "domain0");
ASSERT_EQ(dns.search_domains[1], "domain1");
ASSERT_EQ(dns.search_domains[2], "domain2");
ASSERT_EQ(dns.search_domains[0].to_string(), "domain0");
ASSERT_EQ(dns.search_domains[1].to_string(), "domain1");
ASSERT_EQ(dns.search_domains[2].to_string(), "domain2");
ASSERT_EQ(dns.servers.size(), 3u);
@@ -65,17 +65,14 @@ TEST(Dns, Options)
{
ASSERT_EQ(i, 1);
ASSERT_TRUE(server.address4.specified());
ASSERT_EQ(server.address4.to_string(), "2.2.2.2");
ASSERT_EQ(server.port4, 5353u);
ASSERT_TRUE(server.address6.unspecified());
ASSERT_EQ(server.port6, 0u);
ASSERT_TRUE(server.addresses.size() == 1u);
ASSERT_EQ(server.addresses[0].address.version(), IP::Addr::V4);
ASSERT_EQ(server.addresses[0].address.to_string(), "2.2.2.2");
ASSERT_EQ(server.addresses[0].port, 5353u);
ASSERT_EQ(server.domains.size(), 2u);
ASSERT_EQ(server.domain_type, DnsServer::DomainType::Resolve);
ASSERT_EQ(server.domains[0], "rdom0");
ASSERT_EQ(server.domains[1], "rdom1");
ASSERT_EQ(server.domains[0].to_string(), "rdom0");
ASSERT_EQ(server.domains[1].to_string(), "rdom1");
ASSERT_EQ(server.dnssec, DnsServer::Security::Optional);
@@ -86,17 +83,16 @@ TEST(Dns, Options)
{
ASSERT_EQ(i, 2);
ASSERT_TRUE(server.address4.specified());
ASSERT_EQ(server.address4.to_string(), "1.1.1.1");
ASSERT_EQ(server.port4, 0u);
ASSERT_TRUE(server.addresses.size() == 2u);
ASSERT_EQ(server.addresses[0].address.version(), IP::Addr::V4);
ASSERT_EQ(server.addresses[0].address.to_string(), "1.1.1.1");
ASSERT_EQ(server.addresses[0].port, 0u);
ASSERT_TRUE(server.address6.specified());
ASSERT_EQ(server.address6.to_string(), "1::1");
ASSERT_EQ(server.port6, 5353u);
ASSERT_EQ(server.addresses[1].address.version(), IP::Addr::V6);
ASSERT_EQ(server.addresses[1].address.to_string(), "1::1");
ASSERT_EQ(server.addresses[1].port, 5353u);
ASSERT_EQ(server.domains.size(), 1u);
ASSERT_EQ(server.domain_type, DnsServer::DomainType::Exclude);
ASSERT_EQ(server.domains[0], "xdom0");
ASSERT_EQ(server.domains.size(), 0u);
ASSERT_EQ(server.dnssec, DnsServer::Security::Unset);
@@ -107,16 +103,20 @@ TEST(Dns, Options)
{
ASSERT_EQ(i, 3);
ASSERT_TRUE(server.address4.specified());
ASSERT_EQ(server.address4.to_string(), "3.2.1.0");
ASSERT_EQ(server.port4, 4242u);
ASSERT_TRUE(server.addresses.size() == 3u);
ASSERT_EQ(server.addresses[0].address.version(), IP::Addr::V6);
ASSERT_EQ(server.addresses[0].address.to_string(), "3::3");
ASSERT_EQ(server.addresses[0].port, 0u);
ASSERT_TRUE(server.address6.specified());
ASSERT_EQ(server.address6.to_string(), "3::3");
ASSERT_EQ(server.port6, 0u);
ASSERT_EQ(server.addresses[1].address.version(), IP::Addr::V4);
ASSERT_EQ(server.addresses[1].address.to_string(), "3.2.1.0");
ASSERT_EQ(server.addresses[1].port, 4242u);
ASSERT_EQ(server.addresses[2].address.version(), IP::Addr::V6);
ASSERT_EQ(server.addresses[2].address.to_string(), "3:3::3:3");
ASSERT_EQ(server.addresses[2].port, 3333u);
ASSERT_EQ(server.domains.size(), 0u);
ASSERT_EQ(server.domain_type, DnsServer::DomainType::Unset);
ASSERT_EQ(server.dnssec, DnsServer::Security::No);
@@ -151,13 +151,33 @@ TEST(Dns, OptionsMerger)
TEST(Dns, ServerNoAddress)
{
OptionList config;
config.parse_from_config("dns server 0 exclude-domains xdom0\n", nullptr);
config.parse_from_config("dns server 0 resolve-domains dom0\n", nullptr);
config.update_map();
JY_EXPECT_THROW(DnsOptions dns(config),
option_error,
"dns server 0 does not have an address assigned");
}
TEST(Dns, ServerEightAddresses)
{
OptionList config;
config.parse_from_config("dns server 0 address 1::1 2::2 3::3 4::4 5::5 6::6 7::7 8::8\n", nullptr);
config.update_map();
DnsOptions dns(config);
ASSERT_EQ(dns.servers.size(), 1u);
ASSERT_EQ(dns.servers[0].addresses.size(), 8u);
}
TEST(Dns, ServerTooManyAddresses)
{
OptionList config;
config.parse_from_config("dns server 0 address 1::1 2::2 3::3 4::4 5::5 6::6 7::7 8::8 9::9\n", nullptr);
config.update_map();
JY_EXPECT_THROW(DnsOptions dns(config),
option_error,
"dns server 0 option 'address' unknown or too many parameters");
}
TEST(Dns, ServerInvalidAddress)
{
OptionList config;
@@ -208,28 +228,201 @@ TEST(Dns, ServerInvalidTransport)
}
}
TEST(Dns, ServerMixedDomainType)
TEST(Dns, ToStringMinValuesSet)
{
{
OptionList config;
config.parse_from_config(
"dns server 0 resolve-domains this that\n"
"dns server 0 exclude-domains foo bar baz\n",
nullptr);
config.update_map();
JY_EXPECT_THROW(DnsOptions dns(config),
option_error,
"dns server 0 cannot use exclude-domains and resolve-domains together");
}
{
OptionList config;
config.parse_from_config(
"dns server 0 exclude-domains foo bar baz\n"
"dns server 0 resolve-domains this that\n",
nullptr);
config.update_map();
JY_EXPECT_THROW(DnsOptions dns(config),
option_error,
"dns server 0 cannot use resolve-domains and exclude-domains together");
}
OptionList config;
config.parse_from_config("dns server 10 address 1::1\n", nullptr);
config.update_map();
DnsOptions dns(config);
ASSERT_EQ(dns.to_string(),
"DNS Servers:\n"
" Priority: 10\n"
" Addresses:\n"
" 1::1\n");
}
TEST(Dns, ToStringAllValuesSet)
{
OptionList config;
config.parse_from_config(
"dns search-domains dom1 dom2 dom3\n"
"dns server 10 address 1::1 1.1.1.1\n"
"dns server 10 resolve-domains rdom11 rdom12\n"
"dns server 10 transport DoT\n"
"dns server 10 sni snidom1\n"
"dns server 10 dnssec optional\n"
"dns server 20 address 2::2 2.2.2.2\n"
"dns server 20 resolve-domains rdom21 rdom22\n"
"dns server 20 transport DoH\n"
"dns server 20 sni snidom2\n"
"dns server 20 dnssec yes\n",
nullptr);
config.update_map();
DnsOptions dns(config);
ASSERT_EQ(dns.to_string(),
"DNS Servers:\n"
" Priority: 10\n"
" Addresses:\n"
" 1::1\n"
" 1.1.1.1\n"
" Domains:\n"
" rdom11\n"
" rdom12\n"
" DNSSEC: Optional\n"
" Transport: TLS\n"
" SNI: snidom1\n"
" Priority: 20\n"
" Addresses:\n"
" 2::2\n"
" 2.2.2.2\n"
" Domains:\n"
" rdom21\n"
" rdom22\n"
" DNSSEC: Yes\n"
" Transport: HTTPS\n"
" SNI: snidom2\n"
"DNS Search Domains:\n"
" dom1\n"
" dom2\n"
" dom3\n");
}
TEST(Dns, JsonRoundtripMinValuesSet)
{
OptionList config;
config.parse_from_config("dns server 10 address 1::1\n", nullptr);
config.update_map();
DnsOptions toJson(config);
Json::Value json = toJson.to_json();
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
ASSERT_EQ(Json::writeString(builder, json),
"{\n"
" \"servers\" : \n"
" {\n"
" \"10\" : \n"
" {\n"
" \"addresses\" : \n"
" [\n"
" {\n"
" \"address\" : \"1::1\"\n"
" }\n"
" ]\n"
" }\n"
" }\n"
"}");
DnsOptions fromJson;
fromJson.from_json(json, "json test");
ASSERT_EQ(fromJson.to_string(),
"DNS Servers:\n"
" Priority: 10\n"
" Addresses:\n"
" 1::1\n");
}
TEST(Dns, JsonRoundtripAllValuesSet)
{
OptionList config;
config.parse_from_config(
"dns search-domains dom1 dom2 dom3\n"
"dns server 10 address 1::1 1.1.1.1\n"
"dns server 10 resolve-domains rdom11 rdom12\n"
"dns server 10 transport DoT\n"
"dns server 10 sni snidom1\n"
"dns server 10 dnssec optional\n"
"dns server 20 address [2::2]:5353 2.2.2.2:5353\n"
"dns server 20 resolve-domains rdom21 rdom22\n"
"dns server 20 transport DoH\n"
"dns server 20 sni snidom2\n"
"dns server 20 dnssec yes\n",
nullptr);
config.update_map();
DnsOptions toJson(config);
Json::Value json = toJson.to_json();
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
ASSERT_EQ(Json::writeString(builder, json),
"{\n"
" \"search_domains\" : \n"
" [\n"
" \"dom1\",\n"
" \"dom2\",\n"
" \"dom3\"\n"
" ],\n"
" \"servers\" : \n"
" {\n"
" \"10\" : \n"
" {\n"
" \"addresses\" : \n"
" [\n"
" {\n"
" \"address\" : \"1::1\"\n"
" },\n"
" {\n"
" \"address\" : \"1.1.1.1\"\n"
" }\n"
" ],\n"
" \"dnssec\" : \"Optional\",\n"
" \"domains\" : \n"
" [\n"
" \"rdom11\",\n"
" \"rdom12\"\n"
" ],\n"
" \"sni\" : \"snidom1\",\n"
" \"transport\" : \"TLS\"\n"
" },\n"
" \"20\" : \n"
" {\n"
" \"addresses\" : \n"
" [\n"
" {\n"
" \"address\" : \"2::2\",\n"
" \"port\" : 5353\n"
" },\n"
" {\n"
" \"address\" : \"2.2.2.2\",\n"
" \"port\" : 5353\n"
" }\n"
" ],\n"
" \"dnssec\" : \"Yes\",\n"
" \"domains\" : \n"
" [\n"
" \"rdom21\",\n"
" \"rdom22\"\n"
" ],\n"
" \"sni\" : \"snidom2\",\n"
" \"transport\" : \"HTTPS\"\n"
" }\n"
" }\n"
"}");
DnsOptions fromJson;
fromJson.from_json(json, "json test");
ASSERT_EQ(fromJson.to_string(),
"DNS Servers:\n"
" Priority: 10\n"
" Addresses:\n"
" 1::1\n"
" 1.1.1.1\n"
" Domains:\n"
" rdom11\n"
" rdom12\n"
" DNSSEC: Optional\n"
" Transport: TLS\n"
" SNI: snidom1\n"
" Priority: 20\n"
" Addresses:\n"
" 2::2 5353\n"
" 2.2.2.2 5353\n"
" Domains:\n"
" rdom21\n"
" rdom22\n"
" DNSSEC: Yes\n"
" Transport: HTTPS\n"
" SNI: snidom2\n"
"DNS Search Domains:\n"
" dom1\n"
" dom2\n"
" dom3\n");
}