5c96f13169616a8fb3b73a36296a52bd590aa9fc
[invirt/packages/invirt-dhcp.git] / invirt-dhcpserver
1 #!/usr/bin/python
2 import sys
3 import pydhcplib
4 import pydhcplib.dhcp_network
5 from pydhcplib.dhcp_packet import *
6 from pydhcplib.type_hw_addr import hwmac
7 from pydhcplib.type_ipv4 import ipv4
8 from pydhcplib.type_strlist import strlist
9 import socket
10 import IN
11 from Queue import Queue
12 from threading import Thread
13 from subprocess import PIPE, Popen
14 import netifaces as ni
15 sys.path.append('/usr/lib/xen-default/lib/python/')
16 from xen.lowlevel import xs
17
18 import syslog as s
19
20 import time
21 from invirt import database
22 from invirt.config import structs as config
23
24 dhcp_options = {'domain_name_server': ','.join(config.dhcp.dns),
25                 'ip_address_lease_time': config.dhcp.get('leasetime', 60*60*24)}
26
27 class DhcpBackend:
28     def __init__(self, queue):
29         database.connect()
30         self.queue = queue
31         self.main_ip = ni.ifaddresses(config.xen.iface)[ni.AF_INET][0]['addr']
32     def add_route_and_arp(self, ip, intf, gateway):
33         try:
34             p = Popen(['ip', 'route', 'add', ip, 'dev', intf, 'src', self.main_ip, 'metric', '2' if intf.startswith('vif') else '1'], stdout=PIPE, stderr=PIPE)
35             (out, err) = p.communicate()
36             if p.returncode == 0:
37                 s.syslog(s.LOG_INFO, "Added route for IP %s to interface %s" % (ip, intf))
38                 self.queue.put((ip, gateway))
39             sys.stderr.write(err)
40             sys.stdout.write(out)
41         except Exception as e:
42             s.syslog(s.LOG_ERR, "Could not add route for IP %s: %s" % (ip, e))
43     def findNIC(self, mac):
44         database.clear_cache()
45         return database.NIC.query.filter_by(mac_addr=mac).first()
46     def find_interface(self, packet):
47         chaddr = hwmac(packet.GetHardwareAddress())
48         nic = self.findNIC(str(chaddr))
49         return self.find_interface_by_nic(nic)
50     def find_interface_by_nic(self, nic):
51         if nic is None or nic.ip is None:
52             return None
53         ipstr = ''.join(reversed(['%02X' % i for i in ipv4(nic.ip.encode("utf-8")).list()]))
54         for line in open('/proc/net/route'):
55             parts = line.split()
56             if parts[1] == ipstr:
57                 s.syslog(s.LOG_DEBUG, "find_interface found "+str(nic.ip)+" on "+parts[0])
58                 return parts[0]
59         # Either the machine isn't running, or the route is missing.  We can
60         # fix the latter.
61         try:
62             xsc = xs.xs()
63             domid = xsc.read('', '/vm/%s/device/vif/0/frontend-id' % (nic.machine.uuid))
64             # If we didn't find the domid, the machine is either off or the
65             # UUID in xenstore isn't right.  Try slightly harder.
66             if not domid:
67                 for uuid in xsc.ls('', '/vm'):
68                     if xsc.read('', '/vm/%s/name' % (uuid)) == 'd_' + nic.machine.name:
69                         domid = xsc.read('', '/vm/%s/device/vif/0/frontend-id' % (uuid))
70             if not domid:
71                 xsc.close()
72                 return None
73             for vifnum in xsc.ls('', '/local/domain/0/backend/vif/%s' % (domid)):
74                 if xsc.read('', '/local/domain/0/backend/vif/%s/%s/mac' % (domid, vifnum)) == nic.mac_addr:
75                     # Prefer the tap if it exists; paravirtualized HVMs will
76                     # have already unplugged it, so if it's there, it's the one
77                     # in use.
78                     for viftype in ('tap', 'vif'):
79                         vif = '%s%s.%s' % (viftype, domid, vifnum)
80                         if vif in ni.interfaces():
81                             self.add_route_and_arp(nic.ip, vif, nic.gateway)
82                             xsc.close()
83                             return vif
84             xsc.close()
85         except Exception as e:
86             try:
87                 xsc.close()
88             except Exception as e2:
89                 s.syslog(s.LOG_ERR, "Could not close connection to xenstore: %s" % (e2))
90             s.syslog(s.LOG_ERR, "Could not find interface and add missing route: %s" % (e))
91         return None
92                             
93     def getParameters(self, **extra):
94         all_options=dict(dhcp_options)
95         all_options.update(extra)
96         options = {}
97         for parameter, value in all_options.iteritems():
98             if value is None:
99                 continue
100             option_type = DhcpOptionsTypes[DhcpOptions[parameter]]
101
102             if option_type == "ipv4" :
103                 # this is a single ip address
104                 options[parameter] = map(int,value.split("."))
105             elif option_type == "ipv4+" :
106                 # this is multiple ip address
107                 iplist = value.split(",")
108                 opt = []
109                 for single in iplist :
110                     opt.extend(ipv4(single).list())
111                 options[parameter] = opt
112             elif option_type == "32-bits" :
113                 # This is probably a number...
114                 digit = int(value)
115                 options[parameter] = [digit>>24&0xFF,(digit>>16)&0xFF,(digit>>8)&0xFF,digit&0xFF]
116             elif option_type == "16-bits" :
117                 digit = int(value)
118                 options[parameter] = [(digit>>8)&0xFF,digit&0xFF]
119
120             elif option_type == "char" :
121                 digit = int(value)
122                 options[parameter] = [digit&0xFF]
123
124             elif option_type == "bool" :
125                 if value=="False" or value=="false" or value==0 :
126                     options[parameter] = [0]
127                 else : options[parameter] = [1]
128                     
129             elif option_type == "string" :
130                 options[parameter] = strlist(value).list()
131             
132             elif option_type == "RFC3397" :
133                 parsed_value = ""
134                 for item in value:
135                     components = item.split('.')
136                     item_fmt = "".join(chr(len(elt)) + elt for elt in components) + "\x00"
137                     parsed_value += item_fmt
138                 
139                 options[parameter] = strlist(parsed_value).list()
140             
141             else :
142                 options[parameter] = strlist(value).list()
143         return options
144
145     def Discover(self, packet):
146         s.syslog(s.LOG_DEBUG, "dhcp_backend : Discover ")
147         chaddr = hwmac(packet.GetHardwareAddress())
148         nic = self.findNIC(str(chaddr))
149         if nic is None or nic.machine is None:
150             return False
151         ip = nic.ip.encode("utf-8")
152         if ip is None:  #Deactivated?
153             return False
154
155         options = {}
156         options['subnet_mask'] = nic.netmask.encode("utf-8")
157         options['router'] = nic.gateway.encode("utf-8")
158         if nic.hostname and '.' in nic.hostname:
159             options['host_name'], options['domain_name'] = nic.hostname.encode('utf-8').split('.', 1)
160         elif nic.machine.name:
161             options['host_name'] = nic.machine.name.encode('utf-8')
162             options['domain_name'] = config.dns.domains[0]
163         else:
164             hostname = None
165         if DhcpOptions['domain_search'] in packet.GetOption('parameter_request_list'):
166             options['host_name'] += '.' + options['domain_name']
167             del options['domain_name']
168             options['domain_search'] = [config.dhcp.search_domain]
169         ip = ipv4(ip)
170         s.syslog(s.LOG_DEBUG,"dhcp_backend : Discover result = "+str(ip))
171         packet_parameters = self.getParameters(**options)
172
173         # FIXME: Other offer parameters go here
174         packet_parameters["yiaddr"] = ip.list()
175
176         packet.SetMultipleOptions(packet_parameters)
177         return True
178         
179     def Request(self, packet):
180         s.syslog(s.LOG_DEBUG, "dhcp_backend : Request")
181         
182         discover = self.Discover(packet)
183         
184         chaddr = hwmac(packet.GetHardwareAddress())
185         request = packet.GetOption("request_ip_address")
186         if not request:
187             request = packet.GetOption("ciaddr")
188         yiaddr = packet.GetOption("yiaddr")
189
190         if not discover:
191             s.syslog(s.LOG_INFO,"Unknown MAC address: "+str(chaddr))
192             return False
193         
194         if yiaddr!="0.0.0.0" and yiaddr == request :
195             s.syslog(s.LOG_INFO,"Ack ip "+str(yiaddr)+" for "+str(chaddr))
196             n = self.findNIC(str(chaddr))
197             intf = self.find_interface_by_nic(n)
198             s.syslog(s.LOG_ERR, "Interface is %s" % (intf))
199             # Don't perform "other" actions if the machine isn't running
200             other_action = n.other_action if n.other_action and intf else ''
201             if other_action in ('renumber', 'renumber_dhcp'):
202                 (n.ip, n.netmask, n.gateway, n.other_ip, n.other_netmask,
203                  n.other_gateway) = (
204                  n.other_ip, n.other_netmask, n.other_gateway, n.ip,
205                  n.netmask, n.gateway)
206                 other_action = n.other_action = 'dnat'
207                 database.session.add(n)
208                 database.session.flush()
209             if other_action == 'dnat':
210                 # If the machine was booted in 'dnat' mode, then both
211                 # routes were already added by the invirt-database script.
212                 # If the machine was already on and has just been set to
213                 # 'dnat' mode, we need to add the route for the 'other' IP.
214                 # If the machine has just been 'renumbered' by us above,
215                 # the IPs will be swapped and only the route for the main
216                 # IP needs to be added.  Just try adding both of them, and
217                 # arp for whichever of them turns out to be new.
218                 for parms in [(n.ip, n.gateway), (n.other_ip, n.other_gateway)]:
219                     self.add_route_and_arp(parms[0], intf, parms[1])
220                 try:
221                     # iptables will let you add the same rule again and again;
222                     # let's not do that.
223                     p = Popen(['iptables', '-t', 'nat', '-C', 'PREROUTING', '-d', n.other_ip, '-j', 'DNAT', '--to-destination', n.ip], stdout=PIPE, stderr=PIPE)
224                     (out, err) = p.communicate()
225                     sys.stderr.write(err)
226                     sys.stdout.write(out)
227                     if p.returncode != 0:
228                         p2 = Popen(['iptables', '-t', 'nat', '-A', 'PREROUTING', '-d', n.other_ip, '-j', 'DNAT', '--to-destination', n.ip], stdout=PIPE, stderr=PIPE)
229                         (out, err) = p2.communicate()
230                         sys.stderr.write(err)
231                         sys.stdout.write(out)
232                         if p2.returncode == 0:
233                             s.syslog(s.LOG_INFO, "Added DNAT for IP %s to %s" % (n.other_ip, n.ip))
234                         else:
235                             s.syslog(s.LOG_ERR, "Could not add DNAT for IP %s to %s" % (n.other_ip, n.ip))
236                 except Exception as e:
237                     s.syslog(s.LOG_ERR, "Could not check and/or add DNAT for IP %s to %s: %s" % (n.other_ip, n.ip, e))
238             if other_action == 'remove':
239                 try:
240                     p = Popen(['ip', 'route', 'del', n.other_ip, 'dev', intf], stdout=PIPE, stderr=PIPE)
241                     (out, err) = p.communicate()
242                     sys.stderr.write(err)
243                     sys.stderr.write(out)
244                     if p.returncode == 0:
245                         s.syslog(s.LOG_INFO, "Removed route for IP %s" % (n.other_ip))
246                     else:
247                         s.syslog(s.LOG_ERR, "Could not remove route for IP %s" % (n.other_ip))
248                 except Exception as e:
249                     s.syslog(s.LOG_ERR, "Could not run ip to remove route for IP %s: %s" % (n.other_ip, e))
250                 try:
251                     p = Popen(['iptables', '-t', 'nat', '-D', 'PREROUTING', '-d', n.other_ip, '-j', 'DNAT', '--to-destination', n.ip], stdout=PIPE, stderr=PIPE)
252                     (out, err) = p.communicate()
253                     sys.stderr.write(err)
254                     sys.stdout.write(out)
255                     if p.returncode == 0:
256                         s.syslog(s.LOG_INFO, "Removed DNAT for IP %s" % (n.other_ip))
257                     else:
258                         s.syslog(s.LOG_ERR, "Could not remove DNAT for IP %s" % (n.other_ip))
259                 except Exception as e:
260                     s.syslog(s.LOG_ERR, "Could not run iptables to remove DNAT for IP %s: %s" % (n.other_ip, e))
261                 n.other_ip = n.other_netmask = n.other_gateway = n.other_action = None
262                 database.session.add(n)
263                 database.session.flush()
264             # We went through the DISCOVER codepath already to populate some
265             # of the packet's parameters.  If we renumbered the VM just above,
266             # the packet is set to offer them what they asked for - the old
267             # address.  So, we'll send them a DHCPNACK and they'll come right
268             # back and be offered the new address.  The code above won't be
269             # able to add duplicate routes, won't insert a duplicate DNAT,
270             # and won't ARP again because the routes will exist, so this won't
271             # incur much extra work.
272             if request != map(int, n.ip.split('.')):
273                 return False
274             return True
275         else:
276             s.syslog(s.LOG_INFO,"Requested ip "+str(request)+" not available for "+str(chaddr))
277         return False
278
279     def Decline(self, packet):
280         pass
281     def Release(self, packet):
282         pass
283     
284
285 class DhcpServer(pydhcplib.dhcp_network.DhcpServer):
286     def __init__(self, backend, options = {'client_listenport':68,'server_listenport':67}):
287         pydhcplib.dhcp_network.DhcpServer.__init__(self,"0.0.0.0",options["client_listen_port"],options["server_listen_port"],)
288         self.backend = backend
289         s.syslog(s.LOG_DEBUG, "__init__ DhcpServer")
290
291     def SendDhcpPacketTo(self, To, packet):
292         intf = self.backend.find_interface(packet)
293         if intf:
294             self.dhcp_socket.setsockopt(socket.SOL_SOCKET, IN.SO_BINDTODEVICE, intf)
295             ret = self.dhcp_socket.sendto(packet.EncodePacket(), (To,self.emit_port))
296             self.dhcp_socket.setsockopt(socket.SOL_SOCKET, IN.SO_BINDTODEVICE, '')
297             return ret
298         else:
299             return self.dhcp_socket.sendto(packet.EncodePacket(),(To,self.emit_port))
300
301     def SendPacket(self, packet):
302         """Encode and send the packet."""
303         
304         giaddr = packet.GetOption('giaddr')
305
306         # in all case, if giaddr is set, send packet to relay_agent
307         # network address defines by giaddr
308         if giaddr!=[0,0,0,0] :
309             agent_ip = ".".join(map(str,giaddr))
310             self.SendDhcpPacketTo(agent_ip,packet)
311             s.syslog(s.LOG_DEBUG, "SendPacket to agent : "+agent_ip)
312
313         # FIXME: This shouldn't broadcast if it has an IP address to send
314         # it to instead. See RFC2131 part 4.1 for full details
315         else :
316             s.syslog(s.LOG_DEBUG, "No agent, broadcast packet.")
317             self.SendDhcpPacketTo("255.255.255.255",packet)
318             
319
320     def HandleDhcpDiscover(self, packet):
321         """Build and send DHCPOFFER packet in response to DHCPDISCOVER
322         packet."""
323
324         logmsg = "Get DHCPDISCOVER packet from " + hwmac(packet.GetHardwareAddress()).str()
325
326         s.syslog(s.LOG_INFO, logmsg)
327         offer = DhcpPacket()
328         offer.CreateDhcpOfferPacketFrom(packet)
329         
330         if self.backend.Discover(offer):
331             self.SendPacket(offer)
332         # FIXME : what if false ?
333
334
335     def HandleDhcpRequest(self, packet):
336         """Build and send DHCPACK or DHCPNACK packet in response to
337         DHCPREQUEST packet. 4 types of DHCPREQUEST exists."""
338
339         ip = packet.GetOption("request_ip_address")
340         sid = packet.GetOption("server_identifier")
341         ciaddr = packet.GetOption("ciaddr")
342         #packet.PrintHeaders()
343         #packet.PrintOptions()
344
345         if sid != [0,0,0,0] and ciaddr == [0,0,0,0] :
346             s.syslog(s.LOG_INFO, "Get DHCPREQUEST_SELECTING_STATE packet")
347
348         elif sid == [0,0,0,0] and ciaddr == [0,0,0,0] and ip :
349             s.syslog(s.LOG_INFO, "Get DHCPREQUEST_INITREBOOT_STATE packet")
350
351         elif sid == [0,0,0,0] and ciaddr != [0,0,0,0] and not ip :
352             s.syslog(s.LOG_INFO,"Get DHCPREQUEST_INITREBOOT_STATE packet")
353
354         else : s.syslog(s.LOG_INFO,"Get DHCPREQUEST_UNKNOWN_STATE packet : not implemented")
355
356         if self.backend.Request(packet):
357             packet.TransformToDhcpAckPacket()
358             self.SendPacket(packet)
359         elif self.backend.Discover(packet):
360             packet.TransformToDhcpNackPacket()
361             self.SendPacket(packet)
362         else:
363             pass # We aren't authoritative, so don't reply if we don't know them.
364
365     # FIXME: These are not yet implemented.
366     def HandleDhcpDecline(self, packet):
367         s.syslog(s.LOG_INFO, "Get DHCPDECLINE packet")
368         self.backend.Decline(packet)
369         
370     def HandleDhcpRelease(self, packet):
371         s.syslog(s.LOG_INFO,"Get DHCPRELEASE packet")
372         self.backend.Release(packet)
373         
374     def HandleDhcpInform(self, packet):
375         s.syslog(s.LOG_INFO, "Get DHCPINFORM packet")
376
377         if self.backend.Request(packet) :
378             packet.TransformToDhcpAckPacket()
379             # FIXME : Remove lease_time from options
380             self.SendPacket(packet)
381
382         # FIXME : what if false ?
383
384 class ArpspoofWorker(Thread):
385     def __init__(self, queue):
386         Thread.__init__(self)
387         self.queue = queue
388         self.iface = config.xen.iface
389
390     def run(self):
391         while True:
392             (ip, gw) = self.queue.get()
393             try:
394                 p = Popen(['timeout', '5', 'arpspoof', '-i', self.iface, '-t', gw, ip], stdout=PIPE, stderr=PIPE)
395                 (out, err) = p.communicate()
396                 if p.returncode != 124:
397                     s.syslog(s.LOG_ERR, "arpspoof returned %s for IP %s gateway %s" % (p.returncode, ip, gw))
398                 else:
399                     s.syslog(s.LOG_INFO, "aprspoof'd for IP %s gateway %s" % (ip, gw))
400                 sys.stderr.write(err)
401                 sys.stdout.write(out)
402             except Exception as e:
403                 s.syslog(s.LOG_ERR, "Could not run arpspoof for IP %s gateway %s: %s" % (ip, gw, e))
404             self.queue.task_done()
405
406 if '__main__' == __name__:
407     options = { "server_listen_port":67,
408                 "client_listen_port":68,
409                 "listen_address":"0.0.0.0"}
410
411     myip = socket.gethostbyname(socket.gethostname())
412     if not myip:
413         print "invirt-dhcpserver: cannot determine local IP address by looking up %s" % socket.gethostname()
414         sys.exit(1)
415     
416     dhcp_options['server_identifier'] = ipv4(myip).int()
417
418     queue = Queue()
419
420     backend = DhcpBackend(queue)
421     server = DhcpServer(backend, options)
422
423     for x in range(2):
424         worker = ArpspoofWorker(queue)
425         worker.daemon = True
426         worker.start()
427
428     while True : server.GetNextDhcpPacket()