Skip to content

Services: Kea DHCPv6: Dynamic prefix delegation#10252

Open
Monviech wants to merge 40 commits intomasterfrom
kea-dynamic-poc
Open

Services: Kea DHCPv6: Dynamic prefix delegation#10252
Monviech wants to merge 40 commits intomasterfrom
kea-dynamic-poc

Conversation

@Monviech
Copy link
Copy Markdown
Member

@Monviech Monviech commented May 4, 2026

Important notices

Before you submit a pull request, we ask you kindly to acknowledge the following:

If AI was used, please disclose:

  • Model used: ChatGPT 5.4
  • Extent of AI involvement: Brainstorming about this, rubberducking, help with IPv6 splitting functions and stuff like that that handles prefix calculations

Describe the problem

Fixes: #9941


Describe the proposed solution

The scope is intentionally limited. Validations prevent interactions we do not want. Things that can make KEA crash are auto generated. The user cannot decide much to protect from hard to troubleshoot issues.

The design revolves around two steps:

  1. Create a fully functional KEA config, prevent the user from having too much freedom by constraining every input possible (this can be relaxed later). Keep the GUI as lean as possible, no dynamic hints, dynamic grid values etc.
    To allow KEA to always start, even without any valid prefixes on interfaces, placeholder prefixes are emitted to the subnet, and a client-classes test prevents clients from getting leases from such subnets.
  2. A hook script that will take care of prefix changes and subnet lease whipe by using user-context keys from the running KEA config and writing them to the KEA socket. (Read write). We do not need a post apply action this way.
    The hook script is indempodent and very cheap to execute. A failure or a wrong configuration file cannot crash the running KEA daemon.

A concept for 1 is in POC phase and finished
A concept for 2 is in POC phase and finished


Generated Configuration file example
Placeholder prefix:

        "subnet6": [
            {
                "id": 1,
                "subnet": "dead:beef::\/58",
                "option-data": [],
                "pools": [
                    {
                        "pool": "dead:beef::\/64"
                    }
                ],
                "pd-pools": [
                    {
                        "prefix": "dead:beef:0:20::",
                        "prefix-len": 59,
                        "delegated-len": 64,
                        "user-context": {
                            "uuid": "c6dabbe0-a0b2-4688-b198-a8402b9b2723"
                        }
                    }
                ],
                "reservations": [],
                "interface": "hn2",
                "user-context": {
                    "uuid": "ab5bdee9-6381-487e-966c-a1cee1bd0a39",
                    "dynamic_prefix": true,
                    "prefix_valid": false,
                    "prefix_source": "wan"
                },
                "require-client-classes": [
                    "NO_LEASES_PLEASE"
                ],
            },
            {
                "id": 2,
                "subnet": "dead:beef:0:68::\/61",
                "option-data": [],
                "pools": [
                    {
                        "pool": "dead:beef:0:68::\/64"
                    }
                ],
                "pd-pools": [
                    {
                        "prefix": "dead:beef:0:6c::",
                        "prefix-len": 62,
                        "delegated-len": 64,
                        "user-context": {
                            "uuid": "fa0a7323-dad8-47ea-92cc-b43bd6e61d37"
                        }
                    }
                ],
                "reservations": [],
                "interface": "hn3",
                "user-context": {
                    "uuid": "057de7ee-bc6e-4677-b6df-8b0ccdedf084",
                    "dynamic_prefix": true,
                    "prefix_valid": false,
                    "prefix_source": "wan"
                },
                "require-client-classes": [
                    "NO_LEASES_PLEASE"
                ],
            },
        ],
        "client-classes": [
            {
                "name": "NO_LEASES_PLEASE",
                "test": "not member('ALL')",
                "only-if-required": true
            }
    }
}

"Real" prefix:

# cat /tmp/hn1_prefixv6
2001:db8:1234::/56
        "subnet6": [
            {
                "id": 1,
                "subnet": "2001:db8:1234::\/58",
                "option-data": [],
                "pools": [
                    {
                        "pool": "2001:db8:1234::\/64"
                    }
                ],
                "pd-pools": [
                    {
                        "prefix": "2001:db8:1234:20::",
                        "prefix-len": 59,
                        "delegated-len": 64,
                        "user-context": {
                            "uuid": "c6dabbe0-a0b2-4688-b198-a8402b9b2723"
                        }
                    }
                ],
                "reservations": [],
                "interface": "hn2",
                "user-context": {
                    "uuid": "ab5bdee9-6381-487e-966c-a1cee1bd0a39",
                    "dynamic_prefix": true,
                    "prefix_valid": true,
                    "prefix_source": "wan"
                },
            },
            {
                "id": 2,
                "subnet": "2001:db8:1234:68::\/61",
                "option-data": [],
                "pools": [
                    {
                        "pool": "2001:db8:1234:68::\/64"
                    }
                ],
                "pd-pools": [
                    {
                        "prefix": "2001:db8:1234:6c::",
                        "prefix-len": 62,
                        "delegated-len": 64,
                        "user-context": {
                            "uuid": "fa0a7323-dad8-47ea-92cc-b43bd6e61d37"
                        }
                    }
                ],
                "reservations": [],
                "interface": "hn3",
                "user-context": {
                    "uuid": "057de7ee-bc6e-4677-b6df-8b0ccdedf084",
                    "dynamic_prefix": true,
                    "prefix_valid": true,
                    "prefix_source": "wan"
                },
            },

Monviech added 13 commits April 30, 2026 13:58
… should be enriched in a post apply hook later
…alue and reservations for a dynamic prefix subnet. The subnet must be empty since it is auto configured, the pool is auto configured as ::1000-::2000 and seeded with initial prefix, reservations cannot be created because that would blow up as there is no concept like partial IPv6 addresses in KEA. We always want to bootstrap KEA with an initial working configuration.
…e largets possible prefix that does not include the IA_NA generated address pool. Validation ensures the user can only change the delegated prefix length, but not anything about the pool itself. KEA is very strict about validations, auto generation is required here to ensure the model stays sane.
…re is nothing we can do if we offer both IA_NA and IA_PD, at least /63 would be required for one IA_NA and one IA_PD pool.
…refix delegation. Cannot be cleanly solved, and if somebody doesn't use identity associaton in interface configurations it does not make sense to arbitrary reduce the size here.
@Monviech Monviech added the feature Adding new functionality label May 4, 2026
Comment thread src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv6.volt
Copy link
Copy Markdown
Member

@fichtner fichtner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks nice! my only worries here are:

  1. Validation of "track interface" relationship between subnet and prefix is not well-defined. In the old days you'd select a PD to be on a specific interface in which case it would already know which prefix to use (by looking up LAN -> WAN).
  2. Only one dynamic PD with the current enforcement, but need to check the old code and the GUI workflow in order to judge.

@Monviech
Copy link
Copy Markdown
Member Author

Monviech commented May 5, 2026

The main constraint here is that a subnet in kea must be unique. So you cannot attach the same prefix to multiple interfaces.

If we want to have that we would need more GUI magic that allows the WAN prefix to be splitted into smaller prefixes, or implement some partial address logic. Both would be fragile.

The current state that allows at least one network to host dynamic PD is a good start I think.

Im open for ideas here.

@fichtner
Copy link
Copy Markdown
Member

fichtner commented May 5, 2026

If that's the constraint we want to go forward with I don't mind. It also limits the scope and one PD is better than none :)

@Monviech
Copy link
Copy Markdown
Member Author

Monviech commented May 5, 2026

I try really hard to limit the scope with this one. Expanding scope later should be doable if users have needs that cannot be met with this model.

EG adding a new prefix ID field or something the like.

But since providers also like playing switcheroo with delegated prefix sizes for single customers all flexibility will bite someone at some point.

EDIT: All of these concerns to not apply anymore to the new model we use now that allows full flexibility.

Comment thread src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php Outdated
Comment thread src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml Outdated
Copy link
Copy Markdown
Member

@fichtner fichtner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tentative approval for the draft. looks snappy.

@Monviech
Copy link
Copy Markdown
Member Author

Monviech commented May 6, 2026

@fichtner Now the hook script will be easy. It doesnt need to know or calculate anything :)

Comment thread src/etc/inc/plugins.inc.d/kea.inc Outdated
Comment thread src/etc/inc/plugins.inc.d/kea.inc Outdated
@Monviech Monviech marked this pull request as ready for review May 7, 2026 09:31
Comment thread src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml Outdated
Comment thread src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml Outdated
Comment thread src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv6.volt
Comment thread src/opnsense/mvc/app/library/OPNsense/Interface/Idassoc.php
Comment thread src/etc/inc/plugins.inc.d/kea.inc Outdated
Monviech added 3 commits May 8, 2026 07:52
…e some of the kea_prefix_renew logging. Streamline the STDOUT result as well.
…is deprecated. Use 'only-in-additional-list' instead
@Monviech Monviech requested a review from Copilot May 8, 2026 07:34
@Monviech
Copy link
Copy Markdown
Member Author

Monviech commented May 8, 2026

Please be gentle Copilot

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds dynamic IPv6 prefix delegation support to the Kea DHCPv6 service by deriving subnets/pools from tracked identity-association (idassoc6) prefixes and automatically regenerating/reloading Kea when the WAN IPv6 prefix changes (Issue #9941).

Changes:

  • Add an Idassoc helper to compute per-interface on-link and allocatable IPv6 prefixes from tracked PD state.
  • Extend Kea DHCPv6 model/GUI/config generation to support “dynamic prefix” subnets and auto-generated pools/PD pools.
  • Add a hook script + plugin entry to wipe leases, regenerate config, and reload Kea on WAN IPv6 changes.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/opnsense/scripts/kea/kea_prefix_renew.py Hook script to wipe dynamic-prefix leases, regenerate dhcp6 config, and reload Kea
src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv6.volt UI grid formatting + hide/show fields for dynamic prefix mode
src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml Model updates for dynamic_prefix field and related UI/display tweaks
src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php Validation + config generation for dynamic-prefix subnets and PD pools
src/opnsense/mvc/app/library/OPNsense/Interface/Idassoc.php New library to compute identity-association prefixes (on-link/allocated/associated)
src/opnsense/mvc/app/library/OPNsense/Firewall/Util.php Add IPv6 prefix splitting helper for deriving non-overlapping child prefixes
src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogSubnet6.xml Add Dynamic Prefix toggle and grid formatting for subnet/pools
src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogPDPool6.xml Add grid formatting and styling for PD pool prefix fields
src/etc/inc/plugins.inc.d/kea.inc Add pluginctl action for dhcpv6 config generation + newwanip(inet6) hook
plist Install new Idassoc library and kea_prefix_renew script

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php
Comment thread src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php
Comment thread src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php
Comment thread src/opnsense/scripts/kea/kea_prefix_renew.py
Comment thread src/opnsense/scripts/kea/kea_prefix_renew.py
Comment thread src/opnsense/mvc/app/library/OPNsense/Interface/Idassoc.php
Comment thread src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv6.volt
Copilot stopped work on behalf of Monviech due to an error May 8, 2026 07:51
@Monviech
Copy link
Copy Markdown
Member Author

Monviech commented May 8, 2026

No, bad copilot, don't commit changes! Now I have to clean up. Meh.

@Monviech Monviech force-pushed the kea-dynamic-poc branch from 4e433c4 to e382f17 Compare May 8, 2026 07:54
$client_classes[] = [
'name' => 'NO_LEASES_PLEASE',
'test' => "not member('ALL')",
'only-in-additional-list' => true,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong here after all. KEA documentation:

The only-in-additional-list flag is not mandatory; when its value is set to false (the default), membership is determined during classification and is available for subnet selection, for instance. When the value is set to true, membership is evaluated only if the class appears in an evaluate-additional-classes list and is usable only for option configuration.

$record['user-context']['prefix_source'] = $idassoc['prefix_source'] ?? $if;
// If the prefix is temporary placeholder, we will not send leases to any client
if (empty($idassoc['prefix_valid'])) {
$record['evaluate-client-classes'] = ['NO_LEASES_PLEASE'];
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also just become client-classes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Adding new functionality

Development

Successfully merging this pull request may close these issues.

kea: dynamic PD support for DHCPv6 WAN

3 participants