Skip to content

Commit

Permalink
Merge pull request #5 from ryanpetrello/master
Browse files Browse the repository at this point in the history
Remove arping and send gratuitous ARP via Python's socket lib
  • Loading branch information
fzylogic committed Jun 5, 2015
2 parents e344cb2 + d266028 commit 52b2ef7
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 17 deletions.
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

0 comments on commit 52b2ef7

Please sign in to comment.