Commit 8775c8dd authored by chantra's avatar chantra Committed by Petr Špaček

dot module in lua

This modules allows knot-resolver to discover authoritative servers SPKI
digest by leveraging magic NS target names ala DNSCurve.
parent a1ba8458
......@@ -323,6 +323,7 @@ modules.load('detect_time_skew')
modules.load('detect_time_jump')
modules.load('ta_sentinel')
modules.load('edns_keepalive')
modules.load('dot')
-- Interactive command evaluation
function eval_cmd(line, raw)
......
.. _mod-dot:
DNS-over-TLS (DoT) Auto-discovery
---------------------------------
DoT module enables automatic discovery of authoritative servers' SPKI
fingerprint via the use of magic NS names. It is very similar to `dnscurve`_ mechanism.
.. warning:: This module is experimental.
Requirements
^^^^^^^^^^^^
At the time of this writting, this module is to be built on top of the
`cloudflare`_ branch of knot-resolver.
How it works
^^^^^^^^^^^^
The module will look for NS target names formatted as:
``dot-{base32(sha256(SPKI))}....``
For instance:
.. code-block:: bash
example.com NS dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.example.com
will automatically discover that example.com NS supports DoT with the base64-encoded SPKI digest of ``m+12GgMFIiheEhKvUcOynjbn3WYQUp5tVGDh7Snwj/Q=``
and will associate it with the IPs of ``dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.example.com``.
In that example, the base32 encoded (no padding) version of the sha256 PIN is ``tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a``, which when
converted to base64 translates to ``m+12GgMFIiheEhKvUcOynjbn3WYQUp5tVGDh7Snwj/Q=``.
Generating NS targets
^^^^^^^^^^^^^^^^^^^^^
To generate the NS target name, use the following command to generate the base32 encoded string of the SPKI fingerprint:
.. code-block:: bash
openssl x509 -in /path/to/cert.pem -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
base32 | tr -d '=' | tr '[:upper:]' '[:lower:]'
tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a
Then add a target to your NS with: ``dot-${b32}.a.example.com``
Finally, map ``dot-${b32}.a.example.com`` to the right set of IPs.
.. code-block:: bash
...
...
;; QUESTION SECTION:
;example.com. IN NS
;; AUTHORITY SECTION:
example.com. 3600 IN NS dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.a.example.com.
example.com. 3600 IN NS dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.b.example.com.
;; ADDITIONAL SECTION:
dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.a.example.com. 3600 IN A 192.0.2.1
dot-tpwxmgqdaurcqxqsckxvdq5sty3opxlgcbjj43kumdq62kpqr72a.b.example.com. 3600 IN AAAA 2001:DB8::1
...
...
Example configuration
^^^^^^^^^^^^^^^^^^^^^
To enable the module, add this stanza to your config:
.. code-block:: lua
-- Load the module
modules.load('dot')
Caveats
^^^^^^^
The module relies on seeing the reply of the NS query and as such will not work
if knot-resolver use its cache. You may need to delete the cache before starting ``kresd`` to work around this.
The module also assumes that the NS query answer will return both the NS targets in the Authority section as well as the glue records in the Additional section.
.. _dnscurve: https://dnscurve.org/
.. _cloudflare: https://gitlab.labs.nic.cz/knot/knot-resolver/tree/cloudflare
--------------------------------------------------------------------------------
-- util functions
--------------------------------------------------------------------------------
local function divide_string( str, max, fillChar )
fillChar = fillChar or ""
local result = {}
local start = 1
for i = 1, #str do
if i % max == 0 then
table.insert( result, str:sub( start, i ) )
start = i + 1
elseif i == #str then
table.insert( result, str:sub( start, i ) )
end
end
return result
end
local function number_to_bit( num, length )
local bits = {}
while num > 0 do
local rest = math.floor( math.fmod( num, 2 ) )
table.insert( bits, rest )
num = ( num - rest ) / 2
end
while #bits < length do
table.insert( bits, "0" )
end
return string.reverse( table.concat( bits ) )
end
local function ignore_set( str, set )
if set then
str = str:gsub( "["..set.."]", "" )
end
return str
end
local function pure_from_bit( str )
return ( str:gsub( '........', function ( cc )
return string.char( tonumber( cc, 2 ) )
end ) )
end
local function unexpected_char_error( str, pos )
local c = string.sub( str, pos, pos )
return string.format( "unexpected character at position %d: '%s'", pos, c )
end
--------------------------------------------------------------------------------
local basexx = {}
--------------------------------------------------------------------------------
-- base2(bitfield) decode and encode function
--------------------------------------------------------------------------------
local bitMap = { o = "0", i = "1", l = "1" }
function basexx.from_bit( str, ignore )
str = ignore_set( str, ignore )
str = string.lower( str )
str = str:gsub( '[ilo]', function( c ) return bitMap[ c ] end )
local pos = string.find( str, "[^01]" )
if pos then return nil, unexpected_char_error( str, pos ) end
return pure_from_bit( str )
end
function basexx.to_bit( str )
return ( str:gsub( '.', function ( c )
local byte = string.byte( c )
local bits = {}
for i = 1,8 do
table.insert( bits, byte % 2 )
byte = math.floor( byte / 2 )
end
return table.concat( bits ):reverse()
end ) )
end
--------------------------------------------------------------------------------
-- base16(hex) decode and encode function
--------------------------------------------------------------------------------
function basexx.from_hex( str, ignore )
str = ignore_set( str, ignore )
local pos = string.find( str, "[^%x]" )
if pos then return nil, unexpected_char_error( str, pos ) end
return ( str:gsub( '..', function ( cc )
return string.char( tonumber( cc, 16 ) )
end ) )
end
function basexx.to_hex( str )
return ( str:gsub( '.', function ( c )
return string.format('%02X', string.byte( c ) )
end ) )
end
--------------------------------------------------------------------------------
-- generic function to decode and encode base32/base64
--------------------------------------------------------------------------------
local function from_basexx( str, alphabet, bits )
local result = {}
for i = 1, #str do
local c = string.sub( str, i, i )
if c ~= '=' then
local index = string.find( alphabet, c, 1, true )
if not index then
return nil, unexpected_char_error( str, i )
end
table.insert( result, number_to_bit( index - 1, bits ) )
end
end
local value = table.concat( result )
local pad = #value % 8
return pure_from_bit( string.sub( value, 1, #value - pad ) )
end
local function to_basexx( str, alphabet, bits, pad )
local bitString = basexx.to_bit( str )
local chunks = divide_string( bitString, bits )
local result = {}
for key,value in ipairs( chunks ) do
if ( #value < bits ) then
value = value .. string.rep( '0', bits - #value )
end
local pos = tonumber( value, 2 ) + 1
table.insert( result, alphabet:sub( pos, pos ) )
end
table.insert( result, pad )
return table.concat( result )
end
--------------------------------------------------------------------------------
-- rfc 3548: http://www.rfc-editor.org/rfc/rfc3548.txt
--------------------------------------------------------------------------------
local base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
local base32PadMap = { "", "======", "====", "===", "=" }
function basexx.from_base32( str, ignore )
str = ignore_set( str, ignore )
return from_basexx( string.upper( str ), base32Alphabet, 5 )
end
function basexx.to_base32( str )
return to_basexx( str, base32Alphabet, 5, base32PadMap[ #str % 5 + 1 ] )
end
--------------------------------------------------------------------------------
-- crockford: http://www.crockford.com/wrmg/base32.html
--------------------------------------------------------------------------------
local crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
local crockfordMap = { O = "0", I = "1", L = "1" }
function basexx.from_crockford( str, ignore )
str = ignore_set( str, ignore )
str = string.upper( str )
str = str:gsub( '[ILOU]', function( c ) return crockfordMap[ c ] end )
return from_basexx( str, crockfordAlphabet, 5 )
end
function basexx.to_crockford( str )
return to_basexx( str, crockfordAlphabet, 5, "" )
end
--------------------------------------------------------------------------------
-- base64 decode and encode function
--------------------------------------------------------------------------------
local base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"..
"abcdefghijklmnopqrstuvwxyz"..
"0123456789+/"
local base64PadMap = { "", "==", "=" }
function basexx.from_base64( str, ignore )
str = ignore_set( str, ignore )
return from_basexx( str, base64Alphabet, 6 )
end
function basexx.to_base64( str )
return to_basexx( str, base64Alphabet, 6, base64PadMap[ #str % 3 + 1 ] )
end
--------------------------------------------------------------------------------
-- URL safe base64 decode and encode function
--------------------------------------------------------------------------------
local url64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"..
"abcdefghijklmnopqrstuvwxyz"..
"0123456789-_"
function basexx.from_url64( str, ignore )
str = ignore_set( str, ignore )
return from_basexx( str, url64Alphabet, 6 )
end
function basexx.to_url64( str )
return to_basexx( str, url64Alphabet, 6, "" )
end
--------------------------------------------------------------------------------
--
--------------------------------------------------------------------------------
local function length_error( len, d )
return string.format( "invalid length: %d - must be a multiple of %d", len, d )
end
local z85Decoder = { 0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00,
0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x40, 0x00, 0x49, 0x42, 0x4A, 0x47,
0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00,
0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00 }
function basexx.from_z85( str, ignore )
str = ignore_set( str, ignore )
if ( #str % 5 ) ~= 0 then
return nil, length_error( #str, 5 )
end
local result = {}
local value = 0
for i = 1, #str do
local index = string.byte( str, i ) - 31
if index < 1 or index >= #z85Decoder then
return nil, unexpected_char_error( str, i )
end
value = ( value * 85 ) + z85Decoder[ index ]
if ( i % 5 ) == 0 then
local divisor = 256 * 256 * 256
while divisor ~= 0 do
local b = math.floor( value / divisor ) % 256
table.insert( result, string.char( b ) )
divisor = math.floor( divisor / 256 )
end
value = 0
end
end
return table.concat( result )
end
local z85Encoder = "0123456789"..
"abcdefghijklmnopqrstuvwxyz"..
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"..
".-:+=^!/*?&<>()[]{}@%$#"
function basexx.to_z85( str )
if ( #str % 4 ) ~= 0 then
return nil, length_error( #str, 4 )
end
local result = {}
local value = 0
for i = 1, #str do
local b = string.byte( str, i )
value = ( value * 256 ) + b
if ( i % 4 ) == 0 then
local divisor = 85 * 85 * 85 * 85
while divisor ~= 0 do
local index = ( math.floor( value / divisor ) % 85 ) + 1
table.insert( result, z85Encoder:sub( index, index ) )
divisor = math.floor( divisor / 85 )
end
value = 0
end
end
return table.concat( result )
end
--------------------------------------------------------------------------------
return basexx
-- Module interface
local ffi = require('ffi')
local basexx = require('basexx')
local C = ffi.C
-- Export module interface
local M = {}
M.layer = {}
local base32 = {}
local base64 = {}
local str = {}
local AF_INET = 2
local AF_INET6 = 10
local INET_ADDRSTRLEN = 16
local INET6_ADDRSTRLEN = 46
ffi.cdef[[
/*
* Data structures
*/
typedef int socklen_t;
struct sockaddr_storage{
unsigned short int ss_family;
unsigned long int __ss_align;
char __ss_padding[128 - (2 *sizeof(unsigned long int))];
};
struct in_addr{
unsigned char s_addr[4];
};
struct in6_addr{
unsigned char s6_addr[16];
};
struct sockaddr_in{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
} __attribute__ ((__packed__));
struct sockaddr_in6{
unsigned short sin6_family;
unsigned short sin6_port;
unsigned int sin6_flowinfo;
struct in6_addr sin6_addr;
unsigned int sin6_scope_id;
};
typedef unsigned short sa_family_t;
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[108];
};
const char *inet_ntop(
int af,
const void *cp,
char *buf,
socklen_t len);
]]
function base32.pad(b32)
local m = #b32 % 8
if m ~= 0 then
b32 = b32 .. string.rep("=", 8 - m)
end
return b32
end
function str.starts(String,Start)
return string.sub(String,1,string.len(Start))==Start
end
-- Handle DoT signalling NS domains.
function M.layer.consume(state, _, pkt)
if state == kres.FAIL then return state end
-- Only successful answers
pkt = kres.pkt_t(pkt)
-- log("%s", pkt:tostring())
local authority = pkt:section(kres.section.AUTHORITY)
local additional = pkt:section(kres.section.ADDITIONAL)
for _, rr in ipairs(authority) do
--log("%d %s", rr.type, kres.dname2str(rr.rdata))
if rr.type == kres.type.NS then
local name = kres.dname2str(rr.rdata):upper()
-- log("NS %d", name:len())
if name:len() > 56 and str.starts(name, "DOT-") then
local k = basexx.to_base64(
basexx.from_base32(
base32.pad(string.sub(name, 5, string.find(name, '[.]') - 1))
)
)
for _, rr_add in ipairs(additional) do
if rr_add.type == kres.type.A or rr_add.type == kres.type.AAAA then
local name_add = kres.dname2str(rr_add.owner):upper()
if name == name_add then
local addrbuf
if rr_add.type == kres.type.A then
ns_addr = ffi.new("struct sockaddr_in")
ns_addr.sin_family = AF_INET
ns_addr.sin_addr.s_addr = rr_add.rdata
addrbuf = ffi.new("char[?]", INET_ADDRSTRLEN)
C.inet_ntop(AF_INET, ns_addr.sin_addr, addrbuf, INET_ADDRSTRLEN)
else
ns_addr = ffi.new("struct sockaddr_in6")
ns_addr.sin6_family = AF_INET6
ns_addr.sin6_addr.s6_addr = rr_add.rdata
addrbuf = ffi.new("char[?]", INET6_ADDRSTRLEN)
C.inet_ntop(AF_INET6, ns_addr.sin6_addr, addrbuf, INET6_ADDRSTRLEN)
end
net.tls_client(ffi.string(addrbuf).."@853", {k})
log("Adding %s IP %s %s", name_add, ffi.string(addrbuf).."@853", k)
end
end
end
end
end
end
return state
end
return M
dot_SOURCES := dot.lua
$(call make_lua_module,dot)
......@@ -17,6 +17,7 @@ modules_TARGETS += bogus_log \
nsid \
etcd \
ta_sentinel \
dot \
graphite \
policy \
view \
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment