]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
modules/daf: support for first firewall rules
authorMarek Vavrusa <marek@vavrusa.com>
Thu, 9 Jun 2016 07:42:59 +0000 (00:42 -0700)
committerMarek Vavrusa <marek@vavrusa.com>
Wed, 6 Jul 2016 06:33:38 +0000 (23:33 -0700)
the format of rules resembles libpcap filters,
but it also requires action that should be taken
when the filter(s) match.

the action can be anything the policy module
supports, and the filters can be both policy
module or view module based (so it's possible to
filter on source address and packet contents at
the same time)

modules/daf/README.md
modules/daf/daf.js [new file with mode: 0644]
modules/daf/daf.lua
modules/daf/daf.mk
modules/http/http.lua

index a267aaa7a80d2f731c9a40f58435f684633fd6b9..9d93336a683fa43a3b415aec202200a30003793a 100644 (file)
@@ -11,3 +11,24 @@ Example configuration
 .. code-block:: lua
 
        modules = { 'http', 'daf' }
+
+       -- Let's write some daft rules!
+       
+       -- Block all queries with QNAME = example.com
+       daf.add 'qname = example.com deny'
+
+       -- Filters can be combined using AND/OR...
+       -- Block all queries with QNAME match regex and coming from given subnet
+       daf.add 'qname ~ %w+.example.com AND src = 192.0.2.0/24 deny'
+
+       -- We also can reroute addresses in response to alternate target
+       -- This reroutes 1.2.3.4 to localhost
+       daf.add 'src = 127.0.0.0/8 reroute 192.0.2.1-127.0.0.1'
+
+       -- Subnets work too, this reroutes a whole subnet
+       -- e.g. 192.0.2.55 to 127.0.0.55
+       daf.add 'src = 127.0.0.0/8 reroute 192.0.2.0/24-127.0.0.0'
+
+       -- This rewrites all A answers for 'example.com' from
+       -- whatever the original address was to 127.0.0.2
+       daf.add 'src = 127.0.0.0/8 rewrite example.com A 127.0.0.2'
diff --git a/modules/daf/daf.js b/modules/daf/daf.js
new file mode 100644 (file)
index 0000000..82b8b8d
--- /dev/null
@@ -0,0 +1 @@
+console.log('Hello from DAF!')
index 976bf6c9812c70a53e1280f40600cf2a0c8d21fc..e3b8269d3e282f659f7b3c40e510d4371c05a192 100644 (file)
@@ -6,7 +6,32 @@ if not policy then modules.load('policy') end
 
 -- Actions
 local actions = {
-       pass = 1, deny = 2, drop = 3, tc = 4, forward = policy.FORWARD,
+       pass = 1, deny = 2, drop = 3, tc = 4,
+       forward = function (g)
+               return policy.FORWARD(g())
+       end,
+       reroute = function (g)
+               local rules = {}
+               local tok = g()
+               while tok do
+                       local from, to = tok:match '([^-]+)-(%S+)'
+                       rules[from] = to
+                       tok = g()
+               end
+               return policy.REROUTE(rules)
+       end,
+       rewrite = function (g)
+               local rules = {}
+               local tok = g()
+               while tok do
+                       -- This is currently limited to A/AAAA rewriting
+                       -- in fixed format '<owner> <type> <addr>'
+                       local _, to = g(), g()
+                       rules[tok] = to
+                       tok = g()
+               end
+               return policy.REROUTE(rules, true)
+       end,
 }
 
 -- Filter rules per column
@@ -14,7 +39,7 @@ 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)
+               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
        end,
@@ -39,30 +64,29 @@ local function parse_rule(g)
        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
+                       local fa, fb = f, parse_filter(g(), g)
+                       f = function (req, qry) return fa(req, qry) and fb(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
+                       local fa, fb = f, parse_filter(g(), g)
+                       f = function (req, qry) return fa(req, qry) or fb(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
+       local ok, actid, filter = pcall(parse_rule, g)
+       if not ok then return nil, actid end
+       if not actions[actid] then return nil, string.format('invalid action "%s"', actid) end
        -- Parse and interpret action
-       action = actions[action]
+       local action = actions[actid]
        if type(action) == 'function' then
-               action = action(g())
+               action = action(g)
        end
-       return action, filter
+       return filter, action, actid
 end
 
 -- Compile a rule described by query language
@@ -105,6 +129,7 @@ end
 function M.deinit()
        if http then
                http.endpoints['/daf'] = nil
+               http.endpoints['/daf.js'] = nil
                http.snippets['/daf'] = nil
        end
 end
@@ -113,21 +138,30 @@ end
 function M.config(conf)
        if not http then error('"http" module is not loaded, cannot load DAF') end
        -- Export API and data publisher
+       http.endpoints['/daf.js'] = http.page('daf.js', 'daf')
        http.endpoints['/daf'] = {'application/json', api, publish}
        -- Export snippet
        http.snippets['/daf'] = {'Application Firewall', [[
-               <p>Hello world!</p>
+               <script type="text/javascript" src="daf.js"></script>
+               <table id="daf-rules"><th><td>No rules here yet.</td></th></table>
        ]]}
-       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)
+function M.add(rule)
+       local filter, action, id = compile(rule)
+       if not filter then error(action) end
+       -- Combine filter and action into policy
+       local p = function (req, qry)
+               return filter(req, qry) and action
+       end
+       table.insert(M.rules, {rule=rule, action=id, policy=p})
+       -- Enforce in policy module, special actions are postrules
+       if id == 'reroute' or id == 'rewrite' then
+               table.insert(policy.postrules, p)
+       else
+               table.insert(policy.rules, p)
+       end
 end
 
 return M
\ No newline at end of file
index bd66866ae06646ce04b1f1bded4ddaefea8bf363..0f02675bfd487b26ff35bd7b195aeb6a2dd8122f 100644 (file)
@@ -1,2 +1,3 @@
 daf_SOURCES := daf.lua
+daf_INSTALL := modules/daf/daf.js
 $(call make_lua_module,daf)
index e30660c560dc3c0924b17969ac272d2c14c19e15..c17bb8eb3a6381dd78258a686b932ad1fc0bab90 100644 (file)
@@ -25,31 +25,33 @@ local mime_types = {
 }
 
 -- Preload static contents, nothing on runtime will touch the disk
-local function pgload(relpath)
-       local fp, err = io.open(moduledir..'/http/'..relpath, 'r')
+local function pgload(relpath, modname)
+       if not modname then modname = 'http' end
+       local fp, err = io.open(string.format('%s/%s/%s', moduledir, modname, relpath), 'r')
        if not fp then error(err) end
        local data = fp:read('*all')
        fp:close()
        -- Guess content type
        local ext = relpath:match('[^\\.]+$')
-       return {'/'..relpath, mime_types[ext] or 'text', data, 86400}
+       return {mime_types[ext] or 'text', data, nil, 86400}
 end
+M.page = pgload
 
 -- Preloaded static assets
 local pages = {
-       pgload('favicon.ico'),
-       pgload('rickshaw.min.css'),
-       pgload('kresd.js'),
-       pgload('datamaps.world.min.js'),
-       pgload('topojson.js'),
-       pgload('jquery.js'),
-       pgload('rickshaw.min.js'),
-       pgload('d3.js'),
+       'favicon.ico',
+       'rickshaw.min.css',
+       'kresd.js',
+       'datamaps.world.min.js',
+       'topojson.js',
+       'jquery.js',
+       'rickshaw.min.js',
+       'd3.js',
 }
 
 -- Serve preloaded root page
 local function serve_root()
-       local data = pgload('main.tpl')[3]
+       local data = pgload('main.tpl')[2]
        data = data
                :gsub('{{ title }}', 'kresd @ '..hostname())
                :gsub('{{ host }}', hostname())
@@ -73,8 +75,7 @@ M.endpoints = {
 
 -- Export static pages
 for _, pg in ipairs(pages) do
-       local path, mime, data, ttl = unpack(pg)
-       M.endpoints[path] = {mime, data, nil, ttl}
+       M.endpoints['/'..pg] = pgload(pg)
 end
 
 -- Export built-in prometheus interface