dhcp.py
Go to the documentation of this file.
00001 #! /usr/bin/env python
00002 
00003 import roslib; roslib.load_manifest('multi_interface_roam')
00004 import asmach as smach
00005 import scapy.all as scapy
00006 from twisted.internet.defer import inlineCallbacks, returnValue
00007 from netlink_monitor import monitor, IFSTATE
00008 import async_helpers
00009 import l2socket
00010 import event
00011 import random
00012 import time
00013 import traceback
00014 import ipaddr
00015 import state_publisher
00016 import config
00017 
00018 # FIXME Add support for multiple leases.
00019 # FIXME Add a way to signal x amount of time before we lose the lease.
00020 
00021 class DhcpLease:
00022     def __init__(self):
00023         self.timeout_time = {}
00024         self.public_config = {}
00025 
00026 class DhcpData:
00027     def __init__(self, iface):
00028         self.iface = iface
00029         self.socket = None
00030         self.link_addr_state = monitor.get_state_publisher(self.iface, IFSTATE.LINK_ADDR) 
00031         self.error_event = event.Event()
00032         self.binding_publisher = state_publisher.StatePublisher(None)
00033         self.error_timeout = 5
00034         self.exp_backoff_min = 0.2
00035         self.exp_backoff_max = 0.5
00036         self.exp_backoff_timeout = config.get_parameter('dhcp_timeout', 2)
00037         self.leases = {}
00038 
00039     def start_socket(self):
00040         if not self.socket or self.socket.port.fileno() == -1:
00041             self.socket = async_helpers.ReadDescrEventStream(l2socket.L2Port, iface = self.iface, 
00042                     filter='udp and dst port 68 and src port 67')
00043 
00044     def stop_socket(self):
00045         self.socket = None
00046 
00047 class DhcpState(smach.State):
00048     def __init__(self, *args, **kwargs):
00049         smach.State.__init__(self, input_keys=['dhcp'], output_keys=['dhcp'], *args, **kwargs)
00050 
00051 def find_dhcp_option(key, default, dhcp):
00052     for opt in dhcp.options:
00053         if opt == key:
00054             return None
00055         if opt[0] == key:
00056             return opt[1:]
00057     return default
00058 
00059 
00060 def dhcp_type_str(id):
00061     try:
00062         return scapy.DHCPTypes[id[0]]
00063     except KeyError:
00064         return "<unknown %s>"%repr(id)
00065  
00066 
00067 
00068 class ExchangeRetryExponentialBackoff:
00069     def init_timeouts(self, ud):
00070         self.cur_retry_max = ud.dhcp.exp_backoff_min
00071         return ud.dhcp.exp_backoff_timeout
00072 
00073     def get_next_retry(self, ud):
00074         interval = self.cur_retry_max * random.uniform(0.5, 1)
00075         self.cur_retry_max = min(ud.dhcp.exp_backoff_max, 2 * self.cur_retry_max)
00076         return interval
00077 
00078 
00079 
00080 class ExchangeRetryHalve:
00081     def init_timeouts(self, ud):
00082         return ud.dhcp.lease.timeout_time[self.type] - time.time()
00083 
00084     def get_next_retry(self, ud):
00085         return 60 + (ud.dhcp.lease.timeout_time[self.type] - time.time()) / 2
00086 
00087 
00088         
00089 class ExchangeDiscover:
00090     message_type = "discover"
00091 
00092     def init_xid(self, ud):
00093         ud.dhcp.lease.xid = random.randint(0, 0xFFFF)
00094 
00095     def validate(self, ud, pkt):
00096         try:
00097             ip = pkt.payload
00098             udp = ip.payload 
00099             bootp = udp.payload
00100             dhcp = bootp.payload
00101             
00102             if not self.validate_common(ud, pkt, ip, udp, bootp, dhcp):
00103                 print "validate_common returned False"
00104                 return False
00105 
00106             message_type = dhcp_type_str(find_dhcp_option('message-type', None, dhcp))
00107             if message_type != 'offer':
00108                 print "Ignoring packet based on unexpected message_type %s"%message_type
00109                 return False
00110                 
00111             ip = ud.dhcp.lease.public_config['ip'] = bootp.yiaddr
00112             ud.dhcp.lease.server_ip = find_dhcp_option('server_id', "0.0.0.0", dhcp)[0]
00113             ud.dhcp.lease.server_mac = pkt.src
00114             ud.dhcp.lease.public_config['gateway'] = find_dhcp_option('router', "0.0.0.0", dhcp)[0]
00115             netmask = ud.dhcp.lease.public_config['netmask'] = find_dhcp_option('subnet_mask', "0.0.0.0", dhcp)[0]
00116             net = ipaddr.IPv4Network("%s/%s"%(ip, netmask))
00117             ud.dhcp.lease.public_config['netmask_bits'] = net.prefixlen
00118             ud.dhcp.lease.public_config['network'] = net.network
00119             ud.dhcp.lease.public_config['ip_slashed'] = "%s/%i"%(ip, net.prefixlen)
00120             ud.dhcp.lease.public_config['network_slashed'] = "%s/%i"%(net.network, net.prefixlen)
00121 
00122             # TODO do more stuff here?
00123             return 'success'
00124             
00125 
00126         except:
00127             traceback.print_exc()
00128             print "Excepiton validating packet."
00129             return False
00130 
00131 
00132 class ExchangeRequest:
00133     message_type = "request"
00134     
00135     def init_xid(self, ud):
00136         pass
00137         
00138     def validate(self, ud, pkt):
00139         try:
00140             ip = pkt.payload
00141             udp = ip.payload 
00142             bootp = udp.payload
00143             dhcp = bootp.payload
00144             
00145             if not self.validate_common(ud, pkt, ip, udp, bootp, dhcp):
00146                 print "validate_common returned False"
00147                 return False
00148 
00149             message_type = dhcp_type_str(find_dhcp_option('message-type', None, dhcp))
00150             if message_type == 'nak':
00151                 return 'fail' # TODO Check that from right server?
00152             
00153             if message_type != 'ack':
00154                 print "Ignoring packet based on unexpected message_type %s"%message_type
00155                 return False
00156 
00157             lease_time = find_dhcp_option('lease_time', None, dhcp)
00158             if not lease_time:
00159                 print "Ignoring packet with no lease_time"
00160             lease_time = lease_time[0] 
00161             renewal_time = find_dhcp_option('renewal_time', (random.uniform(0.45, 0.55) * lease_time, ), dhcp)[0]
00162             rebind_time = find_dhcp_option('rebinding_time', (random.uniform(0.825, 0.925) * lease_time, ), dhcp)[0]
00163             #lease_time = 10
00164             #rebind_time = 2
00165             #renewal_time = 1
00166             ud.dhcp.lease.timeout_time['BOUND'] = renewal_time * 0.99 + self.send_time
00167             ud.dhcp.lease.timeout_time['RENEW'] = rebind_time * 0.99 + self.send_time
00168             ud.dhcp.lease.timeout_time['REBIND'] = lease_time * 0.99 + self.send_time
00169 
00170 
00171             # FIXME Read other options here.
00172             return 'success'
00173 
00174         except:
00175             traceback.print_exc()
00176             print "Excepiton validating packet."
00177             return False
00178 
00179 
00180 
00181 class NoLink(DhcpState):
00182     def __init__(self):
00183         DhcpState.__init__(self, outcomes=['bound', 'init', 'init_reboot'])
00184 
00185     @inlineCallbacks
00186     def execute_async(self, ud):
00187         ud.dhcp.binding_publisher.set(None)
00188         ud.dhcp.hwaddr = yield async_helpers.wait_for_state(ud.dhcp.link_addr_state, lambda x: x != False)
00189         network_id = "" # Include MAC, network_id
00190         if network_id not in ud.dhcp.leases:
00191             ud.dhcp.leases[network_id] = DhcpLease()
00192         ud.dhcp.lease = ud.dhcp.leases[network_id]
00193         returnValue('init')
00194 
00195 
00196 
00197 class Init(DhcpState):
00198     def __init__(self):
00199         DhcpState.__init__(self, outcomes=['done', 'nolink'])
00200 
00201     def execute_async(self, ud):
00202         ud.dhcp.binding_publisher.set(None)
00203         ud.dhcp.start_socket()
00204         return 'done'
00205 
00206 ETHER_BCAST='ff:ff:ff:ff:ff:ff'
00207 IP_BCAST='255.255.255.255'
00208 IP_ZERO='0.0.0.0'
00209 
00210 class Exchange(DhcpState):
00211     def __init__(self):
00212         DhcpState.__init__(self, outcomes=['success', 'fail', 'nolink'])
00213 
00214     def send(self, ud):
00215         hwbytes = scapy.mac2str(ud.dhcp.hwaddr)
00216 
00217         # Prepare options
00218         options = [
00219                 ("message-type", self.message_type), 
00220                 ("param_req_list", 
00221                     chr(scapy.DHCPRevOptions["renewal_time"][0]),
00222                     chr(scapy.DHCPRevOptions["rebinding_time"][0]),
00223                     chr(scapy.DHCPRevOptions["lease_time"][0]),
00224                     chr(scapy.DHCPRevOptions["subnet_mask"][0]),
00225                     chr(scapy.DHCPRevOptions["router"][0]),
00226                     )
00227                 ]
00228         if self.type in ["REQUEST", "REBOOT", ]:
00229             options.append(('requested_addr', ud.dhcp.lease.public_config['ip']))
00230         if self.type in ["REQUEST", ]:
00231             options.append(('server_id', ud.dhcp.lease.server_ip))
00232         options.append('end')
00233         pkt = scapy.DHCP(options=options)
00234 
00235         # Prepare BOOTP
00236         pkt = scapy.BOOTP(chaddr=[hwbytes], xid=ud.dhcp.lease.xid)/pkt
00237         if self.type in [ "RENEW", "REBIND", ]:
00238             pkt.ciaddr = ud.dhcp.lease.public_config['ip'] 
00239 
00240         # Prepare UDP/IP
00241         pkt = scapy.IP(src=pkt.ciaddr, dst=IP_BCAST)/scapy.UDP(sport=68, dport=67)/pkt
00242         if self.type in [ "RENEW" ]:
00243             pkt.dst = ud.dhcp.lease.server_ip
00244 
00245         # Prepare Ethernet
00246         pkt = scapy.Ether(src=ud.dhcp.hwaddr, dst=ETHER_BCAST)/pkt
00247         if self.type in [ "RENEW" ]:
00248             pkt.dst = ud.dhcp.lease.server_mac
00249         #print "Out:", repr(scapy.Ether(str(pkt)))
00250         ud.dhcp.socket.port.send(str(pkt))
00251                     
00252     def validate_common(self, ud, pkt, ip, udp, bootp, dhcp): 
00253         # Should we be receiving this packet?
00254         if pkt.dst != ud.dhcp.hwaddr and pkt.dst != ETHER_BCAST:
00255             print "Discarding packet based on destination MAC: %s != %s"%(pkt.dst, ud.dhcp.hwaddr)
00256             return False
00257 
00258         # Does the xid match?
00259         if pkt.xid != ud.dhcp.lease.xid:
00260             print "Discarding packet based on xid: %i != %i"%(pkt.xid, ud.dhcp.lease.xid)
00261             return False
00262        
00263         # TODO Check that from right server?
00264         
00265         return True
00266 
00267     @inlineCallbacks
00268     def execute_async(self, ud):
00269         # Parameters that will depend on the state.
00270         timeout = async_helpers.Timeout(self.init_timeouts(ud))
00271 
00272         # Make sure we aren't discarding incoming dhcp packets.
00273         ud.dhcp.socket.set_discard(False)
00274 
00275         # Generate an xid for the exchange if necessary.
00276         self.init_xid(ud)
00277 
00278         while True:
00279             # Send a request packet
00280             try:
00281                 self.send_time = time.time()
00282                 self.send(ud)
00283             except:
00284                 traceback.print_exc()
00285                 returnValue('fail')
00286 
00287             # How long to wait before retry
00288             interval = self.get_next_retry(ud)
00289 
00290             while True:
00291                 # Wait for an event
00292                 events = yield async_helpers.select(
00293                         async_helpers.StateCondition(ud.dhcp.link_addr_state, lambda x: x == False), 
00294                         ud.dhcp.socket, 
00295                         async_helpers.Timeout(interval),
00296                         timeout)
00297 
00298                 if 0 in events: # Lost link
00299                     returnValue('nolink')
00300     
00301                 if 1 in events: # Got packet
00302                     pkt = scapy.Ether(ud.dhcp.socket.recv())
00303                     #print "In:", repr(pkt)
00304 
00305                     result = self.validate(ud, pkt)
00306                     if result:
00307                         returnValue(result)
00308     
00309                 if 2 in events:
00310                     break
00311     
00312                 if 3 in events:
00313                     returnValue('fail')
00314 
00315 
00316 
00317 class Rebooting  (Exchange, ExchangeRetryExponentialBackoff, ExchangeRequest ): type = "REBOOT"
00318 class Selecting  (Exchange, ExchangeRetryExponentialBackoff, ExchangeDiscover): type = "SELECT"
00319 class Requesting (Exchange, ExchangeRetryExponentialBackoff, ExchangeRequest ): type = "REQUEST"
00320 class Renewing   (Exchange, ExchangeRetryHalve,              ExchangeRequest ): type = "RENEW"
00321 class Rebinding  (Exchange, ExchangeRetryHalve,              ExchangeRequest ): type = "REBIND"
00322 
00323 
00324 
00325 class Error(DhcpState):
00326     def __init__(self):
00327         DhcpState.__init__(self, outcomes=['done', 'nolink'])
00328  
00329     @inlineCallbacks
00330     def execute_async(self, ud):
00331         ud.dhcp.binding_publisher.set(None)
00332         ud.dhcp.error_event.trigger()
00333         ud.dhcp.socket.set_discard(True)
00334         events = yield async_helpers.select(
00335                 async_helpers.StateCondition(ud.dhcp.link_addr_state, lambda x: x == False),
00336                 async_helpers.Timeout(ud.dhcp.error_timeout)
00337                 )
00338 
00339         if 0 in events:
00340             returnValue('nolink')
00341 
00342         returnValue('done')
00343 
00344 
00345 class Bound(DhcpState):
00346     def __init__(self):
00347         DhcpState.__init__(self, outcomes=['timeout', 'nolink'])
00348  
00349     @inlineCallbacks
00350     def execute_async(self, ud):
00351         ud.dhcp.socket.set_discard(True)
00352         ud.dhcp.binding_publisher.set(ud.dhcp.lease.public_config)
00353         events = yield async_helpers.select(
00354             async_helpers.StateCondition(ud.dhcp.link_addr_state, lambda x: x == False),
00355             async_helpers.Timeout(ud.dhcp.lease.timeout_time['BOUND'] - time.time()),
00356             )
00357 
00358         if 0 in events:
00359             returnValue('nolink')
00360 
00361         returnValue('timeout')
00362 
00363 
00364 def dhcp_client(iface):
00365     sm = smach.StateMachine(outcomes=[], input_keys=['dhcp'])
00366     smadd = smach.StateMachine.add
00367     with sm:
00368         smadd('NOLINK',      NoLink(),      transitions = {'bound'  :'BOUND', 'init':'INIT', 'init_reboot':'INIT_REBOOT'})
00369         smadd('INIT_REBOOT', Init(),        transitions = {'done'   :'REBOOT',                   'nolink':'NOLINK'})
00370         smadd('REBOOT',      Rebooting(),   transitions = {'success':'BOUND',   'fail':'INIT',   'nolink':'NOLINK'})
00371         smadd('INIT',        Init(),        transitions = {'done'   :'SELECT',                   'nolink':'NOLINK'})
00372         smadd('SELECT',      Selecting(),   transitions = {'success':'REQUEST', 'fail':'ERROR',  'nolink':'NOLINK'})
00373         smadd('REQUEST',     Requesting(),  transitions = {'success':'BOUND',   'fail':'ERROR',  'nolink':'NOLINK'})
00374         smadd('BOUND',       Bound(),       transitions = {'timeout':'RENEW',                    'nolink':'NOLINK'})
00375         smadd('RENEW',       Renewing(),    transitions = {'success':'BOUND',   'fail':'REBIND', 'nolink':'NOLINK'})
00376         smadd('REBIND',      Rebinding(),   transitions = {'success':'BOUND',   'fail':'INIT',   'nolink':'NOLINK'})
00377         smadd('ERROR',       Error(),       transitions = {'done'   :'INIT',                     'nolink':'NOLINK'})
00378 
00379     ud = smach.UserData()
00380     ud.dhcp = DhcpData(iface)
00381 #    def shutdown(value):
00382 #        reactor.fireSystemEvent('shutdown')
00383 #    def ignore_eintr(error):
00384 #        if error.type == IOError:
00385 #            import errno
00386 #            if error.value.errno == errno.EINTR:
00387 #                return None
00388 #        return error
00389     sm.execute_async(ud)#.addCallback(shutdown).addErrback(ignore_eintr)
00390     return ud.dhcp
00391 
00392 if __name__ == "__main__":
00393     import sys
00394     from twisted.internet import reactor
00395 
00396     if len(sys.argv) != 2:
00397         print "usage: dhcp.py <interface>"
00398         sys.exit(1)
00399 
00400     iface = sys.argv[1]
00401 
00402     dhcp_client(iface)
00403     reactor.run()    


multi_interface_roam
Author(s): Blaise Gassend
autogenerated on Thu Jan 2 2014 11:26:15