mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-31 04:53:01 +02:00
Merge pull request #1251 from kernelkit/dhcpv6-client
Add DHCPv6 client support
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/usr/share/udhcpc/default.script
|
||||
@@ -0,0 +1 @@
|
||||
/usr/share/udhcpc/default6.script
|
||||
@@ -104,7 +104,6 @@ case "$ACTION" in
|
||||
deconfig)
|
||||
clr_dhcp_addresses
|
||||
clr_dhcp_routes
|
||||
/bin/ip link set dev $interface up
|
||||
|
||||
# drop info from this interface
|
||||
rm -f "$RESOLV_CONF"
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/sh
|
||||
# This script expects a system with resolvconf (openresolv) and iproute2
|
||||
|
||||
[ -z "$1" ] && echo "Error: should be called from udhcpc6" && exit 1
|
||||
|
||||
ACTION="$1"
|
||||
RESOLV_CONF="/run/resolvconf/interfaces/${interface}-ipv6.conf"
|
||||
NTPFILE="/run/chrony/dhcp-sources.d/${interface}-ipv6.sources"
|
||||
|
||||
[ -n "$metric" ] || metric=5
|
||||
|
||||
if [ -z "${IF_WAIT_DELAY}" ]; then
|
||||
IF_WAIT_DELAY=10
|
||||
fi
|
||||
|
||||
log()
|
||||
{
|
||||
logger -I $$ -t udhcpc6 -p user.notice "${interface}: $*"
|
||||
}
|
||||
|
||||
dbg()
|
||||
{
|
||||
logger -I $$ -t udhcpc6 -p user.debug "${interface}: $*"
|
||||
}
|
||||
|
||||
err()
|
||||
{
|
||||
logger -I $$ -t udhcpc6 -p user.err "${interface}: $*"
|
||||
}
|
||||
|
||||
clr_dhcpv6_addresses()
|
||||
{
|
||||
addrs=$(ip -j addr show dev $interface \
|
||||
| jq -c '.[0].addr_info[] | select(.family == "inet6") | select(.protocol == "dhcp")')
|
||||
|
||||
for addr in $addrs; do
|
||||
ip="$(echo "$addr" | jq -r '."local"')"
|
||||
prefix="$(echo "$addr" | jq -r '."prefixlen"')"
|
||||
log "removing $ip/$prefix"
|
||||
ip addr del "$ip/$prefix" dev "$interface"
|
||||
done
|
||||
}
|
||||
|
||||
log "action $ACTION"
|
||||
case "$ACTION" in
|
||||
deconfig)
|
||||
clr_dhcpv6_addresses
|
||||
|
||||
# drop info from this interface
|
||||
rm -f "$RESOLV_CONF"
|
||||
rm -f "$NTPFILE"
|
||||
;;
|
||||
|
||||
leasefail|nak)
|
||||
# DHCPv6 lease failed - log it
|
||||
err "DHCPv6 lease failed"
|
||||
;;
|
||||
|
||||
renew|bound)
|
||||
# Add IPv6 address if provided (stateful DHCPv6)
|
||||
if [ -n "$ipv6" ]; then
|
||||
if /bin/ip addr add dev $interface $ipv6 proto dhcp; then
|
||||
log "assigned address $ipv6"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Handle delegated prefix (prefix delegation)
|
||||
if [ -n "$ipv6prefix" ]; then
|
||||
log "received delegated prefix $ipv6prefix"
|
||||
# Prefix delegation handling could be added here
|
||||
# For now, just log it - actual routing/distribution
|
||||
# would need additional configuration
|
||||
fi
|
||||
|
||||
# drop info from this interface
|
||||
truncate -s 0 "$RESOLV_CONF"
|
||||
|
||||
# DHCPv6 domain search list (option 24)
|
||||
if [ -n "$search" ]; then
|
||||
dbg "adding search $search"
|
||||
echo "search $search # $interface" >> $RESOLV_CONF
|
||||
fi
|
||||
|
||||
# DHCPv6 DNS servers (option 23)
|
||||
if [ -n "$dns" ]; then
|
||||
for i in $dns ; do
|
||||
dbg "adding dns $i"
|
||||
echo "nameserver $i # $interface" >> $RESOLV_CONF
|
||||
done
|
||||
resolvconf -u
|
||||
fi
|
||||
|
||||
# NTP servers (option 56)
|
||||
if [ -n "$ntpsrv" ]; then
|
||||
truncate -s 0 "$NTPFILE"
|
||||
for srv in $ntpsrv; do
|
||||
dbg "got NTP server $srv"
|
||||
echo "server $srv iburst" >> "$NTPFILE"
|
||||
done
|
||||
chronyc reload sources >/dev/null
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
HOOK_DIR="$0.d"
|
||||
for hook in "${HOOK_DIR}/"*; do
|
||||
[ -f "${hook}" -a -x "${hook}" ] || continue
|
||||
"${hook}" "$ACTION"
|
||||
done
|
||||
|
||||
exit 0
|
||||
@@ -31,6 +31,10 @@ All notable changes to the project are documented in this file.
|
||||
now enabled using a presence container instead of a separate `enabled` leaf
|
||||
- The `enabled` nore for IPv4 autoconf (ZeroConf) has been dropped, `autoconf`
|
||||
is now a presence container. Configuration automatically migrated on upgrade
|
||||
- Add DHCPv6 client support for per-interface IPv6 configuration, augmenting
|
||||
`/interfaces/interface[name]/ipv6/infix-dhcpv6-client:dhcp`, issue #1110
|
||||
- Fix namespace for DHCPv4 client YANG module from `urn:ietf:params:xml:ns:yang`
|
||||
to `urn:infix:params:xml:ns:yang` to properly reflect custom implementation
|
||||
- Improvements to `sdcard.img` generation, useful for developers mostly:
|
||||
- The NanoPi R2S bootloader is now automatically built and uploaded to
|
||||
the [`latest-boot` release][lastest-boot] tag
|
||||
|
||||
+101
-5
@@ -959,16 +959,55 @@ Client Configuration](system.md#ntp-client-configuration) section.
|
||||
|
||||
Multiple address assignment methods are available:
|
||||
|
||||
| **Type** | **Yang Model** | **Description** |
|
||||
|:---------------- |:-------------- |:------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| static | ietf-ip | Static assignment of IPv6 address, e.g., *2001:db8:0:1::1/64* |
|
||||
| link-local | ietf-ip[^2] | (RFC4862) Auto-configured link-local IPv6 address (*fe80::0* prefix + interface identifier, e.g., *fe80::ccd2:82ff:fe52:728b/64*) |
|
||||
| global auto-conf | ietf-ip | (RFC4862) Auto-configured (stateless) global IPv6 address (prefix from router + interface identifier, e.g., *2001:db8:0:1:ccd2:82ff:fe52:728b/64* |
|
||||
| **Type** | **Yang Model** | **Description** |
|
||||
|:---------------- |:-------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| static | ietf-ip | Static assignment of IPv6 address, e.g., *2001:db8:0:1::1/64* |
|
||||
| link-local | ietf-ip[^2] | (RFC4862) Auto-configured link-local IPv6 address (*fe80::0* prefix + interface identifier, e.g., *fe80::ccd2:82ff:fe52:728b/64*) |
|
||||
| global auto-conf | ietf-ip | (RFC4862) Auto-configured (stateless) global IPv6 address (prefix from router + interface identifier, e.g., *2001:db8:0:1:ccd2:82ff:fe52:728b/64* |
|
||||
| dhcp | infix-dhcpv6-client | Assignment of IPv6 address by DHCPv6 server, e.g., *2001:db8::42/128* |
|
||||
|
||||
Both for *link-local* and *global auto-configuration*, it is possible
|
||||
to auto-configure using a random suffix instead of the interface
|
||||
identifier.
|
||||
|
||||
> [!NOTE]
|
||||
> The DHCPv6 address method is only available for *LAN* interfaces
|
||||
> (Ethernet, virtual Ethernet (veth), bridge, link aggregates, etc.)
|
||||
|
||||
Supported DHCPv6 (request) options, configurability (Cfg) and defaults,
|
||||
are listed below. Configurable options can be disabled on a per client
|
||||
interface basis, some options, like `client-id` and `client-fqdn`, are
|
||||
possible to set the value of as well.
|
||||
|
||||
| **Opt** | **Name** | **Cfg** | **Description** |
|
||||
|---------|----------------------------|---------|--------------------------------------------------------|
|
||||
| 1 | `client-id` | Yes | Client identifier (DUID), auto-generated by default |
|
||||
| 2 | `server-id` | Yes | Server identifier (DUID) |
|
||||
| 23 | `dns-server` | Yes | DNS recursive name servers, static ones take precedence|
|
||||
| 24 | `domain-search` | Yes | Domain search list |
|
||||
| 25 | `ia-pd` | Yes | Prefix delegation for downstream networks |
|
||||
| 31 | `sntp-server` | Yes | Simple Network Time Protocol servers |
|
||||
| 32 | `information-refresh-time` | Yes | Refresh time for stateless DHCPv6 |
|
||||
| 39 | `client-fqdn` | Yes | Client FQDN, request DNS update from server |
|
||||
| 56 | `ntp-server` | Yes | NTP time servers, static ones take precedence |
|
||||
| | | | |
|
||||
|
||||
**Default:** `dns-server`, `domain-search`, `ntp-server`
|
||||
|
||||
DHCPv6 supports both **stateful** (address assignment) and **stateless**
|
||||
(information-only) modes:
|
||||
|
||||
- **Stateful DHCPv6**: The server assigns IPv6 addresses to clients. This is
|
||||
the default mode when enabling the DHCPv6 client.
|
||||
- **Stateless DHCPv6**: Used with SLAAC (Stateless Address Autoconfiguration)
|
||||
when only configuration information (DNS, NTP, etc.) is needed. Enable with
|
||||
the `information-only` setting.
|
||||
|
||||
When configuring a DHCPv6 client, ensure that the NTP client is enabled
|
||||
for the `ntp-server` DHCPv6 option to be processed correctly. If the
|
||||
NTP client is not enabled, any NTP servers provided by the DHCPv6 server
|
||||
will be ignored. For details on how to enable the NTP client, see the
|
||||
[NTP Client Configuration](system.md#ntp-client-configuration) section.
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -1080,6 +1119,63 @@ Other useful DHCP options include:
|
||||
|
||||
For advanced usage with vendor-specific options, see the YANG model.
|
||||
|
||||
#### Use of DHCPv6 for IPv6 address assignment
|
||||
|
||||

|
||||
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit interface eth0 ipv6
|
||||
admin@example:/config/interface/eth0/ipv6/> set dhcp
|
||||
admin@example:/config/interface/eth0/ipv6/> leave
|
||||
admin@example:/> show interface
|
||||
INTERFACE PROTOCOL STATE DATA
|
||||
eth0 ethernet UP 02:00:00:00:00:00
|
||||
ipv6 2001:db8::42/128 (dhcp)
|
||||
ipv6 fe80::ff:fe00:0/64 (link-layer)
|
||||
lo ethernet UP 00:00:00:00:00:00
|
||||
ipv4 127.0.0.1/8 (static)
|
||||
ipv6 ::1/128 (static)
|
||||
admin@example:/>
|
||||
|
||||
The resulting address (2001:db8::42/128) is of type *dhcp*.
|
||||
|
||||
To configure DHCPv6 client options, such as requesting prefix delegation
|
||||
for downstream networks, you can specify options:
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit interface eth0 ipv6 dhcp
|
||||
admin@example:/config/interface/eth0/ipv6/dhcp/> set option ia-pd
|
||||
admin@example:/config/interface/eth0/ipv6/dhcp/> set option dns-server
|
||||
admin@example:/config/interface/eth0/ipv6/dhcp/> show
|
||||
option dns-server;
|
||||
option ia-pd;
|
||||
admin@example:/config/interface/eth0/ipv6/dhcp/> leave
|
||||
admin@example:/>
|
||||
```
|
||||
|
||||
For stateless DHCPv6 (used with SLAAC to get only configuration information):
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit interface eth0 ipv6 dhcp
|
||||
admin@example:/config/interface/eth0/ipv6/dhcp/> set information-only true
|
||||
admin@example:/config/interface/eth0/ipv6/dhcp/> show
|
||||
information-only true;
|
||||
option dns-server;
|
||||
option domain-search;
|
||||
admin@example:/config/interface/eth0/ipv6/dhcp/> leave
|
||||
admin@example:/>
|
||||
```
|
||||
|
||||
Other useful DHCPv6 options include:
|
||||
|
||||
- `duid` - Set a specific DHCPv6 Unique Identifier (auto-generated by default)
|
||||
- `client-fqdn` - Request the server to update DNS records with client's FQDN
|
||||
- `route-preference` - Set the administrative distance for DHCPv6-learned routes (default: 5)
|
||||
|
||||
For advanced usage with vendor-specific options, see the YANG model.
|
||||
|
||||
#### Disabling IPv6 link-local address(es)
|
||||
|
||||
The (only) way to disable IPv6 link-local addresses is by disabling IPv6
|
||||
|
||||
@@ -45,6 +45,7 @@ confd_plugin_la_SOURCES = \
|
||||
ietf-routing.c \
|
||||
infix-dhcp-common.c \
|
||||
infix-dhcp-client.c \
|
||||
infix-dhcpv6-client.c \
|
||||
infix-dhcp-server.c \
|
||||
infix-factory.c \
|
||||
infix-firewall.c \
|
||||
|
||||
@@ -198,6 +198,10 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod
|
||||
if ((rc = infix_dhcp_client_change(session, config, diff, event, confd)))
|
||||
goto free_diff;
|
||||
|
||||
/* infix-dhcpv6-client*/
|
||||
if ((rc = infix_dhcpv6_client_change(session, config, diff, event, confd)))
|
||||
goto free_diff;
|
||||
|
||||
/* ietf-keystore */
|
||||
if ((rc = ietf_keystore_change(session, config, diff, event, confd)))
|
||||
goto free_diff;
|
||||
@@ -428,10 +432,6 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
|
||||
goto err;
|
||||
|
||||
rc = infix_dhcp_server_candidate_init(&confd);
|
||||
if (rc)
|
||||
goto err;
|
||||
|
||||
rc = infix_dhcp_client_candidate_init(&confd);
|
||||
if (rc)
|
||||
goto err;
|
||||
/* YOUR_INIT GOES HERE */
|
||||
|
||||
@@ -204,12 +204,11 @@ static inline int infix_containers_rpc_init(struct confd *confd) { return 0; }
|
||||
static inline int infix_containers_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd) { return 0; }
|
||||
#endif
|
||||
|
||||
/* infix-dhcp-common.c */
|
||||
int dhcp_option_lookup(const struct lyd_node *id);
|
||||
|
||||
/* infix-dhcp-client.c */
|
||||
int infix_dhcp_client_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
|
||||
int infix_dhcp_client_candidate_init(struct confd *confd);
|
||||
|
||||
/* infix-dhcpv6-client.c */
|
||||
int infix_dhcpv6_client_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
|
||||
|
||||
/* infix-dhcp-server.c */
|
||||
int infix_dhcp_server_candidate_init(struct confd *confd);
|
||||
|
||||
@@ -160,6 +160,10 @@ static int ifchange_cand(sr_session_ctx_t *session, uint32_t sub_id, const char
|
||||
if (err)
|
||||
break;
|
||||
|
||||
err = ifchange_cand_infer_dhcp(session, new->xpath);
|
||||
if (err)
|
||||
break;
|
||||
|
||||
err = cni_ifchange_cand_infer_type(session, new->xpath);
|
||||
if (err)
|
||||
break;
|
||||
|
||||
@@ -144,6 +144,9 @@ int ifchange_cand_infer_vlan(sr_session_ctx_t *session, const char *path);
|
||||
int vlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
|
||||
int vlan_add_deps(struct lyd_node *cif);
|
||||
|
||||
/* infix-dhcp-common.c */
|
||||
int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *path);
|
||||
|
||||
/* infix-if-vxlan.c */
|
||||
int vxlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <srx/srx_val.h>
|
||||
|
||||
#include "core.h"
|
||||
#include "infix-dhcp-common.h"
|
||||
|
||||
#define ARPING_MSEC 1000
|
||||
#define MODULE "infix-dhcp-client"
|
||||
#define XPATH "/ietf-interfaces:interfaces/interface/ietf-ip:ipv4/infix-dhcp-client:dhcp"
|
||||
@@ -48,140 +50,6 @@ static char *ip_cache(const char *ifname, char *str, size_t len)
|
||||
return str;
|
||||
}
|
||||
|
||||
static char *hostname(struct lyd_node *cfg, char *str, size_t len)
|
||||
{
|
||||
struct lyd_node *node;
|
||||
const char *hostname;
|
||||
char *ptr;
|
||||
|
||||
node = lydx_get_xpathf(cfg, "/ietf-system:system/hostname");
|
||||
if (!node)
|
||||
return NULL;
|
||||
|
||||
hostname = lyd_get_value(node);
|
||||
if (!hostname || hostname[0] == 0)
|
||||
return NULL;
|
||||
|
||||
ptr = strchr(hostname, '.');
|
||||
if (ptr)
|
||||
*ptr = 0;
|
||||
|
||||
snprintf(str, len, "-x hostname:%s ", hostname);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
static char *fqdn(const char *val, char *str, size_t len)
|
||||
{
|
||||
snprintf(str, len, "-F \"%s\" ", val);
|
||||
return str;
|
||||
}
|
||||
|
||||
static char *os_name_version(char *str, size_t len)
|
||||
{
|
||||
char *val;
|
||||
|
||||
if (!str || !len)
|
||||
return NULL;
|
||||
|
||||
str[0] = 0;
|
||||
|
||||
val = fgetkey("/etc/os-release", "NAME");
|
||||
if (val)
|
||||
snprintf(str, len, "-V \"%.32s ", val);
|
||||
|
||||
val = fgetkey("/etc/os-release", "VERSION");
|
||||
if (val) {
|
||||
strlcat(str, val, len);
|
||||
strlcat(str, "\"", len);
|
||||
}
|
||||
|
||||
if (strlen(str) > 0 && str[strlen(str) - 1] != '"') {
|
||||
str[0] = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
static char *compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id,
|
||||
const char *val, const char *hex, char *option, size_t len)
|
||||
{
|
||||
const char *name = lyd_get_value(id);
|
||||
int num = dhcp_option_lookup(id);
|
||||
|
||||
if (num == -1) {
|
||||
ERROR("Failed looking up DHCP client %s option %s, skipping.", ifname, name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (val || hex) {
|
||||
switch (num) {
|
||||
case 81: /* fqdn */
|
||||
if (!val)
|
||||
return NULL;
|
||||
return fqdn(val, option, len);
|
||||
case 12: /* hostname */
|
||||
if (val && !strcmp(val, "auto"))
|
||||
return hostname(cfg, option, len);
|
||||
/* fallthrough */
|
||||
default:
|
||||
if (hex) {
|
||||
snprintf(option, len, "-x %d:", num);
|
||||
strlcat(option, hex, len);
|
||||
strlcat(option, " ", len);
|
||||
} else {
|
||||
/* string value */
|
||||
snprintf(option, len, "-x %d:'\"%s\"' ", num, val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
struct { int num; char *(*cb)(const char *, char *, size_t); } opt[] = {
|
||||
{ 50, ip_cache }, /* address */
|
||||
{ 81, NULL }, /* fqdn */
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < NELEMS(opt); i++) {
|
||||
if (num != opt[i].num)
|
||||
continue;
|
||||
|
||||
if (!opt[i].cb || !opt[i].cb(ifname, option, len))
|
||||
return NULL;
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
snprintf(option, len, "-O %d ", num);
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
static char *compose_options(struct lyd_node *cfg, const char *ifname, char **options,
|
||||
struct lyd_node *id, const char *val, const char *hex)
|
||||
{
|
||||
char opt[300];
|
||||
|
||||
if (!compose_option(cfg, ifname, id, val, hex, opt, sizeof(opt)))
|
||||
return *options;
|
||||
|
||||
if (*options) {
|
||||
char *opts;
|
||||
|
||||
opts = realloc(*options, strlen(*options) + strlen(opt) + 1);
|
||||
if (!opts) {
|
||||
ERROR("failed reallocating options: %s", strerror(errno));
|
||||
free(*options);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*options = strcat(opts, opt);
|
||||
} else
|
||||
*options = strdup(opt);
|
||||
|
||||
return *options;
|
||||
}
|
||||
|
||||
static char *fallback_options(const char *ifname)
|
||||
{
|
||||
@@ -206,7 +74,7 @@ static char *dhcp_options(const char *ifname, struct lyd_node *cfg)
|
||||
const char *val = lydx_get_cattr(option, "value");
|
||||
const char *hex = lydx_get_cattr(option, "hex");
|
||||
|
||||
options = compose_options(cfg, ifname, &options, id, val, hex);
|
||||
options = dhcp_compose_options(cfg, ifname, &options, id, val, hex, ip_cache);
|
||||
}
|
||||
|
||||
return options ?: fallback_options(ifname);
|
||||
@@ -244,7 +112,7 @@ static void add(const char *ifname, struct lyd_node *cfg)
|
||||
|
||||
options = dhcp_options(ifname, cfg);
|
||||
|
||||
os_name_version(vendor, sizeof(vendor));
|
||||
dhcp_os_name_version(vendor, sizeof(vendor));
|
||||
|
||||
fp = fopenf("w", "/etc/finit.d/available/dhcp-client-%s.conf", ifname);
|
||||
if (!fp) {
|
||||
@@ -331,89 +199,3 @@ int infix_dhcp_client_change(sr_session_ctx_t *session, struct lyd_node *config,
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
/*
|
||||
* Default DHCP options for udhcpc, from networking/udhcp/common.c OPTION_REQ
|
||||
*/
|
||||
static void infer_options(sr_session_ctx_t *session, const char *xpath)
|
||||
{
|
||||
const char *opt[] = {
|
||||
"netmask",
|
||||
"broadcast",
|
||||
"router",
|
||||
"domain",
|
||||
"hostname", /* server may use this to register our current name */
|
||||
"dns-server",
|
||||
"ntp-server" /* will not be activated unless ietf-system also is */
|
||||
};
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < NELEMS(opt); i++)
|
||||
srx_set_item(session, NULL, 0, "%s/option[id='%s']", xpath, opt[i]);
|
||||
}
|
||||
|
||||
static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
|
||||
const char *xpath, sr_event_t event, unsigned request_id, void *priv)
|
||||
{
|
||||
sr_change_iter_t *iter;
|
||||
sr_change_oper_t op;
|
||||
sr_val_t *old, *new;
|
||||
sr_error_t err;
|
||||
|
||||
switch (event) {
|
||||
case SR_EV_UPDATE:
|
||||
case SR_EV_CHANGE:
|
||||
break;
|
||||
default:
|
||||
return SR_ERR_OK;
|
||||
}
|
||||
|
||||
err = sr_dup_changes_iter(session, XPATH "//*", &iter);
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
while (sr_get_change_next(session, iter, &op, &old, &new) == SR_ERR_OK) {
|
||||
char *xpath, *ptr;
|
||||
size_t cnt = 0;
|
||||
|
||||
switch (op) {
|
||||
case SR_OP_CREATED:
|
||||
case SR_OP_MODIFIED:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
xpath = strdupa(new->xpath);
|
||||
if (!xpath) {
|
||||
ERRNO("Failed strdupa()");
|
||||
return SR_ERR_SYS;
|
||||
}
|
||||
|
||||
if ((ptr = strstr(xpath, "]/")) == NULL)
|
||||
continue;
|
||||
ptr[1] = 0;
|
||||
|
||||
err = srx_nitems(session, &cnt, "%s/option", xpath);
|
||||
if (err || cnt) {
|
||||
continue;
|
||||
}
|
||||
|
||||
infer_options(session, xpath);
|
||||
}
|
||||
|
||||
sr_free_change_iter(iter);
|
||||
return SR_ERR_OK;
|
||||
}
|
||||
|
||||
int infix_dhcp_client_candidate_init(struct confd *confd)
|
||||
{
|
||||
int rc;
|
||||
|
||||
REGISTER_CHANGE(confd->cand, MODULE, XPATH"//.", SR_SUBSCR_UPDATE, cand, confd, &confd->sub);
|
||||
|
||||
return SR_ERR_OK;
|
||||
fail:
|
||||
ERROR("init failed: %s", sr_strerror(rc));
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#include <ctype.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/sysinfo.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <srx/common.h>
|
||||
#include <srx/lyx.h>
|
||||
#include <srx/srx_val.h>
|
||||
|
||||
#include <libyang/libyang.h>
|
||||
#include "core.h"
|
||||
#include "infix-dhcp-common.h"
|
||||
|
||||
int dhcp_option_lookup(const struct lyd_node *id)
|
||||
{
|
||||
@@ -43,8 +55,238 @@ int dhcp_option_lookup(const struct lyd_node *id)
|
||||
val = strtol(name, &endptr, 10);
|
||||
if (*endptr == 0 && val > 0 && val < 255)
|
||||
return (int)val;
|
||||
} else if (type->basetype == LY_TYPE_UINT16) {
|
||||
char *endptr;
|
||||
long val;
|
||||
|
||||
val = strtol(name, &endptr, 10);
|
||||
if (*endptr == 0 && val > 0 && val < 65536)
|
||||
return (int)val;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *dhcp_hostname(struct lyd_node *cfg, char *str, size_t len)
|
||||
{
|
||||
struct lyd_node *node;
|
||||
const char *hostname;
|
||||
char *ptr;
|
||||
|
||||
node = lydx_get_xpathf(cfg, "/ietf-system:system/hostname");
|
||||
if (!node)
|
||||
return NULL;
|
||||
|
||||
hostname = lyd_get_value(node);
|
||||
if (!hostname || hostname[0] == 0)
|
||||
return NULL;
|
||||
|
||||
ptr = strchr(hostname, '.');
|
||||
if (ptr)
|
||||
*ptr = 0;
|
||||
|
||||
snprintf(str, len, "-x hostname:%s ", hostname);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
char *dhcp_fqdn(const char *val, char *str, size_t len)
|
||||
{
|
||||
snprintf(str, len, "-F \"%s\" ", val);
|
||||
return str;
|
||||
}
|
||||
|
||||
char *dhcp_os_name_version(char *str, size_t len)
|
||||
{
|
||||
char *val;
|
||||
|
||||
if (!str || !len)
|
||||
return NULL;
|
||||
|
||||
str[0] = 0;
|
||||
|
||||
val = fgetkey("/etc/os-release", "NAME");
|
||||
if (val)
|
||||
snprintf(str, len, "-V \"%.32s ", val);
|
||||
|
||||
val = fgetkey("/etc/os-release", "VERSION");
|
||||
if (val) {
|
||||
strlcat(str, val, len);
|
||||
strlcat(str, "\"", len);
|
||||
}
|
||||
|
||||
if (strlen(str) > 0 && str[strlen(str) - 1] != '"') {
|
||||
str[0] = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
char *dhcp_compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id,
|
||||
const char *val, const char *hex, char *option, size_t len,
|
||||
char *(*ip_cache_cb)(const char *, char *, size_t))
|
||||
{
|
||||
const char *name = lyd_get_value(id);
|
||||
int num = dhcp_option_lookup(id);
|
||||
|
||||
if (num == -1) {
|
||||
ERROR("Failed looking up DHCP client %s option %s, skipping.", ifname, name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (val || hex) {
|
||||
switch (num) {
|
||||
case 81: /* fqdn */
|
||||
if (!val)
|
||||
return NULL;
|
||||
return dhcp_fqdn(val, option, len);
|
||||
case 12: /* hostname */
|
||||
if (val && !strcmp(val, "auto"))
|
||||
return dhcp_hostname(cfg, option, len);
|
||||
/* fallthrough */
|
||||
default:
|
||||
if (hex) {
|
||||
snprintf(option, len, "-x %d:", num);
|
||||
strlcat(option, hex, len);
|
||||
strlcat(option, " ", len);
|
||||
} else {
|
||||
/* string value */
|
||||
snprintf(option, len, "-x %d:'\"%s\"' ", num, val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
struct { int num; char *(*cb)(const char *, char *, size_t); } opt[] = {
|
||||
{ 50, ip_cache_cb }, /* address */
|
||||
{ 81, NULL }, /* fqdn */
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < NELEMS(opt); i++) {
|
||||
if (num != opt[i].num)
|
||||
continue;
|
||||
|
||||
if (!opt[i].cb || !opt[i].cb(ifname, option, len))
|
||||
return NULL;
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
snprintf(option, len, "-O %d ", num);
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
char *dhcp_compose_options(struct lyd_node *cfg, const char *ifname, char **options,
|
||||
struct lyd_node *id, const char *val, const char *hex,
|
||||
char *(*ip_cache_cb)(const char *, char *, size_t))
|
||||
{
|
||||
char opt[300];
|
||||
|
||||
if (!dhcp_compose_option(cfg, ifname, id, val, hex, opt, sizeof(opt), ip_cache_cb))
|
||||
return *options;
|
||||
|
||||
if (*options) {
|
||||
char *opts;
|
||||
|
||||
opts = realloc(*options, strlen(*options) + strlen(opt) + 1);
|
||||
if (!opts) {
|
||||
ERROR("failed reallocating options: %s", strerror(errno));
|
||||
free(*options);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*options = strcat(opts, opt);
|
||||
} else
|
||||
*options = strdup(opt);
|
||||
|
||||
return *options;
|
||||
}
|
||||
|
||||
/*
|
||||
* Default DHCP options for udhcpc, from networking/udhcp/common.c OPTION_REQ
|
||||
*/
|
||||
static void infer_options_v4(sr_session_ctx_t *session, const char *xpath)
|
||||
{
|
||||
const char *opt[] = {
|
||||
"netmask",
|
||||
"broadcast",
|
||||
"router",
|
||||
"domain",
|
||||
"hostname", /* server may use this to register our current name */
|
||||
"dns-server",
|
||||
"ntp-server" /* will not be activated unless ietf-system also is */
|
||||
};
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < NELEMS(opt); i++)
|
||||
srx_set_item(session, NULL, 0, "%s/option[id='%s']", xpath, opt[i]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Default DHCPv6 options
|
||||
* Note: udhcpc6 only supports dns-server (dns) and domain-search (search) with string names.
|
||||
* Other options use numeric codes, which dhcp_compose_option() handles automatically.
|
||||
*/
|
||||
static void infer_options_v6(sr_session_ctx_t *session, const char *xpath)
|
||||
{
|
||||
const char *opt[] = {
|
||||
"dns-server", /* option 23 - udhcpc6: -O dns */
|
||||
"domain-search", /* option 24 - udhcpc6: -O search */
|
||||
"client-fqdn", /* option 39 - udhcpc6: -O 39 (equivalent to DHCPv4 hostname) */
|
||||
"ntp-server" /* option 56 - udhcpc6: -O 56 */
|
||||
};
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < NELEMS(opt); i++)
|
||||
srx_set_item(session, NULL, 0, "%s/option[id='%s']", xpath, opt[i]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Called from ietf-interfaces.c ifchange_cand() to infer DHCP options
|
||||
* for both DHCPv4 and DHCPv6 client configurations
|
||||
*/
|
||||
int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *xpath)
|
||||
{
|
||||
sr_error_t err = SR_ERR_OK;
|
||||
char *path, *ptr;
|
||||
size_t cnt = 0;
|
||||
|
||||
/* Extract path up to and including the dhcp container */
|
||||
path = strdup(xpath);
|
||||
if (!path)
|
||||
return SR_ERR_SYS;
|
||||
|
||||
/* Find the dhcp container in the path */
|
||||
ptr = strstr(path, ":dhcp");
|
||||
if (!ptr) {
|
||||
free(path);
|
||||
return SR_ERR_OK;
|
||||
}
|
||||
|
||||
/* Move past ":dhcp" to find end of container name */
|
||||
ptr += 5; /* strlen(":dhcp") */
|
||||
|
||||
/* If there's more after dhcp (like /arping), truncate it */
|
||||
if (*ptr == '/')
|
||||
*ptr = '\0';
|
||||
|
||||
/* Check if options already exist */
|
||||
err = srx_nitems(session, &cnt, "%s/option", path);
|
||||
if (err || cnt) {
|
||||
ERROR("%s(): no %s/options err %d cnt %zu", __func__, path, err, cnt);
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Infer options based on IPv4 or IPv6 */
|
||||
if (strstr(path, ":ipv4/"))
|
||||
infer_options_v4(session, path);
|
||||
else if (strstr(path, ":ipv6/"))
|
||||
infer_options_v6(session, path);
|
||||
|
||||
out:
|
||||
free(path);
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#ifndef INFIX_DHCP_COMMON_H_
|
||||
#define INFIX_DHCP_COMMON_H_
|
||||
|
||||
#include <libyang/libyang.h>
|
||||
|
||||
/* DHCP option lookup and composition helpers */
|
||||
int dhcp_option_lookup(const struct lyd_node *id);
|
||||
char *dhcp_hostname(struct lyd_node *cfg, char *str, size_t len);
|
||||
char *dhcp_fqdn(const char *val, char *str, size_t len);
|
||||
char *dhcp_os_name_version(char *str, size_t len);
|
||||
char *dhcp_compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id,
|
||||
const char *val, const char *hex, char *option, size_t len,
|
||||
char *(*ip_cache_cb)(const char *, char *, size_t));
|
||||
char *dhcp_compose_options(struct lyd_node *cfg, const char *ifname, char **options,
|
||||
struct lyd_node *id, const char *val, const char *hex,
|
||||
char *(*ip_cache_cb)(const char *, char *, size_t));
|
||||
|
||||
#endif /* INFIX_DHCP_COMMON_H_ */
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <ifaddrs.h>
|
||||
|
||||
#include <srx/common.h>
|
||||
#include <srx/lyx.h>
|
||||
#include <srx/srx_val.h>
|
||||
|
||||
#include "core.h"
|
||||
#include <ifaddrs.h>
|
||||
#include "infix-dhcp-common.h"
|
||||
|
||||
#define MODULE "infix-dhcp-server"
|
||||
#define ROOT_XPATH "/infix-dhcp-server:"
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/sysinfo.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <srx/common.h>
|
||||
#include <srx/lyx.h>
|
||||
#include <srx/srx_val.h>
|
||||
|
||||
#include "core.h"
|
||||
#include "infix-dhcp-common.h"
|
||||
|
||||
#define MODULE "infix-dhcpv6-client"
|
||||
#define XPATH "/ietf-interfaces:interfaces/interface/ietf-ip:ipv6/infix-dhcpv6-client:dhcp"
|
||||
|
||||
static char *fallback_options_v6(const char *ifname)
|
||||
{
|
||||
/* dns-server, domain-search, client-fqdn, ntp-server */
|
||||
const char *defaults = "-O 23 -O 24 -O 39 -O 56 ";
|
||||
|
||||
return strdup(defaults);
|
||||
}
|
||||
|
||||
static char *dhcp_options_v6(const char *ifname, struct lyd_node *cfg, bool *request_pd)
|
||||
{
|
||||
struct lyd_node *option;
|
||||
char *options = NULL;
|
||||
|
||||
*request_pd = false;
|
||||
|
||||
LYX_LIST_FOR_EACH(lyd_child(cfg), option, "option") {
|
||||
struct lyd_node *id = lydx_get_child(option, "id");
|
||||
const char *val = lydx_get_cattr(option, "value");
|
||||
const char *hex = lydx_get_cattr(option, "hex");
|
||||
const char *name = lyd_get_value(id);
|
||||
int num = dhcp_option_lookup(id);
|
||||
|
||||
/* ia-pd option requires -d flag, not -O 25 */
|
||||
if (num == 25 || (name && !strcmp(name, "ia-pd"))) {
|
||||
*request_pd = true;
|
||||
continue; /* Don't add to options string */
|
||||
}
|
||||
|
||||
options = dhcp_compose_options(cfg, ifname, &options, id, val, hex, NULL);
|
||||
}
|
||||
|
||||
return options ?: fallback_options_v6(ifname);
|
||||
}
|
||||
|
||||
static void add_v6(const char *ifname, struct lyd_node *cfg)
|
||||
{
|
||||
const char *metric = lydx_get_cattr(cfg, "route-preference");
|
||||
const char *duid = lydx_get_cattr(cfg, "duid");
|
||||
char *client_duid = NULL, *options = NULL;
|
||||
const char *action = "disable";
|
||||
char info_only[4] = { 0 };
|
||||
char prefix_del[4] = { 0 };
|
||||
bool request_pd = false;
|
||||
FILE *fp;
|
||||
|
||||
if (lydx_is_enabled(cfg, "information-only"))
|
||||
strlcpy(info_only, "-l ", sizeof(info_only));
|
||||
|
||||
if (duid && duid[0]) {
|
||||
size_t len = strlen(duid) + 32;
|
||||
|
||||
client_duid = malloc(len);
|
||||
if (!client_duid)
|
||||
goto generr;
|
||||
|
||||
snprintf(client_duid, len, "-x 1:%s ", duid);
|
||||
}
|
||||
|
||||
options = dhcp_options_v6(ifname, cfg, &request_pd);
|
||||
if (request_pd)
|
||||
strlcpy(prefix_del, "-d ", sizeof(prefix_del));
|
||||
|
||||
fp = fopenf("w", "/etc/finit.d/available/dhcpv6-client-%s.conf", ifname);
|
||||
if (!fp) {
|
||||
generr:
|
||||
ERRNO("failed creating DHCPv6 client %s: %s", ifname, strerror(errno));
|
||||
goto err;
|
||||
}
|
||||
|
||||
fprintf(fp, "# Generated by Infix confd\n");
|
||||
fprintf(fp, "metric=%s\n", metric);
|
||||
fprintf(fp, "service <!> name:dhcpv6-client :%s <net/%s/running> \\\n"
|
||||
" [2345] udhcpc6 -f -p /run/dhcpv6-client-%s.pid -t 3 -T 5 -A 30 -S -R \\\n"
|
||||
" %s%s%s%s \\\n"
|
||||
" -i %s %s \\\n"
|
||||
" -- DHCPv6 client @%s\n",
|
||||
ifname, ifname, ifname,
|
||||
info_only, prefix_del,
|
||||
options ? "-o " : "", options ?: "",
|
||||
ifname, client_duid ?: "", ifname);
|
||||
fclose(fp);
|
||||
action = "enable";
|
||||
err:
|
||||
systemf("initctl -bfqn %s dhcpv6-client-%s", action, ifname);
|
||||
if (options)
|
||||
free(options);
|
||||
if (client_duid)
|
||||
free(client_duid);
|
||||
}
|
||||
|
||||
static void del_v6(const char *ifname)
|
||||
{
|
||||
systemf("initctl -bfq delete dhcpv6-client-%s", ifname);
|
||||
}
|
||||
|
||||
int infix_dhcpv6_client_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff,
|
||||
sr_event_t event, struct confd *confd)
|
||||
{
|
||||
struct lyd_node *ifaces, *difaces, *iface, *diface, *ipv6, *dhcp, *ddhcp;
|
||||
sr_error_t err = 0;
|
||||
|
||||
if (event != SR_EV_DONE)
|
||||
return SR_ERR_OK;
|
||||
|
||||
ifaces = lydx_get_descendant(config, "interfaces", "interface", NULL);
|
||||
difaces = lydx_get_descendant(diff, "interfaces", "interface", NULL);
|
||||
|
||||
/* find the modified interfaces */
|
||||
LYX_LIST_FOR_EACH(difaces, diface, "interface") {
|
||||
const char *ifname = lydx_get_cattr(diface, "name");
|
||||
struct lyd_node *dipv6;
|
||||
|
||||
dipv6 = lydx_get_descendant(lyd_child(diface), "ipv6", NULL);
|
||||
if (!dipv6)
|
||||
continue;
|
||||
|
||||
ddhcp = lydx_get_descendant(lyd_child(dipv6), "dhcp", NULL);
|
||||
if (!ddhcp)
|
||||
continue;
|
||||
|
||||
/* Check if dhcp container was deleted */
|
||||
if (lydx_get_op(ddhcp) == LYDX_OP_DELETE) {
|
||||
del_v6(ifname);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Find corresponding interface in config to check if dhcp is present */
|
||||
LYX_LIST_FOR_EACH(ifaces, iface, "interface") {
|
||||
if (strcmp(ifname, lydx_get_cattr(iface, "name")))
|
||||
continue;
|
||||
|
||||
ipv6 = lydx_get_descendant(lyd_child(iface), "ipv6", NULL);
|
||||
if (!ipv6) {
|
||||
del_v6(ifname);
|
||||
break;
|
||||
}
|
||||
|
||||
dhcp = lydx_get_descendant(lyd_child(ipv6), "dhcp", NULL);
|
||||
if (!dhcp)
|
||||
del_v6(ifname);
|
||||
else
|
||||
add_v6(ifname, dhcp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
@@ -29,8 +29,9 @@ MODULES=(
|
||||
"infix-routing@2025-11-12.yang"
|
||||
"ieee802-dot1ab-lldp@2022-03-15.yang"
|
||||
"infix-lldp@2025-05-05.yang"
|
||||
"infix-dhcp-common@2025-01-29.yang"
|
||||
"infix-dhcp-client@2025-11-02.yang"
|
||||
"infix-dhcp-common@2025-11-09.yang"
|
||||
"infix-dhcp-client@2025-11-09.yang"
|
||||
"infix-dhcpv6-client@2025-11-09.yang"
|
||||
"infix-dhcp-server@2025-10-28.yang"
|
||||
"infix-firewall@2025-04-26.yang"
|
||||
"infix-firewall-services@2025-04-26.yang"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module infix-dhcp-client {
|
||||
yang-version 1.1;
|
||||
namespace "urn:ietf:params:xml:ns:yang:dhcp-client";
|
||||
namespace "urn:infix:params:xml:ns:yang:dhcp-client";
|
||||
prefix dhcp-client;
|
||||
|
||||
import ietf-interfaces {
|
||||
@@ -16,6 +16,10 @@ module infix-dhcp-client {
|
||||
contact "kernelkit@googlegroups.com";
|
||||
description "This module implements a DHCPv4 client";
|
||||
|
||||
revision 2025-11-09 {
|
||||
description "Fix namespace to use infix instead of ietf.";
|
||||
reference "internal";
|
||||
}
|
||||
revision 2025-11-02 {
|
||||
description "Migrate DHCP client to ietf-ip augment.
|
||||
- Relocate from /dhcp-client to /interfaces/interface/ipv4/dhcp
|
||||
|
||||
@@ -7,6 +7,10 @@ module infix-dhcp-common {
|
||||
contact "kernelkit@googlegroups.com";
|
||||
description "Shared types between DHCP server and client.";
|
||||
|
||||
revision 2025-11-09 {
|
||||
description "Add DHCPv6 options typedef.";
|
||||
reference "internal";
|
||||
}
|
||||
revision 2025-01-29 {
|
||||
description "Initial revision.";
|
||||
reference "internal";
|
||||
@@ -113,4 +117,73 @@ module infix-dhcp-common {
|
||||
pattern '([0-9a-fA-F]{2}:)*[0-9a-fA-F]{2}';
|
||||
}
|
||||
}
|
||||
|
||||
typedef duid {
|
||||
description "DHCPv6 Unique Identifier (DUID), RFC 8415 section 11.
|
||||
|
||||
A DUID consists of a two-octet type code and a variable length
|
||||
identifier field. The minimum length is 4 octets (type + data).
|
||||
|
||||
Common DUID types:
|
||||
- DUID-LLT (type 1): Link-layer address plus time
|
||||
- DUID-EN (type 2): Enterprise number
|
||||
- DUID-LL (type 3): Link-layer address
|
||||
- DUID-UUID (type 4): UUID
|
||||
|
||||
Format: Colon-separated hex octets, minimum 4 octets.
|
||||
Example: 00:03:00:01:08:00:27:fe:8c:2a (DUID-LL with Ethernet MAC)";
|
||||
type string {
|
||||
pattern '([0-9a-fA-F]{2}:){3,}[0-9a-fA-F]{2}';
|
||||
/* Minimum 4 octets: ([0-9a-fA-F]{2}:){3,} matches at least 3 colons,
|
||||
* plus final [0-9a-fA-F]{2} makes 4 octets total */
|
||||
}
|
||||
}
|
||||
|
||||
typedef options-v6 {
|
||||
description "DHCPv6 options for client.";
|
||||
type union {
|
||||
type uint16 {
|
||||
range "1..65535";
|
||||
}
|
||||
type enumeration {
|
||||
enum client-id {
|
||||
value 1;
|
||||
description "Client Identifier (DUID), RFC 8415.
|
||||
Automatically generated by default.";
|
||||
}
|
||||
enum server-id {
|
||||
value 2;
|
||||
description "Server Identifier (DUID), RFC 8415.";
|
||||
}
|
||||
enum dns-server {
|
||||
value 23;
|
||||
description "DNS recursive name servers, RFC 3646.";
|
||||
}
|
||||
enum domain-search {
|
||||
value 24;
|
||||
description "Domain search list, RFC 3646.";
|
||||
}
|
||||
enum ia-pd {
|
||||
value 25;
|
||||
description "Identity Association for Prefix Delegation, RFC 8415.";
|
||||
}
|
||||
enum ntp-server {
|
||||
value 56;
|
||||
description "NTP time servers, RFC 5908.";
|
||||
}
|
||||
enum client-fqdn {
|
||||
value 39;
|
||||
description "Client Fully Qualified Domain Name, RFC 4704.";
|
||||
}
|
||||
enum information-refresh-time {
|
||||
value 32;
|
||||
description "Information refresh time for stateless DHCPv6, RFC 8415.";
|
||||
}
|
||||
enum sntp-server {
|
||||
value 31;
|
||||
description "Simple Network Time Protocol servers, RFC 4075.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
module infix-dhcpv6-client {
|
||||
yang-version 1.1;
|
||||
namespace "urn:infix:params:xml:ns:yang:dhcpv6-client";
|
||||
prefix dhcpv6-client;
|
||||
|
||||
import ietf-interfaces {
|
||||
prefix if;
|
||||
}
|
||||
import ietf-ip {
|
||||
prefix ip;
|
||||
}
|
||||
import infix-dhcp-common {
|
||||
prefix dhcp;
|
||||
}
|
||||
|
||||
organization "KernelKit";
|
||||
contact "kernelkit@googlegroups.com";
|
||||
description "This module implements a DHCPv6 client";
|
||||
|
||||
revision 2025-11-09 {
|
||||
description "Initial revision:
|
||||
- Augment for /interfaces/interface/ipv6/dhcp
|
||||
- Support for DHCPv6 options, DUID, prefix delegation";
|
||||
reference "rfc8415";
|
||||
}
|
||||
|
||||
/*
|
||||
* Typedefs
|
||||
*/
|
||||
|
||||
typedef route-preference {
|
||||
type uint32;
|
||||
description "This type is used for selecting route preference (distance).";
|
||||
}
|
||||
|
||||
/*
|
||||
* DHCPv6 Client Configuration
|
||||
*/
|
||||
|
||||
augment "/if:interfaces/if:interface/ip:ipv6" {
|
||||
container dhcp {
|
||||
presence "Enable DHCPv6 client for this interface.";
|
||||
description "DHCPv6 client configuration";
|
||||
|
||||
leaf duid {
|
||||
type dhcp:duid;
|
||||
description "Optional DHCPv6 Unique Identifier (DUID), option 1, RFC 8415.
|
||||
|
||||
When omitted, the client automatically generates a DUID
|
||||
based on link-layer address plus time (DUID-LLT) or link-layer
|
||||
address only (DUID-LL), depending on the implementation.
|
||||
|
||||
This setting allows manual configuration of the DUID in
|
||||
hexadecimal format. The DUID type and content are encoded
|
||||
as defined in RFC 8415 section 11.
|
||||
|
||||
For full control of the DUID format, use the generic option
|
||||
list instead, which supports entering raw HEX data.
|
||||
|
||||
Example: 00:03:00:01:08:00:27:fe:8c:2a
|
||||
(DUID-LL with Ethernet address)";
|
||||
}
|
||||
|
||||
leaf information-only {
|
||||
type boolean;
|
||||
must "not(. = 'true') or ../../ip:autoconf/ip:create-global-addresses = 'true' " +
|
||||
"or ../../ip:autoconf/ip:create-temporary-addresses = 'true'" {
|
||||
error-message "IPv6 autoconf (SLAAC) must be enabled when information-only is true";
|
||||
error-app-tag "invalid-dhcpv6-config";
|
||||
}
|
||||
description "Use stateless DHCPv6 (information-request mode).
|
||||
|
||||
When enabled, the client requests configuration information
|
||||
(like DNS servers) but does not request IPv6 addresses or
|
||||
prefixes. This is used with SLAAC (Stateless Address
|
||||
Autoconfiguration) when only additional configuration is
|
||||
needed from DHCPv6.";
|
||||
}
|
||||
|
||||
list option {
|
||||
key "id";
|
||||
description "List of DHCPv6 options to request (and accept).
|
||||
|
||||
The default is an empty list, meaning all supported options. To
|
||||
request specific options, add them here. For prefix delegation,
|
||||
include the ia-pd option in the list.";
|
||||
|
||||
must "not(id = 'client-fqdn' and hex)" {
|
||||
error-message "Client FQDN option must use string format";
|
||||
error-app-tag "invalid-fqdn-format";
|
||||
}
|
||||
|
||||
leaf id {
|
||||
type dhcp:options-v6;
|
||||
description "DHCPv6 option to request from, or inform server of.";
|
||||
}
|
||||
|
||||
choice value {
|
||||
case value {
|
||||
leaf value {
|
||||
description "Optional string value to send to server.
|
||||
|
||||
Example:
|
||||
option:client-fqdn, value:host.example.com
|
||||
|
||||
This will send option 39 with the specified FQDN,
|
||||
requesting the server to update DNS records.";
|
||||
type string;
|
||||
}
|
||||
}
|
||||
case hex {
|
||||
leaf hex {
|
||||
description "Optional binary value in hexadecimal format.
|
||||
|
||||
This allows full control over the option payload,
|
||||
useful for vendor-specific options or custom data.
|
||||
|
||||
Example:
|
||||
option:client-id, hex:00:03:00:01:08:00:27:fe:8c:2a
|
||||
|
||||
This would set a DUID-LL (type 3) with the specified
|
||||
link-layer address.";
|
||||
type dhcp:octet-string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leaf route-preference {
|
||||
type route-preference;
|
||||
default 5;
|
||||
description
|
||||
"The preference (administrative distance) that all DHCPv6 routes
|
||||
are installed with. The default preference (5) is higher (less worth)
|
||||
than static routes, but lower than those learned via dynamic routing
|
||||
protocols, like OSPF.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
infix-dhcpv6-client.yang
|
||||
@@ -2,7 +2,7 @@
|
||||
# shellcheck disable=SC2034,SC2154
|
||||
|
||||
# Current container image
|
||||
INFIX_TEST=ghcr.io/kernelkit/infix-test:2.6
|
||||
INFIX_TEST=ghcr.io/kernelkit/infix-test:2.7
|
||||
|
||||
ixdir=$(readlink -f "$testdir/..")
|
||||
logdir=$(readlink -f "$testdir/.log")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""DHCPv6 Basic
|
||||
|
||||
Enable a DHCPv6 client and verify it requests an IPv6 lease from a
|
||||
DHCPv6 server that is then set on the interface.
|
||||
|
||||
"""
|
||||
|
||||
import infamy
|
||||
import infamy.dhcp
|
||||
import infamy.iface as iface
|
||||
import infamy.route as route
|
||||
from infamy.util import until
|
||||
|
||||
with infamy.Test() as test:
|
||||
SERVER = '2001:db8::1'
|
||||
CLIENT = '2001:db8::42'
|
||||
DOMAIN = 'example.com'
|
||||
VERIFY = 'server.' + DOMAIN
|
||||
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
client = env.attach("client", "mgmt")
|
||||
tgtssh = env.attach("client", "mgmt", "ssh")
|
||||
_, host = env.ltop.xlate("host", "data")
|
||||
_, port = env.ltop.xlate("client", "data")
|
||||
|
||||
with infamy.IsolatedMacVlan(host) as netns:
|
||||
netns.addip(SERVER, prefix_length=64, proto="ipv6")
|
||||
with infamy.dhcp.Server6Dnsmasq(netns,
|
||||
start=CLIENT,
|
||||
end=CLIENT,
|
||||
dns=SERVER,
|
||||
domain=DOMAIN,
|
||||
address=SERVER):
|
||||
|
||||
with test.step("Configure DHCPv6 client"):
|
||||
config = {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": f"{port}",
|
||||
"ipv6": {
|
||||
"enabled": True,
|
||||
"infix-dhcpv6-client:dhcp": {
|
||||
"option": [
|
||||
{"id": "dns-server"},
|
||||
{"id": "domain-search"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
client.put_config_dict("ietf-interfaces", config)
|
||||
|
||||
with test.step(f"Verify client lease for {CLIENT}"):
|
||||
until(lambda: iface.address_exist(client, port, CLIENT, prefix_length=128))
|
||||
|
||||
with test.step("Verify client default route ::/0"):
|
||||
until(lambda: route.ipv6_route_exist(client, "::/0"), attempts=20)
|
||||
|
||||
with test.step("Verify client domain name resolution"):
|
||||
rc = tgtssh.runsh(f"ping -6 -c1 -w20 {VERIFY}")
|
||||
assert rc.returncode == 0, f"Client failed: ping {VERIFY}"
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,23 @@
|
||||
graph "1x2" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+100";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt> mgmt | <data> data }",
|
||||
pos="0,20!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
client [
|
||||
label="{ <mgmt> mgmt | <data> data } | client",
|
||||
pos="200,20!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt -- client:mgmt [requires="mgmt", color=lightgrey]
|
||||
host:data -- client:data [color=black, taillabel="2001:db8::1/64", headlabel="2001:db8::42/128"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""DHCPv6 Prefix Delegation
|
||||
|
||||
Verify DHCPv6 prefix delegation (IA_PD) where a client requests an IPv6
|
||||
prefix from a DHCPv6 server. This is commonly used on WAN interfaces of
|
||||
routers to obtain a prefix for distribution to downstream networks.
|
||||
|
||||
"""
|
||||
|
||||
import infamy, infamy.dhcp
|
||||
import infamy.iface as iface
|
||||
from infamy.util import until
|
||||
import time
|
||||
|
||||
|
||||
def checkrun(dut):
|
||||
"""Check DUT is running DHCPv6 client"""
|
||||
res = dut.runsh(f"pgrep -f 'udhcpc6.*{port}'")
|
||||
# print(f"Checking for udhcpc6: {res.stdout}")
|
||||
if res.stdout.strip() != "":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def checklog(dut):
|
||||
"""Check syslog for prefix delegation message"""
|
||||
rc = dut.runsh("tail -10 /log/syslog | grep 'received delegated prefix'")
|
||||
# print(f"DHCPv6 client logs:\n{res.stdout}")
|
||||
if rc.stdout.strip() != "":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
SERVER = '2001:db8::1'
|
||||
CLIENT = '2001:db8::42'
|
||||
PREFIX = '2001:db8:1::/48'
|
||||
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
client = env.attach("client", "mgmt")
|
||||
tgtssh = env.attach("client", "mgmt", "ssh")
|
||||
_, host = env.ltop.xlate("host", "data")
|
||||
_, port = env.ltop.xlate("client", "data")
|
||||
|
||||
with infamy.IsolatedMacVlan(host) as netns:
|
||||
netns.addip(SERVER, prefix_length=48, proto="ipv6")
|
||||
with infamy.dhcp.Server6Dhcpd(netns=netns,
|
||||
start="2001:db8::100",
|
||||
end="2001:db8::200",
|
||||
prefix="2001:db8:100::",
|
||||
prefix_len=64,
|
||||
dns="2001:db8::1",
|
||||
iface="iface",
|
||||
subnet="2001:db8::/48"):
|
||||
|
||||
with test.step("Configure DHCPv6 client w/ prefix delegation"):
|
||||
config = {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": f"{port}",
|
||||
"ipv6": {
|
||||
"enabled": True,
|
||||
"infix-dhcpv6-client:dhcp": {
|
||||
"option": [
|
||||
{"id": "dns-server"},
|
||||
{"id": "ia-pd"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
client.put_config_dict("ietf-interfaces", config)
|
||||
|
||||
with test.step("Verify DHCPv6 client is running"):
|
||||
until(lambda: checkrun(tgtssh))
|
||||
|
||||
with test.step("Verify prefix delegation in logs"):
|
||||
until(lambda: checklog(tgtssh))
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,23 @@
|
||||
graph "1x2" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+100";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt> mgmt | <data> data }",
|
||||
pos="0,20!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
client [
|
||||
label="{ <mgmt> mgmt | <data> data } | client",
|
||||
pos="200,20!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt -- client:mgmt [requires="mgmt", color=lightgrey]
|
||||
host:data -- client:data [color=black, taillabel="2001:db8::1/64", headlabel="DHCPv6-PD: 2001:db8:1::/48"]
|
||||
}
|
||||
@@ -10,3 +10,9 @@
|
||||
|
||||
- name: DHCP option 121 vs option 3
|
||||
case: client_routes/test.py
|
||||
|
||||
- name: DHCPv6 Basic
|
||||
case: client6_basic/test.py
|
||||
|
||||
- name: DHCPv6 Prefix Delegation
|
||||
case: client6_prefix_delegation/test.py
|
||||
|
||||
@@ -4,6 +4,8 @@ FROM alpine:3.18.0
|
||||
RUN apk add --no-cache \
|
||||
busybox-extras \
|
||||
curl \
|
||||
dhcp-server-vanilla \
|
||||
dnsmasq \
|
||||
e2fsprogs \
|
||||
e2tools \
|
||||
ethtool \
|
||||
|
||||
+33
-23
@@ -4,38 +4,48 @@ Checklist for Updating Infamy Docker Image
|
||||
This directory holds the Dockerfile and any extras needed to build and
|
||||
update the infix-test container image used for the test system.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Remember the final `.` in the `docker build` command, otherwise the
|
||||
> build directory will be test/docker/ and you will get COPY errors!
|
||||
|
||||
Build Checklist
|
||||
---------------
|
||||
|
||||
The following is a checklist/reminder to maintainers for how to update
|
||||
the image, e.g., with missing Alpine packages.
|
||||
the image, e.g., with missing Alpine packages:
|
||||
|
||||
1. Update the Dockerfile
|
||||
|
||||
cd test/docker/
|
||||
edit Dockerfile
|
||||
|
||||
2. Build the new image version, for latest version, see released images
|
||||
here: <https://github.com/kernelkit/infix/pkgs/container/infix-test>
|
||||
in this example we use version 0.4:
|
||||
1. Update the Dockerfile
|
||||
|
||||
cd test/
|
||||
docker build -f docker/Dockerfile -t ghcr.io/kernelkit/infix-test:0.4 .
|
||||
edit docker/Dockerfile
|
||||
|
||||
3. Update the `test/.env` file to use the new version
|
||||
4. Verify your new image works properly (remember to remove your `~/.infix/venv`)
|
||||
5. Send PR to co-maintainer for review
|
||||
2. Build the new image version, for latest version, see released images
|
||||
here: <https://github.com/kernelkit/infix/pkgs/container/infix-test>
|
||||
in this example we use version 2.7:
|
||||
|
||||
docker build -f docker/Dockerfile -t ghcr.io/kernelkit/infix-test:2.7 .
|
||||
|
||||
3. Update the `test/.env` file to use the new version
|
||||
4. Verify your new image works properly (remember to remove your `~/.infix/venv`)
|
||||
5. Send PR to co-maintainer for review
|
||||
|
||||
Upload New Image
|
||||
----------------
|
||||
|
||||
The co-maintainer should then verify themselves before approving the PR.
|
||||
A crucial step to remember is to:
|
||||
|
||||
1. Push the new image version to <https://ghcr.io>. For details on how
|
||||
to do this, see this excellent guide to the [GitHub Container
|
||||
Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry):
|
||||
2. Shorthand after setup (above):
|
||||
1. Push the new image version to <https://ghcr.io>. For details on how
|
||||
to do this, see this excellent guide to the [GitHub Container
|
||||
Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry):
|
||||
2. Shorthand after setup (above):
|
||||
|
||||
echo $CR_PAT | docker login ghcr.io -u troglobit --password-stdin
|
||||
docker push ghcr.io/kernelkit/infix-test:0.4
|
||||
echo $CR_PAT | docker login ghcr.io -u troglobit --password-stdin
|
||||
docker push ghcr.io/kernelkit/infix-test:2.7
|
||||
|
||||
3. Merge the PR to the `main` branch.
|
||||
3. Merge the PR to the `main` branch.
|
||||
|
||||
> **Note:** the co-maintainer may delegate the chore of uploading the
|
||||
> new image to the one who prepared the PR (you), provided of course
|
||||
> they have the access rights to do so.
|
||||
> [!NOTE]
|
||||
> The co-maintainer may delegate the chore of uploading the new image to
|
||||
> the one who prepared the PR (you), provided of course they have the
|
||||
> access rights to do so.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""Start a DHCP server in the background"""
|
||||
import os
|
||||
import tempfile as tf
|
||||
import subprocess
|
||||
|
||||
|
||||
class Server:
|
||||
config_file = '/tmp/udhcpd.conf'
|
||||
@@ -59,3 +62,276 @@ option lease 864000
|
||||
self.process.terminate()
|
||||
self.process.wait()
|
||||
self.process = None
|
||||
|
||||
|
||||
class Server6Dnsmasq:
|
||||
"""DHCPv6 server using dnsmasq"""
|
||||
|
||||
def __init__(self, netns, start=None, end=None, dns=None, domain=None,
|
||||
iface="iface", address=None):
|
||||
"""Initialize DHCPv6 server
|
||||
|
||||
Args:
|
||||
netns: Network namespace to run server in
|
||||
start: Starting IPv6 address for range (e.g., "2001:db8::100")
|
||||
end: Ending IPv6 address for range (e.g., "2001:db8::200")
|
||||
dns: DNS server addresses (list or string)
|
||||
domain: DNS search domain
|
||||
iface: Interface to listen on
|
||||
"""
|
||||
self.process = None
|
||||
self.netns = netns
|
||||
self.iface = iface
|
||||
self.config_file = tf.NamedTemporaryFile(mode='w', prefix='dnsmasq6_',
|
||||
suffix='.conf', delete=False)
|
||||
self.config_path = self.config_file.name
|
||||
self.leases_file = tf.NamedTemporaryFile(mode='w', prefix='dnsmasq6_',
|
||||
suffix='.leases', delete=False)
|
||||
self.leases_path = self.leases_file.name
|
||||
self.leases_file.close()
|
||||
|
||||
self.hosts_file = tf.NamedTemporaryFile(mode='w', delete=False)
|
||||
self.hosts_path = self.hosts_file.name
|
||||
|
||||
self._create_config(start, end, dns, domain, address)
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if os.path.exists(self.config_path):
|
||||
os.unlink(self.config_path)
|
||||
if os.path.exists(self.leases_path):
|
||||
os.unlink(self.leases_path)
|
||||
if os.path.exists(self.hosts_path):
|
||||
os.unlink(self.hosts_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, _, __, ___):
|
||||
self.stop()
|
||||
|
||||
def _create_config(self, start, end, dns, domain, address):
|
||||
"""Create dnsmasq configuration for DHCPv6"""
|
||||
with self.hosts_file:
|
||||
# NOTE: most basic tooling expect the server to reply with
|
||||
# and A record, not just a AAAA record, so a client
|
||||
# like ping, in dhcpv6_basic, will fail even if -6
|
||||
self.hosts_file.write("# Generated by Infamy DHCPv6\n")
|
||||
self.hosts_file.write(f"10.10.0.1 server.{domain}\n") # dummy
|
||||
self.hosts_file.write(f"{address} server.{domain}\n")
|
||||
|
||||
with self.config_file:
|
||||
self.config_file.write("# Generated by Infamy DHCPv6\n")
|
||||
self.config_file.write(f"interface={self.iface}\n")
|
||||
self.config_file.write(f"addn-hosts={self.hosts_path}\n")
|
||||
self.config_file.write(f"domain={domain}\n")
|
||||
self.config_file.write("enable-ra\n")
|
||||
self.config_file.write(f"dhcp-leasefile={self.leases_path}\n")
|
||||
self.config_file.write(f"dhcp-range={self.iface},")
|
||||
|
||||
if start and end:
|
||||
# Stateful DHCPv6 - assign addresses
|
||||
self.config_file.write(f"{start},{end},64,3600\n")
|
||||
else:
|
||||
# Stateless DHCPv6 - only provide options
|
||||
# TODO: needs more testing with ra-names + slaac
|
||||
self.config_file.write(f"::10,::fff,constructor:{self.iface},")
|
||||
self.config_file.write("64,3600\n")
|
||||
|
||||
if dns:
|
||||
if isinstance(dns, list):
|
||||
dns_str = ','.join(dns)
|
||||
else:
|
||||
dns_str = dns
|
||||
self.config_file.write(f"dhcp-option=option6:dns-server,{dns_str}\n")
|
||||
|
||||
if domain:
|
||||
self.config_file.write(f"dhcp-option=option6:domain-search,{domain}\n")
|
||||
|
||||
self.config_file.write("log-debug\n")
|
||||
self.config_file.write("log-dhcp\n")
|
||||
|
||||
def start(self):
|
||||
"""Start the DHCPv6 server"""
|
||||
if not os.path.exists(self.config_path):
|
||||
raise Exception("Config file does not exist")
|
||||
|
||||
# Drop DEVNULL redirec to debug
|
||||
cmd = f"dnsmasq --conf-file={self.config_path} --no-daemon"
|
||||
self.process = self.netns.popen(cmd.split(" "),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the DHCPv6 server"""
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process.wait()
|
||||
self.process = None
|
||||
|
||||
class Server6Dhcpd:
|
||||
"""DHCPv6 server using ISC dhcpd with prefix delegation support"""
|
||||
|
||||
def __init__(self, netns, start=None, end=None, prefix=None, prefix_len=64,
|
||||
dns=None, domain=None, iface="iface", address=None, subnet="2001:db8::/48"):
|
||||
"""Initialize DHCPv6 server with ISC dhcpd
|
||||
|
||||
Args:
|
||||
netns: Network namespace to run server in
|
||||
start: Starting IPv6 address for IA_NA range (e.g., "2001:db8::100")
|
||||
end: Ending IPv6 address for IA_NA range (e.g., "2001:db8::200")
|
||||
prefix: IPv6 prefix pool for prefix delegation (e.g., "2001:db8:100::")
|
||||
prefix_len: Length of delegated prefixes (e.g., 64 for /64 prefixes)
|
||||
dns: DNS server addresses (list or string)
|
||||
domain: DNS search domain
|
||||
iface: Interface to listen on
|
||||
address: Server address on the interface (for subnet config)
|
||||
subnet: Subnet declaration (e.g., "2001:db8::/48")
|
||||
"""
|
||||
self.process = None
|
||||
self.netns = netns
|
||||
self.iface = iface
|
||||
self.subnet = subnet
|
||||
|
||||
self.config_file = tf.NamedTemporaryFile(mode='w', prefix='dhcpd6_',
|
||||
suffix='.conf', delete=False)
|
||||
self.config_path = self.config_file.name
|
||||
|
||||
self.leases_file = tf.NamedTemporaryFile(mode='w', prefix='dhcpd6_',
|
||||
suffix='.leases', delete=False)
|
||||
self.leases_path = self.leases_file.name
|
||||
self.leases_file.close()
|
||||
|
||||
self.pid_file = tf.NamedTemporaryFile(mode='w', prefix='dhcpd6_',
|
||||
suffix='.pid', delete=False)
|
||||
self.pid_path = self.pid_file.name
|
||||
self.pid_file.close()
|
||||
|
||||
self._create_config(start, end, prefix, prefix_len, dns, domain)
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if os.path.exists(self.config_path):
|
||||
os.unlink(self.config_path)
|
||||
if os.path.exists(self.leases_path):
|
||||
os.unlink(self.leases_path)
|
||||
if os.path.exists(self.pid_path):
|
||||
os.unlink(self.pid_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, _, __, ___):
|
||||
self.stop()
|
||||
|
||||
def _create_config(self, start, end, prefix, prefix_len, dns, domain):
|
||||
"""Create ISC dhcpd configuration for DHCPv6 with prefix delegation"""
|
||||
with self.config_file:
|
||||
self.config_file.write("# Generated by Infamy DHCPv6\n")
|
||||
|
||||
# Global options
|
||||
self.config_file.write("default-lease-time 3600;\n")
|
||||
self.config_file.write("max-lease-time 7200;\n")
|
||||
self.config_file.write("log-facility local7;\n")
|
||||
self.config_file.write("\n")
|
||||
|
||||
# DNS options
|
||||
if dns:
|
||||
if isinstance(dns, list):
|
||||
dns_str = ', '.join(dns)
|
||||
else:
|
||||
dns_str = dns
|
||||
self.config_file.write(f"option dhcp6.name-servers {dns_str};\n")
|
||||
|
||||
if domain:
|
||||
self.config_file.write(f"option dhcp6.domain-search \"{domain}\";\n")
|
||||
|
||||
self.config_file.write("\n")
|
||||
|
||||
# Subnet declaration
|
||||
self.config_file.write(f"subnet6 {self.subnet} {{\n")
|
||||
|
||||
# Address range for IA_NA (regular address assignment)
|
||||
if start and end:
|
||||
self.config_file.write(f" range6 {start} {end};\n")
|
||||
|
||||
# Prefix delegation pool
|
||||
if prefix:
|
||||
# Calculate end of prefix pool based on prefix_len
|
||||
# For simplicity, we'll create a reasonable range
|
||||
# e.g., if prefix is 2001:db8:100:: and prefix_len is 64,
|
||||
# delegate /64 prefixes from 2001:db8:100:: to 2001:db8:1ff::
|
||||
prefix_base = prefix.rstrip(':')
|
||||
# Simple approach: if prefix ends with ::, add range
|
||||
if prefix.endswith('::'):
|
||||
prefix_start = prefix
|
||||
# Create end address by incrementing last hex digit before ::
|
||||
# For 2001:db8:100::, end would be 2001:db8:1ff::
|
||||
parts = prefix_base.split(':')
|
||||
if len(parts) >= 2:
|
||||
last_part = parts[-2] if parts[-2] else parts[-3]
|
||||
try:
|
||||
last_val = int(last_part, 16)
|
||||
end_val = last_val + 0xff
|
||||
parts_copy = parts[:-1]
|
||||
parts_copy[-1] = f"{end_val:x}"
|
||||
prefix_end = ':'.join(parts_copy) + '::'
|
||||
except:
|
||||
prefix_end = prefix # Fallback
|
||||
else:
|
||||
prefix_end = prefix
|
||||
else:
|
||||
prefix_start = prefix
|
||||
prefix_end = prefix
|
||||
|
||||
self.config_file.write(f" prefix6 {prefix_start} {prefix_end} /{prefix_len};\n")
|
||||
|
||||
self.config_file.write("}\n")
|
||||
|
||||
def start(self):
|
||||
"""Start the DHCPv6 server"""
|
||||
if not os.path.exists(self.config_path):
|
||||
raise Exception("Config file does not exist")
|
||||
|
||||
# Debug: show config and interface status
|
||||
# self.netns.popen(f"cat {self.config_path}".split(" "))
|
||||
# self.netns.popen("ifconfig")
|
||||
|
||||
# ISC dhcpd command for IPv6
|
||||
# -6: IPv6 mode
|
||||
# -f: Stay in foreground (no daemon)
|
||||
# -d: Debug/log to stderr
|
||||
# -cf: Config file
|
||||
# -lf: Lease file
|
||||
# -pf: PID file
|
||||
cmd = [
|
||||
"dhcpd",
|
||||
"-6", # IPv6 mode
|
||||
"-f", # Foreground
|
||||
"-d", # Debug output
|
||||
"-cf", self.config_path,
|
||||
"-lf", self.leases_path,
|
||||
"-pf", self.pid_path,
|
||||
self.iface # Interface to listen on
|
||||
]
|
||||
|
||||
self.process = self.netns.popen(cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the DHCPv6 server"""
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
self.process = None
|
||||
|
||||
+63
-24
@@ -2,13 +2,15 @@
|
||||
Fetch interface status from remote device.
|
||||
"""
|
||||
|
||||
|
||||
def get_xpath(iface, path=None):
|
||||
"""Compose complete XPath to a YANG node in /ietf-interfaces"""
|
||||
xpath=f"/ietf-interfaces:interfaces/interface[name='{iface}']"
|
||||
if not path is None:
|
||||
xpath=f"{xpath}/{path}"
|
||||
xpath = f"/ietf-interfaces:interfaces/interface[name='{iface}']"
|
||||
if path is not None:
|
||||
xpath = f"{xpath}/{path}"
|
||||
return xpath
|
||||
|
||||
|
||||
def _extract_param(json_content, param):
|
||||
"""Returns (extracted) value for parameter 'param'"""
|
||||
interfaces = json_content.get('interfaces')
|
||||
@@ -22,6 +24,7 @@ def _extract_param(json_content, param):
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_param(target, iface, param=None):
|
||||
"""Fetch target dict for iface and extract param from JSON"""
|
||||
content = target.get_data(get_xpath(iface, param))
|
||||
@@ -29,42 +32,78 @@ def get_param(target, iface, param=None):
|
||||
return None
|
||||
return _extract_param(content, param)
|
||||
|
||||
|
||||
def exist(target, iface):
|
||||
"""Verify that the target interface exists"""
|
||||
return get_param(target, iface, "name") is not None
|
||||
|
||||
def address_exist(target, iface, address, prefix_length = 24, proto="dhcp"):
|
||||
|
||||
def address_exist(target, iface, address, prefix_length=None, proto="dhcp"):
|
||||
"""Check if 'address' is set on iface"""
|
||||
if not prefix_length:
|
||||
if ':' in address:
|
||||
prefix_length = 64
|
||||
else:
|
||||
prefix_length = 24
|
||||
|
||||
addrs = get_ipv4_address(target, iface)
|
||||
if not addrs:
|
||||
return False
|
||||
for addr in addrs:
|
||||
if addr['origin'] == proto and addr['ip'] == address and addr['prefix-length'] == prefix_length:
|
||||
return True
|
||||
if addrs:
|
||||
for addr in addrs:
|
||||
if addr['origin'] == proto and addr['ip'] == address and\
|
||||
addr['prefix-length'] == prefix_length:
|
||||
return True
|
||||
|
||||
addrs = get_ipv6_address(target, iface)
|
||||
if addrs:
|
||||
for addr in addrs:
|
||||
if addr['origin'] == proto and addr['ip'] == address and\
|
||||
addr['prefix-length'] == prefix_length:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_ipv4_address(target, iface):
|
||||
"""Fetch interface IPv4 addresses from (operational status)"""
|
||||
# The interface array is different in restconf/netconf, netconf has a keyed list but
|
||||
# restconf has a numbered list, i think i read that this was a bug in rousette, but
|
||||
# have not found it.
|
||||
interface=target.get_iface(iface)
|
||||
"""Fetch interface IPv4 addresses from operational"""
|
||||
# The interface array is different in restconf/netconf, netconf has
|
||||
# a keyed list but restconf has a numbered list, i think i read that
|
||||
# this was a bug in rousette, but have not found it.
|
||||
interface = target.get_iface(iface)
|
||||
if interface is None:
|
||||
raise "Interface not found"
|
||||
|
||||
ipv4 = interface.get("ipv4") or interface.get("ietf-ip:ipv4")
|
||||
if ipv4 is None or 'address' not in ipv4:
|
||||
ip = interface.get("ipv4") or interface.get("ietf-ip:ipv4")
|
||||
if ip is None or 'address' not in ip:
|
||||
return None
|
||||
return ipv4['address']
|
||||
return ip['address']
|
||||
|
||||
|
||||
def get_ipv6_address(target, iface):
|
||||
"""Fetch interface IPv6 addresses from operational"""
|
||||
# The interface array is different in restconf/netconf, netconf has
|
||||
# a keyed list but restconf has a numbered list, i think i read that
|
||||
# this was a bug in rousette, but have not found it.
|
||||
interface = target.get_iface(iface)
|
||||
if interface is None:
|
||||
raise "Interface not found"
|
||||
|
||||
ip = interface.get("ipv6") or interface.get("ietf-ip:ipv6")
|
||||
if ip is None or 'address' not in ip:
|
||||
return None
|
||||
return ip['address']
|
||||
|
||||
|
||||
def get_phys_address(target, iface):
|
||||
"""Fetch interface MAC address (operational status)"""
|
||||
return get_param(target, iface, "phys-address")
|
||||
|
||||
|
||||
def exist_bridge_multicast_filter(target, group, iface, bridge):
|
||||
# The interface array is different in restconf/netconf, netconf has a keyed list but
|
||||
# restconf has a numbered list, i think i read that this was a bug in rousette, but
|
||||
# have not found it.
|
||||
interface=target.get_iface(bridge)
|
||||
"""Check if a bridge has a multicast filter for group with iface"""
|
||||
# The interface array is different in restconf/netconf, netconf has
|
||||
# a keyed list but restconf has a numbered list, i think i read that
|
||||
# this was a bug in rousette, but have not found it.
|
||||
interface = target.get_iface(bridge)
|
||||
if interface is None:
|
||||
raise "Interface not found"
|
||||
|
||||
@@ -72,9 +111,9 @@ def exist_bridge_multicast_filter(target, group, iface, bridge):
|
||||
if brif is None:
|
||||
return False
|
||||
|
||||
for filter in brif.get("multicast-filters", {}).get("multicast-filter", {}):
|
||||
if filter.get("group") == group:
|
||||
for p in filter.get("ports"):
|
||||
for f in brif.get("multicast-filters", {}).get("multicast-filter", {}):
|
||||
if f.get("group") == group:
|
||||
for p in f.get("ports"):
|
||||
if p["port"] == iface:
|
||||
return True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user