Merge pull request #78 from ryanpetrello/linux
Implement a Linux firewall with iptables and ip6tables.
This commit is contained in:
commit
a4cadd55c3
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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'):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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 }
|
||||
"""
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
||||
|
|
|
@ -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')
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
]
|
|
@ -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))
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue