Commit 00776953 authored by Marek Vavrusa's avatar Marek Vavrusa

modules/daf: new interface version, allows mirror

parent d3a6be12
......@@ -32,3 +32,40 @@ Example configuration
-- 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'
-- Mirror queries matching given name to DNS logger
daf.add 'qname ~ %w+.example.com MIRROR 127.0.0.2'
-- Truncate queries based on destination IPs
daf.add 'dst = 192.0.2.51 truncate'
-- Show active rules
daf.rules
[1] => {
[rule] => {
[count] => 42
[id] => 1
[cb] => function: 0x1a3eda38
}
[info] => qname = example.com AND src = 127.0.0.1/8 deny
[policy] => function: 0x1a3eda38
}
[2] => {
[rule] => {
[suspended] => true
[count] => 123522
[id] => 2
[cb] => function: 0x1a3ede88
}
[info] => qname ~ %w+.facebook.com AND src = 127.0.0.1/8 deny...
[policy] => function: 0x1a3ede88
}
...
-- Disable a rule
daf.disable 2
-- Enable a rule
daf.enable 2
-- Delete a rule
daf.del 2
/* Filter grammar */
const dafg = {
key: {'qname': true, 'src': true},
key: {'qname': true, 'src': true, 'dst': true},
op: {'=': true, '~': true},
conj: {'and': true, 'or': true},
action: {'pass': true, 'deny': true, 'drop': true, 'truncate': true, 'forward': true, 'reroute': true, 'rewrite': true},
action: {'pass': true, 'deny': true, 'drop': true, 'truncate': true, 'forward': true, 'reroute': true, 'rewrite': true, 'mirror': true},
suggest: [
'QNAME = example.com',
'QNAME ~ %d+.example.com',
'SRC = 127.0.0.1',
'SRC = 127.0.0.1/8',
'DST = 127.0.0.1',
'DST = 127.0.0.1/8',
/* Action examples */
'PASS', 'DENY', 'DROP', 'TRUNCATE',
'FORWARD 127.0.0.1',
'MIRROR 127.0.0.1',
'REROUTE 127.0.0.1-192.168.1.1',
'REROUTE 127.0.0.1/24-192.168.1.0',
'REWRITE example.com A 127.0.0.1',
......@@ -96,13 +99,66 @@ function formatRule(input) {
return res.join('');
}
function toggleRule(row, span, enabled) {
if (!enabled) {
span.removeClass('glyphicon-pause');
span.addClass('glyphicon-play');
row.addClass('warning');
} else {
span.removeClass('glyphicon-play');
span.addClass('glyphicon-pause');
row.removeClass('warning');
}
}
function ruleControl(cell, type, url, action) {
const row = cell.parent();
$.ajax({
url: 'daf/' + row.data('rule-id') + url,
type: type,
success: action,
fail: function (data) {
row.show();
const reason = data.responseText.length > 0 ? data.responseText : 'internal error';
cell.find('.alert').remove();
cell.append(
'<div class="alert alert-danger" role="alert">'+
'Failed (code: '+data.status+', reason: '+reason+').'+
'</div>'
);
},
});
}
function bindRuleControl(cell) {
const row = cell.parent();
cell.find('.daf-remove').click(function() {
row.hide();
ruleControl(cell, 'DELETE', '', function (data) {
cell.parent().remove();
});
});
cell.find('.daf-suspend').click(function() {
const span = $(this).find('span');
ruleControl(cell, 'PATCH', span.hasClass('glyphicon-pause') ? '/active/false' : '/active/true');
toggleRule(row, span, span.hasClass('glyphicon-play'));
});
}
function loadRule(rule, tbl) {
const row = $('<tr data-rule-id="'+rule.id+'" />');
row.append('<td class="daf-rule">' + formatRule(rule.info) + '</td>');
row.append('<td class="daf-count">' + rule.count + '</td>');
row.append('<td class="daf-rate"><span class="badge"></span></td>');
row.append('<td class="daf-ctl"></td>');
row.append('<td class="daf-ctl text-right">' +
'<div class="btn-group btn-group-xs">' +
'<button class="btn btn-default daf-suspend"><span class="glyphicon" aria="hidden" /></button>' +
'<button class="btn btn-default daf-remove"><span class="glyphicon glyphicon-remove" aria="hidden" /></button>' +
'</div></td>');
tbl.append(row);
/* Bind rule controls */
bindRuleControl(row.find('.daf-ctl'));
toggleRule(row, row.find('.daf-suspend span'), rule.active);
}
/* Load the filter table from JSON */
......@@ -162,28 +218,38 @@ $(function() {
const tok = input.split(' ');
if (dafg.action[tok[0].toLowerCase()]) {
$('#daf-add').focus();
$(this).close();
} else if(dafComplete(this)) {
/* No more rules after query is complete. */
item.remove();
$(this).close();
}
},
createFilter: function (input) {
const tok = input.split(' ');
var key, op, expr;
/* If there are already filters, allow conjunctions. */
if (tok.length > 0 && this.items.length > 0 && dafg.conj[tok[0]]) {
setValidateHint();
return true;
}
/* First token is expected to be filter key,
* or any postrule with a parameter */
if (validateToken(tok, dafg.key)) {
key = tok.shift();
} else if (tok.length > 1 && validateToken(tok, dafg.action)) {
setValidateHint();
return true;
} else {
return false;
}
/* Input is a filter - second token must be operator */
if (validateToken(tok, dafg.op)) {
op = tok.shift();
} else {
return false;
}
/* Input is a filter - the rest of the tokens are RHS arguments. */
if (tok.length > 0 && tok[0].length > 0) {
expr = tok.join(' ');
} else {
......@@ -219,9 +285,10 @@ $(function() {
loadRule(data, $('#daf-rules'));
})
.fail(function (data) {
const reason = data.responseText.length > 0 ? data.responseText : 'internal error';
form.append(
'<div class="alert alert-danger" role="alert">'+
'Couldn\'t add rule (code: '+data.status+', reason: '+data.responseText+').'+
'Couldn\'t add rule (code: '+data.status+', reason: '+reason+').'+
'</div>'
);
});
......
......@@ -6,10 +6,13 @@ if not policy then modules.load('policy') end
-- Actions
local actions = {
pass = 1, deny = 2, drop = 3, tc = 4,
pass = 1, deny = 2, drop = 3, tc = 4, truncate = 4,
forward = function (g)
return policy.FORWARD(g())
end,
mirror = function (g)
return policy.MIRROR(g())
end,
reroute = function (g)
local rules = {}
local tok = g()
......@@ -46,9 +49,15 @@ local filters = {
-- 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
if op ~= '=' then error('address supports only "=" operator') end
return view.rule_src(true, g())
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())
end,
}
local function parse_filter(tok, g)
......@@ -58,10 +67,15 @@ local function parse_filter(tok, g)
end
local function parse_rule(g)
local f = parse_filter(g(), g)
-- Allow action without filter
local tok = g()
if not filters[tok:lower()] then
return tok, nil
end
local f = parse_filter(tok, g)
-- Compose filter functions on conjunctions
-- or terminate filter chain and return
local tok = g()
tok = g()
while tok do
if tok == 'AND' then
local fa, fb = f, parse_filter(g(), g)
......@@ -87,7 +101,7 @@ local function parse_query(g)
if type(action) == 'function' then
action = action(g)
end
return filter, action, actid
return actid, action, filter
end
-- Compile a rule described by query language
......@@ -108,6 +122,72 @@ local M = {
rules = {}
}
-- @function Remove a rule
-- @function Cleanup module
function M.deinit()
if http then
http.endpoints['/daf'] = nil
http.endpoints['/daf.js'] = nil
http.snippets['/daf'] = nil
end
end
-- @function Add rule
function M.add(rule)
local id, action, filter = compile(rule)
if not id then error(action) end
-- Combine filter and action into policy
local p
if filter then
p = function (req, qry)
return filter(req, qry) and action
end
else
p = function (req, qry)
return action
end
end
local desc = {info=rule, policy=p}
-- Enforce in policy module, special actions are postrules
if id == 'reroute' or id == 'rewrite' then
desc.rule = policy.add(p, true)
else
desc.rule = policy.add(p)
end
table.insert(M.rules, desc)
return desc
end
-- @function Remove a rule
function M.del(id)
for i, r in ipairs(M.rules) do
if r.rule.id == id then
policy.del(id)
table.remove(M.rules, id)
return true
end
end
end
-- @function Enable/disable a rule
function M.toggle(id, val)
for i, r in ipairs(M.rules) do
if r.rule.id == id then
r.rule.suspended = not val
return true
end
end
end
-- @function Enable/disable a rule
function M.disable(id, val)
return M.toggle(id, false)
end
function M.enable(id, val)
return M.toggle(id, true)
end
-- @function Public-facing API
local function api(h, stream)
local m = h:get(':method')
......@@ -115,18 +195,47 @@ local function api(h, stream)
if m == 'GET' then
local ret = {}
for _, r in ipairs(M.rules) do
table.insert(ret, {info=r.info, id=r.rule.id, count=r.rule.count})
table.insert(ret, {info=r.info, id=r.rule.id, active=(r.rule.suspended ~= true), count=r.rule.count})
end
return ret
-- DELETE method
elseif m == 'DELETE' then
local path = h:get(':path')
local id = tonumber(path:match '/([^/]*)$')
if id then
if M.del(id) then
return tojson(true)
end
return 404, 'No such rule' -- Not found
end
return 400 -- Request doesn't have numeric id
-- POST method
elseif m == 'POST' then
local query = stream:get_body_as_string()
if query then
local ok, r = pcall(M.add, query)
if not ok then return 505 end
return {info=r.info, id=r.rule.id, count=r.rule.count}
local ok, r, err = pcall(M.add, query)
if not ok then return 500, r end
return {info=r.info, id=r.rule.id, active=(r.rule.suspended ~= true), count=r.rule.count}
end
return 400
-- PATCH method
elseif m == 'PATCH' then
local path = h:get(':path')
local id, action, val = path:match '(%d+)/([^/]*)/([^/]*)$'
id = tonumber(id)
if not id or not action or not val then
return 400 -- Request not well formatted
end
-- We do not support more actions
if action == 'active' then
if M.toggle(id, val == 'true') then
return tojson(true)
else
return 404, 'No such rule'
end
else
return 501, 'Action not implemented'
end
end
end
......@@ -153,15 +262,6 @@ local function publish(h, ws)
ws:close()
end
-- @function Cleanup module
function M.deinit()
if http then
http.endpoints['/daf'] = nil
http.endpoints['/daf.js'] = nil
http.snippets['/daf'] = nil
end
end
-- @function Configure module
function M.config(conf)
if not http then error('"http" module is not loaded, cannot load DAF') end
......@@ -189,23 +289,4 @@ function M.config(conf)
]]}
end
-- @function Add 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
local desc = {info=rule, policy=p}
-- Enforce in policy module, special actions are postrules
if id == 'reroute' or id == 'rewrite' then
desc.rule = policy:add(p, true)
else
desc.rule = policy:add(p)
end
table.insert(M.rules, desc)
return desc
end
return M
\ No newline at end of file
......@@ -234,10 +234,26 @@ Dependencies
* `lua-http <https://github.com/daurnimator/lua-http>`_ available in LuaRocks
``$ luarocks install --server=http://luarocks.org/dev http``
If you're installing via Homebrew on OS X, you need OpenSSL too.
.. code-block:: bash
$ brew update
$ brew install openssl
$ brew link openssl --force # Override system OpenSSL
Any other system can install from LuaRocks directly:
.. code-block:: bash
$ luarocks install --server=http://luarocks.org/dev http CC=cc
* `mmdblua <https://github.com/daurnimator/mmdblua>`_ available in LuaRocks
``$ luarocks install --server=http://luarocks.org/dev mmdblua``
.. code-block:: bash
$ luarocks install --server=http://luarocks.org/dev mmdblua
$ curl -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
$ gzip -d GeoLite2-City.mmdb.gz
.. _Prometheus: https://prometheus.io
\ No newline at end of file
......@@ -56,6 +56,7 @@ local pages = {
'bootstrap.min.css.map',
'bootstrap-theme.min.css',
'bootstrap-theme.min.css.map',
'glyphicons-halflings-regular.woff2',
}
-- Serve preloaded root page
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -38,38 +38,38 @@ Example configuration
-- Load default policies
modules = { 'policy' }
-- Whitelist 'www[0-9].badboy.cz'
policy:add(policy.pattern(policy.PASS, '\4www[0-9]\6badboy\2cz'))
policy.add(policy.pattern(policy.PASS, '\4www[0-9]\6badboy\2cz'))
-- Block all names below badboy.cz
policy:add(policy.suffix(policy.DENY, {'\6badboy\2cz'}))
policy.add(policy.suffix(policy.DENY, {'\6badboy\2cz'}))
-- Custom rule
policy:add(function (req, query)
policy.add(function (req, query)
if query:qname():find('%d.%d.%d.224\7in-addr\4arpa') then
return policy.DENY
end
end)
-- Disallow ANY queries
policy:add(function (req, query)
policy.add(function (req, query)
if query.type == kres.type.ANY then
return policy.DROP
end
end)
-- Enforce local RPZ
policy:add(policy.rpz(policy.DENY, 'blacklist.rpz'))
policy.add(policy.rpz(policy.DENY, 'blacklist.rpz'))
-- Forward all queries below 'company.se' to given resolver
policy:add(policy.suffix(policy.FORWARD('192.168.1.1'), {'\7company\2se'}))
policy.add(policy.suffix(policy.FORWARD('192.168.1.1'), {'\7company\2se'}))
-- Forward all queries matching pattern
policy:add(policy.pattern(policy.FORWARD('2001:DB8::1'), '\4bad[0-9]\2cz'))
policy.add(policy.pattern(policy.FORWARD('2001:DB8::1'), '\4bad[0-9]\2cz'))
-- Forward all queries (complete stub mode)
policy:add(policy.all(policy.FORWARD('2001:DB8::1')))
policy.add(policy.all(policy.FORWARD('2001:DB8::1')))
-- Mirror all queries and retrieve information
local rule = policy:add(policy.all(policy.MIRROR('127.0.0.2')))
local rule = policy.add(policy.all(policy.MIRROR('127.0.0.2')))
-- Print information about the rule
print(string.format('id: %d, matched queries: %d', rule.id, rule.count)
-- Reroute all addresses found in answer from 192.0.2.0/24 to 127.0.0.x
-- this policy is enforced on answers, therefore 'postrule'
local rule = policy:add(policy.REROUTE({'192.0.2.0/24', '127.0.0.0'}), true)
local rule = policy.add(policy.REROUTE({'192.0.2.0/24', '127.0.0.0'}), true)
-- Delete rule that we just created
policy:del(rule.id)
policy.del(rule.id)
Properties
^^^^^^^^^^
......@@ -102,7 +102,7 @@ Properties
Reroute addresses in response matching given subnet to given target, e.g. ``{'192.0.2.0/24', '127.0.0.0'}`` will rewrite '192.0.2.55' to '127.0.0.55'.
.. function:: policy:add(rule, postrule)
.. function:: policy.add(rule, postrule)
:param rule: added rule, i.e. ``policy.pattern(policy.DENY, '[0-9]+\2cz')``
:param postrule: boolean, if true the rule will be evaluated on answer instead of query
......@@ -110,7 +110,7 @@ Properties
Add a new policy rule that is executed either or queries or answers, depending on the ``postrule`` parameter. You can then use the returned rule description to get information and unique identifier for the rule, as well as match count.
.. function:: policy:del(id)
.. function:: policy.del(id)
:param id: identifier of a given rule
:return: boolean
......
......@@ -242,7 +242,7 @@ policy.layer = {
}
-- Add rule to policy list
function policy.add(policy, rule, postrule)
function policy.add(rule, postrule)
local desc = {id=getruleid(), cb=rule, count=0}
table.insert(postrule and policy.postrules or policy.rules, desc)
return desc
......@@ -260,7 +260,7 @@ local function delrule(rules, id)
end
-- Delete rule from policy list
function policy.del(policy, id)
function policy.del(id)
if not delrule(policy.rules, id) then
if not delrule(policy.postrules, id) then
return false
......@@ -320,6 +320,6 @@ policy.todnames(private_zones)
-- @var Default rules
policy.rules = {}
policy.postrules = {}
policy:add(policy.suffix_common(policy.DENY, private_zones, '\4arpa\0'))
policy.add(policy.suffix_common(policy.DENY, private_zones, '\4arpa\0'))
return policy
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment