= DHCPv4 Module
-The `dhcpv4` module is useful only for `xlat`. To use it,
-put `dhcp` into the `instantiate { ... }` section.
-`%{dhcp_options:<Attribute-ref>}` may be used to decode
-DHCP options data included in RADIUS packets by vendors
-of DHCP to RADIUS gateways.
-This is known to work with the following VSAs:
+## Configuration Settings
-[options="header,autowidth"]
-|===
-| Vendor | VSAs
-| Juniper | `ERX-Dhcp-Options`
-| Alcatel lucent SR | `link:https://infoproducts.alcatel-lucent.com/html/0_add-h-f/93-0088-HTML/7750_SR_OS_Radius_Attributes_Reference_Guide/SROS_RADIUS_Attrib.html[Alc-ToServer-Dhcp-Options]`, `link:https://infoproducts.alcatel-lucent.com/html/0_add-h-f/93-0088-HTML/7750_SR_OS_Radius_Attributes_Reference_Guide/SROS_RADIUS_Attrib.html[Alc-ToClient-Dhcp-Options]`
-|===
+The DHCPv4 module is used as a relay.
-e.g: `%{dhcp_options:RX-Dhcp-Options}`
+For reach request, you should set &request.Packet-Dst-IP-Address to
+the address of the DHCPv4 server (or next relay).
+Packets MUST also have a Gateway-IP-Address option, otherwise they
+will be dropped. This module does not (yet) support receiving
+replies from the DHCPv4 server. Any packets sent to the socket
+IP/port will be dropped.
-## Configuration Settings
+ipaddr: The IP address we use for sending packets.
-This module takes no configuration.
+This MUST be an IPv4 address. IPv6 addresses are not
+supported for DHCPv4.
-## Expansions
+port:: The port we use for sending packets.
-The rlm_dhcpv4 provides the below xlat's functions to handle the DHCPV4 packets.
+The default DHCPv4 client port is 68. It should be changed
+from this only for testing.
-### %(dhcpv4_decode:...)
-Decode DHCP option declared in dictionary.rfc2131.
-.Return: _string_
+interface:: Interface to bind to.
-.Example
-[source,unlang]
-----
-update request {
- &Tmp-Octets-0 := 0x520d0103abcdef0206010203040506
-}
-if ("%(dhcpv4_decode:%{Tmp-Octets-0})" != 2) {
- update reply {
- &Reply-Message := "Problems decoding the DHPCv4 fields."
- }
- reject
-}
-update reply {
- &Reply-Message := "The value of Relay-Agent-Information.Circuit-ID=%{Relay-Agent-Information.Circuit-ID}, Relay-Agent-Information.Remote-ID=%{Relay-Agent-Information.Remote-ID}"
-}
-----
-.Output
+max_packet_size:: The maximum packet size.
-```
-"The value of Relay-Agent-Information.Circuit-ID=0xabcdef, Relay-Agent-Information.Remote-ID=0x010203040506"
-```
+Packets which are larger than this will not be sent.
-### %(dhcpv4_encode:...)
+Useful range: 300..1500 or so.
-Encode DHCP option declared in dictionary.rfc2131.
-.Return: _string_
-.Example
+send_buff:: How big the kernel's send buffer should be.
-[source,unlang]
-----
-update request {
- &Relay-Agent-Information.Circuit-ID := 0xabcdef
- &Relay-Agent-Information.Remote-ID := 0x010203040506
-}
-update request {
- &Tmp-Octets-0 := "%(dhcpv4_encode:&request[*])"
-}
-if (&Tmp-Octets-0 != 0x520d0103abcdef0206010203040506) {
- update reply {
- &Reply-Message := "Invalid DHCP packets"
- }
- reject
-}
-----
-
-.Output
-
-```
-
-```
== Default Configuration
```
dhcpv4 {
+ ipaddr = 127.0.0.1
+# port = 68
+# interface = eth0
+# max_packet_size = 576
+# send_buff = 1048576
}
```
{ NULL }
};
+static fr_dict_attr_t const *attr_transaction_id;
+static fr_dict_attr_t const *attr_message_type;
+static fr_dict_attr_t const *attr_packet_type;
+static fr_dict_attr_t const *attr_packet_dst_ip_address;
+static fr_dict_attr_t const *attr_packet_dst_port;
+static fr_dict_attr_t const *attr_gateway_ip_address;
+
+extern fr_dict_attr_autoload_t rlm_dhcpv4_dict_attr[];
+fr_dict_attr_autoload_t rlm_dhcpv4_dict_attr[] = {
+ { .out = &attr_transaction_id, .name = "Transaction-Id", .type = FR_TYPE_UINT32, .dict = &dict_dhcpv4 },
+ { .out = &attr_gateway_ip_address, .name = "Gateway-IP-Address", .type = FR_TYPE_IPV4_ADDR, .dict = &dict_dhcpv4 },
+ { .out = &attr_message_type, .name = "Message-Type", .type = FR_TYPE_UINT8, .dict = &dict_dhcpv4 },
+ { .out = &attr_packet_type, .name = "Packet-Type", .type = FR_TYPE_UINT32, .dict = &dict_dhcpv4 },
+ { .out = &attr_packet_dst_ip_address, .name = "Packet-Dst-IP-Address", .type = FR_TYPE_IPV4_ADDR, .dict = &dict_dhcpv4 },
+ { .out = &attr_packet_dst_port, .name = "Packet-Dst-Port", .type = FR_TYPE_UINT16, .dict = &dict_dhcpv4 },
+ { NULL }
+};
+
+
/*
* Define a structure for our module configuration.
*
* be used as the instance handle.
*/
typedef struct {
- int nothing;
+ char const *name;
+ char const *xlat_name;
+
+ int fd; //!< only relay, no proxying for now
+
+ fr_ipaddr_t ipaddr; //!< socket IP address
+ uint16_t port; //!< socket port
+
+ char const *interface; //!< Interface to bind to.
+
+ uint32_t recv_buff; //!< How big the kernel's receive buffer should be.
+ uint32_t send_buff; //!< How big the kernel's send buffer should be.
+
+ uint32_t max_packet_size; //!< Maximum packet size.
+
+ bool recv_buff_is_set; //!< Whether we were provided with a recv_buf
+ bool send_buff_is_set; //!< Whether we were provided with a send_buf
} rlm_dhcpv4_t;
+typedef struct {
+ uint8_t *buffer; //!< for encoding packets
+ uint32_t buffer_size; //!< Maximum packet size.
+} rlm_dhcpv4_thread_t;
-/*
- * The module name should be the only globally exported symbol.
- * That is, everything else should be 'static'.
+static const CONF_PARSER module_config[] = {
+ { FR_CONF_OFFSET("ipaddr", FR_TYPE_IPV4_ADDR, rlm_dhcpv4_t, ipaddr), },
+ { FR_CONF_OFFSET("ipv4addr", FR_TYPE_IPV4_ADDR, rlm_dhcpv4_t, ipaddr) },
+
+ { FR_CONF_OFFSET("port", FR_TYPE_UINT16, rlm_dhcpv4_t, port), .dflt = "68" },
+
+ { FR_CONF_OFFSET("interface", FR_TYPE_STRING, rlm_dhcpv4_t, interface) },
+
+ { FR_CONF_OFFSET_IS_SET("recv_buff", FR_TYPE_UINT32, rlm_dhcpv4_t, recv_buff) },
+ { FR_CONF_OFFSET_IS_SET("send_buff", FR_TYPE_UINT32, rlm_dhcpv4_t, send_buff) },
+
+ { FR_CONF_OFFSET("max_packet_size", FR_TYPE_UINT32, rlm_dhcpv4_t, max_packet_size), .dflt = "576" },
+
+ CONF_PARSER_TERMINATOR
+};
+
+/** Bootstrap the module
+ *
+ * Bootstrap I/O and type submodules.
+ *
+ * @param[in] instance Ctx data for this module
+ * @param[in] conf our configuration section parsed to give us instance.
+ * @return
+ * - 0 on success.
+ * - -1 on failure.
+ */
+static int mod_bootstrap(void *instance, CONF_SECTION *conf)
+{
+ rlm_dhcpv4_t *inst = talloc_get_type_abort(instance, rlm_dhcpv4_t);
+
+ inst->xlat_name = cf_section_name2(conf);
+ if (!inst->xlat_name) inst->xlat_name = cf_section_name1(conf);
+ inst->name = inst->xlat_name;
+
+ /*
+ * Ensure that we have a destination address.
+ */
+ if (inst->ipaddr.af == AF_UNSPEC) {
+ cf_log_err(conf, "A value must be given for 'ipaddr'");
+ return -1;
+ }
+
+ if (inst->ipaddr.af != AF_INET) {
+ cf_log_err(conf, "DHCPv4 can only use IPv4 addresses in 'ipaddr'");
+ return -1;
+ }
+
+ if (!inst->port) {
+ cf_log_err(conf, "A value must be given for 'port'");
+ return -1;
+ }
+
+ /*
+ * Clamp max_packet_size first before checking recv_buff and send_buff
+ */
+ FR_INTEGER_BOUND_CHECK("max_packet_size", inst->max_packet_size, >=, DEFAULT_PACKET_SIZE);
+ FR_INTEGER_BOUND_CHECK("max_packet_size", inst->max_packet_size, <=, MAX_PACKET_SIZE);
+
+ if (inst->send_buff_is_set) {
+ FR_INTEGER_BOUND_CHECK("send_buff", inst->send_buff, >=, inst->max_packet_size);
+ FR_INTEGER_BOUND_CHECK("send_buff", inst->send_buff, <=, (1 << 30));
+ }
+
+ return 0;
+}
+
+/** Instantiate the module
+ *
+ * Instantiate I/O and type submodules.
+ *
+ * @param[in] instance data for this module
+ * @param[in] conf our configuration section parsed to give us instance.
+ * @return
+ * - 0 on success.
+ * - -1 on failure.
+ */
+static int mod_instantiate(void *instance, CONF_SECTION *conf)
+{
+ rlm_dhcpv4_t *inst = talloc_get_type_abort(instance, rlm_dhcpv4_t);
+
+ /*
+ * Open the outgoing socket.
+ */
+ inst->fd = fr_socket_server_udp(&inst->ipaddr, &inst->port, NULL, true);
+ if (inst->fd < 0) {
+ cf_log_err(conf, "Failed opening socket: %s", fr_strerror());
+ return -1;
+ }
+
+ /*
+ * Bind to the interface, if required.
+ */
+ if (inst->interface) {
+ if (fr_socket_bind(inst->fd, &inst->ipaddr, &inst->port, inst->interface) < 0) {
+ cf_log_err(conf, "Failed binding to interface %s: %s", inst->interface, fr_strerror());
+ return -1;
+ }
+ }
+
+ fr_nonblock(inst->fd);
+
+#ifdef SO_RCVBUF
+ if (inst->recv_buff_is_set) {
+ int opt;
+
+ opt = inst->recv_buff;
+ if (setsockopt(inst->fd, SOL_SOCKET, SO_RCVBUF, &opt, sizeof(int)) < 0) {
+ cf_log_warn(conf, "Failed setting 'SO_RCVBUF': %s", fr_syserror(errno));
+ }
+ }
+#endif
+
+#ifdef SO_SNDBUF
+ if (inst->send_buff_is_set) {
+ int opt;
+
+ opt = inst->send_buff;
+ if (setsockopt(inst->fd, SOL_SOCKET, SO_SNDBUF, &opt, sizeof(int)) < 0) {
+ cf_log_warn(conf, "Failed setting 'SO_SNDBUF', write performance may be sub-optimal: %s",
+ fr_syserror(errno));
+ }
+ }
+#endif
+
+ return 0;
+}
+
+/** Instantiate thread data for the submodule.
+ *
+ */
+static int mod_thread_instantiate(CONF_SECTION const *cs, void *instance, UNUSED fr_event_list_t *el, void *thread)
+{
+ rlm_dhcpv4_t *inst = talloc_get_type_abort(instance, rlm_dhcpv4_t);
+ rlm_dhcpv4_thread_t *t = talloc_get_type_abort(thread, rlm_dhcpv4_thread_t);
+
+ t->buffer = talloc_array(t, uint8_t, inst->max_packet_size);
+ if (!t->buffer) {
+ cf_log_err(cs, "Failed allocating buffer");
+ return -1;
+ }
+
+ t->buffer_size = inst->max_packet_size;
+
+ return 0;
+}
+
+/** Send packets outbound.
*
- * If the module needs to temporarily modify it's instantiation
- * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE.
- * The server will then take care of ensuring that the module
- * is single-threaded.
*/
+static unlang_action_t CC_HINT(nonnull) mod_process(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request)
+{
+ rlm_dhcpv4_t *inst = talloc_get_type_abort(mctx->instance, rlm_dhcpv4_t);
+ rlm_dhcpv4_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_dhcpv4_thread_t);
+ ssize_t data_len;
+ dhcp_packet_t *original = (dhcp_packet_t *) request->packet->data;
+ dhcp_packet_t *packet;
+ struct sockaddr_storage sockaddr;
+ socklen_t socklen;
+ uint32_t xid;
+ fr_pair_t *vp;
+ int code, port;
+
+ /*
+ * Discard any incoming packets, as we don't care about
+ * them.
+ *
+ * @todo - maybe set up a thread listener to do this, if
+ * we care.
+ */
+ while (read(inst->fd, t->buffer, t->buffer_size) > 0) {
+ /* do nothing with the data, we don't care */
+ }
+
+ /*
+ * We can only send relayed packets, which have a gateway IP
+ */
+ vp = fr_pair_find_by_da(&request->request_pairs, attr_gateway_ip_address, 0);
+ if (!vp) {
+ REDEBUG("Relayed packets MUST have a Gateway-IP-Address attribute");
+ RETURN_MODULE_FAIL;
+ }
+
+ /*
+ * Get the transaction ID.
+ */
+ vp = fr_pair_find_by_da(&request->request_pairs, attr_transaction_id, 0);
+ if (vp) {
+ xid = vp->vp_uint32;
+
+ } else if (original) {
+ xid = ntohl(original->xid);
+
+ } else {
+ xid = fr_rand(); /* shouldn't happen, as we're relaying packets, not creating them (yet) */
+ }
+
+ /*
+ * Set the packet type.
+ *
+ * @todo - make sure it's a client type.
+ */
+ vp = fr_pair_find_by_da(&request->request_pairs, attr_packet_type, 0);
+ if (vp) {
+ code = vp->vp_uint32;
+
+ } else if ((vp = fr_pair_find_by_da(&request->request_pairs, attr_message_type, 0)) != NULL) {
+ code = vp->vp_uint8;
+
+ } else {
+ code = request->packet->code;
+ }
+
+ /*
+ * Set the destination port, defaulting to 67
+ */
+ vp = fr_pair_find_by_da(&request->request_pairs, attr_packet_dst_port, 0);
+ if (vp) {
+ port = vp->vp_uint16;
+ } else {
+ port = 67; /* DHCPv4 server port */
+ }
+
+ /*
+ * Get the destination address / port, and unicast it there.
+ */
+ vp = fr_pair_find_by_da(&request->request_pairs, attr_packet_dst_ip_address, 0);
+ if (!vp) {
+ RDEBUG("No Packet-Dst-IP-Address, cannot relay packet");
+ RETURN_MODULE_NOOP;
+ }
+
+ fr_ipaddr_to_sockaddr(&sockaddr, &socklen, &vp->vp_ip, port);
+
+ /*
+ * Encode the packet using the original information.
+ */
+ data_len = fr_dhcpv4_encode(t->buffer, t->buffer_size, original, code, xid, &request->request_pairs);
+ if (data_len <= 0) {
+ RPEDEBUG("Failed encoding DHCPV4 request");
+ RETURN_MODULE_FAIL;
+ }
+
+ /*
+ * Enforce some other RFC requirements.
+ */
+ packet = (dhcp_packet_t *) t->buffer;
+ if (packet->opcode == 1) {
+ if (original) {
+ if (original->hops < 255) packet->hops = original->hops + 1;
+ } else {
+ if (packet->hops < 255) packet->hops++;
+ }
+
+ } /* else sending a server message? OK boomer. */
+
+ FR_PROTO_HEX_DUMP(t->buffer, data_len, "DHCPv4");
+
+ /*
+ * Send the packet, via "fire and forget".
+ */
+ if (sendto(inst->fd, t->buffer, data_len, 0, (struct sockaddr *) &sockaddr, socklen) < 0) {
+ REDEBUG("Failed sending packet to %pV: %s", &vp->data, fr_syserror(errno));
+ RETURN_MODULE_FAIL;
+ }
+
+ RETURN_MODULE_OK;
+}
+
extern module_t rlm_dhcpv4;
module_t rlm_dhcpv4 = {
.magic = RLM_MODULE_INIT,
.name = "dhcpv4",
.inst_size = sizeof(rlm_dhcpv4_t),
+ .bootstrap = mod_bootstrap,
+ .instantiate = mod_instantiate,
+
+ .config = module_config,
+
+ .thread_inst_size = sizeof(rlm_dhcpv4_thread_t),
+ .thread_inst_type = "rlm_dhcpv4_thread_t",
+ .thread_instantiate = mod_thread_instantiate,
+
+ .methods = {
+ [MOD_AUTHORIZE] = mod_process,
+ [MOD_POST_AUTH] = mod_process,
+ },
+ .method_names = (module_method_names_t[]){
+ { .name1 = CF_IDENT_ANY, .name2 = CF_IDENT_ANY, .method = mod_process },
+ MODULE_NAME_TERMINATOR
+ },
};