-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathdhcppd.py
More file actions
executable file
·373 lines (329 loc) · 16.9 KB
/
dhcppd.py
File metadata and controls
executable file
·373 lines (329 loc) · 16.9 KB
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#!/usr/bin/env python
import sys
import syslog
import eossdk
import subprocess
import os
import socket
import threading
import json
import re
from datetime import datetime
prefix48Regex = re.compile("^([a-fA-F0-9]{1,4}:){1,3}:\\/48$")
dhclientTimeout = 60*5 # 5 minutes
class dhclient:
def __init__(self, workingDir, interface, callback):
if not os.path.isdir(workingDir):
syslog.syslog("DHCP-PD Agent: dhclient working directory does not exist")
raise ValueError()
sockFilePath = workingDir + '/sock'
self.sockFilePath = sockFilePath
try:
os.unlink(sockFilePath)
except OSError as e:
if os.path.exists(sockFilePath):
raise e
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.sock.bind(sockFilePath)
self.callback = callback
self.sockCommThread = threading.Thread(target=self.handleEvent)
self.sockCommThread.start()
scriptFilePath = workingDir + '/dhclient-script.py'
if not os.path.isfile(scriptFilePath):
syslog.syslog("DHCP-PD Agent: dhclient script file missing ({})".format(scriptFilePath))
raise ValueError()
self.pidFilePath = workingDir + '/dhclient.pid'
leaseFilePath = workingDir + '/dhclient.lease'
# -6 = ipv6, -P = prefix delegation, -nw = do not wait for ip acquired
self.args = ['-6', '-P', '-nw',
'-e', 'SOCK_FILE={}'.format(sockFilePath),
'-sf', scriptFilePath,
'-pf', self.pidFilePath,
'-lf', leaseFilePath, interface]
syslog.syslog("DHCP-PD Agent: dhclient socket created")
def start(self):
syslog.syslog("DHCP-PD Agent: start dhclient {}".format(' '.join(self.args)))
dhclientProcess = subprocess.Popen(['dhclient'] + self.args)
ret = dhclientProcess.wait()
if ret != 0:
syslog.syslog("DHCP-PD Agent: unable to start dhclient (return code = {})".format(ret))
def stop(self):
syslog.syslog("DHCP-PD Agent: stop dhclient")
if self.isAlive():
# release leases and stop dhclient
dhclientProcess = subprocess.Popen(['dhclient', '-r'] + self.args)
ret = dhclientProcess.wait()
if ret != 0:
syslog.syslog("DHCP-PD Agent: unable to release leases (return code = {})".format(ret))
def isAlive(self):
try:
with open(self.pidFilePath, 'r') as f:
pid = f.readline()
if not pid:
return False
os.kill(int(pid), 0)
except Exception:
return False
else:
return True
def handleEvent(self):
try:
while True:
data = self.sock.recv(4096)
event = json.loads(data)
self.callback(event)
if not data:
break
except Exception as e:
syslog.syslog("DHCP-PD Agent: dhclient event thread threw exception: {}".format(e))
self.stop()
class dhcppd(eossdk.AgentHandler, eossdk.TimeoutHandler, eossdk.IntfHandler):
def __init__(self, sdk, dhcpInterface, workingDir):
self.agentMgr = sdk.get_agent_mgr()
self.interfaceMgr = sdk.get_intf_mgr()
self.eapiMgr = sdk.get_eapi_mgr()
self.timeoutMgr = sdk.get_timeout_mgr()
self.tracer = eossdk.Tracer("DHCP-PD-Agent")
eossdk.AgentHandler.__init__(self, self.agentMgr)
eossdk.IntfHandler.__init__(self, self.interfaceMgr)
eossdk.TimeoutHandler.__init__(self, self.timeoutMgr)
self.dhcpInterface = dhcpInterface
self.workingDir = workingDir
self.raPrefixes = dict()
# for now we only support one prefix per soliciting interface
# since we do not allow to set client DUID or IAID
self.delegatedPrefix = None
self.lock = threading.RLock()
syslog.syslog("DHCP-PD Agent: constructed")
self.tracer.trace0("Python Agent constructed")
def on_initialized(self):
self.tracer.trace0("Initialized")
syslog.syslog("DHCP-PD Agent Initialized")
intf = eossdk.IntfId(self.dhcpInterface)
if not self.interfaceMgr.exists(intf):
self.tracer.trace0("Interface {} does not exist".format(self.dhcpInterface))
syslog.syslog("DHCP-PD Agent: Interface {} does not exist".format(self.dhcpInterface))
return
kernelInterface = self.interfaceMgr.kernel_intf_name(intf)
if not kernelInterface:
self.tracer.trace0("Interface {} does not have a kernel interface".format(self.dhcpInterface))
syslog.syslog("DHCP-PD Agent: Interface {} does not have a kernel interfac".format(self.dhcpInterface))
return
self.dhclient = dhclient(self.workingDir, kernelInterface, self.on_dhclient_event)
self.dhclient.start()
for optionName in self.agentMgr.agent_option_iter():
self.on_agent_option(optionName, self.agentMgr.agent_option(optionName))
self.timeout_time_is(eossdk.now())
def on_timeout(self):
if not self.dhclient.isAlive():
self.tracer.trace0("Dhclient is not alive. Restarting.")
syslog.syslog("DHCP-PD Agent dhclient is not alive. Restarting.")
self.dhclient.start()
self.timeout_time_is(eossdk.now() + dhclientTimeout)
@staticmethod
def unixTimestampToString(unixTimestamp):
return datetime.utcfromtimestamp(unixTimestamp).strftime('%Y-%m-%d %H:%M:%S')
def on_dhclient_event(self, event):
self.tracer.trace0("Dhclient event {}".format(event))
reason = event.get('reason')
if 'new_dhcp6_server_id' in event:
self.agentMgr.status_set('(A) DUID Server:', str(event['new_dhcp6_server_id']))
if 'new_ip6_prefix' in event:
delegatedPrefixStr = str(event['new_ip6_prefix'])
newDelegatedPrefix = dhcppd.parseDelegatedPrefix48(delegatedPrefixStr)
if newDelegatedPrefix is None:
self.tracer.trace1("Dhclient got invalid prefix {}".format(delegatedPrefixStr))
self.agentMgr.status_set('(B) Delegated Prefix:', 'invalid prefix {}'.format(delegatedPrefixStr))
else:
self.agentMgr.status_set('(B) Delegated Prefix:', delegatedPrefixStr)
if 'new_life_starts' in event and 'new_preferred_life' in event and 'new_max_life' in event:
lifeStarts = int(event['new_life_starts'])
self.agentMgr.status_set('(C) Lifetime Starts:', dhcppd.unixTimestampToString(lifeStarts))
lifePreferred = int(event['new_preferred_life'])
self.agentMgr.status_set('(D) Lifetime Preferred [s]:', str(lifePreferred))
self.agentMgr.status_set('(E) Lifetime Preferred Ends:', dhcppd.unixTimestampToString(lifeStarts + lifePreferred))
lifeValid = int(event['new_max_life'])
self.agentMgr.status_set('(F) Lifetime Valid [s]:', str(lifeValid))
self.agentMgr.status_set('(G) Lifetime Valid Ends:', dhcppd.unixTimestampToString(lifeStarts + lifeValid))
if 'new_dhcp6_client_id' in event:
self.agentMgr.status_set('(H) DUID Client:', str(event['new_dhcp6_client_id']))
if 'new_iaid' in event:
self.agentMgr.status_set('(I) IAID:', str(event['new_iaid']))
if 'new_starts' in event:
iaStarts = int(event['new_starts'])
self.agentMgr.status_set('(J) IA Starts:', dhcppd.unixTimestampToString(iaStarts))
# RFC3633: Recommended values for T1 and T2 are .5 and .8 times the shortest preferred lifetime of the prefix
if 'new_renew' in event: # IA T1
# The time at which the requesting router should
# contact the delegating router from which the
# prefixes in the IA_PD were obtained to extend the
# lifetimes of the prefixes delegated to the IA_PD
iaT1 = int(event['new_renew'])
self.agentMgr.status_set('(K) IA T1 [s]:', str(iaT1))
if iaT1 != 0:
self.agentMgr.status_set('(K1) IA T1 Ends:', dhcppd.unixTimestampToString(iaStarts + iaT1))
else:
self.agentMgr.status_del('(K1) IA T1 Ends:')
if 'new_rebind' in event: # IA T2
# The time at which the requesting router should
# contact any available delegating router to extend
# the lifetimes of the prefixes assigned to the IA_PD
iaT2 = int(event['new_rebind'])
self.agentMgr.status_set('(L) IA T2 [s]:', str(iaT2))
if iaT2 != 0:
self.agentMgr.status_set('(L1) IA T2 Ends:', dhcppd.unixTimestampToString(iaStarts + iaT2))
else:
self.agentMgr.status_del('(L1) IA T2 Ends:')
if reason == 'PREINIT6':
pass # nothing to do
elif reason in ['BOUND6', 'RENEW6', 'REBIND6']:
# BOUND6 = We received a DHCPv6 reply with a prefix
# RENEW6 = The lease was renewed (same prefix)
# REBIND6 = Bound to a new DHCP server
if newDelegatedPrefix is not None:
with self.lock:
if self.delegatedPrefix != newDelegatedPrefix:
if self.delegatedPrefix is not None:
self.removeAllPrefixRAs()
self.delegatedPrefix = newDelegatedPrefix
self.addPrefixRAsToAll()
elif reason == 'DEPREF6':
# if prefered lifetime on our lease is up we get a deprefer message.
# Since we only support one prefix there is nothing to do
pass
elif reason in ['EXPIRE6', 'RELEASE6', 'STOP6']:
# EXPIRE6 = The lease expires and we failed to obtain a new one
# RELEASE6 = The client relinquishes the prefix (dhclient -r)
# STOP6 = Stopping DHCP client with (dhclient -x)
with self.lock:
self.removeAllPrefixRAs()
self.delegatedPrefix = None
else:
self.tracer.trace1("Dhclient event {} unknown".format(reason))
# Should be called with lock held
def removeAllPrefixRAs(self):
for interface, (slaId, _) in self.raPrefixes.items():
self.removePrefixRA(interface, slaId)
def addPrefixRAsToAll(self):
for interface, (slaId, options) in self.raPrefixes.items():
self.addPrefixRA(interface, slaId, options)
# Should be called with lock held
def addPrefixRA(self, interface, slaId, options):
if options is None:
ndRaCommand = 'ipv6 nd prefix {}'.format(dhcppd.prefix48to64(self.delegatedPrefix, slaId))
else:
ndRaCommand = 'ipv6 nd prefix {} {}'.format(dhcppd.prefix48to64(self.delegatedPrefix, slaId), options)
self.raPrefixes[interface] = (slaId, options)
interfaceCommand = 'interface {}'.format(interface)
result = self.eapiMgr.run_config_cmds([interfaceCommand, ndRaCommand])
if result.success():
self.tracer.trace5("Successfully configured RA prefix interface {} ({})".format(interface, ndRaCommand))
syslog.syslog("DHCP-PD Agent: Successfully configured RA prefix interface {} ({})".format(interface, ndRaCommand))
else:
self.tracer.trace1("Error configuring RA prefix interface {} ({}): {}".format(interface, ndRaCommand, result.error_message()))
syslog.syslog("DHCP-PD Agent: Error configuring RA prefix interface {} ({}): {}".format(interface, ndRaCommand, result.error_message()))
# Should be called with lock held
def removePrefixRA(self, interface, slaId):
del self.raPrefixes[interface]
ndRaCommand = 'no ipv6 nd prefix {}'.format(dhcppd.prefix48to64(self.delegatedPrefix, slaId))
interfaceCommand = 'interface {}'.format(interface)
result = self.eapiMgr.run_config_cmds([interfaceCommand, ndRaCommand])
if result.success():
self.tracer.trace5("Successfully removed prefix from {} ({})".format(interface, ndRaCommand))
syslog.syslog("DHCP-PD Agent: Successfully removed prefix from interface {} ({})".format(interface, ndRaCommand))
else:
self.tracer.trace1("Error removing prefix from {} ({}): {}".format(interface, ndRaCommand, result.error_message()))
syslog.syslog("DHCP-PD Agent: Error removing prefix from {} ({}): {}".format(interface, ndRaCommand, result.error_message()))
@staticmethod
def parseRaPrefixOption(value):
# format: <slaId 16 bit hex> <options> => see "ipv6 nd prefix" command for available options
splitIndex = value.find(' ')
if splitIndex == -1:
slaId = value
return (slaId, None)
else:
slaId = value[:splitIndex]
options = value[splitIndex + 1:]
return (slaId, options)
@staticmethod
def parseDelegatedPrefix48(prefix):
# We only support /48 delegated prefixes for now
if not prefix48Regex.match(prefix):
return None
doubleColonIndex = prefix.find('::')
prefixBase = prefix[:doubleColonIndex]
groups = prefixBase.count(':') + 1
# append :0 to make it easy to add SLA ID later
for _ in range(groups, 3):
prefixBase.append(':0')
return prefixBase
@staticmethod
def prefix48to64(prefix48, slaId):
return prefix48 + ':' + slaId + '::/64'
# all options are interpreted as RA interfaces
def on_agent_option(self, interface, value):
if not value:
with self.lock:
self.removePrefixRA(interface, self.raPrefixes[interface])
self.tracer.trace3("RA prefix interface {} deleted".format(interface))
else:
try:
interfaceId = eossdk.IntfId(interface)
except Exception as e:
syslog.syslog("DHCP-PD Agent: option invalid: {}. Ignoring.".format(str(e)))
self.tracer.trace1("option invalid: {}. Ignoring.".format(str(e)))
return
if not self.interfaceMgr.exists(interfaceId):
self.tracer.trace1("RA prefix interface {} does not exist. Ignoring.".format(interface))
syslog.syslog("DHCP-PD Agent: RA prefix interface {} does not exist. Ignoring.".format(interface))
return
slaId, options = dhcppd.parseRaPrefixOption(value)
try:
slaIdInt = int(slaId, 16)
if slaIdInt < 1 or slaIdInt > 0xFFFF:
raise ValueError()
except ValueError:
self.tracer.trace1("RA prefix interface {} invalid SLA_ID = {}. Expecting 16bit hex value. Ignoring.".format(interface, value))
return
if interface in self.raPrefixes:
slaIdOld, optionsOld = self.raPrefixes[interface]
if slaId != slaIdOld:
# SLA_ID changed
self.tracer.trace5("RA prefix interface {} remove old prefix with SLA_ID {}".format(interface, slaIdOld))
with self.lock:
self.removePrefixRA(interface, slaIdOld)
elif options == optionsOld:
# slaId and options are equal to old values => do nothing
return
else:
# options updated
self.tracer.trace5("RA prefix interface {} update options \"{}\" => \"{}\"".format(interface, optionsOld, options))
self.tracer.trace5("RA prefix interface {} add {} {}".format(interface, slaId, options))
with self.lock:
if self.delegatedPrefix is not None:
self.addPrefixRA(interface, slaId, options)
else:
self.raPrefixes[interface] = (slaId, options)
def on_agent_enabled(self, enabled):
if not enabled:
self.dhclient.stop()
# Stopping the dhclient will result in an RELEASE event
# however we might have already been stopped or the dhclient
# might not be running in the first place
# so remove everything while we still have a chance
with self.lock:
self.removeAllPrefixRAs()
self.delegatedPrefix = None
self.agentMgr.agent_shutdown_complete_is(True)
def main():
syslog.openlog(ident="DHCP-PD-AGENT", logoption=syslog.LOG_PID, facility=syslog.LOG_LOCAL0)
sdk = eossdk.Sdk()
if len(sys.argv) != 3:
syslog.syslog("DHCP-PD Agent invalid arguments: dhcppd.py <workingDir> <dhcpInterface>")
sys.exit(-1)
workingDir = sys.argv[1]
dhcpInterface = sys.argv[2]
_ = dhcppd(sdk, dhcpInterface, workingDir)
sdk.main_loop(sys.argv)
if __name__ == "__main__":
main()