OpenStack Networking (Neutron)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

arp_protect.py 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. # Copyright (c) 2015 Mirantis, Inc.
  2. # All Rights Reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. import netaddr
  16. from neutron_lib.utils import net
  17. from oslo_concurrency import lockutils
  18. from oslo_log import log as logging
  19. import tenacity
  20. from neutron.agent.linux import ip_lib
  21. LOG = logging.getLogger(__name__)
  22. SPOOF_CHAIN_PREFIX = 'neutronARP-'
  23. MAC_CHAIN_PREFIX = 'neutronMAC-'
  24. def setup_arp_spoofing_protection(vif, port_details):
  25. if not port_details.get('port_security_enabled', True):
  26. # clear any previous entries related to this port
  27. delete_arp_spoofing_protection([vif])
  28. LOG.info("Skipping ARP spoofing rules for port '%s' because "
  29. "it has port security disabled", vif)
  30. return
  31. if net.is_port_trusted(port_details):
  32. # clear any previous entries related to this port
  33. delete_arp_spoofing_protection([vif])
  34. LOG.debug("Skipping ARP spoofing rules for network owned port "
  35. "'%s'.", vif)
  36. return
  37. _setup_arp_spoofing_protection(vif, port_details)
  38. @lockutils.synchronized('ebtables')
  39. def _setup_arp_spoofing_protection(vif, port_details):
  40. current_rules = ebtables(['-L']).splitlines()
  41. _install_mac_spoofing_protection(vif, port_details, current_rules)
  42. # collect all of the addresses and cidrs that belong to the port
  43. addresses = {f['ip_address'] for f in port_details['fixed_ips']}
  44. if port_details.get('allowed_address_pairs'):
  45. addresses |= {p['ip_address']
  46. for p in port_details['allowed_address_pairs']}
  47. addresses = {ip for ip in addresses
  48. if netaddr.IPNetwork(ip).version == 4}
  49. if any(netaddr.IPNetwork(ip).prefixlen == 0 for ip in addresses):
  50. # don't try to install protection because a /0 prefix allows any
  51. # address anyway and the ARP_SPA can only match on /1 or more.
  52. return
  53. _install_arp_spoofing_protection(vif, addresses, current_rules)
  54. def chain_name(vif):
  55. # start each chain with a common identifier for cleanup to find
  56. return '%s%s' % (SPOOF_CHAIN_PREFIX, vif)
  57. @lockutils.synchronized('ebtables')
  58. def delete_arp_spoofing_protection(vifs):
  59. current_rules = ebtables(['-L']).splitlines()
  60. _delete_arp_spoofing_protection(vifs, current_rules, table='nat',
  61. chain='PREROUTING')
  62. # TODO(haleyb) this can go away in "R" cycle, it's here to cleanup
  63. # old chains in the filter table
  64. current_rules = ebtables(['-L'], table='filter').splitlines()
  65. _delete_arp_spoofing_protection(vifs, current_rules, table='filter',
  66. chain='FORWARD')
  67. def _delete_arp_spoofing_protection(vifs, current_rules, table, chain):
  68. # delete the jump rule and then delete the whole chain
  69. jumps = [vif for vif in vifs if vif_jump_present(vif, current_rules)]
  70. for vif in jumps:
  71. ebtables(['-D', chain, '-i', vif, '-j',
  72. chain_name(vif), '-p', 'ARP'], table=table)
  73. for vif in vifs:
  74. if chain_exists(chain_name(vif), current_rules):
  75. ebtables(['-X', chain_name(vif)], table=table)
  76. _delete_mac_spoofing_protection(vifs, current_rules, table=table,
  77. chain=chain)
  78. def _delete_unreferenced_arp_protection(current_vifs, table, chain):
  79. # deletes all jump rules and chains that aren't in current_vifs but match
  80. # the spoof prefix
  81. current_rules = ebtables(['-L'], table=table).splitlines()
  82. to_delete = []
  83. for line in current_rules:
  84. # we're looking to find and turn the following:
  85. # Bridge chain: SPOOF_CHAIN_PREFIXtap199, entries: 0, policy: DROP
  86. # into 'tap199'
  87. if line.startswith('Bridge chain: %s' % SPOOF_CHAIN_PREFIX):
  88. devname = line.split(SPOOF_CHAIN_PREFIX, 1)[1].split(',')[0]
  89. if devname not in current_vifs:
  90. to_delete.append(devname)
  91. LOG.info("Clearing orphaned ARP spoofing entries for devices %s",
  92. to_delete)
  93. _delete_arp_spoofing_protection(to_delete, current_rules, table=table,
  94. chain=chain)
  95. @lockutils.synchronized('ebtables')
  96. def delete_unreferenced_arp_protection(current_vifs):
  97. _delete_unreferenced_arp_protection(current_vifs,
  98. table='nat', chain='PREROUTING')
  99. # TODO(haleyb) this can go away in "R" cycle, it's here to cleanup
  100. # old chains in the filter table
  101. _delete_unreferenced_arp_protection(current_vifs,
  102. table='filter', chain='FORWARD')
  103. @lockutils.synchronized('ebtables')
  104. def install_arp_spoofing_protection(vif, addresses):
  105. current_rules = ebtables(['-L']).splitlines()
  106. _install_arp_spoofing_protection(vif, addresses, current_rules)
  107. def _install_arp_spoofing_protection(vif, addresses, current_rules):
  108. # make a VIF-specific ARP chain so we don't conflict with other rules
  109. vif_chain = chain_name(vif)
  110. if not chain_exists(vif_chain, current_rules):
  111. ebtables(['-N', vif_chain, '-P', 'DROP'])
  112. # flush the chain to clear previous accepts. this will cause dropped ARP
  113. # packets until the allows are installed, but that's better than leaked
  114. # spoofed packets and ARP can handle losses.
  115. ebtables(['-F', vif_chain])
  116. for addr in sorted(addresses):
  117. ebtables(['-A', vif_chain, '-p', 'ARP', '--arp-ip-src', addr,
  118. '-j', 'ACCEPT'])
  119. # check if jump rule already exists, if not, install it
  120. if not vif_jump_present(vif, current_rules):
  121. ebtables(['-A', 'PREROUTING', '-i', vif, '-j',
  122. vif_chain, '-p', 'ARP'])
  123. def chain_exists(chain, current_rules):
  124. for rule in current_rules:
  125. if rule.startswith('Bridge chain: %s' % chain):
  126. return True
  127. return False
  128. def vif_jump_present(vif, current_rules):
  129. searches = (('-i %s' % vif), ('-j %s' % chain_name(vif)), ('-p ARP'))
  130. for line in current_rules:
  131. if all(s in line for s in searches):
  132. return True
  133. return False
  134. def _install_mac_spoofing_protection(vif, port_details, current_rules):
  135. mac_addresses = {port_details['mac_address']}
  136. if port_details.get('allowed_address_pairs'):
  137. mac_addresses |= {p['mac_address']
  138. for p in port_details['allowed_address_pairs']}
  139. mac_addresses = list(mac_addresses)
  140. vif_chain = _mac_chain_name(vif)
  141. # mac filter chain for each vif which has a default deny
  142. if not chain_exists(vif_chain, current_rules):
  143. ebtables(['-N', vif_chain, '-P', 'DROP'])
  144. # check if jump rule already exists, if not, install it
  145. if not _mac_vif_jump_present(vif, current_rules):
  146. ebtables(['-A', 'PREROUTING', '-i', vif, '-j', vif_chain])
  147. # we can't just feed all allowed macs at once because we can exceed
  148. # the maximum argument size. limit to 500 per rule.
  149. for chunk in (mac_addresses[i:i + 500]
  150. for i in range(0, len(mac_addresses), 500)):
  151. new_rule = ['-A', vif_chain, '-i', vif,
  152. '--among-src', ','.join(sorted(chunk)), '-j', 'RETURN']
  153. ebtables(new_rule)
  154. _delete_vif_mac_rules(vif, current_rules)
  155. def _mac_vif_jump_present(vif, current_rules):
  156. searches = (('-i %s' % vif), ('-j %s' % _mac_chain_name(vif)))
  157. for line in current_rules:
  158. if all(s in line for s in searches):
  159. return True
  160. return False
  161. def _mac_chain_name(vif):
  162. return '%s%s' % (MAC_CHAIN_PREFIX, vif)
  163. def _delete_vif_mac_rules(vif, current_rules):
  164. chain = _mac_chain_name(vif)
  165. for rule in current_rules:
  166. if '-i %s' % vif in rule and '--among-src' in rule:
  167. ebtables(['-D', chain] + rule.split())
  168. def _delete_mac_spoofing_protection(vifs, current_rules, table, chain):
  169. # delete the jump rule and then delete the whole chain
  170. jumps = [vif for vif in vifs
  171. if _mac_vif_jump_present(vif, current_rules)]
  172. for vif in jumps:
  173. ebtables(['-D', chain, '-i', vif, '-j',
  174. _mac_chain_name(vif)], table=table)
  175. for vif in vifs:
  176. chain = _mac_chain_name(vif)
  177. if chain_exists(chain, current_rules):
  178. ebtables(['-X', chain], table=table)
  179. # Used to scope ebtables commands in testing
  180. NAMESPACE = None
  181. @tenacity.retry(
  182. wait=tenacity.wait_exponential(multiplier=0.01),
  183. retry=tenacity.retry_if_exception(lambda e: e.returncode == 255),
  184. reraise=True
  185. )
  186. def ebtables(comm, table='nat'):
  187. execute = ip_lib.IPWrapper(NAMESPACE).netns.execute
  188. return execute(['ebtables', '-t', table, '--concurrent'] + comm,
  189. run_as_root=True)