1 // Copyright (C) 2013-2020 Internet Systems Consortium, Inc. ("ISC")
2 //
3 // This Source Code Form is subject to the terms of the Mozilla Public
4 // License, v. 2.0. If a copy of the MPL was not distributed with this
5 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 
7 /// @file pkt_send_co.cc Defines the pkt4_send and pkt6_send callout functions.
8 
9 #include <config.h>
10 #include <asiolink/io_address.h>
11 #include <hooks/hooks.h>
12 #include <dhcp/dhcp4.h>
13 #include <dhcp/dhcp6.h>
14 #include <dhcp/option_string.h>
15 #include <dhcp/option_custom.h>
16 #include <dhcp/option6_ia.h>
17 #include <dhcp/option6_iaaddr.h>
18 #include <dhcp/option6_iaprefix.h>
19 #include <dhcp/docsis3_option_defs.h>
20 #include <dhcp/pkt4.h>
21 #include <dhcp/pkt6.h>
22 #include <user_chk.h>
23 
24 using namespace isc::dhcp;
25 using namespace isc::hooks;
26 using namespace user_chk;
27 using namespace std;
28 
29 // prototypes for local helper functions
30 void generate_output_record(const std::string& id_type_str,
31                             const std::string& id_val_str,
32                             const std::string& addr_str,
33                             const bool& registered);
34 std::string getV6AddrStr (Pkt6Ptr response);
35 std::string getAddrStrIA_NA(OptionPtr options);
36 std::string getAddrStrIA_PD(OptionPtr options);
37 bool checkIAStatus(boost::shared_ptr<Option6IA>& ia_opt);
38 
39 void add4Options(Pkt4Ptr& response, const UserPtr& user);
40 void add4Option(Pkt4Ptr& response, uint8_t opt_code, std::string& opt_value);
41 void add6Options(Pkt6Ptr& response, const UserPtr& user);
42 void add6Option(OptionPtr& vendor, uint8_t opt_code, std::string& opt_value);
43 const UserPtr& getDefaultUser4();
44 const UserPtr& getDefaultUser6();
45 
46 // Functions accessed by the hooks framework use C linkage to avoid the name
47 // mangling that accompanies use of the C++ compiler as well as to avoid
48 // issues related to namespaces.
49 extern "C" {
50 
51 /// @brief  This callout is called at the "pkt4_send" hook.
52 ///
53 /// This function generates the user check outcome and modifies options
54 /// to the IPv4 response packet based on whether the user is registered or not.
55 ///
56 /// It retrieves a pointer to the registered user from the callout context.
57 /// This value should have been set upstream.  If the registered user pointer
58 /// is non-null (i.e the user is registered), then a registered user outcome
59 /// is recorded in the outcome output and the vendor properties are altered
60 /// based upon this user's properties.
61 ///
62 /// A null value means the user is not registered and a unregistered user
63 /// outcome is recorded in the outcome output and the vendor properties
64 /// are altered based upon the default IPv4 user in the registry (if defined).
65 ///
66 /// @param handle CalloutHandle which provides access to context.
67 ///
68 /// @return 0 upon success, non-zero otherwise.
pkt4_send(CalloutHandle & handle)69 int pkt4_send(CalloutHandle& handle) {
70     CalloutHandle::CalloutNextStep status = handle.getStatus();
71     if (status == CalloutHandle::NEXT_STEP_DROP) {
72         return (0);
73     }
74 
75     if (status == CalloutHandle::NEXT_STEP_SKIP) {
76         isc_throw(isc::InvalidOperation, "packet pack already handled");
77     }
78 
79     try {
80         Pkt4Ptr response;
81         handle.getArgument("response4", response);
82 
83         uint8_t packet_type = response->getType();
84         if (packet_type == DHCPNAK) {
85             std::cout << "DHCP UserCheckHook : pkt4_send"
86                       << "skipping packet type: "
87                       << static_cast<int>(packet_type) << std::endl;
88             return (0);
89         }
90 
91         // Get the user id saved from the query packet.
92         HWAddrPtr hwaddr;
93         handle.getContext(query_user_id_label, hwaddr);
94 
95         // Get registered_user pointer.
96         UserPtr registered_user;
97         handle.getContext(registered_user_label, registered_user);
98 
99         // Fetch the lease address.
100         isc::asiolink::IOAddress addr = response->getYiaddr();
101 
102         if (registered_user) {
103             // add options based on user
104             // then generate registered output record
105             std::cout << "DHCP UserCheckHook : pkt4_send registered_user is: "
106                       << registered_user->getUserId() << std::endl;
107 
108             // Add the outcome entry to the output file.
109             generate_output_record(UserId::HW_ADDRESS_STR, hwaddr->toText(),
110                                    addr.toText(), true);
111             add4Options(response, registered_user);
112         } else {
113             // add default options based
114             // then generate not registered output record
115             std::cout << "DHCP UserCheckHook : pkt4_send no registered_user"
116                       << std::endl;
117             // Add the outcome entry to the output file.
118             generate_output_record(UserId::HW_ADDRESS_STR, hwaddr->toText(),
119                                    addr.toText(), false);
120 
121             add4Options(response, getDefaultUser4());
122         }
123     } catch (const std::exception& ex) {
124         std::cout << "DHCP UserCheckHook : pkt4_send unexpected error: "
125                   << ex.what() << std::endl;
126         return (1);
127     }
128 
129     return (0);
130 }
131 
132 /// @brief  This callout is called at the "pkt6_send" hook.
133 ///
134 /// This function generates the user check outcome and modifies options
135 /// to the IPv6 response packet based on whether the user is registered or not.
136 ///
137 /// It retrieves a pointer to the registered user from the callout context.
138 /// This value should have been set upstream.  If the registered user pointer
139 /// is non-null (i.e the user is registered), then a registered user outcome
140 /// is recorded in the outcome output and the vendor properties are altered
141 /// based upon this user's properties.
142 ///
143 /// A null value means the user is not registered and a unregistered user
144 /// outcome is recorded in the outcome output and the vendor properties
145 /// are altered based upon the default IPv6 user in the registry (if defined).
146 /// @param handle CalloutHandle which provides access to context.
147 ///
148 /// @return 0 upon success, non-zero otherwise.
pkt6_send(CalloutHandle & handle)149 int pkt6_send(CalloutHandle& handle) {
150     CalloutHandle::CalloutNextStep status = handle.getStatus();
151     if (status == CalloutHandle::NEXT_STEP_DROP) {
152         return (0);
153     }
154 
155     if (status == CalloutHandle::NEXT_STEP_SKIP) {
156         isc_throw(isc::InvalidOperation, "packet pack already handled");
157     }
158 
159     try {
160         Pkt6Ptr response;
161         handle.getArgument("response6", response);
162 
163         // Fetch the lease address as a string
164         std::string addr_str = getV6AddrStr(response);
165         if (addr_str.empty()) {
166             // packet did not contain an address, must be failed.
167             std::cout << "pkt6_send: Skipping packet address is blank"
168                       << std::endl;
169             return (0);
170         }
171 
172         // Get the user id saved from the query packet.
173         DuidPtr duid;
174         handle.getContext(query_user_id_label, duid);
175 
176         // Get registered_user pointer.
177         UserPtr registered_user;
178         handle.getContext(registered_user_label, registered_user);
179 
180         if (registered_user) {
181             // add options based on user
182             // then generate registered output record
183             std::cout << "DHCP UserCheckHook : pkt6_send registered_user is: "
184                       << registered_user->getUserId() << std::endl;
185             // Add the outcome entry to the output file.
186             generate_output_record(UserId::DUID_STR, duid->toText(),
187                                    addr_str, true);
188             add6Options(response, registered_user);
189         } else {
190             // add default options based
191             // then generate not registered output record
192             std::cout << "DHCP UserCheckHook : pkt6_send no registered_user"
193                       << std::endl;
194             // Add the outcome entry to the output file.
195             generate_output_record(UserId::DUID_STR, duid->toText(),
196                                    addr_str, false);
197             add6Options(response, getDefaultUser6());
198         }
199     } catch (const std::exception& ex) {
200         std::cout << "DHCP UserCheckHook : pkt6_send unexpected error: "
201                   << ex.what() << std::endl;
202         return (1);
203     }
204 
205     return (0);
206 }
207 
208 } // extern C
209 
210 /// @brief Adds IPv4 options to the response packet based on given user
211 ///
212 /// Adds or replaces IPv4 options with values from the given user, if
213 /// the user has corresponding properties defined. Currently it supports
214 /// the following options:
215 ///
216 /// - DHO_BOOT_FILE_NAME from user property "bootfile"
217 /// - DHO_TFTP_SERVER_NAME from user property "tftp_server"
218 ///
219 /// @param response IPv4 response packet
220 /// @param user User from whom properties are sourced
add4Options(Pkt4Ptr & response,const UserPtr & user)221 void add4Options(Pkt4Ptr& response, const UserPtr& user) {
222     // If user is null, do nothing.
223     if (!user) {
224         return;
225     }
226 
227     // If the user has bootfile property, update it in the response.
228     std::string opt_value = user->getProperty("bootfile");
229     if (!opt_value.empty()) {
230         std::cout << "DHCP UserCheckHook : add4Options "
231               << "adding boot file:" << opt_value << std::endl;
232 
233         // Add boot file to packet.
234         add4Option(response, DHO_BOOT_FILE_NAME, opt_value);
235 
236         // Boot file also goes in file field.
237         response->setFile((const uint8_t*)(opt_value.c_str()),
238                           opt_value.length());
239     }
240 
241     // If the user has tftp server property, update it in the response.
242     opt_value = user->getProperty("tftp_server");
243     if (!opt_value.empty()) {
244         std::cout << "DHCP UserCheckHook : add4Options "
245               << "adding TFTP server:" << opt_value << std::endl;
246 
247         // Add tftp server option to packet.
248         add4Option(response, DHO_TFTP_SERVER_NAME, opt_value);
249     }
250     // add next option here
251 }
252 
253 /// @brief Adds/updates are specific IPv4 string option in response packet.
254 ///
255 /// @param response IPV4 response packet to update
256 /// @param opt_code DHCP standard numeric code of the option
257 /// @param opt_value String value of the option
add4Option(Pkt4Ptr & response,uint8_t opt_code,std::string & opt_value)258 void add4Option(Pkt4Ptr& response, uint8_t opt_code, std::string& opt_value) {
259     // Remove the option if it exists.
260     OptionPtr opt = response->getOption(opt_code);
261     if (opt) {
262         response->delOption(opt_code);
263     }
264 
265     // Now add the option.
266     opt.reset(new OptionString(Option::V4, opt_code, opt_value));
267     response->addOption(opt);
268 }
269 
270 
271 /// @brief Adds IPv6 vendor options to the response packet based on given user
272 ///
273 /// Adds or replaces IPv6 vendor options with values from the given user, if
274 /// the user has the corresponding properties defined. Currently it supports
275 /// the following options:
276 ///
277 /// - DOCSIS3_V6_CONFIG_FILE from user property "bootfile"
278 /// - DOCSIS3_V6_TFTP_SERVERS from user property "tftp_server"
279 ///
280 /// @param response IPv5 response packet
281 /// @param user User from whom properties are sourced
add6Options(Pkt6Ptr & response,const UserPtr & user)282 void add6Options(Pkt6Ptr& response, const UserPtr& user) {
283     if (!user) {
284         return;
285     }
286 
287     /// @todo: if packets have no vendor opt... do we need to add it
288     /// if its not there?  If so how?
289     OptionPtr vendor = response->getOption(D6O_VENDOR_OPTS);
290     if (!vendor) {
291         std::cout << "DHCP UserCheckHook : add6Options "
292               << "response has no vendor option to update" << std::endl;
293         return;
294     }
295 
296     // If the user defines bootfile, set the option in response.
297     std::string opt_value = user->getProperty("bootfile");
298     if (!opt_value.empty()) {
299         std::cout << "DHCP UserCheckHook : add6Options "
300                   << "adding boot file:" << opt_value << std::endl;
301         add6Option(vendor, DOCSIS3_V6_CONFIG_FILE, opt_value);
302     }
303 
304     // If the user defines tftp server, set the option in response.
305     opt_value = user->getProperty("tftp_server");
306     if (!opt_value.empty()) {
307         std::cout << "DHCP UserCheckHook : add6Options "
308                   << "adding tftp server:" << opt_value << std::endl;
309 
310         add6Option(vendor, DOCSIS3_V6_TFTP_SERVERS, opt_value);
311     }
312 
313     // add next option here
314 }
315 
316 /// @brief Adds/updates a specific IPv6 string vendor option.
317 ///
318 /// @param vendor IPv6 vendor option set to update
319 /// @param opt_code DHCP standard numeric code of the option
320 /// @param opt_value String value of the option
add6Option(OptionPtr & vendor,uint8_t opt_code,std::string & opt_value)321 void add6Option(OptionPtr& vendor, uint8_t opt_code, std::string& opt_value) {
322     vendor->delOption(opt_code);
323     OptionPtr option(new OptionString(Option::V6, opt_code, opt_value));
324     vendor->addOption(option);
325 }
326 
327 
328 /// @brief Adds an entry to the end of the user check outcome file.
329 ///
330 /// @todo This ought to be replaced with an abstract output similar to
331 /// UserDataSource to allow greater flexibility.
332 ///
333 /// Each user entry is written in an ini-like format, with one name-value pair
334 /// per line as follows:
335 ///
336 /// id_type=&lt;id type&gt;<br/>
337 /// client=&lt;id str&gt;<br/>
338 /// subnet=&lt;addr str&gt;<br/>
339 /// registered=&lt;is registered&gt;
340 ///
341 /// where:
342 /// &lt;id type&gt; text label of the id type: "HW_ADDR" or "DUID"
343 /// &lt;id str&gt; user's id formatted as either isc::dhcp::Hwaddr.toText() or
344 /// isc::dhcp::DUID.toText()
345 /// &lt;addr str&gt; selected subnet formatted as isc::dhcp::Subnet4::toText() or
346 /// isc::dhcp::Subnet6::toText() as appropriate.
347 /// &lt;is registered&gt; "yes" or "no"
348 ///
349 /// Sample IPv4 entry would like this:
350 ///
351 /// @code
352 /// id_type=DUID
353 /// client=00:01:00:01:19:ef:e6:3b:00:0c:01:02:03:04
354 /// subnet=2001:db8:2::/64
355 /// registered=yes
356 /// id_type=duid
357 /// @endcode
358 ///
359 /// Sample IPv4 entry would like this:
360 ///
361 /// @code
362 /// id_type=DUID
363 /// id_type=HW_ADDR
364 /// client=hwtype=1 00:0c:01:02:03:05
365 /// subnet=152.77.5.0/24
366 /// registered=no
367 /// @endcode
368 ///
369 /// @param id_type_str text label identify the id type
370 /// @param id_val_str text representation of the user id
371 /// @param addr_str text representation  of the selected subnet
372 /// @param registered boolean indicating if the user is registered or not
generate_output_record(const std::string & id_type_str,const std::string & id_val_str,const std::string & addr_str,const bool & registered)373 void generate_output_record(const std::string& id_type_str,
374                             const std::string& id_val_str,
375                             const std::string& addr_str,
376                             const bool& registered)
377 {
378     user_chk_output << "id_type=" << id_type_str << std::endl
379                     << "client=" << id_val_str << std::endl
380                     << "addr=" << addr_str << std::endl
381                     << "registered=" << (registered ? "yes" : "no")
382                     << std::endl
383                     << std::endl;   // extra line in between
384 
385     // @todo Flush is here to ensure output is immediate for demo purposes.
386     // Performance would generally dictate not using it.
387     flush(user_chk_output);
388 }
389 
390 /// @brief Stringify the lease address or prefix IPv6 response packet
391 ///
392 /// Converts the lease value, either an address or a prefix, into a string
393 /// suitable for the user check outcome output.  Note that this will use
394 /// the first address or prefix in the response for responses with more than
395 /// one value.
396 ///
397 /// @param response IPv6 response packet from which to extract the lease value.
398 ///
399 /// @return A string containing the lease value.
400 /// @throw isc::BadValue if the response contains neither an IA_NA nor IA_PD
401 /// option.
getV6AddrStr(Pkt6Ptr response)402 std::string getV6AddrStr(Pkt6Ptr response) {
403     OptionPtr tmp = response->getOption(D6O_IA_NA);
404     if (tmp) {
405         return(getAddrStrIA_NA(tmp));
406     }
407 
408     // IA_NA not there so try IA_PD
409     tmp = response->getOption(D6O_IA_PD);
410     if (!tmp) {
411         isc_throw (isc::BadValue, "Response has neither IA_NA nor IA_PD");
412     }
413 
414     return(getAddrStrIA_PD(tmp));
415 }
416 
417 /// @brief Stringify the lease address in an D6O_IA_NA option set
418 ///
419 /// Converts the IA_NA lease address into a string suitable for the user check
420 /// outcome output.
421 ///
422 /// @param options pointer to the Option6IA instance from which to extract the
423 /// lease address.
424 ///
425 /// @return A string containing the lease address.
426 ///
427 /// @throw isc::BadValue if the lease address cannot be extracted from options.
getAddrStrIA_NA(OptionPtr options)428 std::string getAddrStrIA_NA(OptionPtr options) {
429     boost::shared_ptr<Option6IA> ia =
430         boost::dynamic_pointer_cast<Option6IA>(options);
431 
432     if (!ia) {
433         isc_throw (isc::BadValue, "D6O_IA_NA option invalid");
434     }
435 
436     // If status indicates a failure return a blank string.
437     if (!checkIAStatus(ia)) {
438         return (std::string(""));
439     }
440 
441     options = ia->getOption(D6O_IAADDR);
442     if (!options) {
443         isc_throw (isc::BadValue, "D6O_IAADDR option missing");
444     }
445 
446     boost::shared_ptr<Option6IAAddr> addr_option;
447     addr_option  = boost::dynamic_pointer_cast<Option6IAAddr>(options);
448     if (!addr_option) {
449         isc_throw (isc::BadValue, "D6O_IAADDR Option6IAAddr missing");
450     }
451 
452     isc::asiolink::IOAddress addr = addr_option->getAddress();
453     return (addr.toText());
454 }
455 
456 /// @brief Stringify the lease prefix in an D6O_IA_PD option set
457 ///
458 /// Converts the IA_PD lease prefix into a string suitable for the user check
459 /// outcome output.
460 ///
461 /// @param options pointer to the Option6IA instance from which to extract the
462 /// lease prefix.
463 ///
464 /// @return A string containing lease prefix
465 ///
466 /// @throw isc::BadValue if the prefix cannot be extracted from options.
getAddrStrIA_PD(OptionPtr options)467 std::string getAddrStrIA_PD(OptionPtr options) {
468     boost::shared_ptr<Option6IA> ia =
469         boost::dynamic_pointer_cast<Option6IA>(options);
470 
471     // Make sure we have an IA_PD option.
472     if (!ia) {
473         isc_throw (isc::BadValue, "D6O_IA_PD option invalid");
474     }
475 
476     // Check the response for success status.  If it isn't a success response
477     // there will not be a lease prefix value which is denoted by returning
478     // an empty string.
479     if (!checkIAStatus(ia)) {
480         return (std::string(""));
481     }
482 
483     // Get the prefix option the IA_PD option.
484     options = ia->getOption(D6O_IAPREFIX);
485     if (!options) {
486         isc_throw(isc::BadValue, "D6O_IAPREFIX option is missing");
487     }
488 
489     boost::shared_ptr<Option6IAPrefix> addr_option;
490     addr_option = boost::dynamic_pointer_cast<Option6IAPrefix>(options);
491     if (!addr_option) {
492         isc_throw (isc::BadValue, "D6O_IA_PD addr option bad");
493     }
494 
495     // Get the address and prefix length values.
496     isc::asiolink::IOAddress addr = addr_option->getAddress();
497     uint8_t prefix_len = addr_option->getLength();
498 
499     // Build the output string and return it.
500     stringstream buf;
501     buf << addr.toText() << "/" << static_cast<int>(prefix_len);
502     return (buf.str());
503 }
504 
505 /// @brief Tests given IA option set for successful status.
506 ///
507 /// This function is used to determine if the given  Option6IA represents
508 /// a successful lease operation.  If it contains no status option or a status
509 /// option of 0 (which is defined to mean success), then the option represents
510 /// success and should contain a lease value (address or prefix).
511 ///
512 /// @param ia pointer to the Option6IA to test
513 ///
514 /// @return True if the option represents success, false otherwise.
checkIAStatus(boost::shared_ptr<Option6IA> & ia)515 bool checkIAStatus(boost::shared_ptr<Option6IA>& ia) {
516     OptionCustomPtr status =
517             boost::dynamic_pointer_cast
518                 <OptionCustom>(ia->getOption(D6O_STATUS_CODE));
519 
520    // If a non-zero status is present, bail.
521    if (status) {
522         int status_val = status->readInteger<uint16_t>(0);
523         if (status_val != 0) {
524             std::cout << "SKIPPING PACKET STATUS is not success:"
525                       << status_val << std::endl;
526             return (false);
527         }
528     }
529 
530     return (true);
531 }
532 
533 /// @brief Fetches the default IPv4 user from the registry.
534 ///
535 /// The default user may be used to provide default property values.
536 ///
537 /// @return A pointer to the IPv4 user or null if not defined.
getDefaultUser4()538 const UserPtr& getDefaultUser4() {
539    return (user_registry->findUser(UserId(UserId::HW_ADDRESS,
540                                           default_user4_id_str)));
541 }
542 
543 /// @brief Fetches the default IPv6 user from the registry.
544 ///
545 /// The default user may be used to provide default property values.
546 ///
547 /// @return A pointer to the IPv6 user or null if not defined.
getDefaultUser6()548 const UserPtr& getDefaultUser6() {
549    return (user_registry->findUser(UserId(UserId::DUID,
550                                           default_user6_id_str)));
551 }
552 
553