Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove arping and send gratuitous ARP via Python's socket lib #5

Merged
merged 1 commit into from
Jun 5, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 68 additions & 0 deletions akanda/router/drivers/arp.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@

import logging
import re
import socket
import struct

from akanda.router.drivers import base
from akanda.router.models import Network


LOG = logging.getLogger(__name__)
Expand All @@ -31,6 +34,71 @@ class ARPManager(base.Manager):
"""
EXECUTABLE = '/usr/sbin/arp'

def send_gratuitous_arp_for_floating_ips(self, config, generic_to_host):
"""
Send a gratuitous ARP for every Floating IP.
:type config: akanda.router.models.Configuration
:param config: An akanda.router.models.Configuration object containing
configuration information for the system's network
setup.
:type generic_to_host: callable
:param generic_to_host: A callable which translates a generic interface
name (e.g., "ge0") to a physical name (e.g.,
"eth0")
"""
external_nets = filter(
lambda n: n.network_type == Network.TYPE_EXTERNAL,
config.networks
)
for net in external_nets:
for fip in net.floating_ips:
self.send_gratuitous_arp(
generic_to_host(net.interface.ifname),
str(fip.floating_ip)
)

def send_gratuitous_arp(self, ifname, address):
"""
Send a gratuitous ARP reply. Generally used when Floating IPs are
associated.
:type ifname: str
:param ifname: The real name of the interface to send an ARP on
:type address: str
:param address: The source IPv4 address
"""
HTYPE_ARP = 0x0806
PTYPE_IPV4 = 0x0800

# Bind to the socket
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
sock.bind((ifname, HTYPE_ARP))
hwaddr = sock.getsockname()[4]

# Build a gratuitous ARP packet
gratuitous_arp = [
struct.pack("!h", 1), # HTYPE Ethernet
struct.pack("!h", PTYPE_IPV4), # PTYPE IPv4
struct.pack("!B", 6), # HADDR length, 6 for IEEE 802 MAC addresses
struct.pack("!B", 4), # PADDR length, 4 for IPv4
struct.pack("!h", 2), # OPER, 2 = ARP Reply

# Sender's hardware and protocol address are duplicated in the
# target fields

hwaddr, # Sender MAC
socket.inet_aton(address), # Sender IP address
hwaddr, # Target MAC
socket.inet_aton(address) # Target IP address
]
frame = [
'\xff\xff\xff\xff\xff\xff', # Broadcast destination
hwaddr, # Source address
struct.pack("!h", HTYPE_ARP),
''.join(gratuitous_arp)
]
sock.send(''.join(frame))
sock.close()

def remove_stale_entries(self, config):
"""
A wrapper function that iterates over the networks in <config> and
Expand Down
8 changes: 0 additions & 8 deletions akanda/router/drivers/ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,6 @@ def _update_set(self, real_ifname, interface, old_interface, attribute,
self.sudo(*fmt_args_add(item))
self.up(interface)

# Send a gratuitous ARP for new v4 addressees
ip, prefix = item
if ip.version == 4:
utils.execute([
'arping', '-A', '-c', '1', '-vv', '-I',
real_ifname, str(ip)
], self.root_helper)

for item in (prev_set - next_set):
self.sudo(*fmt_args_delete(item))
ip, prefix = item
Expand Down
4 changes: 4 additions & 0 deletions akanda/router/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ def update_routes(self, cache):

def update_arp(self):
mgr = arp.ARPManager()
mgr.send_gratuitous_arp_for_floating_ips(
self.config,
self.ip_mgr.generic_to_host
)
mgr.remove_stale_entries(self.config)

def get_interfaces(self):
Expand Down
2 changes: 1 addition & 1 deletion scripts/create-akanda-raw-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export DEBIAN_FRONTEND=noninteractive
APT_GET="apt-get -y"
APPLIANCE_BASE_DIR="/tmp/akanda-appliance"
APPLIANCE_SCRIPT_DIR="$APPLIANCE_BASE_DIR/scripts"
PACKAGES="ntp python2.7 python-pip wget dnsmasq bird6 iptables iptables-persistent tcpdump conntrack tshark mtr arping"
PACKAGES="ntp python2.7 python-pip wget dnsmasq bird6 iptables iptables-persistent tcpdump conntrack tshark mtr"
PACKAGES_BUILD="python-dev build-essential isc-dhcp-client"

DNS=8.8.8.8
Expand Down
87 changes: 87 additions & 0 deletions test/unit/drivers/test_arp.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@


import mock
import socket
import unittest2

from akanda.router import models
from akanda.router.drivers import arp

config = mock.Mock()
Expand All @@ -26,6 +28,13 @@
network.address_allocations = [alloc]
config.networks = [network]

def _AF_PACKET_supported():
try:
from socket import AF_PACKET
return True
except:
return False


class ARPTest(unittest2.TestCase):

Expand Down Expand Up @@ -83,3 +92,81 @@ def test_ip_mac_address_mismatch(self):
mock.call('-an'),
mock.call('-d', '10.10.10.2')
])

def test_send_gratuitous_arp_for_config(self):
config = models.Configuration({
'networks': [{
'network_id': 'ABC456',
'interface': {
'ifname': 'ge1',
'name': 'ext',
},
'subnets': [{
'cidr': '172.16.77.0/24',
'gateway_ip': '172.16.77.1',
'dhcp_enabled': True,
'dns_nameservers': []
}],
'network_type': models.Network.TYPE_EXTERNAL,
}],
'floating_ips': [{
'fixed_ip': '192.168.0.2',
'floating_ip': '172.16.77.50'
},{
'fixed_ip': '192.168.0.3',
'floating_ip': '172.16.77.51'
},{
'fixed_ip': '192.168.0.4',
'floating_ip': '172.16.77.52'
},{
'fixed_ip': '192.168.0.5',
'floating_ip': '172.16.77.53'
}]
})

with mock.patch.object(self.mgr, 'send_gratuitous_arp') as send_garp:
self.mgr.send_gratuitous_arp_for_floating_ips(
config,
lambda x: x.replace('ge', 'eth')
)
assert send_garp.call_args_list == [
mock.call('eth1', '172.16.77.50'),
mock.call('eth1', '172.16.77.51'),
mock.call('eth1', '172.16.77.52'),
mock.call('eth1', '172.16.77.53')
]

@unittest2.skipIf(
not _AF_PACKET_supported(),
'socket.AF_PACKET not supported on this platform'
)
@mock.patch('socket.socket')
def test_send_gratuitous_arp(self, socket_constr):
socket_inst = socket_constr.return_value
socket_inst.getsockname.return_value = (
None, None, None, None, 'A1:B2:C3:D4:E5:F6'
)

self.mgr.send_gratuitous_arp('eth1', '1.2.3.4')
socket_constr.assert_called_once_with(
socket.AF_PACKET, socket.SOCK_RAW
)
socket_inst.bind.assert_called_once_with((
'eth1',
0x0806
))
data = socket_inst.send.call_args_list[0][0][0]
assert data == ''.join([
'\xff\xff\xff\xff\xff\xff', # Broadcast destination
'A1:B2:C3:D4:E5:F6', # Source hardware address
'\x08\x06', # HTYPE ARP
'\x00\x01', # Ethernet
'\x08\x00', # Protocol IPv4
'\x06', # HADDR length, 6 for IEEE 802 MAC addresses
'\x04', # PADDR length, 4 for IPv4
'\x00\x02', # OPER, 2 = ARP Reply
'A1:B2:C3:D4:E5:F6', # Source MAC
'\x01\x02\x03\x04', # Source IP
'A1:B2:C3:D4:E5:F6', # Target MAC matches
'\x01\x02\x03\x04' # Target IP matches
])
9 changes: 1 addition & 8 deletions test/unit/drivers/test_ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import logging
import re
import socket
from cStringIO import StringIO

from unittest2 import TestCase
Expand Down Expand Up @@ -259,10 +260,6 @@ def test_address_add(self):
], 'sudo'),
mock.call([cmd, 'link', 'set', 'em0', 'up'], 'sudo'),
mock.call([cmd, 'addr', 'show', 'em0']),
mock.call([
'arping', '-A', '-c', '1', '-vv', '-I', 'em0',
'192.168.105.2'
], 'sudo'),
mock.call([
cmd, '-6', 'addr', 'add',
'fdca:3ba5:a17a:acda:20c:29ff:fe94:723d/64', 'dev', 'em0'
Expand Down Expand Up @@ -318,9 +315,6 @@ def test_update_set(self):
], 'sudo'),
mock.call(['/sbin/ip', 'link', 'set', 'em0', 'up'], 'sudo'),
mock.call(['/sbin/ip', 'addr', 'show', 'em0']),
mock.call([
'arping', '-A', '-c', '1', '-vv', '-I', 'em0', str(a.ip)
], 'sudo'),
mock.call([
'/sbin/ip', 'addr', 'del', str(c), 'dev', 'em0'
], 'sudo'),
Expand Down Expand Up @@ -378,7 +372,6 @@ def test_get_management_address_with_autoconfiguration(self):
mock.call([cmd, 'link', 'set', 'eth0', 'up'], 'sudo')
]


class TestDisableDAD(TestCase):
"""
Duplicate Address Detection should be auto-disabled for non-external
Expand Down