]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
modules/daf: trivial rule compiler implemented
authorMarek Vavrusa <marek@vavrusa.com>
Wed, 8 Jun 2016 07:26:13 +0000 (00:26 -0700)
committerMarek Vavrusa <marek@vavrusa.com>
Wed, 6 Jul 2016 06:33:38 +0000 (23:33 -0700)
the fw can now parse simple rules such as:
'qname = *.example.com AND src = 127.0.0.1/8 deny'

and turn it into filter actions.

this is a building block for custom firewall rules
based on query/answer contents that leverage
existing policy/view modules, but turn those into
easier to write (and eventually persistent) rule
sets

modules/daf/daf.lua
modules/view/view.lua

index 1c8a7d1406c0f7468f07846330118524be19ef94..976bf6c9812c70a53e1280f40600cf2a0c8d21fc 100644 (file)
@@ -1,7 +1,86 @@
 local cqueues = require('cqueues')
 
+-- Load dependent modules
+if not view then modules.load('view') end
+if not policy then modules.load('policy') end
+
+-- Actions
+local actions = {
+       pass = 1, deny = 2, drop = 3, tc = 4, forward = policy.FORWARD,
+}
+
+-- Filter rules per column
+local filters = {
+       -- 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)
+               elseif op == '=' then return policy.suffix(true, {val})
+               else error(string.format('invalid operator "%s" on qname', op)) end
+       end,
+       -- Filter on source address
+       src = function (g)
+               local op = g()
+               if op ~= '=' then error('source address supports only "=" operator') end
+               return view.rule(true, g())
+       end
+}
+
+local function parse_filter(tok, g)
+       local filter = filters[tok]
+       if not filter then error(string.format('invalid filter "%s"', tok)) end
+       return filter(g)
+end
+
+local function parse_rule(g)
+       local f = parse_filter(g(), g)
+       -- Compose filter functions on conjunctions
+       -- or terminate filter chain and return
+       local tok = g()
+       while tok do
+               if tok == 'AND' then
+                       local fnext = parse_filter(g(), g)
+                       f = function (req, qry) return f(req, qry) and fnext(req, qry) end
+               elseif tok == 'OR' then
+                       local fnext = parse_filter(g(), g)
+                       f = function (req, qry) return f(req, qry) or fnext(req, qry) end
+               else
+                       break
+               end
+               tok = g()
+               print('next token is', tok)
+       end
+       return tok, f
+end
+
+local function parse_query(g)
+       local ok, action, filter = pcall(parse_rule, g)
+       if not ok then return nil, action end
+       if not actions[action] then return nil, string.format('invalid action "%s"', action) end
+       -- Parse and interpret action
+       action = actions[action]
+       if type(action) == 'function' then
+               action = action(g())
+       end
+       return action, filter
+end
+
+-- Compile a rule described by query language
+-- The query language is modelled by iptables/nftables
+-- conj = AND | OR
+-- op = IS | NOT | LIKE | IN
+-- filter = <key> <op> <expr>
+-- rule = <filter> | <filter> <conj> <rule>
+-- action = PASS | DENY | DROP | TC | FORWARD
+-- query = <rule> <action>
+local function compile(query)
+       local g = string.gmatch(query, '%S+')
+       return parse_query(g)
+end
+
 -- Module declaration
 local M = {
+       rules = {}
 }
 
 -- @function Public-facing API
@@ -39,6 +118,16 @@ function M.config(conf)
        http.snippets['/daf'] = {'Application Firewall', [[
                <p>Hello world!</p>
        ]]}
+       M.rule('qname = *.example.com AND src = 127.0.0.1/8 deny')
+       -- M.rule('answer ~ (%w+).facebook.com AND src = 127.0.0.1/8 forward 8.8.8.8')
+end
+
+-- @function Add rule
+function M.rule(rule)
+       local action, filter = compile(rule)
+       if not action then error(filter) end
+       table.insert(M.rules, {rule, action, filter})
+       print(action, filter, rule)
 end
 
 return M
\ No newline at end of file
index 2550112d0db77f0df5018fe15622450afc9f7879..1e9c8f7cf202cb0a74e751d7c302424bd0eb64f8 100644 (file)
@@ -19,7 +19,9 @@ function view.addr(view, subnet, policy)
        local subnet_cd = ffi.new('char[16]')
        local family = C.kr_straddr_family(subnet)
        local bitlen = C.kr_straddr_subnet(subnet_cd, subnet)
-       table.insert(view.subnet, {family, subnet_cd, bitlen, policy})
+       local t = {family, subnet_cd, bitlen, policy}
+       table.insert(view.subnet, t)
+       return t
 end
 
 -- @function Match IP against given subnet
@@ -29,7 +31,6 @@ end
 
 -- @function Find view for given request
 local function evaluate(view, req)
-       local answer = req.answer
        local client_key = req.qsource.key
        local match_cb = (client_key ~= nil) and view.key[client_key:owner()] or nil
        -- Search subnets otherwise
@@ -45,6 +46,19 @@ local function evaluate(view, req)
        return match_cb
 end
 
+-- @function Return view policy rule
+function view.rule(action, subnet)
+       local subnet_cd = ffi.new('char[16]')
+       local family = C.kr_straddr_family(subnet)
+       local bitlen = C.kr_straddr_subnet(subnet_cd, subnet)
+       return function(req, _)
+               local src_addr = req.qsource.addr
+               if src_addr ~= nil and match_subnet(family, subnet_cd, bitlen, src_addr) then
+                       return action
+               end
+       end
+end
+
 -- @function Module layers
 view.layer = {
        begin = function(state, req)