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