From ac0b66a825dd61dd436cd4360c73c8437e2c232b Mon Sep 17 00:00:00 2001 From: Guillaume Belanger Date: Thu, 21 Dec 2023 10:21:18 -0500 Subject: [PATCH 1/3] feat: Follows spec defined message content --- README.md | 2 +- client/client.go | 10 ++++------ client/client_test.go | 3 ++- main.go | 3 ++- main_test.go | 4 ++-- messages/header.go | 13 +++++++++++-- messages/heartbeat.go | 33 ++++++++++++++++++++++++++++++++- server/server.go | 30 ++++++------------------------ 8 files changed, 60 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index afe181e..2606655 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ import ( func main() { pfcpClient := client.New("1.2.3.4:8805") - err := pfcpClient.SendHeartbeatRequest() + _, err := pfcpClient.SendHeartbeatRequest() if err != nil { log.Fatalf("SendHeartbeatRequest failed: %v", err) } diff --git a/client/client.go b/client/client.go index 7cb4d18..a882f2d 100644 --- a/client/client.go +++ b/client/client.go @@ -1,7 +1,6 @@ package client import ( - "encoding/binary" "log" "time" @@ -41,12 +40,11 @@ func serializeMessage(header messages.PFCPHeader, payload []byte) []byte { return append(headerBytes, payload...) } -func (pfcp *Pfcp) SendHeartbeatRequest() (messages.RecoveryTimeStamp, error) { - timestamp := time.Now().Unix() - timeBytes := make([]byte, 8) - binary.BigEndian.PutUint64(timeBytes, uint64(timestamp)) +func (pfcp *Pfcp) SendHeartbeatRequest(time time.Time) (messages.RecoveryTimeStamp, error) { + // Create a RecoveryTimeStamp with the current time + recoveryTimeStamp := messages.NewRecoveryTimeStamp(time) + timeBytes := recoveryTimeStamp.ToBytes() header := messages.NewPFCPHeader(1, 1) err := pfcp.sendPfcpMessage(header, timeBytes, "Heartbeat Request") - recoveryTimeStamp := messages.RecoveryTimeStamp(time.Unix(timestamp, 0)) return recoveryTimeStamp, err } diff --git a/client/client_test.go b/client/client_test.go index d57a0dd..9082c34 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2,6 +2,7 @@ package client_test import ( "testing" + "time" "github.com/dot-5g/pfcp/client" ) @@ -24,7 +25,7 @@ func TestGivenPfcpWhenSendHeartbeatRequestThenNoError(t *testing.T) { pfcpClient := client.New("127.0.0.1:8805") pfcpClient.Udp = mockSender - _, err := pfcpClient.SendHeartbeatRequest() + _, err := pfcpClient.SendHeartbeatRequest(time.Now()) if err != nil { t.Errorf("SendHeartbeatRequest failed: %v", err) } diff --git a/main.go b/main.go index 8060080..b9a6884 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "log" + "time" "github.com/dot-5g/pfcp/client" "github.com/dot-5g/pfcp/messages" @@ -10,7 +11,7 @@ import ( func main() { pfcpClient := client.New("1.2.3.4:8805") - _, err := pfcpClient.SendHeartbeatRequest() + _, err := pfcpClient.SendHeartbeatRequest(time.Now()) if err != nil { log.Fatalf("SendHeartbeatRequest failed: %v", err) } diff --git a/main_test.go b/main_test.go index 235f05f..7178458 100644 --- a/main_test.go +++ b/main_test.go @@ -32,7 +32,7 @@ func TestGivenHandleHeartbeatRequestWhenRunThenHeartbeatRequestHandled(t *testin time.Sleep(time.Second) pfcpClient := client.New("127.0.0.1:8805") - sentRecoveryTimeStamp, err := pfcpClient.SendHeartbeatRequest() + sentRecoveryTimeStamp, err := pfcpClient.SendHeartbeatRequest(time.Now()) if err != nil { t.Fatalf("Failed to send Heartbeat request: %v", err) } @@ -44,7 +44,7 @@ func TestGivenHandleHeartbeatRequestWhenRunThenHeartbeatRequestHandled(t *testin t.Errorf("Heartbeat request handler was not called") } if receivedRecoveryTimestamp != sentRecoveryTimeStamp { - t.Errorf("Heartbeat request handler was called with wrong timestamp: %v", receivedRecoveryTimestamp) + t.Errorf("Heartbeat request handler was called with wrong timestamp.\n- Sent timestamp: %v\n- Received timestamp %v\n", sentRecoveryTimeStamp, receivedRecoveryTimestamp) } mu.Unlock() diff --git a/messages/header.go b/messages/header.go index 8ee99a0..0061d2e 100644 --- a/messages/header.go +++ b/messages/header.go @@ -3,6 +3,7 @@ package messages import ( "bytes" "encoding/binary" + "fmt" ) type PFCPHeader struct { @@ -15,7 +16,7 @@ type PFCPHeader struct { func SerializePFCPHeader(header PFCPHeader) []byte { buf := new(bytes.Buffer) - // Octet 1: Version (3 bits), Spare (3 bits), FO (1 bit set to 0), MP (1 bit set to 0), S (1 bit set to 0) + // Octet 1: Version (3 bits), Spare (2 bits), FO (1 bit set to 0), MP (1 bit set to 0), S (1 bit set to 0) firstOctet := (header.Version << 5) buf.WriteByte(firstOctet) @@ -33,13 +34,15 @@ func SerializePFCPHeader(header PFCPHeader) []byte { // Octet 8: Spare (1 byte set to 0) buf.WriteByte(0) + fmt.Printf("Length of PFCP header: %d\n", buf.Len()) + return buf.Bytes() } // NewPFCPHeader creates a new PFCPHeader with the given message type and sequence number. func NewPFCPHeader(messageType byte, sequenceNumber uint32) PFCPHeader { return PFCPHeader{ - Version: 1, // Assuming the version is 1 + Version: 1, MessageType: messageType, MessageLength: 0, // To be set later SequenceNumber: sequenceNumber, @@ -48,6 +51,10 @@ func NewPFCPHeader(messageType byte, sequenceNumber uint32) PFCPHeader { func ParsePFCPHeader(data []byte) PFCPHeader { + if len(data) != 8 { + panic(fmt.Sprintf("Invalid PFCP header length: %d", len(data))) + } + header := PFCPHeader{} header.Version = data[0] >> 5 header.MessageType = data[1] @@ -57,5 +64,7 @@ func ParsePFCPHeader(data []byte) PFCPHeader { copy(seqNumBytes, data[4:7]) header.SequenceNumber = binary.BigEndian.Uint32(seqNumBytes) + fmt.Printf("Parsed PFCP header: %+v\n", header) + return header } diff --git a/messages/heartbeat.go b/messages/heartbeat.go index 6651c50..46d6b30 100644 --- a/messages/heartbeat.go +++ b/messages/heartbeat.go @@ -1,10 +1,15 @@ package messages import ( + "encoding/binary" "time" ) -type RecoveryTimeStamp time.Time +type RecoveryTimeStamp struct { + Type int + Length int + Value int64 // Seconds since 1900 +} type HeartbeatRequest struct { RecoveryTimeStamp RecoveryTimeStamp @@ -13,3 +18,29 @@ type HeartbeatRequest struct { type HeartbeatResponse struct { RecoveryTimeStamp RecoveryTimeStamp } + +func NewRecoveryTimeStamp(value time.Time) RecoveryTimeStamp { + return RecoveryTimeStamp{ + Type: 96, + Length: 8, + Value: value.Unix() + ntpEpochOffset, + } +} + +const ntpEpochOffset = 2208988800 // Offset between Unix and NTP epoch (seconds) + +func (rt RecoveryTimeStamp) ToBytes() []byte { + bytes := make([]byte, 8) + binary.BigEndian.PutUint16(bytes[0:2], uint16(rt.Type)) + binary.BigEndian.PutUint16(bytes[2:4], uint16(rt.Length)) + binary.BigEndian.PutUint32(bytes[4:8], uint32(rt.Value)) + return bytes +} + +func FromBytes(b []byte) RecoveryTimeStamp { + return RecoveryTimeStamp{ + Type: int(binary.BigEndian.Uint16(b[0:2])), + Length: int(binary.BigEndian.Uint16(b[2:4])), + Value: int64(binary.BigEndian.Uint32(b[4:8])), + } +} diff --git a/server/server.go b/server/server.go index a2969b1..1408ce1 100644 --- a/server/server.go +++ b/server/server.go @@ -1,10 +1,7 @@ package server import ( - "encoding/binary" - "log" "net" - "time" "github.com/dot-5g/pfcp/messages" "github.com/dot-5g/pfcp/network" @@ -40,25 +37,15 @@ func (server *Server) Run() { func (server *Server) handleUDPMessage(data []byte, addr net.Addr) { - pfcpMessage := ParseUDPMessage(data) + header := messages.ParsePFCPHeader(data[:8]) + pfcpMessage := PfcpMessage{Header: header, Message: data[8:]} if pfcpMessage.Header.MessageType == 1 { - timestampBytes := pfcpMessage.Message - - if len(timestampBytes) >= 4 { - timestamp := binary.BigEndian.Uint32(timestampBytes) - recoveryTime := time.Unix(int64(timestamp), 0) - - heartbeatRequest := messages.HeartbeatRequest{ - RecoveryTimeStamp: messages.RecoveryTimeStamp(recoveryTime), - } - - if server.heartbeatRequestHandler != nil { - server.heartbeatRequestHandler(&heartbeatRequest) - } - } else { - log.Printf("Error: timestampBytes slice is too short to contain a valid timestamp.") + recoveryTimeStamp := messages.FromBytes(pfcpMessage.Message) + heartbeatRequest := messages.HeartbeatRequest{ + RecoveryTimeStamp: recoveryTimeStamp, } + server.heartbeatRequestHandler(&heartbeatRequest) } } @@ -69,8 +56,3 @@ func (server *Server) HeartbeatRequest(handler HandleHeartbeatRequest) { func (server *Server) HeartbeatResponse(handler HandleHeartbeatResponse) { server.heartbeatResponseHandler = handler } - -func ParseUDPMessage(data []byte) PfcpMessage { - header := messages.ParsePFCPHeader(data) - return PfcpMessage{Header: header, Message: data[12:]} -} From 99d1126dc18f3188c70111360c87cfa1d86136fb Mon Sep 17 00:00:00 2001 From: Guillaume Belanger Date: Thu, 21 Dec 2023 10:48:04 -0500 Subject: [PATCH 2/3] Adds missing unit test --- messages/header.go | 17 +++++----------- messages/header_test.go | 45 +++++++++++++++++++++++++++++++++++++++++ server/server.go | 10 +++++++-- 3 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 messages/header_test.go diff --git a/messages/header.go b/messages/header.go index 0061d2e..9ad3d65 100644 --- a/messages/header.go +++ b/messages/header.go @@ -29,17 +29,14 @@ func SerializePFCPHeader(header PFCPHeader) []byte { // Octets 5, 6, and 7: Sequence Number (3 bytes) seqNumBytes := make([]byte, 4) binary.BigEndian.PutUint32(seqNumBytes, header.SequenceNumber) - buf.Write(seqNumBytes[0:3]) // Only write the first 3 bytes + buf.Write(seqNumBytes[1:]) // Octet 8: Spare (1 byte set to 0) buf.WriteByte(0) - fmt.Printf("Length of PFCP header: %d\n", buf.Len()) - return buf.Bytes() } -// NewPFCPHeader creates a new PFCPHeader with the given message type and sequence number. func NewPFCPHeader(messageType byte, sequenceNumber uint32) PFCPHeader { return PFCPHeader{ Version: 1, @@ -49,10 +46,9 @@ func NewPFCPHeader(messageType byte, sequenceNumber uint32) PFCPHeader { } } -func ParsePFCPHeader(data []byte) PFCPHeader { - +func ParsePFCPHeader(data []byte) (PFCPHeader, error) { if len(data) != 8 { - panic(fmt.Sprintf("Invalid PFCP header length: %d", len(data))) + return PFCPHeader{}, fmt.Errorf("expected 8 bytes, got %d", len(data)) } header := PFCPHeader{} @@ -60,11 +56,8 @@ func ParsePFCPHeader(data []byte) PFCPHeader { header.MessageType = data[1] header.MessageLength = binary.BigEndian.Uint16(data[2:4]) - seqNumBytes := make([]byte, 4) - copy(seqNumBytes, data[4:7]) + seqNumBytes := []byte{0, data[4], data[5], data[6]} header.SequenceNumber = binary.BigEndian.Uint32(seqNumBytes) - fmt.Printf("Parsed PFCP header: %+v\n", header) - - return header + return header, nil } diff --git a/messages/header_test.go b/messages/header_test.go new file mode 100644 index 0000000..b37c413 --- /dev/null +++ b/messages/header_test.go @@ -0,0 +1,45 @@ +package messages_test + +import ( + "bytes" + "encoding/binary" + "testing" + + "github.com/dot-5g/pfcp/messages" +) + +func TestGivenPfcpHeaderWhenSerializePFCPHeaderThenSerializedCorrectly(t *testing.T) { + pfcpHeader := messages.PFCPHeader{ + Version: 1, + MessageType: 2, + MessageLength: 3, + SequenceNumber: 4, + } + + headerBytes := messages.SerializePFCPHeader(pfcpHeader) + + if len(headerBytes) != 8 { + t.Errorf("Expected 8 bytes, got %d", len(headerBytes)) + } + + serializedVersion := headerBytes[0] >> 5 + if serializedVersion != 1 { + t.Errorf("Expected version 1, got %d", serializedVersion) + } + + serializedMessageType := headerBytes[1] + if serializedMessageType != 2 { + t.Errorf("Expected message type 2, got %d", serializedMessageType) + } + + serializedMessageLength := binary.BigEndian.Uint16(headerBytes[2:4]) + if serializedMessageLength != 3 { + t.Errorf("Expected message length 3, got %d", serializedMessageLength) + } + + expectedSeqNum := []byte{0, 0, 4} + serializedSequenceNumber := headerBytes[4:7] + if !bytes.Equal(serializedSequenceNumber, expectedSeqNum) { + t.Errorf("Expected sequence number %v, got %v", expectedSeqNum, serializedSequenceNumber) + } +} diff --git a/server/server.go b/server/server.go index 1408ce1..3ebc7d5 100644 --- a/server/server.go +++ b/server/server.go @@ -1,12 +1,15 @@ package server import ( + "log" "net" "github.com/dot-5g/pfcp/messages" "github.com/dot-5g/pfcp/network" ) +const HeaderSize = 8 + type HandleHeartbeatRequest func(*messages.HeartbeatRequest) type HandleHeartbeatResponse func(*messages.HeartbeatResponse) @@ -37,8 +40,11 @@ func (server *Server) Run() { func (server *Server) handleUDPMessage(data []byte, addr net.Addr) { - header := messages.ParsePFCPHeader(data[:8]) - pfcpMessage := PfcpMessage{Header: header, Message: data[8:]} + header, err := messages.ParsePFCPHeader(data[:HeaderSize]) + if err != nil { + log.Printf("Error parsing PFCP header: %v", err) + } + pfcpMessage := PfcpMessage{Header: header, Message: data[HeaderSize:]} if pfcpMessage.Header.MessageType == 1 { recoveryTimeStamp := messages.FromBytes(pfcpMessage.Message) From 7bdc0e5558c1b432bc4ec6788e3aae959232fd7a Mon Sep 17 00:00:00 2001 From: Guillaume Belanger Date: Thu, 21 Dec 2023 11:08:07 -0500 Subject: [PATCH 3/3] Adds badge --- .github/workflows/main.yml | 11 +++++------ README.md | 3 +++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5df017e..9837a49 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,3 @@ - name: Main workflow on: @@ -17,11 +16,11 @@ jobs: - uses: actions/setup-go@v5 with: - go-version-file: 'go.mod' + go-version-file: "go.mod" - name: Build run: go build - + go-vet: runs-on: ubuntu-latest steps: @@ -29,7 +28,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version-file: 'go.mod' + go-version-file: "go.mod" - name: Go vet run: go vet ./... @@ -41,7 +40,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version-file: 'go.mod' + go-version-file: "go.mod" - name: Unit tests - run: go test ./... + run: go test ./... -cover diff --git a/README.md b/README.md index 2606655..79c9840 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # PFCP +[![GoDoc](https://godoc.org/github.com/dot-5g/pfcp?status.svg)](https://godoc.org/github.com/dot-5g/pfcp) + + A Go library for using the PFCP protocol in 5G networks as defined in the [ETSI TS 29.244 specification](https://www.etsi.org/deliver/etsi_ts/129200_129299/129244/16.04.00_60/ts_129244v160400p.pdf). ## Usage