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
11 from Queue import Queue
12 from threading import Thread
13 from subprocess import PIPE, Popen
14 import netifaces as ni
19 from invirt import database
20 from invirt.config import structs as config
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}
26 def __init__(self, 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:
39 ipstr = ''.join(reversed(['%02X' % i for i in ipv4(nic.ip.encode("utf-8")).list()]))
40 for line in open('/proc/net/route'):
43 s.syslog(s.LOG_DEBUG, "find_interface found "+str(nic.ip)+" on "+parts[0])
47 def getParameters(self, **extra):
48 all_options=dict(dhcp_options)
49 all_options.update(extra)
51 for parameter, value in all_options.iteritems():
54 option_type = DhcpOptionsTypes[DhcpOptions[parameter]]
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(",")
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...
69 options[parameter] = [digit>>24&0xFF,(digit>>16)&0xFF,(digit>>8)&0xFF,digit&0xFF]
70 elif option_type == "16-bits" :
72 options[parameter] = [(digit>>8)&0xFF,digit&0xFF]
74 elif option_type == "char" :
76 options[parameter] = [digit&0xFF]
78 elif option_type == "bool" :
79 if value=="False" or value=="false" or value==0 :
80 options[parameter] = [0]
81 else : options[parameter] = [1]
83 elif option_type == "string" :
84 options[parameter] = strlist(value).list()
86 elif option_type == "RFC3397" :
89 components = item.split('.')
90 item_fmt = "".join(chr(len(elt)) + elt for elt in components) + "\x00"
91 parsed_value += item_fmt
93 options[parameter] = strlist(parsed_value).list()
96 options[parameter] = strlist(value).list()
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:
105 ip = nic.ip.encode("utf-8")
106 if ip is None: #Deactivated?
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]
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]
124 s.syslog(s.LOG_DEBUG,"dhcp_backend : Discover result = "+str(ip))
125 packet_parameters = self.getParameters(**options)
127 # FIXME: Other offer parameters go here
128 packet_parameters["yiaddr"] = ip.list()
130 packet.SetMultipleOptions(packet_parameters)
133 def Request(self, packet):
134 s.syslog(s.LOG_DEBUG, "dhcp_backend : Request")
136 discover = self.Discover(packet)
138 chaddr = hwmac(packet.GetHardwareAddress())
139 request = packet.GetOption("request_ip_address")
141 request = packet.GetOption("ciaddr")
142 yiaddr = packet.GetOption("yiaddr")
145 s.syslog(s.LOG_INFO,"Unknown MAC address: "+str(chaddr))
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,
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)]:
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))
185 # iptables will let you add the same rule again and again;
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))
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':
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))
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))
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))
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('.')):
240 s.syslog(s.LOG_INFO,"Requested ip "+str(request)+" not available for "+str(chaddr))
243 def Decline(self, packet):
245 def Release(self, packet):
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")
255 def SendDhcpPacketTo(self, To, packet):
256 intf = self.backend.find_interface(packet)
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, '')
263 return self.dhcp_socket.sendto(packet.EncodePacket(),(To,self.emit_port))
265 def SendPacket(self, packet):
266 """Encode and send the packet."""
268 giaddr = packet.GetOption('giaddr')
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)
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
280 s.syslog(s.LOG_DEBUG, "No agent, broadcast packet.")
281 self.SendDhcpPacketTo("255.255.255.255",packet)
284 def HandleDhcpDiscover(self, packet):
285 """Build and send DHCPOFFER packet in response to DHCPDISCOVER
288 logmsg = "Get DHCPDISCOVER packet from " + hwmac(packet.GetHardwareAddress()).str()
290 s.syslog(s.LOG_INFO, logmsg)
292 offer.CreateDhcpOfferPacketFrom(packet)
294 if self.backend.Discover(offer):
295 self.SendPacket(offer)
296 # FIXME : what if false ?
299 def HandleDhcpRequest(self, packet):
300 """Build and send DHCPACK or DHCPNACK packet in response to
301 DHCPREQUEST packet. 4 types of DHCPREQUEST exists."""
303 ip = packet.GetOption("request_ip_address")
304 sid = packet.GetOption("server_identifier")
305 ciaddr = packet.GetOption("ciaddr")
306 #packet.PrintHeaders()
307 #packet.PrintOptions()
309 if sid != [0,0,0,0] and ciaddr == [0,0,0,0] :
310 s.syslog(s.LOG_INFO, "Get DHCPREQUEST_SELECTING_STATE packet")
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")
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")
318 else : s.syslog(s.LOG_INFO,"Get DHCPREQUEST_UNKNOWN_STATE packet : not implemented")
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)
327 pass # We aren't authoritative, so don't reply if we don't know them.
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)
334 def HandleDhcpRelease(self, packet):
335 s.syslog(s.LOG_INFO,"Get DHCPRELEASE packet")
336 self.backend.Release(packet)
338 def HandleDhcpInform(self, packet):
339 s.syslog(s.LOG_INFO, "Get DHCPINFORM packet")
341 if self.backend.Request(packet) :
342 packet.TransformToDhcpAckPacket()
343 # FIXME : Remove lease_time from options
344 self.SendPacket(packet)
346 # FIXME : what if false ?
348 class ArpspoofWorker(Thread):
349 def __init__(self, queue):
350 Thread.__init__(self)
352 self.iface = config.xen.iface
356 (ip, gw) = self.queue.get()
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))
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()
370 if '__main__' == __name__:
371 options = { "server_listen_port":67,
372 "client_listen_port":68,
373 "listen_address":"0.0.0.0"}
375 myip = socket.gethostbyname(socket.gethostname())
377 print "invirt-dhcpserver: cannot determine local IP address by looking up %s" % socket.gethostname()
380 dhcp_options['server_identifier'] = ipv4(myip).int()
384 backend = DhcpBackend(queue)
385 server = DhcpServer(backend, options)
388 worker = ArpspoofWorker(queue)
392 while True : server.GetNextDhcpPacket()