diff --git a/daemon/README.rst b/daemon/README.rst index e7ce3c000b8102d5dc1217302a7c5a391860b823..c7979012231767c2bfcad273ea39db7ff5510979 100644 --- a/daemon/README.rst +++ b/daemon/README.rst @@ -595,6 +595,8 @@ For when listening on ``localhost`` just doesn't cut it. > net.tcp_pipeline(50) 50 +.. _tls-server-config: + .. function:: net.tls([cert_path], [key_path]) Get/set path to a server TLS certificate and private key for DNS/TLS. diff --git a/daemon/bindings.c b/daemon/bindings.c index 46aaa8c504a71c566c6d2095aaf027ef522596f2..74e9c865fbb04bf5cf79b6dc1abfa8cf3885e276 100644 --- a/daemon/bindings.c +++ b/daemon/bindings.c @@ -391,7 +391,7 @@ static int net_tls(lua_State *L) int r = tls_certificate_set(net, lua_tostring(L, 1), lua_tostring(L, 2)); if (r != 0) { - lua_pushstring(L, strerror(ENOMEM)); + lua_pushstring(L, kr_strerror(r)); lua_error(L); } @@ -511,7 +511,7 @@ static int net_tls_client(lua_State *L) int r = tls_client_params_set(&net->tls_client_params, addr, port, NULL, NULL, NULL); if (r != 0) { - lua_pushstring(L, strerror(ENOMEM)); + lua_pushstring(L, kr_strerror(r)); lua_error(L); } @@ -529,7 +529,7 @@ static int net_tls_client(lua_State *L) int r = tls_client_params_set(&net->tls_client_params, addr, port, NULL, NULL, pin); if (r != 0) { - lua_pushstring(L, strerror(ENOMEM)); + lua_pushstring(L, kr_strerror(r)); lua_error(L); } lua_pop(L, 1); @@ -555,7 +555,7 @@ static int net_tls_client(lua_State *L) int r = tls_client_params_set(&net->tls_client_params, addr, port, ca_file, NULL, NULL); if (r != 0) { - lua_pushstring(L, strerror(ENOMEM)); + lua_pushstring(L, kr_strerror(r)); lua_error(L); } /* removes 'value'; keeps 'key' for next iteration */ @@ -569,7 +569,7 @@ static int net_tls_client(lua_State *L) int r = tls_client_params_set(&net->tls_client_params, addr, port, NULL, hostname, NULL); if (r != 0) { - lua_pushstring(L, strerror(ENOMEM)); + lua_pushstring(L, kr_strerror(r)); lua_error(L); } /* removes 'value'; keeps 'key' for next iteration */ @@ -1393,7 +1393,7 @@ static int wrk_resolve(lua_State *L) /* Create query packet */ knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_EDNS_MAX_UDP_PAYLOAD, NULL); if (!pkt) { - lua_pushstring(L, strerror(ENOMEM)); + lua_pushstring(L, kr_strerror(ENOMEM)); lua_error(L); } uint8_t dname[KNOT_DNAME_MAXLEN]; diff --git a/daemon/tls.c b/daemon/tls.c index 17e0bf5bbdc0ee729ce069f0d36fcde44c70559c..7c92be92a466e518d40424ece57ae38f50b367f1 100644 --- a/daemon/tls.c +++ b/daemon/tls.c @@ -578,7 +578,7 @@ int tls_client_params_set(map_t *tls_client_paramlist, char key[INET6_ADDRSTRLEN + 6]; size_t keylen = sizeof(key); if (kr_straddr_join(addr, port, key, &keylen) != kr_ok()) { - kr_log_error("[tls client] warning: '%s' is not a valid ip address, ignoring\n", addr); + kr_log_error("[tls_client] warning: '%s' is not a valid ip address, ignoring\n", addr); return kr_ok(); } @@ -593,7 +593,7 @@ int tls_client_params_set(map_t *tls_client_paramlist, int ret = gnutls_certificate_allocate_credentials(&entry->credentials); if (ret != GNUTLS_E_SUCCESS) { free(entry); - kr_log_error("[tls client] error: gnutls_certificate_allocate_credentials() fails (%s)\n", + kr_log_error("[tls_client] error: gnutls_certificate_allocate_credentials() fails (%s)\n", gnutls_strerror_name(ret)); return kr_error(ENOMEM); } @@ -605,7 +605,7 @@ int tls_client_params_set(map_t *tls_client_paramlist, bool already_exists = false; for (size_t i = 0; i < entry->ca_files.len; ++i) { if (strcmp(entry->ca_files.at[i], ca_file) == 0) { - kr_log_error("[tls client] error: ca file '%s'for address '%s' already was set, ignoring\n", ca_file, key); + kr_log_error("[tls_client] error: ca file '%s'for address '%s' already was set, ignoring\n", ca_file, key); already_exists = true; break; } @@ -621,7 +621,7 @@ int tls_client_params_set(map_t *tls_client_paramlist, int res = gnutls_certificate_set_x509_trust_file(entry->credentials, value, GNUTLS_X509_FMT_PEM); if (res < 0) { - kr_log_error("[tls client], failed to import certificate file '%s' (%s)\n", + kr_log_error("[tls_client] failed to import certificate file '%s' (%s)\n", value, gnutls_strerror_name(res)); /* value will be freed at cleanup */ ret = kr_error(EINVAL); @@ -634,7 +634,7 @@ int tls_client_params_set(map_t *tls_client_paramlist, bool already_exists = false; for (size_t i = 0; i < entry->hostnames.len; ++i) { if (strcmp(entry->hostnames.at[i], hostname) == 0) { - kr_log_error("[tls client] error: hostname '%s' for address '%s' already was set, ignoring\n", hostname, key); + kr_log_error("[tls_client] error: hostname '%s' for address '%s' already was set, ignoring\n", hostname, key); already_exists = true; break; } @@ -653,7 +653,7 @@ int tls_client_params_set(map_t *tls_client_paramlist, if ((ret == kr_ok()) && pin && pin[0] != 0) { for (size_t i = 0; i < entry->pins.len; ++i) { if (strcmp(entry->pins.at[i], pin) == 0) { - kr_log_error("[tls client] warning: pin '%s' for address '%s' already was set, ignoring\n", pin, key); + kr_log_error("[tls_client] warning: pin '%s' for address '%s' already was set, ignoring\n", pin, key); return kr_ok(); } } @@ -1009,7 +1009,7 @@ int tls_client_connect_start(struct tls_client_ctx_t *ctx, if (ret == GNUTLS_E_SUCCESS) { return kr_ok(); } else if (gnutls_error_is_fatal(ret) != 0) { - kr_log_error("[tls client] handshake failed (%s)\n", gnutls_strerror(ret)); + kr_log_error("[tls_client] handshake failed (%s)\n", gnutls_strerror(ret)); return kr_error(ECONNABORTED); } return kr_error(EAGAIN); diff --git a/lib/utils.c b/lib/utils.c index ac14767ce63aa29e58c02a7c687110a733b37c65..fa516e55208f7489ae7fb18054736201c421998d 100644 --- a/lib/utils.c +++ b/lib/utils.c @@ -77,7 +77,7 @@ static inline int u16tostr(uint8_t *dst, uint16_t num) static void kres_gnutls_log(int level, const char *message) { - kr_log_verbose("gnutls: (%d) %s", level, message); + kr_log_verbose("[gnutls] (%d) %s", level, message); } bool kr_verbose_set(bool status) diff --git a/modules/policy/README.rst b/modules/policy/README.rst index 2c082ff198cb222837b8609dbecd173298ccb755..9b1bf188faf2b30ae1a3ef465379cec71f76f510 100644 --- a/modules/policy/README.rst +++ b/modules/policy/README.rst @@ -28,7 +28,8 @@ There are several actions available in the ``policy.`` table: * ``DENY`` - reply NXDOMAIN authoritatively * ``DROP`` - terminate query resolution and return SERVFAIL to the requestor * ``TC`` - set TC=1 if the request came through UDP, forcing client to retry with TCP -* ``FORWARD(ip)`` - solve a query via forwarding to an IP while validating and caching locally; +* ``FORWARD(ip)`` - resolve a query via forwarding to an IP while validating and caching locally; +* ``TLS_FORWARD({{ip, authentication}})`` - resolve a query via TLS connection forwarding to an IP while validating and caching locally; the parameter can be a single IP (string) or a lua list of up to four IPs. * ``STUB(ip)`` - similar to ``FORWARD(ip)`` but *without* attempting DNSSEC validation. Each request may be either answered from cache or simply sent to one of the IPs with proxying back the answer. @@ -43,8 +44,41 @@ Most actions stop the policy matching on the query, but "chain actions" allow to .. note:: The module (and ``kres``) expects domain names in wire format, not textual representation. So each label in name is prefixed with its length, e.g. "example.com" equals to ``"\7example\3com"``. You can use convenience function ``todname('example.com')`` for automatic conversion. -Examples -^^^^^^^^ +Forwarding over TLS protocol (DNS-over-TLS) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Policy `TLS_FORWARD` allows you to forward queries using `Transport Layer Security`_ protocol, which hides the content of your queries from an attacker observing the network traffic. Further details about this protocol can be found in `RFC 7858`_ and `IETF draft dprive-dtls-and-tls-profiles`_. + +Queries affected by `TLS_FORWARD` policy will always be resolved over TLS connection. Knot Resolver does not implement fallback to non-TLS connection, so if TLS connection cannot be established or authenticated according to the configuration, the resolution will fail. + +To test this feature you need to either :ref:`configure Knot Resolver as DNS-over-TLS server <tls-server-config>`, or pick some public DNS-over-TLS server. Please see `DNS Privacy Project`_ homepage for list of public servers. + +When multiple servers are specified, the one with the lowest round-trip time is used. + +TLS Examples +~~~~~~~~~~~~ + +.. code-block:: lua + + modules = { 'policy' } + -- forward all queries over TLS to the specified server + policy.add(policy.all(policy.TLS_FORWARD({{'192.0.2.1', pin_sha256='YQ=='}}))) + -- for brevity, other TLS examples omit policy.add(policy.all()) + -- single server authenticated using its certificate pin_sha256 + policy.TLS_FORWARD({{'192.0.2.1', pin_sha256='YQ=='}}) -- pin_sha256 is base64-encoded + -- single server using non-standard port + policy.TLS_FORWARD({{'192.0.2.1@443', pin_sha256='YQ=='}}) -- use @ or # to specify port + -- single server with multiple valid pins (e.g. anycast) + policy.TLS_FORWARD({{'192.0.2.1', pin_sha256={'YQ==', 'Wg=='}}) + -- multiple servers, each with own authenticator + policy.TLS_FORWARD({ -- please note that { here starts list of servers + {'192.0.2.1', pin_sha256='Wg=='}, + -- server must present certificate issued by specified CA and hostname must match + {'2001:DB8::d0c', hostname='res.example.', ca_file='/etc/knot-resolver/tlsca.crt'} + }) + + +Other examples +^^^^^^^^^^^^^^ .. code-block:: lua @@ -164,3 +198,7 @@ Most properties (actions, filters) are described above. .. _RPZ: https://dnsrpz.info/ .. _`Pro DNS and BIND`: http://www.zytrax.com/books/dns/ch7/rpz.html .. _`Jan-Piet Mens's post`: http://jpmens.net/2011/04/26/how-to-configure-your-bind-resolvers-to-lie-using-response-policy-zones-rpz/ +.. _`Transport Layer Security`: https://en.wikipedia.org/wiki/Transport_Layer_Security +.. _`DNS Privacy Project`: https://dnsprivacy.org/ +.. _`RFC 7858`: https://tools.ietf.org/html/rfc7858 +.. _`IETF draft dprive-dtls-and-tls-profiles`: https://tools.ietf.org/html/draft-ietf-dprive-dtls-and-tls-profiles diff --git a/modules/policy/policy.lua b/modules/policy/policy.lua index 7a829954a21c7a5c82b79c305159c6e112b0e180..95010d4f07ed35576279ae607537ece685c5ff7a 100644 --- a/modules/policy/policy.lua +++ b/modules/policy/policy.lua @@ -128,78 +128,114 @@ local function forward(target) end end --- Forward request and all subrequests to upstream over TCP; validate answers +-- object must be non-empty string or non-empty table of non-empty strings +local function is_nonempty_string_or_table(object) + if type(object) == 'string' then + return #object ~= 0 + elseif type(object) ~= 'table' or not next(object) then + return false + end + for _, val in pairs(object) do + if type(val) ~= 'string' or #val == 0 then + return false + end + end + return true +end + +local function insert_from_string_or_table(source, destination) + if type(source) == 'table' then + for _, v in pairs(source) do + table.insert(destination, v) + end + else + table.insert(destination, source) + end +end + +-- Check for allowed authentication types and return type for the current target +local function tls_forward_target_authtype(idx, target) + if (target.pin_sha256 and not (target.ca_file or target.hostname or target.insecure)) then + if not is_nonempty_string_or_table(target.pin_sha256) then + error('TLS_FORWARD target authentication is invalid at position ' + .. idx .. '; pin_sha256 must be string or list of strings') + end + return 'pin_sha256' + elseif (target.insecure and not (target.ca_file or target.hostname or target.pin_sha256)) then + return 'insecure' + elseif (target.ca_file and target.hostname and not (target.insecure or target.pin_sha256)) then + if not (is_nonempty_string_or_table(target.hostname) + and is_nonempty_string_or_table(target.ca_file)) then + error('TLS_FORWARD target authentication is invalid at position ' + .. idx .. '; hostname and ca_file must be string or list of strings') + end + return 'cert' + else + error('TLS_FORWARD authentication options at position ' .. idx + .. ' are invalid; specify one of: pin_sha256 / hostname+ca_file / insecure') + end +end + +local function tls_forward_target_check_syntax(idx, list_entry) + if type(list_entry) ~= 'table' then + error('TLS_FORWARD target must be a non-empty table (found ' + .. type(list_entry) .. ' at position ' .. idx .. ')') + end + if type(list_entry[1]) ~= 'string' then + error('TLS_FORWARD target must start with an IP address (found ' + .. type(list_entry[1]) .. ' at the beginning of target position ' .. idx .. ')') + end +end + +-- Forward request and all subrequests to upstream over TLS; validate answers local function tls_forward(target) - local sockaddr_list = {} - local addr_list = {} + local sockaddr_c_list = {} + local sockaddr_config = {} -- items: { string_addr=<addr string>, auth_type=<auth type> } local ca_files = {} local hostnames = {} local pins = {} - if type(target) ~= 'table' then - assert(false, 'wrong TLS_FORWARD target') - end - for _, upstream_list_entry in pairs(target) do - local upstream_addr = upstream_list_entry[1] - if type(upstream_addr) ~= 'string' then - assert(false, 'bad IP address in TLS_FORWARD target') - end - table.insert(sockaddr_list, addr2sock(upstream_addr, 853)) - table.insert(addr_list, upstream_addr) - local ca_file = upstream_list_entry['ca_file'] - if ca_file ~= nil then - local hostname = upstream_list_entry['hostname'] - if hostname == nil then - assert(false, 'hostname(s) is absent in TLS_FORWARD target') - end - local ca_files_local = {} - if type(ca_file) == 'table' then - for _, v in pairs(ca_file) do - table.insert(ca_files_local, v) - end - else - table.insert(ca_files_local, ca_file) - end - local hostnames_local = {} - if type(hostname) == 'table' then - for _, v in pairs(hostname) do - table.insert(hostnames_local, v) - end - else - table.insert(hostnames_local, hostname) - end - if next(ca_files_local) then - ca_files[upstream_addr] = ca_files_local - end - if next(hostnames_local) then - hostnames[upstream_addr] = hostnames_local - end - end - local pin = upstream_list_entry['pin'] - local pins_local = {} - if pin ~= nil then - if type(pin) == 'table' then - for _, v in pairs(pin) do - table.insert(pins_local, v) - end - else - table.insert(pins_local, pin) - end + if type(target) ~= 'table' or #target < 1 then + error('TLS_FORWARD argument must be a non-empty table') + end + for idx, upstream_list_entry in pairs(target) do + tls_forward_target_check_syntax(idx, upstream_list_entry) + local auth_type = tls_forward_target_authtype(idx, upstream_list_entry) + local string_addr = upstream_list_entry[1] + local sockaddr_c = addr2sock(string_addr, 853) + local sockaddr_lua = ffi.string(sockaddr_c, ffi.C.kr_inaddr_len(sockaddr_c)) + if sockaddr_config[sockaddr_lua] then + error('TLS_FORWARD configuration cannot declare two configs for IP address ' .. string_addr) end - if next(pins_local) then - pins[upstream_addr] = pins_local + table.insert(sockaddr_c_list, sockaddr_c) + sockaddr_config[sockaddr_lua] = {string_addr=string_addr, auth_type=auth_type} + if auth_type == 'cert' then + ca_files[sockaddr_lua] = {} + hostnames[sockaddr_lua] = {} + insert_from_string_or_table(upstream_list_entry.ca_file, ca_files[sockaddr_lua]) + insert_from_string_or_table(upstream_list_entry.hostname, hostnames[sockaddr_lua]) + elseif auth_type == 'pin_sha256' then + pins[sockaddr_lua] = {} + insert_from_string_or_table(upstream_list_entry.pin_sha256, pins[sockaddr_lua]) + elseif auth_type ~= 'insecure' then + -- insecure does nothing, user does not want authentication + assert(false, 'unsupported auth_type') end end - -- Update the global table of authentication data. - for _, v in pairs(addr_list) do - if (pins[v] == nil and ca_files[v] == nil) then - net.tls_client(v) - elseif (pins[v] ~= nil and ca_files[v] == nil) then - net.tls_client(v, pins[v]) - elseif (pins[v] == nil and ca_files[v] ~= nil) then - net.tls_client(v, ca_files[v], hostnames[v]) + -- Update the global table of authentication data only if all checks above passed + for sockaddr_lua, config in pairs(sockaddr_config) do + assert(#config.string_addr > 0) + if config.auth_type == 'insecure' then + net.tls_client(config.string_addr) + elseif config.auth_type == 'pin_sha256' then + assert(#pins[sockaddr_lua] > 0) + net.tls_client(config.string_addr, pins[sockaddr_lua]) + elseif config.auth_type == 'cert' then + assert(#ca_files[sockaddr_lua] > 0) + assert(#hostnames[sockaddr_lua] > 0) + net.tls_client(config.string_addr, ca_files[sockaddr_lua], hostnames[sockaddr_lua]) else - net.tls_client(v, pins[v], ca_files[v], hostnames[v]) + assert(false, 'unsupported auth_type') end end @@ -213,7 +249,7 @@ local function tls_forward(target) qry.flags.AWAIT_CUT = true req.options.TCP = true qry.flags.TCP = true - set_nslist(qry, sockaddr_list) + set_nslist(qry, sockaddr_c_list) return state end end diff --git a/modules/policy/policy_test.lua b/modules/policy/policy_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..61da580fd12c85dc34186edef6c3388437f4f71f --- /dev/null +++ b/modules/policy/policy_test.lua @@ -0,0 +1,45 @@ +-- setup resolver +modules = { 'policy' } + +-- test for default configuration +local function test_tls_forward() + boom(policy.TLS_FORWARD, {}, 'TLS_FORWARD without arguments') + boom(policy.TLS_FORWARD, {'1'}, 'TLS_FORWARD with non-table argument') + boom(policy.TLS_FORWARD, {{}}, 'TLS_FORWARD with empty table') + boom(policy.TLS_FORWARD, {{{}}}, 'TLS_FORWARD with empty target table') + boom(policy.TLS_FORWARD, {{{bleble=''}}}, 'TLS_FORWARD with invalid parameters in table') + + boom(policy.TLS_FORWARD, {{'1'}}, 'TLS_FORWARD with invalid IP address') + -- boom(policy.TLS_FORWARD, {{{'::1', bleble=''}}}, 'TLS_FORWARD with valid IP and invalid parameters') + boom(policy.TLS_FORWARD, {{{'127.0.0.1'}}}, 'TLS_FORWARD with missing auth parameters') + + ok(policy.TLS_FORWARD({{'127.0.0.1', insecure=true}}), 'TLS_FORWARD with no authentication') + boom(policy.TLS_FORWARD, {{{'100:dead::', insecure=true}, + {'100:DEAD:0::', insecure=true} + }}, 'TLS_FORWARD with duplicate IP addresses is not allowed') + ok(policy.TLS_FORWARD({{'100:dead::', insecure=true}, + {'100:dead::@443', insecure=true} + }), 'TLS_FORWARD with duplicate IP addresses but different ports is allowed') + + boom(policy.TLS_FORWARD, {{{'::1', pin_sha256=''}}}, 'TLS_FORWARD with empty pin_sha256') + -- boom(policy.TLS_FORWARD, {{{'::1', pin_sha256='Ä'}}}, 'TLS_FORWARD with bad pin_sha256') + ok(policy.TLS_FORWARD({ + {'::1', pin_sha256='ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQ=='} + }), 'TLS_FORWARD with base64 pin_sha256') + ok(policy.TLS_FORWARD({ + {'::1', pin_sha256={ + 'ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQ==', + 'MTcwYWUzMGNjZDlmYmE2MzBhZjhjZGE2ODQxZTAwYzZiNjU3OWNlYzc3NmQ0MTllNzAyZTIwYzY5YzQ4OGZmOA==' + }}}), 'TLS_FORWARD with table of pins') + + -- ok(policy.TLS_FORWARD({{'::1', hostname='test.', ca_file='/tmp/ca.crt'}}), 'TLS_FORWARD with hostname + CA cert') + boom(policy.TLS_FORWARD, {{{'::1', hostname='test.'}}}, 'TLS_FORWARD with just hostname') + boom(policy.TLS_FORWARD, {{{'::1', ca_file='/tmp/ca.crt'}}}, 'TLS_FORWARD with just CA cert') + boom(policy.TLS_FORWARD, {{{'::1', hostname='', ca_file='/tmp/ca.crt'}}}, 'TLS_FORWARD with empty hostname + CA cert') + boom(policy.TLS_FORWARD, {{{'::1', hostname='test.', ca_file='/dev/a_file_which_surely_does_NOT_exist!'}}}, + 'TLS_FORWARD with hostname + unreadable CA cert') +end + +return { + test_tls_forward +}