Merge pull request #78 from ryanpetrello/linux

Implement a Linux firewall with iptables and ip6tables.
This commit is contained in:
Jordan Tardif 2014-08-13 09:45:44 -07:00
commit a4cadd55c3
22 changed files with 552 additions and 1114 deletions

View File

@ -2,9 +2,8 @@
*Part of the [Akanda Project](https://github.com/dreamhost/akanda).*
Router appliance based upon [OpenBSD](http://www.openbsd.org) and [Packet
Filter](http://www.openbsd.org/faq/pf/). Includes a REST API to monitor,
configure, and manage the router.
A Linux-based L3 software router. Includes a REST API to monitor, configure,
and manage the router.
Akanda routers are recommended to run with 512 MB of RAM and a single vCPU, and
are intended to run within an virtualized L2 overlay to provide complete network

View File

@ -22,7 +22,7 @@ Blueprint for version 1 of the firewall API.
from flask import request
from akanda.router import utils
from akanda.router.drivers import pf
from akanda.router.drivers import iptables
blueprint = utils.blueprint_factory(__name__)
@ -30,86 +30,12 @@ blueprint = utils.blueprint_factory(__name__)
@blueprint.before_request
def get_manager():
request.pf_mgr = pf.PFManager()
request.iptables_mgr = iptables.IPTablesManager()
@blueprint.route('/rules')
def get_rules():
'''
Show loaded firewall rules by pfctl
Show loaded firewall rules by iptables
'''
return request.pf_mgr.get_rules()
@blueprint.route('/states')
def get_states():
'''
Show firewall state table
'''
return request.pf_mgr.get_states()
@blueprint.route('/anchors')
def get_anchors():
'''
Show loaded firewall anchors by pfctl
'''
return request.pf_mgr.get_anchors()
@blueprint.route('/sources')
def get_sources():
'''
Show loaded firewall sources by pfctl
'''
return request.pf_mgr.get_sources()
@blueprint.route('/info')
def get_info():
'''
Show verbose running firewall information
'''
return request.pf_mgr.get_info()
@blueprint.route('/tables')
def get_tables():
'''
Show loaded firewall tables by pfctl
'''
return request.pf_mgr.get_tables()
@blueprint.route('/labels')
@utils.json_response
def get_labels():
'''
Show loaded firewall labels by pfctl
'''
return dict(labels=request.pf_mgr.get_labels())
@blueprint.route('/labels', methods=['POST'])
@utils.json_response
def reset_labels():
'''
Show loaded firewall labels by pfctl and reset the counters
'''
return dict(labels=request.pf_mgr.get_labels(True))
@blueprint.route('/timeouts')
def get_timeouts():
'''
Show firewall connection timeouts
'''
return request.pf_mgr.get_timeouts()
@blueprint.route('/memory')
def get_memory():
'''
Show firewall memory
'''
return request.pf_mgr.get_memory()
return request.iptables_mgr.get_rules()

View File

@ -82,29 +82,3 @@ def configure_gunicorn():
sys.stderr.write('http configured to listen on %s\n' % listen_ip)
except:
sys.stderr.write('Unable to write gunicorn configuration file.')
def configure_default_pf():
"""
"""
mgr = ip.IPManager()
args = {'ifname': mgr.generic_to_host('ge0')}
config = """
ge0 = "%(ifname)s"
set skip on lo
match in all scrub (no-df)
block log (all)
pass proto icmp6 all
pass inet proto icmp icmp-type { echoreq, unreach }
pass proto tcp from $ge0:network to $ge0 port { 22, 5000}
"""
config = textwrap.dedent(config % args).lstrip()
try:
open('/etc/pf.conf', 'w+').write(config)
sys.stderr.write('Default PF rules configured\n')
except:
sys.stderr.write('Unable to write pf configuration file.')

View File

@ -14,27 +14,25 @@
# License for the specific language governing permissions and limitations
# under the License.
import re
SSH = 22
SMTP = 25
DNS = 53
HTTP = 80
BGP = 179
HTTPS = 443
HTTP_ALT = 8080
API_SERVICE = 5000
DHCP = 67
DHCPV6 = 546
NFS_DEVELOPMENT = [111, 1110, 2049, 4045]
MANAGEMENT_PORTS = [SSH, API_SERVICE] # + NFS_DEVELOPMENT
BASE_RULES = [
'set skip on lo',
'match in all scrub (no-df)',
'block log (all)', # FIXME: remove log (all)
'pass proto icmp6 all',
'pass inet proto icmp icmp-type { echoreq, unreach }'
]
# destination address for AWS compliant metadata guests
METADATA_DEST_ADDRESS = '169.254.169.254'
@ -46,4 +44,4 @@ RUG_META_PORT = 9697
def internal_metadata_port(ifname):
return BASE_METADATA_PORT + int(ifname[2:])
return BASE_METADATA_PORT + int(re.sub('[a-zA-Z]', '', ifname))

View File

@ -24,7 +24,7 @@ from akanda.router import utils
LOG = logging.getLogger(__name__)
CONF_PATH = '/etc/bird6.conf'
CONF_PATH = '/etc/bird/bird6.conf'
BIRD = '/usr/local/sbin/bird'
BIRDC = '/usr/local/bin/birdc'
DEFAULT_AREA = 0

View File

@ -142,6 +142,7 @@ class IPManager(base.Manager):
for item in (next_set - prev_set):
self.sudo(*fmt_args_add(item))
self.up(interface)
for item in (prev_set - next_set):
self.sudo(*fmt_args_delete(item))

316
akanda/router/drivers/iptables.py Executable file
View File

@ -0,0 +1,316 @@
# Copyright 2014 DreamHost, LLC
#
# Author: DreamHost, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import re
import itertools
from akanda.router.drivers import base
from akanda.router.models import Network
from akanda.router import defaults, utils
class Rule(object):
def __init__(self, rule, ip_version=None):
self.rule = rule
self.ip_version = ip_version
def __str__(self):
return self.rule
@property
def for_v4(self):
return self.ip_version in (None, 4)
@property
def for_v6(self):
return self.ip_version in (None, 6)
class IPTablesManager(base.Manager):
"""
"""
def save_config(self, config, interface_map):
'''
Save iptables-persistent firewall rules to disk.
:param config: The akanda configuration to save to disk
:type config: akanda.rug.models.Configuration
:param interface_map: A mapping of virtual ('ge0') to physical ('eth0')
interface names
:type interface_map: dict
'''
rules = itertools.chain(
self._build_filter_table(config),
self._build_nat_table(config)
)
for version, rules in zip((4, 6), itertools.tee(rules)):
data = '\n'.join(map(
str,
[r for r in rules if getattr(r, 'for_v%s' % version)]
))
# Map virtual interface names
real_name = interface_map.get('ge0')[:-1]
ifname_re = '\-(?P<flag>i|o)(?P<ws>[\s!])(?P<not>!?)(?P<if>ge)(?P<no>\d+)' # noqa
ifname_sub = r'-\g<flag>\g<ws>\g<not>%s\g<no>' % real_name
data = re.sub(ifname_re, ifname_sub, data) + '\n'
utils.replace_file('/tmp/ip%stables.rules' % version, data)
utils.execute([
'mv',
'/tmp/ip%stables.rules' % version,
'/etc/iptables/rules.v%s' % version
], self.root_helper)
def restart(self):
'''
Reload firewall rules via iptables-persistent
'''
utils.execute(
['/etc/init.d/iptables-persistent', 'restart'],
self.root_helper
)
def get_rules(self):
'''
Return the output of `iptables` and `ip6tables`.
This function is used by akanda-rug -> HTTP as a test for "router
aliveness".
:rtype: str
'''
v4 = utils.execute(['iptables', '-L', '-n'])
v6 = utils.execute(['ip6tables', '-L', '-n'])
return v4 + v6
def get_external_network(self, config):
'''
Returns the external network
:rtype: akanda.router.models.Interface
'''
return self.networks_by_type(config, Network.TYPE_EXTERNAL)[0]
def networks_by_type(self, config, type):
'''
Returns the external network
:rtype: akanda.router.models.Interface
'''
return filter(lambda n: n.network_type == type, config.networks)
def _build_filter_table(self, config):
'''
Build a list of iptables and ip6tables rules to be written to disk.
:param config: the akanda configuration object:
:type config: akanda.router.models.Configuration
:param rules: the list of rules to append to
:type rules: a list of akanda.router.drivers.iptables.Rule objects
'''
return itertools.chain(
self._build_default_filter_rules(),
self._build_management_filter_rules(config),
self._build_internal_network_filter_rules(config)
)
def _build_default_filter_rules(self):
'''
Build rules for default filter policies and ICMP handling
'''
return (
Rule('*filter'),
Rule(':INPUT DROP [0:0]'),
Rule(':FORWARD ACCEPT [0:0]'),
Rule(':OUTPUT ACCEPT [0:0]'),
Rule('-A INPUT -i lo -j ACCEPT'),
Rule(
'-A INPUT -p icmp --icmp-type echo-request -j ACCEPT',
ip_version=4
),
Rule(
'-A INPUT -p icmpv6 -j ACCEPT',
ip_version=6
)
)
def _build_management_filter_rules(self, config):
'''
Add rules specific to the management network, like allowances for SSH,
the HTTP API, and metadata proxying on the management interface.
'''
rules = []
for network in self.networks_by_type(config, Network.TYPE_MANAGEMENT):
# Allow established mgt traffic
rules.append(Rule(
'-A INPUT -i %s -m state --state RELATED,ESTABLISHED -j ACCEPT'
% network.interface.ifname
))
# Open SSH, the HTTP API (5000) and the Nova metadata proxy (9697)
for port in (
defaults.SSH, defaults.API_SERVICE, defaults.RUG_META_PORT
):
rules.append(Rule(
'-A INPUT -i %s -p tcp -m tcp --dport %s -j ACCEPT' % (
network.interface.ifname,
port
), ip_version=6
))
# Disallow any other management network traffic
rules.append(Rule('-A INPUT -i !%s -d %s -j DROP' % (
network.interface.ifname,
network.interface.first_v6
), ip_version=6))
return rules
def _build_internal_network_filter_rules(self, config):
'''
Add rules specific to private tenant networks.
'''
rules = []
ext_if = self.get_external_network(config).interface
for network in self.networks_by_type(config, Network.TYPE_INTERNAL):
for version, address, dhcp_port in (
(4, network.interface.first_v4, defaults.DHCP),
(6, network.interface.first_v6, defaults.DHCPV6)
):
if address:
# Basic state-matching rules. Allows packets related to a
# pre-established session to pass.
rules.append(Rule(
'-A FORWARD -d %s -o %s -m state '
'--state RELATED,ESTABLISHED -j ACCEPT' % (
address,
network.interface.ifname
), ip_version=version
))
# Allow DHCP
rules.append(Rule(
'-A INPUT -i %s -p udp -m udp --dport %s -j ACCEPT' % (
network.interface.ifname,
dhcp_port
), ip_version=version
))
rules.append(Rule(
'-A INPUT -i %s -p tcp -m tcp --dport %s -j ACCEPT' % (
network.interface.ifname,
dhcp_port
), ip_version=version
))
rules.append(Rule(
'-A INPUT -i %s -j ACCEPT' % network.interface.ifname
))
rules.append(Rule(
'-A INPUT -i %s -m state '
'--state RELATED,ESTABLISHED -j ACCEPT' % ext_if.ifname
))
rules.append(Rule('COMMIT'))
return rules
def _build_nat_table(self, config):
'''
Add rules for generic v4 NAT for the internal tenant networks
'''
rules = [
Rule('*nat', ip_version=4),
Rule(':PREROUTING ACCEPT [0:0]', ip_version=4),
Rule(':INPUT ACCEPT [0:0]', ip_version=4),
Rule(':OUTPUT ACCEPT [0:0]', ip_version=4),
Rule(':POSTROUTING ACCEPT [0:0]', ip_version=4),
]
rules.extend(self._build_floating_ips(config))
rules.extend(self._build_v4_nat(config))
rules.append(Rule('COMMIT', ip_version=4))
return rules
def _build_v4_nat(self, config):
rules = []
ext_if = self.get_external_network(config).interface
for network in self.networks_by_type(config, Network.TYPE_INTERNAL):
# NAT for IPv4
ext_v4 = sorted(
a.ip for a in ext_if._addresses if a.version == 4
)[0]
rules.append(Rule(
'-A POSTROUTING -o %s -j SNAT --to %s' % (
network.interface.ifname,
str(ext_v4)
), ip_version=4
))
# Forward metadata requests on the management interface
rules.append(Rule(
'-A PREROUTING -i %s -d %s -p tcp -m tcp '
'--dport %s -j DNAT --to-destination %s:%s' % (
network.interface.ifname,
defaults.METADATA_DEST_ADDRESS,
defaults.HTTP,
network.interface.first_v4,
defaults.internal_metadata_port(
network.interface.ifname
)
), ip_version=4
))
# Add a masquerade catch-all for VMs without floating IPs
rules.append(Rule(
'-A POSTROUTING -o %s -j MASQUERADE' % ext_if.ifname,
ip_version=4
))
return rules
def _build_floating_ips(self, config):
'''
Add rules for neutron FloatingIPs.
'''
rules = []
ext_if = self.get_external_network(config).interface
# Route floating IP addresses
for fip in self.get_external_network(config).floating_ips:
rules.append(Rule('-A POSTROUTING -o %s -s %s -j SNAT --to %s' % (
ext_if.ifname,
fip.fixed_ip,
fip.floating_ip
), ip_version=4))
rules.append(Rule(
'-A PREROUTING -i %s -d %s -j DNAT --to-destination %s' % (
ext_if.ifname,
fip.floating_ip,
fip.fixed_ip
), ip_version=4
))
return rules

View File

@ -1,120 +0,0 @@
# Copyright 2014 DreamHost, LLC
#
# Author: DreamHost, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from akanda.router.drivers import base
from akanda.router.utils import execute, replace_file
from akanda.router import models
class PFManager(base.Manager):
"""
"""
EXECUTABLE = '/sbin/pfctl'
def _show(self, flag, prefix=''):
return self.sudo('-%ss%s' % (prefix, flag))
def get_rules(self):
# -sr
return self._show('r')
def get_states(self):
# -ss
return self._show('s')
def get_anchors(self):
# -sA
return self._show('A')
def get_sources(self):
# -sS
return self._show('S')
def get_info(self):
# -si
return self._show('i')
def get_tables(self):
# -sT
return self._show('T')
def get_labels(self, reset=False):
prefix = 'vz' if reset else ''
data = self._show('l', prefix)
return [self._parse_label_line(l)
for l in data.strip().split('\n') if l]
def get_timeouts(self):
# -st
return self._show('t')
def get_memory(self):
# -sm
return self._show('m')
def update_conf(self, conf_data):
replace_file('/tmp/pf.conf', conf_data)
execute(['mv', '/tmp/pf.conf', '/etc/pf.conf'], self.root_helper)
try:
self.sudo('-f', '/etc/pf.conf')
except RuntimeError as e:
raise RuntimeError(unicode(e) + '\n' + conf_data)
def _parse_label_line(self, line):
parts = line.strip().split()
values = [int(i) for i in parts[1:]]
return {'name': parts[0],
'total_packets': values[2],
'total_bytes': values[3],
'packets_in': values[4],
'bytes_in': values[5],
'packets_out': values[6],
'bytes_out': values[7]}
class TableManager(base.Manager):
"""
"""
EXECUTABLE = '/sbin/pfctl'
def __init__(self, name):
self.name = name
def add(self, cidr):
self._sudo('-t', self.name, '-T', 'add', str(cidr))
def delete(self, cidr):
self._sudo('-t', self.name, '-T', 'delete', str(cidr))
def show(self):
return self._sudo('-t', self.name, '-T', self.name)
def _parse_pf_rules(data, filters=None):
'''
Parser for pfctl -sr
'''
retval = []
return retval
def _parse_pf_rule(line):
'''
Parser for pfctl -sr
'''
retval = {}
return models.PFManager.from_dict(retval)

View File

@ -29,8 +29,8 @@ LOG = logging.getLogger(__name__)
class PingManager(base.Manager):
exe_map = {
4: '/sbin/ping',
6: '/sbin/ping6'
4: '/bin/ping',
6: '/bin/ping6'
}
def __init__(self, root_helper='sudo'):

View File

@ -19,7 +19,7 @@ import os
import re
from akanda.router import models
from akanda.router.drivers import (bird, dnsmasq, ip, metadata, pf, arp)
from akanda.router.drivers import (bird, dnsmasq, ip, metadata, iptables, arp)
class Manager(object):
@ -49,7 +49,7 @@ class Manager(object):
self.update_dhcp()
self.update_metadata()
self.update_bgp_and_radv()
self.update_pf()
self.update_firewall()
self.update_routes(cache)
self.update_arp()
@ -80,11 +80,10 @@ class Manager(object):
mgr.save_config(self.config, self.ip_mgr.generic_mapping)
mgr.restart()
def update_pf(self):
rule_data = self.config.pf_config
rule_data = self._map_virtual_to_real_interfaces(rule_data)
mgr = pf.PFManager()
mgr.update_conf(rule_data)
def update_firewall(self):
mgr = iptables.IPTablesManager()
mgr.save_config(self.config, self.ip_mgr.generic_mapping)
mgr.restart()
def update_routes(self, cache):
mgr = ip.IPManager()

View File

@ -166,7 +166,7 @@ def main():
app = NetworkMetadataProxyHandler(tenant_id,
network_id,
args.config_file)
socket = eventlet.listen(('127.0.0.1', config['listen_port']),
socket = eventlet.listen(('0.0.0.0', config['listen_port']),
backlog=128)
pool.spawn_n(eventlet.wsgi.server, socket, app, custom_pool=pool)

View File

@ -16,13 +16,10 @@
import abc
import os
import re
import netaddr
from akanda.router import defaults
GROUP_NAME_LENGTH = 15
DEFAULT_AS = 64512
@ -187,41 +184,6 @@ class FilterRule(ModelBase):
super(FilterRule, self).__setattr__(name, value)
@property
def pf_rule(self):
retval = [self.action]
if self.direction:
retval.append(self.direction)
if self.interface:
retval.append('on %s' % self.interface)
if self.family:
retval.append(self.family)
if self.protocol:
retval.append('proto %s' % self.protocol)
if self.source or self.source_port:
retval.append('from')
if self.source:
retval.append(self._format_ip_or_table(self.source))
if self.source_port:
retval.append('port %s' % self.source_port)
if (self.destination_interface
or self.destination
or self.destination_port):
retval.append('to')
if self.destination_interface:
retval.append(self.destination_interface)
if self.destination:
retval.append(self._format_ip_or_table(self.destination))
if self.destination_port:
retval.append('port %s' % self.destination_port)
if self.redirect or self.redirect_port:
retval.append('rdr-to')
if self.redirect:
retval.append(str(self.redirect))
if self.redirect_port:
retval.append('port %s' % self.redirect_port)
return ' '.join(retval)
@classmethod
def from_dict(cls, d):
return FilterRule(**d)
@ -239,18 +201,6 @@ class Anchor(ModelBase):
self.name = name
self.rules = rules
@property
def pf_rule(self):
pf_rules = '\n\t'.join([r.pf_rule for r in self.rules])
return "anchor %s {\n%s\n}" % (self.name, pf_rules)
def external_pf_rule(self, base_dir):
path = os.path.abspath(os.path.join(base_dir, self.name))
return 'anchor %s\nload anchor %s from %s' % (self.name,
self.name,
path)
class AddressBookEntry(ModelBase):
def __init__(self, name, cidrs=[]):
@ -265,17 +215,6 @@ class AddressBookEntry(ModelBase):
def cidrs(self, values):
self._cidrs = [netaddr.IPNetwork(a) for a in values]
@property
def pf_rule(self):
return 'table <%s> persist {%s}' % (
self.name, ', '.join(map(str, self.cidrs))
)
def external_pf_rule(self, base_dir):
path = os.path.abspath(os.path.join(base_dir, self.name))
return 'table %s\npersist file "%s"' % (self.name,
path)
def external_table_data(self):
return '\n'.join(map(str, self.cidrs))
@ -323,24 +262,6 @@ class FloatingIP(ModelBase):
def fixed_ip(self, value):
self._fixed_ip = netaddr.IPAddress(value)
@property
def pf_rule(self):
if self.network is not None:
# There is a bug in Neutron that allows floating IPs with e.g.,
# a v6 fixed address and a v4 floating address. Until we get
# a bug fix for this upstream, don't make rules for these, because
# they're invalid.
if self.fixed_ip.version == self.floating_ip.version:
return (
'pass on %s from %s to any binat-to %s' %
(
self.network.interface.ifname,
self.fixed_ip,
self.floating_ip
)
)
return ''
@classmethod
def from_dict(cls, d):
return cls(
@ -387,11 +308,6 @@ class Label(ModelBase):
def cidrs(self, values):
self._cidrs = [netaddr.IPNetwork(a) for a in values]
@property
def pf_rule(self):
return ('match out on egress to {%s} label "%s"' %
(', '.join(map(str, self.cidrs)), self.name))
class Subnet(ModelBase):
def __init__(self, cidr, gateway_ip, dhcp_enabled=True,
@ -642,160 +558,3 @@ class Configuration(ModelBase):
@property
def interfaces(self):
return [n.interface for n in self.networks if n.interface]
@property
def pf_config(self):
rv = defaults.BASE_RULES[:]
# add default deny all external networks and remember 1st for nat
ext_if = None
for n in self.networks:
if n.network_type == Network.TYPE_EXTERNAL:
ext_if = n.interface.ifname
ext_v4_addr = n.interface.first_v4
break
# add in nat and management rules
for network in self.networks:
if network.network_type == Network.TYPE_EXTERNAL:
rv.extend(_format_ext_rule(network.interface))
elif network.network_type == Network.TYPE_INTERNAL:
if ext_if:
rv.extend(
_format_int_to_ext_rule(
ext_if,
ext_v4_addr,
network.interface
)
)
elif network.network_type == Network.TYPE_MANAGEMENT:
rv.extend(_format_mgt_rule(network.interface.ifname))
else:
# isolated and management nets block all between interfaces
rv.extend(_format_isolated_rule(network.interface.ifname))
# add address book tables
rv.extend(ab.pf_rule for ab in self.address_book.values())
# add anchors and rules
rv.extend(a.pf_rule for a in self.anchors)
# add counters
rv.extend(l.pf_rule for l in self.labels)
# add floating ip
for network in self.networks:
rv.extend(
_format_floating_ip(
network.interface.ifname,
network.floating_ips
)
)
return '\n'.join(rv) + '\n'
def _format_ext_rule(interface):
retval = []
name = interface.ifname
if interface.first_v6:
retval.append((
'pass on %s inet6 proto tcp from %s:network '
'to %s:network port 179' % (name, name, name))
)
retval.append((
'pass out quick on %s proto udp from %s to any port %d' %
(name, name, defaults.DNS))
)
retval.append(
'pass out quick on %s proto tcp from %s to any' % (name, name)
)
return retval
def _format_int_to_ext_rule(ext_if, ext_v4_addr, interface):
retval = []
name = interface.ifname
if interface.first_v4:
retval.extend([
_format_metadata_rule(name),
('pass out on %s from %s:network to any nat-to %s' %
(ext_if, name, ext_v4_addr)),
# IPv4 DHCP: Server: 68 Client: 67 need fwd/rev rules
'pass in quick on %s proto udp from port 68 to port 67' % name,
'pass out quick on %s proto udp from port 67 to port 68' % name,
])
if interface.first_v6:
retval.extend([
# IPv6 DHCP: Server: 547 Client: 546 need fwd/rev rules
'pass in quick on %s proto udp from port 546 to port 547' % name,
'pass out quick on %s proto udp from port 547 to port 546' % name,
# Allow IPv6 from this network out via egress
'pass out on %s inet6 from %s:network' % (ext_if, name),
'pass inet6 to %s:network' % (name)
])
retval.extend([
'pass in on %s proto tcp to any' % name,
'pass in on %s proto udp to any' % name,
])
retval.append((
'pass out quick on %s proto udp from %s to any port %d' %
(ext_if, name, defaults.DNS))
)
return retval
def _format_mgt_rule(mgt_if):
ports = ', '.join(str(p) for p in defaults.MANAGEMENT_PORTS)
return [('pass quick proto tcp from %s:network to %s port { %s }' %
(mgt_if, mgt_if, ports)),
('pass quick proto tcp from %s to %s:network port %s' %
(mgt_if, mgt_if, defaults.RUG_META_PORT)),
'block in quick on !%s to %s:network' % (mgt_if, mgt_if)]
def _format_isolated_rule(int_if):
return [_format_metadata_rule(int_if),
'block from %s:network to any' % int_if]
def _format_metadata_rule(int_if):
args = {
'ifname': int_if,
'dest_addr': defaults.METADATA_DEST_ADDRESS,
'local_port': defaults.internal_metadata_port(int_if)
}
return ('pass in quick on %(ifname)s proto tcp to %(dest_addr)s port http '
'rdr-to 127.0.0.1 port %(local_port)d') % args
def _format_floating_ip(ext_if, floating_ips):
bin_nat = [
('pass on %s from %s to any binat-to %s' % (
ext_if,
fip.fixed_ip,
fip.floating_ip
))
for fip in floating_ips
if fip.fixed_ip.version == fip.floating_ip.version
]
bin_nat.extend(
('pass out on %s to %s' % (fip.network.interface.ifname, fip.fixed_ip))
for fip in floating_ips
if fip.network is not None and
fip.fixed_ip.version == fip.floating_ip.version
)
return bin_nat

View File

@ -28,6 +28,5 @@ if __name__ == '__main__':
print '-' * 80
print conf.validate()
print '-' * 80
print conf.pf_config
except Exception as e:
pdb.set_trace()

View File

@ -44,8 +44,6 @@ setup(
'akanda.router.commands.management:configure_ssh',
'akanda-configure-gunicorn = '
'akanda.router.commands.management:configure_gunicorn',
'akanda-configure-default-pf = '
'akanda.router.commands.management:configure_default_pf',
'akanda-api-dev-server = akanda.router.api.server:main',
'akanda-metadata-proxy = akanda.router.metadata_proxy:main',
]

View File

@ -1,121 +0,0 @@
# Copyright 2014 DreamHost, LLC
#
# Author: DreamHost, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
The text output generated by the Firewall API.
"""
sample_firewall_rules = ('pass all flags S/SA block drop in on ! lo0 proto '
'tcp from any to any port 6000:6010')
sample_pfctl_sr = """
pass all flags S/SA
block drop in on ! lo0 proto tcp from any to any port 6000:6010
"""
sample_pfctl_ss = """
all tcp 192.168.229.129:22 <- 192.168.229.1:52130 ESTABLISHED:ESTABLISHED
all udp 192.168.229.255:17500 <- 192.168.229.1:17500 NO_TRAFFIC:SINGLE
all udp 172.16.5.255:17500 <- 172.16.5.1:17500 NO_TRAFFIC:SINGLE
"""
sample_pfctl_si = """
Status: Enabled for 0 days 01:57:48 Debug: err
State Table Total Rate
current entries 4
searches 5638 0.8/s
inserts 86 0.0/s
removals 82 0.0/s
Counters
match 86 0.0/s
bad-offset 0 0.0/s
fragment 0 0.0/s
short 0 0.0/s
normalize 0 0.0/s
memory 0 0.0/s
bad-timestamp 0 0.0/s
congestion 0 0.0/s
ip-option 0 0.0/s
proto-cksum 0 0.0/s
state-mismatch 0 0.0/s
state-insert 0 0.0/s
state-limit 0 0.0/s
src-limit 0 0.0/s
synproxy 0 0.0/s
"""
sample_pfctl_st = """
tcp.first 120s
tcp.opening 30s
tcp.established 86400s
tcp.closing 900s
tcp.finwait 45s
tcp.closed 90s
tcp.tsdiff 30s
udp.first 60s
udp.single 30s
udp.multiple 60s
icmp.first 20s
icmp.error 10s
other.first 60s
other.single 30s
other.multiple 60s
frag 30s
interval 10s
adaptive.start 6000 states
adaptive.end 12000 states
src.track 0s
"""
sample_pfctl_sm = """
states hard limit 10000
src-nodes hard limit 10000
frags hard limit 5000
tables hard limit 1000
table-entries hard limit 200000
"""
sample_pfctl_sl = """
No ALTQ support in kernel
ALTQ related functions disabled
"""
sample_pfctl_sA = """
dh
dh-ssh
dh-www
goodguys
"""
sample_pfctl_sS = """
No ALTQ support in kernel
ALTQ related functions disabled
"""
sample_pfctl_sT = """
table <block_hosts> persist
table <private> const { 10/8, 172.16/12, 192.168/16, 224/8 }
"""

View File

@ -24,15 +24,17 @@ import mock
from unittest2 import TestCase
from akanda.router.api import v1
from akanda.router.drivers.pf import PFManager
class FirewallAPITestCase(TestCase):
"""
"""
def setUp(self):
pf_mgr_patch = mock.patch.object(v1.firewall.pf, 'PFManager')
self.pf_mgr = pf_mgr_patch.start().return_value
ip_mgr_patch = mock.patch.object(
v1.firewall.iptables,
'IPTablesManager'
)
self.iptables_mgr = ip_mgr_patch.start().return_value
self.addCleanup(mock.patch.stopall)
self.app = flask.Flask('firewall_test')
self.app.register_blueprint(v1.firewall.blueprint)
@ -40,7 +42,7 @@ class FirewallAPITestCase(TestCase):
def _test_passthrough_helper(self, resource_name, method_name,
response_code=200):
mock_method = getattr(self.pf_mgr, method_name)
mock_method = getattr(self.iptables_mgr, method_name)
mock_method.return_value = 'the_value'
result = self.test_app.get('/v1/firewall/%s' % resource_name)
self.assertEqual(response_code, result.status_code)
@ -49,37 +51,3 @@ class FirewallAPITestCase(TestCase):
def test_get_rules(self):
self._test_passthrough_helper('rules', 'get_rules')
def test_get_states(self):
self._test_passthrough_helper('states', 'get_states')
def test_get_anchors(self):
self._test_passthrough_helper('anchors', 'get_anchors')
def test_get_sources(self):
self._test_passthrough_helper('sources', 'get_sources')
def test_get_info(self):
self._test_passthrough_helper('info', 'get_info')
def test_get_timeouts(self):
self._test_passthrough_helper('timeouts', 'get_timeouts')
def test_get_tables(self):
self._test_passthrough_helper('tables', 'get_tables')
def test_get_memory(self):
self._test_passthrough_helper('memory', 'get_memory')
def test_get_labels(self, reset_flag=False):
expected = {'labels': 'thelabels'}
self.pf_mgr.get_labels.return_value = 'thelabels'
method = 'post' if reset_flag else 'get'
args = (True, ) if reset_flag else ()
result = getattr(self.test_app, method)('/v1/firewall/labels')
self.assertEqual(result.status_code, 200)
self.pf_mgr.get_labels.assert_called_once_with(*args)
self.assertEqual(json.loads(result.data), expected)
def test_get_labels_reset(self):
self.test_get_labels(True)

View File

@ -89,7 +89,7 @@ class BirdTestCase(TestCase):
'the_config'
)
self.mock_execute.assert_called_once_with(
['mv', '/tmp/bird6.conf', '/etc/bird6.conf'],
['mv', '/tmp/bird6.conf', '/etc/bird/bird6.conf'],
'sudo'
)

View File

@ -241,15 +241,27 @@ class IPTestCase(TestCase):
cmd = '/sbin/ip'
v4 = netaddr.IPNetwork('192.168.105.2/24')
v6 = netaddr.IPNetwork('fdca:3ba5:a17a:acda:20c:29ff:fe94:723d/64')
iface = mock.Mock(all_addresses=[v4, v6])
old_iface = mock.Mock(all_addresses=[])
iface = mock.Mock(all_addresses=[v4, v6], ifname='em0')
old_iface = mock.Mock(all_addresses=[], ifname='em0')
mgr = ip.IPManager()
mgr._update_addresses('em0', iface, old_iface)
assert self.mock_execute.call_args_list == [
mock.call([cmd, 'addr', 'add', str(v4), 'dev', 'em0'], 'sudo'),
mock.call([cmd, '-6', 'addr', 'add', str(v6), 'dev', 'em0'], 'sudo'),
]
with mock.patch.object(
mgr, 'generic_to_host', lambda x: x.replace('ge', 'em')
):
mgr._update_addresses('em0', iface, old_iface)
assert self.mock_execute.call_args_list == [
mock.call([
cmd, 'addr', 'add', '192.168.105.2/24', 'dev', 'em0'
], 'sudo'),
mock.call([cmd, 'link', 'set', 'em0', 'up'], 'sudo'),
mock.call([cmd, 'addr', 'show', 'em0']),
mock.call([
cmd, '-6', 'addr', 'add',
'fdca:3ba5:a17a:acda:20c:29ff:fe94:723d/64', 'dev', 'em0'
], 'sudo'),
mock.call([cmd, 'link', 'set', 'em0', 'up'], 'sudo'),
mock.call([cmd, 'addr', 'show', 'em0'])
]
def test_address_remove(self):
cmd = '/sbin/ip'
@ -262,26 +274,40 @@ class IPTestCase(TestCase):
mgr._update_addresses('em0', iface, old_iface)
assert self.mock_execute.call_args_list == [
mock.call([cmd, 'addr', 'del', str(v4), 'dev', 'em0'], 'sudo'),
mock.call([cmd, '-6', 'addr', 'del', str(v6), 'dev', 'em0'], 'sudo'),
mock.call([
cmd, '-6', 'addr', 'del', str(v6), 'dev', 'em0'
], 'sudo'),
]
def test_update_set(self):
iface = mock.Mock()
iface.all_addresses = ['a', 'b']
iface.ifname = 'em0'
old_iface = mock.Mock()
old_iface.all_addresses = ['b', 'c']
old_iface.ifname = 'em0'
add = lambda g: ('addr', 'add', g, 'dev', 'em0')
delete = lambda g: ('addr', 'del', g, 'dev', 'em0')
mgr = ip.IPManager()
mgr._update_set('em0', iface, old_iface, 'all_addresses', add, delete)
with mock.patch.object(
mgr, 'generic_to_host', lambda x: x.replace('ge', 'em')
):
mgr._update_set('em0', iface, old_iface, 'all_addresses', add,
delete)
self.mock_execute.assert_has_calls([
mock.call(['/sbin/ip', 'addr', 'add', 'a', 'dev', 'em0'], 'sudo'),
mock.call(['/sbin/ip', 'addr', 'del', 'c', 'dev', 'em0'], 'sudo')
])
assert self.mock_execute.call_args_list == [
mock.call([
'/sbin/ip', 'addr', 'add', 'a', 'dev', 'em0'
], 'sudo'),
mock.call(['/sbin/ip', 'link', 'set', 'em0', 'up'], 'sudo'),
mock.call(['/sbin/ip', 'addr', 'show', 'em0']),
mock.call([
'/sbin/ip', 'addr', 'del', 'c', 'dev', 'em0'
], 'sudo')
]
def test_update_set_no_diff(self):
iface = mock.Mock()
@ -326,7 +352,12 @@ class IPTestCase(TestCase):
assert addr == 'fdca:3ba5:a17a:acda:f816:3eff:fe34:ba28'
assert self.mock_execute.call_args_list == [
mock.call([cmd, 'link', 'set', 'eth0', 'up'], 'sudo'),
mock.call([cmd, '-6', 'addr', 'add', addr + '/64', 'dev', 'eth0'], 'sudo')
mock.call([
cmd, '-6', 'addr', 'add',
'fdca:3ba5:a17a:acda:f816:3eff:fe34:ba28/64', 'dev',
'eth0'
], 'sudo'),
mock.call([cmd, 'link', 'set', 'eth0', 'up'], 'sudo')
]

View File

@ -0,0 +1,158 @@
from unittest import TestCase
import mock
from akanda.router import models
from akanda.router.drivers import iptables
CONFIG = models.Configuration({
'networks': [{
'network_id': 'ABC123',
'interface': {
'ifname': 'eth0',
'addresses': [
'fdca:3ba5:a17a:acda:f816:3eff:fe66:33b6/64',
'fe80::f816:3eff:fe66:33b6/64'
]
},
'name': 'mgt',
'network_type': models.Network.TYPE_MANAGEMENT,
}, {
'network_id': 'ABC456',
'interface': {
'ifname': 'eth1',
'addresses': [
'172.16.77.2/24',
'fdee:9f85:83be:0:f816:3eff:fe42:a9f/48'
]
},
'name': 'ext',
'network_type': models.Network.TYPE_EXTERNAL,
'subnets': [{
'cidr': '172.16.77.0/24',
'gateway_ip': '172.16.77.1',
'dhcp_enabled': True,
'dns_nameservers': []
}]
}, {
'network_id': 'ABC789',
'interface': {
'ifname': 'eth2',
'addresses': [
'192.168.0.1/24',
'fdd6:a1fa:cfa8:9df::1/64'
]
},
'name': 'internal',
'network_type': models.Network.TYPE_INTERNAL,
'subnets': [{
'cidr': '192.168.0.0/24',
'gateway_ip': '192.168.0.1',
'dhcp_enabled': True,
'dns_nameservers': []
}]
}],
'floating_ips': [{
'fixed_ip': '192.168.0.2',
'floating_ip': '172.16.77.50'
}]
})
V4_OUTPUT = [
'*filter',
':INPUT DROP [0:0]',
':FORWARD ACCEPT [0:0]',
':OUTPUT ACCEPT [0:0]',
'-A INPUT -i lo -j ACCEPT',
'-A INPUT -p icmp --icmp-type echo-request -j ACCEPT',
'-A INPUT -i eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT',
'-A FORWARD -d 192.168.0.1 -o eth2 -m state --state RELATED,ESTABLISHED -j ACCEPT', # noqa
'-A INPUT -i eth2 -p udp -m udp --dport 67 -j ACCEPT',
'-A INPUT -i eth2 -p tcp -m tcp --dport 67 -j ACCEPT',
'-A INPUT -i eth2 -j ACCEPT',
'-A INPUT -i eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT',
'COMMIT',
'*nat',
':PREROUTING ACCEPT [0:0]',
':INPUT ACCEPT [0:0]',
':OUTPUT ACCEPT [0:0]',
':POSTROUTING ACCEPT [0:0]',
'-A POSTROUTING -o eth1 -s 192.168.0.2 -j SNAT --to 172.16.77.50',
'-A PREROUTING -i eth1 -d 172.16.77.50 -j DNAT --to-destination 192.168.0.2', # noqa
'-A POSTROUTING -o eth2 -j SNAT --to 172.16.77.2',
'-A PREROUTING -i eth2 -d 169.254.169.254 -p tcp -m tcp --dport 80 -j DNAT --to-destination 192.168.0.1:9602', # noqa
'-A POSTROUTING -o eth1 -j MASQUERADE',
'COMMIT'
]
V6_OUTPUT = [
'*filter',
':INPUT DROP [0:0]',
':FORWARD ACCEPT [0:0]',
':OUTPUT ACCEPT [0:0]',
'-A INPUT -i lo -j ACCEPT',
'-A INPUT -p icmpv6 -j ACCEPT',
'-A INPUT -i eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT',
'-A INPUT -i eth0 -p tcp -m tcp --dport 22 -j ACCEPT',
'-A INPUT -i eth0 -p tcp -m tcp --dport 5000 -j ACCEPT',
'-A INPUT -i eth0 -p tcp -m tcp --dport 9697 -j ACCEPT',
'-A INPUT -i !eth0 -d fdca:3ba5:a17a:acda:f816:3eff:fe66:33b6 -j DROP',
'-A FORWARD -d fdd6:a1fa:cfa8:9df::1 -o eth2 -m state --state RELATED,ESTABLISHED -j ACCEPT', # noqa
'-A INPUT -i eth2 -p udp -m udp --dport 546 -j ACCEPT',
'-A INPUT -i eth2 -p tcp -m tcp --dport 546 -j ACCEPT',
'-A INPUT -i eth2 -j ACCEPT',
'-A INPUT -i eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT',
'COMMIT'
]
class TestIPTablesConfiguration(TestCase):
def setUp(self):
super(TestIPTablesConfiguration, self).setUp()
self.execute = mock.patch('akanda.router.utils.execute').start()
self.replace = mock.patch('akanda.router.utils.replace_file').start()
self.patches = [self.execute, self.replace]
def tearDown(self):
super(TestIPTablesConfiguration, self).tearDown()
for p in self.patches:
p.stop()
def test_complete(self):
mgr = iptables.IPTablesManager()
mgr.save_config(CONFIG, {
'ge0': 'eth0',
'ge1': 'eth1',
'ge2': 'eth2'
})
assert self.replace.call_count == 2
assert mock.call(
'/tmp/ip4tables.rules',
'\n'.join(V4_OUTPUT) + '\n'
) in self.replace.call_args_list
assert mock.call(
'/tmp/ip6tables.rules',
'\n'.join(V6_OUTPUT) + '\n'
) in self.replace.call_args_list
assert self.execute.call_args_list == [
mock.call(
['mv', '/tmp/ip4tables.rules', '/etc/iptables/rules.v4'],
'sudo'
),
mock.call(
['mv', '/tmp/ip6tables.rules', '/etc/iptables/rules.v6'],
'sudo'
)
]
def test_restart(self):
mgr = iptables.IPTablesManager()
mgr.restart()
assert self.execute.call_args_list == [
mock.call(['/etc/init.d/iptables-persistent', 'restart'], 'sudo')
]

View File

@ -1,45 +0,0 @@
# Copyright 2014 DreamHost, LLC
#
# Author: DreamHost, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
import unittest2
from akanda.router.drivers import pf
config = mock.Mock()
network = mock.Mock()
alloc = mock.Mock()
network.address_allocations = [alloc]
config.networks = [network]
class PFTest(unittest2.TestCase):
def setUp(self):
self.mgr = pf.PFManager()
@mock.patch.object(pf, 'execute')
@mock.patch.object(pf, 'replace_file')
def test_update_error_includes_file_contents(self, ex, rf):
# Verify that the error message from pf includes the contents
# of the config file.
with mock.patch.object(self.mgr, 'sudo') as sudo:
sudo.side_effect = RuntimeError('base message')
try:
self.mgr.update_conf('conf data')
except RuntimeError as e:
self.assertIn('conf data', unicode(e))

View File

@ -32,8 +32,8 @@ class TestMetadataProxy(unittest.TestCase):
)
metadata_proxy.main()
listen.assert_has_calls(
[mock.call(('127.0.0.1', 9602), backlog=128),
mock.call(('127.0.0.1', 9603), backlog=128)],
[mock.call(('0.0.0.0', 9602), backlog=128),
mock.call(('0.0.0.0', 9603), backlog=128)],
any_order=True
)
# call_args need to be order before we can test it

View File

@ -196,106 +196,12 @@ class FilterRuleModelTestCase(TestCase):
with self.assertRaises(ValueError):
models.FilterRule(action='pass', protocol='made_up_proto')
def _pf_rule_test_helper(self, d, expected):
fr = models.FilterRule(**d)
self.assertEqual(fr.pf_rule, expected)
def test_pf_rule_basic(self):
self._pf_rule_test_helper(dict(action='pass'), 'pass')
self._pf_rule_test_helper(dict(action='block'), 'block')
def test_pf_rule_interface(self):
self._pf_rule_test_helper(dict(action='pass', interface='ge0'),
'pass on ge0')
def test_pf_rule_family(self):
self._pf_rule_test_helper(dict(action='block', family='inet6'),
'block inet6')
def test_pf_rule_protocol(self):
self._pf_rule_test_helper(dict(action='block', protocol='tcp'),
'block proto tcp')
def test_pf_rule_source_table(self):
self._pf_rule_test_helper(dict(action='block', source='foo'),
'block from <foo>')
def test_pf_rule_source_address(self):
args = dict(action='block', source='192.168.1.0/24')
self._pf_rule_test_helper(args, 'block from 192.168.1.0/24')
def test_pf_rule_source_port(self):
args = dict(action='block', source_port=22)
self._pf_rule_test_helper(args, 'block from port 22')
def test_pf_rule_source_address_and_port(self):
args = dict(action='pass', source='192.168.1.1/32', source_port=22)
self._pf_rule_test_helper(args, 'pass from 192.168.1.1/32 port 22')
def test_pf_rule_destination_interface(self):
args = dict(action='block', destination_interface="ge1")
self._pf_rule_test_helper(args, 'block to ge1')
def test_pf_rule_destination_table(self):
args = dict(action='block', destination="foo")
self._pf_rule_test_helper(args, 'block to <foo>')
def test_pf_rule_destination_address(self):
args = dict(action='block', destination="192.168.1.0/24")
self._pf_rule_test_helper(args, 'block to 192.168.1.0/24')
def test_pf_rule_destination_port(self):
args = dict(action='block', destination_port="23")
self._pf_rule_test_helper(args, 'block to port 23')
def test_pf_rule_destination_address_and_port(self):
args = dict(action='block', destination='192.168.1.2/32',
destination_port="23")
self._pf_rule_test_helper(args, 'block to 192.168.1.2/32 port 23')
def test_pf_rule_redirect(self):
args = dict(action='pass',
destination_port="23",
redirect="192.168.1.1")
self._pf_rule_test_helper(args, 'pass to port 23 rdr-to 192.168.1.1')
def test_pf_rule_redirect_port(self):
args = dict(action='pass',
destination_port="23",
redirect_port="24")
self._pf_rule_test_helper(args, 'pass to port 23 rdr-to port 24')
def test_pf_rule_from_dict(self):
args = dict(action='pass',
destination_port="23",
redirect="192.168.1.2")
pr = models.FilterRule.from_dict(args)
self.assertEqual(pr.action, 'pass')
self.assertEqual(pr.destination_port, 23)
self.assertEqual(pr.redirect, netaddr.IPAddress('192.168.1.2'))
class AnchorTestCase(TestCase):
def test_anchor(self):
a = models.Anchor('foo', [])
self.assertEqual(a.name, 'foo')
self.assertEqual(a.rules, [])
def test_anchor_external_pf_rule(self):
a = models.Anchor('foo', [])
self.assertEqual(a.external_pf_rule('/etc/pf'),
'anchor foo\nload anchor foo from /etc/pf/foo')
def test_anchor_pf_rule_empty(self):
a = models.Anchor('foo', [])
self.assertEqual(a.pf_rule, 'anchor foo {\n\n}')
def test_anchor_pf_rule(self):
fr = models.FilterRule(action='block', interface="ge0")
a = models.Anchor('foo', [fr])
self.assertEqual(a.pf_rule, 'anchor foo {\nblock on ge0\n}')
class AddressBookTestCase(TestCase):
def test_entry(self):
@ -303,15 +209,6 @@ class AddressBookTestCase(TestCase):
self.assertEqual(ab.name, 'foo')
self.assertEqual(ab.cidrs, [netaddr.IPNetwork('192.168.1.0/24')])
def test_pf_rule(self):
ab = models.AddressBookEntry('foo', ['192.168.1.0/24'])
self.assertEqual(ab.pf_rule, 'table <foo> persist {192.168.1.0/24}')
def test_external_pf_rule(self):
ab = models.AddressBookEntry('foo', ['192.168.1.0/24'])
self.assertEqual(ab.external_pf_rule('/etc'),
'table foo\npersist file "/etc/foo"')
def test_external_table_data(self):
ab = models.AddressBookEntry('foo', ['192.168.1.0/24',
'172.16.16.0/16'])
@ -325,11 +222,6 @@ class LabelTestCase(TestCase):
self.assertEqual(l.name, 'foo')
self.assertEqual(l.cidrs, [netaddr.IPNetwork('192.168.1.0/24')])
def test_pf_rule(self):
l = models.Label('foo', ['192.168.1.0/24'])
self.assertEqual(l.pf_rule,
'match out on egress to {192.168.1.0/24} label "foo"')
class AllocationTestCase(TestCase):
def test_allocation(self):
@ -357,13 +249,8 @@ class FloatingIPTestCase(TestCase):
self.assertEqual(fip.floating_ip, netaddr.IPAddress('9.9.9.9'))
self.assertEqual(fip.fixed_ip, netaddr.IPAddress('10.0.0.1'))
self.assertEqual(fip.pf_rule, '')
fip.network = network
self.assertEqual(
fip.pf_rule,
'pass on ge1 from 10.0.0.1 to any binat-to 9.9.9.9'
)
def test_floating_ip_with_different_ip_versions(self):
fip = models.FloatingIP(
@ -375,10 +262,6 @@ class FloatingIPTestCase(TestCase):
network.interface.ifname = 'ge1'
fip.network = network
self.assertEqual(
fip.pf_rule,
''
)
class StaticRouteTestCase(TestCase):
@ -641,288 +524,3 @@ class ConfigurationTestCase(TestCase):
anchors=[])
self.assertEqual(c.to_dict(), expected)
def _pf_config_test_helper(self, conf_dict, test_expectations):
base = ['block']
expected = '\n'.join(base + test_expectations + [''])
attrs = dict(
BASE_RULES=base,
MANAGEMENT_PORTS=[22])
with mock.patch.multiple('akanda.router.defaults', **attrs) as defs:
c = models.Configuration(conf_dict)
self.assertEqual(c.pf_config, expected)
def test_pf_config_default(self):
self._pf_config_test_helper({'networks': []}, [])
def test_pf_config_nat(self):
ext_net = dict(network_id='ext',
interface=dict(ifname='ge0', addresses=['9.9.9.1/24']),
network_type='external')
int_net = dict(network_id='int',
interface=dict(ifname='ge1', addresses=['10.0.0.0/8']),
network_type='internal')
self._pf_config_test_helper(
{'networks': [ext_net, int_net]},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
('pass in quick on ge1 proto tcp to 169.254.169.254 port '
'http rdr-to 127.0.0.1 port 9601'),
'pass out on ge0 from ge1:network to any nat-to 9.9.9.1',
'pass in quick on ge1 proto udp from port 68 to port 67',
'pass out quick on ge1 proto udp from port 67 to port 68',
'pass in on ge1 proto tcp to any',
'pass in on ge1 proto udp to any',
'pass out quick on ge0 proto udp from ge1 to any port 53',
]
)
def test_pf_config_nat_with_ip6(self):
ext_net = dict(network_id='ext',
interface=dict(ifname='ge0', addresses=['9.9.9.1/24']),
network_type='external')
int_net = dict(network_id='int',
interface=dict(ifname='ge1', addresses=['10.0.0.0/8']),
network_type='internal')
v6_net = dict(network_id='v6_int',
interface=dict(ifname='ge2', addresses=['fe80::1/64']),
network_type='internal')
self._pf_config_test_helper(
{'networks': [ext_net, int_net, v6_net]},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
('pass in quick on ge1 proto tcp to 169.254.169.254 port '
'http rdr-to 127.0.0.1 port 9601'),
'pass out on ge0 from ge1:network to any nat-to 9.9.9.1',
'pass in quick on ge1 proto udp from port 68 to port 67',
'pass out quick on ge1 proto udp from port 67 to port 68',
'pass in on ge1 proto tcp to any',
'pass in on ge1 proto udp to any',
'pass out quick on ge0 proto udp from ge1 to any port 53',
'pass in quick on ge2 proto udp from port 546 to port 547',
'pass out quick on ge2 proto udp from port 547 to port 546',
'pass out on ge0 inet6 from ge2:network',
'pass inet6 to ge2:network',
'pass in on ge2 proto tcp to any',
'pass in on ge2 proto udp to any',
'pass out quick on ge0 proto udp from ge2 to any port 53',
]
)
def test_pf_config_isolated(self):
ext_net = dict(network_id='ext',
interface=dict(ifname='ge0'),
network_type='external')
int_net = dict(network_id='int',
interface=dict(ifname='ge1'),
network_type='isolated')
self._pf_config_test_helper(
{'networks': [ext_net, int_net]},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
('pass in quick on ge1 proto tcp to 169.254.169.254 port '
'http rdr-to 127.0.0.1 port 9601'),
'block from ge1:network to any'
]
)
def test_pf_config_management(self):
ext_net = dict(network_id='ext',
interface=dict(ifname='ge0'),
network_type='external')
int_net = dict(network_id='int',
interface=dict(ifname='ge1'),
network_type='management')
self._pf_config_test_helper(
{'networks': [ext_net, int_net]},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
'pass quick proto tcp from ge1:network to ge1 port { 22 }',
'pass quick proto tcp from ge1 to ge1:network port 9697',
'block in quick on !ge1 to ge1:network',
]
)
def test_pf_config_with_addressbook(self):
ext_net = dict(network_id='ext',
interface=dict(ifname='ge0'),
network_type='external')
ab = dict(foo=['192.168.1.1/24'])
self._pf_config_test_helper(
{'networks': [ext_net], 'address_book': ab},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
'table <foo> persist {192.168.1.1/24}'
]
)
def test_pf_config_with_anchor(self):
ext_net = dict(network_id='ext',
interface=dict(ifname='ge0'),
network_type='external')
anchor = dict(name='foo',
rules=[dict(action='pass',
protocol='tcp',
destination_port=22)])
self._pf_config_test_helper(
{'networks': [ext_net], 'anchors': [anchor]},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
'anchor foo {\npass proto tcp to port 22\n}'
]
)
def test_pf_config_with_label(self):
ext_net = dict(network_id='ext',
interface=dict(ifname='ge0'),
network_type='external')
label = dict(foo=['192.168.1.0/24'])
self._pf_config_test_helper(
{'networks': [ext_net], 'labels': label},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
'match out on egress to {192.168.1.0/24} label "foo"'
]
)
def test_pf_config_with_floating(self):
ext_net = dict(
network_id='ext',
interface=dict(ifname='ge0', addresses=['9.9.9.1/24']),
network_type='external',
subnets=[
{
'cidr': '9.9.9.0/24',
'gateway_ip': '9.9.9.1',
'dhcp_enabled': True,
'dns_nameservers': [],
}
]
)
int_net = dict(
network_id='int',
interface=dict(ifname='ge1', addresses=['10.0.0.0/24']),
network_type='internal',
subnets=[
{
'cidr': '10.0.0.0/24',
'gateway_ip': '10.0.0.1',
'dhcp_enabled': True,
'dns_nameservers': [],
}
]
)
fip = {
'floating_ip': '9.9.9.9',
'fixed_ip': '10.0.0.1'
}
self._pf_config_test_helper(
{'networks': [ext_net, int_net], 'floating_ips': [fip]},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
('pass in quick on ge1 proto tcp to 169.254.169.254 port '
'http rdr-to 127.0.0.1 port 9601'),
'pass out on ge0 from ge1:network to any nat-to 9.9.9.1',
'pass in quick on ge1 proto udp from port 68 to port 67',
'pass out quick on ge1 proto udp from port 67 to port 68',
'pass in on ge1 proto tcp to any',
'pass in on ge1 proto udp to any',
'pass out quick on ge0 proto udp from ge1 to any port 53',
'pass on ge0 from 10.0.0.1 to any binat-to 9.9.9.9',
'pass out on ge1 to 10.0.0.1'
]
)
def test_pf_config_with_floating_different_ip_versions(self):
ext_net = dict(
network_id='ext',
interface=dict(ifname='ge0', addresses=['9.9.9.1/24']),
network_type='external',
subnets=[
{
'cidr': '9.9.9.0/24',
'gateway_ip': '9.9.9.1',
'dhcp_enabled': True,
'dns_nameservers': [],
}
]
)
int_net = dict(
network_id='int',
interface=dict(ifname='ge1', addresses=['10.0.0.0/24']),
network_type='internal',
subnets=[
{
'cidr': '10.0.0.0/24',
'gateway_ip': '10.0.0.1',
'dhcp_enabled': True,
'dns_nameservers': [],
}
]
)
fip = {
'floating_ip': '9.9.9.9',
'fixed_ip': 'fe80::1'
}
self._pf_config_test_helper(
{'networks': [ext_net, int_net], 'floating_ips': [fip]},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
('pass in quick on ge1 proto tcp to 169.254.169.254 port '
'http rdr-to 127.0.0.1 port 9601'),
'pass out on ge0 from ge1:network to any nat-to 9.9.9.1',
'pass in quick on ge1 proto udp from port 68 to port 67',
'pass out quick on ge1 proto udp from port 67 to port 68',
'pass in on ge1 proto tcp to any',
'pass in on ge1 proto udp to any',
'pass out quick on ge0 proto udp from ge1 to any port 53'
]
)
def test_pf_config_external_ipv4(self):
ext_net = dict(network_id='ext',
interface=dict(ifname='ge0'),
network_type='external')
self._pf_config_test_helper(
{'networks': [ext_net]},
[
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
]
)
def test_pf_config_external_ipv6(self):
ext_net = dict(network_id='ext',
interface=dict(ifname='ge0', addresses=['fe80::1/64']),
network_type='external')
self._pf_config_test_helper(
{'networks': [ext_net]},
[
('pass on ge0 inet6 proto tcp from ge0:network to ge0:network '
'port 179'),
'pass out quick on ge0 proto udp from ge0 to any port 53',
'pass out quick on ge0 proto tcp from ge0 to any',
]
)