|
1 # Copyright 2012, Nachi Ueno, NTT MCL, Inc. |
|
2 # All Rights Reserved. |
|
3 # |
|
4 # Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. |
|
5 # |
|
6 # Licensed under the Apache License, Version 2.0 (the "License"); you may |
|
7 # not use this file except in compliance with the License. You may obtain |
|
8 # a copy of the License at |
|
9 # |
|
10 # http://www.apache.org/licenses/LICENSE-2.0 |
|
11 # |
|
12 # Unless required by applicable law or agreed to in writing, software |
|
13 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
|
14 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
|
15 # License for the specific language governing permissions and limitations |
|
16 # under the License. |
|
17 |
|
18 import collections |
|
19 |
|
20 import netaddr |
|
21 from oslo_config import cfg |
|
22 from oslo_log import log as logging |
|
23 import six |
|
24 |
|
25 from neutron._i18n import _LI |
|
26 from neutron.agent.common import ovs_lib |
|
27 from neutron.agent import firewall |
|
28 from neutron.agent.solaris import packetfilter |
|
29 from neutron.common import constants |
|
30 from neutron.common import utils as c_utils |
|
31 |
|
32 |
|
33 LOG = logging.getLogger(__name__) |
|
34 ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES = [131, 135, 143] |
|
35 |
|
36 DIRECTION_PF_PARAM = {firewall.INGRESS_DIRECTION: 'in', |
|
37 firewall.EGRESS_DIRECTION: 'out'} |
|
38 |
|
39 |
|
40 class PFBaseOVS(ovs_lib.BaseOVS): |
|
41 def get_port_by_id(self, port_id): |
|
42 ports = self.ovsdb.db_find( |
|
43 'Interface', ('external_ids', '=', {'iface-id': port_id}), |
|
44 columns=['name']).execute() |
|
45 if ports: |
|
46 return ports[0]['name'] |
|
47 return None |
|
48 |
|
49 |
|
50 class PFFirewallDriver(firewall.FirewallDriver): |
|
51 """Driver which enforces security groups through PF rules. |
|
52 |
|
53 Please look at neutron.agent.firewall.FirewallDriver for more information |
|
54 on how the methods below are called from the Neutron Open vSwitch agent. It |
|
55 all starts at prepare_port_filter() and then _setup_pf_rules() has all the |
|
56 PF based logic to add correct rules on guest instance's port. |
|
57 """ |
|
58 |
|
59 def __init__(self): |
|
60 self.pf = packetfilter.PacketFilter("_auto/neutron:ovs:agent", |
|
61 layer2=True) |
|
62 # List of port which has security group |
|
63 self.filtered_ports = {} |
|
64 self.unfiltered_ports = {} |
|
65 # List of security group rules for ports residing on this host |
|
66 self.sg_rules = {} |
|
67 # List of security group member ips for ports residing on this host |
|
68 self.sg_members = collections.defaultdict( |
|
69 lambda: collections.defaultdict(list)) |
|
70 # Every PF rule needs to be labeled so that we can later kill the state |
|
71 # associated with that rule (using pfctl -k label -k 110). It is hard |
|
72 # to come up with a meaningfully named label for each PF rule, so we |
|
73 # are resorting to numbers here. |
|
74 self.label_num = 100 |
|
75 self.portid_to_devname = {} |
|
76 |
|
77 def prepare_port_filter(self, port): |
|
78 LOG.debug("Preparing device (%s) filter", port['device']) |
|
79 self._setup_pf_rules(port) |
|
80 |
|
81 def apply_port_filter(self, port): |
|
82 """We never call this method |
|
83 |
|
84 It exists here to override abstract method of parent abstract class. |
|
85 """ |
|
86 pass |
|
87 |
|
88 def update_port_filter(self, port): |
|
89 LOG.debug("Updating device (%s) filter", port['device']) |
|
90 self._setup_pf_rules(port, update=True) |
|
91 |
|
92 def remove_port_filter(self, port): |
|
93 LOG.debug("Removing device (%s) filter", port['device']) |
|
94 self.unfiltered_ports.pop(port['device'], None) |
|
95 self.filtered_ports.pop(port['device'], None) |
|
96 self._remove_rule_port_sec(port) |
|
97 |
|
98 @property |
|
99 def ports(self): |
|
100 return dict(self.filtered_ports, **self.unfiltered_ports) |
|
101 |
|
102 def update_security_group_rules(self, sg_id, sg_rules): |
|
103 LOG.debug("Update rules of security group %s(%s)" % (sg_id, sg_rules)) |
|
104 self.sg_rules[sg_id] = sg_rules |
|
105 |
|
106 def update_security_group_members(self, sg_id, sg_members): |
|
107 LOG.debug("Update members of security group %s(%s)" % |
|
108 (sg_id, sg_members)) |
|
109 self.sg_members[sg_id] = collections.defaultdict(list, sg_members) |
|
110 |
|
111 def security_group_updated(self, action_type, sec_group_ids, |
|
112 device_ids=None): |
|
113 # TODO(gmoodalb): Extend this later to optimize handling of security |
|
114 # groups update |
|
115 pass |
|
116 |
|
117 def _get_label_number(self): |
|
118 self.label_num += 1 |
|
119 return self.label_num |
|
120 |
|
121 def _remove_rule_port_sec(self, port): |
|
122 device_name = self.portid_to_devname.pop(port['id'], None) |
|
123 if not device_name: |
|
124 LOG.info(_LI("Could not find port: %s. Failed to remove PF rules " |
|
125 "for that port"), port['id']) |
|
126 return |
|
127 LOG.debug("Removing PF rules for device_name(%s)" % device_name) |
|
128 # we need to remove both ingress and egress |
|
129 if '/' in device_name: |
|
130 instance_name, datalink = device_name.split('/') |
|
131 instance_name = instance_name.split(':')[1] |
|
132 ingress = '%s_in' % datalink |
|
133 egress = '%s_out' % datalink |
|
134 else: |
|
135 instance_name = device_name |
|
136 ingress = 'in' |
|
137 egress = 'out' |
|
138 existing_anchor_rules = set(self.pf.list_anchor_rules([instance_name])) |
|
139 existing_anchor_rules.discard('anchor "%s" in on %s all' % |
|
140 (ingress, device_name)) |
|
141 existing_anchor_rules.discard('anchor "%s" out on %s all' % |
|
142 (egress, device_name)) |
|
143 self.pf.add_rules(list(existing_anchor_rules), [instance_name]) |
|
144 if existing_anchor_rules: |
|
145 self.pf.remove_anchor_recursively([instance_name, ingress]) |
|
146 self.pf.remove_anchor_recursively([instance_name, egress]) |
|
147 else: |
|
148 self.pf.remove_anchor_recursively([instance_name]) |
|
149 |
|
150 def _setup_pf_rules(self, port, update=False): |
|
151 if not firewall.port_sec_enabled(port): |
|
152 self.unfiltered_ports[port['device']] = port |
|
153 self.filtered_ports.pop(port['device'], None) |
|
154 self._remove_rule_port_sec(port) |
|
155 else: |
|
156 self.filtered_ports[port['device']] = port |
|
157 self.unfiltered_ports.pop(port['device'], None) |
|
158 if update: |
|
159 self._remove_rule_port_sec(port) |
|
160 self._add_rules_by_security_group(port, firewall.INGRESS_DIRECTION) |
|
161 self._add_rules_by_security_group(port, firewall.EGRESS_DIRECTION) |
|
162 |
|
163 def _get_device_name(self, port): |
|
164 bridge = PFBaseOVS() |
|
165 device_name = bridge.get_port_by_id(port['id']) |
|
166 if '/' in device_name: |
|
167 device_name = 'dl:' + device_name |
|
168 return device_name |
|
169 |
|
170 def _split_sgr_by_ethertype(self, security_group_rules): |
|
171 ipv4_sg_rules = [] |
|
172 ipv6_sg_rules = [] |
|
173 for rule in security_group_rules: |
|
174 if rule.get('ethertype') == constants.IPv4: |
|
175 ipv4_sg_rules.append(rule) |
|
176 elif rule.get('ethertype') == constants.IPv6: |
|
177 if rule.get('protocol') in ['icmp', 'icmp6']: |
|
178 rule['protocol'] = 'ipv6-icmp' |
|
179 ipv6_sg_rules.append(rule) |
|
180 return ipv4_sg_rules, ipv6_sg_rules |
|
181 |
|
182 def _select_sgr_by_direction(self, port, direction): |
|
183 return [rule |
|
184 for rule in port.get('security_group_rules', []) |
|
185 if rule['direction'] == direction] |
|
186 |
|
187 def _spoofing_rule(self, port, device_name, ipv4_rules, ipv6_rules): |
|
188 # Fixed rules for traffic sourced from unspecified addresses: 0.0.0.0 |
|
189 # and :: |
|
190 # Allow dhcp client discovery and request |
|
191 ipv4_rules.append('pass out on %s proto udp from 0.0.0.0/32 port 68 ' |
|
192 'to 255.255.255.255/32 port 67 label "%s"' % |
|
193 (device_name, self._get_label_number())) |
|
194 |
|
195 # Allow neighbor solicitation and multicast listener discovery |
|
196 # from the unspecified address for duplicate address detection |
|
197 for icmp6_type in ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES: |
|
198 ipv6_rules.append('pass out on %s inet6 proto ipv6-icmp ' |
|
199 'from ::/128 to ff02::/16 icmp6-type %s ' |
|
200 'label "%s"' % (device_name, icmp6_type, |
|
201 self._get_label_number())) |
|
202 |
|
203 # Fixed rules for traffic after source address is verified |
|
204 # Allow dhcp client renewal and rebinding |
|
205 ipv4_rules.append('pass out on %s proto udp from port 68 to port 67 ' |
|
206 'label "%s"' % (device_name, |
|
207 self._get_label_number())) |
|
208 |
|
209 # Drop Router Advts from the port. |
|
210 ipv6_rules.append('block out quick on %s inet6 proto ipv6-icmp ' |
|
211 'icmp6-type %s label "%s"' % |
|
212 (device_name, constants.ICMPV6_TYPE_RA, |
|
213 self._get_label_number())) |
|
214 # Allow IPv6 ICMP traffic |
|
215 ipv6_rules.append('pass out on %s inet6 proto ipv6-icmp label "%s"' % |
|
216 (device_name, self._get_label_number())) |
|
217 # Allow IPv6 DHCP Client traffic |
|
218 ipv6_rules.append('pass out on %s inet6 proto udp from port 546 ' |
|
219 'to port 547 label "%s"' % |
|
220 (device_name, self._get_label_number())) |
|
221 |
|
222 def _drop_dhcp_rule(self, device_name, ipv4_rules, ipv6_rules): |
|
223 # Drop dhcp packet from VM |
|
224 ipv4_rules.append('block out quick on %s proto udp from port 67 ' |
|
225 'to port 68 label "%s"' % |
|
226 (device_name, self._get_label_number())) |
|
227 ipv6_rules.append('block out quick on %s inet6 proto udp ' |
|
228 'from port 547 to port 546 label "%s"' % |
|
229 (device_name, self._get_label_number())) |
|
230 |
|
231 def _accept_inbound_icmpv6(self, device_name, ipv6_pf_rules): |
|
232 # Allow multicast listener, neighbor solicitation and |
|
233 # neighbor advertisement into the instance |
|
234 for icmp6_type in constants.ICMPV6_ALLOWED_TYPES: |
|
235 ipv6_pf_rules.append('pass in on %s inet6 proto ipv6-icmp ' |
|
236 'icmp6-type %s label "%s"' % |
|
237 (device_name, icmp6_type, |
|
238 self._get_label_number())) |
|
239 |
|
240 def _select_sg_rules_for_port(self, port, direction): |
|
241 """Select rules from the security groups the port is member of.""" |
|
242 port_sg_ids = port.get('security_groups', []) |
|
243 port_rules = [] |
|
244 |
|
245 for sg_id in port_sg_ids: |
|
246 for rule in self.sg_rules.get(sg_id, []): |
|
247 if rule['direction'] == direction: |
|
248 port_rules.extend( |
|
249 self._expand_sg_rule_with_remote_ips( |
|
250 rule, port, direction)) |
|
251 return port_rules |
|
252 |
|
253 def _expand_sg_rule_with_remote_ips(self, rule, port, direction): |
|
254 """Expand a remote group rule to rule per remote group IP.""" |
|
255 remote_group_id = rule.get('remote_group_id') |
|
256 if remote_group_id: |
|
257 ethertype = rule['ethertype'] |
|
258 port_ips = port.get('fixed_ips', []) |
|
259 LOG.debug("Expanding rule: %s with remote IPs: %s" % |
|
260 (rule, self.sg_members[remote_group_id][ethertype])) |
|
261 for ip in self.sg_members[remote_group_id][ethertype]: |
|
262 if ip not in port_ips: |
|
263 ip_rule = rule.copy() |
|
264 direction_ip_prefix = firewall.DIRECTION_IP_PREFIX[ |
|
265 direction] |
|
266 ip_prefix = str(netaddr.IPNetwork(ip).cidr) |
|
267 ip_rule[direction_ip_prefix] = ip_prefix |
|
268 yield ip_rule |
|
269 else: |
|
270 yield rule |
|
271 |
|
272 def _get_remote_sg_ids(self, port, direction=None): |
|
273 sg_ids = port.get('security_groups', []) |
|
274 remote_sg_ids = {constants.IPv4: set(), constants.IPv6: set()} |
|
275 for sg_id in sg_ids: |
|
276 for rule in self.sg_rules.get(sg_id, []): |
|
277 if not direction or rule['direction'] == direction: |
|
278 remote_sg_id = rule.get('remote_group_id') |
|
279 ether_type = rule.get('ethertype') |
|
280 if remote_sg_id and ether_type: |
|
281 remote_sg_ids[ether_type].add(remote_sg_id) |
|
282 return remote_sg_ids |
|
283 |
|
284 def _add_pf_rules(self, port, device_name, direction, ipv4_pf_rules, |
|
285 ipv6_pf_rules): |
|
286 if '/' in device_name: |
|
287 instance_name, datalink = device_name.split('/') |
|
288 instance_name = instance_name.split(':')[1] |
|
289 else: |
|
290 instance_name, datalink = (device_name, "") |
|
291 self.pf.add_nested_anchor_rule(None, instance_name, None) |
|
292 if direction == firewall.INGRESS_DIRECTION: |
|
293 subanchor = '%s%s' % (datalink, '_in' if datalink else 'in') |
|
294 new_anchor_rule = ['anchor "%s" in on %s all' % (subanchor, |
|
295 device_name)] |
|
296 else: |
|
297 subanchor = '%s%s' % (datalink, '_out' if datalink else 'out') |
|
298 new_anchor_rule = ['anchor "%s" out on %s all' % (subanchor, |
|
299 device_name)] |
|
300 existing_anchor_rules = self.pf.list_anchor_rules([instance_name]) |
|
301 final_anchor_rules = set(existing_anchor_rules) | set(new_anchor_rule) |
|
302 self.pf.add_rules(list(final_anchor_rules), [instance_name]) |
|
303 |
|
304 # self.pf.add_nested_anchor_rule(None, anchor_name, anchor_option) |
|
305 self.pf.add_rules(ipv4_pf_rules + ipv6_pf_rules, |
|
306 [instance_name, subanchor]) |
|
307 self.portid_to_devname[port['id']] = device_name |
|
308 |
|
309 def _add_block_everything(self, device_name, direction, ipv4_pf_rules, |
|
310 ipv6_pf_rules): |
|
311 ''' Add a generic block everything rule. The default security group |
|
312 in OpenStack adds 'pass all egress traffic' and prevents all the |
|
313 incoming traffic''' |
|
314 ipv4_pf_rules.append('block %s on %s label "%s"' % |
|
315 (DIRECTION_PF_PARAM[direction], device_name, |
|
316 self._get_label_number())) |
|
317 ipv6_pf_rules.append('block %s on %s inet6 label "%s"' % |
|
318 (DIRECTION_PF_PARAM[direction], device_name, |
|
319 self._get_label_number())) |
|
320 |
|
321 def _add_rules_by_security_group(self, port, direction): |
|
322 LOG.debug("Adding rules for Port: %s", port) |
|
323 |
|
324 device_name = self._get_device_name(port) |
|
325 if not device_name: |
|
326 LOG.info(_LI("Could not find port: %s on the OVS bridge. Failed " |
|
327 "to add PF rules for that port"), port['id']) |
|
328 return |
|
329 # select rules for current port and direction |
|
330 security_group_rules = self._select_sgr_by_direction(port, direction) |
|
331 security_group_rules += self._select_sg_rules_for_port(port, direction) |
|
332 # split groups by ip version |
|
333 # for ipv4, 'pass' will be used |
|
334 # for ipv6, 'pass inet6' will be used |
|
335 ipv4_sg_rules, ipv6_sg_rules = self._split_sgr_by_ethertype( |
|
336 security_group_rules) |
|
337 ipv4_pf_rules = [] |
|
338 ipv6_pf_rules = [] |
|
339 self._add_block_everything(device_name, direction, ipv4_pf_rules, |
|
340 ipv6_pf_rules) |
|
341 # include fixed egress/ingress rules |
|
342 if direction == firewall.EGRESS_DIRECTION: |
|
343 self._add_fixed_egress_rules(port, device_name, ipv4_pf_rules, |
|
344 ipv6_pf_rules) |
|
345 elif direction == firewall.INGRESS_DIRECTION: |
|
346 self._accept_inbound_icmpv6(device_name, ipv6_pf_rules) |
|
347 # include IPv4 and IPv6 iptable rules from security group |
|
348 LOG.debug("Converting %s IPv4 SG rules: %s" % |
|
349 (direction, ipv4_sg_rules)) |
|
350 ipv4_pf_rules += self._convert_sgr_to_pfr(device_name, direction, |
|
351 ipv4_sg_rules) |
|
352 LOG.debug("... to %s IPv4 PF rules: %s" % (direction, ipv4_pf_rules)) |
|
353 LOG.debug("Converting %s IPv6 SG rules: %s" % |
|
354 (direction, ipv6_sg_rules)) |
|
355 ipv6_pf_rules += self._convert_sgr_to_pfr(device_name, direction, |
|
356 ipv6_sg_rules) |
|
357 LOG.debug("... to %s IPv6 PF rules: %s" % (direction, ipv6_pf_rules)) |
|
358 |
|
359 self._add_pf_rules(port, device_name, direction, ipv4_pf_rules, |
|
360 ipv6_pf_rules) |
|
361 |
|
362 def _add_fixed_egress_rules(self, port, device_name, ipv4_pf_rules, |
|
363 ipv6_pf_rules): |
|
364 self._spoofing_rule(port, device_name, ipv4_pf_rules, ipv6_pf_rules) |
|
365 self._drop_dhcp_rule(device_name, ipv4_pf_rules, ipv6_pf_rules) |
|
366 |
|
367 def _protocol_param(self, protocol, pf_rule): |
|
368 if protocol: |
|
369 pf_rule.append('proto %s' % protocol) |
|
370 |
|
371 def _port_param(self, protocol, port_range_min, port_range_max, pf_rule): |
|
372 if port_range_min is None: |
|
373 return |
|
374 if protocol in ('tcp', 'udp'): |
|
375 if port_range_min == port_range_max: |
|
376 pf_rule.append('port %s' % port_range_min) |
|
377 else: |
|
378 pf_rule.append('port %s:%s' % (port_range_min, |
|
379 port_range_max)) |
|
380 elif protocol in ('icmp', 'ipv6-icmp'): |
|
381 icmp_type = 'icmp-type' if protocol == 'icmp' else 'icmp6-type' |
|
382 pf_rule.append('%s %s' % (icmp_type, port_range_min)) |
|
383 if port_range_max is not None: |
|
384 pf_rule.append('code %s' % port_range_max) |
|
385 |
|
386 def _ip_prefix_param(self, direction, ip_prefix, pf_rule): |
|
387 if ip_prefix != 'any': |
|
388 if '/' not in ip_prefix: |
|
389 # we need to convert it into a cidr |
|
390 ip_prefix = c_utils.ip_to_cidr(ip_prefix) |
|
391 elif ip_prefix.endswith('/0'): |
|
392 ip_prefix = 'any' |
|
393 direction = 'from' if direction == firewall.INGRESS_DIRECTION else 'to' |
|
394 pf_rule.append('%s %s' % (direction, ip_prefix)) |
|
395 |
|
396 def _ip_prefix_port_param(self, direction, sg_rule, pf_rule): |
|
397 protocol = sg_rule.get('protocol') |
|
398 if direction == firewall.INGRESS_DIRECTION: |
|
399 ip_prefix = sg_rule.get('source_ip_prefix') |
|
400 ip_prefix = ip_prefix if ip_prefix else 'any' |
|
401 self._ip_prefix_param(direction, ip_prefix, pf_rule) |
|
402 self._port_param(protocol, |
|
403 sg_rule.get('source_port_range_min'), |
|
404 sg_rule.get('source_port_range_max'), pf_rule) |
|
405 self._ip_prefix_param(firewall.EGRESS_DIRECTION, 'any', pf_rule) |
|
406 self._port_param(protocol, |
|
407 sg_rule.get('port_range_min'), |
|
408 sg_rule.get('port_range_max'), pf_rule) |
|
409 else: |
|
410 self._ip_prefix_param(firewall.INGRESS_DIRECTION, 'any', pf_rule) |
|
411 self._port_param(protocol, |
|
412 sg_rule.get('source_port_range_min'), |
|
413 sg_rule.get('source_port_range_max'), pf_rule) |
|
414 |
|
415 ip_prefix = sg_rule.get('dest_ip_prefix') |
|
416 ip_prefix = ip_prefix if ip_prefix else 'any' |
|
417 self._ip_prefix_param(direction, ip_prefix, pf_rule) |
|
418 self._port_param(protocol, |
|
419 sg_rule.get('port_range_min'), |
|
420 sg_rule.get('port_range_max'), pf_rule) |
|
421 |
|
422 def _convert_sgr_to_pfr(self, device_name, direction, |
|
423 security_group_rules): |
|
424 pf_rules = [] |
|
425 for sg_rule in security_group_rules: |
|
426 pf_rule = ['pass'] |
|
427 pf_rule.append("%s on %s" % (DIRECTION_PF_PARAM[direction], |
|
428 device_name)) |
|
429 if sg_rule.get('ethertype') == constants.IPv6: |
|
430 pf_rule.append('inet6') |
|
431 else: |
|
432 pf_rule.append('inet') |
|
433 protocol = sg_rule.get('protocol') |
|
434 self._protocol_param(protocol, pf_rule) |
|
435 self._ip_prefix_port_param(direction, sg_rule, pf_rule) |
|
436 pf_rule.append('label "%s"' % self._get_label_number()) |
|
437 pf_rules.append(' '.join(pf_rule)) |
|
438 return pf_rules |