Skip to content

Name the configured static UE address in the rule the UPF installs - #711

Merged
gab-arrobo merged 2 commits into
omec-project:mainfrom
grewelltech:fix/static-ue-ip-with-upf-alloc
Oct 26, 2026
Merged

gab-arrobo merged 2 commits into
omec-project:mainfrom
grewelltech:fix/static-ue-ip-with-upf-alloc

Conversation

@bgrewell

Copy link
Copy Markdown
Contributor

Why this is needed

A subscriber given a fixed address in staticIpInfo does not get it, and nothing says so.

The configuration is accepted, the SMF selects the address, and the session establishes. The UE then holds an address out of the user plane's pool instead. On a lab core, imsi-001010100007510 configured for 192.168.100.100 came up on 192.168.100.15; every other subscriber was unaffected, so the feature looks like it works until the one address an operator cares about is checked. The only trace of the configuration anywhere was the SMF's own Release IP[192.168.100.100] — a log line that reads like ordinary teardown.

This is the reference deployment's default, not a corner case. aether-onramp runs bess-upf with enable_ue_ip_alloc: true, so the UPF advertises the UEIP feature and the condition holds for every session on every such cluster. Static addressing is used for the things that have to be reachable at a known address — a fixed-wireless CPE, a camera, a sensor gateway reached by a fleet controller — so the failure lands on the subscribers whose address is load-bearing.

What is broken

context/datapath.go builds the UE IP Address IE for a PDI in three places, each of them identically:

ueIpAddr := UEIPAddress{}
if dpNode.UPF.IsUpfSupportUeIpAddrAlloc() {
	ueIpAddr.CHV4 = true
} else {
	ueIpAddr.V4 = true
	ueIpAddr.Ipv4Address = smContext.PDUAddress.Ip.To4()
}

A UPF that advertises UEIP is asked to choose the address, whatever address the session already holds. The static configuration is consulted earlier, in the allocator (context/ip_allocator.go), and is lost here.

What follows is ordinary SMF behaviour, which is why it is silent:

  1. Allocate returns the configured address for the IMSI and the session holds it.
  2. The PDR carries CHV4, so the UPF allocates from its own pool (pfcpiface/parse_pdr.go, needAllocIP) and returns its choice in the Created PDR.
  3. HandlePfcpSessionEstablishmentResponse adopts that address, calls ReleaseUeIpAddr on the configured one, and sets UpfProvided.
  4. The UE is told the UPF's address in the PDU Session Establishment Accept.

No step is in error on its own, and no log line reports that a configured address was dropped.

How it is fixed

The three copies become one function, and for a session holding a configured address it names that address rather than asking the UPF to choose:

func ueIpAddressIE(smContext *SMContext, upf *UPF) UEIPAddress {
	if upf.IsUpfSupportUeIpAddrAlloc() && !smContext.HasStaticUeIpAddr() {
		return UEIPAddress{CHV4: true}
	}

	return UEIPAddress{V4: true, Ipv4Address: smContext.PDUAddress.Ip.To4()}
}

Naming the address is what the interface is for

TS 29.244 clause 8.2.62 makes V4 and CHV4 mutually exclusive: the control plane either assigns the address or asks the user plane to. Both are normal. A UPF that can allocate is not obliged to, and bess-upf does not — needAllocIP allocates only for a PDI whose CHV4 is set, and a Created PDR carries a UE IP Address only for a PDR it allocated for. So with the address named there is nothing for the SMF to adopt and nothing to release, and the rest of the establishment path needs no change at all.

Every session without a configured address is untouched: CHV4 as today, so the UE keeps getting an address out of the pool the user plane routes and NATs for.

The fact is recorded on the session, not re-derived

UeIpAddr gains Static, set when the address is allocated.

Asking the allocator at the time the rule is built does not answer for a session that outlived the SMF. IPAllocator has only unexported fields, so an SMContext read back from the database carries one that marshalled as {} and knows no configuration at all. A rule rebuilt for such a session — adding a PDU session anchor or a ULCL rule, which reaches ActivateTunnelAndPDR without going through the restoration path's pinUeAddress — would carry CHV4 again, the UPF would allocate, and the configured address would be dropped a second time.

The field is new and untagged, so an SMContext written by an older SMF decodes with Static false and behaves exactly as it does today.

HasStaticUeIpAddr answers for the address the session holds now

Not for what was allocated:

  • UpfProvided overrides Static. A UPF that allocated over a static address — the misconfiguration described below — leaves Static set while the address the session holds is the UPF's. Without this, the UPF's own choice would be reported and logged as the configured one, concealing the precondition the log line exists to surface.
  • 0.0.0.0 is rejected. ReleaseUeIpAddr writes it back to mean "no address", and the user plane reads the unspecified address as any address, so naming it would emit a rule matching every subscriber. producer/callback.go's sessionUEAddress guards the same sentinel for the same reason.

Two allocators on one subnet, and nothing to reconcile them

Naming the address leaves the SMF placing one session on an address while the UPF allocates for every other session on the same UPF, out of the same subnet. Nothing reconciles the two: the UPF's ue_ip_pool knows nothing of staticIpInfo (pfcpiface/ip_pool.go's NewIPPool enumerates the whole subnet minus network and broadcast, and needAllocIP honours a named address without marking it used), and the SMF withholds the configured addresses from its own pool only. A configured address inside the UPF's pool is handed to a second session once the pool reaches it, and both sessions then carry rules for the same UE address.

So where the UPF allocates, the configured addresses have to be kept out of its pool — which is where their route and NAT rule have to be arranged anyway. The SMF cannot check it: the UPF's pool is not signalled over N4, and PFCP offers no way to reserve an address there. Both the doc comment and the log line say so.

The override is logged, with the precondition

One line at INFO per node, from ActivateTunnelAndPDR (the only caller of ActivateUpLinkPdr and ActivateDlLinkPdr), naming the UPF, the subscriber, the address, and that the UPF's own pool must not contain it. The defect it replaces was invisible, and a statically addressed subscriber is by nature a small number of sessions. The IE builder itself stays pure.

StaticIpForImsi

The static lookup that Allocate and Release each carried a copy of becomes one method, so the allocation site can ask what Allocate just did.

What this does not fix

Deliberately out of scope — each is a defect this change does not cause and does not make worse, and each is better raised on its own:

  • A configured address is not reserved in the user plane's pool, as above. It needs a reservation across N4 that PFCP does not provide.
  • staticIpInfo is not validated or merged. ReserveStaticIps replaces its map rather than merging, and reserveStaticIpsIfNeeded calls it once per entry matching the DNN, so with more than one entry all but the last are withheld from the dynamic pool and then never handed to the subscriber configured for them. Nothing rejects an entry that cannot be a UE address either. Both are independent of this change: such a subscriber gets a dynamic address and a CHV4 rule, exactly as before.
  • A missing allocator is dereferenced. A DNN configured with no ueSubnet, or an invalid one, is stored with no allocator (context/context.go only warns), so the first PDU session on it panics the SMF in Allocate; ReleaseUeIpAddr reaches the same deref for a session restored from the database. Both predate this change, which adds no new call site on those paths.
  • A rule rebuilt mid-session asks the UPF to re-choose. For a session with no configured address this change keeps today's behaviour, so with ULCLSupport set the ULCL path can still move a live UE's address. Telling an established session from one at establishment needs to know whether its rules are installed, and SMContextState cannot answer it — ChangeState writes it without SMLock, which is why createInFlight exists for the restoration sweep.

Testing

5 new test functions in context/datapath_test.go, beside the ActivateUpLinkPdr / ActivateDlLinkPdr tests already there. Four fail without the change:

  • the uplink and the downlink rule must name the configured address when the UPF advertises UEIP (without it: CHV4 set: the UPF is asked to choose an address although 192.168.100.100 is configured for imsi-001010100007510);
  • a rule rebuilt for a session restored without its allocator must name it too;
  • an address the UPF allocated over a static one must not be reported as static (without it: 192.168.100.3 is reported as imsi-001010100007510's configured address 192.168.100.100);
  • the released-address sentinel must not be named (without it: rule names 0.0.0.0, which matches every subscriber).

The fifth pins what must not change: a session with no configured address is still left to the UPF's allocator, with CHV4 set and V4 clear.

Existing tests are unmodified. go build ./..., go vet, gofmt, gofumpt, gci, staticcheck ./... and go test -race ./... are clean.

@bgrewell
bgrewell requested a review from a team October 26, 2026 16:56
A subscriber given an address in staticIpInfo does not get it when the user plane
allocates UE addresses, which is the reference deployment's default
(enable_ue_ip_alloc: true on bess-upf). Nothing reports it. The SMF selects the
configured address and logs it, the PDR it then builds carries CHV4 because the
UPF advertises UEIP, the UPF allocates from its own pool and returns its choice,
and the SMF adopts that address and releases the configured one. Observed on a
cluster as imsi-001010100007510, configured for 192.168.100.100, holding
192.168.100.15; the only trace of the configuration was the SMF's own
"Release IP[192.168.100.100]". Other subscribers were unaffected, so the feature
looks like it works until the one address an operator cares about is checked.

The three places that build the UE IP Address IE now share one function, and for
a session holding a configured address it names that address instead of asking
the UPF to choose. TS 29.244 clause 8.2.62 provides for either -- V4 and CHV4 are
mutually exclusive -- and a UPF that allocates still honours a named address: it
allocates only for a PDI whose CHV4 is set, and returns an address in the Created
PDR only for one it allocated for, so the SMF has nothing to adopt and nothing to
release. Every other session is untouched and still left to the UPF's allocator.

Which address was configured is recorded on the session, as UeIpAddr.Static, when
the address is allocated. Asking the allocator at the time the rule is built
would not answer for a session that outlived the SMF: an IPAllocator has only
unexported fields, so an SMContext read back from the database carries one that
marshalled as {} and knows no configuration at all, and a rule rebuilt for such a
session through ActivateTunnelAndPDR -- adding a PDU session anchor or a ULCL
rule, which the restoration path's pinUeAddress does not cover -- would carry
CHV4 and lose the address again. The field is new and untagged, so a session
written by an older SMF decodes with Static false and behaves as it does today.

HasStaticUeIpAddr answers for the address the session holds now, not for what was
allocated: UpfProvided overrides Static, and the 0.0.0.0 ReleaseUeIpAddr writes
back is not an address. Without the first, a UPF that allocated over a static
address -- the misconfiguration below -- would have its own choice reported and
logged as the configured one. Without the second, a rule could name 0.0.0.0,
which the user plane reads as every address; producer/callback.go's
sessionUEAddress guards the same sentinel.

StaticIpForImsi replaces the copy of the static lookup that Allocate and Release
each carried, so the allocation site can ask what Allocate just did.

Naming a static address leaves the SMF and the UPF drawing on one subnet with
nothing to reconcile them, and the doc comment and the log line both say so: the
UPF's pool knows nothing of staticIpInfo, so a configured address inside that
pool is handed to a second session once the pool reaches it. Where the UPF
allocates, the configured addresses have to be kept out of its pool -- which is
where their route and NAT rule have to be arranged anyway. The SMF cannot check
it, because the UPF's pool is not signalled over N4, and PFCP offers no way to
reserve an address there.

Tests: five new test functions in context/datapath_test.go, beside the
ActivateUpLinkPdr and ActivateDlLinkPdr tests already there. Four fail without
the change: the uplink and the downlink rule must name the configured address
when the UPF advertises UEIP, a rule rebuilt for a session restored without its
allocator must name it too, and neither an address the UPF allocated over it nor
the released-address sentinel may be named. The fifth pins what must not change:
a session with no configured address is still left to the UPF's allocator, with
CHV4 set and V4 clear.

Signed-off-by: Benjamin Grewell <bgrewell@gmail.com>
@gab-arrobo
gab-arrobo force-pushed the fix/static-ue-ip-with-upf-alloc branch from baf6d7c to 5de959c Compare October 26, 2026 19:08
@gab-arrobo
gab-arrobo requested a balanced review from Copilot October 26, 2026 19:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Releasing a static-address session after database restoration can dereference an uninitialized allocator and panic.

1 open issue
What changed in this PR

This PR makes the SMF name configured static UE addresses in UPF rules instead of requesting a replacement address, while preserving dynamic allocation behavior.

Changes:

  • Record static-address ownership on the session so it survives persistence.
  • Consolidate UE address rule construction and log static-address overrides.
  • Add regression tests for static, dynamic, restored, released, and UPF-overridden addresses.
File Description
producer/​pdu_session.go Record whether the allocated session address is static.
context/​sm_context.go Store static ownership and identify currently held static addresses.
context/​ip_allocator.go Share static-address lookup across allocation and release.
context/​datapath.go Build consistent address rules and log static overrides.
context/​datapath_test.go Cover address selection and session-state edge cases.

Review effort: Balanced

Comment thread context/datapath.go
The two cases this change added use "default" as a PDR map key in a
composite literal, which brought the count goconst sees in the package to
four and failed the lint job: context/datapath_test.go:30:5: string
`default` has 4 occurrences, make it a constant (goconst). The key gets a
constant beside testDnn and every use in the file goes through it.

Signed-off-by: Benjamin Grewell <bgrewell@gmail.com>
@gab-arrobo
gab-arrobo merged commit ff990e1 into omec-project:main Oct 26, 2026
12 checks passed
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.

3 participants