-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathtiny-dhcp-server.py
executable file
·226 lines (194 loc) · 7.48 KB
/
tiny-dhcp-server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
#!/usr/bin/env python
# Copyright (C) 2015-2018 by Yuri Victorovich. All rights reserved.
# This code is licensed under BSD license.
##
## tiny-dhcp-server
##
## This is the minimalistic DHCP server implementation for FreeBSD (has some OS-specific parts)
## It responds on one specified network interface, allocates the new IP address for each server
## and assigns itself as default gateway and DNS.
##
## NOTE FreeBSD doesn't allow multiple DHCP listening sockets, so only one
## instance of this program can run at a time, and it handles all
## needed interfaces. It probably can't be combined with another DHCP server.
##
import sys, os, getopt
import socket
import struct
import codecs
import string
import netifaces # from port net/py-netifaces
import tiny_utils as tu
# missing constants
socket_IP_RECVIF=20
##
## Command line arguments and usage
##
arg_daemonize=False
arg_log_file=None
arg_pid_file=None
arg_unprivileged=False
arg_unprivileged_ug=None
def usage():
print('%s -d {-l <log-file>} {-p <pid-file>} {-U usr:grp|-u} iface1 {iface2 {...}}' % (sys.argv[0]))
sys.exit(2)
try:
opts, args = getopt.getopt(sys.argv[1:], "dl:p:uU:",["daemonize","log=","pid=","unprivileged","unprivileged2="])
except getopt.GetoptError:
usage()
for opt,arg in opts:
if opt in ("-d", "--daemonize"):
arg_daemonize = True
elif opt in ("-l", "--log"):
arg_log_file = arg
elif opt in ("-p", "--pid"):
arg_pid_file = arg
elif opt in ("-u", "--unprivileged"):
arg_unprivileged = True
elif opt in ("-U", "--unprivileged2"):
arg_unprivileged = True
arg_unprivileged_ug = arg.split(':')
if len(args) < 1:
usage()
## HDCP/BOOTP format
BOOTREQUEST = 1
BOOTREPLY = 2
BOOTPFormat = '!4bIHH4s4s4s4s16s64s128s64s'
BOOTPFormatSize = struct.calcsize(BOOTPFormat)
DHCPFormat = '!4bIHH4s4s4s4s16s64s128s4s'
DHCPFormatSize = struct.calcsize(DHCPFormat)
(BOOTP_OP,BOOTP_HTYPE,BOOTP_HLEN,BOOTP_HOPS,BOOTP_XID,BOOTP_SECS,
BOOTP_FLAGS,BOOTP_CIADDR,BOOTP_YIADDR,BOOTP_SIADDR,BOOTP_GIADDR,
BOOTP_CHADDR,BOOTP_SNAME,BOOTP_FILE,BOOTP_VEND) = range(15)
DHCP_DISCOVER = 1
DHCP_OFFER = 2
DHCP_REQUEST = 3
#DHCP_DECLINE = 4
DHCP_ACK = 5
#DHCP_NAK = 6
#DHCP_RELEASE = 7
DHCP_INFORM = 8
#DHCP_RENEWING = 100
DHCP_IP_MASK = 1
DHCP_IP_GATEWAY = 3
DHCP_IP_DNS = 6
DHCP_LEASE_TIME = 51
DHCP_MSG = 53
DHCP_SERVER = 54
DHCP_END = 255
def logfile():
return arg_log_file if arg_log_file is not None else '/var/log/tiny-dhcp-server.log'
def log(s):
with open(logfile(), "a") as myfile:
myfile.write('%s %s\n' % (tu.tm_log(), s))
def log_discard(what):
with open(logfile(), "a") as myfile:
myfile.write('%s discarded %s\n' % (tu.tm_log(), what))
##
## MAIN
##
## permissions
if not os.geteuid()==0:
sys.exit("Only root can run tiny-dhcp-server")
## starting
tu.mk_file_rw(logfile())
log('starting')
## signals
tu.handle_signals(lambda msg: log(msg))
## initialize structure per iface
ifaces = {}
for iface in args:
ifaces[iface] = {}
## determine server IP and netmask
for iface,s in ifaces.items():
s['server'] = socket.inet_aton(netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr'])
s['server_broadcast'] = netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['broadcast']
s['netmask'] = socket.inet_aton(netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['netmask'])
s['ip_pool'] = int.from_bytes(s['server'], 'big')+1
s['mac_to_ip'] = {}
## create receving socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.IPPROTO_IP, socket_IP_RECVIF, 1)
sock.bind(('0.0.0.0', 67))
## daemonize, write pid file, lose privileges
tu.process_common_args(arg_daemonize, arg_pid_file, arg_unprivileged, arg_unprivileged_ug, logfile())
## read/reply loop
while True:
## read
(data, flags, ancillary, addr) = sock.recvmsg(1024, 256)
# parse out iface from flags
for flag in flags:
(x, fn, bb) = flag
if fn == socket_IP_RECVIF:
msg_iface="".join(map(chr, bb[8:8+bb[5]])) # kernel struct sockaddr_dl, fields nlen and data
if msg_iface in ifaces:
s = ifaces[msg_iface]
else:
log_discard('request from host %s on socket %s via an unknown interface %s' % (addr, sock.getsockname(), msg_iface))
continue # ignore other interfaces
log('==> got packet: addr %s on socket %s via interface %s' % (addr, sock.getsockname(), msg_iface))
if len(data) < DHCPFormatSize:
log_discard('too small request of size='+len(data)+' from host %s on socket %s via interface %s' % (addr, sock.getsockname(), msg_iface))
continue
if socket.inet_aton(addr[0]) == s['server']:
log_discard('invalid request from host %s on socket %s via interface %s' % (addr, sock.getsockname(), msg_iface))
continue
## parse
tail = data[DHCPFormatSize:]
buf = list(struct.unpack(DHCPFormat, data[:DHCPFormatSize]))
if buf[BOOTP_OP] != BOOTREQUEST:
log_discard('packet which is not a BOOTREQUEST (op=%d)' % (buf[BOOTP_OP]))
continue
## options
options = {}
while tail:
tag = tail[0]
if tag == 0:
continue
if tag == 0xff:
break
length = tail[1]
(value, ) = struct.unpack('!%ss' % length, tail[2:2+length])
tail = tail[2+length:]
options[tag] = value
## message type
dhcp_msg_type = options[53][0]
if dhcp_msg_type == DHCP_DISCOVER:
dhcp_reply = DHCP_OFFER
elif dhcp_msg_type == DHCP_REQUEST:
dhcp_reply = DHCP_ACK
elif dhcp_msg_type == DHCP_INFORM:
log_discard('discarding DHCP INFORM message from host %s on socket %s via interface %s' % (addr, sock.getsockname(), msg_iface))
continue # Windows sends INFORM requests to learn NetBIOS, Domain Server, Domain Name, etc, discard these
else:
log_discard('discarding unknown DHCP message type %d from host %s on socket %s via interface %s' % (dhcp_msg_type, addr, sock.getsockname(), msg_iface))
continue
## allocate IP
# buf[BOOTP_CIADDR] has client's previous address, but we go strictly by MAC address buf[BOOTP_CHADDR]
log('client says he has MAC = ' + codecs.getencoder('hex')(buf[BOOTP_CHADDR])[0].decode("utf-8"))
mac = codecs.getencoder('hex')(buf[BOOTP_CHADDR])[0].decode("utf-8")
if mac in s['mac_to_ip']:
buf[BOOTP_YIADDR] = s['mac_to_ip'][mac]
log('client already has IP '+ socket.inet_ntoa(buf[BOOTP_YIADDR]))
else:
buf[BOOTP_YIADDR] = struct.pack('>I', s['ip_pool'])
buf[BOOTP_SECS] = 0
buf[BOOTP_FLAGS] = 0
s['mac_to_ip'][mac] = buf[BOOTP_YIADDR]
s['ip_pool'] = s['ip_pool'] + 1
log('allocating IP '+ socket.inet_ntoa(buf[BOOTP_YIADDR]))
buf[BOOTP_SIADDR] = buf[BOOTP_GIADDR] = s['server']
## reply
buf[BOOTP_OP] = BOOTREPLY
pkt = struct.pack(DHCPFormat, *buf)
pkt += struct.pack('!BBB', DHCP_MSG, 1, dhcp_reply)
pkt += struct.pack('!BB4s', DHCP_SERVER, 4, s['server'])
pkt += struct.pack('!BB4s', DHCP_IP_MASK, 4, s['netmask'])
pkt += struct.pack('!BB4s', DHCP_IP_GATEWAY, 4, s['server'])
pkt += struct.pack('!BB4s', DHCP_IP_DNS, 4, s['server'])
pkt += struct.pack('!BBI', DHCP_LEASE_TIME, 4, int(365*3600))
pkt += struct.pack('!BB', DHCP_END, 0)
sock.sendto(pkt, (s['server_broadcast'], 68))
log('<== sent response')