LCOV - code coverage report
Current view: top level - modules/daf - daf.lua Hit Total Coverage
Test: Knot Resolver 3.2.1-POSIX coverage report Lines: 0 174 0.0 %
Date: 2019-03-12 03:31:59
Legend: Lines: hit not hit

          Line data    Source code
       1             : -- Load dependent modules
       2           0 : if not view then modules.load('view') end
       3           0 : if not policy then modules.load('policy') end
       4             : 
       5             : -- Actions
       6           0 : local actions = {
       7             :         pass = 1, deny = 2, drop = 3, tc = 4, truncate = 4,
       8             :         forward = function (g)
       9           0 :                 local addrs = {}
      10           0 :                 local tok = g()
      11           0 :                 for addr in string.gmatch(tok, '[^,]+') do
      12           0 :                         table.insert(addrs, addr)
      13             :                 end
      14           0 :                 return policy.FORWARD(addrs)
      15             :         end,
      16             :         mirror = function (g)
      17           0 :                 return policy.MIRROR(g())
      18             :         end,
      19             :         reroute = function (g)
      20           0 :                 local rules = {}
      21           0 :                 local tok = g()
      22           0 :                 while tok do
      23           0 :                         local from, to = tok:match '([^-]+)-(%S+)'
      24           0 :                         rules[from] = to
      25           0 :                         tok = g()
      26             :                 end
      27           0 :                 return policy.REROUTE(rules)
      28             :         end,
      29             :         rewrite = function (g)
      30           0 :                 local rules = {}
      31           0 :                 local tok = g()
      32           0 :                 while tok do
      33             :                         -- This is currently limited to A/AAAA rewriting
      34             :                         -- in fixed format '<owner> <type> <addr>'
      35           0 :                         local _, to = g(), g()
      36           0 :                         rules[tok] = to
      37           0 :                         tok = g()
      38             :                 end
      39           0 :                 return policy.REROUTE(rules, true)
      40             :         end,
      41             : }
      42             : 
      43             : -- Filter rules per column
      44           0 : local filters = {
      45             :         -- Filter on QNAME (either pattern or suffix match)
      46             :         qname = function (g)
      47           0 :                 local op, val = g(), todname(g())
      48           0 :                 if     op == '~' then return policy.pattern(true, val:sub(2)) -- Skip leading label length
      49           0 :                 elseif op == '=' then return policy.suffix(true, {val})
      50           0 :                 else error(string.format('invalid operator "%s" on qname', op)) end
      51             :         end,
      52             :         -- Filter on source address
      53             :         src = function (g)
      54           0 :                 local op = g()
      55           0 :                 if op ~= '=' then error('address supports only "=" operator') end
      56           0 :                 return view.rule_src(true, g())
      57             :         end,
      58             :         -- Filter on destination address
      59             :         dst = function (g)
      60           0 :                 local op = g()
      61           0 :                 if op ~= '=' then error('address supports only "=" operator') end
      62           0 :                 return view.rule_dst(true, g())
      63             :         end,
      64             : }
      65             : 
      66             : local function parse_filter(tok, g, prev)
      67           0 :         if not tok then error(string.format('expected filter after "%s"', prev)) end
      68           0 :         local filter = filters[tok:lower()]
      69           0 :         if not filter then error(string.format('invalid filter "%s"', tok)) end
      70           0 :         return filter(g)
      71             : end
      72             : 
      73             : local function parse_rule(g)
      74             :         -- Allow action without filter
      75           0 :         local tok = g()
      76           0 :         if not filters[tok:lower()] then
      77           0 :                 return tok, nil
      78             :         end
      79           0 :         local f = parse_filter(tok, g)
      80             :         -- Compose filter functions on conjunctions
      81             :         -- or terminate filter chain and return
      82           0 :         tok = g()
      83           0 :         while tok do
      84           0 :                 if tok:lower() == 'and' then
      85           0 :                         local fa, fb = f, parse_filter(g(), g, tok)
      86           0 :                         f = function (req, qry) return fa(req, qry) and fb(req, qry) end
      87           0 :                 elseif tok:lower() == 'or' then
      88           0 :                         local fa, fb = f, parse_filter(g(), g, tok)
      89           0 :                         f = function (req, qry) return fa(req, qry) or fb(req, qry) end
      90             :                 else
      91             :                         break
      92             :                 end
      93           0 :                 tok = g()
      94             :         end
      95           0 :         return tok, f
      96             : end
      97             : 
      98             : local function parse_query(g)
      99           0 :         local ok, actid, filter = pcall(parse_rule, g)
     100           0 :         if not ok then return nil, actid end
     101           0 :         actid = actid:lower()
     102           0 :         if not actions[actid] then return nil, string.format('invalid action "%s"', actid) end
     103             :         -- Parse and interpret action
     104           0 :         local action = actions[actid]
     105           0 :         if type(action) == 'function' then
     106           0 :                 action = action(g)
     107             :         end
     108           0 :         return actid, action, filter
     109             : end
     110             : 
     111             : -- Compile a rule described by query language
     112             : -- The query language is modelled by iptables/nftables
     113             : -- conj = AND | OR
     114             : -- op = IS | NOT | LIKE | IN
     115             : -- filter = <key> <op> <expr>
     116             : -- rule = <filter> | <filter> <conj> <rule>
     117             : -- action = PASS | DENY | DROP | TC | FORWARD
     118             : -- query = <rule> <action>
     119             : local function compile(query)
     120           0 :         local g = string.gmatch(query, '%S+')
     121           0 :         return parse_query(g)
     122             : end
     123             : 
     124             : -- @function Describe given rule for presentation
     125             : local function rule_info(r)
     126           0 :         return {info=r.info, id=r.rule.id, active=(r.rule.suspended ~= true), count=r.rule.count}
     127             : end
     128             : 
     129             : -- Module declaration
     130           0 : local M = {
     131           0 :         rules = {}
     132             : }
     133             : 
     134             : -- @function Remove a rule
     135             : 
     136             : -- @function Cleanup module
     137           0 : function M.deinit()
     138           0 :         if http and http.endpoints then
     139           0 :                 http.endpoints['/daf'] = nil
     140           0 :                 http.endpoints['/daf.js'] = nil
     141           0 :                 http.snippets['/daf'] = nil
     142             :         end
     143             : end
     144             : 
     145             : -- @function Add rule
     146           0 : function M.add(rule)
     147             :         -- Ignore duplicates
     148           0 :         for _, r in ipairs(M.rules) do
     149           0 :                 if r.info == rule then return r end
     150             :         end
     151           0 :         local id, action, filter = compile(rule)
     152           0 :         if not id then error(action) end
     153             :         -- Combine filter and action into policy
     154             :         local p
     155           0 :         if filter then
     156             :                 p = function (req, qry)
     157           0 :                         return filter(req, qry) and action
     158             :                 end
     159             :         else
     160             :                 p = function ()
     161           0 :                         return action
     162             :                 end
     163             :         end
     164           0 :         local desc = {info=rule, policy=p}
     165             :         -- Enforce in policy module, special actions are postrules
     166           0 :         if id == 'reroute' or id == 'rewrite' then
     167           0 :                 desc.rule = policy.add(p, true)
     168             :         else
     169           0 :                 desc.rule = policy.add(p)
     170             :         end
     171           0 :         table.insert(M.rules, desc)
     172           0 :         return desc
     173             : end
     174             : 
     175             : -- @function Remove a rule
     176           0 : function M.del(id)
     177           0 :         for _, r in ipairs(M.rules) do
     178           0 :                 if r.rule.id == id then
     179           0 :                         policy.del(id)
     180           0 :                         table.remove(M.rules, id)
     181           0 :                         return true
     182             :                 end
     183             :         end
     184             : end
     185             : 
     186             : -- @function Find a rule
     187           0 : function M.get(id)
     188           0 :         for _, r in ipairs(M.rules) do
     189           0 :                 if r.rule.id == id then
     190           0 :                         return r
     191             :                 end
     192             :         end
     193             : end
     194             : 
     195             : -- @function Enable/disable a rule
     196           0 : function M.toggle(id, val)
     197           0 :         for _, r in ipairs(M.rules) do
     198           0 :                 if r.rule.id == id then
     199           0 :                         r.rule.suspended = not val
     200           0 :                         return true
     201             :                 end
     202             :         end
     203             : end
     204             : 
     205             : -- @function Enable/disable a rule
     206           0 : function M.disable(id)
     207           0 :         return M.toggle(id, false)
     208             : end
     209           0 : function M.enable(id)
     210           0 :         return M.toggle(id, true)
     211             : end
     212             : 
     213             : local function consensus(op, ...)
     214           0 :         local ret = true
     215           0 :         local results = map(string.format(op, ...))
     216           0 :         for _, r in ipairs(results) do
     217           0 :                 ret = ret and r
     218             :         end
     219           0 :         return ret
     220             : end
     221             : 
     222             : -- @function Public-facing API
     223             : local function api(h, stream)
     224           0 :         local m = h:get(':method')
     225             :         -- GET method
     226           0 :         if m == 'GET' then
     227           0 :                 local path = h:get(':path')
     228           0 :                 local id = tonumber(path:match '/([^/]*)$')
     229           0 :                 if id then
     230           0 :                         local r = M.get(id)
     231           0 :                         if r then
     232           0 :                                 return rule_info(r)
     233             :                         end
     234           0 :                         return 404, '"No such rule"' -- Not found
     235             :                 else
     236           0 :                         local ret = {}
     237           0 :                         for _, r in ipairs(M.rules) do
     238           0 :                                 table.insert(ret, rule_info(r))
     239             :                         end
     240           0 :                         return ret
     241             :                 end
     242             :         -- DELETE method
     243           0 :         elseif m == 'DELETE' then
     244           0 :                 local path = h:get(':path')
     245           0 :                 local id = tonumber(path:match '/([^/]*)$')
     246           0 :                 if id then
     247           0 :                         if consensus('daf.del "%s"', id) then
     248           0 :                                 return tojson(true)
     249             :                         end
     250           0 :                         return 404, '"No such rule"' -- Not found
     251             :                 end
     252           0 :                 return 400 -- Request doesn't have numeric id
     253             :         -- POST method
     254           0 :         elseif m == 'POST' then
     255           0 :                 local query = stream:get_body_as_string()
     256           0 :                 if query then
     257           0 :                         local ok, r = pcall(M.add, query)
     258           0 :                         if not ok then return 500, string.format('"%s"', r:match('/([^/]+)$')) end
     259             :                         -- Dispatch to all other workers
     260           0 :                         consensus('daf.add "%s"', query)
     261           0 :                         return rule_info(r)
     262             :                 end
     263           0 :                 return 400
     264             :         -- PATCH method
     265           0 :         elseif m == 'PATCH' then
     266           0 :                 local path = h:get(':path')
     267           0 :                 local id, action, val = path:match '(%d+)/([^/]*)/([^/]*)$'
     268           0 :                 id = tonumber(id)
     269           0 :                 if not id or not action or not val then
     270           0 :                         return 400 -- Request not well formatted
     271             :                 end
     272             :                 -- We do not support more actions
     273           0 :                 if action == 'active' then
     274           0 :                         if consensus('daf.toggle(%d, %s)', id, val == 'true' or 'false') then
     275           0 :                                 return tojson(true)
     276             :                         else
     277           0 :                                 return 404, '"No such rule"'
     278             :                         end
     279             :                 else
     280           0 :                         return 501, '"Action not implemented"'
     281             :                 end
     282             :         end
     283             : end
     284             : 
     285             : local function getmatches()
     286           0 :         local update = {}
     287           0 :         for _, rules in ipairs(map 'daf.rules') do
     288           0 :                 for _, r in ipairs(rules) do
     289           0 :                         local id = tostring(r.rule.id)
     290             :                         -- Must have string keys for JSON object and not an array
     291           0 :                         update[id] = (update[id] or 0) + r.rule.count
     292             :                 end
     293             :         end
     294           0 :         return update
     295             : end
     296             : 
     297             : -- @function Publish DAF statistics
     298             : local function publish(_, ws)
     299           0 :         local ok, last = true, nil
     300           0 :         while ok do
     301             :                 -- Check if we have new rule matches
     302           0 :                 local diff = {}
     303           0 :                 local has_update, update = pcall(getmatches)
     304           0 :                 if has_update then
     305           0 :                         if last then
     306           0 :                                 for id, count in pairs(update) do
     307           0 :                                         if not last[id] or last[id] < count then
     308           0 :                                                 diff[id] = count
     309             :                                         end
     310             :                                 end
     311             :                         end
     312           0 :                         last = update
     313             :                 end
     314             :                 -- Update counters when there is a new data
     315           0 :                 if next(diff) ~= nil then
     316           0 :                         ok = ws:send(tojson(diff))
     317             :                 else
     318           0 :                         ok = ws:send_ping()
     319             :                 end
     320           0 :                 worker.sleep(1)
     321             :         end
     322             : end
     323             : 
     324             : -- @function Configure module
     325           0 : function M.config()
     326           0 :         if not http or not http.endpoints then return end
     327             :         -- Export API and data publisher
     328           0 :         http.endpoints['/daf.js'] = http.page('daf.js', 'daf')
     329           0 :         http.endpoints['/daf'] = {'application/json', api, publish}
     330             :         -- Export snippet
     331           0 :         http.snippets['/daf'] = {'Application Firewall', [[
     332             :                 <script type="text/javascript" src="daf.js"></script>
     333             :                 <div class="row" style="margin-bottom: 5px">
     334             :                         <form id="daf-builder-form">
     335             :                                 <div class="col-md-11">
     336             :                                         <input type="text" id="daf-builder" class="form-control" aria-label="..." />
     337             :                                 </div>
     338             :                                 <div class="col-md-1">
     339             :                                         <button type="button" id="daf-add" class="btn btn-default btn-sm">Add</button>
     340             :                                 </div>
     341             :                         </form>
     342             :                 </div>
     343             :                 <div class="row">
     344             :                         <div class="col-md-12">
     345             :                                 <table id="daf-rules" class="table table-striped table-responsive">
     346             :                                 <th><td>No rules here yet.</td></th>
     347             :                                 </table>
     348             :                         </div>
     349             :                 </div>
     350             :         ]]}
     351             : end
     352             : 
     353           0 : return M

Generated by: LCOV version 1.13