merge lp:nova

This commit is contained in:
Mark Washenberger 2011-03-10 17:41:57 -05:00
commit cda582a07f
5 changed files with 659 additions and 278 deletions

View File

@ -17,15 +17,17 @@
Implements vlans, bridges, and iptables rules using linux utilities.
"""
import inspect
import os
from eventlet import semaphore
from nova import db
from nova import exception
from nova import flags
from nova import log as logging
from nova import utils
LOG = logging.getLogger("nova.linux_net")
@ -52,8 +54,6 @@ flags.DEFINE_string('dhcpbridge', _bin_file('nova-dhcpbridge'),
'location of nova-dhcpbridge')
flags.DEFINE_string('routing_source_ip', '$my_ip',
'Public IP of network host')
flags.DEFINE_bool('use_nova_chains', False,
'use the nova_ routing chains instead of default')
flags.DEFINE_string('input_chain', 'INPUT',
'chain to add nova_input to')
@ -63,79 +63,332 @@ flags.DEFINE_string('dmz_cidr', '10.128.0.0/24',
'dmz range that should be accepted')
binary_name = os.path.basename(inspect.stack()[-1][1])
class IptablesRule(object):
"""An iptables rule
You shouldn't need to use this class directly, it's only used by
IptablesManager
"""
def __init__(self, chain, rule, wrap=True, top=False):
self.chain = chain
self.rule = rule
self.wrap = wrap
self.top = top
def __eq__(self, other):
return ((self.chain == other.chain) and
(self.rule == other.rule) and
(self.top == other.top) and
(self.wrap == other.wrap))
def __ne__(self, other):
return not self == other
def __str__(self):
if self.wrap:
chain = '%s-%s' % (binary_name, self.chain)
else:
chain = self.chain
return '-A %s %s' % (chain, self.rule)
class IptablesTable(object):
"""An iptables table"""
def __init__(self):
self.rules = []
self.chains = set()
self.unwrapped_chains = set()
def add_chain(self, name, wrap=True):
"""Adds a named chain to the table
The chain name is wrapped to be unique for the component creating
it, so different components of Nova can safely create identically
named chains without interfering with one another.
At the moment, its wrapped name is <binary name>-<chain name>,
so if nova-compute creates a chain named "OUTPUT", it'll actually
end up named "nova-compute-OUTPUT".
"""
if wrap:
self.chains.add(name)
else:
self.unwrapped_chains.add(name)
def remove_chain(self, name, wrap=True):
"""Remove named chain
This removal "cascades". All rule in the chain are removed, as are
all rules in other chains that jump to it.
If the chain is not found, this is merely logged.
"""
if wrap:
chain_set = self.chains
else:
chain_set = self.unwrapped_chains
if name not in chain_set:
LOG.debug(_("Attempted to remove chain %s which doesn't exist"),
name)
return
chain_set.remove(name)
self.rules = filter(lambda r: r.chain != name, self.rules)
if wrap:
jump_snippet = '-j %s-%s' % (binary_name, name)
else:
jump_snippet = '-j %s' % (name,)
self.rules = filter(lambda r: jump_snippet not in r.rule, self.rules)
def add_rule(self, chain, rule, wrap=True, top=False):
"""Add a rule to the table
This is just like what you'd feed to iptables, just without
the "-A <chain name>" bit at the start.
However, if you need to jump to one of your wrapped chains,
prepend its name with a '$' which will ensure the wrapping
is applied correctly.
"""
if wrap and chain not in self.chains:
raise ValueError(_("Unknown chain: %r") % chain)
if '$' in rule:
rule = ' '.join(map(self._wrap_target_chain, rule.split(' ')))
self.rules.append(IptablesRule(chain, rule, wrap, top))
def _wrap_target_chain(self, s):
if s.startswith('$'):
return '%s-%s' % (binary_name, s[1:])
return s
def remove_rule(self, chain, rule, wrap=True, top=False):
"""Remove a rule from a chain
Note: The rule must be exactly identical to the one that was added.
You cannot switch arguments around like you can with the iptables
CLI tool.
"""
try:
self.rules.remove(IptablesRule(chain, rule, wrap, top))
except ValueError:
LOG.debug(_("Tried to remove rule that wasn't there:"
" %(chain)r %(rule)r %(wrap)r %(top)r"),
{'chain': chain, 'rule': rule,
'top': top, 'wrap': wrap})
class IptablesManager(object):
"""Wrapper for iptables
See IptablesTable for some usage docs
A number of chains are set up to begin with.
First, nova-filter-top. It's added at the top of FORWARD and OUTPUT. Its
name is not wrapped, so it's shared between the various nova workers. It's
intended for rules that need to live at the top of the FORWARD and OUTPUT
chains. It's in both the ipv4 and ipv6 set of tables.
For ipv4 and ipv6, the builtin INPUT, OUTPUT, and FORWARD filter chains are
wrapped, meaning that the "real" INPUT chain has a rule that jumps to the
wrapped INPUT chain, etc. Additionally, there's a wrapped chain named
"local" which is jumped to from nova-filter-top.
For ipv4, the builtin PREROUTING, OUTPUT, and POSTROUTING nat chains are
wrapped in the same was as the builtin filter chains. Additionally, there's
a snat chain that is applied after the POSTROUTING chain.
"""
def __init__(self, execute=None):
if not execute:
if FLAGS.fake_network:
self.execute = lambda *args, **kwargs: ('', '')
else:
self.execute = utils.execute
else:
self.execute = execute
self.ipv4 = {'filter': IptablesTable(),
'nat': IptablesTable()}
self.ipv6 = {'filter': IptablesTable()}
# Add a nova-filter-top chain. It's intended to be shared
# among the various nova components. It sits at the very top
# of FORWARD and OUTPUT.
for tables in [self.ipv4, self.ipv6]:
tables['filter'].add_chain('nova-filter-top', wrap=False)
tables['filter'].add_rule('FORWARD', '-j nova-filter-top',
wrap=False, top=True)
tables['filter'].add_rule('OUTPUT', '-j nova-filter-top',
wrap=False, top=True)
tables['filter'].add_chain('local')
tables['filter'].add_rule('nova-filter-top', '-j $local',
wrap=False)
# Wrap the builtin chains
builtin_chains = {4: {'filter': ['INPUT', 'OUTPUT', 'FORWARD'],
'nat': ['PREROUTING', 'OUTPUT', 'POSTROUTING']},
6: {'filter': ['INPUT', 'OUTPUT', 'FORWARD']}}
for ip_version in builtin_chains:
if ip_version == 4:
tables = self.ipv4
elif ip_version == 6:
tables = self.ipv6
for table, chains in builtin_chains[ip_version].iteritems():
for chain in chains:
tables[table].add_chain(chain)
tables[table].add_rule(chain, '-j $%s' % (chain,),
wrap=False)
# Add a nova-postrouting-bottom chain. It's intended to be shared
# among the various nova components. We set it as the last chain
# of POSTROUTING chain.
self.ipv4['nat'].add_chain('nova-postrouting-bottom', wrap=False)
self.ipv4['nat'].add_rule('POSTROUTING', '-j nova-postrouting-bottom',
wrap=False)
# We add a snat chain to the shared nova-postrouting-bottom chain
# so that it's applied last.
self.ipv4['nat'].add_chain('snat')
self.ipv4['nat'].add_rule('nova-postrouting-bottom', '-j $snat',
wrap=False)
# And then we add a floating-snat chain and jump to first thing in
# the snat chain.
self.ipv4['nat'].add_chain('floating-snat')
self.ipv4['nat'].add_rule('snat', '-j $floating-snat')
self.semaphore = semaphore.Semaphore()
@utils.synchronized('iptables')
def apply(self):
"""Apply the current in-memory set of iptables rules
This will blow away any rules left over from previous runs of the
same component of Nova, and replace them with our current set of
rules. This happens atomically, thanks to iptables-restore.
We wrap the call in a semaphore lock, so that we don't race with
ourselves. In the event of a race with another component running
an iptables-* command at the same time, we retry up to 5 times.
"""
with self.semaphore:
s = [('iptables', self.ipv4)]
if FLAGS.use_ipv6:
s += [('ip6tables', self.ipv6)]
for cmd, tables in s:
for table in tables:
current_table, _ = self.execute('sudo',
'%s-save' % (cmd,),
'-t', '%s' % (table,),
attempts=5)
current_lines = current_table.split('\n')
new_filter = self._modify_rules(current_lines,
tables[table])
self.execute('sudo', '%s-restore' % (cmd,),
process_input='\n'.join(new_filter),
attempts=5)
def _modify_rules(self, current_lines, table, binary=None):
unwrapped_chains = table.unwrapped_chains
chains = table.chains
rules = table.rules
# Remove any trace of our rules
new_filter = filter(lambda line: binary_name not in line,
current_lines)
seen_chains = False
rules_index = 0
for rules_index, rule in enumerate(new_filter):
if not seen_chains:
if rule.startswith(':'):
seen_chains = True
else:
if not rule.startswith(':'):
break
our_rules = []
for rule in rules:
rule_str = str(rule)
if rule.top:
# rule.top == True means we want this rule to be at the top.
# Further down, we weed out duplicates from the bottom of the
# list, so here we remove the dupes ahead of time.
new_filter = filter(lambda s: s.strip() != rule_str.strip(),
new_filter)
our_rules += [rule_str]
new_filter[rules_index:rules_index] = our_rules
new_filter[rules_index:rules_index] = [':%s - [0:0]' % \
(name,) \
for name in unwrapped_chains]
new_filter[rules_index:rules_index] = [':%s-%s - [0:0]' % \
(binary_name, name,) \
for name in chains]
seen_lines = set()
def _weed_out_duplicates(line):
line = line.strip()
if line in seen_lines:
return False
else:
seen_lines.add(line)
return True
# We filter duplicates, letting the *last* occurrence take
# precendence.
new_filter.reverse()
new_filter = filter(_weed_out_duplicates, new_filter)
new_filter.reverse()
return new_filter
iptables_manager = IptablesManager()
def metadata_forward():
"""Create forwarding rule for metadata"""
_confirm_rule("PREROUTING", '-t', 'nat', '-s', '0.0.0.0/0',
'-d', '169.254.169.254/32', '-p', 'tcp', '-m', 'tcp',
'--dport', '80', '-j', 'DNAT',
'--to-destination',
'%s:%s' % (FLAGS.ec2_dmz_host, FLAGS.ec2_port))
iptables_manager.ipv4['nat'].add_rule("PREROUTING",
"-s 0.0.0.0/0 -d 169.254.169.254/32 "
"-p tcp -m tcp --dport 80 -j DNAT "
"--to-destination %s:%s" % \
(FLAGS.ec2_dmz_host, FLAGS.ec2_port))
iptables_manager.apply()
def init_host():
"""Basic networking setup goes here"""
if FLAGS.use_nova_chains:
_execute('sudo', 'iptables', '-N', 'nova_input', check_exit_code=False)
_execute('sudo', 'iptables', '-D', FLAGS.input_chain,
'-j', 'nova_input',
check_exit_code=False)
_execute('sudo', 'iptables', '-A', FLAGS.input_chain,
'-j', 'nova_input')
_execute('sudo', 'iptables', '-N', 'nova_forward',
check_exit_code=False)
_execute('sudo', 'iptables', '-D', 'FORWARD', '-j', 'nova_forward',
check_exit_code=False)
_execute('sudo', 'iptables', '-A', 'FORWARD', '-j', 'nova_forward')
_execute('sudo', 'iptables', '-N', 'nova_output',
check_exit_code=False)
_execute('sudo', 'iptables', '-D', 'OUTPUT', '-j', 'nova_output',
check_exit_code=False)
_execute('sudo', 'iptables', '-A', 'OUTPUT', '-j', 'nova_output')
_execute('sudo', 'iptables', '-t', 'nat', '-N', 'nova_prerouting',
check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-D', 'PREROUTING',
'-j', 'nova_prerouting', check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-A', 'PREROUTING',
'-j', 'nova_prerouting')
_execute('sudo', 'iptables', '-t', 'nat', '-N', 'nova_postrouting',
check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-D', 'POSTROUTING',
'-j', 'nova_postrouting', check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-A', 'POSTROUTING',
'-j', 'nova_postrouting')
_execute('sudo', 'iptables', '-t', 'nat', '-N', 'nova_snatting',
check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-D', 'POSTROUTING',
'-j nova_snatting', check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-A', 'POSTROUTING',
'-j', 'nova_snatting')
_execute('sudo', 'iptables', '-t', 'nat', '-N', 'nova_output',
check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-D', 'OUTPUT',
'-j nova_output', check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-A', 'OUTPUT',
'-j', 'nova_output')
else:
# NOTE(vish): This makes it easy to ensure snatting rules always
# come after the accept rules in the postrouting chain
_execute('sudo', 'iptables', '-t', 'nat', '-N', 'SNATTING',
check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-D', 'POSTROUTING',
'-j', 'SNATTING', check_exit_code=False)
_execute('sudo', 'iptables', '-t', 'nat', '-A', 'POSTROUTING',
'-j', 'SNATTING')
# NOTE(devcamcar): Cloud public SNAT entries and the default
# SNAT rule for outbound traffic.
_confirm_rule("SNATTING", '-t', 'nat', '-s', FLAGS.fixed_range,
'-j', 'SNAT', '--to-source', FLAGS.routing_source_ip,
append=True)
iptables_manager.ipv4['nat'].add_rule("snat",
"-s %s -j SNAT --to-source %s" % \
(FLAGS.fixed_range,
FLAGS.routing_source_ip))
_confirm_rule("POSTROUTING", '-t', 'nat', '-s', FLAGS.fixed_range,
'-d', FLAGS.dmz_cidr, '-j', 'ACCEPT')
_confirm_rule("POSTROUTING", '-t', 'nat', '-s', FLAGS.fixed_range,
'-d', FLAGS.fixed_range, '-j', 'ACCEPT')
iptables_manager.ipv4['nat'].add_rule("POSTROUTING",
"-s %s -d %s -j ACCEPT" % \
(FLAGS.fixed_range, FLAGS.dmz_cidr))
iptables_manager.ipv4['nat'].add_rule("POSTROUTING",
"-s %(range)s -d %(range)s "
"-j ACCEPT" % \
{'range': FLAGS.fixed_range})
iptables_manager.apply()
def bind_floating_ip(floating_ip, check_exit_code=True):
@ -153,31 +406,36 @@ def unbind_floating_ip(floating_ip):
def ensure_vlan_forward(public_ip, port, private_ip):
"""Sets up forwarding rules for vlan"""
_confirm_rule("FORWARD", '-d', private_ip, '-p', 'udp',
'--dport', '1194', '-j', 'ACCEPT')
_confirm_rule("PREROUTING", '-t', 'nat', '-d', public_ip, '-p', 'udp',
'--dport', port, '-j', 'DNAT', '--to', '%s:1194'
% private_ip)
iptables_manager.ipv4['filter'].add_rule("FORWARD",
"-d %s -p udp "
"--dport 1194 "
"-j ACCEPT" % private_ip)
iptables_manager.ipv4['nat'].add_rule("PREROUTING",
"-d %s -p udp "
"--dport %s -j DNAT --to %s:1194" %
(public_ip, port, private_ip))
iptables_manager.apply()
def ensure_floating_forward(floating_ip, fixed_ip):
"""Ensure floating ip forwarding rule"""
_confirm_rule("PREROUTING", '-t', 'nat', '-d', floating_ip, '-j', 'DNAT',
'--to', fixed_ip)
_confirm_rule("OUTPUT", '-t', 'nat', '-d', floating_ip, '-j', 'DNAT',
'--to', fixed_ip)
_confirm_rule("SNATTING", '-t', 'nat', '-s', fixed_ip, '-j', 'SNAT',
'--to', floating_ip)
for chain, rule in floating_forward_rules(floating_ip, fixed_ip):
iptables_manager.ipv4['nat'].add_rule(chain, rule)
iptables_manager.apply()
def remove_floating_forward(floating_ip, fixed_ip):
"""Remove forwarding for floating ip"""
_remove_rule("PREROUTING", '-t', 'nat', '-d', floating_ip, '-j', 'DNAT',
'--to', fixed_ip)
_remove_rule("OUTPUT", '-t', 'nat', '-d', floating_ip, '-j', 'DNAT',
'--to', fixed_ip)
_remove_rule("SNATTING", '-t', 'nat', '-s', fixed_ip, '-j', 'SNAT',
'--to', floating_ip)
for chain, rule in floating_forward_rules(floating_ip, fixed_ip):
iptables_manager.ipv4['nat'].remove_rule(chain, rule)
iptables_manager.apply()
def floating_forward_rules(floating_ip, fixed_ip):
return [("PREROUTING", "-d %s -j DNAT --to %s" % (floating_ip, fixed_ip)),
("OUTPUT", "-d %s -j DNAT --to %s" % (floating_ip, fixed_ip)),
("floating-snat",
"-s %s -j SNAT --to %s" % (fixed_ip, floating_ip))]
def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
@ -269,19 +527,12 @@ def ensure_bridge(bridge, interface, net_attrs=None):
"enslave it to bridge %s.\n" % (interface, bridge)):
raise exception.Error("Failed to add interface: %s" % err)
if FLAGS.use_nova_chains:
(out, err) = _execute('sudo', 'iptables', '-N', 'nova_forward',
check_exit_code=False)
if err != 'iptables: Chain already exists.\n':
# NOTE(vish): chain didn't exist link chain
_execute('sudo', 'iptables', '-D', 'FORWARD', '-j', 'nova_forward',
check_exit_code=False)
_execute('sudo', 'iptables', '-A', 'FORWARD', '-j', 'nova_forward')
_confirm_rule("FORWARD", '--in-interface', bridge, '-j', 'ACCEPT')
_confirm_rule("FORWARD", '--out-interface', bridge, '-j', 'ACCEPT')
_execute('sudo', 'iptables', '-N', 'nova-local', check_exit_code=False)
_confirm_rule("FORWARD", '-j', 'nova-local')
iptables_manager.ipv4['filter'].add_rule("FORWARD",
"--in-interface %s -j ACCEPT" % \
bridge)
iptables_manager.ipv4['filter'].add_rule("FORWARD",
"--out-interface %s -j ACCEPT" % \
bridge)
def get_dhcp_hosts(context, network_id):
@ -401,27 +652,6 @@ def _device_exists(device):
return not err
def _confirm_rule(chain, *cmd, **kwargs):
append = kwargs.get('append', False)
"""Delete and re-add iptables rule"""
if FLAGS.use_nova_chains:
chain = "nova_%s" % chain.lower()
if append:
loc = "-A"
else:
loc = "-I"
_execute('sudo', 'iptables', '--delete', chain, *cmd,
check_exit_code=False)
_execute('sudo', 'iptables', loc, chain, *cmd)
def _remove_rule(chain, *cmd):
"""Remove iptables rule"""
if FLAGS.use_nova_chains:
chain = "%s" % chain.lower()
_execute('sudo', 'iptables', '--delete', chain, *cmd)
def _dnsmasq_cmd(net):
"""Builds dnsmasq command"""
cmd = ['sudo', '-E', 'dnsmasq',

View File

@ -29,11 +29,153 @@ from nova import log as logging
from nova import test
from nova import utils
from nova.auth import manager
from nova.network import linux_net
FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.network')
class IptablesManagerTestCase(test.TestCase):
sample_filter = ['#Generated by iptables-save on Fri Feb 18 15:17:05 2011',
'*filter',
':INPUT ACCEPT [2223527:305688874]',
':FORWARD ACCEPT [0:0]',
':OUTPUT ACCEPT [2172501:140856656]',
':nova-compute-FORWARD - [0:0]',
':nova-compute-INPUT - [0:0]',
':nova-compute-local - [0:0]',
':nova-compute-OUTPUT - [0:0]',
':nova-filter-top - [0:0]',
'-A FORWARD -j nova-filter-top ',
'-A OUTPUT -j nova-filter-top ',
'-A nova-filter-top -j nova-compute-local ',
'-A INPUT -j nova-compute-INPUT ',
'-A OUTPUT -j nova-compute-OUTPUT ',
'-A FORWARD -j nova-compute-FORWARD ',
'-A INPUT -i virbr0 -p udp -m udp --dport 53 -j ACCEPT ',
'-A INPUT -i virbr0 -p tcp -m tcp --dport 53 -j ACCEPT ',
'-A INPUT -i virbr0 -p udp -m udp --dport 67 -j ACCEPT ',
'-A INPUT -i virbr0 -p tcp -m tcp --dport 67 -j ACCEPT ',
'-A FORWARD -s 192.168.122.0/24 -i virbr0 -j ACCEPT ',
'-A FORWARD -i virbr0 -o virbr0 -j ACCEPT ',
'-A FORWARD -o virbr0 -j REJECT --reject-with '
'icmp-port-unreachable ',
'-A FORWARD -i virbr0 -j REJECT --reject-with '
'icmp-port-unreachable ',
'COMMIT',
'# Completed on Fri Feb 18 15:17:05 2011']
sample_nat = ['# Generated by iptables-save on Fri Feb 18 15:17:05 2011',
'*nat',
':PREROUTING ACCEPT [3936:762355]',
':INPUT ACCEPT [2447:225266]',
':OUTPUT ACCEPT [63491:4191863]',
':POSTROUTING ACCEPT [63112:4108641]',
':nova-compute-OUTPUT - [0:0]',
':nova-compute-floating-ip-snat - [0:0]',
':nova-compute-SNATTING - [0:0]',
':nova-compute-PREROUTING - [0:0]',
':nova-compute-POSTROUTING - [0:0]',
':nova-postrouting-bottom - [0:0]',
'-A PREROUTING -j nova-compute-PREROUTING ',
'-A OUTPUT -j nova-compute-OUTPUT ',
'-A POSTROUTING -j nova-compute-POSTROUTING ',
'-A POSTROUTING -j nova-postrouting-bottom ',
'-A nova-postrouting-bottom -j nova-compute-SNATTING ',
'-A nova-compute-SNATTING -j nova-compute-floating-ip-snat ',
'COMMIT',
'# Completed on Fri Feb 18 15:17:05 2011']
def setUp(self):
super(IptablesManagerTestCase, self).setUp()
self.manager = linux_net.IptablesManager()
def test_filter_rules_are_wrapped(self):
current_lines = self.sample_filter
table = self.manager.ipv4['filter']
table.add_rule('FORWARD', '-s 1.2.3.4/5 -j DROP')
new_lines = self.manager._modify_rules(current_lines, table)
self.assertTrue('-A run_tests.py-FORWARD '
'-s 1.2.3.4/5 -j DROP' in new_lines)
table.remove_rule('FORWARD', '-s 1.2.3.4/5 -j DROP')
new_lines = self.manager._modify_rules(current_lines, table)
self.assertTrue('-A run_tests.py-FORWARD '
'-s 1.2.3.4/5 -j DROP' not in new_lines)
def test_nat_rules(self):
current_lines = self.sample_nat
new_lines = self.manager._modify_rules(current_lines,
self.manager.ipv4['nat'])
for line in [':nova-compute-OUTPUT - [0:0]',
':nova-compute-floating-ip-snat - [0:0]',
':nova-compute-SNATTING - [0:0]',
':nova-compute-PREROUTING - [0:0]',
':nova-compute-POSTROUTING - [0:0]']:
self.assertTrue(line in new_lines, "One of nova-compute's chains "
"went missing.")
seen_lines = set()
for line in new_lines:
line = line.strip()
self.assertTrue(line not in seen_lines,
"Duplicate line: %s" % line)
seen_lines.add(line)
last_postrouting_line = ''
for line in new_lines:
if line.startswith('-A POSTROUTING'):
last_postrouting_line = line
self.assertTrue('-j nova-postrouting-bottom' in last_postrouting_line,
"Last POSTROUTING rule does not jump to "
"nova-postouting-bottom: %s" % last_postrouting_line)
for chain in ['POSTROUTING', 'PREROUTING', 'OUTPUT']:
self.assertTrue('-A %s -j run_tests.py-%s' \
% (chain, chain) in new_lines,
"Built-in chain %s not wrapped" % (chain,))
def test_filter_rules(self):
current_lines = self.sample_filter
new_lines = self.manager._modify_rules(current_lines,
self.manager.ipv4['filter'])
for line in [':nova-compute-FORWARD - [0:0]',
':nova-compute-INPUT - [0:0]',
':nova-compute-local - [0:0]',
':nova-compute-OUTPUT - [0:0]']:
self.assertTrue(line in new_lines, "One of nova-compute's chains"
" went missing.")
seen_lines = set()
for line in new_lines:
line = line.strip()
self.assertTrue(line not in seen_lines,
"Duplicate line: %s" % line)
seen_lines.add(line)
for chain in ['FORWARD', 'OUTPUT']:
for line in new_lines:
if line.startswith('-A %s' % chain):
self.assertTrue('-j nova-filter-top' in line,
"First %s rule does not "
"jump to nova-filter-top" % chain)
break
self.assertTrue('-A nova-filter-top '
'-j run_tests.py-local' in new_lines,
"nova-filter-top does not jump to wrapped local chain")
for chain in ['INPUT', 'OUTPUT', 'FORWARD']:
self.assertTrue('-A %s -j run_tests.py-%s' \
% (chain, chain) in new_lines,
"Built-in chain %s not wrapped" % (chain,))
class NetworkTestCase(test.TestCase):
"""Test cases for network code"""
def setUp(self):

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import re
import os
import eventlet
@ -301,16 +302,22 @@ class IptablesFirewallTestCase(test.TestCase):
self.manager.delete_user(self.user)
super(IptablesFirewallTestCase, self).tearDown()
in_rules = [
in_nat_rules = [
'# Generated by iptables-save v1.4.10 on Sat Feb 19 00:03:19 2011',
'*nat',
':PREROUTING ACCEPT [1170:189210]',
':INPUT ACCEPT [844:71028]',
':OUTPUT ACCEPT [5149:405186]',
':POSTROUTING ACCEPT [5063:386098]'
]
in_filter_rules = [
'# Generated by iptables-save v1.4.4 on Mon Dec 6 11:54:13 2010',
'*filter',
':INPUT ACCEPT [969615:281627771]',
':FORWARD ACCEPT [0:0]',
':OUTPUT ACCEPT [915599:63811649]',
':nova-block-ipv4 - [0:0]',
'-A INPUT -i virbr0 -p udp -m udp --dport 53 -j ACCEPT ',
'-A INPUT -i virbr0 -p tcp -m tcp --dport 53 -j ACCEPT ',
'-A INPUT -i virbr0 -p udp -m udp --dport 67 -j ACCEPT ',
'-A INPUT -i virbr0 -p tcp -m tcp --dport 67 -j ACCEPT ',
'-A FORWARD -d 192.168.122.0/24 -o virbr0 -m state --state RELATED'
',ESTABLISHED -j ACCEPT ',
@ -322,7 +329,7 @@ class IptablesFirewallTestCase(test.TestCase):
'# Completed on Mon Dec 6 11:54:13 2010',
]
in6_rules = [
in6_filter_rules = [
'# Generated by ip6tables-save v1.4.4 on Tue Jan 18 23:47:56 2011',
'*filter',
':INPUT ACCEPT [349155:75810423]',
@ -385,21 +392,31 @@ class IptablesFirewallTestCase(test.TestCase):
def fake_iptables_execute(*cmd, **kwargs):
process_input = kwargs.get('process_input', None)
if cmd == ('sudo', 'ip6tables-save', '-t', 'filter'):
return '\n'.join(self.in6_rules), None
return '\n'.join(self.in6_filter_rules), None
if cmd == ('sudo', 'iptables-save', '-t', 'filter'):
return '\n'.join(self.in_rules), None
return '\n'.join(self.in_filter_rules), None
if cmd == ('sudo', 'iptables-save', '-t', 'nat'):
return '\n'.join(self.in_nat_rules), None
if cmd == ('sudo', 'iptables-restore'):
self.out_rules = process_input.split('\n')
lines = process_input.split('\n')
if '*filter' in lines:
self.out_rules = lines
return '', ''
if cmd == ('sudo', 'ip6tables-restore'):
self.out6_rules = process_input.split('\n')
lines = process_input.split('\n')
if '*filter' in lines:
self.out6_rules = lines
return '', ''
self.fw.execute = fake_iptables_execute
print cmd, kwargs
from nova.network import linux_net
linux_net.iptables_manager.execute = fake_iptables_execute
self.fw.prepare_instance_filter(instance_ref)
self.fw.apply_instance_filter(instance_ref)
in_rules = filter(lambda l: not l.startswith('#'), self.in_rules)
in_rules = filter(lambda l: not l.startswith('#'),
self.in_filter_rules)
for rule in in_rules:
if not 'nova' in rule:
self.assertTrue(rule in self.out_rules,
@ -422,17 +439,18 @@ class IptablesFirewallTestCase(test.TestCase):
self.assertTrue(security_group_chain,
"The security group chain wasn't added")
self.assertTrue('-A %s -p icmp -s 192.168.11.0/24 -j ACCEPT' % \
security_group_chain in self.out_rules,
regex = re.compile('-A .* -p icmp -s 192.168.11.0/24 -j ACCEPT')
self.assertTrue(len(filter(regex.match, self.out_rules)) > 0,
"ICMP acceptance rule wasn't added")
self.assertTrue('-A %s -p icmp -s 192.168.11.0/24 -m icmp --icmp-type '
'8 -j ACCEPT' % security_group_chain in self.out_rules,
regex = re.compile('-A .* -p icmp -s 192.168.11.0/24 -m icmp '
'--icmp-type 8 -j ACCEPT')
self.assertTrue(len(filter(regex.match, self.out_rules)) > 0,
"ICMP Echo Request acceptance rule wasn't added")
self.assertTrue('-A %s -p tcp -s 192.168.10.0/24 -m multiport '
'--dports 80:81 -j ACCEPT' % security_group_chain \
in self.out_rules,
regex = re.compile('-A .* -p tcp -s 192.168.10.0/24 -m multiport '
'--dports 80:81 -j ACCEPT')
self.assertTrue(len(filter(regex.match, self.out_rules)) > 0,
"TCP port 80/81 acceptance rule wasn't added")
db.instance_destroy(admin_ctxt, instance_ref['id'])

View File

@ -139,34 +139,44 @@ def execute(*cmd, **kwargs):
stdin = kwargs.get('stdin', subprocess.PIPE)
stdout = kwargs.get('stdout', subprocess.PIPE)
stderr = kwargs.get('stderr', subprocess.PIPE)
attempts = kwargs.get('attempts', 1)
cmd = map(str, cmd)
LOG.debug(_("Running cmd (subprocess): %s"), ' '.join(cmd))
env = os.environ.copy()
if addl_env:
env.update(addl_env)
obj = subprocess.Popen(cmd, stdin=stdin,
stdout=stdout, stderr=stderr, env=env)
result = None
if process_input != None:
result = obj.communicate(process_input)
else:
result = obj.communicate()
obj.stdin.close()
if obj.returncode:
LOG.debug(_("Result was %s") % obj.returncode)
if type(check_exit_code) == types.IntType \
and obj.returncode != check_exit_code:
(stdout, stderr) = result
raise ProcessExecutionError(exit_code=obj.returncode,
stdout=stdout,
stderr=stderr,
cmd=' '.join(cmd))
# NOTE(termie): this appears to be necessary to let the subprocess call
# clean something up in between calls, without it two
# execute calls in a row hangs the second one
greenthread.sleep(0)
return result
while attempts > 0:
attempts -= 1
try:
LOG.debug(_("Running cmd (subprocess): %s"), ' '.join(cmd))
env = os.environ.copy()
if addl_env:
env.update(addl_env)
obj = subprocess.Popen(cmd, stdin=stdin,
stdout=stdout, stderr=stderr, env=env)
result = None
if process_input != None:
result = obj.communicate(process_input)
else:
result = obj.communicate()
obj.stdin.close()
if obj.returncode:
LOG.debug(_("Result was %s") % obj.returncode)
if type(check_exit_code) == types.IntType \
and obj.returncode != check_exit_code:
(stdout, stderr) = result
raise ProcessExecutionError(exit_code=obj.returncode,
stdout=stdout,
stderr=stderr,
cmd=' '.join(cmd))
# NOTE(termie): this appears to be necessary to let the subprocess
# call clean something up in between calls, without
# it two execute calls in a row hangs the second one
greenthread.sleep(0)
return result
except ProcessExecutionError:
if not attempts:
raise
else:
LOG.debug(_("%r failed. Retrying."), cmd)
greenthread.sleep(random.randint(20, 200) / 100.0)
def ssh_execute(ssh, cmd, process_input=None,

View File

@ -56,7 +56,6 @@ from nova import flags
from nova import log as logging
#from nova import test
from nova import utils
#from nova.api import context
from nova.auth import manager
from nova.compute import instance_types
from nova.compute import power_state
@ -1222,10 +1221,14 @@ class NWFilterFirewall(FirewallDriver):
class IptablesFirewallDriver(FirewallDriver):
def __init__(self, execute=None, **kwargs):
self.execute = execute or utils.execute
from nova.network import linux_net
self.iptables = linux_net.iptables_manager
self.instances = {}
self.nwfilter = NWFilterFirewall(kwargs['get_connection'])
self.iptables.ipv4['filter'].add_chain('sg-fallback')
self.iptables.ipv4['filter'].add_rule('sg-fallback', '-j DROP')
def setup_basic_filtering(self, instance):
"""Use NWFilter from libvirt for this."""
return self.nwfilter.setup_basic_filtering(instance)
@ -1234,128 +1237,97 @@ class IptablesFirewallDriver(FirewallDriver):
"""No-op. Everything is done in prepare_instance_filter"""
pass
def remove_instance(self, instance):
def unfilter_instance(self, instance):
if instance['id'] in self.instances:
del self.instances[instance['id']]
self.remove_filters_for_instance(instance)
self.iptables.apply()
else:
LOG.info(_('Attempted to unfilter instance %s which is not '
'filtered'), instance['id'])
def add_instance(self, instance):
self.instances[instance['id']] = instance
def unfilter_instance(self, instance):
self.remove_instance(instance)
self.apply_ruleset()
def prepare_instance_filter(self, instance):
self.add_instance(instance)
self.apply_ruleset()
self.instances[instance['id']] = instance
self.add_filters_for_instance(instance)
self.iptables.apply()
def apply_ruleset(self):
current_filter, _ = self.execute('sudo', 'iptables-save',
'-t', 'filter')
current_lines = current_filter.split('\n')
new_filter = self.modify_rules(current_lines, 4)
self.execute('sudo', 'iptables-restore',
process_input='\n'.join(new_filter))
if(FLAGS.use_ipv6):
current_filter, _ = self.execute('sudo', 'ip6tables-save',
'-t', 'filter')
current_lines = current_filter.split('\n')
new_filter = self.modify_rules(current_lines, 6)
self.execute('sudo', 'ip6tables-restore',
process_input='\n'.join(new_filter))
def add_filters_for_instance(self, instance):
chain_name = self._instance_chain_name(instance)
def modify_rules(self, current_lines, ip_version=4):
self.iptables.ipv4['filter'].add_chain(chain_name)
ipv4_address = self._ip_for_instance(instance)
self.iptables.ipv4['filter'].add_rule('local',
'-d %s -j $%s' %
(ipv4_address, chain_name))
if FLAGS.use_ipv6:
self.iptables.ipv6['filter'].add_chain(chain_name)
ipv6_address = self._ip_for_instance_v6(instance)
self.iptables.ipv6['filter'].add_rule('local',
'-d %s -j $%s' %
(ipv6_address,
chain_name))
ipv4_rules, ipv6_rules = self.instance_rules(instance)
for rule in ipv4_rules:
self.iptables.ipv4['filter'].add_rule(chain_name, rule)
if FLAGS.use_ipv6:
for rule in ipv6_rules:
self.iptables.ipv6['filter'].add_rule(chain_name, rule)
def remove_filters_for_instance(self, instance):
chain_name = self._instance_chain_name(instance)
self.iptables.ipv4['filter'].remove_chain(chain_name)
if FLAGS.use_ipv6:
self.iptables.ipv6['filter'].remove_chain(chain_name)
def instance_rules(self, instance):
ctxt = context.get_admin_context()
# Remove any trace of nova rules.
new_filter = filter(lambda l: 'nova-' not in l, current_lines)
seen_chains = False
for rules_index in range(len(new_filter)):
if not seen_chains:
if new_filter[rules_index].startswith(':'):
seen_chains = True
elif seen_chains == 1:
if not new_filter[rules_index].startswith(':'):
break
ipv4_rules = []
ipv6_rules = []
our_chains = [':nova-fallback - [0:0]']
our_rules = ['-A nova-fallback -j DROP']
# Always drop invalid packets
ipv4_rules += ['-m state --state ' 'INVALID -j DROP']
ipv6_rules += ['-m state --state ' 'INVALID -j DROP']
our_chains += [':nova-local - [0:0]']
our_rules += ['-A FORWARD -j nova-local']
our_rules += ['-A OUTPUT -j nova-local']
# Allow established connections
ipv4_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
ipv6_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
security_groups = {}
# Add our chains
# First, we add instance chains and rules
for instance_id in self.instances:
instance = self.instances[instance_id]
chain_name = self._instance_chain_name(instance)
if(ip_version == 4):
ip_address = self._ip_for_instance(instance)
elif(ip_version == 6):
ip_address = self._ip_for_instance_v6(instance)
dhcp_server = self._dhcp_server_for_instance(instance)
ipv4_rules += ['-s %s -p udp --sport 67 --dport 68 '
'-j ACCEPT' % (dhcp_server,)]
our_chains += [':%s - [0:0]' % chain_name]
#Allow project network traffic
if FLAGS.allow_project_net_traffic:
cidr = self._project_cidr_for_instance(instance)
ipv4_rules += ['-s %s -j ACCEPT' % (cidr,)]
# Jump to the per-instance chain
our_rules += ['-A nova-local -d %s -j %s' % (ip_address,
chain_name)]
# We wrap these in FLAGS.use_ipv6 because they might cause
# a DB lookup. The other ones are just list operations, so
# they're not worth the clutter.
if FLAGS.use_ipv6:
# Allow RA responses
ra_server = self._ra_server_for_instance(instance)
if ra_server:
ipv6_rules += ['-s %s/128 -p icmpv6 -j ACCEPT' % (ra_server,)]
# Always drop invalid packets
our_rules += ['-A %s -m state --state '
'INVALID -j DROP' % (chain_name,)]
#Allow project network traffic
if FLAGS.allow_project_net_traffic:
cidrv6 = self._project_cidrv6_for_instance(instance)
ipv6_rules += ['-s %s -j ACCEPT' % (cidrv6,)]
# Allow established connections
our_rules += ['-A %s -m state --state '
'ESTABLISHED,RELATED -j ACCEPT' % (chain_name,)]
# Jump to each security group chain in turn
for security_group in \
db.security_group_get_by_instance(ctxt,
instance['id']):
security_groups[security_group['id']] = security_group
sg_chain_name = self._security_group_chain_name(
security_group['id'])
our_rules += ['-A %s -j %s' % (chain_name, sg_chain_name)]
if(ip_version == 4):
# Allow DHCP responses
dhcp_server = self._dhcp_server_for_instance(instance)
our_rules += ['-A %s -s %s -p udp --sport 67 --dport 68 '
'-j ACCEPT ' % (chain_name, dhcp_server)]
#Allow project network traffic
if (FLAGS.allow_project_net_traffic):
cidr = self._project_cidr_for_instance(instance)
our_rules += ['-A %s -s %s -j ACCEPT' % (chain_name, cidr)]
elif(ip_version == 6):
# Allow RA responses
ra_server = self._ra_server_for_instance(instance)
if ra_server:
our_rules += ['-A %s -s %s -p icmpv6 -j ACCEPT' %
(chain_name, ra_server + "/128")]
#Allow project network traffic
if (FLAGS.allow_project_net_traffic):
cidrv6 = self._project_cidrv6_for_instance(instance)
our_rules += ['-A %s -s %s -j ACCEPT' %
(chain_name, cidrv6)]
# If nothing matches, jump to the fallback chain
our_rules += ['-A %s -j nova-fallback' % (chain_name,)]
security_groups = db.security_group_get_by_instance(ctxt,
instance['id'])
# then, security group chains and rules
for security_group_id in security_groups:
chain_name = self._security_group_chain_name(security_group_id)
our_chains += [':%s - [0:0]' % chain_name]
rules = \
db.security_group_rule_get_by_security_group(ctxt,
security_group_id)
for security_group in security_groups:
rules = db.security_group_rule_get_by_security_group(ctxt,
security_group['id'])
for rule in rules:
logging.info('%r', rule)
@ -1366,14 +1338,16 @@ class IptablesFirewallDriver(FirewallDriver):
continue
version = _get_ip_version(rule.cidr)
if version != ip_version:
continue
if version == 4:
rules = ipv4_rules
else:
rules = ipv6_rules
protocol = rule.protocol
if version == 6 and rule.protocol == 'icmp':
protocol = 'icmpv6'
args = ['-A', chain_name, '-p', protocol, '-s', rule.cidr]
args = ['-p', protocol, '-s', rule.cidr]
if rule.protocol in ['udp', 'tcp']:
if rule.from_port == rule.to_port:
@ -1394,32 +1368,39 @@ class IptablesFirewallDriver(FirewallDriver):
icmp_type_arg += '/%s' % icmp_code
if icmp_type_arg:
if(ip_version == 4):
if version == 4:
args += ['-m', 'icmp', '--icmp-type',
icmp_type_arg]
elif(ip_version == 6):
elif version == 6:
args += ['-m', 'icmp6', '--icmpv6-type',
icmp_type_arg]
args += ['-j ACCEPT']
our_rules += [' '.join(args)]
rules += [' '.join(args)]
new_filter[rules_index:rules_index] = our_rules
new_filter[rules_index:rules_index] = our_chains
logging.info('new_filter: %s', '\n'.join(new_filter))
return new_filter
ipv4_rules += ['-j $sg-fallback']
ipv6_rules += ['-j $sg-fallback']
return ipv4_rules, ipv6_rules
def refresh_security_group_members(self, security_group):
pass
def refresh_security_group_rules(self, security_group):
self.apply_ruleset()
for instance in self.instances.values():
# We use the semaphore to make sure noone applies the rule set
# after we've yanked the existing rules but before we've put in
# the new ones.
with self.iptables.semaphore:
self.remove_filters_for_instance(instance)
self.add_filters_for_instance(instance)
self.iptables.apply()
def _security_group_chain_name(self, security_group_id):
return 'nova-sg-%s' % (security_group_id,)
def _instance_chain_name(self, instance):
return 'nova-inst-%s' % (instance['id'],)
return 'inst-%s' % (instance['id'],)
def _ip_for_instance(self, instance):
return db.instance_get_fixed_address(context.get_admin_context(),