...
 
Commits (52)
# Installer Makefile for Alpine Wall
# Copyright (C) 2012-2017 Kaarle Ritvanen
# Copyright (C) 2012-2020 Kaarle Ritvanen
# See LICENSE file for license details
ROOT_DIR := /
LUA_VERSION := 5.2
LUA_VERSION := 5.3
resdir := usr/share/awall
confdir := etc/awall
......@@ -34,7 +34,8 @@ files += $(2)
endef
$(eval $(call copy,awall,usr/share/lua/$(LUA_VERSION)/awall,lua))
$(eval $(call copy,json,$(resdir)/mandatory,json))
$(eval $(call copy,mandatory,$(resdir)/mandatory,json))
$(eval $(call copy,optional,$(resdir)/optional,json))
$(eval $(call rename,awall-cli,usr/sbin/awall,755))
$(eval $(call rename,sample-policy.json,$(resdir)/sample/sample-policy.json,644))
......
......@@ -455,13 +455,14 @@ whether to consider the source (**src**, default) or destination
defaults to **pass** and cannot be set to any other value.
Filter objects may have an attribute named **dnat**, the value of
which is an IPv4 address. If defined, this enables destination NAT for
all IPv4 packets matching the rule, such that the specified address
replaces the original destination address. If also port translation is
desired, the attribute may be defined as an object consisting of
attributes **addr** and **port**. The format of the **port** attribute
is similar to that of the **to-port** attribute of [NAT
rules](#nat). This option has no effect on IPv6 packets.
which is an IPv4 address or a DNS name resolving to a single IPv4
address. If defined, this enables destination NAT for all IPv4 packets
matching the rule, such that the specified address replaces the
original destination address. If also port translation is desired, the
attribute may be defined as an object consisting of attributes
**addr** and **port**. The format of the **port** attribute is similar
to that of the **to-port** attribute of [NAT rules](#nat). This option
has no effect on IPv6 packets.
Filter objects may have a boolean attribute named **no-track**. If set
to **true**, connection tracking is bypassed for the matching
......@@ -630,6 +631,17 @@ customized chain, using the **custom:** prefix. It is also possible to
constrain each rule to IPv4 or IPv6 only by defining the **family**
attribute as **inet** or **inet6**, respectively.
## <a name="dedicated"></a>Co-Existence with Other Firewall Management Tools
If awall is used on a host running other software that manipulates
iptables rules, it is recommended to set the
**awall_dedicated_chains** variable to **true**, which will have the
following effects:
* Awall installs its own rules to dedicated chains prefixed with
**awall-**.
* Activation of awall rules leaves any unrelated rule intact.
## Command Line Syntax
### Translating Policy Files to Firewall Configuration Files
......@@ -655,9 +667,15 @@ the Return key within 10 seconds or the `--force` option is used, the
configuration is saved to the files. Otherwise, the old configuration
is restored.
**awall flush**
**awall flush** \[**-a** | **--all**\]
This command configures the firewall to drop all packets.
Normally, this command deletes all firewall rules and configures it to
drop all packets.
If awall is configured to [co-exist with other firewall management
tools](#dedicated), this command flushes only the rules installed by
awall. Specifying `--all` overrides this behavior and causes all rules
to be flushed.
### Optional Policies
......@@ -667,7 +685,7 @@ Optional policies can be enabled or disabled using this command:
Optional policies can be listed using this command:
**awall list**
**awall list** \[**-a** | **--all**\]
The **enabled** status means that the policy has been enabled by the
user. The **disabled** status means that the policy is not in use. The
......@@ -675,6 +693,9 @@ user. The **disabled** status means that the policy is not in use. The
user but is in use because it is required by another policy which is
in use.
Normally, the command lists only optional policies. Specifying `--all`
makes it list all policies and more information about them.
### Debugging Policies
This command can be used to dump variable, zone, and other definitions
......@@ -690,6 +711,150 @@ information is displayed on higher levels.
Displays the difference in the input policy files and generated output
files since the last **translate** or **activate** command.
When the **--output** option is used, the updated configuration is
When the `--output` option is used, the updated configuration is
compared to the generated files in the specified directory (generated
by the equivalent **translate** command).
## Default Policies
Awall ships with a set of optional policies, which can be used as the
basis for firewall configuration:
<table>
<thead><tr><th>Name</th><th>Description</th></tr></thead>
<tbody>
<tr>
<td><strong>adp-clamp-mss</strong></td>
<td>Clamp MSS on WAN</td>
</tr>
<tr>
<td><strong>adp-dhcp</strong></td>
<td>Allow DHCP on specified zones</td>
</tr>
<tr>
<td><strong>adp-http-server</strong></td>
<td>Allow HTTP server on the firewall host</td>
</tr>
<tr>
<td><strong>adp-local-outbound</strong></td>
<td>Policy for local outbound traffic</td>
</tr>
<tr>
<td><strong>adp-ntp-client</strong></td>
<td>Allow DNS and NTP clients on the firewall host</td>
</tr>
<tr>
<td><strong>adp-ping</td></strong>
<td>
Allow ICMP echo request. On WAN, rate is limited to 3 packets
per second.
</td>
</tr>
<tr>
<td><strong>adp-router</strong></td>
<td>
Routing policy from LAN to WAN, possibly with NAT. Prevent LAN
address spoofing from WAN.
</td>
</tr>
<tr>
<td><strong>adp-ssh-client</strong></td>
<td>Allow SSH clients on the firewall host</td>
</tr>
<tr>
<td><strong>adp-ssh-server</strong></td>
<td>
Allow SSH server on the firewall host. On WAN, rate is limited
to 1 connection per 10 seconds.
</td>
</tr>
<tr>
<td><strong>adp-web-client</strong></td>
<td>Allow DNS, HTTP, and HTTPS from specified zones to WAN</td>
</tr>
</tbody>
</table>
The behavior of these policies can be tuned by defining variables and
zones in a policy named **adp-config** or another policy imported by
this policy. On Alpine Linux, the **setup-firewall** utility
automatically enables some of the policies and generates an initial
**adp-config** policy by making an educated guess.
### Zones
<table>
<thead><tr><th>Name</th><th>Used by</th><th>Description</th></tr></thead>
<tbody>
<tr>
<td><strong>adp-lan</strong></td>
<td><strong>adp-router</strong></td>
<td>
Local Area Network (LAN), defined by variables prefixed with
<strong>adp_lan_</strong>
</td>
</tr>
<tr>
<td><strong>adp-wan</strong></td>
<td>
<strong>adp-clamp-mss</strong><br>
<strong>adp-ping</strong><br>
<strong>adp-router</strong><br>
<strong>adp-ssh-server</strong><br>
<strong>adp-web-client</strong>
</td>
<td>
Wide Area Network (WAN), to be defined in <strong>adp-config</strong>
</td>
</tr>
</tbody>
</table>
### Variables
<table>
<thead><tr><th>Name</th><th>Used by</th><th>Description</th></tr></thead>
<tbody>
<tr>
<td><strong>adp_dhcp_zones</strong></td>
<td><strong>adp-dhcp</strong></td>
<td>Zones on which DHCP is allowed</td>
</tr>
<tr>
<td><strong>adp_lan_addrs</strong></td>
<td><strong>adp-router</strong></td>
<td>LAN addresses</td>
</tr>
<tr>
<td><strong>adp_lan_ifaces</strong></td>
<td><strong>adp-router</strong></td>
<td>LAN interfaces</td>
</tr>
<td><strong>adp_lan_private_addrs</strong></td>
<td><strong>adp-router</strong></td>
<td>
Private LAN addresses for which NAT must be applied when routing to WAN
</td>
</tr>
<tr>
<td><strong>adp_local_policy</strong></td>
<td><strong>adp-local-outbound</strong></td>
<td>
Policy for local outbound traffic, defaults to <strong>reject</strong>
</td>
</tr>
<tr>
<td><strong>adp_router_policy</strong></td>
<td><strong>adp-router</strong></td>
<td>Routing policy from LAN to WAN</td>
</tr>
<tr>
<td><strong>adp_web_client_zones</strong></td>
<td><strong>adp-web-client</strong></th>
<td>
Zones on which web clients are allowed, defaults to the
firewall host only
</td>
</tr>
</tbody>
</table>
......@@ -2,7 +2,7 @@
--[[
Alpine Wall
Copyright (C) 2012-2018 Kaarle Ritvanen
Copyright (C) 2012-2020 Kaarle Ritvanen
See LICENSE file for license details
]]--
......@@ -11,6 +11,7 @@ lpc = require('lpc')
posix = require('posix')
signal = posix.signal
stat = posix.stat
stringy = require('stringy')
......@@ -20,7 +21,7 @@ if not table.unpack then table.unpack = unpack end
function help()
io.stderr:write([[
Alpine Wall
Copyright (C) 2012-2018 Kaarle Ritvanen
Copyright (C) 2012-2020 Kaarle Ritvanen
This is free software with ABSOLUTELY NO WARRANTY,
available under the terms of the GNU General Public License, version 2
......@@ -48,10 +49,15 @@ Run-time activation of new firewall configuration:
configuration is restored.
Flush firewall configuration:
awall flush
awall flush [-a|--all]
This command deletes all firewall rules and configures it to drop
all packets.
Normally, this command deletes all firewall rules and configures
it to drop all packets.
If awall is configured to co-exist with other firewall management
tools, this command flushes only the rules installed by awall.
Specifying --all overrides this behavior and causes all rules to
be flushed.
Enable/disable optional policies:
awall {enable|disable} <policy>...
......@@ -147,11 +153,12 @@ end
if dev_mode then
util.setdefault(pol_paths, 'mandatory', {'/etc/awall'})
table.insert(pol_paths.mandatory, basedir..'/json')
table.insert(pol_paths.mandatory, basedir..'/mandatory')
end
uerror = require('awall.uerror')
call = uerror.call
raise = uerror.raise
if not call(
function()
......@@ -194,7 +201,7 @@ if not call(
repeat
local name = arg[opind]
local policy = policyset.policies[name]
if not policy then uerror.raise('No such policy: '..name) end
if not policy then raise('No such policy: '..name) end
policy[mode](policy)
opind = opind + 1
until opind > #arg
......@@ -202,6 +209,22 @@ if not call(
end
local iptables = require('awall.iptables')
if mode == 'fallback' then
for _, sig in ipairs{'HUP', 'INT', 'PIPE'} do
signal(posix['SIG'..sig], 'SIG_IGN')
end
posix.sleep(10)
printmsg('\nTimeout, reverting to the old configuration')
iptables.revert()
os.exit()
end
local input = policyset:load()
if mode == 'dump' then level = 0 + (arg[opind] or 0) end
......@@ -284,13 +307,10 @@ if not call(
local dumpfile = outputdir and outputdir..'/dump' or sysdumpfile
local iptables = require('awall.iptables')
if mode == 'dump' then dump(level)
elseif mode == 'diff' then
if not posix.stat(dumpfile) then
if not stat(dumpfile) then
printmsg('Please translate or activate first')
os.exit(2)
end
......@@ -320,18 +340,15 @@ if not call(
elseif mode == 'activate' then
iptables.backup()
local function translate()
config:dump()
filedump(sysdumpfile)
end
local pid, interrupted
local interrupted
if not force then
signal(
posix.SIGCHLD,
function()
if pid and lpc.wait(pid, 1) then os.exit(1) end
end
)
for i, sig in ipairs({'INT', 'TERM'}) do
for _, sig in ipairs{'INT', 'TERM'} do
signal(
posix['SIG'..sig],
function()
......@@ -340,11 +357,44 @@ if not call(
end
)
end
end
local stdio, stdout
pid, stdio, stdout = lpc.run(arg[0], 'fallback')
stdio:close()
stdout:close()
if not iptables.isenabled() then
local INIT = '/usr/libexec/awall-init'
if not force and stat(INIT) then
printmsg('Firewall is not active')
io.stderr:write(
'Press RETURN to perform initial configuration and activation: '
)
if io.read() then
translate()
for _, family in ipairs(require('awall.family').ACTIVE) do
os.execute(
INIT..' '..
({inet='iptables', inet6='ip6tables'})[family]
)
end
os.exit(0)
end
printmsg('\nCanceled')
os.exit(2)
end
raise('Firewall not enabled in kernel')
end
iptables.backup()
local pid
if not force then
signal(
posix.SIGCHLD,
function()
if pid and lpc.wait(pid, 1) then os.exit(1) end
end
)
pid = util.run(arg[0], 'fallback')
end
local function kill()
......@@ -375,8 +425,7 @@ if not call(
end
end
config:dump()
filedump(sysdumpfile)
translate()
else
if not force then kill() end
......@@ -384,18 +433,9 @@ if not call(
end
elseif mode == 'fallback' then
for i, sig in ipairs({'HUP', 'PIPE'}) do
signal(posix['SIG'..sig], 'SIG_IGN')
end
posix.sleep(10)
printmsg('\nTimeout, reverting to the old configuration')
iptables.revert()
elseif mode == 'flush' then iptables.flush()
elseif mode == 'flush' then
if all then iptables.flush()
else config:flush() end
else assert(false) end
......
--[[
Dependency order resolver for Alpine Wall
Copyright (C) 2012-2014 Kaarle Ritvanen
Copyright (C) 2012-2018 Kaarle Ritvanen
See LICENSE file for license details
]]--
local util = require('awall.util')
local contains = util.contains
local sortedkeys = util.sortedkeys
return function(items)
local visited = {}
......@@ -17,8 +18,8 @@ return function(items)
visited[key] = true
local after = util.list(items[key].after)
for k, v in pairs(items) do
if contains(v.before, key) then table.insert(after, k) end
for _, k in sortedkeys(items) do
if contains(items[k].before, key) then table.insert(after, k) end
end
for i, k in ipairs(after) do
if items[k] then
......@@ -30,7 +31,7 @@ return function(items)
table.insert(res, key)
end
for i, k in util.sortedkeys(items) do
for _, k in sortedkeys(items) do
local ek = visit(k)
if ek ~= nil then return ek end
end
......
--[[
Address family module for Alpine Wall
Copyright (C) 2012-2019 Kaarle Ritvanen
See LICENSE file for license details
]]--
local M = {ACTIVE={}, ALL={}}
local stat = require('posix').stat
for family, procfile in pairs{inet='raw', inet6='raw6'} do
table.insert(M.ALL, family)
if stat('/proc/net/'..procfile) then table.insert(M.ACTIVE, family) end
end
return M
--[[
Host address resolver for Alpine Wall
Copyright (C) 2012-2017 Kaarle Ritvanen
Copyright (C) 2012-2019 Kaarle Ritvanen
See LICENSE file for license details
]]--
......@@ -29,21 +29,20 @@ function M.resolve(host, context)
if not dnscache[host] then
dnscache[host] = {}
for rec in io.popen('dig -t ANY '..host):lines() do
local name, rtype, addr =
rec:match(
'^('..familypatterns.domain..')%s+%d+%s+IN%s+(A+)%s+(.+)'
)
if name and name:sub(1, host:len() + 1) == host..'.' then
if rtype == 'A' then family = 'inet'
elseif rtype == 'AAAA' then family = 'inet6'
else family = nil end
if family then
assert(getfamily(addr, context) == family)
table.insert(dnscache[host], {family, addr})
end
for family, rtype in pairs{inet='A', inet6='AAAA'} do
local answer
for rec in io.popen('drill '..host..' '..rtype):lines() do
if answer then
if rec == '' then break end
local addr = rec:match(
'^'..familypatterns.domain..'%s+%d+%s+IN%s+'..rtype..
'%s+(.+)'
)
if addr then
assert(getfamily(addr, context) == family)
table.insert(dnscache[host], {family, addr})
end
elseif rec == ';; ANSWER SECTION:' then answer = true end
end
end
if not dnscache[host][1] then
......
--[[
Alpine Wall main module
Copyright (C) 2012-2016 Kaarle Ritvanen
Copyright (C) 2012-2020 Kaarle Ritvanen
See LICENSE file for license details
]]--
......@@ -10,11 +10,8 @@ local M = {}
local class = require('awall.class')
local resolve = require('awall.dependency')
local IPSet = require('awall.ipset')
local IPTables = require('awall.iptables').IPTables
local optfrag = require('awall.optfrag')
local combinations = optfrag.combinations
local iptables = require('awall.iptables')
local combinations = require('awall.optfrag').combinations
M.PolicySet = require('awall.policy')
local util = require('awall.util')
......@@ -60,7 +57,7 @@ function M.loadmodules(path)
table.sort(modules)
local imported = {}
for i, name in ipairs(modules) do
for _, name in ipairs(modules) do
extend(imported, readmetadata(require(name)))
end
......@@ -81,14 +78,20 @@ M.Config = class()
function M.Config:init(policyconfig)
self.objects = policyconfig:expand()
self.iptables = IPTables()
local dedicated = self.objects.variable.awall_dedicated_chains
self.iptables = dedicated and iptables.PartialIPTables() or
iptables.IPTables()
self.prefix = dedicated and 'awall-' or ''
local actions = {}
local function insertrules(trules, obj)
for i, trule in ipairs(trules) do
local t = self.iptables.config[trule.family][trule.table][trule.chain]
local opts = optfrag.command(trule)
for _, trule in ipairs(trules) do
local t = self.iptables.config[trule.family][trule.table][
self.prefix..trule.chain
]
local opts = self:ofragcmd(trule)
if trule.target then
local acfrag = {
......@@ -96,7 +99,7 @@ function M.Config:init(policyconfig)
table=trule.table,
chain=trule.target
}
local key = optfrag.location(acfrag)
local key = self:ofragloc(acfrag)
if not actions[key] then
actions[key] = true
if stringy.startswith(trule.target, 'custom:') then
......@@ -105,12 +108,7 @@ function M.Config:init(policyconfig)
if not rules then
obj:error('Invalid custom chain: '..name)
end
insertrules(
combinations(
{{chain=trule.target}}, util.list(rules), {acfrag}
),
rules
)
insertrules(combinations(util.list(rules), {acfrag}), rules)
else insertrules(combinations(achains, {acfrag})) end
end
end
......@@ -123,7 +121,7 @@ function M.Config:init(policyconfig)
end
end
for i, path in ipairs(procorder) do
for _, path in ipairs(procorder) do
if path:sub(1, 1) ~= '%' then
local objs = self.objects[path]
if objs then
......@@ -138,7 +136,7 @@ function M.Config:init(policyconfig)
end
end
for i, event in ipairs(procorder) do
for _, event in ipairs(procorder) do
if event:sub(1, 1) == '%' then
local r = events[event].rules
if r then
......@@ -149,7 +147,7 @@ function M.Config:init(policyconfig)
end
end
elseif self.objects[event] then
for i, rule in ipairs(self.objects[event]) do
for _, rule in ipairs(self.objects[event]) do
insertrules(rule:trules(), rule)
end
end
......@@ -158,6 +156,19 @@ function M.Config:init(policyconfig)
self.ipset = IPSet(self.objects.ipset)
end
function M.Config:ofragloc(of)
return of.family..'/'..of.table..'/'..self.prefix..of.chain
end
function M.Config:ofragcmd(of)
local target = ''
if of.target then
target = '-j '..(util.startswithupper(of.target) and '' or self.prefix)..
of.target
end
return (of.match and of.match..' ' or '')..target
end
function M.Config:print()
self.ipset:print()
io.write('\n')
......@@ -179,5 +190,7 @@ function M.Config:activate()
self.iptables:activate()
end
function M.Config:flush() self.iptables:flush() end
return M
--[[
Ipset file dumper for Alpine Wall
Copyright (C) 2012-2016 Kaarle Ritvanen
Copyright (C) 2012-2020 Kaarle Ritvanen
See LICENSE file for license details
]]--
......@@ -20,10 +20,9 @@ end
function IPSet:create()
for name, ipset in pairs(self.config) do
local pid = lpc.run(
if util.execute(
'ipset', '-!', 'create', name, table.unpack(ipset.options)
)
if lpc.wait(pid) ~= 0 then
) ~= 0 then
util.printmsg('ipset creation failed: '..name)
end
end
......
--[[
Iptables file dumper for Alpine Wall
Copyright (C) 2012-2016 Kaarle Ritvanen
Copyright (C) 2012-2020 Kaarle Ritvanen
See LICENSE file for license details
]]--
local class = require('awall.class')
local ACTIVE = require('awall.family').ACTIVE
local raise = require('awall.uerror').raise
local util = require('awall.util')
......@@ -13,20 +14,25 @@ local printmsg = util.printmsg
local sortedkeys = util.sortedkeys
local mkdir = require('posix').mkdir
local lpc = require('lpc')
local posix = require('posix')
local stringy = require('stringy')
local M = {}
local families = {inet={cmd='iptables',
file='rules-save',
procfile='/proc/net/ip_tables_names'},
inet6={cmd='ip6tables',
file='rules6-save',
procfile='/proc/net/ip6_tables_names'}}
local families = {
inet={
cmd='iptables', file='rules-save', procfile='/proc/net/ip_tables_names'
},
inet6={
cmd='ip6tables',
file='rules6-save',
procfile='/proc/net/ip6_tables_names'
}
}
M.builtin = {
local builtin = {
filter={'FORWARD', 'INPUT', 'OUTPUT'},
mangle={'FORWARD', 'INPUT', 'OUTPUT', 'POSTROUTING', 'PREROUTING'},
nat={'INPUT', 'OUTPUT', 'POSTROUTING', 'PREROUTING'},
......@@ -37,6 +43,23 @@ M.builtin = {
local backupdir = '/var/run/awall'
local _actfamilies
local function actfamilies()
if _actfamilies then return _actfamilies end
_actfamilies = {}
for _, family in ipairs(ACTIVE) do
if posix.stat(families[family].procfile) then
table.insert(_actfamilies, family)
else printmsg('Warning: firewall not enabled for '..family) end
end
return _actfamilies
end
function M.isenabled() return #actfamilies() > 0 end
function M.isbuiltin(tbl, chain) return util.contains(builtin[tbl], chain) end
local BaseIPTables = class()
function BaseIPTables:print()
......@@ -54,65 +77,65 @@ function BaseIPTables:dump(dir)
end
end
function BaseIPTables:restore(test)
local disabled = true
for family, params in pairs(families) do
local file = io.open(params.procfile)
if file then
io.close(file)
local pid, stdin, stdout = lpc.run(
params.cmd..'-restore', table.unpack{test and '-t' or nil}
)
stdout:close()
self:dumpfile(family, stdin)
stdin:close()
assert(lpc.wait(pid) == 0)
disabled = false
function BaseIPTables:restorecmd(family, test)
local cmd = {families[family].cmd..'-restore'}
if test then table.insert(cmd, '-t') end
return table.unpack(cmd)
end
elseif test then printmsg('Warning: '..family..' rules not tested') end
function BaseIPTables:restore(test)
for _, family in ipairs(actfamilies()) do
local pid, stdin, stdout = lpc.run(self:restorecmd(family, test))
stdout:close()
self:dumpfile(family, stdin)
stdin:close()
assert(lpc.wait(pid) == 0)
end
if disabled then raise('Firewall not enabled in kernel') end
end
function BaseIPTables:activate()
M.flush()
self:flush()
self:restore(false)
end
function BaseIPTables:test() self:restore(true) end
function BaseIPTables:flush() M.flush() end
M.IPTables = class(BaseIPTables)
function M.IPTables:init()
self.config = {}
setmetatable(self.config,
{__index=function(t, k)
t[k] = {}
setmetatable(t[k], getmetatable(t))
return t[k]
end})
local function nestedtable(levels)
return levels > 0 and setmetatable(
{},
{
__index=function(t, k)
t[k] = nestedtable(getmetatable(t).levels - 1)
return t[k]
end,
levels=levels
}
) or {}
end
self.config = nestedtable(3)
end
function M.IPTables:dumpfile(family, iptfile)
iptfile:write('# '..families[family].file..' generated by awall\n')
local tables = self.config[family]
for i, tbl in sortedkeys(tables) do
for _, tbl in sortedkeys(tables) do
iptfile:write('*'..tbl..'\n')
local chains = tables[tbl]
for i, chain in sortedkeys(chains) do
for _, chain in sortedkeys(chains) do
local policy = '-'
if util.contains(M.builtin[tbl], chain) then
if M.isbuiltin(tbl, chain) then
policy = tbl == 'filter' and 'DROP' or 'ACCEPT'
end
iptfile:write(':'..chain..' '..policy..' [0:0]\n')
end
for i, chain in sortedkeys(chains) do
for i, rule in ipairs(chains[chain]) do
for _, chain in sortedkeys(chains) do
for _, rule in ipairs(chains[chain]) do
iptfile:write('-A '..chain..' '..rule..'\n')
end
end
......@@ -121,6 +144,64 @@ function M.IPTables:dumpfile(family, iptfile)
end
M.PartialIPTables = class(M.IPTables)
function M.PartialIPTables:restorecmd(family, test)
local cmd = {M.PartialIPTables.super(self):restorecmd(family, test)}
table.insert(cmd, '-n')
return table.unpack(cmd)
end
function M.PartialIPTables:dumpfile(family, iptfile)
local tables = self.config[family]
for tbl, chains in pairs(tables) do
local builtins = {}
for chain, _ in pairs(chains) do
if stringy.startswith(chain, 'awall-') then
local base = chain:sub(7, -1)
if M.isbuiltin(tbl, base) then table.insert(builtins, base) end
end
end
for _, chain in ipairs(builtins) do
chains[chain] = {'-j awall-'..chain}
end
end
M.PartialIPTables.super(self):dumpfile(family, iptfile)
end
function M.PartialIPTables:flush()
for _, family in ipairs(actfamilies()) do
local cmd = families[family].cmd
for tbl in io.lines(families[family].procfile) do
if builtin[tbl] then
local pid, stdin, stdout = lpc.run(cmd, '-t', tbl, '-S')
stdin:close()
local chains = {}
local rules = {}
for line in stdout:lines() do
if stringy.startswith(line, '-N awall-') then
table.insert(chains, line:sub(4, -1))
else
local chain, target = line:match('^%-A (%u+) %-j (awall%-%u+)$')
if chain then table.insert(rules, {chain, '-j', target}) end
end
end
stdout:close()
assert(lpc.wait(pid) == 0)
local function exec(...)
assert(util.execute(cmd, '-t', tbl, table.unpack{...}) == 0)
end
for _, rule in ipairs(rules) do exec('-D', table.unpack(rule)) end
for _, opt in ipairs{'-F', '-X'} do
for _, chain in ipairs(chains) do exec(opt, chain) end
end
end
end
end
end
local Current = class(BaseIPTables)
function Current:dumpfile(family, iptfile)
......@@ -142,7 +223,7 @@ end
function M.backup()
mkdir(backupdir)
posix.mkdir(backupdir)
Current():dump(backupdir)
end
......@@ -150,16 +231,13 @@ function M.revert() Backup():activate() end
function M.flush()
local empty = M.IPTables()
for family, params in pairs(families) do
local success, lines = pcall(io.lines, params.procfile)
if success then
for tbl in lines do
if M.builtin[tbl] then
for i, chain in ipairs(M.builtin[tbl]) do
empty.config[family][tbl][chain] = {}
end
else printmsg('Warning: not flushing unknown table: '..tbl) end
end
for _, family in pairs(actfamilies()) do
for tbl in io.lines(families[family].procfile) do
if builtin[tbl] then
for _, chain in ipairs(builtin[tbl]) do
empty.config[family][tbl][chain] = {}
end
else printmsg('Warning: not flushing unknown table: '..tbl) end
end
end
empty:restore(false)
......
--[[
Base data model for Alpine Wall
Copyright (C) 2012-2017 Kaarle Ritvanen
Copyright (C) 2012-2020 Kaarle Ritvanen
See LICENSE file for license details
]]--
......@@ -10,11 +10,11 @@ local M = {}
local loadclass = require('awall').loadclass
M.class = require('awall.class')
local FAMILIES = require('awall.family').ALL
local resolvelist = require('awall.host').resolvelist
local builtin = require('awall.iptables').builtin
local isbuiltin = require('awall.iptables').isbuiltin
local optfrag = require('awall.optfrag')
local FAMILIES = optfrag.FAMILIES
local combinations = optfrag.combinations
local prune = optfrag.prune
......@@ -100,9 +100,9 @@ function M.ConfigObject:trules() return {} end
function M.ConfigObject:info()
local rules = {}
for _, trule in ipairs(self:trules()) do
local loc = optfrag.location(trule)
local loc = self.context:ofragloc(trule)
table.insert(
setdefault(rules, loc, {}), {' '..loc, optfrag.command(trule)}
setdefault(rules, loc, {}), {' '..loc, self.context:ofragcmd(trule)}
)
end
......@@ -125,7 +125,7 @@ function M.Zone:optfrags(dir)
local aopts = nil
if self.addr then
aopts = {}
for _, addr in resolvelist(self.addr) do
for _, addr in resolvelist(self.addr, self) do
table.insert(
aopts,
{family=addr[1], [aprop]=addr[2], match='-'..aopt..' '..addr[2]}
......@@ -591,7 +591,7 @@ function M.Rule:trules()
end
ofrags = filter(
combinations(ofrags, optfrag.FAMILYFRAGS),
combinations(ofrags, optfrag.FAMILIES),
function(r) return self:trulefilter(r) end
)
......@@ -602,8 +602,9 @@ end
function M.Rule:customtarget()
if self.action then
local as = self.action:sub(1, 1)
if as == as:upper() or startswith(self.action, 'custom:') then
if util.startswithupper(self.action) or startswith(
self.action, 'custom:'
) then
return self.action
end
end
......@@ -619,10 +620,7 @@ function M.Rule:convertchains(ofrags)
local res = {}
for _, ofrag in ipairs(ofrags) do
if contains(builtin[self:table()], ofrag.chain) then
table.insert(res, ofrag)
if isbuiltin(self:table(), ofrag.chain) then table.insert(res, ofrag)
else
local ofs, recursive
if ofrag.chain == 'PREROUTING' then
......@@ -735,14 +733,14 @@ function M.Maskable:recentmask(name)
for i = 0, 3 do
if len <= i * 8 then octet = 0
elseif len > i * 8 + 7 then octet = 255
else octet = 256 - 2^(8 - len % 8) end
else octet = 256 - math.floor(2^(8 - len % 8)) end
mask = util.join(mask, '.', octet)
end
elseif family == 'inet6' then
while len > 0 do
if #mask % 5 == 4 then mask = mask..':' end
mask = mask..('%x'):format(16 - 2^math.max(0, 4 - len))
mask = mask..('%x'):format(16 - math.floor(2^math.max(0, 4 - len)))
len = len - 4
end
while #mask % 5 < 4 do mask = mask..'0' end
......
--[[
Filter module for Alpine Wall
Copyright (C) 2012-2017 Kaarle Ritvanen
Copyright (C) 2012-2019 Kaarle Ritvanen
See LICENSE file for license details
]]--
local loadclass = require('awall').loadclass
local FAMILIES = require('awall.family').ALL
local resolve = require('awall.host').resolve
local model = require('awall.model')
local class = model.class
local Rule = model.Rule
local optfrag = require('awall.optfrag')
local combinations = optfrag.combinations
local combinations = require('awall.optfrag').combinations
local util = require('awall.util')
local contains = util.contains
......@@ -100,7 +100,34 @@ local TranslatingRule = class(Rule)
function TranslatingRule:init(...)
TranslatingRule.super(self):init(...)
if type(self.dnat) == 'string' then self.dnat = {addr=self.dnat} end
if self.dnat then
if self.ipset then
self:error('dnat and ipset options cannot be used simultaneously')
end
if type(self.dnat) == 'string' then self.dnat = {addr=self.dnat} end
if self.dnat.addr:find('/') then
self:error('DNAT target cannot be a network address')
end
local dnataddr
for _, addr in ipairs(resolve(self.dnat.addr, self)) do
if addr[1] == 'inet' then
if dnataddr then
self:error(
self.dnat.addr..' resolves to multiple IPv4 addresses'
)
end
dnataddr = addr[2]
end
end
if not dnataddr then
self:error(self.dnat.addr..' does not resolve to any IPv4 address')
end
self.dnat.addr = dnataddr
end
end
function TranslatingRule:destoptfrags()
......@@ -274,34 +301,12 @@ function Filter:extratrules()
if self['no-track'] then
self:error('dnat option not allowed with no-track')
end
if self.ipset then
self:error('dnat and ipset options cannot be used simultaneously')
end
if self.dnat.addr:find('/') then
self:error('DNAT target cannot be a network address')
end
local dnataddr
for i, addr in ipairs(resolve(self.dnat.addr, self)) do
if addr[1] == 'inet' then
if dnataddr then
self:error(
self.dnat.addr..' resolves to multiple IPv4 addresses'
)
end
dnataddr = addr[2]
end
end
if not dnataddr then
self:error(self.dnat.addr..' does not resolve to any IPv4 address')
end
extrarules(
'dnat',
'dnat',
{
update={['to-addr']=dnataddr, ['to-port']=self.dnat.port},
update={['to-addr']=self.dnat.addr, ['to-port']=self.dnat.port},
discard='out'
}
)
......@@ -444,7 +449,7 @@ local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}
local function stateful(config)
local res = {}
for _, family in ipairs(optfrag.FAMILIES) do
for _, family in ipairs(FAMILIES) do
local er = combinations(
fchains,
......
......@@ -54,29 +54,31 @@ function Log:optfrags()
local targets = {}
if mode then
local optmap = {
log={level='level', prefix='prefix'},
nflog={
group='group',
prefix='prefix',
range='size',
threshold='threshold'
},
ulog={
group='nlgroup',
prefix='prefix',
range='cprange',
threshold='qthreshold'
local optmap = (
{
log={level='level', prefix='prefix'},
nflog={
group='group',
prefix='prefix',
range='size',
threshold='threshold'
},
ulog={
group='nlgroup',
prefix='prefix',
range='cprange',
threshold='qthreshold'
}
}
}
if not optmap[mode] then self:error('Invalid logging mode: '..mode) end
)[mode]
if not optmap then self:error('Invalid logging mode: '..mode) end
local target = mode:upper()
for s, t in pairs(optmap[mode]) do
for _, s in util.sortedkeys(optmap) do
local value = self[s]
if value then
if s == 'prefix' then value = util.quote(value) end
target = target..' --'..mode..'-'..t..' '..value
target = target..' --'..mode..'-'..optmap[s]..' '..value
end
end
......@@ -85,7 +87,7 @@ function Log:optfrags()
)
end
for _, addr in resolvelist(self.mirror) do
for _, addr in resolvelist(self.mirror, self) do
table.insert(targets, {family=addr[1], target='TEE --gateway '..addr[2]})
end
......
--[[
Packet marking module for Alpine Wall
Copyright (C) 2012-2017 Kaarle Ritvanen
Copyright (C) 2012-2019 Kaarle Ritvanen
See LICENSE file for license details
]]--
......@@ -40,7 +40,7 @@ end
local function restoremark(config)
if list(config['route-track'])[1] then
return combinations(
optfrag.FAMILYFRAGS,
optfrag.FAMILIES,
{{chain='OUTPUT'}, {chain='PREROUTING'}},
{
{
......
--[[
IPSet-based masquerading module for Alpine Wall
Copyright (C) 2012-2016 Kaarle Ritvanen
Copyright (C) 2012-2020 Kaarle Ritvanen
See LICENSE file for license details
]]--
......@@ -15,12 +15,12 @@ return {
table='nat',
chain='POSTROUTING',
match='-m set --match-set awall-masquerade src',
target='awall-masquerade'
target='masquerade'
},
{
family='inet',
table='nat',
chain='awall-masquerade',
chain='masquerade',
match='-m set ! --match-set awall-masquerade dst',
target='MASQUERADE'
}
......
--[[
Transparent proxy module for Alpine Wall
Copyright (C) 2012-2017 Kaarle Ritvanen
Copyright (C) 2012-2019 Kaarle Ritvanen
See LICENSE file for license details
]]--
......@@ -59,7 +59,7 @@ local function divert(config)
ofrags,
{chain='PREROUTING', match='-m socket', target='divert'}
)
return combinations(optfrag.FAMILYFRAGS, {{table='mangle'}}, ofrags)
return combinations(optfrag.FAMILIES, {{table='mangle'}}, ofrags)
end
end
......
--[[
Option fragment module for Alpine Wall
Copyright (C) 2012-2017 Kaarle Ritvanen
Copyright (C) 2012-2020 Kaarle Ritvanen
See LICENSE file for license details
]]--
local M = {}
local FAMILIES = require('awall.family').ALL
local util = require('awall.util')
local map = util.map
......@@ -14,8 +16,7 @@ local function ffrags(families)
return map(families, function(f) return {family=f} end)
end
M.FAMILIES = {'inet', 'inet6'}
M.FAMILYFRAGS = ffrags(M.FAMILIES)
M.FAMILIES = ffrags(FAMILIES)
function M.combinations(of1, ...)
local arg = {...}
......@@ -85,11 +86,4 @@ function M.prune(...)
)
end
function M.location(of) return of.family..'/'..of.table..'/'..of.chain end
function M.command(of)
return (of.match and of.match..' ' or '')..
(of.target and '-j '..of.target or '')
end
return M
--[[
Utility module for Alpine Wall
Copyright (C) 2012-2017 Kaarle Ritvanen
Copyright (C) 2012-2020 Kaarle Ritvanen
See LICENSE file for license details
]]--
local M = {}
local lpc = require('lpc')
function M.split(s, sep)
if s == '' then return {} end
local res = {}
......@@ -118,6 +121,12 @@ function M.join(a, sep, b)
end
function M.startswithupper(s)
if s == '' then return false end
local c = s:sub(1, 1)
return c == c:upper()
end
function M.quote(s) return '"'..s:gsub('(["\\])', '\\%1')..'"' end
......@@ -150,4 +159,14 @@ function M.printtabular(tbl) M.printtabulars({tbl}) end
function M.printmsg(msg) io.stderr:write(msg..'\n') end
function M.run(...)
local pid, stdin, stdout = lpc.run(...)
stdin:close()
stdout:close()
return pid
end
function M.execute(...) return lpc.wait(M.run(...)) end
return M
{
"before": "%defaults",
"variable": { "awall_tproxy_mark": 1 },
"variable": { "awall_dedicated_chains": false, "awall_tproxy_mark": 1 },
"log": { "_default": { "limit": 1 } }
}
......@@ -9,12 +9,12 @@
"bgp": { "proto": "tcp", "port": 179 },
"dhcp": { "family": "inet", "proto": "udp", "port": [ 67, 68 ] },
"discard": [
{ "proto": "udp", "port": 9 },
{ "proto": "tcp", "port": 9 }
{ "proto": "tcp", "port": 9 },
{ "proto": "udp", "port": 9 }
],
"dns": [
{ "proto": "udp", "port": 53 },
{ "proto": "tcp", "port": 53 }
{ "proto": "tcp", "port": 53 },
{ "proto": "udp", "port"