Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions context/datapath.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,15 +663,24 @@ func (dpNode *DataPathNode) CreateSessRuleQer(smContext *SMContext) (*QER, error
return flowQER, nil
}

// ueIpAddressIE builds the UE IP Address IE for a PDI of this session.
//
// A UPF that allocates is asked to choose (CHV4), except for a session on a staticIpInfo address:
// that one is named, or the UPF allocates over it and the SMF adopts its choice.
//
// Where the UPF allocates, a staticIpInfo address has to be outside the UPF's own pool. PFCP
// cannot reserve it there, and the SMF cannot check it: the UPF's pool is not signalled over N4.
func ueIpAddressIE(smContext *SMContext, upf *UPF) UEIPAddress {
if upf.IsUpfSupportUeIpAddrAlloc() && !smContext.HasStaticUeIpAddr() {
return UEIPAddress{CHV4: true}
Comment thread
gab-arrobo marked this conversation as resolved.
}

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

// ActivateUpLinkPdr
func (dpNode *DataPathNode) ActivateUpLinkPdr(smContext *SMContext, defQER *QER, defPrecedence uint32) error {
ueIpAddr := UEIPAddress{}
if dpNode.UPF.IsUpfSupportUeIpAddrAlloc() {
ueIpAddr.CHV4 = true
} else {
ueIpAddr.V4 = true
ueIpAddr.Ipv4Address = smContext.PDUAddress.Ip.To4()
}
ueIpAddr := ueIpAddressIE(smContext, dpNode.UPF)

curULTunnel := dpNode.UpLinkTunnel
for name, ULPDR := range curULTunnel.PDR {
Expand Down Expand Up @@ -745,14 +754,7 @@ func (dpNode *DataPathNode) ActivateDlLinkPdr(smContext *SMContext, defQER *QER,
var iface *UPFInterfaceInfo
curDLTunnel := dpNode.DownLinkTunnel

// UPF provided UE ip-addr
ueIpAddr := UEIPAddress{}
if dpNode.UPF.IsUpfSupportUeIpAddrAlloc() {
ueIpAddr.CHV4 = true
} else {
ueIpAddr.V4 = true
ueIpAddr.Ipv4Address = smContext.PDUAddress.Ip.To4()
}
ueIpAddr := ueIpAddressIE(smContext, dpNode.UPF)

for name, DLPDR := range curDLTunnel.PDR {
logger.CtxLog.Infof("activate Downlink PDR[%v]:[%v]", name, DLPDR)
Expand Down Expand Up @@ -865,6 +867,12 @@ func (dataPath *DataPath) ActivateTunnelAndPDR(smContext *SMContext, precedence

logger.CtxLog.Debugln("calculate", curDataPathNode.UPF.PFCPAddr().String())

if curDataPathNode.UPF.IsUpfSupportUeIpAddrAlloc() && smContext.HasStaticUeIpAddr() {
logger.CtxLog.Infof("UPF[%s] allocates UE addresses; naming %s's static address %s in "+
"its rules, which the UPF's own pool must not contain",
curDataPathNode.UPF.PFCPAddr(), smContext.Supi, smContext.PDUAddress.Ip)
}

// Setup UpLink PDR
if curDataPathNode.UpLinkTunnel != nil {
if err := curDataPathNode.ActivateUpLinkPdr(smContext, defQER, precedence); err != nil {
Expand All @@ -879,13 +887,7 @@ func (dataPath *DataPath) ActivateTunnelAndPDR(smContext *SMContext, precedence
}
}

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

if curDataPathNode.DownLinkTunnel != nil {
if curDataPathNode.DownLinkTunnel.SrcEndPoint == nil {
Expand Down
207 changes: 207 additions & 0 deletions context/datapath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
UPF: &context.UPF{},
UpLinkTunnel: &context.GTPTunnel{
PDR: map[string]*context.PDR{
"default": {

Check failure on line 30 in context/datapath_test.go

View workflow job for this annotation

GitHub Actions / lint / lint

string `default` has 4 occurrences, make it a constant (goconst)
Precedence: 0,
FAR: &context.FAR{},
},
Expand Down Expand Up @@ -262,3 +262,210 @@
t.Errorf("a UPF on no path matched")
}
}

const (
staticPool = "192.168.100.0/24"
staticSupi = "imsi-001010100007510"
staticAddr = "192.168.100.100"
)

// ueipUpf advertises the UEIP feature, as bess-upf does with enable_ue_ip_alloc set.
func ueipUpf() *context.UPF {
return &context.UPF{
UPFunctionFeatures: &context.UPFunctionFeatures{
SupportedFeatures1: context.UpFunctionFeatures1Ueip,
},
}
}

// staticAllocator holds one subscriber's configured address.
func staticAllocator(t *testing.T) *context.IPAllocator {
t.Helper()

allocator, err := context.NewIPAllocator(staticPool)
if err != nil {
t.Fatalf("NewIPAllocator(%q): %v", staticPool, err)
}
allocator.ReserveStaticIps(&map[string]string{staticSupi: staticAddr})

return allocator
}

// session returns a session for supi holding the address the allocator gives it.
func session(t *testing.T, supi string) *context.SMContext {
t.Helper()

allocator := staticAllocator(t)

ip, err := allocator.Allocate(supi)
if err != nil {
t.Fatalf("Allocate(%q): %v", supi, err)
}

return &context.SMContext{
Supi: supi,
Dnn: testDnn,
PDUAddress: &context.UeIpAddr{
Ip: ip,
Static: allocator.StaticIpForImsi(supi) != nil,
},
DNNInfo: &context.SnssaiSmfDnnInfo{UeIPAllocator: allocator},
Tunnel: &context.UPTunnel{
ANInformation: struct {
IPAddress net.IP
TEID uint32
}{IPAddress: net.IP{10, 0, 0, 1}, TEID: 12345},
},
}
}

func uplinkNode(upf *context.UPF) *context.DataPathNode {
return &context.DataPathNode{
UPF: upf,
UpLinkTunnel: &context.GTPTunnel{
PDR: map[string]*context.PDR{"default": {FAR: &context.FAR{}}},
},
}
}

func downlinkNode(upf *context.UPF) *context.DataPathNode {
return &context.DataPathNode{
UPF: upf,
DownLinkTunnel: &context.GTPTunnel{
PDR: map[string]*context.PDR{"default": {FAR: &context.FAR{}}},
},
}
}

// Asking a UPF that allocates to choose (CHV4) discards staticIpInfo in silence: it allocates from
// its own pool, and the SMF adopts that address and releases the configured one.
func TestStaticUeIpIsNamedInTheRuleWhenTheUpfAllocates(t *testing.T) {
want := net.ParseIP(staticAddr).To4()

t.Run("uplink", func(t *testing.T) {
smContext := session(t, staticSupi)
dpNode := uplinkNode(ueipUpf())

if err := dpNode.ActivateUpLinkPdr(smContext, &context.QER{}, 10); err != nil {
t.Fatalf("ActivateUpLinkPdr: %v", err)
}

assertNamesAddress(t, dpNode.UpLinkTunnel.PDR["default"], want)
})

t.Run("downlink", func(t *testing.T) {
smContext := session(t, staticSupi)
dpNode := downlinkNode(ueipUpf())

dataPath := &context.DataPath{FirstDPNode: dpNode}
if err := dpNode.ActivateDlLinkPdr(smContext, &context.QER{}, 10, dataPath); err != nil {
t.Fatalf("ActivateDlLinkPdr: %v", err)
}

assertNamesAddress(t, dpNode.DownLinkTunnel.PDR["default"], want)
})
}

func assertNamesAddress(t *testing.T, pdr *context.PDR, want net.IP) {
t.Helper()

if pdr == nil || pdr.PDI.UEIPAddress == nil {
t.Fatalf("rule carries no UE IP Address IE")
}
if pdr.PDI.UEIPAddress.CHV4 {
t.Errorf("CHV4 set: the UPF is asked to choose an address although %s is configured for %s",
want, staticSupi)
}
if !pdr.PDI.UEIPAddress.V4 {
t.Errorf("V4 unset, so the rule names no address")
}
if !pdr.PDI.UEIPAddress.Ipv4Address.Equal(want) {
t.Errorf("rule names %v, want the configured %v", pdr.PDI.UEIPAddress.Ipv4Address, want)
}
}

// A subscriber with no configured address is still left to the UPF's allocator.
func TestDynamicUeIpStillAsksTheUpfToChoose(t *testing.T) {
smContext := session(t, "imsi-001010100007511")
dpNode := uplinkNode(ueipUpf())

if err := dpNode.ActivateUpLinkPdr(smContext, &context.QER{}, 10); err != nil {
t.Fatalf("ActivateUpLinkPdr: %v", err)
}

ueIP := dpNode.UpLinkTunnel.PDR["default"].PDI.UEIPAddress
if ueIP == nil {
t.Fatalf("rule carries no UE IP Address IE")
}
if !ueIP.CHV4 {
t.Errorf("CHV4 unset for a subscriber with no configured address, so the SMF's own choice "+
"(%v) is imposed on a UPF that allocates", ueIP.Ipv4Address)
}
if ueIP.V4 {
t.Errorf("V4 set alongside CHV4, which TS 29.244 clause 8.2.62 does not allow: the two are "+
"mutually exclusive, and the address carried (%v) is not the one the UE will hold",
ueIP.Ipv4Address)
}
}

// A rule rebuilt for a session that outlived the SMF names the address the UE holds, which only
// the session carries: a restored SMContext has an allocator that marshalled as {}.
func TestStaticUeIpSurvivesIntoARestoredSession(t *testing.T) {
restored := &context.SMContext{
Supi: staticSupi,
Dnn: testDnn,
PDUAddress: &context.UeIpAddr{
Ip: net.ParseIP(staticAddr).To4(),
Static: true,
},
DNNInfo: &context.SnssaiSmfDnnInfo{UeIPAllocator: &context.IPAllocator{}},
}

if !restored.HasStaticUeIpAddr() {
t.Fatalf("a restored session with a configured address does not report one")
}

dpNode := uplinkNode(ueipUpf())
if err := dpNode.ActivateUpLinkPdr(restored, &context.QER{}, 10); err != nil {
t.Fatalf("ActivateUpLinkPdr: %v", err)
}

assertNamesAddress(t, dpNode.UpLinkTunnel.PDR["default"], net.ParseIP(staticAddr).To4())
}

// ReleaseUeIpAddr writes 0.0.0.0 back, and the user plane reads it as any address, so a rule
// built after that must ask rather than name it. producer/callback.go guards the same sentinel.
func TestTheReleasedAddressSentinelIsNotNamedInARule(t *testing.T) {
smContext := session(t, staticSupi)
smContext.PDUAddress = &context.UeIpAddr{Ip: net.IPv4zero.To4(), Static: true}

dpNode := uplinkNode(ueipUpf())
if err := dpNode.ActivateUpLinkPdr(smContext, &context.QER{}, 10); err != nil {
t.Fatalf("ActivateUpLinkPdr: %v", err)
}

ueIP := dpNode.UpLinkTunnel.PDR["default"].PDI.UEIPAddress
if ueIP == nil {
t.Fatalf("rule carries no UE IP Address IE")
}
if !ueIP.CHV4 {
t.Errorf("rule names %v, which matches every subscriber", ueIP.Ipv4Address)
}
}

// A UPF that allocated over a static address leaves Static set and UpfProvided true. The address
// the session holds is then the UPF's, so it is not reported as the configured one.
func TestAStaticAddressTheUpfOverrodeIsNoLongerStatic(t *testing.T) {
smContext := session(t, staticSupi)
if !smContext.HasStaticUeIpAddr() {
t.Fatalf("%s does not hold its configured address to begin with", staticSupi)
}

// What pfcp/handler and pfcp/adapter do with an address out of the UPF's own pool.
smContext.PDUAddress.Ip = net.IPv4(192, 168, 100, 3).To4()
smContext.PDUAddress.UpfProvided = true

if smContext.HasStaticUeIpAddr() {
t.Errorf("%v is reported as %s's configured address %s", smContext.PDUAddress.Ip, staticSupi, staticAddr)
}
}
26 changes: 16 additions & 10 deletions context/ip_allocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,23 @@ func IPAddrOffset(in, base net.IP) int {
return offset
}

// StaticIpForImsi returns the address configured for imsi in staticIpInfo, or nil for none.
func (a *IPAllocator) StaticIpForImsi(imsi string) net.IP {
if a.g.staticIps == nil {
return nil
}
ipStr := (*a.g.staticIps)[imsi]
if ipStr == "" {
return nil
}
return net.ParseIP(ipStr).To4()
}

// Allocate will allocate the IP address and returns it
func (a *IPAllocator) Allocate(imsi string) (net.IP, error) {
// check if static IP already reserved for this IMSI
if a.g.staticIps != nil {
staticIps := *a.g.staticIps
if ipStr := staticIps[imsi]; ipStr != "" {
return net.ParseIP(ipStr).To4(), nil
}
if ip := a.StaticIpForImsi(imsi); ip != nil {
return ip, nil
}

// The offset is the address's place in the pool; nothing is added to it. The old
Expand Down Expand Up @@ -117,11 +126,8 @@ func (a *IPAllocator) BlockIp(ip net.IP) {

func (a *IPAllocator) Release(imsi string, ip net.IP) {
// Don't release static IPs
if a.g.staticIps != nil {
staticIps := *a.g.staticIps
if ipStr := staticIps[imsi]; ipStr != "" {
return
}
if a.StaticIpForImsi(imsi) != nil {
return
}

offset := IPAddrOffset(ip, a.ipNetwork.IP)
Expand Down
17 changes: 17 additions & 0 deletions context/sm_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func decSMContextActive() uint64 {
type UeIpAddr struct {
Ip net.IP
UpfProvided bool
Static bool // Ip was allocated from staticIpInfo; ask HasStaticUeIpAddr, which UpfProvided overrides
}

type SMContext struct {
Expand Down Expand Up @@ -936,6 +937,22 @@ func GetSMContextBySEID(SEID uint64) (smContext *SMContext) {
return
}

// HasStaticUeIpAddr reports whether the address this session holds came from staticIpInfo.
//
// Static is what was allocated; UpfProvided is what replaced it, and the 0.0.0.0 ReleaseUeIpAddr
// writes back is not an address at all. Either answers no, because a rule may name only an
// address the UE actually holds.
func (smContext *SMContext) HasStaticUeIpAddr() bool {
if smContext.PDUAddress == nil || smContext.PDUAddress.Ip == nil {
return false
}
if smContext.PDUAddress.UpfProvided || smContext.PDUAddress.Ip.IsUnspecified() {
return false
}

return smContext.PDUAddress.Static
}

func (smContext *SMContext) ReleaseUeIpAddr() error {
if smContext.PDUAddress == nil {
logger.CtxLog.Warnf("ReleaseUeIpAddr: PduSessionUeAddress is nil, skipping release")
Expand Down
6 changes: 5 additions & 1 deletion producer/pdu_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,11 @@ func HandlePDUSessionSMContextCreate(eventData any) error {
txn.Rsp = smContext.GeneratePDUSessionEstablishmentReject("IpAllocError")
return fmt.Errorf("IpAllocError")
} else {
smContext.PDUAddress = &smf_context.UeIpAddr{Ip: ip, UpfProvided: false}
smContext.PDUAddress = &smf_context.UeIpAddr{
Ip: ip,
UpfProvided: false,
Static: smContext.DNNInfo.UeIPAllocator.StaticIpForImsi(smContext.Supi) != nil,
}
smContext.SubPduSessLog.Debugf("PDUSessionSMContextCreate, IP alloc success IP[%s]",
smContext.PDUAddress.Ip.String())
}
Expand Down
Loading