From 2ba4003905c81f8671b98600540ad4a67c2006fd Mon Sep 17 00:00:00 2001 From: Louis Royer Date: Thu, 19 Dec 2024 15:56:42 +0100 Subject: [PATCH] (wip) Add handover; close #8; close #9; close #10; close #11; close #12 --- go.mod | 14 ++-- go.sum | 40 ++++++---- internal/app/control.go | 5 ++ internal/cli/cli.go | 29 ++++++++ internal/cli/ps-handover.go | 58 +++++++++++++++ internal/gtp/gtp.go | 11 ++- internal/session/errors.go | 5 +- internal/session/handover_command.go | 65 ++++++++++++++++ internal/session/handover_confirm.go | 62 ++++++++++++++++ internal/session/handover_request.go | 74 +++++++++++++++++++ internal/session/n2-establishment-request.go | 12 +-- internal/session/pdu_sessions.go | 3 + internal/session/pdu_sessions_manager.go | 78 +++++++++++++++----- 13 files changed, 407 insertions(+), 49 deletions(-) create mode 100644 internal/cli/cli.go create mode 100644 internal/cli/ps-handover.go create mode 100644 internal/session/handover_command.go create mode 100644 internal/session/handover_confirm.go create mode 100644 internal/session/handover_request.go diff --git a/go.mod b/go.mod index c0833b9..1d831c2 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/json-api v0.0.14 + github.com/nextmn/json-api v0.0.15-0.20241223192440-30b4537ace9e github.com/nextmn/logrus-formatter v0.0.1 github.com/sirupsen/logrus v1.9.3 github.com/urfave/cli/v2 v2.27.5 @@ -14,7 +14,7 @@ require ( ) require ( - github.com/bytedance/sonic v1.12.5 // indirect + github.com/bytedance/sonic v1.12.6 // indirect github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect @@ -24,7 +24,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.23.0 // indirect - github.com/goccy/go-json v0.10.3 // indirect + github.com/goccy/go-json v0.10.4 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -39,9 +39,9 @@ require ( github.com/vishvananda/netns v0.0.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/arch v0.12.0 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/net v0.31.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.20.0 // indirect - google.golang.org/protobuf v1.35.2 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.36.0 // indirect ) diff --git a/go.sum b/go.sum index b763da7..7ecb69f 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w= -github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= +github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -28,8 +28,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -48,8 +48,20 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -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/json-api v0.0.15-0.20241218142156-a64418a36b7d h1:lhybNMDI+qjJB+rKDgJiHrXuhkoR7FWhQ4nEfpqZy1g= +github.com/nextmn/json-api v0.0.15-0.20241218142156-a64418a36b7d/go.mod h1:CQXeNPj9MDGsEExtnqJFIGjLgZAKsmOoO2fy+mep7Ak= +github.com/nextmn/json-api v0.0.15-0.20241223150142-2189fb1dc3af h1:/YGOPznGEQ0x/2E1Yk9HZ/3FosWtK5FouI0NDSiN2sE= +github.com/nextmn/json-api v0.0.15-0.20241223150142-2189fb1dc3af/go.mod h1:CQXeNPj9MDGsEExtnqJFIGjLgZAKsmOoO2fy+mep7Ak= +github.com/nextmn/json-api v0.0.15-0.20241223151312-9ece63d06e84 h1:eMjBBW9K21ffZ0CmbY7nKZd8d7O41DMB10t2HOTkUHw= +github.com/nextmn/json-api v0.0.15-0.20241223151312-9ece63d06e84/go.mod h1:CQXeNPj9MDGsEExtnqJFIGjLgZAKsmOoO2fy+mep7Ak= +github.com/nextmn/json-api v0.0.15-0.20241223184721-e18ca5cd5f80 h1:bCHBRPROQR9S31nyqTSMuC3GQ0sENbv0976+v28IdnY= +github.com/nextmn/json-api v0.0.15-0.20241223184721-e18ca5cd5f80/go.mod h1:CQXeNPj9MDGsEExtnqJFIGjLgZAKsmOoO2fy+mep7Ak= +github.com/nextmn/json-api v0.0.15-0.20241223190836-9214d6edc562 h1:J7A9dVo41ZUc2Df+Ri58I5fwNdmlPmy0/WfNHi0nmjE= +github.com/nextmn/json-api v0.0.15-0.20241223190836-9214d6edc562/go.mod h1:CQXeNPj9MDGsEExtnqJFIGjLgZAKsmOoO2fy+mep7Ak= +github.com/nextmn/json-api v0.0.15-0.20241223192051-3fc04f155386 h1:RP6Vc+ITbrH6JhBKLGn41eIcttP46gI2g6Inu65BvRY= +github.com/nextmn/json-api v0.0.15-0.20241223192051-3fc04f155386/go.mod h1:CQXeNPj9MDGsEExtnqJFIGjLgZAKsmOoO2fy+mep7Ak= +github.com/nextmn/json-api v0.0.15-0.20241223192440-30b4537ace9e h1:yDqGoYWiSbG5Ca869nEQN7i9OY77JowwGruBmn2fn+w= +github.com/nextmn/json-api v0.0.15-0.20241223192440-30b4537ace9e/go.mod h1:CQXeNPj9MDGsEExtnqJFIGjLgZAKsmOoO2fy+mep7Ak= github.com/nextmn/logrus-formatter v0.0.1 h1:Bsf78jjiEESc+rV8xE6IyKj4frDPGMwXFNrLQzm6A1E= github.com/nextmn/logrus-formatter v0.0.1/go.mod h1:vdSZ+sIcSna8vjbXkSFxsnsKHqRwaUEed4JCPcXoGyM= github.com/pascaldekloe/goe v0.1.1 h1:Ah6WQ56rZONR3RW3qWa2NCZ6JAVvSpUcoLBaOmYFt9Q= @@ -89,20 +101,20 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/control.go b/internal/app/control.go index 1d12ece..b877554 100644 --- a/internal/app/control.go +++ b/internal/app/control.go @@ -12,6 +12,7 @@ import ( "net/netip" "time" + "github.com/nextmn/gnb-lite/internal/cli" "github.com/nextmn/gnb-lite/internal/radio" "github.com/nextmn/gnb-lite/internal/session" @@ -29,10 +30,14 @@ type HttpServerEntity struct { } func NewHttpServerEntity(bindAddr netip.AddrPort, r *radio.Radio, ps *session.PduSessions) *HttpServerEntity { + c := cli.NewCli(r, ps) // TODO: gin.SetMode(gin.DebugMode) / gin.SetMode(gin.ReleaseMode) depending on log level h := gin.Default() h.GET("/status", Status) + // CLI + c.Register(h) + // Radio r.Register(h) diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..b37e044 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,29 @@ +// 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 cli + +import ( + "github.com/nextmn/gnb-lite/internal/radio" + "github.com/nextmn/gnb-lite/internal/session" + + "github.com/gin-gonic/gin" +) + +type Cli struct { + Radio *radio.Radio + PduSessions *session.PduSessions +} + +func NewCli(r *radio.Radio, p *session.PduSessions) *Cli { + return &Cli{ + Radio: r, + PduSessions: p, + } +} + +func (cli *Cli) Register(e *gin.Engine) { + e.POST("/cli/ps/handover", cli.PsHandover) +} diff --git a/internal/cli/ps-handover.go b/internal/cli/ps-handover.go new file mode 100644 index 0000000..e728682 --- /dev/null +++ b/internal/cli/ps-handover.go @@ -0,0 +1,58 @@ +// 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 cli + +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" +) + +type PsHandover struct { + UeCtrl jsonapi.ControlURI `json:"ue-ctrl"` + GNBTarget jsonapi.ControlURI `json:"gnb-target"` + Sessions []n1n2.Session `json:"sessions"` +} + +func (cli *Cli) PsHandover(c *gin.Context) { + var ps PsHandover + 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}) + } + go cli.HandlePsHandover(ps) + c.Status(http.StatusNotImplemented) +} + +func (cli *Cli) HandlePsHandover(ps PsHandover) { + ctx := cli.PduSessions.Context() + hr := n1n2.HandoverRequired{ + SourcegNB: cli.PduSessions.Control, + Ue: ps.UeCtrl, + } + reqBody, err := json.Marshal(hr) + if err != nil { + logrus.WithError(err).Error("Could not marshal n1n2.HandoverRequired") + return + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, cli.PduSessions.Cp.JoinPath("ps/handover-required").String(), bytes.NewBuffer(reqBody)) + if err != nil { + logrus.WithError(err).Error("Could not create ps/handover-required") + return + } + req.Header.Set("User-Agent", cli.PduSessions.UserAgent) + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + if _, err := cli.PduSessions.Client.Do(req); err != nil { + logrus.WithError(err).Error("Could not send ps/handover-required") + return + } +} diff --git a/internal/gtp/gtp.go b/internal/gtp/gtp.go index 3e29bf7..ed66ef2 100644 --- a/internal/gtp/gtp.go +++ b/internal/gtp/gtp.go @@ -42,7 +42,7 @@ func (gtp *Gtp) Start(ctx context.Context) error { uConn := gtpv1.NewUPlaneConn(laddr) uConn.DisableErrorIndication() uConn.AddHandler(message.MsgTypeTPDU, func(c gtpv1.Conn, senderAddr net.Addr, msg message.Message) error { - return gtp.tpduHandler(c, senderAddr, msg) + return gtp.tpduHandler(ctx, c, senderAddr, msg) }) go func(ctx context.Context) error { defer close(gtp.closed) @@ -59,8 +59,15 @@ func (gtp *Gtp) Start(ctx context.Context) error { } // handle GTP PDU (Downlink) -func (gtp *Gtp) tpduHandler(c gtpv1.Conn, senderAddr net.Addr, msg message.Message) error { +func (gtp *Gtp) tpduHandler(ctx context.Context, c gtpv1.Conn, senderAddr net.Addr, msg message.Message) error { teid := msg.TEID() + // Try forwarding downlink (handover) + if fd, err := gtp.psMan.GetForwarding(teid); err == nil { + packet := msg.(*message.TPDU).Decapsulate() + return gtp.psMan.ForwardUplink(ctx, packet, fd) + } + + // Try to forward to UE over radio ue, err := gtp.psMan.GetUECtrl(teid) if err != nil { return err diff --git a/internal/session/errors.go b/internal/session/errors.go index 86efe1d..da04fc1 100644 --- a/internal/session/errors.go +++ b/internal/session/errors.go @@ -12,6 +12,7 @@ import ( var ( ErrNilCtx = errors.New("nil context") - ErrUnsupportedPDUType = errors.New("Unsupported PDU type") - ErrPduSessionNotFound = errors.New("PDU Session not found") + ErrUnsupportedPDUType = errors.New("Unsupported PDU type") + ErrPduSessionNotFound = errors.New("PDU Session not found") + ErrForwardDownlinkNotFound = errors.New("Forward Downlink rule not found") ) diff --git a/internal/session/handover_command.go b/internal/session/handover_command.go new file mode 100644 index 0000000..19f3923 --- /dev/null +++ b/internal/session/handover_command.go @@ -0,0 +1,65 @@ +// 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 session + +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 (s *PduSessions) HandoverCommand(c *gin.Context) { + var ps n1n2.HandoverCommand + 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.UeCtrl.String(), + }).Info("New Handover Command") + go s.HandleHandoverCommand(ps) + c.JSON(http.StatusAccepted, jsonapi.Message{Message: "please refer to logs for more information"}) +} + +func (s *PduSessions) HandleHandoverCommand(ps n1n2.HandoverCommand) { + // Add forwarder for downlink + for _, session := range ps.Sessions { + if session.ForwardDownlinkFteid == nil || session.DownlinkFteid == nil { + // TODO: notify CP of error + continue + } + s.manager.ForwardDownlink[session.DownlinkFteid.Teid] = session.ForwardDownlinkFteid + // TODO: remove downlink forward with a timer + // TODO: remove pdu session after a timer + } + + ctx := s.Context() + // Forward to UE + reqBody, err := json.Marshal(ps) + if err != nil { + logrus.WithError(err).Error("Could not marshal n1n2.HandoverCommand") + return + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, ps.UeCtrl.JoinPath("ps/handover-command").String(), bytes.NewBuffer(reqBody)) + if err != nil { + logrus.WithError(err).Error("Could not create ps/handover-command") + return + } + req.Header.Set("User-Agent", s.UserAgent) + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + if _, err := s.Client.Do(req); err != nil { + logrus.WithError(err).Error("Could not send ps/handover-command") + return + } + +} diff --git a/internal/session/handover_confirm.go b/internal/session/handover_confirm.go new file mode 100644 index 0000000..f6f01ef --- /dev/null +++ b/internal/session/handover_confirm.go @@ -0,0 +1,62 @@ +// 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 session + +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 (s *PduSessions) HandoverConfirm(c *gin.Context) { + var ps n1n2.HandoverConfirm + 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.UeCtrl.String(), + }).Info("New Handover Confirm") + go s.HandleHandoverConfirm(ps) + c.JSON(http.StatusAccepted, jsonapi.Message{Message: "please refer to logs for more information"}) +} + +func (s *PduSessions) HandleHandoverConfirm(ps n1n2.HandoverConfirm) { + ctx := s.Context() + // forward to CP + resp := n1n2.HandoverNotify{ + // Header + UeCtrl: ps.UeCtrl, + Cp: ps.Cp, + TargetGnb: ps.TargetGnb, + // Handover Notify + Sessions: ps.Sessions, + SourceGnb: ps.SourceGnb, + } + reqBody, err := json.Marshal(resp) + if err != nil { + logrus.WithError(err).Error("Could not marshal n1n2.HandoverNotify") + return + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.Cp.JoinPath("ps/handover-notify").String(), bytes.NewBuffer(reqBody)) + if err != nil { + logrus.WithError(err).Error("Could not create ps/handover-notify") + return + } + req.Header.Set("User-Agent", s.UserAgent) + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + if _, err := s.Client.Do(req); err != nil { + logrus.WithError(err).Error("Could not send ps/handover-notify") + return + } +} diff --git a/internal/session/handover_request.go b/internal/session/handover_request.go new file mode 100644 index 0000000..0d80c8f --- /dev/null +++ b/internal/session/handover_request.go @@ -0,0 +1,74 @@ +// 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 session + +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 (s *PduSessions) HandoverRequest(c *gin.Context) { + var ps n1n2.HandoverRequest + 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.UeCtrl.String(), + }).Info("New Handver Request") + go s.HandleHandoverRequest(ps) + c.JSON(http.StatusAccepted, jsonapi.Message{Message: "please refer to logs for more information"}) +} + +func (s *PduSessions) HandleHandoverRequest(ps n1n2.HandoverRequest) { + ctx := s.Context() + + rsp_sessions := make([]n1n2.Session, len(ps.Sessions)) + copy(rsp_sessions, ps.Sessions) + for i, session := range ps.Sessions { + // allocate DL FTEID + downlinkFTeid, err := s.manager.NewPduSession(ctx, session.Addr, ps.UeCtrl, session.UplinkFteid) + if err != nil { + logrus.WithError(err).Error("Could create PDU Session") + // TODO: notify CP of the error + return + } + rsp_sessions[i].DownlinkFteid = downlinkFTeid + } + rsp := n1n2.HandoverRequestAck{ + // Header + Cp: ps.Cp, + TargetgNB: ps.TargetgNB, + // Handover Request Ack + UeCtrl: ps.UeCtrl, + Sessions: rsp_sessions, + } + + reqBody, err := json.Marshal(rsp) + if err != nil { + logrus.WithError(err).Error("Could not marshal n1n2.HandoverRequestAck") + return + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.Cp.JoinPath("ps/handover-request-ack").String(), bytes.NewBuffer(reqBody)) + if err != nil { + logrus.WithError(err).Error("Could not create ps/handover-request-ack") + return + } + req.Header.Set("User-Agent", s.UserAgent) + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + if _, err := s.Client.Do(req); err != nil { + logrus.WithError(err).Error("Could not send ps/handover-request-ack") + return + } +} diff --git a/internal/session/n2-establishment-request.go b/internal/session/n2-establishment-request.go index 25ea560..85f2b08 100644 --- a/internal/session/n2-establishment-request.go +++ b/internal/session/n2-establishment-request.go @@ -27,8 +27,8 @@ func (p *PduSessions) N2EstablishmentRequest(c *gin.Context) { } logrus.WithFields(logrus.Fields{ "ue": ps.UeInfo.Header.Ue.String(), - "upf": ps.Upf, - "uplink-teid": ps.UplinkTeid, + "upf": ps.UplinkFteid.Addr, + "uplink-teid": ps.UplinkFteid.Teid, }).Info("New PDU Session establishment Request") go p.HandleN2EstablishmentRequest(ps) c.JSON(http.StatusAccepted, jsonapi.Message{Message: "please refer to logs for more information"}) @@ -37,9 +37,10 @@ func (p *PduSessions) N2EstablishmentRequest(c *gin.Context) { func (p *PduSessions) HandleN2EstablishmentRequest(ps n1n2.N2PduSessionReqMsg) { ctx := p.Context() // allocate downlink teid - downlinkTeid, err := p.manager.NewPduSession(ctx, ps.UeInfo.Addr, ps.UeInfo.Header.Ue, ps.Upf, ps.UplinkTeid) + downlinkFteid, err := p.manager.NewPduSession(ctx, ps.UeInfo.Addr, ps.UeInfo.Header.Ue, &ps.UplinkFteid) if err != nil { logrus.WithError(err).Error("Could create PDU Session") + // TODO: notify CP of the error return } @@ -63,9 +64,8 @@ func (p *PduSessions) HandleN2EstablishmentRequest(ps n1n2.N2PduSessionReqMsg) { } psresp := n1n2.N2PduSessionRespMsg{ - UeInfo: ps.UeInfo, - Gnb: p.GnbGtp, - DownlinkTeid: downlinkTeid, + UeInfo: ps.UeInfo, + DownlinkFteid: *downlinkFteid, } // send N2PsResp to CP (with dl fteid) n2reqBody, err := json.Marshal(psresp) diff --git a/internal/session/pdu_sessions.go b/internal/session/pdu_sessions.go index 3ccfc80..13f6a0c 100644 --- a/internal/session/pdu_sessions.go +++ b/internal/session/pdu_sessions.go @@ -60,4 +60,7 @@ func (p *PduSessions) Context() context.Context { func (p *PduSessions) Register(e *gin.Engine) { e.POST("/ps/establishment-request", p.EstablishmentRequest) e.POST("/ps/n2-establishment-request", p.N2EstablishmentRequest) + e.POST("/ps/handover-request", p.HandoverRequest) + e.POST("/ps/handover-command", p.HandoverCommand) + e.POST("/ps/handover-confirm", p.HandoverConfirm) } diff --git a/internal/session/pdu_sessions_manager.go b/internal/session/pdu_sessions_manager.go index 83b473c..6632b2b 100644 --- a/internal/session/pdu_sessions_manager.go +++ b/internal/session/pdu_sessions_manager.go @@ -25,21 +25,57 @@ const GTPU_PORT = 2152 type PduSessionsManager struct { sync.Mutex - Downlink map[uint32]jsonapi.ControlURI // teid: UE control uri - Uplink map[netip.Addr]*Fteid // ue 5G ip address: uplink fteid - GtpAddr netip.Addr - upfs map[netip.Addr]*gtpv1.UPlaneConn + Downlink map[uint32]jsonapi.ControlURI // teid: UE control uri + ForwardDownlink map[uint32]*jsonapi.Fteid + Uplink map[netip.Addr]*jsonapi.Fteid // ue 5G ip address: uplink fteid + GtpAddr netip.Addr + upfs map[netip.Addr]*gtpv1.UPlaneConn } func NewPduSessionsManager(gtpAddr netip.Addr) *PduSessionsManager { return &PduSessionsManager{ - Downlink: make(map[uint32]jsonapi.ControlURI), - Uplink: make(map[netip.Addr]*Fteid), - GtpAddr: gtpAddr, - upfs: make(map[netip.Addr]*gtpv1.UPlaneConn), + Downlink: make(map[uint32]jsonapi.ControlURI), + ForwardDownlink: make(map[uint32]*jsonapi.Fteid), + Uplink: make(map[netip.Addr]*jsonapi.Fteid), + GtpAddr: gtpAddr, + upfs: make(map[netip.Addr]*gtpv1.UPlaneConn), } } +func (p *PduSessionsManager) ForwardUplink(ctx context.Context, pkt []byte, fteid *jsonapi.Fteid) error { + gpdu := message.NewHeaderWithExtensionHeaders(0x30, message.MsgTypeTPDU, fteid.Teid, 0, pkt, []*message.ExtensionHeader{}...) + b, err := gpdu.Marshal() + if err != nil { + return err + } + raddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(fteid.Addr, GTPU_PORT)) + uConn, ok := p.upfs[fteid.Addr] + if !ok { + laddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(p.GtpAddr, 0)) + uConn, err = gtpv1.DialUPlane(ctx, laddr, raddr) + if err != nil { + logrus.WithFields(logrus.Fields{ + "upf": raddr, + }).Error("Failure to dial UPF") + return err + } + p.upfs[fteid.Addr] = uConn + go func(ctx context.Context, uConn *gtpv1.UPlaneConn) error { + select { + case <-ctx.Done(): + uConn.Close() + return ctx.Err() + } + return nil + }(ctx, uConn) + } + logrus.WithFields(logrus.Fields{ + "fteid": fteid, + }).Trace("Forwarding packet to GTP") + _, err = uConn.WriteTo(b, raddr) + return err +} + func (p *PduSessionsManager) WriteUplink(ctx context.Context, pkt []byte) error { if len(pkt) < 20 { logrus.Trace("too small to be an ipv4 packet") @@ -62,8 +98,8 @@ func (p *PduSessionsManager) WriteUplink(ctx context.Context, pkt []byte) error if err != nil { return err } - uConn, ok := p.upfs[fteid.IpAddr] - raddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(fteid.IpAddr, GTPU_PORT)) + raddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(fteid.Addr, GTPU_PORT)) + uConn, ok := p.upfs[fteid.Addr] if !ok { laddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(p.GtpAddr, 0)) uConn, err = gtpv1.DialUPlane(ctx, laddr, raddr) @@ -73,7 +109,7 @@ func (p *PduSessionsManager) WriteUplink(ctx context.Context, pkt []byte) error }).Error("Failure to dial UPF") return err } - p.upfs[fteid.IpAddr] = uConn + p.upfs[fteid.Addr] = uConn go func(ctx context.Context, uConn *gtpv1.UPlaneConn) error { select { case <-ctx.Done(): @@ -98,12 +134,21 @@ func (p *PduSessionsManager) GetUECtrl(teid uint32) (jsonapi.ControlURI, error) return ueCtrl, nil } +func (p *PduSessionsManager) GetForwarding(teid uint32) (*jsonapi.Fteid, error) { + fteid, ok := p.ForwardDownlink[teid] + if !ok { + return fteid, ErrForwardDownlinkNotFound + } + return fteid, nil +} + type Fteid struct { IpAddr netip.Addr Teid uint32 } -func (p *PduSessionsManager) NewPduSession(ctx context.Context, ueIpAddr netip.Addr, ueControlURI jsonapi.ControlURI, upf netip.Addr, uplinkTeid uint32) (uint32, error) { +// Returns the new DL TEID allocated +func (p *PduSessionsManager) NewPduSession(ctx context.Context, ueIpAddr netip.Addr, ueControlURI jsonapi.ControlURI, uplinkFteid *jsonapi.Fteid) (*jsonapi.Fteid, error) { p.Lock() defer p.Unlock() @@ -111,13 +156,10 @@ func (p *PduSessionsManager) NewPduSession(ctx context.Context, ueIpAddr netip.A defer cancel() dlTeid, err := p.newTeidDl(ctxTimeout, ueControlURI) if err != nil { - return dlTeid, err - } - p.Uplink[ueIpAddr] = &Fteid{ - IpAddr: upf, - Teid: uplinkTeid, + return nil, err } - return dlTeid, err + p.Uplink[ueIpAddr] = uplinkFteid + return jsonapi.NewFteid(p.GtpAddr, dlTeid), err } // Warning: not thread safe