Skip to content

Commit ce475ac

Browse files
authored
Merge pull request #1734 from gwenya/dhcp-static-routes
Allow announcing extra routes through DHCPv4
2 parents dabb2e1 + e2516a0 commit ce475ac

File tree

13 files changed

+124
-0
lines changed

13 files changed

+124
-0
lines changed

.github/workflows/tests.yml

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ jobs:
3838
- id: ShellCheck
3939
name: Differential ShellCheck
4040
uses: redhat-plumbers-in-action/differential-shellcheck@v5
41+
env:
42+
SHELLCHECK_OPTS: --shell sh
4143
with:
4244
token: ${{ secrets.GITHUB_TOKEN }}
4345
if: github.event_name == 'pull_request' && matrix.go == 'stable'

cmd/incusd/main_forknet.go

+15
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,21 @@ func (c *cmdForknet) RunDHCP(cmd *cobra.Command, args []string) error {
391391
return nil
392392
}
393393

394+
for _, staticRoute := range lease.Offer.ClasslessStaticRoute() {
395+
route := &ip.Route{
396+
DevName: iface,
397+
Route: staticRoute.Dest.String(),
398+
Via: staticRoute.Router.String(),
399+
Family: ip.FamilyV4,
400+
}
401+
402+
err = route.Add()
403+
if err != nil {
404+
fmt.Fprintf(os.Stderr, "Giving up on DHCP, couldn't add classless static route to %q: %v\n", iface, err)
405+
return nil
406+
}
407+
}
408+
394409
// Create PID file.
395410
err = os.WriteFile(filepath.Join(args[0], "dhcp.pid"), []byte(fmt.Sprintf("%d", os.Getpid())), 0644)
396411
if err != nil {

doc/.wordlist.txt

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Diátaxis
6565
Diffie
6666
Distrobuilder
6767
DNS
68+
dnsmasq
6869
DNSSEC
6970
DoS
7071
DRM

doc/api-extensions.md

+4
Original file line numberDiff line numberDiff line change
@@ -2717,3 +2717,7 @@ Adds support for `DNS-01` challenge to the Incus ACME support for certificate ge
27172717
## `security_iommu`
27182718
Introduce a new `security.iommu` configuration key to control whether to
27192719
enable IOMMU emulation. This is done through `virtio_iommu` on Linux and the emulated Intel IOMMU on Windows.
2720+
2721+
## `network_ipv4_dhcp_routes`
2722+
Introduces a new `ipv4.dhcp.routes` configuration option on bridged and OVN networks.
2723+
This allows specifying pairs of CIDR networks and gateway address to be announced by the DHCP server.

doc/reference/network_bridge.md

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Key | Type | Condition | Defau
7575
`ipv4.dhcp.expiry` | string | IPv4 DHCP | `1h` | When to expire DHCP leases
7676
`ipv4.dhcp.gateway` | string | IPv4 DHCP | IPv4 address | Address of the gateway for the subnet
7777
`ipv4.dhcp.ranges` | string | IPv4 DHCP | all addresses | Comma-separated list of IP ranges to use for DHCP (FIRST-LAST format)
78+
`ipv4.dhcp.routes` | string | IPv4 DHCP | - | Static routes to provide via DHCP option 121, as a comma-separated list of alternating subnets (CIDR) and gateway addresses (same syntax as dnsmasq)
7879
`ipv4.firewall` | bool | IPv4 address | `true` | Whether to generate filtering firewall rules for this network
7980
`ipv4.nat` | bool | IPv4 address | `false` (initial value on creation if `ipv4.address` is set to `auto`: `true`) | Whether to NAT
8081
`ipv4.nat.address` | string | IPv4 address | - | The source address used for outbound traffic from the bridge

doc/reference/network_ovn.md

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Key | Type | Condition | Defau
5252
`dns.zone.reverse.ipv6` | string | - | - | DNS zone name for IPv6 reverse DNS records
5353
`ipv4.address` | string | standard mode | - (initial value on creation: `auto`) | IPv4 address for the bridge (use `none` to turn off IPv4 or `auto` to generate a new random unused subnet) (CIDR)
5454
`ipv4.dhcp` | bool | IPv4 address | `true` | Whether to allocate addresses using DHCP
55+
`ipv4.dhcp.routes` | string | IPv4 DHCP | - | Static routes to provide via DHCP option 121, as a comma-separated list of alternating subnets (CIDR) and gateway addresses (same syntax as dnsmasq and OVN)
5556
`ipv4.l3only` | bool | IPv4 address | `false` | Whether to enable layer 3 only mode.
5657
`ipv4.nat` | bool | IPv4 address | `false` (initial value on creation if `ipv4.address` is set to `auto`: `true`) | Whether to NAT
5758
`ipv4.nat.address` | string | IPv4 address | - | The source address used for outbound traffic from the network (requires uplink `ovn.ingress_mode=routed`)

internal/server/network/driver_bridge.go

+5
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ func (n *bridge) Validate(config map[string]string) error {
183183
"ipv4.dhcp.gateway": validate.Optional(validate.IsNetworkAddressV4),
184184
"ipv4.dhcp.expiry": validate.IsAny,
185185
"ipv4.dhcp.ranges": validate.Optional(validate.IsListOf(validate.IsNetworkRangeV4)),
186+
"ipv4.dhcp.routes": validate.Optional(validate.IsDHCPRouteList),
186187
"ipv4.routes": validate.Optional(validate.IsListOf(validate.IsNetworkV4)),
187188
"ipv4.routing": validate.Optional(validate.IsBool),
188189
"ipv4.ovn.ranges": validate.Optional(validate.IsListOf(validate.IsNetworkRangeV4)),
@@ -928,6 +929,10 @@ func (n *bridge) setup(oldConfig map[string]string) error {
928929
dnsmasqCmd = append(dnsmasqCmd, fmt.Sprintf("--dhcp-option-force=119,%s", strings.Trim(dnsSearch, " ")))
929930
}
930931

932+
if n.config["ipv4.dhcp.routes"] != "" {
933+
dnsmasqCmd = append(dnsmasqCmd, fmt.Sprintf("--dhcp-option-force=121,%s", strings.Replace(n.config["ipv4.dhcp.routes"], " ", "", -1)))
934+
}
935+
931936
expiry := "1h"
932937
if n.config["ipv4.dhcp.expiry"] != "" {
933938
expiry = n.config["ipv4.dhcp.expiry"]

internal/server/network/driver_ovn.go

+2
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ func (n *ovn) Validate(config map[string]string) error {
394394
}),
395395
"ipv4.dhcp": validate.Optional(validate.IsBool),
396396
"ipv4.dhcp.ranges": validate.Optional(validate.IsListOf(validate.IsNetworkRangeV4)),
397+
"ipv4.dhcp.routes": validate.Optional(validate.IsDHCPRouteList),
397398
"ipv6.address": validate.Optional(func(value string) error {
398399
if validate.IsOneOf("none", "auto")(value) == nil {
399400
return nil
@@ -2656,6 +2657,7 @@ func (n *ovn) setup(update bool) error {
26562657
MTU: bridgeMTU,
26572658
Netmask: dhcpV4Netmask,
26582659
DNSSearchList: n.getDNSSearchList(),
2660+
StaticRoutes: n.config["ipv4.dhcp.routes"],
26592661
}
26602662

26612663
if uplinkNet != nil {

internal/server/network/ovn/ovn_nb_actions.go

+7
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ type OVNDHCPv4Opts struct {
108108
MTU uint32
109109
Netmask string
110110
DNSSearchList []string
111+
StaticRoutes string
111112
}
112113

113114
// OVNDHCPv6Opts IPv6 DHCP option set that can be created (and then applied to a switch port by resulting ID).
@@ -1287,6 +1288,12 @@ func (o *NB) UpdateLogicalSwitchDHCPv4Options(ctx context.Context, switchName OV
12871288
dhcpOption.Options["netmask"] = opts.Netmask
12881289
}
12891290

1291+
if opts.StaticRoutes != "" {
1292+
dhcpOption.Options["classless_static_route"] = fmt.Sprintf("{%s}", opts.StaticRoutes)
1293+
} else {
1294+
delete(dhcpOption.Options, "classless_static_route")
1295+
}
1296+
12901297
// Prepare the changes.
12911298
operations := []ovsdb.Operation{}
12921299
if dhcpOption.UUID == "" {

internal/version/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ var APIExtensions = []string{
466466
"api_filtering_extended",
467467
"acme_dns01",
468468
"security_iommu",
469+
"network_ipv4_dhcp_routes",
469470
}
470471

471472
// APIExtensionsCount returns the number of available API extensions.

shared/validate/validate.go

+24
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,30 @@ func IsNetworkPortRange(value string) error {
512512
return nil
513513
}
514514

515+
// IsDHCPRouteList validates a comma-separated list of alternating CIDR networks and IP addresses.
516+
func IsDHCPRouteList(value string) error {
517+
parts := strings.Split(value, ",")
518+
for i, s := range parts {
519+
// routes are pairs of subnet and gateway
520+
var err error
521+
if i%2 == 0 { // subnet part
522+
err = IsNetworkV4(s)
523+
} else { // gateway part
524+
err = IsNetworkAddressV4(s)
525+
}
526+
527+
if err != nil {
528+
return err
529+
}
530+
}
531+
532+
if len(parts)%2 != 0 { // uneven number of parts means the gateway of the last route is missing
533+
return fmt.Errorf("missing gateway for route %v", parts[len(parts)-1])
534+
}
535+
536+
return nil
537+
}
538+
515539
// IsURLSegmentSafe validates whether value can be used in a URL segment.
516540
func IsURLSegmentSafe(value string) error {
517541
for _, char := range []string{"/", "?", "&", "+"} {

test/main.sh

+1
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ if [ "${1:-"all"}" != "cluster" ]; then
318318
run_test test_server_config "server configuration"
319319
run_test test_filemanip "file manipulations"
320320
run_test test_network "network management"
321+
run_test test_network_dhcp_routes "network dhcp routes"
321322
run_test test_network_acl "network ACL management"
322323
run_test test_network_forward "network address forwards"
323324
run_test test_network_zone "network DNS zones"

test/suites/network_dhcp_routes.sh

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
test_network_dhcp_routes() {
2+
ensure_import_testimage
3+
ensure_has_localhost_remote "${INCUS_ADDR}"
4+
5+
# bridge network
6+
incus network create inct$$
7+
incus network set inct$$ ipv4.address 10.13.37.1/24
8+
incus network set inct$$ ipv4.dhcp.routes 1.2.0.0/16,10.13.37.5,2.3.0.0/16,10.13.37.7
9+
10+
incus launch testimage nettest -n inct$$
11+
12+
cat > "${TEST_DIR}"/udhcpc.sh <<EOL
13+
#!/bin/sh
14+
[ "\$1" = "bound" ] && echo "STATICROUTES: \$staticroutes"
15+
EOL
16+
17+
incus file push "${TEST_DIR}/udhcpc.sh" nettest/udhcpc.sh
18+
incus exec nettest -- chmod a+rx /udhcpc.sh
19+
20+
staticroutes_output=$(incus exec nettest -- udhcpc -s /udhcpc.sh 2>/dev/null | grep STATICROUTES)
21+
22+
echo "$staticroutes_output" | grep -q "1.2.0.0/16 10.13.37.5"
23+
echo "$staticroutes_output" | grep -q "2.3.0.0/16 10.13.37.7"
24+
25+
incus delete nettest -f
26+
27+
if [ -n "${INCUS_OFFLINE:-}" ]; then
28+
echo "==> SKIP: Skipping OCI tests as running offline"
29+
else
30+
ensure_has_localhost_remote "${INCUS_ADDR}"
31+
32+
incus remote add docker https://docker.io --protocol=oci
33+
incus launch docker:alpine nettest --network=inct$$
34+
35+
incus exec nettest -- ip route list | grep -q "1.2.0.0/16 via 10.13.37.5"
36+
incus exec nettest -- ip route list | grep -q "2.3.0.0/16 via 10.13.37.7"
37+
38+
incus delete -f nettest
39+
incus remote remove docker
40+
fi
41+
42+
incus network delete inct$$
43+
44+
45+
# ovn network
46+
if incus network create inct$$ -t ovn network=none; then
47+
incus network set inct$$ ipv4.address 10.13.37.1/24
48+
incus network set inct$$ ipv4.dhcp.routes 1.2.0.0/16,10.13.37.5,2.3.0.0/16,10.13.37.7
49+
50+
incus launch testimage nettest -n inct$$
51+
52+
incus exec nettest -- ip route list | grep -q "1.2.0.0/16 via 10.13.37.5"
53+
incus exec nettest -- ip route list | grep -q "2.3.0.0/16 via 10.13.37.7"
54+
55+
incus delete nettest -f
56+
incus network delete inct$$
57+
else
58+
echo "==> SKIP: Skipping OVN tests"
59+
fi
60+
}

0 commit comments

Comments
 (0)