diff --git a/go.mod b/go.mod index 6469ff7..c1f1c54 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.7 require ( github.com/adrg/xdg v0.5.3 github.com/gin-gonic/gin v1.10.0 - github.com/nextmn/go-pfcp-networking v0.0.39 + github.com/nextmn/go-pfcp-networking v0.0.40-0.20241210144909-788cba7178ed github.com/nextmn/json-api v0.0.14 github.com/nextmn/logrus-formatter v0.0.1 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index 35be5e4..b1bbe7c 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nextmn/go-pfcp-networking v0.0.39 h1:8LDz3O0pjQ3PPLGDnds3z369mB7xBEkTtLqAzMMrtFE= github.com/nextmn/go-pfcp-networking v0.0.39/go.mod h1:KYoKLiltDmHL2YMU5mz2k/E1xMoz4TpmzTz6Nr5u5gA= +github.com/nextmn/go-pfcp-networking v0.0.40-0.20241209181313-d2a4f5e5d557 h1:8DVrxu7roGv1vs+5Su1lbD23GQApzhP+h6Rk1SWiuDQ= +github.com/nextmn/go-pfcp-networking v0.0.40-0.20241209181313-d2a4f5e5d557/go.mod h1:KYoKLiltDmHL2YMU5mz2k/E1xMoz4TpmzTz6Nr5u5gA= +github.com/nextmn/go-pfcp-networking v0.0.40-0.20241210142758-3ca58ba7258d h1:RRrz3IANGdLTRm55J7AxUR6Ger5SYXFduzAaQA6FuRc= +github.com/nextmn/go-pfcp-networking v0.0.40-0.20241210142758-3ca58ba7258d/go.mod h1:KYoKLiltDmHL2YMU5mz2k/E1xMoz4TpmzTz6Nr5u5gA= +github.com/nextmn/go-pfcp-networking v0.0.40-0.20241210144909-788cba7178ed h1:Rum21YTERWhYzO7WipfKymhbzH0MYh6JG4U839a2b1Y= +github.com/nextmn/go-pfcp-networking v0.0.40-0.20241210144909-788cba7178ed/go.mod h1:KYoKLiltDmHL2YMU5mz2k/E1xMoz4TpmzTz6Nr5u5gA= github.com/nextmn/json-api v0.0.14 h1:m4uHOVcXsxkXoxbrhqemLTRG4T86eYkejjirew1nDUU= github.com/nextmn/json-api v0.0.14/go.mod h1:CQXeNPj9MDGsEExtnqJFIGjLgZAKsmOoO2fy+mep7Ak= github.com/nextmn/logrus-formatter v0.0.1 h1:Bsf78jjiEESc+rV8xE6IyKj4frDPGMwXFNrLQzm6A1E= diff --git a/internal/amf/amf.go b/internal/amf/amf.go new file mode 100644 index 0000000..7dbb47a --- /dev/null +++ b/internal/amf/amf.go @@ -0,0 +1,88 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package amf + +import ( + "context" + "net" + "net/http" + "net/netip" + "time" + + "github.com/nextmn/cp-lite/internal/smf" + + "github.com/nextmn/json-api/healthcheck" + "github.com/nextmn/json-api/jsonapi" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +type Amf struct { + control jsonapi.ControlURI + client http.Client + userAgent string + smf *smf.Smf + srv *http.Server +} + +func NewAmf(bindAddr netip.AddrPort, control jsonapi.ControlURI, userAgent string, smf *smf.Smf) *Amf { + amf := Amf{ + control: control, + client: http.Client{}, + userAgent: userAgent, + smf: smf, + } + // TODO: gin.SetMode(gin.DebugMode) / gin.SetMode(gin.ReleaseMode) depending on log level + r := gin.Default() + r.GET("/status", Status) + + // PDU Sessions + r.POST("/ps/establishment-request", amf.EstablishmentRequest) + r.POST("/ps/n2-establishment-response", amf.N2EstablishmentResponse) + + logrus.WithFields(logrus.Fields{"http-addr": bindAddr}).Info("HTTP Server created") + amf.srv = &http.Server{ + Addr: bindAddr.String(), + Handler: r, + } + + return &amf +} + +func (amf *Amf) Start(ctx context.Context) error { + l, err := net.Listen("tcp", amf.srv.Addr) + if err != nil { + return err + } + go func(ln net.Listener) { + logrus.Info("Starting HTTP Server") + if err := amf.srv.Serve(ln); err != nil && err != http.ErrServerClosed { + logrus.WithError(err).Error("Http Server error") + } + }(l) + go func(ctx context.Context) { + select { + case <-ctx.Done(): + ctxShutdown, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + if err := amf.srv.Shutdown(ctxShutdown); err != nil { + logrus.WithError(err).Info("HTTP Server Shutdown") + } + } + }(ctx) + + return nil +} + +// get status of the controller +func Status(c *gin.Context) { + status := healthcheck.Status{ + Ready: true, + } + c.Header("Cache-Control", "no-cache") + c.JSON(http.StatusOK, status) +} diff --git a/internal/amf/establishment_request.go b/internal/amf/establishment_request.go new file mode 100644 index 0000000..702aaf4 --- /dev/null +++ b/internal/amf/establishment_request.go @@ -0,0 +1,67 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package amf + +import ( + "bytes" + "encoding/json" + "net/http" + + "github.com/nextmn/json-api/jsonapi" + "github.com/nextmn/json-api/jsonapi/n1n2" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +func (amf *Amf) EstablishmentRequest(c *gin.Context) { + var ps n1n2.PduSessionEstabReqMsg + if err := c.BindJSON(&ps); err != nil { + logrus.WithError(err).Error("could not deserialize") + c.JSON(http.StatusBadRequest, jsonapi.MessageWithError{Message: "could not deserialize", Error: err}) + return + } + logrus.WithFields(logrus.Fields{ + "ue": ps.Ue.String(), + "gnb": ps.Gnb.String(), + "dnn": ps.Dnn, + }).Info("New PDU Session establishment Request") + + pduSession, err := amf.smf.CreateSessionUplink(c, ps.Ue, ps.Gnb, ps.Dnn) + if err != nil { + c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "could not create pdu session uplink", Error: err}) + return + } + + // send PseAccept to UE + n2PsReq := n1n2.N2PduSessionReqMsg{ + Cp: amf.control, + UeInfo: n1n2.PduSessionEstabAcceptMsg{ + Header: ps, + Addr: pduSession.UeIpAddr, + }, + Upf: pduSession.UplinkFteid.Addr, + UplinkTeid: pduSession.UplinkFteid.Teid, + } + reqBody, err := json.Marshal(n2PsReq) + if err != nil { + c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "could not marshal json", Error: err}) + return + } + req, err := http.NewRequestWithContext(c, http.MethodPost, ps.Gnb.JoinPath("ps/n2-establishment-request").String(), bytes.NewBuffer(reqBody)) + if err != nil { + c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "could not create request", Error: err}) + return + } + req.Header.Set("User-Agent", amf.userAgent) + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + resp, err := amf.client.Do(req) + if err != nil { + c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "no http response", Error: err}) + return + } + defer resp.Body.Close() +} diff --git a/internal/amf/n2_establishment_response.go b/internal/amf/n2_establishment_response.go new file mode 100644 index 0000000..23af6eb --- /dev/null +++ b/internal/amf/n2_establishment_response.go @@ -0,0 +1,46 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package amf + +import ( + "net/http" + + "github.com/nextmn/json-api/jsonapi" + "github.com/nextmn/json-api/jsonapi/n1n2" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +func (amf *Amf) N2EstablishmentResponse(c *gin.Context) { + var ps n1n2.N2PduSessionRespMsg + if err := c.BindJSON(&ps); err != nil { + logrus.WithError(err).Error("could not deserialize") + c.JSON(http.StatusBadRequest, jsonapi.MessageWithError{Message: "could not deserialize", Error: err}) + return + } + pduSession, err := amf.smf.CreateSessionDownlink(c, ps.UeInfo.Header.Ue, ps.UeInfo.Header.Dnn, ps.Gnb, ps.DownlinkTeid) + if err != nil { + logrus.WithError(err).WithFields(logrus.Fields{ + "ue-ip-addr": ps.UeInfo.Addr, + "ue": ps.UeInfo.Header.Ue, + "gnb": ps.UeInfo.Header.Gnb, + "dnn": ps.UeInfo.Header.Dnn, + }).Error("could not create downlink path") + c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "could not create downlink path", Error: err}) + return + } + logrus.WithFields(logrus.Fields{ + "ue": ps.UeInfo.Header.Ue.String(), + "gnb": ps.UeInfo.Header.Gnb.String(), + "ip-addr": ps.UeInfo.Addr, + "gtp-upf": pduSession.UplinkFteid.Addr, + "gtp-uplink-teid": pduSession.UplinkFteid.Teid, + "gtp-gnb": pduSession.DownlinkFteid.Addr, + "gtp-downlink-teid": pduSession.DownlinkFteid.Teid, + "dnn": ps.UeInfo.Header.Dnn, + }).Info("New PDU Session Established") +} diff --git a/internal/app/control.go b/internal/app/control.go deleted file mode 100644 index 86a2545..0000000 --- a/internal/app/control.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. -// Use of this source code is governed by a MIT-style license that can be -// found in the LICENSE file. -// SPDX-License-Identifier: MIT - -package app - -import ( - "context" - "net" - "net/http" - "net/netip" - "time" - - "github.com/nextmn/json-api/healthcheck" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -type HttpServerEntity struct { - srv *http.Server - ps *PduSessions -} - -func NewHttpServerEntity(bindAddr netip.AddrPort, ps *PduSessions) *HttpServerEntity { - // TODO: gin.SetMode(gin.DebugMode) / gin.SetMode(gin.ReleaseMode) depending on log level - r := gin.Default() - r.GET("/status", Status) - - // PDU Sessions - r.POST("/ps/establishment-request", ps.EstablishmentRequest) - r.POST("/ps/n2-establishment-response", ps.N2EstablishmentResponse) - - logrus.WithFields(logrus.Fields{"http-addr": bindAddr}).Info("HTTP Server created") - e := HttpServerEntity{ - srv: &http.Server{ - Addr: bindAddr.String(), - Handler: r, - }, - ps: ps, - } - return &e -} - -func (e *HttpServerEntity) Start() error { - l, err := net.Listen("tcp", e.srv.Addr) - if err != nil { - return err - } - go func(ln net.Listener) { - logrus.Info("Starting HTTP Server") - if err := e.srv.Serve(ln); err != nil && err != http.ErrServerClosed { - logrus.WithError(err).Error("Http Server error") - } - }(l) - return nil -} - -func (e *HttpServerEntity) Stop() { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) // context.Background() is already Done() - defer cancel() - if err := e.srv.Shutdown(ctx); err != nil { - logrus.WithError(err).Info("HTTP Server Shutdown") - } -} - -// get status of the controller -func Status(c *gin.Context) { - status := healthcheck.Status{ - Ready: true, - } - c.Header("Cache-Control", "no-cache") - c.JSON(http.StatusOK, status) -} diff --git a/internal/app/pdu_session.go b/internal/app/pdu_session.go deleted file mode 100644 index 2cacb4b..0000000 --- a/internal/app/pdu_session.go +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. -// Use of this source code is governed by a MIT-style license that can be -// found in the LICENSE file. -// SPDX-License-Identifier: MIT - -package app - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "math/rand" - "net/http" - "net/netip" - "strings" - "sync" - "time" - - "github.com/nextmn/cp-lite/internal/config" - - "github.com/nextmn/json-api/jsonapi" - "github.com/nextmn/json-api/jsonapi/n1n2" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -type Pool struct { - pool netip.Prefix - current netip.Addr -} - -func NewPool(pool netip.Prefix) *Pool { - return &Pool{ - pool: pool, - current: pool.Addr(), - } -} - -func (p *Pool) Next() (netip.Addr, error) { - addr := p.current.Next() - p.current = addr - if !p.pool.Contains(addr) { - return addr, fmt.Errorf("out of range") - } - return addr, nil -} - -type PduSessions struct { - PduSessionsMap sync.Map // key: UE 5G IP ; value: PduSession - UpfMap sync.Map // Upfipaddr : UpfTeids - Client http.Client - Control jsonapi.ControlURI - UserAgent string - Slices map[string]config.Slice - Pools map[string]*Pool - pfcp *PFCPServer -} - -type PduSession struct { - Upf netip.Addr - UpfN3 netip.Addr - UplinkTeid uint32 - Gnb netip.Addr - DownlinkTeid uint32 -} - -func NewPduSessions(control jsonapi.ControlURI, slices map[string]config.Slice, pfcp *PFCPServer, userAgent string) *PduSessions { - pools := make(map[string]*Pool) - for name, p := range slices { - pools[name] = NewPool(p.Pool) - } - return &PduSessions{ - PduSessionsMap: sync.Map{}, - UpfMap: sync.Map{}, - Client: http.Client{}, - Control: control, - UserAgent: userAgent, - Slices: slices, - Pools: pools, - pfcp: pfcp, - } -} - -type UpfTeids struct { - Teids sync.Map // teid: ue 5G ipaddr -} - -func (p *PduSessions) EstablishmentRequest(c *gin.Context) { - var ps n1n2.PduSessionEstabReqMsg - if err := c.BindJSON(&ps); err != nil { - logrus.WithError(err).Error("could not deserialize") - c.JSON(http.StatusBadRequest, jsonapi.MessageWithError{Message: "could not deserialize", Error: err}) - return - } - logrus.WithFields(logrus.Fields{ - "ue": ps.Ue.String(), - "gnb": ps.Gnb.String(), - "dnn": ps.Dnn, - }).Info("New PDU Session establishment Request") - - // allocate new ue ip addr - pool, ok := p.Pools[ps.Dnn] - if !ok { - logrus.WithFields(logrus.Fields{ - "dnn": ps.Dnn, - }).Error("unknown pool") - c.JSON(http.StatusInternalServerError, jsonapi.Message{Message: "unknown pool"}) - return - } - UeIpAddr, err := pool.Next() - if err != nil { - logrus.WithError(err).Error("no address available in pool") - c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "no address available in pool", Error: err}) - return - } - - upf := p.Slices[ps.Dnn].Upfs[0] - upfTeids := &UpfTeids{} - l, ok := p.UpfMap.Load(upf.NodeID) - if !ok { - p.UpfMap.Store(upf.NodeID, upfTeids) - } else { - upfTeids = l.(*UpfTeids) - } - ctxTimeout, cancel := context.WithTimeout(c, 100*time.Millisecond) - defer cancel() - done := false - var teid uint32 = 0 - for !done { - select { - case <-ctxTimeout.Done(): - logrus.Error("could not create uplink TEID") - c.JSON(http.StatusInternalServerError, jsonapi.Message{Message: "could not create uplink TEID"}) - return - default: - teid = rand.Uint32() - if teid == 0 { - break // bad luck :( - } - if _, loaded := upfTeids.Teids.LoadOrStore(teid, UeIpAddr); !loaded { - done = true - break - } - } - } - var iface netip.Addr - ifacedone := false - for _, i := range upf.Interfaces { - if strings.ToLower(i.Type) == "n3" { - iface = i.Addr - ifacedone = true - break - } - } - if !ifacedone { - logrus.Error("could not find n3 interface on first upf") - c.JSON(http.StatusInternalServerError, jsonapi.Message{Message: "could not find n3 interface on first upf"}) - return - } - // allocate uplink teid - pduSession := PduSession{ - Upf: upf.NodeID, - UpfN3: iface, - UplinkTeid: teid, - } - - p.PduSessionsMap.Store(UeIpAddr, pduSession) - - // send PseAccept to UE - n2PsReq := n1n2.N2PduSessionReqMsg{ - Cp: p.Control, - UeInfo: n1n2.PduSessionEstabAcceptMsg{ - Header: ps, - Addr: UeIpAddr, - }, - Upf: pduSession.UpfN3, - UplinkTeid: pduSession.UplinkTeid, - } - reqBody, err := json.Marshal(n2PsReq) - if err != nil { - c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "could not marshal json", Error: err}) - return - } - req, err := http.NewRequestWithContext(c, http.MethodPost, ps.Gnb.JoinPath("ps/n2-establishment-request").String(), bytes.NewBuffer(reqBody)) - if err != nil { - c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "could not create request", Error: err}) - return - } - req.Header.Set("User-Agent", p.UserAgent) - req.Header.Set("Content-Type", "application/json; charset=UTF-8") - resp, err := p.Client.Do(req) - if err != nil { - c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "no http response", Error: err}) - return - } - defer resp.Body.Close() -} - -func (p *PduSessions) N2EstablishmentResponse(c *gin.Context) { - var ps n1n2.N2PduSessionRespMsg - if err := c.BindJSON(&ps); err != nil { - logrus.WithError(err).Error("could not deserialize") - c.JSON(http.StatusBadRequest, jsonapi.MessageWithError{Message: "could not deserialize", Error: err}) - return - } - pduSession, ok := p.PduSessionsMap.LoadAndDelete(ps.UeInfo.Addr) - if !ok { - logrus.Error("No PDU Session establishment procedure started for this UE") - c.JSON(http.StatusInternalServerError, jsonapi.Message{Message: "no pdu session establishment procedure started for this UE"}) - return - } - - psStruct := pduSession.(PduSession) - - psStruct.DownlinkTeid = ps.DownlinkTeid - psStruct.Gnb = ps.Gnb - p.PduSessionsMap.Store(ps.UeInfo.Addr, psStruct) - logrus.WithFields(logrus.Fields{ - "ue": ps.UeInfo.Header.Ue.String(), - "gnb": ps.UeInfo.Header.Gnb.String(), - "ip-addr": ps.UeInfo.Addr, - "upf-pfcp": psStruct.Upf, - "gtp-upf": psStruct.UpfN3, - "gtp-uplink-teid": psStruct.UplinkTeid, - "gtp-downlink-teid": psStruct.DownlinkTeid, - "gtp-gnb": psStruct.Gnb, - "dnn": ps.UeInfo.Header.Dnn, - }).Info("New PDU Session Established") - - err := p.pfcp.CreateSession(ps.UeInfo.Addr, psStruct.UplinkTeid, psStruct.DownlinkTeid, psStruct.Upf, psStruct.UpfN3, psStruct.Gnb, ps.UeInfo.Header.Dnn) - if err != nil { - logrus.WithError(err).Error("Could not configure PDR/FAR in UPF") - c.JSON(http.StatusInternalServerError, jsonapi.MessageWithError{Message: "could not configure PDR/FAR in UPF", Error: err}) - return - } - -} diff --git a/internal/app/pfcp.go b/internal/app/pfcp.go deleted file mode 100644 index 513e9fc..0000000 --- a/internal/app/pfcp.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. -// Use of this source code is governed by a MIT-style license that can be -// found in the LICENSE file. -// SPDX-License-Identifier: MIT - -package app - -import ( - "context" - "fmt" - "net/netip" - "time" - - "github.com/nextmn/cp-lite/internal/config" - - pfcp "github.com/nextmn/go-pfcp-networking/pfcp" - pfcpapi "github.com/nextmn/go-pfcp-networking/pfcp/api" - - "github.com/sirupsen/logrus" - "github.com/wmnsk/go-pfcp/ie" -) - -type PFCPServer struct { - srv *pfcp.PFCPEntityCP - slices map[string]config.Slice - associations map[netip.Addr]pfcpapi.PFCPAssociationInterface -} - -func NewPFCPServer(addr netip.Addr, slices map[string]config.Slice) *PFCPServer { - return &PFCPServer{ - srv: pfcp.NewPFCPEntityCP(addr.String(), addr.String()), - slices: slices, - associations: make(map[netip.Addr]pfcpapi.PFCPAssociationInterface), - } -} - -func (p *PFCPServer) Start(ctx context.Context) error { - logrus.Info("PFCP Server started") - go func() { - err := p.srv.ListenAndServeContext(ctx) - if err != nil { - logrus.WithError(err).Trace("PFCP server stopped") - } - }() - ctxTimeout, cancel := context.WithTimeout(ctx, 100*time.Millisecond) - defer cancel() - done := false - for !done { - select { - case <-time.After(10 * time.Millisecond): //FIXME: this should not be required - if p.srv.RecoveryTimeStamp() != nil { - done = true - break - } - case <-ctxTimeout.Done(): - return ctx.Err() - - } - } - for _, slice := range p.slices { - for _, upf := range slice.Upfs { - association, err := p.srv.NewEstablishedPFCPAssociation(ie.NewNodeIDHeuristic(upf.NodeID.String())) - if err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "upf": upf.NodeID, - }).Error("Could not perform PFCP association") - return err - } - p.associations[upf.NodeID] = association - } - } - logrus.Info("PFCP Associations complete") - return nil -} - -func (p *PFCPServer) CreateSession(ue netip.Addr, uplinkTeid uint32, downlinkTeid uint32, upfI netip.Addr, upfIn3 netip.Addr, gNB netip.Addr, slice string) error { - a, ok := p.associations[upfI] - if !ok { - return fmt.Errorf("Could not create PFCP Session: not associated with UPF") - } - // TODO: don't hardcode pdr/far ids - pdrIes := []*ie.IE{ - // uplink - ie.NewCreatePDR(ie.NewPDRID(1), ie.NewPrecedence(255), - ie.NewPDI( - ie.NewSourceInterface(ie.SrcInterfaceAccess), - ie.NewFTEID(0x01, uplinkTeid, upfIn3.AsSlice(), nil, 0), // ipv4: 0x01 - ie.NewNetworkInstance(slice), - ie.NewUEIPAddress(0x02, ue.String(), "", 0, 0), // ipv4: 0x02 - ), - ie.NewOuterHeaderRemoval(0x00, 0), // remove gtp-u/udp/ipv4: 0x00 - ie.NewFARID(1), - ), - // downlink - ie.NewCreatePDR(ie.NewPDRID(2), ie.NewPrecedence(255), - ie.NewPDI(ie.NewSourceInterface(ie.SrcInterfaceCore), - ie.NewNetworkInstance(slice), - ie.NewUEIPAddress(0x02, ue.String(), "", 0, 0), // ipv4: 0x02 - ), - ie.NewFARID(2), - ), - } - farIes := []*ie.IE{ - // uplink - ie.NewCreateFAR(ie.NewFARID(1), - ie.NewApplyAction(0x02), // FORW - ie.NewForwardingParameters( - ie.NewDestinationInterface(ie.DstInterfaceCore), - ie.NewNetworkInstance(slice), - ), - ), - // downlink - ie.NewCreateFAR(ie.NewFARID(2), - ie.NewApplyAction(0x02), // FORW - ie.NewForwardingParameters( - ie.NewDestinationInterface(ie.DstInterfaceAccess), - ie.NewNetworkInstance(slice), - ie.NewOuterHeaderCreation( - 0x0100, // GTP/UDP/IPv4 - downlinkTeid, - gNB.String(), - "", 0, 0, 0, - ), - ), - ), - } - pdrs, err, _, _ := pfcp.NewPDRMap(pdrIes) - if err != nil { - return err - } - fars, err, _, _ := pfcp.NewFARMap(farIes) - if err != nil { - return err - } - _, err = a.CreateSession(nil, pdrs, fars) - if err != nil { - return err - } - return nil -} diff --git a/internal/app/setup.go b/internal/app/setup.go index 8dd4e24..8c3953a 100644 --- a/internal/app/setup.go +++ b/internal/app/setup.go @@ -8,29 +8,30 @@ package app import ( "context" + "github.com/nextmn/cp-lite/internal/amf" "github.com/nextmn/cp-lite/internal/config" + "github.com/nextmn/cp-lite/internal/smf" ) type Setup struct { - config *config.CPConfig - httpServerEntity *HttpServerEntity - pfcp *PFCPServer + config *config.CPConfig + amf *amf.Amf + smf *smf.Smf } func NewSetup(config *config.CPConfig) *Setup { - pfcp := NewPFCPServer(config.Pfcp, config.Slices) - ps := NewPduSessions(config.Control.Uri, config.Slices, pfcp, "go-github-nextmn-cp-lite") + smf := smf.NewSmf(config.Pfcp, config.Slices) return &Setup{ - config: config, - httpServerEntity: NewHttpServerEntity(config.Control.BindAddr, ps), - pfcp: pfcp, + config: config, + amf: amf.NewAmf(config.Control.BindAddr, config.Control.Uri, "go-github-nextmn-cp-lite", smf), + smf: smf, } } func (s *Setup) Init(ctx context.Context) error { - if err := s.pfcp.Start(ctx); err != nil { + if err := s.smf.Start(ctx); err != nil { return err } - if err := s.httpServerEntity.Start(); err != nil { + if err := s.amf.Start(ctx); err != nil { return err } return nil @@ -48,6 +49,5 @@ func (s *Setup) Run(ctx context.Context) error { } func (s *Setup) Exit() error { - s.httpServerEntity.Stop() return nil } diff --git a/internal/smf/errors.go b/internal/smf/errors.go new file mode 100644 index 0000000..46e48cd --- /dev/null +++ b/internal/smf/errors.go @@ -0,0 +1,22 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "errors" +) + +var ( + ErrDnnNotFound = errors.New("DNN not found") + ErrPDUSessionAlreadyExists = errors.New("PDU Session already exists") + ErrPDUSessionNotFound = errors.New("PDU Session not found") + + ErrUpfNotAssociated = errors.New("UPF not associated") + ErrUpfNotFound = errors.New("UPF not found") + ErrInterfaceNotFound = errors.New("interface not found") + ErrNoPFCPRule = errors.New("no PFCP rule to push") + ErrNoIpAvailableInPool = errors.New("no IP address available in pool") +) diff --git a/internal/smf/fteid.go b/internal/smf/fteid.go new file mode 100644 index 0000000..1807e37 --- /dev/null +++ b/internal/smf/fteid.go @@ -0,0 +1,15 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "net/netip" +) + +type Fteid struct { + Addr netip.Addr + Teid uint32 +} diff --git a/internal/smf/pdu-session-n3.go b/internal/smf/pdu-session-n3.go new file mode 100644 index 0000000..bc9b1f5 --- /dev/null +++ b/internal/smf/pdu-session-n3.go @@ -0,0 +1,16 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "net/netip" +) + +type PduSessionN3 struct { + UeIpAddr netip.Addr + UplinkFteid *Fteid + DownlinkFteid *Fteid +} diff --git a/internal/smf/pfcp_helper.go b/internal/smf/pfcp_helper.go new file mode 100644 index 0000000..cde52e8 --- /dev/null +++ b/internal/smf/pfcp_helper.go @@ -0,0 +1,15 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +// PFCP Constants +const ( + FteidTypeIPv4 = 0x01 + UEIpAddrTypeIPv4 = 0x02 + OuterHeaderRemoveGtpuUdpIpv4 = 0x00 + ApplyActionForw = 0x02 + OuterHeaderCreationGtpuUdpIpv4 = 0x0100 +) diff --git a/internal/smf/pfcp_rules.go b/internal/smf/pfcp_rules.go new file mode 100644 index 0000000..c5be563 --- /dev/null +++ b/internal/smf/pfcp_rules.go @@ -0,0 +1,35 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "sync" + + pfcpapi "github.com/nextmn/go-pfcp-networking/pfcp/api" + + "github.com/wmnsk/go-pfcp/ie" +) + +type Pfcprules struct { + createpdrs []*ie.IE + createfars []*ie.IE + updatepdrs []*ie.IE + updatefars []*ie.IE + currentpdrid uint16 + currentfarid uint32 + session pfcpapi.PFCPSessionInterface + + sync.Mutex +} + +func NewPfcpRules() *Pfcprules { + return &Pfcprules{ + createpdrs: make([]*ie.IE, 0), + createfars: make([]*ie.IE, 0), + updatepdrs: make([]*ie.IE, 0), + updatefars: make([]*ie.IE, 0), + } +} diff --git a/internal/smf/sessions.go b/internal/smf/sessions.go new file mode 100644 index 0000000..c81a536 --- /dev/null +++ b/internal/smf/sessions.go @@ -0,0 +1,14 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "sync" +) + +type SessionsMap struct { + sync.Map // key: UE Ctrl ; value: *PduSessionN3 +} diff --git a/internal/smf/slice.go b/internal/smf/slice.go new file mode 100644 index 0000000..f396c6a --- /dev/null +++ b/internal/smf/slice.go @@ -0,0 +1,44 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "net/netip" + "sync" + + "github.com/nextmn/cp-lite/internal/config" +) + +type SlicesMap struct { + sync.Map // slice name: Slice +} + +func NewSlicesMap(slices map[string]config.Slice) *SlicesMap { + m := SlicesMap{} + for k, slice := range slices { + upfs := make([]netip.Addr, len(slice.Upfs)) + for i, upf := range slice.Upfs { + upfs[i] = upf.NodeID + } + sl := NewSlice(slice.Pool, upfs) + m.Store(k, sl) + } + return &m +} + +type Slice struct { + Upfs []netip.Addr + Pool *UeIpPool + sessions *SessionsMap +} + +func NewSlice(pool netip.Prefix, upfs []netip.Addr) *Slice { + return &Slice{ + Pool: NewUeIpPool(pool), + Upfs: upfs, + sessions: &SessionsMap{}, + } +} diff --git a/internal/smf/smf.go b/internal/smf/smf.go new file mode 100644 index 0000000..58e224f --- /dev/null +++ b/internal/smf/smf.go @@ -0,0 +1,216 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "context" + "net/netip" + "time" + + "github.com/nextmn/cp-lite/internal/config" + + pfcp "github.com/nextmn/go-pfcp-networking/pfcp" + "github.com/nextmn/json-api/jsonapi" + + "github.com/sirupsen/logrus" + "github.com/wmnsk/go-pfcp/ie" +) + +type UpfPath []netip.Addr + +type Smf struct { + upfs *UpfsMap + slices *SlicesMap + srv *pfcp.PFCPEntityCP + started bool +} + +func NewSmf(addr netip.Addr, slices map[string]config.Slice) *Smf { + s := NewSlicesMap(slices) + upfs := NewUpfsMap(slices) + return &Smf{ + srv: pfcp.NewPFCPEntityCP(addr.String(), addr), + slices: s, + upfs: upfs, + } +} + +func (smf *Smf) Start(ctx context.Context) error { + logrus.Info("PFCP Server started") + go func() { + err := smf.srv.ListenAndServeContext(ctx) + if err != nil { + logrus.WithError(err).Trace("PFCP server stopped") + } + }() + ctxTimeout, cancel := context.WithTimeout(ctx, 100*time.Millisecond) + defer cancel() + done := false + for !done { + select { + case <-time.After(10 * time.Millisecond): //FIXME: this should not be required + if smf.srv.RecoveryTimeStamp() != nil { + done = true + break + } + case <-ctxTimeout.Done(): + return ctx.Err() + + } + } + var failure error + smf.upfs.Range(func(key, value any) bool { + nodeId := key.(netip.Addr) + upf := value.(*Upf) + association, err := smf.srv.NewEstablishedPFCPAssociation(ie.NewNodeIDHeuristic(nodeId.String())) + if err != nil { + logrus.WithError(err).WithFields(logrus.Fields{ + "upf": nodeId, + }).Error("Could not perform PFCP association") + failure = err + return false + } + upf.Associate(association) + return true + }) + if failure != nil { + return failure + } + logrus.Info("PFCP Associations complete") + smf.started = true + return nil +} + +func (smf *Smf) CreateSessionDownlink(ctx context.Context, ueCtrl jsonapi.ControlURI, dnn string, gnb netip.Addr, gnb_teid uint32) (*PduSessionN3, error) { + // check for existing session + s, ok := smf.slices.Load(dnn) + if !ok { + return nil, ErrDnnNotFound + } + slice := s.(*Slice) + session_any, ok := slice.sessions.Load(ueCtrl) + if !ok { + return nil, ErrPDUSessionNotFound + } + session := session_any.(*PduSessionN3) + session.DownlinkFteid = &Fteid{ + Addr: gnb, + Teid: gnb_teid, + } + if len(slice.Upfs) == 0 { + return nil, ErrUpfNotFound + } + last_fteid := session.DownlinkFteid + for i, upf_ctrl := range slice.Upfs { + upf_any, ok := smf.upfs.Load(upf_ctrl) + if !ok { + return nil, ErrUpfNotFound + } + upf := upf_any.(*Upf) + var err error + var upf_iface netip.Addr + if i == 0 { + upf_iface, err = upf.GetN3() + } else if i != len(slice.Upfs)-1 { + upf_iface, err = upf.GetN6() + } + if err != nil { + return nil, err + } + if i == len(slice.Upfs)-1 { + upf.UpdateDownlinkAnchor(session.UeIpAddr, dnn, last_fteid) + } else { + last_fteid, err = upf.UpdateDownlinkIntermediate(ctx, session.UeIpAddr, dnn, upf_iface, last_fteid) + if err != nil { + return nil, err + } + } + if err := upf.UpdateSession(session.UeIpAddr); err != nil { + return nil, err + } + } + + return nil, nil +} +func (smf *Smf) CreateSessionUplink(ctx context.Context, ueCtrl jsonapi.ControlURI, gnbCtrl jsonapi.ControlURI, dnn string) (*PduSessionN3, error) { + // check for existing session + s, ok := smf.slices.Load(dnn) + if !ok { + return nil, ErrDnnNotFound + } + slice := s.(*Slice) + _, ok = slice.sessions.Load(ueCtrl) + if ok { + return nil, ErrPDUSessionAlreadyExists + } + // create ue ip addr + ueIpAddr, err := slice.Pool.Next() + if err != nil { + return nil, err + } + // create new session + // 1. check path + if len(slice.Upfs) == 0 { + return nil, ErrUpfNotFound + } + // 2. init anchor + upfa_ctrl := slice.Upfs[len(slice.Upfs)-1] + upfa_any, ok := smf.upfs.Load(upfa_ctrl) + if !ok { + return nil, ErrUpfNotFound + } + upfa := upfa_any.(*Upf) + var upfa_iface netip.Addr + if len(slice.Upfs) == 1 { + upfa_iface, err = upfa.GetN3() + } else { + upfa_iface, err = upfa.GetN6() + } + if err != nil { + return nil, err + } + last_fteid, err := upfa.CreateUplinkAnchor(ctx, ueIpAddr, dnn, upfa_iface) + if err != nil { + return nil, err + } + if err := upfa.CreateSession(ueIpAddr); err != nil { + return nil, err + } + + // 3. init path from anchor + for i := len(slice.Upfs) - 2; i >= 0; i-- { + upf_ctrl := slice.Upfs[i] + upf_any, ok := smf.upfs.Load(upf_ctrl) + if !ok { + return nil, ErrUpfNotFound + } + upf := upf_any.(*Upf) + var upf_iface netip.Addr + if i == 0 { + upf_iface, err = upf.GetN3() + } else { + upf_iface, err = upf.GetN6() + } + if err != nil { + return nil, err + } + last_fteid, err = upf.CreateUplinkIntermediate(ctx, ueIpAddr, dnn, upf_iface, last_fteid) + if err != nil { + return nil, err + } + if err := upf.CreateSession(ueIpAddr); err != nil { + return nil, err + } + } + + session := PduSessionN3{ + UeIpAddr: ueIpAddr, + UplinkFteid: last_fteid, + } + // store session + slice.sessions.Store(ueCtrl, &session) + return &session, nil +} diff --git a/internal/smf/teids_pool.go b/internal/smf/teids_pool.go new file mode 100644 index 0000000..07e853b --- /dev/null +++ b/internal/smf/teids_pool.go @@ -0,0 +1,50 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "context" + "math/rand" + "sync" +) + +type TEIDsPool struct { + teids map[uint32]struct{} + sync.Mutex +} + +func NewTEIDsPool() *TEIDsPool { + return &TEIDsPool{ + teids: make(map[uint32]struct{}), + } +} + +func (t *TEIDsPool) Next(ctx context.Context) (uint32, error) { + t.Lock() + defer t.Unlock() + var teid uint32 = 0 + for { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + teid = rand.Uint32() + if teid == 0 { + continue + } + if _, ok := t.teids[teid]; !ok { + t.teids[teid] = struct{}{} + return teid, nil + } + } + } +} + +func (t *TEIDsPool) Delete(teid uint32) { + t.Lock() + defer t.Unlock() + delete(t.teids, teid) +} diff --git a/internal/smf/ue_ip_pool.go b/internal/smf/ue_ip_pool.go new file mode 100644 index 0000000..2ad1dd2 --- /dev/null +++ b/internal/smf/ue_ip_pool.go @@ -0,0 +1,31 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "net/netip" +) + +type UeIpPool struct { + pool netip.Prefix + current netip.Addr +} + +func NewUeIpPool(pool netip.Prefix) *UeIpPool { + return &UeIpPool{ + pool: pool, + current: pool.Addr(), + } +} + +func (p *UeIpPool) Next() (netip.Addr, error) { + addr := p.current.Next() + p.current = addr + if !p.pool.Contains(addr) { + return addr, ErrNoIpAvailableInPool + } + return addr, nil +} diff --git a/internal/smf/upf.go b/internal/smf/upf.go new file mode 100644 index 0000000..96bc571 --- /dev/null +++ b/internal/smf/upf.go @@ -0,0 +1,314 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "context" + "net/netip" + "strings" + "sync" + + "github.com/nextmn/cp-lite/internal/config" + + pfcp "github.com/nextmn/go-pfcp-networking/pfcp" + pfcpapi "github.com/nextmn/go-pfcp-networking/pfcp/api" + + "github.com/wmnsk/go-pfcp/ie" +) + +type UpfsMap struct { + sync.Map +} + +func NewUpfsMap(slices map[string]config.Slice) *UpfsMap { + m := UpfsMap{} + for _, slice := range slices { + for _, upf := range slice.Upfs { + if _, ok := m.Load(upf.NodeID); ok { + // upf used in more than a single slice + continue + } + m.Store(upf.NodeID, NewUpf(upf.Interfaces)) + } + } + return &m +} + +type Upf struct { + association pfcpapi.PFCPAssociationInterface + interfaces map[netip.Addr]*UpfInterface + sessions map[netip.Addr]*Pfcprules +} + +func (upf *Upf) GetN3() (netip.Addr, error) { + for addr, iface := range upf.interfaces { + if strings.ToLower(iface.Type) == "n3" { + return addr, nil + } + } + return netip.Addr{}, ErrInterfaceNotFound +} + +func (upf *Upf) GetN6() (netip.Addr, error) { + for addr, iface := range upf.interfaces { + if strings.ToLower(iface.Type) == "n6" { + return addr, nil + } + } + return netip.Addr{}, ErrInterfaceNotFound +} + +func NewUpf(interfaces []config.Interface) *Upf { + upf := Upf{ + interfaces: NewUpfInterfaceMap(interfaces), + sessions: make(map[netip.Addr]*Pfcprules), + } + return &upf +} + +func (upf *Upf) Associate(a pfcpapi.PFCPAssociationInterface) { + upf.association = a +} + +func (upf *Upf) Rules(ueIp netip.Addr) *Pfcprules { + rules, ok := upf.sessions[ueIp] + if !ok { + rules = NewPfcpRules() + upf.sessions[ueIp] = rules + } + return rules +} + +func (upf *Upf) NextListenFteid(ctx context.Context, listenInterface netip.Addr) (*Fteid, error) { + iface, ok := upf.interfaces[listenInterface] + if !ok { + return nil, ErrInterfaceNotFound + } + teid, err := iface.Teids.Next(ctx) + if err != nil { + return nil, err + } + return &Fteid{ + Addr: listenInterface, + Teid: teid, + }, nil +} + +func (upf *Upf) CreateUplinkIntermediate(ctx context.Context, ueIp netip.Addr, dnn string, listenInterface netip.Addr, forwardFteid *Fteid) (*Fteid, error) { + listenFteid, err := upf.NextListenFteid(ctx, listenInterface) + if err != nil { + return nil, err + } + upf.CreateUplinkIntermediateWithFteid(ueIp, dnn, listenFteid, forwardFteid) + return listenFteid, nil +} + +func (upf *Upf) CreateUplinkIntermediateWithFteid(ueIp netip.Addr, dnn string, listenFteid *Fteid, forwardFteid *Fteid) { + r := upf.Rules(ueIp) + r.Lock() + defer r.Unlock() + r.currentpdrid += 1 + r.currentfarid += 1 + + r.createpdrs = append(r.createpdrs, ie.NewCreatePDR(ie.NewPDRID(r.currentpdrid), ie.NewPrecedence(255), + ie.NewPDI( + ie.NewSourceInterface(ie.SrcInterfaceAccess), + ie.NewFTEID(FteidTypeIPv4, listenFteid.Teid, listenFteid.Addr.AsSlice(), nil, 0), + ie.NewNetworkInstance(dnn), + ie.NewUEIPAddress(UEIpAddrTypeIPv4, ueIp.String(), "", 0, 0), + ), + ie.NewOuterHeaderRemoval(OuterHeaderRemoveGtpuUdpIpv4, 0), + ie.NewFARID(r.currentfarid), + )) + r.createfars = append(r.createfars, ie.NewCreateFAR(ie.NewFARID(r.currentfarid), + ie.NewApplyAction(ApplyActionForw), + ie.NewForwardingParameters( + ie.NewDestinationInterface(ie.DstInterfaceCore), + ie.NewNetworkInstance(dnn), + ie.NewOuterHeaderCreation( + OuterHeaderCreationGtpuUdpIpv4, + forwardFteid.Teid, + forwardFteid.Addr.String(), + "", 0, 0, 0, + ), + ), + )) +} + +func (upf *Upf) CreateUplinkAnchor(ctx context.Context, ueIp netip.Addr, dnn string, listenInterface netip.Addr) (*Fteid, error) { + listenFteid, err := upf.NextListenFteid(ctx, listenInterface) + if err != nil { + return nil, err + } + upf.CreateUplinkAnchorWithFteid(ueIp, dnn, listenFteid) + return listenFteid, nil +} + +func (upf *Upf) CreateUplinkAnchorWithFteid(ueIp netip.Addr, dnn string, listenFteid *Fteid) { + r := upf.Rules(ueIp) + r.Lock() + defer r.Unlock() + r.currentpdrid += 1 + r.currentfarid += 1 + + r.createpdrs = append(r.createpdrs, ie.NewCreatePDR(ie.NewPDRID(r.currentpdrid), ie.NewPrecedence(255), + ie.NewPDI( + ie.NewSourceInterface(ie.SrcInterfaceAccess), + ie.NewFTEID(FteidTypeIPv4, listenFteid.Teid, listenFteid.Addr.AsSlice(), nil, 0), + ie.NewNetworkInstance(dnn), + ie.NewUEIPAddress(UEIpAddrTypeIPv4, ueIp.String(), "", 0, 0), + ), + ie.NewOuterHeaderRemoval(OuterHeaderRemoveGtpuUdpIpv4, 0), + ie.NewFARID(r.currentfarid), + )) + r.createfars = append(r.createfars, ie.NewCreateFAR(ie.NewFARID(r.currentfarid), + ie.NewApplyAction(ApplyActionForw), + ie.NewForwardingParameters( + ie.NewDestinationInterface(ie.DstInterfaceCore), + ie.NewNetworkInstance(dnn), + ), + )) +} + +func (upf *Upf) UpdateDownlinkAnchor(ueIp netip.Addr, dnn string, forwardFteid *Fteid) { + r := upf.Rules(ueIp) + r.Lock() + defer r.Unlock() + r.currentpdrid += 1 + r.currentfarid += 1 + + r.createpdrs = append(r.createpdrs, ie.NewCreatePDR(ie.NewPDRID(r.currentpdrid), ie.NewPrecedence(255), + ie.NewPDI(ie.NewSourceInterface(ie.SrcInterfaceCore), + ie.NewNetworkInstance(dnn), + ie.NewUEIPAddress(UEIpAddrTypeIPv4, ueIp.String(), "", 0, 0), + ), + ie.NewFARID(r.currentfarid), + ), + ) + r.createfars = append(r.createfars, ie.NewCreateFAR(ie.NewFARID(r.currentfarid), + ie.NewApplyAction(ApplyActionForw), + ie.NewForwardingParameters( + ie.NewDestinationInterface(ie.DstInterfaceAccess), + ie.NewNetworkInstance(dnn), + ie.NewOuterHeaderCreation( + OuterHeaderCreationGtpuUdpIpv4, + forwardFteid.Teid, + forwardFteid.Addr.String(), + "", 0, 0, 0, + ), + ), + )) +} + +func (upf *Upf) UpdateDownlinkIntermediate(ctx context.Context, ueIp netip.Addr, dnn string, listenInterface netip.Addr, forwardFteid *Fteid) (*Fteid, error) { + listenFteid, err := upf.NextListenFteid(ctx, listenInterface) + if err != nil { + return nil, err + } + upf.UpdateDownlinkIntermediateWithFteid(ueIp, dnn, listenFteid, forwardFteid) + return listenFteid, nil +} + +func (upf *Upf) UpdateDownlinkIntermediateWithFteid(ueIp netip.Addr, dnn string, listenFteid *Fteid, forwardFteid *Fteid) { + r := upf.Rules(ueIp) + r.Lock() + defer r.Unlock() + r.currentpdrid += 1 + r.currentfarid += 1 + + r.createpdrs = append(r.createpdrs, ie.NewCreatePDR(ie.NewPDRID(r.currentpdrid), ie.NewPrecedence(255), + ie.NewPDI( + ie.NewSourceInterface(ie.SrcInterfaceCore), + ie.NewFTEID(FteidTypeIPv4, listenFteid.Teid, listenFteid.Addr.AsSlice(), nil, 0), + ie.NewNetworkInstance(dnn), + ie.NewUEIPAddress(UEIpAddrTypeIPv4, ueIp.String(), "", 0, 0), + ), + ie.NewOuterHeaderRemoval(OuterHeaderRemoveGtpuUdpIpv4, 0), + ie.NewFARID(r.currentfarid), + ), + ) + r.createfars = append(r.createfars, ie.NewCreateFAR(ie.NewFARID(r.currentfarid), + ie.NewApplyAction(ApplyActionForw), + ie.NewForwardingParameters( + ie.NewDestinationInterface(ie.DstInterfaceAccess), + ie.NewNetworkInstance(dnn), + ie.NewOuterHeaderCreation( + OuterHeaderCreationGtpuUdpIpv4, + forwardFteid.Teid, + forwardFteid.Addr.String(), + "", 0, 0, 0, + ), + ), + )) +} + +func (upf *Upf) CreateSession(ue netip.Addr) error { + rules, ok := upf.sessions[ue] + if !ok { + return ErrNoPFCPRule + } + rules.Lock() + defer rules.Unlock() + + createpdrs, err, _, _ := pfcp.NewPDRMap(rules.createpdrs) + if err != nil { + return err + } + createfars, err, _, _ := pfcp.NewFARMap(rules.createfars) + if err != nil { + return err + } + if upf.association == nil { + return ErrUpfNotAssociated + } + rules.session, err = upf.association.CreateSession(nil, createpdrs, createfars) + if err != nil { + return err + } + // clear + rules.createpdrs = make([]*ie.IE, 0) + rules.createfars = make([]*ie.IE, 0) + return nil +} + +func (upf *Upf) UpdateSession(ue netip.Addr) error { + rules, ok := upf.sessions[ue] + if !ok { + return ErrNoPFCPRule + } + rules.Lock() + defer rules.Unlock() + createpdrs, err, _, _ := pfcp.NewPDRMap(rules.createpdrs) + if err != nil { + return err + } + createfars, err, _, _ := pfcp.NewFARMap(rules.createfars) + if err != nil { + return err + } + updatepdrs, err, _, _ := pfcp.NewPDRMap(rules.updatepdrs) + if err != nil { + return err + } + updatefars, err, _, _ := pfcp.NewFARMap(rules.updatefars) + if err != nil { + return err + } + if upf.association == nil { + return ErrUpfNotAssociated + } + err = rules.session.AddUpdatePDRsFARs(createpdrs, createfars, updatepdrs, updatefars) + if err != nil { + return err + } + // clear + rules.createpdrs = make([]*ie.IE, 0) + rules.createfars = make([]*ie.IE, 0) + rules.updatepdrs = make([]*ie.IE, 0) + rules.updatefars = make([]*ie.IE, 0) + + return nil +} diff --git a/internal/smf/upf_interface.go b/internal/smf/upf_interface.go new file mode 100644 index 0000000..9762658 --- /dev/null +++ b/internal/smf/upf_interface.go @@ -0,0 +1,31 @@ +// Copyright 2024 Louis Royer and the NextMN contributors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file. +// SPDX-License-Identifier: MIT + +package smf + +import ( + "net/netip" + + "github.com/nextmn/cp-lite/internal/config" +) + +type UpfInterface struct { + Teids *TEIDsPool + Type string +} + +func NewUpfInterface(t string) *UpfInterface { + return &UpfInterface{ + Teids: NewTEIDsPool(), + Type: t, + } +} +func NewUpfInterfaceMap(ifaces []config.Interface) map[netip.Addr]*UpfInterface { + r := make(map[netip.Addr]*UpfInterface) + for _, v := range ifaces { + r[v.Addr] = NewUpfInterface(v.Type) + } + return r +}