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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
|
// Copyright (C) 2013-2021 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Note: the prefix "hooksdg" to all labels is an abbreviation for "Hooks
// Developer's Guide" and is used to prevent a clash with symbols in any
// other Doxygen file.
/**
@page hooksdgDevelopersGuide Hooks Developer's Guide
@section hooksdgIntroduction Introduction
Although the Kea framework and its DHCP programs
provide comprehensive functionality, there will be times when it does
not quite do what you require: the processing has to be extended in some
way to solve your problem.
Since the Kea source code is freely available (Kea being an
open-source project), one option is to modify it to do what
you want. Whilst perfectly feasible, there are drawbacks:
- Although well-documented, Kea is a large program. Just
understanding how it works will take a significant amount of time. In
addition, despite the fact that its object-oriented design keeps the
coupling between modules to a minimum, an inappropriate change to one
part of the program during the extension could cause another to
behave oddly or to stop working altogether.
- The change may need to be re-applied or re-written with every new
version of Kea. As new functionality is added or bugs are fixed,
the code or algorithms in the core software may change - and may change
significantly.
To overcome these problems, Kea provides the "Hooks" interface -
a defined interface for third-party or user-written code. (For ease of
reference in the rest of this document, all such code will be referred
to as "user code".) At specific points in its processing
("hook points") Kea will make a call to this code. The call passes
data that the user code can examine and, if required, modify.
Kea uses the modified data in the remainder of its processing.
In order to minimize the interaction between Kea and the user code,
the latter is built independently of Kea in the form of one or more
dynamic shared objects, called here (for historical reasons), shared
libraries. These are made known to Kea through its configuration
mechanism, and Kea loads the library at run time. Libraries can be
unloaded and reloaded as needed while Kea is running.
Use of a defined API and the Kea configuration mechanism means that
as new versions of Kea are released, there is no need to modify
the user code. Unless there is a major change in an interface
(which will be clearly documented), all that will be required is a rebuild
of the libraries.
@note Although the defined interface should not change, the internals
of some of the classes and structures referenced by the user code may
change between versions of Kea. These changes have to be reflected
in the compiled version of the software, hence the need for a rebuild.
@subsection hooksdgLanguages Languages
The core of Kea is written in C++. While it is the intention to
provide interfaces into user code written in other languages, the initial
versions of the Hooks system required that user code be written in C++.
It is no longer the case and there are examples of hooks written for
instance in Python but this guide does not document how to do that.
All examples in this guide are in C++.
@subsection hooksdgTerminology Terminology
In the remainder of this guide, the following terminology is used:
- Hook/Hook Point - used interchangeably, this is a point in the code at
which a call to user functions is made. Each hook has a name and
each hook can have any number (including 0) of user functions
attached to it.
- Callout - a user function called by the server at a hook
point. This is so-named because the server "calls out" to the library
to execute a user function.
- Framework function - the functions that a user library needs to
supply in order for the hooks framework to load and unload the library.
- User code/user library - non-Kea code that is compiled into a
shared library and loaded by Kea into its address space.
@section hooksdgTutorial Tutorial
To illustrate how to write code that integrates with Kea, we will
use the following (rather contrived) example:
<i>The Kea DHCPv4 server is used to allocate IPv4 addresses to clients
(as well as to pass them other information such as the address of DNS
servers). We will suppose that we need to classify clients requesting
IPv4 addresses according to their hardware address, and want to log both
the hardware address and allocated IP address for the clients of interest.</i>
The following sections describe how to implement these requirements.
The code presented here is not efficient and there are better ways of
doing the task. The aim however, is to illustrate the main features of
user hooks code, not to provide an optimal solution.
@subsection hooksdgFrameworkFunctions Framework Functions
Loading and initializing a library holding user code makes use
of three (user-supplied) functions:
- version - defines the version of Kea code with which the user-library
is built
- load - called when the library is loaded by the server.
- unload - called when the library is unloaded by the server.
- multi_threading_compatible - defines the compatibility (or not) of
the user-library and a multi-threaded DHCP service.
Of these, only "version" is mandatory, although in our example, all four
are used.
@subsubsection hooksdgVersionFunction The "version" Function
"version" is used by the hooks framework to check that the libraries
it is loading are compatible with the version of Kea being run.
Although the hooks system allows Kea and user code to interface
through a defined API, the relationship is somewhat tight in that the
user code will depend on the internal structures of Kea. If these
change - as they can between Kea releases - and Kea is run with
a version of user code built against an earlier version of Kea, a program
crash could result.
To guard against this, the "version" function must be provided in every
library. It returns a constant defined in header files of the version
of Kea against which it was built. The hooks framework checks this
for compatibility with the running version of Kea before loading
the library.
In this tutorial, we'll put "version" in its own file, version.cc. The
contents are:
@code
// version.cc
#include <hooks/hooks.h>
extern "C" {
int version() {
return (KEA_HOOKS_VERSION);
}
}
@endcode
The file "hooks/hooks.h" is specified relative to the Kea libraries
source directory - this is covered later in the section @ref hooksdgBuild.
It defines the symbol KEA_HOOKS_VERSION, which has a value that changes
on every release of Kea: this is the value that needs to be returned
to the hooks framework.
A final point to note is that the definition of "version" is enclosed
within 'extern "C"' braces. All functions accessed by the hooks
framework use C linkage, mainly to avoid the name mangling that
accompanies use of the C++ compiler, but also to avoid issues related
to namespaces.
@subsubsection hooksdgLoadUnloadFunctions The "load" and "unload" Functions
As the names suggest, "load" is called when a library is loaded and
"unload" called when it is unloaded. (It is always guaranteed that
"load" is called: "unload" may not be called in some circumstances,
e.g., if the system shuts down abnormally.) These functions are the
places where any library-wide resources are allocated and deallocated.
"load" is also the place where any callouts with non-standard names
(names that are not hook point names) can be registered:
this is covered further in the section @ref hooksdgCalloutRegistration.
The example does not make any use callouts with non-standard names. However,
as our design requires that the log file be open while Kea is active
and the library loaded, we'll open the file in the "load" function and close
it in "unload".
We create two files, one for the file handle declaration:
@code
// library_common.h
#ifndef LIBRARY_COMMON_H
#define LIBRARY_COMMON_H
#include <fstream>
// "Interesting clients" log file handle declaration.
extern std::fstream interesting;
#endif // LIBRARY_COMMON_H
@endcode
... and one to hold the "load" and "unload" functions:
@code
// load_unload.cc
#include <hooks/hooks.h>
#include "library_common.h"
using namespace isc::hooks;
// "Interesting clients" log file handle definition.
std::fstream interesting;
extern "C" {
int load(LibraryHandle&) {
interesting.open("/data/clients/interesting.log",
std::fstream::out | std::fstream::app);
return (interesting ? 0 : 1);
}
int unload() {
if (interesting) {
interesting.close();
}
return (0);
}
}
@endcode
Notes:
- The file handle ("interesting") is declared in a header file and defined
outside of any function. This means it can be accessed by any function
within the user library. For convenience, the definition is in the
load_unload.cc file.
- "load" is called with a LibraryHandle argument, this being used in
the registration of functions. As no functions are being registered
in this example, the argument specification omits the variable name
(whilst retaining the type) to avoid an "unused variable" compiler
warning. (The LibraryHandle and its use is discussed in the section
@ref hooksdgLibraryHandle.)
- In the initial version of the hooks framework, it was not possible to pass
any configuration information to the "load" function. The name of the log
file had therefore to be hard-coded as an absolute path name or communicated
to the user code by some other means.
- "load" must return 0 on success and non-zero on error. The hooks framework
will abandon the loading of the library if "load" returns an error status.
(In this example, "interesting" can be tested as a boolean value,
returning "true" if the file opened successfully.)
- "unload" closes the log file if it is open and is a no-op otherwise. As
with "load", a zero value must be returned on success and a non-zero value
on an error. The hooks framework will record a non-zero status return
as an error in the current Kea log but otherwise ignore it.
- As before, the function definitions are enclosed in 'extern "C"' braces.
In some cases to restrict the library loading to DHCP servers so it
cannot be loaded by the DDNS server or the Control Agent.
The best way to perform this is to check the process name returned
by @c isc::dhcp::Daemon::getProcName() static / class method declared
in process/daemon.h header against "kea-dhcp4" and "kea-dhcp6"
(other values are "kea-dhcp-ddns", "kea-ctrl-agent" and "kea-netconf").
If you'd like to check the address family too it is returned in DHCP servers
by isc::dhcp::CfgMgr::instance().getFamily() declared in dhcpsrv/cfgmgr.h
with AF_INET and AF_INET6 values.
@subsubsection hooksdgMultiThreadingCompatibleFunction The "multi_threading_compatible" function
"multi_threading_compatible" is used by the hooks framework to check
if the libraries it is loading are compatible with the DHCPv4 or DHCPv6
server multi-threading configuration. The value 0 means not compatible
and is the default when the function is not implemented. Non 0 values
mean compatible.
If your code implements it and returns the value 0 it is recommended
to document the reason so someone revisiting the code will not by
accident change the code.
To be compatible means:
- the code associated with DHCP packet processing callouts e.g.
pkt4_receive or pkt6_send must be thread safe so the multi-threaded
DHCP service can simultaneously call more than once one of these callouts.
- commands registered by a library are not required to be thread safe because
commands are executed by the main thread. Now it is a good idea to make
them thread safe and to document cases where they are not.
- when a library implements a thread safe backend API (e.g. host data
source) the service methods must be thread safe.
- a library which modifies the internal configuration of the server,
e.g. creates or deletes a subnet, it must enter a critical section using
the @c isc::util::MultiThreadingCriticalSection RAII class.
In the tutorial, we'll put "multi_threading_compatible" in its own file,
multi_threading_compatible.cc. The contents are:
@code
// multi_threading_compatible.cc
extern "C" {
int multi_threading_compatible() {
return (1);
}
}
@endcode
and for a command creating a new subnet:
@code
#include <util/multi_threading_mgr.h>
int commandHandler(CalloutHandle& handle) {
...
{
// Enter the critical section.
isc::util::MultiThreadingCriticalSection ct;
<add the subnet>
// Leave the critical section.
}
...
}
@endcode
@ref hooksMultiThreading provides more details about thread safety
requirements.
@subsection hooksdgCallouts Callouts
Having sorted out the framework, we now come to the functions that
actually do something. These functions are known as "callouts" because
the Kea code "calls out" to them. Each Kea server has a number of
hooks to which callouts can be attached: server-specific documentation
describes in detail the points in the server at which the hooks are
present together with the data passed to callouts attached to them.
Before we continue with the example, we'll discuss how arguments are
passed to callouts and information is returned to the server. We will
also discuss how information can be moved between callouts.
@subsubsection hooksdgCalloutSignature The Callout Signature
All callouts are declared with the signature:
@code
extern "C" {
int callout(CalloutHandle& handle);
}
@endcode
(As before, the callout is declared with "C" linkage.) Information is passed
between Kea and the callout through name/value pairs in the @c CalloutHandle
object. The object is also used to pass information between callouts on a
per-request basis. (Both of these concepts are explained below.)
A callout returns an @c int as a status return. A value of 0 indicates
success, anything else signifies an error. The status return has no
effect on server processing; the only difference between a success
and error code is that if the latter is returned, the server will
log an error, specifying both the library and hook that generated it.
Effectively the return status provides a quick way for a callout to log
error information to the Kea logging system.
@subsubsection hooksdgArguments Callout Arguments
The @c CalloutHandle object provides two methods to get and set the
arguments passed to the callout. These methods are called (naturally
enough) getArgument and setArgument. Their usage is illustrated by the
following code snippets.
@code
// Server-side code snippet to show the setting of arguments
int count = 10;
boost::shared_ptr<Pkt4> pktptr = ... // Set to appropriate value
// Assume that "handle" has been created
handle.setArgument("data_count", count);
handle.setArgument("inpacket", pktptr);
// Call the callouts attached to the hook
...
// Retrieve the modified values
handle.getArgument("data_count", count);
handle.getArgument("inpacket", pktptr);
@endcode
In the callout
@code
int number;
boost::shared_ptr<Pkt4> packet;
// Retrieve data set by the server.
handle.getArgument("data_count", number);
handle.getArgument("inpacket", packet);
// Modify "number"
number = ...;
// Update the arguments to send the value back to the server.
handle.setArgument("data_count", number);
@endcode
As can be seen @c getArgument is used to retrieve data from the
@c CalloutHandle, and @c setArgument used to put data into it. If a callout
wishes to alter data and pass it back to the server, it should retrieve
the data with @c getArgument, modify it, and call @c setArgument to send
it back.
There are several points to be aware of:
- the data type of the variable in the call to @c getArgument must match
the data type of the variable passed to the corresponding @c setArgument
<B>exactly</B>: using what would normally be considered to be a
"compatible" type is not enough. For example, if the server passed
an argument as an @c int and the callout attempted to retrieve it as a
@c long, an exception would be thrown even though any value that can
be stored in an @c int will fit into a @c long. This restriction also
applies the "const" attribute but only as applied to data pointed to by
pointers, e.g., if an argument is defined as a @c char*, an exception will
be thrown if an attempt is made to retrieve it into a variable of type
@c const @c char*. (However, if an argument is set as a @c const @c int,
it can be retrieved into an @c int.) The documentation of each hook
point will detail the data type of each argument.
- Although all arguments can be modified, some altered values may not
be read by the server. (These would be ones that the server considers
"read-only".) Consult the documentation of each hook to see whether an
argument can be used to transfer data back to the server.
- If a pointer to an object is passed to a callout (either a "raw"
pointer, or a boost smart pointer (as in the example above), and the
underlying object is altered through that pointer, the change will be
reflected in the server even if no call is made to setArgument.
In all cases, consult the documentation for the particular hook to see whether
parameters can be modified. As a general rule:
- Do not alter arguments unless you mean the change to be reflected in
the server.
- If you alter an argument, call @c CalloutHandle::setArgument to update the
value in the @c CalloutHandle object.
@subsubsection hooksdgNextStep The Next step status
Note: This functionality used to be provided in Kea 0.9.2 and earlier using
boolean skip flag. See @ref hooksdgSkipFlag for explanation and tips how
to migrate your hooks code to this new API.
When a to callouts attached to a hook returns, the server will usually continue
its processing. However, a callout might have done something that means that
the server should follow another path. Possible actions a server could take
include:
- Continue as usual. This is the default value. Unless callouts explicitly
change the status, the server will continue processing. There is no need
to set the status, unless one callout wants to override the status set
by another callout. This action is represented by CalloutHandle::NEXT_STEP_CONTINUE.
- Skip the next stage of processing because the callout has already
done it. For example, a hook is located just before the DHCP server
allocates an address to the client. A callout may decide to allocate
special addresses for certain clients, in which case it needs to tell
the server not to allocate an address in this case. This action is
hook specific and is represented by CalloutHandle::NEXT_STEP_SKIP.
- Drop the packet and continue with the next request. A possible scenario
is a server where a callout inspects the hardware address of the client
sending the packet and compares it against a black list; if the address
is on it, the callout notifies the server to drop the packet. This
action is represented by CalloutHandle::NEXT_STEP_DROP.
To handle these common cases, the @c CalloutHandle has a setStatus method.
This is set by a callout when it wishes the server to change the normal
processing. Exact meaning is hook specific. Please consult hook API
documentation for details. For historic reasons (Kea 0.9.2 used a single
boolean flag called skip that also doubled in some cases as an indicator
to drop the packet) several hooks use SKIP status to drop the packet.
The methods to get and set the "skip" or "drop" state are getStatus and
setStatus. Their usage is intuitive:
@code
// Get the current setting of the next step status.
auto status = handle.getStatus();
if (status == CalloutHandle::NEXT_STEP_DROP)
// Do something...
:
if (status == CalloutHandle::NEXT_STEP_SKIP)
// Do something...
:
// Do some processing...
:
if (lease_allocated) {
// Flag the server to skip the next step of the processing as we
// already have an address.
handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
}
return;
@endcode
Like arguments, the next step status is passed to all callouts on a hook. Callouts
later in the list are able to examine (and modify) the settings of earlier ones.
If using multiple libraries, when the library wants to drop the current packet,
the DROP status must be used instead of the SKIP status so that the packet
processing ends at that specific hook point.
It is recommended for all callouts to check the status before doing any
processing. As callouts can modify the status, it is recommended to take good
care when doing so, because this will have impact on all remaining hooks as well.
It is highly recommended to not reset the SKIP or DROP status to CONTINUE, even
though possible, so that the rest of the loaded hooks and the server can check
and perform the proper action.
Some hook points handle special functionality for the server, like pkt4_receive,
pkt6_receive, which handle unpacking of the received packet, pkt4_send, pkt6_send,
which handle packing of the response packet.
If the hook handles these actions and sets the next step flag to SKIP, it should
also perform a check for the SKIP flag before anything else. If it is already
set, do not pack/unpack the packet (other library, or even the same library, if
loaded multiple times, has done it already). Some libraries might also need to
throw exceptions in such cases because they need to perform specific actions before
pack/unpack (eg. addOption/delOption before pack action), which have no effect if
pack/unpack action is done previously by some other library.
@code
// Check if other library has already set SKIP flag and performed unpack
// so that unpack is skipped
if (handle.getStatus() != CalloutHandle::NEXT_STEP_SKIP) {
query->unpack();
}
@endcode
@code
// Check the status state.
auto status = handle.getStatus();
if (status == CalloutHandle::NEXT_STEP_SKIP) {
isc_throw(InvalidOperation, "packet pack already handled");
}
...
response->delOption(DEL_OPTION_CODE);
...
response->addOption(ADD_OPTION_CODE);
...
response->pack();
@endcode
As stated before, the order of loading libraries is critical in achieving the
desired behavior, so please read @ref hooksdgMultipleLibraries when configuring
multiple libraries.
@subsubsection hooksdgSkipFlag The "Skip" Flag (deprecated)
In releases 0.9.2 and earlier, the functionality currently offered by next step
status (see @ref hooksdgNextStep) was provided by
a boolean flag called "Skip". However, since it only allowed to either continue
or skip the next processing step and was not extensible to other decisions,
setSkip(bool) call was replaced with a setStatus(enum) in Kea 1.0. This
new approach is extensible. If we decide to add new results (e.g., WAIT
or RATELIMIT), we will be able to do so without changing the API again.
If you have your hooks libraries that take advantage of skip flag, migrating
to the next step status is very easy. See @ref hooksdgNextStep for detailed
explanation of the new status field.
To migrate, replace this old code:
@code
handle.setSkip(false); // This is the default.
handle.setSkip(true); // Tell the server to skip the next processing step.
bool skip = hangle.getSkip(); // Check the skip flag state.
if (skip) {
...
}
@endcode
with this:
@code
// This is the default.
handle.setStatus(CalloutHandle::NEXT_STEP_CONTINUE);
// Tell the server to skip the next processing step.
handle.setStatus(CalloutHandle::NEXT_STEP_SKIP);
// Check the status state.
auto status = handle.getStatus();
if (status == CalloutHandle::NEXT_STEP_SKIP) {
...
}
@endcode
@subsubsection hooksdgCalloutContext Per-Request Context
Although the Kea modules can be characterized as handling a single
packet at a time - e.g., the DHCPv4 server receives a DHCPDISCOVER packet,
processes it and responds with an DHCPOFFER, this may not always be true.
Future developments may have the server processing multiple packets
simultaneously, or to suspend processing on a packet and resume it at
a later time after other packets have been processed.
As well as argument information, the @c CalloutHandle object can be used by
callouts to attach information to a packet being handled by the server.
This information (known as "context") is not used by the server: its purpose
is to allow callouts to pass information between one another on a
per-packet basis.
Context associated with a packet only exists only for the duration of the
processing of that packet: when processing is completed, the context is
destroyed. A new packet starts with a new (empty) context. Context is
particularly useful in servers that may be processing multiple packets
simultaneously: callouts can effectively attach data to a packet that
follows the packet around the system.
Context information is held as name/value pairs in the same way
as arguments, being accessed by the pair of methods @c setContext and
@c getContext. They have the same restrictions as the @c setArgument and
@c getArgument methods - the type of data retrieved from context must
<B>exactly</B> match the type of the data set.
The example in the next section illustrates their use.
@subsection hooksdgExampleCallouts Example Callouts
Continuing with the tutorial, the requirements need us to retrieve the
hardware address of the incoming packet, classify it, and write it,
together with the assigned IP address, to a log file. Although we could
do this in one callout, for this example we'll use two:
- pkt4_receive - a callout on this hook is invoked when a packet has been
received and has been parsed. It is passed a single argument, "query4"
which is an isc::dhcp::Pkt4Ptr object, holding a pointer to the
isc::dhcp::Pkt4 object (representing a DHCPv4 packet). We will do the
classification here.
- pkt4_send - called when a response is just about to be sent back to
the client. It is passed a single argument "response4". This is the
point at which the example code will write the hardware and IP addresses
to the log file.
The standard for naming callouts is to give them the same name as
the hook. If this is done, the callouts will be automatically found
by the Hooks system (this is discussed further in section @ref
hooksdgCalloutRegistration). For our example, we will assume this is the
case, so the code for the first callout (used to classify the client's
hardware address) is:
@code
// pkt_receive4.cc
#include <hooks/hooks.h>
#include <dhcp/pkt4.h>
#include "library_common.h"
#include <string>
using namespace isc::dhcp;
using namespace isc::hooks;
using namespace std;
extern "C" {
// This callout is called at the "pkt4_receive" hook.
int pkt4_receive(CalloutHandle& handle) {
// A pointer to the packet is passed to the callout via a "boost" smart
// pointer. The include file "pkt4.h" typedefs a pointer to the Pkt4
// object as Pkt4Ptr. Retrieve a pointer to the object.
Pkt4Ptr query4_ptr;
handle.getArgument("query4", query4_ptr);
// Point to the hardware address.
HWAddrPtr hwaddr_ptr = query4_ptr->getHWAddr();
// The hardware address is held in a public member variable. We'll classify
// it as interesting if the sum of all the bytes in it is divisible by 4.
// (This is a contrived example after all!)
long sum = 0;
for (int i = 0; i < hwaddr_ptr->hwaddr_.size(); ++i) {
sum += hwaddr_ptr->hwaddr_[i];
}
// Classify it.
if (sum % 4 == 0) {
// Store the text form of the hardware address in the context to pass
// to the next callout.
string hwaddr = hwaddr_ptr->toText();
handle.setContext("hwaddr", hwaddr);
}
return (0);
};
}
@endcode
The "pkt4_receive" callout placed the hardware address of an interesting client in
the "hwaddr" context for the packet. Turning now to the callout that will
write this information to the log file:
@code
// pkt4_send.cc
#include <hooks/hooks.h>
#include <dhcp/pkt4.h>
#include "library_common.h"
#include <string>
using namespace isc::dhcp;
using namespace isc::hooks;
using namespace std;
extern "C" {
// This callout is called at the "pkt4_send" hook.
int pkt4_send(CalloutHandle& handle) {
// Obtain the hardware address of the "interesting" client. We have to
// use a try...catch block here because if the client was not interesting,
// no information would be set and getArgument would thrown an exception.
string hwaddr;
try {
handle.getContext("hwaddr", hwaddr);
// getContext didn't throw so the client is interesting. Get a pointer
// to the reply.
Pkt4Ptr response4_ptr;
handle.getArgument("response4", response4_ptr);
// Get the string form of the IP address.
string ipaddr = response4_ptr->getYiaddr().toText();
// Write the information to the log file.
interesting << hwaddr << " " << ipaddr << "\n";
// ... and to guard against a crash, we'll flush the output stream.
flush(interesting);
} catch (const NoSuchCalloutContext&) {
// No such element in the per-request context with the name "hwaddr".
// This means that the request was not an interesting, so do nothing
// and dismiss the exception.
}
return (0);
}
}
@endcode
@subsection hooksdgLogging Logging in the Hooks Library
Hooks libraries take part in the DHCP message processing. They also often
modify the server's behavior by taking responsibility for processing
the DHCP message at certain stages and instructing the server to skip
the default processing for that stage. Thus, hooks libraries play an
important role in the DHCP server operation and, depending on their
purpose, they may have high complexity, which increases likelihood of the
defects in the libraries.
All hooks libraries should use Kea logging system to facilitate diagnostics
of the defects in the libraries and issues with the DHCP server's operation.
Even if the issue doesn't originate in the hooks library itself, the use
of the library may uncover issues in the Kea code that only
manifest themselves in some special circumstances.
Hooks libraries use the Kea logging system in the same way as any other
standard Kea library. A hooks library should have at least one logger
defined, but may have multiple loggers if it is desired
to separate log messages from different functional parts of the library.
Assuming that it has been decided to use logging in the hooks library, the
implementor must select a unique name for the logger. Ideally the name
should have some relationship with the name of the library so that it is
easy to distinguish messages logged from this library. For example,
if the hooks library is used to capture incoming and outgoing DHCP
messages, and the name of the library is "libkea-packet-capture",
a suitable logger name could be "packet-capture".
In order to use a logger within the library, the logger should be declared
in a header file, which must be included in all files using
the logger:
@code
#ifndef PACKET_CAPTURE_LOG_H
#define PACKET_CAPTURE_LOG_H
#include <log/message_initializer.h>
#include <log/macros.h>
#include <user_chk_messages.h>
namespace packet_capture {
extern isc::log::Logger packet_capture_logger;
}
#endif
@endcode
The logger should be defined and initialized in the implementation file,
as illustrated below:
@code
#include <packet_capture_log.h>
namespace packet_capture {
isc::log::Logger packet_capture_logger("packet-capture");
}
@endcode
These files may contain multiple logger declarations and initializations
when the use of more than one logger is desired.
The next step is to add the appropriate message file as described in the
@ref logMessageFiles.
The implementor must make sure that log messages appear in the right
places and that they are logged at the appropriate level. The choice
of the place where the message should appear is not always obvious:
it depends if the particular function being called already logs enough
information and whether adding log message before and/or after the
call to this function would simply duplicate some messages. Sometimes
the choice whether the log message should appear within the function or
outside of it depends on the level of details available for logging. For
example, in many cases it is desirable to include the client identifier
or transaction id of the DHCP packet being processed in logging message.
If this information is available at the higher level but not in the
function being called, it is often better to place the log message at
higher level. However, the function parameters list could be extended
to include the additional information, and to be logged and the logging
call made from within the function.
Ideally, the hooks library should contain debug log messages (traces)
in all significant decision points in the code, with the information as to
how the code hit this decision point, how it will proceed and why.
However, care should be taken when selecting the log level for those
messages, because selecting too high logging level may impact the
performance of the system. For this reason, traces (messages of
the debug severity) should use different debug levels for the
messages of different importance or having different performance
requirements to generate the log message. For example, generation of
a log message, which prints full details of a packet, usually requires
more CPU bandwidth than the generation of the message which only prints
the packet type and length. Thus, the former should be logged at
lower debug level (see @ref logSeverity for details of using
various debug levels using "dbglevel" parameter).
All loggers defined within the hooks libraries derive the default
configuration from the root logger. For example, when the hooks
library is attached to the DHCPv4 server, the root logger name is
"kea-dhcp4", and the library by default uses configuration of this
logger. The configuration of the library's logger can
be modified by adding a configuration entry for it
to the configuration file. In case of the "packet-capture"
logger declared above, the full name of the logger in the
configuration file will be "kea-dhcp4.packet-capture". The
configuration specified for this logger will override the default
configuration derived from the root logger.
@subsection hooksdgBuild Building the Library
Building the code requires building a sharable library. This requires
the the code be compiled as position-independent code (using the
compiler's "-fpic" switch) and linked as a shared library (with the
linker's "-shared" switch). The build command also needs to point to
the Kea include directory and link in the appropriate libraries.
Assuming that Kea has been installed in the default location, the
command line needed to create the library using the Gnu C++ compiler on a
Linux system is:
@code
g++ -I <install-dir>/include/kea -L <install-dir>/lib -fpic -shared -o example.so \
load_unload.cc pkt4_receive.cc pkt4_send.cc version.cc \
-lkea-dhcpsrv -lkea-dhcp++ -lkea-hooks -lkea-log -lkea-util -lkea-exceptions
@endcode
Notes:
- Replace "<install-dir>" with the location in which you installed Kea. Unless
you specified the "--prefix" switch on the "configure" command line when
building Kea, it will be installed in the default location, usually /usr/local.
- The compilation command and switches required may vary depending on
your operating system and compiler - consult the relevant documentation
for details.
- The list of libraries that need to be included in the command line
depends on the functionality used by the hook code and the module to
which they are attached. Depending on operating system, you may also need
to explicitly list libraries on which the Kea libraries you link against depend.
@subsection hooksdgConfiguration Configuring the Hooks Library
The final step is to make the library known to Kea. The configuration
keywords of all Kea modules to which hooks can be added contain the
"hooks-libraries" element and user libraries are added to this. (The Kea
hooks system can handle multiple libraries - this is discussed below.)
To add the example library (assumed to be in /usr/local/lib) to the
DHCPv4 module, it must be listed in the "hooks-libraries" element of the
"Dhcp4" part of the configuration file:
@code
"Dhcp4": {
:
"hooks-libraries": [
{
"library": "/usr/local/lib/example.so"
}
]
:
}
@endcode
(Note that "hooks" is plural.)
Each entry in the "hooks-libraries" list is a structure (a "map" in JSON
parlance) that holds the following element:
- library - the name of the library to load. This must be a string.
@note The syntax of the hooks-libraries configuration element has changed
since kea 0.9.2 (in that version, "hooks-libraries" was just a list of
libraries). This change is in preparation for the introduction of
library-specific parameters, which will be added to Kea in a version after 1.0.
The DHCPv4 server will load the library and execute the callouts each time a
request is received.
@note All the above assumes that the hooks library will be used with a
version of Kea that is dynamically-linked. For information regarding
running hooks libraries against a statically-linked Kea, see @ref
hooksdgStaticallyLinkedKea.
@section hooksdgAdvancedTopics Advanced Topics
@subsection hooksdgContextCreateDestroy Context Creation and Destruction
As well as the hooks defined by the server, the hooks framework defines
two hooks of its own, "context_create" and "context_destroy". The first
is called when a request is created in the server, before any of the
server-specific hooks gets called. It's purpose it to allow a library
to initialize per-request context. The second is called after all
server-defined hooks have been processed, and is to allow a library to
tidy up.
As an example, the "pkt4_send" example above required that the code
check for an exception being thrown when accessing the "hwaddr" context
item in case it was not set. An alternative strategy would have been to
provide a callout for the "context_create" hook and set the context item
"hwaddr" to an empty string. Instead of needing to handle an exception,
"pkt4_send" would be guaranteed to get something when looking for
the hwaddr item and so could write or not write the output depending on
the value.
In most cases, "context_destroy" is not needed as the Hooks system
automatically deletes context. An example where it could be required
is where memory has been allocated by a callout during the processing
of a request and a raw pointer to it stored in the context object. On
destruction of the context, that memory will not be automatically
released. Freeing in the memory in the "context_destroy" callout will solve
that problem.
Actually, when the context is destroyed, the destructor
associated with any objects stored in it are run. Rather than point to
allocated memory with a raw pointer, a better idea would be to point to
it with a boost "smart" pointer and store that pointer in the context.
When the context is destroyed, the smart pointer's destructor is run,
which will automatically delete the pointed-to object.
These approaches are illustrated in the following examples.
Here it is assumed that the hooks library is performing some form of
security checking on the packet and needs to maintain information in
a user-specified "SecurityInformation" object. (The details of this
fictitious object are of no concern here.) The object is created in
the "context_create" callout and used in both the "pkt4_receive" and the
"pkt4_send" callouts.
@code
// Storing information in a "raw" pointer. Assume that the
#include <hooks/hooks.h>
:
extern "C" {
// context_create callout - called when the request is created.
int context_create(CalloutHandle& handle) {
// Create the security information and store it in the context
// for this packet.
SecurityInformation* si = new SecurityInformation();
handle.setContext("security_information", si);
}
// Callouts that use the context
int pkt4_receive(CalloutHandle& handle) {
// Retrieve the pointer to the SecurityInformation object
SecurityInformation* si;
handle.getContext("security_information", si);
:
:
// Set the security information
si->setSomething(...);
// The pointed-to information has been updated but the pointer has not been
// altered, so there is no need to call setContext() again.
}
int pkt4_send(CalloutHandle& handle) {
// Retrieve the pointer to the SecurityInformation object
SecurityInformation* si;
handle.getContext("security_information", si);
:
:
// Retrieve security information
bool active = si->getSomething(...);
:
}
// Context destruction. We need to delete the pointed-to SecurityInformation
// object because we will lose the pointer to it when the @c CalloutHandle is
// destroyed.
int context_destroy(CalloutHandle& handle) {
// Retrieve the pointer to the SecurityInformation object
SecurityInformation* si;
handle.getContext("security_information", si);
// Delete the pointed-to memory.
delete si;
}
@endcode
The requirement for the "context_destroy" callout can be eliminated if
a Boost shared ptr is used to point to the allocated memory:
@code
// Storing information in a "raw" pointer. Assume that the
#include <hooks/hooks.h>
#include <boost/shared_ptr.hpp>
:
extern "C" {
// context_create callout - called when the request is created.
int context_create(CalloutHandle& handle) {
// Create the security information and store it in the context for this
// packet.
boost::shared_ptr<SecurityInformation> si(new SecurityInformation());
handle.setContext("security_information", si);
}
// Other than the data type, a shared pointer has similar semantics to a "raw"
// pointer. Only the code from "pkt4_receive" is shown here.
int pkt4_receive(CalloutHandle& handle) {
// Retrieve the pointer to the SecurityInformation object
boost::shared_ptr<SecurityInformation> si;
handle.setContext("security_information", si);
:
:
// Modify the security information
si->setSomething(...);
// The pointed-to information has been updated but the pointer has not
// altered, so there is no need to reset the context.
}
// No context_destroy callout is needed to delete the allocated
// SecurityInformation object. When the @c CalloutHandle is destroyed, the shared
// pointer object will be destroyed. If that is the last shared pointer to the
// allocated memory, then it too will be deleted.
@endcode
(Note that a Boost shared pointer - rather than any other Boost smart pointer -
should be used, as the pointer objects are copied within the hooks framework and
only shared pointers have the correct behavior for the copy operation.)
@subsection hooksdgCalloutRegistration Registering Callouts
As briefly mentioned in @ref hooksdgExampleCallouts, the standard is for
callouts in the user library to have the same name as the name of the
hook to which they are being attached. This convention was followed
in the tutorial, e.g., the callout that needed to be attached to the
"pkt4_receive" hook was named pkt4_receive.
The reason for this convention is that when the library is loaded, the
hook framework automatically searches the library for functions with
the same names as the server hooks. When it finds one, it attaches it
to the appropriate hook point. This simplifies the loading process and
bookkeeping required to create a library of callouts.
However, the hooks system is flexible in this area: callouts can have
non-standard names, and multiple callouts can be registered on a hook.
@subsubsection hooksdgLibraryHandle The LibraryHandle Object
The way into the part of the hooks framework that allows callout
registration is through the LibraryHandle object. This was briefly
introduced in the discussion of the framework functions, in that
an object of this type is pass to the "load" function. A LibraryHandle
can also be obtained from within a callout by calling the CalloutHandle's
@c getLibraryHandle() method.
The LibraryHandle provides three methods to manipulate callouts:
- @c registerCallout - register a callout on a hook.
- @c deregisterCallout - deregister a callout from a hook.
- @c deregisterAllCallouts - deregister all callouts on a hook.
The following sections cover some of the ways in which these can be used.
@subsubsection hooksdgNonstandardCalloutNames Non-Standard Callout Names
The example in the tutorial used standard names for the callouts. As noted
above, it is possible to use non-standard names. Suppose, instead of the
callout names "pkt4_receive" and "pkt4_send", we had named our callouts
"classify" and "write_data". The hooks framework would not have registered
these callouts, so we would have needed to do it ourself. The place to
do this is the "load" framework function, and its code would have had to
been modified to:
@code
int load(LibraryHandle& libhandle) {
// Register the callouts on the hooks. We assume that a header file
// declares the "classify" and "write_data" functions.
libhandle.registerCallout("pkt4_receive", classify);
libhandle.registerCallout("pkt4_send", write_data);
// Open the log file
interesting.open("/data/clients/interesting.log",
std::fstream::out | std::fstream::app);
return (interesting ? 0 : 1);
}
@endcode
It is possible for a library to contain callouts with both standard and
non-standard names: ones with standard names will be registered automatically,
ones with non-standard names need to be registered manually.
@subsubsection hooksdgCommandHandlers Using Callouts as Command handlers
Kea servers natively support a set of control commands to retrieve and update
runtime information, e.g. server configuration, basic statistics etc. In
many cases, however, DHCP deployments require support for additional commands
or the natively supported commands don't exactly fulfill one's requirements.
Taking advantage of Kea's modularity and hooks framework, it is now possible
to easily extend the pool of supported commands by implementing additional
(non-standard) commands within hook libraries.
A hook library needs to register command handlers for control commands within
its @c load function as follows:
@code
int load(LibraryHandle& handle) {
handle.registerCommandCallout("diagnostics-enable", diagnostics_enable);
handle.registerCommandCallout("diagnostics-dump", diagnostics_dump);
return (0);
}
@endcode
Internally, the @c LibraryHandle associates command handlers @c diagnostics_enable
and @c diagnostics_dump with dedicated hook points. These hook points are
given names after the command names, i.e. "$diagnostics_enable" and
"$diagnostics_dump". The dollar sign before the hook point name indicates
that the hook point is dedicated for a command handler, i.e. is not one of
the standard hook points used by the Kea servers. This is just a naming convention,
usually invisible to the hook library implementation and is mainly aimed at
minimizing a risk of collision between names of the hook points registered with
command handlers and standard hook points.
Once the hook library is loaded and the command handlers supported by the
library are registered, the Kea servers will be able to recognize that those
specific commands are supported and will dispatch commands with the corresponding
names to the hook library (or multiple hook libraries) for processing. See the
documentation of the @ref isc::config::HookedCommandMgr for more details how
it uses @c HooksManager::commandHandlersPresent to determine if the received
command should be dispatched to a hook library for processing.
The @c diagnostics_enable and @c diagnostics_dump command
handlers must be implemented within the hook library in analogous way to
regular callouts:
@code
int diagnostics_enable(CalloutHandle& handle) {
ConstElementPtr response;
try {
ConstElementPtr command;
handle.getArgument("command", command);
ConstElementPtr args;
static_cast<void>(isc::config::parseCommand(args, command));
// ...
// handle command here.
// ...
response = createAnswer(CONTROL_RESULT_SUCCESS, "successful");
} catch (const std::exception& ex) {
response = createAnswer(CONTROL_RESULT_ERROR, ex.what());
}
handle.setArgument("response", response);
return (0);
}
@endcode
The sample code above retrieves the "command" argument which is always provided.
It represents the control command as sent by the controlling client. It includes
command name and command specific arguments. The generic @ref isc::config::parseCommand
can be used to retrieve arguments included in the command. The callout then interprets
these arguments, takes appropriate action and creates a response to the client.
Care should be taken to catch any non-fatal exceptions that may arise during the callout
that should be reported as a failure to the controlling client. In such case, the response
with @c CONTROL_RESULT_ERROR is returned and the callout should return the value of 0.
The non-zero result should only be returned by the callout in case of fatal errors, i.e.
errors which result in inability to generate a response to the client. If the response
is generated, the command handler must set it as "response" argument prior to return.
It is uncommon but valid scenario to have multiple hook libraries providing command
handlers for the same command. They are invoked sequentially and each of them
can freely modify a response set by a previous callout. This includes entirely
replacing the response provided by previous callouts, if necessary.
@subsubsection hooksdgMultipleCallouts Multiple Callouts on a Hook
The Kea hooks framework allows multiple callouts to be attached to
a hook point. Although it is likely to be rare for user code to need to
do this, there may be instances where it make sense.
To register multiple callouts on a hook, just call
@c LibraryHandle::registerCallout multiple times on the same hook, e.g.,
@code
libhandle.registerCallout("pkt4_receive", classify);
libhandle.registerCallout("pkt4_receive", write_data);
@endcode
The hooks framework will call the callouts in the order they are
registered. The same @c CalloutHandle is passed between them, so any
change made to the CalloutHandle's arguments, "skip" flag, or per-request
context by the first is visible to the second.
@subsection hooksdgMultipleLibraries Multiple User Libraries
As alluded to in the section @ref hooksdgConfiguration, Kea can load
multiple libraries. The libraries are loaded in the order specified in
the configuration, and the callouts attached to the hooks in the order
presented by the libraries.
The following picture illustrates this, and also illustrates the scope of
data passed around the system.
@image html DataScopeArgument.png "Scope of Arguments"
In this illustration, a server has three hook points, alpha, beta
and gamma. Two libraries are configured, library 1 and library 2.
Library 1 registers the callout "authorize" for hook alpha, "check" for
hook beta and "add_option" for hook gamma. Library 2 registers "logpkt",
"validate" and "putopt"
The horizontal red lines represent arguments to callouts. When the server
calls hook alpha, it creates an argument list and calls the
first callout for the hook, "authorize". When that callout returns, the
same (but possibly modified) argument list is passed to the next callout
in the chain, "logpkt". Another, separate argument list is created for
hook beta and passed to the callouts "check" and "validate" in
that order. A similar sequence occurs for hook gamma.
The next picture shows the scope of the context associated with a
request.
@image html DataScopeContext.png "Illustration of per-library context"
The vertical blue lines represent callout context. Context is
per-packet but also per-library. When the server calls "authorize",
the CalloutHandle's @c getContext and @c setContext methods access a context
created purely for library 1. The next callout on the hook will access
context created for library 2. These contexts are passed to the callouts
associated with the next hook. So when "check" is called, it gets the
context data that was set by "authorize", when "validate" is called,
it gets the context data set by "logpkt".
It is stressed that the context for callouts associated with different
libraries is entirely separate. For example, suppose "authorize" sets
the CalloutHandle's context item "foo" to 2 and "logpkt" sets an item of
the same name to the string "bar". When "check" accesses the context
item "foo", it gets a value of 2; when "validate" accesses an item of
the same name, it gets the value "bar".
It is also stressed that all this context exists only for the life of the
request being processed. When that request is complete, all the
context associated with that request - for all libraries - is destroyed,
and new context created for the next request.
This structure means that library authors can use per-request context
without worrying about the presence of other libraries. Other libraries
may be present, but will not affect the context values set by a library's
callouts.
Configuring multiple libraries just requires listing the libraries
as separate elements of the hooks-libraries configuration element, e.g.,
@code
"Dhcp4": {
:
"hooks-libraries": [
{
"library": "/usr/lib/library1.so"
},
{
"library": "/opt/library2.so"
}
:
]
}
@endcode
@subsection hooksdgInterLibraryData Passing Data Between Libraries
In rare cases, it is possible that one library may want to pass
data to another. This can be done in a limited way by means of the
CalloutHandle's @c setArgument and @c getArgument calls. For example, in the
above diagram, the callout "add_option" can pass a value to "putopt"
by setting a name.value pair in the hook's argument list. "putopt"
would be able to read this, but would not be able to return information
back to "add_option".
All argument names used by Kea will be a combination of letters
(both upper- and lower-case), digits, hyphens and underscores: no
other characters will be used. As argument names are simple strings,
it is suggested that if such a mechanism be used, the names of the data
values passed between the libraries include a special character such as
the dollar symbol or percent sign. In this way there is no danger that
a name will conflict with any existing or future Kea argument names.
@subsection hooksdgStaticallyLinkedKea Running Against a Statically-Linked Kea
If Kea is built with the --enable-static-link switch (set when
running the "configure" script), no shared Kea libraries are built;
instead, archive libraries are created and Kea is linked to them.
If you create a hooks library also linked against these archive libraries,
when the library is loaded you end up with two copies of the library code,
one in Kea and one in your library.
To run successfully, your library needs to perform run-time initialization
of the Kea code in your library (something performed by Kea
in the case of shared libraries). To do this, call the function
isc::hooks::hooksStaticLinkInit() as the first statement of the load()
function. (If your library does not include a load() function, you need
to add one.) For example:
@code
#include <hooks/hooks.h>
extern "C" {
int version() {
return (KEA_HOOKS_VERSION);
}
int load() {
isc::hooks::hooksStaticLinkInit();
:
}
// Other callout functions
:
}
@endcode
@subsection hooksdgHooksConfig Configuring Hooks Libraries
Sometimes it is useful for the hook library to have some configuration parameters.
This capability was introduced in Kea 1.1. This is often convenient to follow
generic Kea configuration approach rather than invent your own configuration
logic. Consider the following example:
@code
"hooks-libraries": [
{
"library": "/opt/first.so"
},
{
"library": "/opt/second.so",
"parameters": {
}
},
{
"library": "/opt/third.so",
"parameters": {
"mail": "spam@example.com",
"floor": 13,
"debug": false,
"users": [ "alice", "bob", "charlie" ],
"languages": {
"french": "bonjour",
"klingon": "yl'el"
}
}
}
]
@endcode
This example has three hook libraries configured. The first and second have
no parameters. Note that parameters map is optional, but it's perfectly okay to
specify it as an empty map. The third library is more interesting. It has five
parameters specified. The first one called 'mail' is a string. The second one
is an integer and the third one is boolean. Fourth and fifth parameters are
slightly more complicated as they are a list and a map respectively. JSON
structures can be nested if necessary, e.g., you can have a list, which contains
maps, maps that contain maps that contain other maps etc. Any valid JSON
structure can be represented. One important limitation here is that the top
level "parameters" structure must either be a map or not present at all.
Those parameters can be accessed in load() method. Passed isc::hooks::LibraryHandle
object has a method called getParameter that returns an instance of
isc::data::ConstElementPtr or null pointer if there was no parameter specified. This pointer
will point to an object derived from isc::data::Element class. For detailed
explanation how to use those objects, see isc::data::Element class.
Here's a brief overview of how to use those elements:
- x = getParameter("mail") will return instance of isc::data::StringElement. The content
can be accessed with x->stringValue() and will return std::string.
- x = getParameter("floor") will return an instance of isc::data::IntElement.
The content can be accessed with x->intValue() and will return int.
- x = getParameter("debug") will return an instance of isc::data::BoolElement.
Its value can be accessed with x->boolValue() and will return bool.
- x = getParameter("users") will return an instance of isc::data::ListElement.
Its content can be accessed with the following methods:
x->size(), x->get(index)
- x = getParameter("watch-list") will return an instance of isc::data::MapElement.
Its content can be accessed with the following methods:
x->find("klingon"), x->contains("french"), x->size()
Keep in mind that the user can structure his config file incorrectly.
Remember to check if the structure has the expected type before using type specific
method. For example calling stringValue on IntElement will throw an exception.
You can do this by calling isc::data::Element::getType.
Here's an example that obtains all of the parameters specified above.
If you want to get nested elements, Element::get(index) and Element::find(name)
will return ElementPtr, which can be iterated in similar manner.
@code
int load(LibraryHandle& handle) {
ConstElementPtr mail = handle.getParameter("mail");
ConstElementPtr floor = handle.getParameter("floor");
ConstElementPtr debug = handle.getParameter("debug");
ConstElementPtr users = handle.getParameter("users");
ConstElementPtr lang = handle.getParameter("languages");
// String handling example
if (!mail) {
// Handle missing 'mail' parameter here.
return (1);
}
if (mail->getType() != Element::string) {
// Handle incorrect 'mail' parameter here.
return (1);
}
std::string mail_str = mail->stringValue();
// In the following examples safety checks are omitted for clarity.
// Make sure you do it properly similar to mail example above
// or you risk dereferencing null pointer or at least throwing
// an exception!
// Integer handling example
int floor_num = floor->intValue();
// Boolean handling example
bool debug_flag = debug->boolValue();
// List handling example
std::cout << "There are " << users->size() << " users defined." << std::endl;
for (int i = 0; i < users->size(); i++) {
ConstElementPtr user = users->get(i);
std::cout << "User " << user->stringValue() << std::endl;
}
// Map handling example
std::cout << "There are " << lang->size() << " languages defined." << std::endl;
if (lang->contains("french")) {
std::cout << "One of them is French!" << std::endl;
}
ConstElementPtr greeting = lang->find("klingon");
if (greeting) {
std::cout << "Lt. Worf says " << greeting->stringValue() << std::endl;
}
// All validation steps were successful. The library has all the parameters
// it needs, so we should report a success.
return (0);
}
@endcode
A good sources of examples could be unit-tests in file src/lib/cc/tests/data_unittests.cc
which are dedicated to isc::data::Element testing and src/lib/hooks/tests/callout_params_library.cc,
which is an example library used in testing. This library expects exactly 3 parameters:
svalue (which is a string), ivalue (which is an integer) and bvalue (which is a boolean).
@subsection hooksMemoryManagement Memory Management Considerations for Hooks Writer
Both Kea server memory space and hook library memory space share a common
address space between the opening of the hook (call to dlopen() as the first
phase of the hook library loading) and the closing of the hook (call to
dlclose() as the last phase of the hook library unloading). There are
pointers between the two memory spaces with at least two bad consequences
when they are not correctly managed:
- Kea uses shared pointers for its objects. If the hook ownership keeps
ownership of an object, this object will never be destroyed, leading to
a trivial memory leak. Some care is recommended when the hook library
uses a garbage collector to not postpone releases of no longer used
objects. Cycles should be avoided too, for instance using weak pointers.
Of course at the opposite, if a Kea object is needed ownership on, it must
be kept in order to not get a dangling pointer when it will be destroyed
at the end of its last reference lifetime.
- Kea can take some pointers to the hook library memory space, for instance
when a hook object is registered. If these pointers are not destroyed
before the hook library memory space is unmapped by dlclose() this likely
leads to a crash.
Communication between Kea code and hook library code is provided by
callout handles. For callout points related to a packet, the callout
handle is associated with the packet allowing to get the same callout handle
for all callout points called during processing of a query.
Hook libraries are closed i.e. hook library memory spaces are unmapped
only when there is no active callout handles. This enforces a correct
behavior at two conditions:
- there is no "wild" dangling pointers, for instance no registered
objects.
- this can happen i.e. the hook library does not keep a shared pointer
to a query packet.
To allow hook writers to fulfill these two conditions the unload() entry
point is called in the first phase of the unloading process since Kea
version 1.7.10. For instance if the hook library uses the PIMPL code
pattern the unload() entry point must reset the pointer to the
hook library implementation.
@subsection hooksMultiThreading Multi-Threading Considerations for Hooks Writers
Multi-threading programming in C++ is not easy. For instance STL containers
do not support simultaneous read and write accesses. Kea is written in C++
so a priori for all Kea APIs one should never assume thread safety.
When a hook library is internally multi-threaded, its code and any Kea API
used simultaneously by different threads must be thread safe. To mark
the difference between this and the other thread safety requirement this
is called "generic thread safe".
When multi-threaded packet processing is enabled, Kea servers perform
some actions by the main thread and packet processing by members of
a thread pool. The multi-threading mode is returned by:
@code
isc::util::MultiThreadingMgr::instance().getMode()
@endcode
When it is false, Kea is single threaded and there is no thread safety
requirement, when it is true, the requirement is named Kea packet processing
thread safe shorten into "Kea thread safe".
A typical Kea thread safe looks like:
@code
int Foo() {
if (MultiThreadingMgr::instance().getMode()) {
std::lock_guard<std::mutex> lock(mutex_);
return (FooInternal());
} else {
return (FooInternal());
}
}
@endcode
The overhead of mutexes and other synchronization tools is far greater
than a test and branch so it is the recommended way to implement Kea
thread safety.
When a hook library entry point can be called from a packet processing
thread, typically from a packet processing callout but also when
implementing a lease or host backend API, the entry point code must
be Kea thread safe. If it is not possible the hook library must
be marked as not multi-threading compatible (i.e. return 0 from
multi_threading_compatible).
At the opposite during (re)configuration including libload command
and config backend, only the main thread runs, so version, load, unload,
multi_threading_compatible, dhcp4_srv_configured, dhcp6_srv_configured,
cb4_updated and cb6_updated have no thread safety requirements.
Other hook library entry points are called by the main thread:
- io service (io context is recent boost versions) is polled by the main
thread
- external socket callbacks are executed by the main thread
- commands including command_process
The packet processing threads are not stopped so either the entry
point code is Kea thread safe or it uses a critical section
(@c isc::util::MultiThreadingCriticalSection) to stop the packet
processing threads during the execution of the not Kea thread safe code.
Of course critical sections have an impact on performance so they should
be used only for particular cases where no better choice is available.
Some Kea APIs were made thread safe mainly because they are used by the
packet processing:
- logging is generic thread safe and even multi process safe i.e.
messages logged into a file or by syslog from multiple processes
do not mix.
- statistics update is Kea thread safe.
- lease and host database backends are Kea thread safe. Note if you need to
perform a direct MySQL or PostgreSQL query you must use the connection pool.
- state model and watched thread are generic thread safe (libkea-util)
- interval timer setup and cancel are generic thread safe (libkea-asiolink)
- parking lots are generic thread safe (libkea-hooks)
- external sockets are generic thread safe (libkea-dhcp++)
- http client is Kea thread safe (libkea-http)
Some other Kea APIs are intrinsically thread safe because they do not
involve a shared structure so for instance despite of its name the
interface manager send methods are generic thread safe.
Per library documentation details thread safety to help hooks writers
and to provide an exhaustive list of Kea thread safe APIs:
- @ref utilMTConsiderations
- @ref logMTConsiderations
- @ref asiolinkMTConsiderations
- @ref ccMTConsiderations
- @ref databaseMTConsiderations
- @ref ctrlSocketMTConsiderations
- @ref libdhcpMTConsiderations
- @ref statsMTConsiderations
- @ref yangMTConsiderations
- @ref libdhcp_ddnsMTConsiderations
- @ref dhcpEvalMTConsiderations
- @ref cplMTConsiderations
- @ref dhcpDatabaseBackendsMTConsiderations
- @ref libdhcpsrvMTConsiderations
- @ref httpMTConsiderations
*/
|