Mark McClain 4dde1f78e7 Ensure VPN settings are more prescriptive.
Previously VPN service relied on default behaviours and an open
firewall.  This specifies more values and ensures the firewall is
properly set.  Additionally, test coverage is expanded.

Closes-Bug:1564213
Change-Id: Iefaccddaad54c412195802f97811722bb593b2ca
2016-03-30 23:33:16 -04:00

491 lines
16 KiB
Python

# 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
import os
from astara_router.drivers import base
from astara_router.models import Network
from astara_router import settings, 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 astara configuration to save to disk
:type config: astara.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),
self._build_mangle_table(config),
self._build_raw_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 [netfilter/iptables]-persistent
Note that at some point iptables-persistent merged into
netfilter-persistent as a plugin, so use that instead if it is
available
'''
_init = '%s-persistent'
if os.path.isfile('/etc/init.d/netfilter-persistent'):
init = _init % 'netfilter'
else:
init = _init % 'iptables'
utils.execute(
['service', init, 'restart'],
self.root_helper
)
def get_rules(self):
'''
Return the output of `iptables` and `ip6tables`.
This function is used by astara orchestrator -> HTTP as a test for
"router aliveness".
:rtype: str
'''
v4 = utils.execute(['iptables', '-L', '-n'], self.root_helper)
v6 = utils.execute(['ip6tables', '-L', '-n'], self.root_helper)
return v4 + v6
def get_external_network(self, config):
'''
Returns the external network
:rtype: astara_router.models.Network
'''
try:
return self.networks_by_type(config, Network.TYPE_EXTERNAL)[0]
except IndexError:
return None
def get_management_network(self, config):
'''
Returns the management network
:rtype: astara_router.models.Network
'''
return self.networks_by_type(config, Network.TYPE_MANAGEMENT)[0]
def get_internal_networks(self, config):
'''
Returns the internal networks
:rtype: [astara_router.models.Network]
'''
return self.networks_by_type(config, Network.TYPE_INTERNAL)
def networks_by_type(self, config, type):
'''
Returns the external network
:rtype: astara_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 astara configuration object:
:type config: astara_router.models.Configuration
:param rules: the list of rules to append to
:type rules: a list of astara_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),
self._build_vpn_filter_rules(config),
[Rule('COMMIT')]
)
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 (
settings.SSH, settings.API_SERVICE,
settings.ORCHESTRATOR_METADATA_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_net = self.get_external_network(config)
if ext_net:
ext_if = ext_net.interface
else:
ext_if = None
for network in self.get_internal_networks(config):
for version, address, dhcp_port in (
(4, network.interface.first_v4, settings.DHCP),
(6, network.interface.first_v6, settings.DHCPV6)
):
if address:
# 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
))
if ext_if:
rules.append(Rule(
'-A INPUT -i %s -m state '
'--state RELATED,ESTABLISHED -j ACCEPT' % ext_if.ifname
))
return rules
def _build_vpn_filter_rules(self, config):
rules = []
ext_net = self.get_external_network(config)
if ext_net:
ext_if = ext_net.interface
else:
ext_net = None
if ext_net is None or not config.vpn:
return rules
template = (
('-A INPUT -i %%s -p udp -m udp --dport %d -j ACCEPT ' %
settings.ISAKMP),
('-A INPUT -i %%s -p udp -m udp --dport %d -j ACCEPT ' %
settings.IPSEC_NAT_T),
'-A INPUT -i %s -p esp -j ACCEPT',
'-A INPUT -i %s -p ah -j ACCEPT'
)
for version in (4, 6):
rules.extend(
(Rule(t % ext_if.ifname, ip_version=version) for t in template)
)
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),
]
rules.extend(self._build_public_snat_chain(config))
rules.extend([
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 = []
for network in self.get_internal_networks(config):
if network.interface.first_v4:
# 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,
settings.METADATA_DEST_ADDRESS,
settings.HTTP,
network.interface.first_v4,
settings.internal_metadata_port(
network.interface.ifname
)
), ip_version=4
))
# Add a masquerade catch-all for VMs without floating IPs
ext_net = self.get_external_network(config)
if ext_net:
ext_if = ext_net.interface
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_net = self.get_external_network(config)
if ext_net:
ext_if = ext_net.interface
else:
return []
# NAT floating IP addresses
for fip in ext_net.floating_ips:
# Neutron has a bug whereby you can create a floating ip that has
# mixed IP versions between the fixed and floating address. If
# people create these accidentally, just ignore them (because
# iptables will barf if it encounters them)
if fip.fixed_ip.version == fip.floating_ip.version:
if ext_if:
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
))
for network in self.get_internal_networks(config):
rules.append(Rule(
'-A PREROUTING -i %s -d %s -j DNAT '
'--to-destination %s' % (
network.interface.ifname,
fip.floating_ip,
fip.fixed_ip
), ip_version=4
))
if rules:
for network in self.get_internal_networks(config):
for subnet in network.subnets:
if subnet.cidr.version == 4:
rules.append(
Rule('-A POSTROUTING -s %s -j PUBLIC_SNAT' % (
subnet.cidr
), ip_version=4)
)
return rules
def _build_public_snat_chain(self, config):
'''
Build a chain for SNAT for neutron FloatingIPs. This chain ignores NAT
for traffic marked as private.
'''
external_network = self.get_external_network(config)
if not external_network:
return []
rules = [
Rule(':PUBLIC_SNAT - [0:0]', ip_version=4),
Rule(
'-A PUBLIC_SNAT -m mark --mark 0xACDA -j RETURN',
ip_version=4
)
]
# NAT floating IP addresses
for fip in external_network.floating_ips:
if fip.fixed_ip.version == fip.floating_ip.version:
rules.append(
Rule('-A PUBLIC_SNAT -s %s -j SNAT --to %s' % (
fip.fixed_ip,
fip.floating_ip
), ip_version=4)
)
# Add source NAT to handle NAT loopback case where external floating IP
# is used as the destination from internal endpoint
mgt_if = self.get_management_network(config).interface
rules.append(Rule(
'-A PUBLIC_SNAT ! -o %s -j SNAT --to %s' % (
mgt_if.ifname,
str(external_network.interface.first_v4)
),
ip_version=4
))
return rules
def _build_mangle_table(self, config):
rules = [
Rule('*mangle', ip_version=4),
Rule(':INPUT - [0:0]', ip_version=4),
Rule(':OUTPUT - [0:0]', ip_version=4),
Rule(':FORWARD - [0:0]', ip_version=4),
Rule(':PREROUTING - [0:0]', ip_version=4),
Rule(':POSTROUTING - [0:0]', ip_version=4),
Rule(
('-A POSTROUTING -p udp -m udp --dport 68 '
'-j CHECKSUM --checksum-fill'),
ip_version=4),
Rule('COMMIT', ip_version=4)
]
return rules
def _build_raw_table(self, config):
'''
Add raw rules (so we can mark private traffic and avoid NATing it)
'''
rules = [
Rule('*raw', ip_version=4),
Rule(':INPUT - [0:0]', ip_version=4),
Rule(':OUTPUT - [0:0]', ip_version=4),
Rule(':FORWARD - [0:0]', ip_version=4),
Rule(':PREROUTING - [0:0]', ip_version=4)
]
# do not NAT traffic generated from within the appliance
rules.append(Rule('-A OUTPUT -j MARK --set-mark 0xACDA', ip_version=4))
ext_net = self.get_external_network(config)
if ext_net:
ext_if = ext_net.interface
rules.append(Rule(
'-A PREROUTING -i %s -j MARK --set-mark 0xACDA' %
ext_if.ifname, ip_version=4
))
for network in self.networks_by_type(config, Network.TYPE_INTERNAL):
if network.interface.first_v4:
address = sorted(
str(a) for a in network.interface.addresses
if a.version == 4
)[0]
rules.append(Rule(
'-A PREROUTING -d %s -j MARK --set-mark 0xACDA' % address,
ip_version=4
))
rules.append(Rule(':POSTROUTING - [0:0]', ip_version=4))
rules.append(Rule('COMMIT', ip_version=4))
return rules