Skip to content

Commit f12d4a9

Browse files
gthiemongebeagles
authored andcommitted
Fixed ip advertisement for ipv6
1 parent 0317087 commit f12d4a9

File tree

2 files changed

+191
-1
lines changed

2 files changed

+191
-1
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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))

templates/octaviaamphoracontroller/bin/octavia_hm_advertisement.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from pyroute2 import IPRoute
2121

22-
import octavia.amphorae.backends.utils.ip_advertisement as ip_adv
22+
import ip_advertisement as ip_adv
2323

2424
try:
2525
interface_name = sys.argv[1]

0 commit comments

Comments
 (0)