|
| 1 | +# Copyright 2020 Red Hat, Inc. All rights reserved. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 4 | +# not use this file except in compliance with the License. You may obtain |
| 5 | +# a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 11 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 12 | +# License for the specific language governing permissions and limitations |
| 13 | +# under the License. |
| 14 | +# |
| 15 | +# Adapted from octavia/amphorae/backends/utils/ip_advertisement.py |
| 16 | +import fcntl |
| 17 | +import socket |
| 18 | +import struct |
| 19 | + |
| 20 | +from oslo_log import log as logging |
| 21 | + |
| 22 | +from octavia.amphorae.backends.utils import network_namespace |
| 23 | +from octavia.common import constants |
| 24 | +from octavia.common import utils as common_utils |
| 25 | + |
| 26 | +LOG = logging.getLogger(__name__) |
| 27 | + |
| 28 | + |
| 29 | +def garp(interface, ip_address, net_ns=None): |
| 30 | + """Sends a gratuitous ARP for ip_address on the interface. |
| 31 | +
|
| 32 | + :param interface: The interface name to send the GARP on. |
| 33 | + :param ip_address: The IP address to advertise in the GARP. |
| 34 | + :param net_ns: The network namespace to send the GARP from. |
| 35 | + :returns: None |
| 36 | + """ |
| 37 | + ARP_ETHERTYPE = 0x0806 |
| 38 | + BROADCAST_MAC = b'\xff\xff\xff\xff\xff\xff' |
| 39 | + |
| 40 | + # Get a socket, optionally inside a network namespace |
| 41 | + garp_socket = None |
| 42 | + if net_ns: |
| 43 | + with network_namespace.NetworkNamespace(net_ns): |
| 44 | + garp_socket = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) |
| 45 | + else: |
| 46 | + garp_socket = socket.socket(socket.AF_PACKET, socket.SOCK_RAW) |
| 47 | + |
| 48 | + # Bind the socket with the ARP ethertype protocol |
| 49 | + garp_socket.bind((interface, ARP_ETHERTYPE)) |
| 50 | + |
| 51 | + # Get the MAC address of the interface |
| 52 | + source_mac = garp_socket.getsockname()[4] |
| 53 | + |
| 54 | + garp_msg = [ |
| 55 | + struct.pack('!h', 1), # Hardware type ethernet |
| 56 | + struct.pack('!h', 0x0800), # Protocol type IPv4 |
| 57 | + struct.pack('!B', 6), # Hardware size |
| 58 | + struct.pack('!B', 4), # Protocol size |
| 59 | + struct.pack('!h', 1), # Opcode request |
| 60 | + source_mac, # Sender MAC address |
| 61 | + socket.inet_aton(ip_address), # Sender IP address |
| 62 | + BROADCAST_MAC, # Target MAC address |
| 63 | + socket.inet_aton(ip_address)] # Target IP address |
| 64 | + |
| 65 | + garp_ethernet = [ |
| 66 | + BROADCAST_MAC, # Ethernet destination |
| 67 | + source_mac, # Ethernet source |
| 68 | + struct.pack('!h', ARP_ETHERTYPE), # Ethernet type |
| 69 | + b''.join(garp_msg)] # The GARP message |
| 70 | + |
| 71 | + garp_socket.send(b''.join(garp_ethernet)) |
| 72 | + garp_socket.close() |
| 73 | + |
| 74 | + |
| 75 | +def calculate_icmpv6_checksum(packet): |
| 76 | + """Calculate the ICMPv6 checksum for a packet. |
| 77 | +
|
| 78 | + :param packet: The packet bytes to checksum. |
| 79 | + :returns: The checksum integer. |
| 80 | + """ |
| 81 | + total = 0 |
| 82 | + |
| 83 | + # Add up 16-bit words |
| 84 | + num_words = len(packet) // 2 |
| 85 | + for chunk in struct.unpack(f"!{num_words}H", packet[0:num_words * 2]): |
| 86 | + total += chunk |
| 87 | + |
| 88 | + # Add any left over byte |
| 89 | + if len(packet) % 2: |
| 90 | + total += packet[-1] << 8 |
| 91 | + |
| 92 | + # Fold 32-bits into 16-bits |
| 93 | + total = (total >> 16) + (total & 0xffff) |
| 94 | + total += total >> 16 |
| 95 | + return ~total + 0x10000 & 0xffff |
| 96 | + |
| 97 | + |
| 98 | +def neighbor_advertisement(interface, ip_address, net_ns=None): |
| 99 | + """Sends a unsolicited neighbor advertisement for an ip on the interface. |
| 100 | +
|
| 101 | + :param interface: The interface name to send the GARP on. |
| 102 | + :param ip_address: The IP address to advertise in the GARP. |
| 103 | + :param net_ns: The network namespace to send the GARP from. |
| 104 | + :returns: None |
| 105 | + """ |
| 106 | + ALL_NODES_ADDR = 'ff02::1' |
| 107 | + SIOCGIFHWADDR = 0x8927 |
| 108 | + |
| 109 | + # Get a socket, optionally inside a network namespace |
| 110 | + na_socket = None |
| 111 | + if net_ns: |
| 112 | + with network_namespace.NetworkNamespace(net_ns): |
| 113 | + na_socket = socket.socket( |
| 114 | + socket.AF_INET6, socket.SOCK_RAW, |
| 115 | + socket.getprotobyname(constants.IPV6_ICMP)) |
| 116 | + else: |
| 117 | + na_socket = socket.socket(socket.AF_INET6, socket.SOCK_RAW, |
| 118 | + socket.getprotobyname(constants.IPV6_ICMP)) |
| 119 | + |
| 120 | + # Per RFC 4861 section 4.4, the hop limit should be 255 |
| 121 | + na_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255) |
| 122 | + |
| 123 | + # TODO(gthiemonge) this line is specific for the octavia-operator |
| 124 | + # The original function didn't seem to send the packets on the right |
| 125 | + # interface |
| 126 | + na_socket.setsockopt(socket.SOL_SOCKET, 25, bytes(interface, 'ascii')) |
| 127 | + |
| 128 | + # Bind the socket with the source address |
| 129 | + na_socket.bind((ip_address, 0)) |
| 130 | + |
| 131 | + # Get the byte representation of the MAC address of the interface |
| 132 | + # Note: You can't use getsockname() to get the MAC on this type of socket |
| 133 | + source_mac = fcntl.ioctl( |
| 134 | + na_socket.fileno(), SIOCGIFHWADDR, struct.pack( |
| 135 | + '256s', bytes(interface, 'utf-8')))[18:24] |
| 136 | + |
| 137 | + # Get the byte representation of the source IP address |
| 138 | + source_ip_bytes = socket.inet_pton(socket.AF_INET6, ip_address) |
| 139 | + |
| 140 | + icmpv6_na_msg_prefix = [ |
| 141 | + struct.pack('!B', 136), # ICMP Type Neighbor Advertisement |
| 142 | + struct.pack('!B', 0)] # ICMP Code |
| 143 | + icmpv6_na_msg_postfix = [ |
| 144 | + struct.pack('!I', 0xa0000000), # Flags (Router, Override) |
| 145 | + source_ip_bytes, # Target address |
| 146 | + struct.pack('!B', 2), # ICMPv6 option type target link-layer address |
| 147 | + struct.pack('!B', 1), # ICMPv6 option length |
| 148 | + source_mac] # ICMPv6 option link-layer address |
| 149 | + |
| 150 | + # Calculate the ICMPv6 checksum |
| 151 | + icmpv6_pseudo_header = [ |
| 152 | + source_ip_bytes, # Source IP address |
| 153 | + socket.inet_pton(socket.AF_INET6, ALL_NODES_ADDR), # Destination IP |
| 154 | + struct.pack('!I', 58), # IPv6 next header (ICMPv6) |
| 155 | + struct.pack('!h', 32)] # IPv6 payload length |
| 156 | + icmpv6_tmp_chksum = struct.pack('!H', 0) # Checksum->zeros for calculation |
| 157 | + tmp_chksum_msg = b''.join(icmpv6_pseudo_header + icmpv6_na_msg_prefix + |
| 158 | + [icmpv6_tmp_chksum] + icmpv6_pseudo_header) |
| 159 | + checksum = struct.pack('!H', calculate_icmpv6_checksum(tmp_chksum_msg)) |
| 160 | + |
| 161 | + # Build the ICMPv6 unsolicitated neighbor advertisement |
| 162 | + icmpv6_msg = b''.join(icmpv6_na_msg_prefix + [checksum] + |
| 163 | + icmpv6_na_msg_postfix) |
| 164 | + |
| 165 | + na_socket.sendto(icmpv6_msg, (ALL_NODES_ADDR, 0, 0, 0)) |
| 166 | + na_socket.close() |
| 167 | + |
| 168 | + |
| 169 | +def send_ip_advertisement(interface, ip_address, net_ns=None): |
| 170 | + """Send an address advertisement. |
| 171 | +
|
| 172 | + This method will send either GARP (IPv4) or neighbor advertisements (IPv6) |
| 173 | + for the ip address specified. |
| 174 | +
|
| 175 | + :param interface: The interface name to send the advertisement on. |
| 176 | + :param ip_address: The IP address to advertise. |
| 177 | + :param net_ns: The network namespace to send the advertisement from. |
| 178 | + :returns: None |
| 179 | + """ |
| 180 | + try: |
| 181 | + if common_utils.is_ipv4(ip_address): |
| 182 | + garp(interface, ip_address, net_ns) |
| 183 | + elif common_utils.is_ipv6(ip_address): |
| 184 | + neighbor_advertisement(interface, ip_address, net_ns) |
| 185 | + else: |
| 186 | + LOG.error('Unknown IP version for address: "%s". Skipping', |
| 187 | + ip_address) |
| 188 | + except Exception as e: |
| 189 | + LOG.warning('Unable to send address advertisement for address: "%s", ' |
| 190 | + 'error: %s. Skipping', ip_address, str(e)) |
0 commit comments