├── .gitignore ├── LICENSE ├── README.md ├── scripts ├── sleepproxy_debug_osx ├── sleepproxyd └── test ├── setup.py ├── sleepproxy ├── __init__.py ├── arp.py ├── dnsserve.py ├── manager.py ├── mdns.py ├── sniff.py ├── tcp.py └── wol.py └── test └── udp.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Russell Cloran 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SleepProxyServer 2 | ================ 3 | mDNS (Bonjour) [Sleep Proxy](http://stuartcheshire.org/SleepProxy/) Server implementation for Python 4 | 5 | Provides the [Wake on Demand service](http://support.apple.com/kb/HT3774?viewlocale=en_US&locale=en_US), similarly to Apple TV and Airport Express devices 6 | 7 | Status 8 | ------ 9 | SPS has been tested against [SleepProxyClient](http://github.com/awein/SleepProxyClient), OSX 10.9's mDNSResponder, and OSX 10.10's discoveryd. 10 | See Debugging instructions below to test other implementations. 11 | Selective-port waking is not implemented, any TCP request to a sleep client will result in a wakeup attempt. 12 | 13 | A port to C(++) or Go would be welcome for resource limited ARM devices. 14 | 15 | Internals 16 | ------ 17 | The included server daemon, scripts/sleeproxyd, binds port 5353 and loops up some greenlets: 18 | * dnsserve.py: Handle DNSUPDATE registrations sent to UDP:5353 from sleep proxy clients (aka. "sleepers") that are powering down 19 | * arp.py: Spoof ARP replies for requests from wakers for the sleepers' IP addr. Also monitors sleepers own gratuitous ARPs after wakeup, which deregisters them. 20 | * mdns.py: Mirrors the mDNS service advertisements of sleepers with Avahi so that their services can still be browsed by wakers on the local network. 21 | * tcp.py: Listen for wakers' TCP requests to sleepers. On receipt, will attempt to wake the sleeper with a WOL "magic packet". 22 | 23 | Installation 24 | ------- 25 | Being based on ZeroConf, SPS requires almost no configuration. 26 | Just run it and clients will see its mDNS advertisement and register to it within their regular polling intervals and/or just before sleeping. 27 | `sudo pmset networkoversleep 1` may be necessary to ensure OSX clients will publish services at-all-costs. 28 | You must ensure both SPS server and client use the same network-segment and IP subnet and that IP Multicast traffic between them is not blocked. 29 | 30 | gevent 1.0 is required for its co-operative threading feature; its packaged in Debian jessie. 31 | Because of this, SPS can't be run under python3 (FIXME: replace gevent with asyncio) 32 | 33 | * Debian 8.0+ (jessie) & Ubuntu 14.04+ (Trusty Tahr) 34 | ``` 35 | apt-get install python-scapy python-netifaces python-dbus python-gevent python-pip python-setuptools avahi-daemon git 36 | pip install git+https://github.com/kfix/SleepProxyServer.git 37 | nohup sleepproxyd >/dev/null 2>&1 & 38 | #^put that in rc.local or an initscript or systemd-unit 39 | ``` 40 | 41 | * [OpenWRT](https://github.com/enigmagroup/enigmabox-openwrt/blob/master/python-gevent/Makefile) 42 | * [oWRT asyncIO](https://github.com/openwrt/packages/blob/master/lang/python3/files/python3-package-asyncio.mk) `opkg install python3-asyncio` 43 | * 44 | 45 | Development & Debugging 46 | ----- 47 | * run a canned client-less server and test a with a mock registration 48 | ``` 49 | scripts/test 50 | ``` 51 | 52 | * debug segfaults in cpython or gevent on Debian/Ubuntu 53 | ``` 54 | apt-get install gdb python2.7-dbg libc6-dbg python-dbus-dbg python-netifaces-dbg 55 | cd SleepProxyServer/ 56 | python setup.py develop --exclude-scripts 57 | gdb -ex r --args python2.7 scripts/sleepproxyd 58 | ``` 59 | 60 | * play with scapy filters 61 | ``` 62 | scapy 63 | sniff(tcpwatch, prn=lambda x: x.display(), filter='tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack = 0 and dst host 10.1.0.15', iface='eth0') 64 | sniff(prn=lambda x: x.display(), filter='arp host 10.1.0.15', iface='eth0') 65 | ``` 66 | 67 | * Watch OSX syslog for alll SPS-related actions (sudo-root required) 68 | Press Ctrl-T to sleep-wake-cycle your Mac, generating a SPS registration 69 | ``` 70 | scripts/sleepproxy_debug_osx 71 | ``` 72 | 73 | * advertise services to SPS from your (Obj)C.app by unsetting service flag [kDNSServiceFlagsWakeOnlyService](https://developer.apple.com/library/mac/documentation/Networking/Reference/DNSServiceDiscovery_CRef/Reference/reference.html#jumpTo_166) 74 | 75 | Further Reading 76 | ------- 77 | * [mDNS rfc](http://datatracker.ietf.org/doc/rfc6762/) 78 | * draft #8 is last version to describe Sleep Proxy services @ sec 17.: http://tools.ietf.org/id/draft-cheshire-dnsext-multicastdns-08.txt 79 | * [ZeroConf rfc](http://datatracker.ietf.org/doc/rfc6763/) 80 | * http://datatracker.ietf.org/wg/dnssd/charter/ 81 | * changes to mDNS/ZC for larger networks are under development: https://datatracker.ietf.org/doc/draft-ietf-dnssd-hybrid/ 82 | * https://datatracker.ietf.org/doc/rfc8222/ 83 | -------------------------------------------------------------------------------- /scripts/sleepproxy_debug_osx: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #run as root 4 | [ $(id -u) != 0 ] && exec sudo $0 5 | 6 | dump_sps() { 7 | # dump sleep proxies 8 | for sps in $(printf list | scutil | grep SleepProxyServers | awk '{print $NF}'); do 9 | printf "open\nget $sps\nd.show" | scutil 10 | done 11 | } 12 | 13 | function ctrl_c() { 14 | echo "** Trapped CTRL-C: returning DNS cache to normal logging" 15 | killall mDNSResponder #restore default log levels to speed up syslog 16 | discoveryutil loglevel Basic 17 | discoveryutil logclass Everything 18 | kill $! 19 | echo "Should see $(hostname -s).local with TTL of 125 and with local interface names in the results, otherwise SPS server is retaining mirror record post-wake!" 20 | dns-sd -G v4v6 $(hostname -s).local & 21 | sleep 3 #dns-sd is clingy.. 22 | dump_sps 23 | exit 24 | } 25 | 26 | function ctrl_t() { 27 | echo "** Trapped CTRL-T: going to sleep to trigger a SPS registration! Will wake up 15 secs later..." 28 | dump_sps 29 | pmset relative wake 15; pmset sleepnow 30 | dump_sps 31 | } 32 | 33 | if (launchctl blame system/com.apple.networking.discoveryd > /dev/null); then # OSX 10.10 Yosemite 34 | launchctl stop com.apple.networking.discoveryd 35 | ( sleep 3 36 | discoveryutil lognoclass Everything 37 | discoveryutil logclass SleepProxy 38 | discoveryutil logclass Network 39 | discoveryutil logclass Packets 40 | #discoveryutil logclass DNSResolver #some sps-related "registration" messages, but a lot of generic app dns activity 41 | discoveryutil loglevel VeryDetailed 42 | ) & 43 | syslog -w 0 -k Sender discoveryd [SPS]mDNSCoreReceiveUpdate() -> [SLEEPER]mDNSCoreReceive(),mDNSCoreReceiveUpdateR() 24 | # [WAKER]*L3 -> [SPS]*BPF,SendResponses(),SendWakeup(),WakeOnResolve++,mDNSSendWakeOnResolve() -> [SLEEPER] 25 | 26 | # http://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-11 27 | dns.edns.UL = 2 28 | dns.edns.OWNER = 4 29 | 30 | class UpdateLeaseOption(dns.edns.Option): 31 | """EDNS option for Dynamic DNS Update Leases 32 | http://tools.ietf.org/html/draft-sekar-dns-ul-01""" 33 | def __init__(self, lease): 34 | super(UpdateLeaseOption, self).__init__(dns.edns.UL) 35 | self.lease = lease 36 | 37 | def to_wire(self, file): 38 | data = struct.pack("!L", self.lease) 39 | file.write(data) 40 | 41 | @classmethod 42 | def from_wire(cls, otype, wire, current, olen): 43 | data = wire[current:current + olen] 44 | (lease,) = struct.unpack("!L", data) 45 | return cls(lease) 46 | 47 | def __repr__(self): 48 | return "%s[OPT#%s](%s)" % ( 49 | self.__class__.__name__, 50 | self.otype, 51 | self.lease 52 | ) 53 | 54 | dns.edns._type_to_class.update({dns.edns.UL: UpdateLeaseOption}) 55 | 56 | #SetupOwnerOpt() mDNS.c 57 | class OwnerOption(dns.edns.Option): 58 | """EDNS option for DNS-SD Sleep Proxy Service client mac address hinting 59 | http://tools.ietf.org/html/draft-cheshire-edns0-owner-option-00""" 60 | def __init__(self, ver=0, seq=1, pmac=None, wmac=None, passwd=None): 61 | super(OwnerOption, self).__init__(dns.edns.OWNER) 62 | self.ver = ver 63 | self.seq = seq 64 | self.pmac = self._mac2text(pmac) 65 | self.wmac = self._mac2text(wmac) 66 | self.passwd = passwd 67 | 68 | @staticmethod 69 | def _mac2text(mac): 70 | if not mac: return mac 71 | #if len(mac) == 6: mac.encode('hex') #this was a wire-format binary 72 | mac = binascii.hexlify(mac) 73 | return mac.lower().translate(None,'.:-') #del common octet delimiters 74 | 75 | def to_wire(self, file): 76 | data = '' + ver + seq 77 | #data += self.pmac.decode('hex') 78 | data += binascii.unhexlify(self.pmac) 79 | if self.pmac != self.wmac: 80 | data += self.wmac.decode('hex') 81 | if passwd: data += passwd 82 | 83 | file.write(data) 84 | 85 | @classmethod 86 | def from_wire(cls, otype, wire, current, olen): 87 | data = wire[current:current + olen] 88 | if olen == 20: 89 | opt = (ver, seq, pmac, wmac, passwd) = struct.unpack('!BB6s6s6s',data) 90 | elif olen == 18: 91 | opt = (ver, seq, pmac, wmac, passwd) = struct.unpack('!BB6s6s4s',data) 92 | elif olen == 14: 93 | opt = (ver, seq, pmac, wmac) = struct.unpack("!BB6s6s",data) 94 | elif olen == 8: 95 | opt = (ver, seq, pmac) = struct.unpack("!BB6s",data) 96 | 97 | return cls(*opt) 98 | 99 | def __repr__(self): 100 | return "%s[OPT#%s](%s, %s, %s, %s, %s)" % ( 101 | self.__class__.__name__, 102 | self.otype, 103 | self.ver, 104 | self.seq, 105 | self.pmac, 106 | self.wmac, 107 | self.passwd 108 | ) 109 | 110 | dns.edns._type_to_class.update({dns.edns.OWNER: OwnerOption}) 111 | 112 | from sleepproxy.manager import manage_host 113 | 114 | from gevent.server import DatagramServer 115 | #https://github.com/surfly/gevent/blob/master/gevent/server.py#L106 116 | 117 | __all__ = ['SleepProxyServer'] 118 | 119 | class SleepProxyServer(DatagramServer): 120 | 121 | # #@classmethod 122 | # #def get_listener(self, address, family=None): 123 | # # #return _udp_socket(address, reuse_addr=self.reuse_addr, family=family) 124 | # # sock = socket.socket(family=family, type=socket.SOCK_DGRAM) 125 | # # #if family == socket.AF_INET6: logging.warning("dual-stacking!"); sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False) 126 | # # if family == socket.AF_INET6: logging.warning("disabling dual-stacking!"); sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, True) 127 | # # sock.bind(address) 128 | # # return sock 129 | 130 | def handle(self, message, raddress): 131 | try: 132 | #with ignored(dns.message.BadEDNS): 133 | message = dns.message.from_wire(message, ignore_trailing=True) 134 | except dns.message.BadEDNS: 135 | #yosemite's discoveryd sends an OPT record per active NIC, dnspython doesn't like more than 1 OPT record 136 | # https://github.com/rthalley/dnspython/blob/master/dns/message.py#L642 137 | # so turn off Wi-Fi for ethernet-connected clients 138 | pass #or send back an nxdomain or servfail 139 | except: #no way to just catch dns.exceptions.* 140 | logging.warning("Error decoding DNS message from %s" % raddress[0]) 141 | logging.debug(traceback.format_exc()) 142 | return 143 | 144 | if message.edns < 0: 145 | logging.warning("Received non-EDNS message from %s, ignoring" % raddress[0]) 146 | return 147 | 148 | if not (message.opcode() == 5 and message.authority): 149 | logging.warning("Received non-UPDATE message from %s, ignoring" % raddress[0]) 150 | return 151 | 152 | logging.debug("Received SPS registration from %s, parsing" % raddress[0]) 153 | 154 | info = {'records': [], 'addresses': []} 155 | 156 | # Try to guess the interface this came in on 157 | # todo - precompute this table on new()? 158 | for iface in netifaces.interfaces(): 159 | ifaddresses = netifaces.ifaddresses(iface) 160 | for af, addresses in ifaddresses.items(): 161 | if af not in (netifaces.AF_INET, netifaces.AF_INET6): continue 162 | for address in addresses: 163 | mask = address['netmask'] 164 | if af == netifaces.AF_INET6: mask = (mask.count('f') * 4) # convert linux masks to prefix length...gooney 165 | if address['addr'].find('%') > -1: continue #more linux ipv6 stupidity 166 | iface_net = ipaddress.ip_interface('%s/%s' % (address['addr'], mask)).network 167 | if ipaddress.ip_address(raddress[0]) in iface_net: 168 | info['mymac'] = ifaddresses[netifaces.AF_LINK][0]['addr'] 169 | info['myif'] = iface 170 | 171 | for rrset in message.authority: 172 | rrset.rdclass %= dns.rdataclass.UNIQUE #remove cache-flush bit 173 | #rrset.rdata = rrset.rdata.decode(utf-8) 174 | info['records'].append(rrset) 175 | self._add_addresses(info, rrset) 176 | 177 | logging.debug('NSUPDATE START--\n\n%s\n\n%s\n\n--NSUPDATE END' % (message,message.options)) 178 | 179 | for option in message.options: 180 | if option.otype == dns.edns.UL: 181 | info['ttl'] = option.lease #send-WOL-no-later-than timer TTL 182 | if option.otype == dns.edns.OWNER: 183 | info['othermac'] = option.pmac #WOL target mac 184 | #if option.passwd: # password required in wakeup packet 185 | # mDNS.c:SendSPSRegistrationForOwner() doesn't seem to add a password 186 | 187 | self._answer(raddress, message) 188 | 189 | if len(message.options) == 2: 190 | # need both an owner and an update-lease option, else its just a post-wake notification (incremented seq number) 191 | manage_host(info) 192 | 193 | def _add_addresses(self, info, rrset): 194 | if rrset.rdtype != dns.rdatatype.PTR: return 195 | if rrset.rdclass != dns.rdataclass.IN: return 196 | 197 | #if not rrset.name.to_text().endswith('.in-addr.arpa.'): return #TODO: support SYN sniffing for .ip6.arpa. hosts 198 | if not rrset.name.to_text().endswith('.arpa.'): return #all we care about are reverse-dns records 199 | 200 | info['addresses'].append(dns.reversename.to_address(rrset.name)) 201 | 202 | def _answer(self, address, query): 203 | response = dns.message.make_response(query) 204 | response.flags = dns.flags.QR | dns.opcode.to_flags(dns.opcode.UPDATE) 205 | #needs a single OPT record to confirm registration: 0 TTL 4500 48 . OPT Max 1440 Lease 7200 Vers 0 Seq 21 MAC D4:9A:20:DE:9D:38 206 | response.use_edns(edns=True, ednsflags=dns.rcode.NOERROR, payload=query.payload, options=[query.options[0]]) #payload should be 1440, theoretical udp-over-eth maxsz stdframe 207 | logging.warning("Confirming SPS registration @%s with %s[%s] for %s secs" % (query.options[1].seq, address[0], query.options[1].pmac, query.options[0].lease)) 208 | logging.debug('RESPONSE--\n\n%s\n\n%s\n\n--RESPONSE END' % (response,response.options)) 209 | self.socket.sendto(response.to_wire(), address) 210 | -------------------------------------------------------------------------------- /sleepproxy/manager.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import sleepproxy.mdns as mdns 3 | import sleepproxy.arp as arp 4 | import sleepproxy.tcp as tcp 5 | 6 | import logging 7 | 8 | # https://docs.python.org/release/3.1.5/library/logging.html#using-loggeradapters-to-impart-contextual-information 9 | #logconn = loggingLoggerAdapter(logging.getLogger().addHandler(syslog), conn) 10 | #def log_host(info *args): 11 | # logconn.info(info, *args) 12 | 13 | # https://docs.python.org/release/3.1.5/library/logging.html#using-filters-to-impart-contextual-information 14 | #class ConnLogFilter(logging.Filter): 15 | # def filter(self, record): 16 | # record.sleeper = choice(arp._HOSTS) 17 | # record.waker = raddress 18 | # return True 19 | 20 | def manage_host(info): 21 | #log.addFilter(ConnLogFilter()) 22 | sleep(5) # prevent immediate waking after registration by backlogged in-flight ARP/TCP requests 23 | arp.handle(info['othermac'], info['addresses'], info['mymac'], info['myif']) #+1 Thread.start() #handle grat-arps first 24 | tcp.handle(info['othermac'], info['addresses'], info['myif']) #+1 Thread.start() #then L3 reqs 25 | mdns.handle(info['othermac'], info['records']) #advertise last 26 | 27 | def forget_host(mac): 28 | logging.warning("De-registering %s from SPS" % mac) 29 | mdns.forget(mac) #drop adv first so mac's don't de-collide their own names 30 | tcp.forget(mac) 31 | arp.forget(mac) 32 | 33 | def print_hosts(*args): 34 | logging.warning("MDNS: %s" % mdns._HOSTS) 35 | logging.warning("ARP: %s" % arp._HOSTS) 36 | logging.warning("TCP: %s" % tcp._HOSTS) 37 | 38 | def advertise(*args): 39 | mdns.register_service({ 40 | 'name': '10-34-10-70 SleepProxyServer', #---. 41 | # http://www.opensource.apple.com/source/mDNSResponder/mDNSResponder-522.1.11/mDNSCore/mDNS.c ConstructSleepProxyServerName() 42 | # http://www.opensource.apple.com/source/mDNSResponder/mDNSResponder-522.1.11/mDNSCore/mDNSEmbeddedAPI.h 43 | # just make sure SPSType == 10, see SetSPS() 44 | # SPSFeatureFlags: 1=TCP KeepAlive based packet mangling for Back To My Mac 45 | # http://www.opensource.apple.com/source/mDNSResponder/mDNSResponder-522.1.11/mDNSMacOSX/mDNSMacOSX.c SPSCreateDict() 46 | 'stype': '_sleep-proxy._udp', 47 | 'domain': '', 48 | 'host': '', #"" = localhost, use fqdn to isolate to a specific interface 49 | 'protocol': mdns.PROTO_UNSPEC, 50 | 'port': 3535, 51 | }) 52 | -------------------------------------------------------------------------------- /sleepproxy/mdns.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import dbus 3 | import dns.rdatatype 4 | import dns.rdataclass 5 | 6 | # http://git.0pointer.net/avahi.git/tree/avahi-python/avahi/__init__.py 7 | IF_UNSPEC = -1 8 | PROTO_UNSPEC = -1 #dual-stack 9 | PROTO_INET = 0 #v4 10 | PROTO_INET6 = 1 11 | 12 | dns.rdataclass.UNIQUE = 0x8000 #32768 13 | #"cache-flush bit" in mdns RFC6762 ch#10.2, and see ch#22 14 | # http://www.opensource.apple.com/source/mDNSResponder/mDNSResponder-522.1.11/mDNSCore/DNSCommon.c 15 | # http://www.opensource.apple.com/source/mDNSResponder/mDNSResponder-522.1.11/mDNSCore/mDNSEmbeddedAPI.h 16 | # kDNSClass_UniqueRRSet 17 | # have to filter it out from some OSX SPS clients' rdatas 18 | dns.rdataclass._by_value.update({0x8001: 'IN'}) #for nicer nsupdate text dumps 19 | 20 | _HOSTS = {} 21 | 22 | def string_to_byte_array(s): 23 | r = [] 24 | for c in s: 25 | r.append(dbus.Byte(ord(c))) 26 | return r 27 | 28 | def string_array_to_txt_array(t): 29 | l = [] 30 | for s in t: 31 | l.append(string_to_byte_array(s)) 32 | return l 33 | 34 | def register_service(record): 35 | group = _get_group() 36 | 37 | #http://linux.die.net/man/5/avahi.service 38 | group.AddService( 39 | record.get('iface', IF_UNSPEC), 40 | record.get('protocol', PROTO_UNSPEC), 41 | dbus.UInt32(record.get('flags', 0)), 42 | record.get('name'), 43 | record.get('stype'), 44 | record.get('domain'), 45 | record.get('host'), 46 | dbus.UInt16(record.get('port')), 47 | string_array_to_txt_array(record.get('text', '')), 48 | ) 49 | 50 | group.Commit() 51 | 52 | def handle(mac, records): 53 | if mac in _HOSTS: 54 | logging.debug("I already seem to be handling mDNS for %s" % (mac, )) 55 | return 56 | logging.info('Now mirroring mDNS advertisements from %s to local Avahi server' % (mac)) 57 | group = _get_group() 58 | _HOSTS[mac] = group 59 | _update_to_group(group, records) 60 | result = group.Commit(utf8_strings=True) 61 | logging.debug("Result of Commit() on mDNS records was %s" % (result, )) 62 | 63 | def forget(mac): 64 | logging.info("Removing %s from mDNS handler & Avahi" % (mac, )) 65 | if mac not in _HOSTS: 66 | logging.debug("I don't seem to be managing mDNS for %s" % (mac, )) 67 | return 68 | group = _HOSTS.pop(mac) 69 | group.Free() 70 | 71 | def _update_to_group(group, rrsets): 72 | """Convert a DNS UPDATE to additions to an Avahi mDNS group""" 73 | #logging.debug('parsing DNS UPDATE:\n\n\%s' % rrsets) 74 | for rrset in rrsets: 75 | for record in rrset: 76 | record.rdclass %= dns.rdataclass.UNIQUE #remove cache-flush bit 77 | 78 | if record.rdtype not in [dns.rdatatype.PTR, dns.rdatatype.A, dns.rdatatype.AAAA, dns.rdatatype.TXT, dns.rdatatype.SRV]: 79 | logging.warning('Invalid DNS RR type (%s), not adding mDNS record to Avahi' % record.rdtype) 80 | continue 81 | 82 | if record.rdclass != dns.rdataclass.IN: 83 | logging.warning('Invalid DNS RR class (%s), not adding mDNS record to Avahi' % record.rdclass) 84 | continue 85 | 86 | #if (record.rdtype == dns.rdatatype.PTR and ':' in record.to_digestable()) or record.rdtype == dns.rdatatype.AAAA: 87 | # continue #ignore IPV6 for now, can't sniff those connections 88 | 89 | try: 90 | group.AddRecord( #http://avahi.sourcearchive.com/documentation/0.6.30-5/avahi-client_2publish_8h_a849f3042580d6c8534cba820644517ac.html#a849f3042580d6c8534cba820644517ac 91 | IF_UNSPEC, # iface * 92 | PROTO_UNSPEC, # proto _INET & _INET6 93 | dbus.UInt32(256), # AvahiPublishFlags (use multicast) 94 | str(rrset.name).decode('utf-8'), #name 95 | dbus.UInt16(record.rdclass), #class 96 | dbus.UInt16(record.rdtype), #type 97 | dbus.UInt32(rrset.ttl), #ttl 98 | string_array_to_txt_array([record.to_digestable()])[0] #rdata 99 | ) 100 | logging.info('added mDNS record to Avahi: %s' % rrset.to_text()) 101 | except UnicodeDecodeError: 102 | logging.warn('malformed unicode in rdata, skipping: %s' % rrset.to_text()) 103 | except dbus.exceptions.DBusException, e: 104 | if e.get_dbus_name() == 'org.freedesktop.Avahi.InvalidDomainNameError': 105 | logging.warning('not mirroring mDNS record with special chars: %s' % rrset.to_text()) 106 | continue # skip this record since Avahi will reject it 107 | # mac probably sent a device_info PTR with spaces and parentheses in the friendly description 108 | # per https://tools.ietf.org/html/rfc6763#section-4.1.3 109 | # fanboy\032\(2\)._eppc._tcp.local. 4500 CLASS32769 TXT "" # `fanboy (2)` 110 | # mDNS.c sends UTF8, dnspythom.from_wire() assumes ASCII, DBUS wants Unicode, Avahi only takes [a-zA-Z0-9.-] 111 | # http://dbus.freedesktop.org/doc/dbus-python/api/dbus.String-class.html 112 | # http://dbus.freedesktop.org/doc/dbus-python/api/dbus.UTF8String-class.html 113 | # http://www.avahi.org/ticket/21 http://avahi.org/ticket/63 114 | # http://git.0pointer.net/avahi.git/commit/?id=5c22acadcbe5b01d910d75b71e86e06a425172d3 115 | # http://git.0pointer.net/avahi.git/commit/?id=ee2820a23c6968bbeadbdf510389301dca6bc765 116 | # http://git.0pointer.net/avahi.git/tree/avahi-common/domain.c 117 | raise 118 | 119 | 120 | def _get_group(): 121 | """Create a group, on the system bus""" 122 | bus = dbus.SystemBus() 123 | server = dbus.Interface( 124 | bus.get_object('org.freedesktop.Avahi', '/'), 125 | 'org.freedesktop.Avahi.Server', 126 | ) 127 | 128 | return dbus.Interface( 129 | bus.get_object('org.freedesktop.Avahi', server.EntryGroupNew()), 130 | 'org.freedesktop.Avahi.EntryGroup', 131 | ) 132 | -------------------------------------------------------------------------------- /sleepproxy/sniff.py: -------------------------------------------------------------------------------- 1 | from select import select 2 | from threading import Event, Thread 3 | 4 | from scapy.config import conf 5 | from scapy.data import ETH_P_ALL, MTU 6 | 7 | class SnifferThread(Thread): 8 | """A thread which runs a scapy sniff, and can be stopped""" 9 | 10 | def __init__(self, prn, filterexp, iface): 11 | Thread.__init__(self) #make this a greenlet? 12 | self._prn = prn 13 | self._filterexp = filterexp 14 | self._iface = iface 15 | self._stop_recd = Event() 16 | 17 | def run(self): 18 | self._sniff() 19 | 20 | def stop(self): 21 | self._stop_recd.set() 22 | 23 | def _sniff(self): 24 | sock = conf.L2listen(type=ETH_P_ALL, filter=self._filterexp, iface=self._iface) 25 | 26 | while 1: 27 | try: 28 | sel = select([sock], [], [], 1) 29 | if sock in sel[0]: 30 | p = sock.recv(MTU) 31 | if p is None: 32 | break 33 | self._prn(p) 34 | if self._stop_recd.is_set(): 35 | print "Breaking out of sniffer thread %s" % (self, ) 36 | break 37 | except KeyboardInterrupt: 38 | break 39 | sock.close() 40 | -------------------------------------------------------------------------------- /sleepproxy/tcp.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | import logging 3 | 4 | from scapy.all import IP, TCP 5 | 6 | import sleepproxy.manager 7 | from sleepproxy.sniff import SnifferThread 8 | from sleepproxy.wol import wake 9 | from time import sleep 10 | 11 | _HOSTS = {} 12 | 13 | def handle(mac, addresses, iface): 14 | if mac in _HOSTS: 15 | logging.debug("Ignoring already managed TCP host %s" % (mac, )) 16 | 17 | logging.info("Now handling TCP SYNs for %s:%s on %s" % (mac, addresses, iface)) 18 | 19 | for address in addresses: 20 | #we can be fancier, wake on port 22 with plain packets, not just syn 21 | #http://www.opensource.apple.com/source/mDNSResponder/mDNSResponder-522.1.11/mDNSCore/mDNS.c mDNSCoreReceiveRawTransportPacket() 22 | if ':' in address: #ipv6 23 | expr = "ip6[6]=6 && ip6[53]&4!=0 and ip6[6]=6 && ip6[53]&1=0 and dst host %s" % (address) #ipv6 can have multiple headers, so no tcp* shortcuts in pcap-filter 24 | else: 25 | expr = "tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack = 0 and dst host %s" % (address) 26 | thread = SnifferThread( filterexp=expr, prn=partial(_handle_packet, mac, address), iface=iface) #using a callback, but nut not doing it async 27 | _HOSTS[mac] = thread 28 | thread.start() #make this a greenlet? 29 | 30 | def forget(mac): 31 | logging.info("Removing host %s from TCP handler" % (mac, )) 32 | if mac not in _HOSTS: 33 | logging.info("I don't seem to know about %s, ignoring" % (mac, )) 34 | return 35 | _HOSTS[mac].stop() 36 | del _HOSTS[mac] 37 | 38 | def _handle_packet(mac, address, packet): 39 | """Do something with a SYN for the other machine!""" 40 | if not (IP in packet and TCP in packet): 41 | return 42 | if packet[IP].dst != address: 43 | logging.debug("Sniffed a TCP SYN for the wrong address?: %s" % packet.show() ) 44 | return 45 | #logging.debug(packet.display()) 46 | sleepproxy.manager.mdns.forget(mac) # pre-emptively drop adv to keep the mac from de-colliding its name 47 | sleep(0.4) 48 | wake(mac) #retry=15? 49 | #sleepproxy.manager.forget_host(mac) 50 | 51 | -------------------------------------------------------------------------------- /sleepproxy/wol.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from scapy.all import sendp, Ether, IP, UDP, Raw 3 | 4 | def wake(mac): 5 | logging.warning("Sending WOL packet to %s" % (mac, )) 6 | mac = mac.decode("hex") 7 | sendp(Ether(dst='ff:ff:ff:ff:ff:ff') / IP(dst='255.255.255.255', flags="DF") / UDP(dport=9, sport=39227) / Raw('\xff' * 6 + mac * 16)) 8 | 9 | if __name__ == '__main__': 10 | import sys 11 | wake(sys.argv[1]) 12 | -------------------------------------------------------------------------------- /test/udp.py: -------------------------------------------------------------------------------- 1 | # ad-hoc tests for the UDP server 2 | 3 | from sleepproxy.udp import DatagramServer 4 | 5 | 6 | def echo(*args, **kwargs): 7 | print args 8 | print kwargs 9 | # print "Message from %s: %s" % (message, address) 10 | 11 | if __name__ == '__main__': 12 | server = DatagramServer(('127.0.0.1', 6000), echo) 13 | print "Starting UDP server on port 6000..." 14 | server.serve_forever() 15 | --------------------------------------------------------------------------------