Query policies¶
This module can block, rewrite, or alter inbound queries based on user-defined policies. It does not affect queries generated by the resolver itself, e.g. when following CNAME chains etc.
Each policy rule has two parts: a filter and an action. A filter selects which queries will be affected by the policy, and action which modifies queries matching the associated filter.
Typically a rule is defined as follows: filter(action(action parameters), filter parameters)
. For example, a filter can be suffix
which matches queries whose suffix part is in specified set, and one of possible actions is policy.DENY
, which denies resolution. These are combined together into policy.suffix(policy.DENY, {todname('badguy.example.')})
. The rule is effective when it is added into rule table using policy.add()
, please see examples below.
This module is enabled by default because it implements mandatory RFC 6761 logic. When no rule applies to a query, built-in rules for special-use and locally-served domain names are applied. These rules can be overridden by action policy.PASS
. For debugging purposes you can also add modules.unload('policy')
to your config to unload the module.
Filters¶
A filter selects which queries will be affected by specified Actions. There are several policy filters available in the policy.
table:
- policy.all(action)¶
Always applies the action.
- policy.pattern(action, pattern)¶
Applies the action if query name matches a Lua regular expression.
- policy.suffix(action, suffix_table)¶
Applies the action if query name suffix matches one of suffixes in the table (useful for “is domain in zone” rules).
policy.add(policy.suffix(policy.DENY, policy.todnames({'example.com', 'example.net'})))
Note
For speed this filter requires domain names in DNS wire format, not textual representation, so each label in the name must be prefixed with its length. Always use convenience function policy.todnames()
for automatic conversion from strings! For example:
Note
Non-ASCII is not supported.
Knot Resolver does not provide any convenience support for IDN. Therefore everywhere (all configuration, logs, RPZ files) you need to deal with the xn-- forms of domain name labels, instead of directly using unicode characters.
- policy.domains(action, domain_table)¶
Like
policy.suffix()
match, but the queried name must match exactly, not just its suffix.
- policy.suffix_common(action, suffix_table[, common_suffix])¶
- Parameters:
action – action if the pattern matches query name
suffix_table – table of valid suffixes
common_suffix – common suffix of entries in suffix_table
Like
policy.suffix()
match, but you can also provide a common suffix of all matches for faster processing (nil otherwise). This function is faster for small suffix tables (in the order of “hundreds”).
It is also possible to define custom filter function with any name.
- policy.custom_filter(state, query)¶
- Parameters:
state – Request processing state
kr_layer_state
, typically not used by filter function.query – Incoming DNS query as
kr_query
structure.
- Returns:
An action function or
nil
if filter did not match.
Typically filter function is generated by another function, which allows easy parametrization - this technique is called closure. An practical example of such filter generator is:
function match_query_type(action, target_qtype)
return function (state, query)
if query.stype == target_qtype then
-- filter matched the query, return action function
return action
else
-- filter did not match, continue with next filter
return nil
end
end
end
This custom filter can be used as any other built-in filter. For example this applies our custom filter and executes action policy.DENY
on all queries of type HINFO:
-- custom filter which matches HINFO queries, action is policy.DENY
policy.add(match_query_type(policy.DENY, kres.type.HINFO))
Actions¶
An action is a function which modifies DNS request, and is either of type chain or non-chain:
Non-chain actions modify state of the request and stop rule processing. An example of such action is Forwarding.
Chain actions modify state of the request and allow other rules to evaluate and act on the same request. One such example is
policy.MIRROR()
.
Non-chain actions¶
Following actions stop the policy matching on the query, i.e. other rules are not evaluated once rule with following actions matches:
- policy.PASS¶
Let the query pass through; it’s useful to make exceptions before wider rules. For example:
More specific whitelist rule must precede generic blacklist rule:
-- Whitelist 'good.example.com' policy.add(policy.pattern(policy.PASS, todname('good.example.com.'))) -- Block all names below example.com policy.add(policy.suffix(policy.DENY, {todname('example.com.')}))
- policy.DENY¶
Deny existence of names matching filter, i.e. reply NXDOMAIN authoritatively.
- policy.DENY_MSG(message[, extended_error=kres.extended_error.BLOCKED])¶
Deny existence of a given domain and add explanatory message. NXDOMAIN reply contains an additional explanatory message as TXT record in the additional section.
You may override the extended DNS error to provide the user with more information. By default,
BLOCKED
is returned to indicate the domain is blocked due to the internal policy of the operator. Other suitable error codes areCENSORED
(for externally imposed policy reasons) orFILTERED
(for blocking requested by the client). For more information, please refer to RFC 8914.
- policy.DROP¶
Terminate query resolution and return SERVFAIL to the requestor.
- policy.REFUSE¶
Terminate query resolution and return REFUSED to the requestor.
- policy.NO_ANSWER¶
Terminate query resolution and do not return any answer to the requestor.
Warning
During normal operation, an answer should always be returned. Deliberate query drops are indistinguishable from packet loss and may cause problems as described in RFC 8906. Only use
NO_ANSWER
on very specific occasions, e.g. as a defense mechanism during an attack, and prefer other actions (e.g.DROP
orREFUSE
) for normal operation.
- policy.TC¶
Force requestor to use TCP. It sets truncated bit (TC) in response to true if the request came through UDP, which will force standard-compliant clients to retry the request over TCP.
- policy.REROUTE({subnet = target, ...})¶
Reroute IP addresses in response matching given subnet to given target, e.g.
{['192.0.2.0/24'] = '127.0.0.0'}
will rewrite ‘192.0.2.55’ to ‘127.0.0.55’, see renumber module for more information. Seepolicy.add()
and do not forget to specify that this is postrule. Quick example:-- this policy is enforced on answers -- therefore we have to use 'postrule' -- (the "true" at the end of policy.add) policy.add(policy.all(policy.REROUTE({['192.0.2.0/24'] = '127.0.0.0'})), true)
- policy.ANSWER({ type = { rdata=data, [ttl=1] } }, [nodata=false])¶
Overwrite Resource Records in responses with specified values.
type - RR type to be replaced, e.g.
[kres.type.A]
or numeric value.rdata - RR data in DNS wire format, i.e. binary form specific for given RR type. Set of multiple RRs can be specified as table
{ rdata1, rdata2, ... }
. Use helper functionkres.str2ip()
to generate wire format for A and AAAA records. Wire format for other record types can be generated withkres.parse_rdata()
.ttl - TTL in seconds. Default: 1 second.
nodata - If type requested by client is not configured in this policy:
true
: Return empty answer (NODATA).false
: Ignore this policy and continue processing other rules.
Default:
false
.
-- policy to change IPv4 address and TTL for example.com policy.add( policy.domains( policy.ANSWER( { [kres.type.A] = { rdata=kres.str2ip('192.0.2.7'), ttl=300 } } ), { todname('example.com') })) -- policy to generate two TXT records (specified in binary format) for example.net policy.add( policy.domains( policy.ANSWER( { [kres.type.TXT] = { rdata={'\005first', '\006second'}, ttl=5 } } ), { todname('example.net') }))
- kres.parse_rdata({str, ...})¶
Parse string representation of RTYPE and RDATA into RDATA wire format. Expects a table of string(s) and returns a table of wire data.
-- create wire format RDATA that can be passed to policy.ANSWER kres.parse_rdata({'SVCB 1 resolver.example. alpn=dot'}) kres.parse_rdata({ 'SVCB 1 resolver.example. alpn=dot ipv4hint=192.0.2.1 ipv6hint=2001:db8::1', 'SVCB 2 resolver.example. mandatory=key65380 alpn=h2 key65380=/dns-query{?dns}', })
More complex non-chain actions are described in their own chapters, namely:
Chain actions¶
Following actions act on request and then processing continue until first non-chain action (specified in the previous section) is triggered:
- policy.MIRROR(ip_address)¶
Send copy of incoming DNS queries to a given IP address using DNS-over-UDP and continue resolving them as usual. This is useful for sanity testing new versions of DNS resolvers.
policy.add(policy.all(policy.MIRROR('127.0.0.2')))
Actions for extra logging¶
These are also “chain” actions, i.e. they don’t stop processing the policy rule list. Similarly to other actions, they apply during whole processing of the client’s request, i.e. including any sub-queries.
The log lines from these policy actions are tagged by extra [reqdbg]
prefix, and they are produced regardless of your log_level()
setting. They are marked as debug
level, so e.g. with journalctl command you can use -p info
to skip them.
Warning
Beware of producing too much logs.
These actions are not suitable for use on a large fraction of resolver’s requests. The extra logs have significant performance impact and might also overload your logging system (or get rate-limited by it). You can use Filters to further limit on which requests this happens.
- policy.DEBUG_ALWAYS¶
Print debug-level logging for this request. That also includes messages from client (
REQTRACE
), upstream servers (QTRACE
), and stats about interesting records at the end.-- debug requests that ask for flaky.example.net or below policy.add(policy.suffix(policy.DEBUG_ALWAYS, policy.todnames({'flaky.example.net'})))
- policy.DEBUG_CACHE_MISS¶
Same as
DEBUG_ALWAYS
but only if the request required information which was not available locally, i.e. requests which forced resolver to ask upstream server(s). Intended usage is for debugging problems with remote servers.
- policy.DEBUG_IF(test_function)¶
- Parameters:
test_function – Function with single argument of type
kr_request
which returnstrue
if debug logs for that request should be generated andfalse
otherwise.
Same as
DEBUG_ALWAYS
but only logs if the test_function says so.Note
test_function
is evaluated only when request is finished. As a result all debug logs this request must be collected, and at the end they get either printed or thrown away.Example usage which gathers verbose logs for all requests in subtree
dnssec-failed.org.
and prints debug logs for those finishing in a different state thankres.DONE
(most importantlykres.FAIL
, seekr_layer_state
).policy.add(policy.suffix( policy.DEBUG_IF(function(req) return (req.state ~= kres.DONE) end), policy.todnames({'dnssec-failed.org.'})))
- policy.QTRACE¶
Pretty-print DNS responses from upstream servers (or cache) into logs. It’s useful for debugging weird DNS servers.
If you do not use
QTRACE
in combination withDEBUG*
, you additionally need eitherlog_groups({'iterat'})
(possibly with other groups) orlog_level('debug')
to see the output in logs.
- policy.REQTRACE¶
Pretty-print DNS requests from clients into the verbose log. It’s useful for debugging weird DNS clients. It makes most sense together with Views and ACLs (enabling per-client) and probably with verbose logging those request (e.g. use
DEBUG_ALWAYS
instead).
- policy.IPTRACE¶
Log how the request arrived. Most notably, this includes the client’s IP address, so beware of privacy implications.
-- example usage in configuration policy.add(policy.all(policy.IPTRACE)) -- you might want to combine it with some other logs, e.g. policy.add(policy.all(policy.DEBUG_ALWAYS))
-- example log lines from IPTRACE: [reqdbg][policy][57517.00] request packet arrived from ::1#37931 to ::1#00853 (TCP + TLS) [reqdbg][policy][65538.00] request packet arrived internally
Custom actions¶
- policy.custom_action(state, request)¶
- Parameters:
state – Request processing state
kr_layer_state
.request – Current DNS request as
kr_request
structure.
- Returns:
Returning a new
kr_layer_state
prevents evaluating other policy rules. Returningnil
creates a chain action and allows to continue evaluating other rules.
This is real example of an action function:
-- Custom action which generates fake A record
local ffi = require('ffi')
local function fake_A_record(state, req)
local answer = req:ensure_answer()
if answer == nil then return nil end
local qry = req:current()
if qry.stype ~= kres.type.A then
return state
end
ffi.C.kr_pkt_make_auth_header(answer)
answer:rcode(kres.rcode.NOERROR)
answer:begin(kres.section.ANSWER)
answer:put(qry.sname, 900, answer:qclass(), kres.type.A, '\192\168\1\3')
return kres.DONE
end
This custom action can be used as any other built-in action. For example this applies our fake A record action and executes it on all queries in subtree example.net
:
policy.add(policy.suffix(fake_A_record, policy.todnames({'example.net'})))
The action function can implement arbitrary logic so it is possible to implement complex heuristics, e.g. to deflect Slow drip DNS attacks or gray-list resolution of misbehaving zones.
Warning
The policy module currently only looks at whole DNS requests. The rules won’t be re-applied e.g. when following CNAMEs.
Forwarding¶
Forwarding action alters behavior for cache-miss events. If an information is missing in the local cache the resolver will forward the query to another DNS resolver for resolution (instead of contacting authoritative servers directly). DNS answers from the remote resolver are then processed locally and sent back to the original client.
Actions policy.FORWARD()
, policy.TLS_FORWARD()
and policy.STUB()
accept up to four IP addresses at once and the resolver will automatically select IP address which statistically responds the fastest.
- policy.FORWARD(ip_address)¶
- policy.FORWARD({ ip_address, [ip_address, ...] })
Forward cache-miss queries to specified IP addresses (without encryption), DNSSEC validate received answers and cache them. Target IP addresses are expected to be DNS resolvers.
-- Forward all queries to public resolvers https://www.nic.cz/odvr policy.add(policy.all( policy.FORWARD( {'2001:148f:fffe::1', '2001:148f:ffff::1', '185.43.135.1', '193.14.47.1'})))
A variant which uses encrypted DNS-over-TLS transport is called
policy.TLS_FORWARD()
, please see section Forwarding over TLS protocol (DNS-over-TLS).
- policy.STUB(ip_address)¶
- policy.STUB({ ip_address, [ip_address, ...] })
Similar to
policy.FORWARD()
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.This mode does not support encryption and should be used only for Replacing part of the DNS tree. Use
policy.FORWARD()
mode if possible.-- Answers for reverse queries about the 192.168.1.0/24 subnet -- are to be obtained from IP address 192.0.2.1 port 5353 -- This disables DNSSEC validation! policy.add(policy.suffix( policy.STUB('192.0.2.1@5353'), {todname('1.168.192.in-addr.arpa')}))
Note
By default, forwarding targets must support EDNS and 0x20 randomization. See example in Replacing part of the DNS tree.
Warning
Limiting forwarding actions by filters (e.g. policy.suffix()
) may have unexpected consequences. Notably, forwarders can inject any records into your cache even if you “restrict” them to an insignificant DNS subtree – except in cases where DNSSEC validation applies, of course.
The behavior is probably best understood through the fact that filters and actions are completely decoupled. The forwarding actions have no clue about why they were executed, e.g. that the user wanted to restrict the forwarder only to some subtree. The action just selects some set of forwarders to process this whole request from the client, and during that processing it might need some other “sub-queries” (e.g. for validation). Some of those might not’ve passed the intended filter, but policy rule-set only applies once per client’s request.
Forwarding over TLS protocol (DNS-over-TLS)¶
- policy.TLS_FORWARD({ {ip_address, authentication}, [...] } )¶
Same as
policy.FORWARD()
but send query over DNS-over-TLS protocol (encrypted). Each target IP address needs explicit configuration how to validate TLS certificate so each IP address is configured by pair:{ip_address, authentication}
. See sections below for more details.
Policy 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 policy.TLS_FORWARD()
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 configure Knot Resolver as DNS-over-TLS server, or pick some public DNS-over-TLS server. Please see DNS Privacy Project homepage for list of public servers.
Note
Some public DNS-over-TLS providers may apply rate-limiting which makes their service incompatible with Knot Resolver’s TLS forwarding. Notably, Google Public DNS doesn’t work as of 2019-07-10.
When multiple servers are specified, the one with the lowest round-trip time is used.
CA+hostname authentication¶
Traditional PKI authentication requires server to present certificate with specified hostname, which is issued by one of trusted CAs. Example policy is:
policy.TLS_FORWARD({
{'2001:DB8::d0c', hostname='res.example.com'}})
hostname
must be a valid domain name matching server’s certificate. It will also be sent to the server as SNI.ca_file
optionally contains a path to a CA certificate (or certificate bundle) in PEM format. If you omit that, the system CA certificate store will be used instead (usually sufficient). A list of paths is also accepted, but all of them must be valid PEMs.
Key-pinned authentication¶
Instead of CAs, you can specify hashes of accepted certificates in pin_sha256
. They are in the usual format – base64 from sha256. You may still specify hostname
if you want SNI to be sent.
TLS Examples¶
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 authenticated using hostname and system-wide CA certificates
policy.TLS_FORWARD({{'192.0.2.1', hostname='res.example.com'}})
-- 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.com', ca_file='/etc/knot-resolver/tlsca.crt'}
})
Forwarding to multiple targets¶
With the use of policy.slice()
function, it is possible to split the entire DNS namespace into distinct slices. When used in conjunction with policy.TLS_FORWARD()
, it’s possible to forward different queries to different targets.
- policy.slice(slice_func, action[, action[, ...])¶
- Parameters:
slice_func – slicing function that returns index based on query
action – action to be performed for the slice
This function splits the entire domain space into multiple slices (determined by the number of provided
actions
). Aslice_func
is called to determine which slice a query belongs to. The correspondingaction
is then executed.
- policy.slice_randomize_psl(seed=os.time() / 3600 * 24 * 7)¶
- Parameters:
seed – seed for random assignment
The function initializes and returns a slicing function, which deterministically assigns
query
to a slice based on the query name.It utilizes the Public Suffix List to ensure domains under the same registrable domain end up in a single slice. (see example below)
seed
can be used to re-shuffle the slicing algorithm when the slicing function is initialized. By default, the assignment is re-shuffled after one week (when resolver restart / reloads config). To force a stable distribution, pass a fixed value. To re-shuffle on every resolver restart, useos.time()
.The following example demonstrates a distribution among 3 slices:
slice 1/3: example.com a.example.com b.example.com x.b.example.com example3.com slice 2/3: example2.co.uk slice 3/3: example.co.uk a.example.co.uk
These two functions can be used together to forward queries for names in different parts of DNS name space to different target servers:
policy.add(policy.slice(
policy.slice_randomize_psl(),
policy.TLS_FORWARD({{'192.0.2.1', hostname='res.example.com'}}),
policy.TLS_FORWARD({
-- multiple servers can be specified for a single slice
-- the one with lowest round-trip time will be used
{'193.17.47.1', hostname='odvr.nic.cz'},
{'185.43.135.1', hostname='odvr.nic.cz'},
})
))
Note
The privacy implications of using this feature aren’t clear. Since websites often make requests to multiple domains, these might be forwarded to different targets. This could result in decreased privacy (e.g. when the remote targets are both logging or otherwise processing your DNS traffic). The intended use-case is to use this feature with semi-trusted resolvers which claim to do no logging (such as those listed on dnsprivacy.org), to decrease the potential exposure of your DNS data to a malicious resolver operator.
Replacing part of the DNS tree¶
Following procedure applies only to domains which have different content publicly and internally. For example this applies to “your own” top-level domain example.
which does not exist in the public (global) DNS namespace.
Dealing with these internal-only domains requires extra configuration because DNS was designed as “single namespace” and local modifications like adding your own TLD break this assumption.
Warning
Use of internal names which are not delegated from the public DNS is causing technical problems with caching and DNSSEC validation and generally makes DNS operation more costly. We recommend against using these non-delegated names.
To make such internal domain available in your resolver it is necessary to graft your domain onto the public DNS namespace, but grafting creates new issues:
These grafted domains will be rejected by DNSSEC validation because such domains are technically indistinguishable from an spoofing attack against the public DNS. Therefore, if you trust the remote resolver which hosts the internal-only domain, and you trust your link to it, you need to use the policy.STUB()
policy instead of policy.FORWARD()
to disable DNSSEC validation for those grafted domains.
extraTrees = policy.todnames(
{'faketldtest.',
'sld.example.',
'internal.example.com.',
'2.0.192.in-addr.arpa.' -- this applies to reverse DNS tree as well
})
-- Beware: the rule order is important, as policy.STUB is not a chain action.
-- Flags: for "dumb" targets disabling EDNS can help (below) as DNSSEC isn't
-- validated anyway; in some of those cases adding 'NO_0X20' can also help,
-- though it also lowers defenses against off-path attacks on communication
-- between the two servers.
-- With kresd <= 5.5.3 you also needed 'NO_CACHE' flag to avoid unintentional
-- NXDOMAINs that could sometimes happen due to aggressive DNSSEC caching.
policy.add(policy.suffix(policy.FLAGS({'NO_EDNS'}), extraTrees))
policy.add(policy.suffix(policy.STUB({'2001:db8::1'}), extraTrees))
Response policy zones¶
Warning
There is no published Internet Standard for RPZ and implementations vary. At the moment Knot Resolver supports limited subset of RPZ format and deviates from implementation in BIND. Nevertheless it is good enough for blocking large lists of spam or advertising domains.
The RPZ file format is basically a DNS zone file with very special semantics. For example:
; left hand side ; TTL and class ; right hand side ; encodes RPZ trigger ; ignored ; encodes action ; (i.e. filter) blocked.domain.example 600 IN CNAME . ; block main domain *.blocked.domain.example 600 IN CNAME . ; block subdomainsThe only “trigger” supported in Knot Resolver is query name, i.e. left hand side must be a domain name which triggers the action specified on the right hand side.
Subset of possible RPZ actions is supported, namely:
RPZ Right Hand Side
Knot Resolver Action
BIND Compatibility
.
action
is usedcompatible if
action
ispolicy.DENY
*.
yes
rpz-passthru.
yes
rpz-tcp-only.
yes
rpz-drop.
no [1]
fake A/AAAA
yes
fake CNAME
not supported
no
Note
To debug which domains are affected by RPZ (or other policy actions), you can enable the
policy
log group:log_groups({'policy'})See also non-ASCII support note.
- policy.rpz(action, path[, watch = true])¶
- Parameters:
action – the default action for match in the zone; typically you want
policy.DENY
path – path to zone file
watch – boolean, if true, the file will be reloaded on file change
Enforce RPZ rules. This can be used in conjunction with published blocklist feeds. The RPZ operation is well described in this Jan-Piet Mens’s post, or the Pro DNS and BIND book.
For example, we can store the example snippet with domain
blocked.domain.example
(above) into file/etc/knot-resolver/blocklist.rpz
and configure resolver to answer with NXDOMAIN plus the specified additional text to queries for this domain:policy.add( policy.rpz(policy.DENY_MSG('domain blocked by your resolver operator'), '/etc/knot-resolver/blocklist.rpz', true))
Resolver will reload RPZ file at run-time if the RPZ file changes. Recommended RPZ update procedure is to store new blocklist in a new file (newblocklist.rpz) and then rename the new file to the original file name (blocklist.rpz). This avoids problems where resolver might attempt to re-read an incomplete file.
Additional properties¶
Most properties (actions, filters) are described above.
- policy.add(rule, postrule)¶
- Parameters:
rule – added rule, i.e.
policy.pattern(policy.DENY, '[0-9]+\2cz')
postrule – boolean, if true the rule will be evaluated on answer instead of query
- Returns:
rule description
Add a new policy rule that is executed either or queries or answers, depending on the
postrule
parameter. You can then use the returned rule description to get information and unique identifier for the rule, as well as match count.-- mirror all queries, keep handle so we can retrieve information later local rule = policy.add(policy.all(policy.MIRROR('127.0.0.2'))) -- we can print statistics about this rule any time later print(string.format('id: %d, matched queries: %d', rule.id, rule.count)
- policy.del(id)¶
- Parameters:
id – identifier of a given rule returned by
policy.add()
- Returns:
boolean
true
if rule was deleted,false
otherwise
Remove a rule from policy list.
- policy.todnames({name, ...})¶
- Param:
names table of domain names in textual format
Returns table of domain names in wire format converted from strings.
-- Convert single name assert(todname('example.com') == '\7example\3com\0') -- Convert table of names policy.todnames({'example.com', 'me.cz'}) { '\7example\3com\0', '\2me\2cz\0' }