From: Marek Vavrusa Date: Thu, 9 Jun 2016 07:42:59 +0000 (-0700) Subject: modules/daf: support for first firewall rules X-Git-Tag: v1.1.0~52 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e74444911fcc6f07ee8fbb3a20a114de2eaa138f;p=thirdparty%2Fknot-resolver.git modules/daf: support for first firewall rules 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) --- diff --git a/modules/daf/README.md b/modules/daf/README.md index a267aaa7a..9d93336a6 100644 --- a/modules/daf/README.md +++ b/modules/daf/README.md @@ -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 index 000000000..82b8b8d0e --- /dev/null +++ b/modules/daf/daf.js @@ -0,0 +1 @@ +console.log('Hello from DAF!') diff --git a/modules/daf/daf.lua b/modules/daf/daf.lua index 976bf6c98..e3b8269d3 100644 --- a/modules/daf/daf.lua +++ b/modules/daf/daf.lua @@ -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 ' ' + 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', [[ -

Hello world!

+ +
No rules here yet.
]]} - 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 diff --git a/modules/daf/daf.mk b/modules/daf/daf.mk index bd66866ae..0f02675bf 100644 --- a/modules/daf/daf.mk +++ b/modules/daf/daf.mk @@ -1,2 +1,3 @@ daf_SOURCES := daf.lua +daf_INSTALL := modules/daf/daf.js $(call make_lua_module,daf) diff --git a/modules/http/http.lua b/modules/http/http.lua index e30660c56..c17bb8eb3 100644 --- a/modules/http/http.lua +++ b/modules/http/http.lua @@ -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