2021SC@SDUSC
今天我们聚焦Neutron的iptables
基础概念
iptables的工作是网络的准入控制,主要完成封包过滤、封包重定向和网络地址转换(NAT)等功能。
iptables的核心数据结构是表->链->规则,它们的关系在博客iptables详解(1):iptables概念中讲得非常清晰。阅读本文前推荐看一遍这篇博客。
这里我主要从源代码出发展示这些关系是如何被实现的。
agent/linux/iptables_manager.py
这个文件是iptables的根据地,核心数据结构在此定义。这个文件中共有3个类:IptablesRule, IptablesTable, IptablesManager。
其中,IptablesRules定义了“规则”的结构:chain, comment, rule(), tag, top, wrap, wrap_name。这个类应该被认为是私有类,只应由IptablesTable和IptablesManager代为访问。
IptablesTable定义了“表”的结构:rules(该表中的规则,注意,一个规则其实是与一个chain绑定的,但这种关联体现在rule的内部,即,将这个rule关联的chain作为这个IptablesRules的属性存在chain成员中),remove_rules[] (要被删除的规则),chains(链,每条链对应一个状态转换的时间点,例如“输入时”、“转发前”等),unwrapped_chains(未被包装的链),remove_chains(要被删除的链),wrap_name(包装名)。IptablesTable封装了一些关于规则和链的操作:add_chain(…), remove_chain(…), add_rule(…), remove_rule(…), empty_chain(…), clear_rules_by_tag(…),和一些被上述操作使用的私有函数。
IptablesManager是对iptables的控制类。它有两张IptablesTable,分别是filter和nat。初始化IptablesManager时,将一些链加入了IptablesTables中:首先加入的是neutron-filter-top,它被加在了FORWARD和OUTPUT链前面,它的名字没有被包装,所以它可以被多个neutron worker共享。它是为那些需要在FORWARD和OUTPUT之前使用的规则设置的,它在ipv4和ipv6的表中都有。对于ipv4和ipv6, INPUT, OUTPUT, FORWARD过滤链都是被包装过的,意味着实际的INPUT链有一条规则是跳转到被包装过的INPUT链。此外,neutron-filter-top将跳转到一个名为local的包装过的链。对于ipv4,内置的PREROUTING, OUTPUT, POSTROUTINNG nat链的包装情况与filter表中相同,并在POSTROUTING链后面多加了一个snat链。
初始化代码如下:
def __init__(self, state_less=False, use_ipv6=False, nat=True,
namespace=None, binary_name=binary_name, external_lock=True):
self.use_ipv6 = use_ipv6
self.namespace = namespace
self.iptables_apply_deferred = False
self.wrap_name = binary_name[:16]
self.external_lock = external_lock
self.ipv4 = {'filter': IptablesTable(binary_name=self.wrap_name)}
self.ipv6 = {'filter': IptablesTable(binary_name=self.wrap_name)}
# Add a neutron-filter-top chain. It's intended to be shared
# among the various neutron components. It sits at the very top
# of FORWARD and OUTPUT.
for tables in [self.ipv4, self.ipv6]:
tables['filter'].add_chain('neutron-filter-top', wrap=False)
tables['filter'].add_rule('FORWARD', '-j neutron-filter-top',
wrap=False, top=True)
tables['filter'].add_rule('OUTPUT', '-j neutron-filter-top',
wrap=False, top=True)
tables['filter'].add_chain('local')
tables['filter'].add_rule('neutron-filter-top', '-j $local',
wrap=False)
self.ipv4.update({'raw': IptablesTable(binary_name=self.wrap_name)})
self.ipv6.update({'raw': IptablesTable(binary_name=self.wrap_name)})
# Wrap the built-in chains
builtin_chains = {4: {'filter': ['INPUT', 'OUTPUT', 'FORWARD']},
6: {'filter': ['INPUT', 'OUTPUT', 'FORWARD']}}
builtin_chains[4].update({'raw': ['PREROUTING', 'OUTPUT']})
builtin_chains[6].update({'raw': ['PREROUTING', 'OUTPUT']})
self._configure_builtin_chains(builtin_chains)
if not state_less:
self.initialize_mangle_table()
if nat:
self.initialize_nat_table()
注意到,除了直接初始化filter表外,它还调用initialize_mangle_table()和initialize_nat_table()初始化了mangle表和nat表。nat表结构上文已经讲解。mangle表的主要功能是根据规则修改数据包的一些标志位,以便其他规则或程序可以利用这种标志对数据包进行过滤或策略路由,其中有5条链:PREROUTING, INPUT, FORWARD, OUTPUT, POSROUTING。
IptablesTable还提供了很多管理iptables的函数,例如get_tables(…), is_chain_empty(…), get_rules_for_table(…), defer_apply(…), apply(…)等,以及它们调用的一些私有函数。其中,apply(self)的运行过程不那么明了,因此拿出来分析一下:
def apply(self):
if self.iptables_apply_deferred:
return
return self._apply()
如果推迟应用,则直接返回;如果不推迟,则返回_ apply()的结果。_apply()是什么呢?
def _apply(self):
lock_name = 'iptables'
if self.namespace:
lock_name += '-' + self.namespace
# NOTE(ihrachys) we may get rid of the lock once all supported
# platforms get iptables with 999eaa241212d3952ddff39a99d0d55a74e3639e
# ("iptables-restore: support acquiring the lock.")
with lockutils.lock(lock_name, runtime.SYNCHRONIZED_PREFIX,
external=self.external_lock):
first = self._apply_synchronized()
if not cfg.CONF.AGENT.debug_iptables_rules:
return first
LOG.debug('List of IPTables Rules applied: %s', '\n'.join(first))
second = self._apply_synchronized()
if second:
msg = (_("IPTables Rules did not converge. Diff: %s") %
'\n'.join(second))
LOG.error(msg)
raise l3_exc.IpTablesApplyException(msg)
return first
它原子性地完成几个操作:先把内存中的iptable和之前运行留下的同步了一下,并返回同步后的iptable,如果关了debug_iptables_rules,则直接返回这个结果即可;如果打开了,则又做了一遍相同的同步并获取操作,如果发现第二次获取的和第一次不同,说明iptables rules不收敛,哪里出问题了,因此抛出异常。如无异常,返回第一次获取的(虽然返回第二次获取的也一样)。
这个apply()函数在很多很多地方都被调用到,作用修改了iptable内容之后把修改应用一下(就类似在idea中设置了一些东西,然后要点击apply)
agent/linux/iptables_firewall.py
看完iptables的定义,来看一个它在路由中的应用。这个函数里面定义了一个类IptalesFirewallDriver。Neutron用它来实现安全组(security group)功能。
它有很多成员变量,其中就有iptables,使用
iptables_manager.IptablesManager(state_less=True, use_ipv6=netutils.is_ipv6_enabled(), namespace=namespace) 进行初始化
IptablesFirewallDriver封装了非常多的函数,公开函数有:
security_group_updated(...),
process_trusted_ports(...),
remove_trusted_ports(...),
update_security_group_rules(...),
update_security_group_members(...),
prepare_port_filter(...),
update_port_filter(...),
remove_port_filter(...),
filter_defer_apply_on(...),
filter_defer_apply_off(...)
与iptables相关的主要是prepare_port_filter(…), update_port_filter(…),
remove_port_filter(…)三个:
def prepare_port_filter(self, port):
LOG.debug("Preparing device (%s) filter", port['device'])
self._set_ports(port)
# each security group has it own chains
self._setup_chains()
return self.iptables.apply()
def update_port_filter(self, port):
LOG.debug("Updating device (%s) filter", port['device'])
if port['device'] not in self.ports:
LOG.info('Attempted to update port filter which is not '
'filtered %s', port['device'])
return
self._remove_chains()
self._set_ports(port)
self._setup_chains()
return self.iptables.apply()
def remove_port_filter(self, port):
LOG.debug("Removing device (%s) filter", port['device'])
if port['device'] not in self.ports:
LOG.info('Attempted to remove port filter which is not '
'filtered %r', port)
return
self._remove_chains()
self._remove_conntrack_entries_from_port_deleted(port)
self._unset_ports(port)
self._setup_chains()
return self.iptables.apply()
它们调用的几个主要操作是self._ set_ports(port), self._ setup_chains(), self._ remove_chains(), self._ remove_conntrack_entries_from_port_deleted(port), self._ unset_ports(port)和iptables.apply()。
现在逐一看这几个操作拿port参数对iptables做了什么:
def _set_ports(self, port):
if not firewall.port_sec_enabled(port):
self.unfiltered_ports[port['device']] = port
self.filtered_ports.pop(port['device'], None)
else:
self.filtered_ports[port['device']] = port
self.unfiltered_ports.pop(port['device'], None)
如果这个port是被filtered的,就把它加入filtered_ports中,并从unfiltered_ports中把它拿出来,反之则相反操作。
def _setup_chains(self):
"""Setup ingress and egress chain for a port."""
if not self._defer_apply:
self._setup_chains_apply(self.filtered_ports,
self.unfiltered_ports)
它调用了self._setup_chains_apply(self.filtered_ports, self.unfiltered_ports),这个函数内部逻辑是:
def _setup_chains_apply(self, ports, unfiltered_ports):
self._add_chain_by_name_v4v6(SG_CHAIN)
# sort by port so we always do this deterministically between
# agent restarts and don't cause unnecessary rule differences
for pname in sorted(ports):
port = ports[pname]
self._add_conntrack_jump(port)
self._setup_chain(port, constants.INGRESS_DIRECTION)
self._setup_chain(port, constants.EGRESS_DIRECTION)
self.iptables.ipv4['filter'].add_rule(SG_CHAIN, '-j ACCEPT')
self.iptables.ipv6['filter'].add_rule(SG_CHAIN, '-j ACCEPT')
for port in unfiltered_ports.values():
self._add_accept_rule_port_sec(port, constants.INGRESS_DIRECTION)
self._add_accept_rule_port_sec(port, constants.EGRESS_DIRECTION)
把SG_CHAIN所指的’sg-chain’加入ipv4和ipv6的tables中。调用self._ add_conntrack_jump(port),对于ports中的每一个port,根据这个port相关的信息(比如device_zone, security group, device_owner等)生成若干条跳转规则,并将这些规则逐一加入PREROUTING链中。调用self._ setup_chain(port, constants.INGRESS_DIRECTION),根据这个port的设备信息和DIRECTION前缀生成一个链名,并将这个链加入ipv4和ipv6的表中,然后,为当前这个port和direction选择规则(区分ipv4和ipv6),分别加入这个port对应这个direction的链的ipv4和ipv6表中。然后,在iptables的ipv4和ipv6的表中添加属于SG_CHAIN链的规则’-j ACCEPT’。对于每个未被filter的port,在这个port对应这个direction的链的ipv4和ipv6表中的FORWARD, INPUT链中添加跳转规则
jump_rule = ['-m physdev --%s %s --physdev-is-bridged '
'-j ACCEPT' % (self.IPTABLES_DIRECTION[direction],
device)]
那几个remove_xxx函数基本是相反的操作,在此不多说明了。
总结
最后用开头提到的博客中的一个图总结iptable在防火墙中的作用:
再次感谢iptables详解(1):iptables概念的作者朱双印的详细、清晰的讲解。