-- Filter rules per column
M.filters = {
-- Filter on QTYPE
- qtype = function (g)
- local op, val = g(), g()
- local qtype = kres.type[val]
- if not qtype then
- error(string.format('invalid query type "%s"', val))
+ qtype = function (op, arg)
+ for i, v in ipairs(arg) do
+ arg[i] = kres.type[v]
+ if not arg[i] then
+ panic('invalid query type "%s"', v)
+ end
end
- if op == '=' then return policy.query_type(true, {qtype})
- else error(string.format('invalid operator "%s" on qtype', op)) end
+ if op == '=' then return policy.query_type(true, arg)
+ else panic('invalid operator "%s" on qtype', op) end
end,
-- Filter on QNAME (either pattern or suffix match)
- qname = function (g)
- local op, val = g(), todname(g())
- if op == '~' then return policy.pattern(true, val:sub(2)) -- Skip leading label length
- elseif op == '=' then return policy.suffix(true, {val})
- else error(string.format('invalid operator "%s" on qname', op)) end
+ qname = function (op, arg)
+ if op == '~' then
+ local name = todname(arg[1])
+ if name == nil or #arg ~= 1 then
+ error('operator "~"" on qname must have exactly one domain name as an argument')
+ end
+ return policy.pattern(true, name:sub(2)) -- Skip leading label length
+ elseif op == '=' then
+ return policy.suffix(true, policy.todnames(arg))
+ else
+ panic('invalid operator "%s" on qname', op)
+ end
end,
-- Filter on NS
- ns = function (g)
- local op, val = g(), todname(g())
- if op == '=' then return policy.ns_suffix(true, {val})
- else error(string.format('invalid operator "%s" on ns', op)) end
+ ns = function (op, arg)
+ if op == '=' then return policy.ns_suffix(true, policy.todnames(arg))
+ else panic('invalid operator "%s" on ns', op) end
end,
-- Filter on source address
- src = function (g)
- local op = g()
- if op ~= '=' then error('address supports only "=" operator') end
- return view.rule_src(true, g())
+ src = function (op, arg)
+ if op ~= '=' or #arg ~= 1 then error('address supports only "=" operator with single argument') end
+ return view.rule_src(true, arg[1])
end,
-- Filter on destination address
- dst = function (g)
- local op = g()
- if op ~= '=' then error('address supports only "=" operator') end
- return view.rule_dst(true, g())
+ dst = function (op, arg)
+ if op ~= '=' or #arg ~= 1 then error('address supports only "=" operator with single argument') end
+ return view.rule_dst(true, arg[1])
end,
}
+-- Allowed operators
+local operators = {
+ ['='] = '=',
+ ['~'] = '~',
+}
+
local function parse_filter(tok, g, prev)
- if not tok then error(string.format('expected filter after "%s"', prev)) end
+ if not tok then panic('expected filter after "%s"', prev) end
local filter = M.filters[tok:lower()]
- if not filter then error(string.format('invalid filter "%s"', tok)) end
- return filter(g)
+ if not filter then panic('invalid filter "%s"', tok) end
+ -- Parse operator (if not exists, defaults to equality like nftables)
+ -- e.g. qname = example.com
+ -- qname example.com
+ local op = g()
+ local arg
+ if not operators[op] then
+ arg = op
+ op = '='
+ else
+ arg = g()
+ end
+ if not arg then
+ panic('expected argument after filter "%s %s"', tok, op)
+ end
+ -- Parse argument table
+ -- e.g. src {192.168.1.0 127.0.0.1}
+ local res = {}
+ if arg:find('^[{]') then
+ while arg do
+ table.insert(res, arg:match('[^{}%s]+'))
+ if arg:find('[}]$') then
+ break
+ end
+ arg = g()
+ end
+ else
+ table.insert(res, arg)
+ end
+ return filter(op, res)
end
local function parse_rule(g)
local function parse_query(g)
local ok, actid, filter = pcall(parse_rule, g)
- if not ok then return nil, actid end
- actid = actid:lower()
+ if not ok then
+ return nil, actid
+ end
+ if actid then
+ actid = actid:lower()
+ end
if not M.actions[actid] then
return nil, string.format('invalid action "%s"', actid)
end
-- Compile a rule described by query language
-- The query language is modelled by iptables/nftables
-- conj = AND | OR
--- op = IS | NOT | LIKE | IN
+-- op = = | ~
-- filter = <key> <op> <expr>
-- rule = <filter> | <filter> <conj> <rule>
-- action = PASS | DENY | DROP | TC | FORWARD
return {info=r.info, id=r.rule.id, active=(r.rule.suspended ~= true), count=r.rule.count}
end
--- @function Remove a rule
+-- @function Parse and compile a rule
+M.compile = compile
-- @function Cleanup module
function M.deinit()
same(rcode, kres.rcode.NXDOMAIN, '0..0.ip6.arpa. returns NXDOMAIN')
end
+local function get_filter(rule)
+ local _, _, filter = daf.compile(rule)
+ return filter or function () return true end
+end
+
+-- test rules parser
+local function test_parser()
+ local a_query = {stype = kres.type.A}
+ local aaaa_query = {stype = kres.type.AAAA}
+ local txt_query = {stype = kres.type.TXT}
+
+ -- invalid rules
+ nok(daf.compile('qname'), 'rejects "qname"')
+ nok(daf.compile('qname '), 'rejects "qname "')
+ nok(daf.compile('qname {'), 'rejects "qname {"')
+ nok(daf.compile('qname {A'), 'rejects "qname {A"')
+ nok(daf.compile('qname A}'), 'rejects "qname A}"')
+ nok(daf.compile('qname @ {A AAAA} deny'), 'rejects "qname @ {A AAAA} deny"')
+ nok(daf.compile('qname ~ {A AAAA} deny'), 'rejects "qname ~ {A AAAA} deny"')
+ nok(daf.compile('qname and'), 'rejects "qname and"')
+ nok(daf.compile('qname A or'), 'rejects "qname A or"')
+
+ local filters = {
+ -- test catch all
+ ['deny'] = {true, true, true},
+ -- test explicit operator '='
+ ['qtype = A deny'] = {true, nil, nil},
+ -- test implicit operator '='
+ ['qtype A deny'] = {true, nil, nil},
+ -- test multiple arguments
+ ['qtype { A TXT } deny'] = {true, true, nil},
+ ['qtype {A TXT } deny'] = {true, true, nil},
+ ['qtype {A TXT} deny'] = {true, true, nil},
+ }
+
+ for filter, e in pairs(filters) do
+ local match = get_filter(filter)
+ same(e[1], match(nil, a_query), 'matches ' .. filter .. ' (A query)')
+ same(e[2], match(nil, txt_query), 'matches ' .. filter .. ' (TXT query)')
+ same(e[3], match(nil, aaaa_query), 'matches ' .. filter .. ' (AAAA query)')
+ end
+end
+
-- test filters running in begin phase
local function test_actions()
local filters = {
-- plan tests
local tests = {
test_builtin_rules,
+ test_parser,
test_actions,
test_features,
}