From 7d9c43916b0600ac4879dfe9793eab807a83ab2b Mon Sep 17 00:00:00 2001 From: Kaarle Ritvanen Date: Mon, 10 Mar 2014 22:45:18 +0200 Subject: rename ACF2 to Alpine Configurator (aconf) --- aconf/modules/demo-awall.lua | 149 ++++++++++++++++++++++ aconf/modules/demo-generic.lua | 14 ++ aconf/modules/network.lua | 281 +++++++++++++++++++++++++++++++++++++++++ aconf/modules/openssh.lua | 28 ++++ 4 files changed, 472 insertions(+) create mode 100644 aconf/modules/demo-awall.lua create mode 100644 aconf/modules/demo-generic.lua create mode 100644 aconf/modules/network.lua create mode 100644 aconf/modules/openssh.lua (limited to 'aconf/modules') diff --git a/aconf/modules/demo-awall.lua b/aconf/modules/demo-awall.lua new file mode 100644 index 0000000..7d28989 --- /dev/null +++ b/aconf/modules/demo-awall.lua @@ -0,0 +1,149 @@ +--[[ +Copyright (c) 2012-2014 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = require('aconf.model') +local object = require('aconf.object') + + +local Direction = object.class(M.String) +function Direction:init(params) + if not params then params = {} end + params.choice = {'in', 'out'} + object.super(self, Direction):init(params) +end + + +-- TODO reference types? + +local IPSet = M.new() +-- TODO choices +IPSet.type = M.String{required=true} +IPSet.family = M.String{required=true, choice={'inet', 'inet6'}} +-- TODO only for bitmaps +IPSet.range = M.Range{type=M.net.IPv4Address} + +local Service = M.new() +Service.proto = M.String{required=true, ui_name='Protocol'} +Service.port = M.Set{type=M.Range{type=M.net.Port}} +Service.icmp_type = M.String{ui_name='ICMP type'} +Service.ct_helper = M.String{ui_name='Connection tracking helper'} + +-- TODO fw zone + +local Zone = M.new() +Zone.iface = M.Set{type=M.String, ui_name='Interfaces'} +Zone.addr = M.Set{type=M.String, ui_name='Addresses'} +Zone.route_back = M.Boolean{default=false} + +local LogClass = M.new() +LogClass.mode = M.String{ + required=true, default='log', choice={'log', 'nflog', 'ulog'} +} +LogClass.every = M.Integer{ui_name='Sampling frequency'} +LogClass.limit = M.Integer +LogClass.prefix = M.String +LogClass.probability = M.Number +LogClass.group = M.Integer +LogClass.range = M.Integer +LogClass.threshold = M.Integer + +local IPSetReference = M.new() +IPSetReference.name = M.Reference{scope='/awall/ipset', required=true} +IPSetReference.args = M.List{ + type=Direction, required=true, ui_name='Arguments' +} + +local Rule = M.new() +Rule['in'] = M.Set{ + type=M.Reference{scope='/awall/zone'}, ui_name='Ingress zones' +} +Rule.out = M.Set{ + type=M.Reference{scope='/awall/zone'}, ui_name='Egress zones' +} +Rule.src = M.Set{type=M.String, ui_name='Sources'} +Rule.dest = M.Set{type=M.String, ui_name='Destinations'} +Rule.ipset = M.Model{model=IPSetReference, ui_name='IP set'} +Rule.ipsec = Direction{ui_name='Require IPsec'} +Rule.service = M.Set{type=M.Reference{scope='/awall/service'}} +Rule.action = M.String{choice={'accept'}} + + +local PacketLogRule = M.new(Rule) +PacketLogRule.log = M.Reference{scope='../../log', ui_name='Log class'} + +-- TODO no service field +local PolicyRule = M.new(PacketLogRule) +PolicyRule.action = M.String{ + required=true, choice={'accept', 'drop', 'reject', 'tarpit'} +} + +local Limit = M.new() +Limit.count = M.Integer +Limit.interval = M.Integer +Limit.log = M.Reference{scope='../../../log'} + +local FilterRule = M.new(PolicyRule) +FilterRule.conn_limit = M.Model{model=Limit, ui_name='Connection limit'} +FilterRule.flow_limit = M.Model{model=Limit, ui_name='Flow limit'} +FilterRule.dnat = M.net.IPv4Address{ui_name='DNAT target'} +FilterRule.no_track = M.Boolean{default=false, ui_name='CT bypass'} +FilterRule.related = M.List{type=Rule, ui_name='Related packet rules'} + +local DivertRule = M.new(Rule) +DivertRule.to_port = M.Range{type=M.net.Port, ui_name='Target port'} + +local NATRule = M.new(DivertRule) +NATRule.to_addr = M.Range{type=M.net.IPv4Address, ui_name='Target address'} + +local MarkRule = M.new(Rule) +MarkRule.mark = M.Integer{required=true} + +local ClampMSSRule = M.new(Rule) +ClampMSSRule.mss = M.Integer{ui_name='MSS'} + + +local AWall = M.new() +-- TODO differentiate lists? +AWall.service = M.Collection{type=M.List{type=Service}} +AWall.zone = M.Collection{type=Zone} +AWall.log = M.Collection{ + type=LogClass, ui_name='Log classes', ui_member='Log class' +} +AWall.policy = M.List{type=PolicyRule, ui_name='Policies', ui_member='Policy'} +AWall.packet_log = M.List{ + type=PacketLogRule, ui_name='Logging', ui_member='Logging rule' +} +AWall.filter = M.List{type=FilterRule} +AWall.dnat = M.List{type=NATRule, ui_name='DNAT', ui_member='DNAT rule'} +AWall.snat = M.List{type=NATRule, ui_name='SNAT', ui_member='SNAT rule'} +AWall.mark = M.List{ + type=MarkRule, ui_name='Packet marking', ui_member='Packet marking rule' +} +AWall.route_track = M.List{ + type=MarkRule, ui_name='Route tracking', ui_member='Route tracking rule' +} +AWall.tproxy = M.List{ + type=DivertRule, + ui_name='Transparent proxy', + ui_member='Transparent proxy rule' +} +AWall.clamp_mss = M.List{ + type=ClampMSSRule, ui_name='MSS clamping', ui_member='MSS clamping rule' +} +AWall.no_track = M.List{ + type=Rule, ui_name='CT bypass', ui_member='Connection tracking bypass rule' +} +AWall.ipset = M.Collection{type=IPSet, ui_name='IP sets', ui_member='IP set'} + +M.register( + 'awall', + AWall, + { + addr='/json'..require('posix').getcwd()..'/config/awall.json', + ui_name='Alpine Wall' + } +) + +M.permission.defaults('/awall') diff --git a/aconf/modules/demo-generic.lua b/aconf/modules/demo-generic.lua new file mode 100644 index 0000000..4e602fa --- /dev/null +++ b/aconf/modules/demo-generic.lua @@ -0,0 +1,14 @@ +--[[ +Copyright (c) 2012-2014 Kaarle Ritvanen +See LICENSE file for license details +--]] + +-- provided as an example, to be removed from production version + +local M = require('aconf.model') + +M.register('proc', M.Mixed, {addr='/files/proc', ui_name='/proc'}) +M.permission.defaults('/proc') + +M.register('augeas', M.Mixed, {addr='/augeas'}) +M.permission.defaults('/augeas') diff --git a/aconf/modules/network.lua b/aconf/modules/network.lua new file mode 100644 index 0000000..ad772ed --- /dev/null +++ b/aconf/modules/network.lua @@ -0,0 +1,281 @@ +--[[ +Copyright (c) 2012-2014 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = require('aconf.model') + +local posix = require('posix') + + +local Host = M.new() +Host.address = M.net.IPAddress{addr='ipaddr'} +Host.canonical = M.String{ui_name='Canonical name'} +Host.alias = M.Set{ + type=M.String, + addr='alias/#', + ui_name='Aliases', + ui_member='Alias', + detail=false +} + +local Resolv = M.new() +Resolv.servers = M.List{ + type=M.net.IPAddress, addr='nameserver/#', widget='inline' +} +Resolv.search_domains = M.List{ + type=M.String, addr='search/domain/#', widget='inline' +} + + +local iface_aug_addr = '/augeas/etc/network/interfaces' +local iface_sys_dir = '/sys/class/net' +local vlan_pattern = '^(.+)%.(%d+)$' + +for _, trigger in ipairs{ + {phase='pre', action='stop'}, {phase='post', action='start'} +} do + M.trigger( + trigger.phase, + iface_aug_addr, + function() os.execute('rc-service networking '..trigger.action) end + ) +end + +M.defer(iface_aug_addr) + + +-- TODO allow multiple addresses of same family + +local IPv4 = M.new() +IPv4.method = M.String{ + required=true, + choice={ + {'dhcp', 'DHCP'}, + {'loopback', enabled=false}, + {'unconfigured', be_value='manual'}, + 'static' + }, + default='unconfigured' +} +IPv4.address = M.net.IPv4Address{ + condition={method='static'}, required=true, cidr=true, mask_addr='netmask' +} +IPv4.gateway = M.net.IPv4Address{condition={method='static'}} + + +local IPv6 = M.new() +IPv6.method = M.String{ + required=true, + choice={ + {'loopback', enabled=false}, {'unconfigured', be_value='manual'}, 'static' + }, + default='unconfigured' +} +IPv6.address = M.net.IPv6Address{ + condition={method='static'}, required=true, cidr=true, mask_addr='netmask' +} +IPv6.gateway = M.net.IPv6Address{condition={method='static'}} + + + +local Interface = M.new() + +function Interface:validate() + if self.status == 'attached' then + for _, version in ipairs{4, 6} do + self['ipv'..version].method = + self.class == 'loopback' and 'loopback' or 'unconfigured' + end + end +end + +function Interface:is_removable() return self.class == 'logical' end + +function Interface:auto_set() + for _, set in M.node.ipairs(self:fetch('../../enabled-ifaces')) do + if M.node.contains(set, self) then return set end + end +end + +function Interface:auto_vlan_tag() + local name = M.node.name(self) + local _, tag = name:match(vlan_pattern) + if tag then return tag end + return name:match('^vlan(%d+)$') +end + +Interface.enabled = M.Boolean{ + compute=function(self, iface) return iface:auto_set() and true or false end, + store=function(self, iface, value) + local set = iface:auto_set() + if value and not set then + M.node.insert(iface:fetch('../../enabled-ifaces/1'), iface) + elseif not value and set then set[iface] = nil end + end +} + +Interface.class = M.String{ + compute=function(self, iface, txn) + local name = M.node.name(iface) + if name == 'lo' then return 'loopback' end + + local saddr = M.path.rawjoin('/files', iface_sys_dir, name, '.') + if not txn:get(saddr) then return 'logical' end + + for _, addr in ipairs{ + {saddr, 'bonding'}, + {saddr, 'bridge'}, + {'/files/proc/net/vlan', name} + } do + if txn:get(M.path.join(unpack(addr))) then + return 'logical' + end + end + return 'physical' + end, + choice={'loopback', 'logical', 'physical'} +} + +Interface.type = M.String{ + condition={class='logical'}, + compute=function(self, iface) + if #iface.slaves > 0 then return 'bond' end + if #iface.ports > 0 then return 'bridge' end + if iface.vlan_tag then return 'vlan' end + end, + editable=function(self, iface) return not iface:auto_vlan_tag() end, + required=true, + choice={'bond', 'bridge', {'vlan', 'VLAN'}} +} + +Interface.status = M.String{ + visible=false, + compute=function(self, obj) + if obj.class == 'loopback' then return 'attached' end + + for _, iface in M.node.pairs(M.node.parent(obj)) do + if ( + iface.type == 'bond' and M.node.contains(iface.slaves, obj) + ) or ( + iface.type == 'bridge' and M.node.contains(iface.ports, obj) + ) then + return 'attached' + end + + if iface.type == 'vlan' and iface.trunk == obj then + return 'configured' + end + end + + for _, version in ipairs{4, 6} do + if obj['ipv'..version].method ~= 'unconfigured' then + return 'configured' + end + end + + return 'detached' + end +} + +Interface.slaves = M.Set{ + condition={type='bond'}, + type=M.Reference{scope='../..', filter={status='detached'}}, + required=true, + addr='@family/link/@method/none/bond-slaves' +} +Interface.ports = M.Set{ + condition={type='bridge'}, + type=M.Reference{scope='../..', filter={status='detached'}}, + required=true, + addr='@family/link/@method/none/bridge-ports' +} +-- TODO do not allow VLAN creation for non-existent interfaces +Interface.trunk = M.Reference{ + condition={type='vlan'}, + compute=function(self, iface) + local trunk = M.node.name(iface):match(vlan_pattern) + if trunk and iface:fetch('..')[trunk] then return trunk end + end, + required=true, + scope='..', + filter={status={'detached', 'configured'}}, + addr='@family/link/@method/none/vlan-raw-device' +} +-- TODO ensure that (trunk, tag) is unique +Interface.vlan_tag = M.Integer{ + condition={type='vlan'}, + compute='auto_vlan_tag', + required=true, + min=0, + max=4095, + addr='@family/link/@method/none/vlan-id', + ui_name='VLAN tag' +} + +Interface.ipv4 = M.Model{ + model=IPv4, + condition={status={'detached', 'configured'}}, + create=true, + addr='@family/inet', + ui_name='IPv4 configuration', + widget='inline' +} +Interface.ipv6 = M.Model{ + model=IPv6, + condition={status={'detached', 'configured'}}, + create=true, + addr='@family/inet6', + ui_name='IPv6 configuration', + widget='inline' +} + +Interface.stats = M.Collection{ + type=M.Number{editable=false}, + editable=false, + addr=function(path) + return M.path.join( + '/files/sys/class/net', M.path.name(path), 'statistics' + ) + end, + ui_name='Statistics', + ui_member='', + widget='inline' +} + + +local Net = M.new() +Net.host_name = M.String{addr='/augeas/etc/hostname/hostname'} +Net.hosts = M.List{type=Host, addr='/augeas/etc/hosts'} +Net.resolver = M.Model{ + model=Resolv, addr='/augeas/etc/resolv.conf', ui_name='DNS resolver' +} + +Net.enabled_ifaces = M.List{ + type=M.Set{type=M.Reference{scope='../../interfaces', on_delete='set-null'}}, + visible=false, + addr=iface_aug_addr..'/auto/#' +} +Net.interfaces = M.Collection{ + type=Interface, addr=iface_aug_addr..'/iface/@', widget='inline' +} + +M.register('net', Net, {ui_name='Network'}) +M.permission.defaults('/net') + +return function(txn) + local ifaces = txn:fetch('/net/interfaces') + for _, name in ipairs(posix.dir(iface_sys_dir)) do + if not ifaces[name] and posix.stat( + M.path.join(iface_sys_dir, name), 'type' + ) == 'link' then + ifaces[name] = {} + if ifaces[name].class == 'logical' then ifaces[name] = nil + else + for _, version in ipairs{4, 6} do + ifaces[name]['ipv'..version].method = 'unconfigured' + end + end + end + end + end diff --git a/aconf/modules/openssh.lua b/aconf/modules/openssh.lua new file mode 100644 index 0000000..5ca2544 --- /dev/null +++ b/aconf/modules/openssh.lua @@ -0,0 +1,28 @@ +--[[ +Copyright (c) 2013 Natanael Copa +Copyright (c) 2013-2014 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = require('aconf.model') + +local Sshd = M.service('sshd') +Sshd.root_login = M.Boolean{ + addr='PermitRootLogin', ui_name='Permit root login', default=true +} +Sshd.password_auth = M.Boolean{ + addr='PasswordAuthentication', + ui_name='Password authentication', + default=true +} +Sshd.use_dns = M.Boolean{addr='UseDNS', ui_name='Use DNS', default=true} +Sshd.agent_forward = M.Boolean{ + addr='AllowAgentForwarding', ui_name='Allow agent forwarding', default=true +} + +M.register( + 'sshd', + Sshd, + {addr='/augeas/etc/ssh/sshd_config', ui_name='SSH daemon'} +) +M.permission.defaults('/sshd') -- cgit v1.2.3