Skip to content

dhcp: allow reserved leases outside scope pool range but within subnet#2032

Open
CoryCharlton wants to merge 1 commit into
TechnitiumSoftware:masterfrom
CoryCharlton:fix/dhcp-reservation-outside-scope-range
Open

dhcp: allow reserved leases outside scope pool range but within subnet#2032
CoryCharlton wants to merge 1 commit into
TechnitiumSoftware:masterfrom
CoryCharlton:fix/dhcp-reservation-outside-scope-range

Conversation

@CoryCharlton

Copy link
Copy Markdown

Fixes #831.

Allows DHCP reserved leases whose IP is outside the scope's [startingAddress, endingAddress] pool range but still within the scope's subnet — accepted on add and correctly served through renewal.

Problem

Two defects combined to make out-of-pool reservations unusable:

  1. Validation asymmetry. The Scope.ReservedLeases setter (used by the console form via dhcp/scopes/set) validated reserved addresses against the pool range with IsAddressInRange, rejecting anything outside [startingAddress, endingAddress]. The API path addReservedLeaseScope.TryAddReservedLease did no range check, so the API could create reservations the UI refused.

  2. Renewal drop (the real footgun). Reserved leases are keyed by MAC, so a DISCOVER (ciaddr = 0) hits the broadcast branch of FindScope and an out-of-pool reservation is offered/assigned fine. But on renewal the client unicasts a REQUEST with ciaddr = its out-of-pool IP → the no-relay unicast branch of FindScope selected the scope only via IsAddressInRange(ciaddr) → no scope matched → FindScope returned null → the REQUEST was silently dropped (no NAK). The client held the address until T1, then lost the lease and re-DISCOVERed — a churn loop. So out-of-pool reservations "worked" until the first renewal.

This was confirmed on a production fleet (v15.3): reserved leases inside the pool renewed normally, while reserved leases outside the pool had their leaseExpires frozen at the last server restart and only recovered by re-DISCOVERing at expiry.

Changes

Two files, ~3 hunks:

  • Scope.cs — validate reserved-lease addresses by subnet membership (IsAddressInNetwork) instead of pool range, in both the ReservedLeases setter and TryAddReservedLease, so the console form and the API agree. Wrong-subnet (and network/broadcast) addresses are still rejected.
  • DhcpServer.cs — in the no-relay unicast (RENEW/REBIND) branch of FindScope, match the scope by the client's reserved lease (keyed on MAC) before the pool-range check, mirroring what the broadcast and relay branches already do. Once the scope is found, the existing RENEWING path already ACKs correctly (the reserved offer is built without a range check).

Reserved-first ordering also resolves the ambiguity where a reserved IP happens to fall inside a different scope's pool: the scope that actually reserved the address for that MAC wins.

Known limitation

If an out-of-pool reservation is deleted while a client still holds it, unicast renewal can no longer locate the scope (no reserved lease, ciaddr out of range), so the REQUEST is dropped rather than NAK'd; the client re-DISCOVERs at timeout. Same net effect as today for that address. Fixing it would require matching by the existing dynamic-lease table in FindScope — a larger change I left out to keep this focused.

Testing

This repo has no automated test suite, so the change was verified manually with a real, isolated DHCP exchange:

  • Two Linux network namespaces (server + client) joined by a veth pair, on a subnet chosen not to collide with the host LAN. (A plain dummy interface can't be used — it drops egress, so packets never reach the server.)
  • A DnsServerApp instance in the server namespace; a scapy-based client crafting DISCOVER / REQUEST / unicast RENEW at L2.
  • Scenario: scope pool …100-.250/24, reserved lease …20 (out of pool) added via addReservedLease.
    • DISCOVEROFFER of .20, REQUEST (selecting) → ACK — regression check that allocation still works.
    • Unicast RENEW with ciaddr = .20, no server-id, no requested-ip → ACK of .20 (before the fix: silent drop).
    • Control: reverting only the FindScope change reproduces the silent drop on renewal, confirming the test exercises the bug.
    • Negative: a wrong-subnet reserved IP is rejected on both addReservedLease and dhcp/scopes/set with a subnet-membership error.

If you'd like, I'm happy to add unit coverage around FindScope and the ReservedLeases setter — I held off since the project has no test framework yet and that choice seemed better left to you.

Reserved DHCP leases whose IP is outside the scope's [startingAddress,
endingAddress] pool range but still inside the scope's subnet were rejected on
add and, when added via the API back door, failed unicast renewal.

- Scope: validate reserved-lease addresses by subnet membership
  (IsAddressInNetwork) instead of pool range (IsAddressInRange), in both the
  ReservedLeases setter and TryAddReservedLease so the console form and API
  agree. Wrong-subnet addresses are still rejected.
- DhcpServer.FindScope: in the no-relay unicast (RENEW/REBIND) branch, match the
  scope by the client's reserved lease (keyed on MAC) before the pool-range
  check, mirroring the broadcast and relay branches. Previously an out-of-pool
  ciaddr matched no scope, so the REQUEST was silently dropped and the client
  lost its lease at T1.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DHCP feature request: allow reservations outside of scope

1 participant