Last active
August 29, 2015 14:14
-
-
Save kk7ds/48da850ffd3418196ad3 to your computer and use it in GitHub Desktop.
Daemon to watch for ARP requests and issue WOL packets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright 2015 Dan Smith <[email protected]> | |
import logging | |
import optparse | |
import os | |
import socket | |
import struct | |
import subprocess | |
import sys | |
import time | |
ETH_P_ALL = 3 | |
ETH_P_IP = 0x0800 | |
ETH_P_ARP = 0x0806 | |
ETHER_PROTOS = { | |
ETH_P_IP: 'IP', | |
ETH_P_ARP: 'ARP', | |
} | |
ARP_OPS = { | |
1: 'request', | |
2: 'reply', | |
} | |
def hexify(data, delim=' '): | |
hexed = [] | |
for c in data: | |
hexed.append('%02x' % ord(c)) | |
return delim.join(hexed) | |
class ArpPacket(object): | |
@classmethod | |
def from_raw(cls, data): | |
f = cls() | |
f.htype, f.ptype, f.hlen, f.plen, op = ( | |
struct.unpack('>HHBBH', data[:8])) | |
f.operation = ARP_OPS.get(op, '?') | |
f._sender_mac = data[8:14] | |
f._sender_ip = data[14:18] | |
f._target_mac = data[18:24] | |
f._target_ip = data[24:28] | |
return f | |
@property | |
def sender_mac(self): | |
return hexify(self._sender_mac, ':') | |
@property | |
def target_mac(self): | |
return hexify(self._target_mac, ':') | |
@property | |
def sender_ip(self): | |
return socket.inet_ntoa(self._sender_ip) | |
@property | |
def target_ip(self): | |
return socket.inet_ntoa(self._target_ip) | |
def __str__(self): | |
lines = [] | |
lines.append('HType: %04x' % self.htype) | |
lines.append('PType: %04x' % self.ptype) | |
lines.append('HLen: %04x' % self.hlen) | |
lines.append('PLen: %04x' % self.plen) | |
lines.append('Op: %s' % self.operation) | |
lines.append('Sender: %s %s' % (self.sender_mac, self.sender_ip)) | |
lines.append('Target: %s %s' % (self.target_mac, self.target_ip)) | |
return '\n'.join(lines) | |
class EthernetFrame(object): | |
@classmethod | |
def from_raw(cls, data): | |
f = cls() | |
f.src = data[:6] | |
f.dst = data[6:12] | |
f.proto, = struct.unpack('>H', data[12:14]) | |
if f.proto == ETH_P_ARP: | |
f.fproto = ArpPacket.from_raw(data[14:]) | |
else: | |
f.fproto = None | |
return f | |
@property | |
def source_mac(self): | |
return hexify(self.src, ':') | |
@property | |
def dest_mac(self): | |
return hexify(self.dst, ':') | |
def __str__(self): | |
lines = [] | |
lines.append('Src: %s' % hexify(self.src, ':')) | |
lines.append('Dst: %s' % hexify(self.dst, ':')) | |
lines.append('Proto: %04x (%s)' % (self.proto, | |
ETHER_PROTOS.get(self.proto, '?'))) | |
return '\n'.join(lines) + '\n' + str(self.fproto) | |
class HostState(object): | |
def __init__(self, ip, alias, interface=None, timeout=90): | |
self._ip = ip | |
self._alias = alias | |
self._interface = interface | |
self._timeout = timeout | |
self._last_seen = 0 | |
self._last_ping = 0 | |
def ping(self): | |
information_age = time.time() - self._last_seen | |
ping_interval = self._timeout / 2 | |
if (information_age > ping_interval and | |
time.time() - self._last_ping >= ping_interval): | |
LOG.info('Pinging %s' % self) | |
with file(os.devnull, 'wb') as devnull: | |
r = subprocess.call('fping -q -c1 -t50 %s' % self._ip, | |
shell=True, stdout=devnull, stderr=devnull) | |
if r == 0: | |
self.seen_alive() | |
self._last_ping = time.time() | |
def seen_alive(self): | |
last_seen_ago = '%i sec ago' % (time.time() - self._last_seen) | |
LOG.info('%s seen alive (since %s)' % ( | |
self._alias, self._last_seen and last_seen_ago or 'never')) | |
self._last_seen = time.time() | |
@property | |
def is_alive(self): | |
return (time.time() - self._last_seen) < self._timeout | |
def __str__(self): | |
information_age = '%i sec ago' % (time.time() - self._last_seen) | |
return '%s [%s]: Alive: %s (%s)' % ( | |
self._alias, self._ip, self.is_alive, | |
self._last_seen and information_age or 'never') | |
def wake(self): | |
LOG.warning('Waking %s' % self._alias) | |
if self._interface: | |
intfarg = '-i %s' % self._interface | |
else: | |
intfarg = '' | |
r = subprocess.call('etherwake %s %s' % (intfarg, | |
self._alias), | |
shell=True) | |
if r != 0: | |
LOG.error('Failed to wake %s' % (self._alias)) | |
def update_timers(watches, arp_frame): | |
seen = [] | |
for ip, state in watches.items(): | |
if arp_frame.sender_ip == ip: | |
state.seen_alive() | |
state.ping() | |
return bool(seen) | |
def main(): | |
global LOG | |
parser = optparse.OptionParser() | |
parser.add_option('-D', '--debug', help='Debug output', | |
default=False, action='store_true') | |
parser.add_option('-v', '--verbose', help='Verbose output', | |
default=False, action='store_true') | |
parser.add_option('-t', '--timeout', default=90, | |
help='Timeout (sec) before considering a host offline', | |
type='int') | |
options, args = parser.parse_args() | |
if len(args) == 0: | |
print 'Arguments are required in the form of ip:hostname' | |
return 1 | |
logging.basicConfig(format='%(asctime)-15s %(levelname)s %(message)s') | |
LOG = logging.getLogger() | |
if options.debug: | |
LOG.setLevel(logging.DEBUG) | |
elif options.verbose: | |
LOG.setLevel(logging.INFO) | |
else: | |
LOG.setLevel(logging.WARNING) | |
watches = {} | |
for arg in args: | |
if ':' not in arg: | |
print '`%s\' is not in valid ip:hostname[:iface] format' | |
return 1 | |
try: | |
ip, hostname = arg.split(':') | |
iface = None | |
except ValueError: | |
ip, hostname, iface = arg.split(':') | |
watches[ip] = HostState(ip, hostname, interface=iface, | |
timeout=options.timeout) | |
LOG.debug('Watching %s' % watches[ip]) | |
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ARP)); | |
while True: | |
data = s.recv(65535) | |
f = EthernetFrame.from_raw(data) | |
if f.proto == ETH_P_ARP: | |
LOG.debug('ARP: %s %s -%s-> %s %s' % ( | |
f.fproto.sender_mac, f.fproto.sender_ip, | |
f.fproto.operation, | |
f.fproto.target_mac, f.fproto.target_ip)) | |
update_timers(watches, f.fproto) | |
if f.fproto.target_ip in watches: | |
watch = watches[f.fproto.target_ip] | |
if not watch.is_alive: | |
watch.wake() | |
if __name__ == '__main__': | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment