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