diff --git a/LICENSE b/LICENSE index 261eeb9..1c0983d 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2023 Guillaume Belanger Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index aec7dd7..afe181e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,53 @@ # PFCP -A Go library for using the PFCP protocol in 5G networks. +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 ### Client ```go -pfcpClient := pfcp.New("1.2.3.4:8805") -err := pfcpClient.SendHeartbeatRequest() -if err != nil { - t.Errorf("SendHeartbeatRequest failed: %v", err) +package main + +import ( + "log" + + "github.com/dot-5g/pfcp/client" +) + +func main() { + pfcpClient := client.New("1.2.3.4:8805") + err := pfcpClient.SendHeartbeatRequest() + if err != nil { + log.Fatalf("SendHeartbeatRequest failed: %v", err) + } +} +``` + +### Server + + +```go +package main + +import ( + "github.com/dot-5g/pfcp/messages" + "github.com/dot-5g/pfcp/server" +) + +func main() { + pfcpServer := server.New("localhost:8805") + pfcpServer.HeartbeatRequest(HandleHeartbeatRequest) + pfcpServer.HeartbeatResponse(HandleHeartbeatResponse) + pfcpServer.Run() +} + +func HandleHeartbeatRequest(h *messages.HeartbeatRequest) { + // Do something +} + +func HandleHeartbeatResponse(h *messages.HeartbeatResponse) { + // Do something } + ``` diff --git a/client/client.go b/client/client.go index c96bea5..7cb4d18 100644 --- a/client/client.go +++ b/client/client.go @@ -5,6 +5,7 @@ import ( "log" "time" + "github.com/dot-5g/pfcp/messages" "github.com/dot-5g/pfcp/network" ) @@ -13,16 +14,6 @@ type Pfcp struct { Udp network.UdpSender } -type RecoveryTimeStamp time.Time - -type HeartbeatRequest struct { - RecoveryTimeStamp RecoveryTimeStamp -} - -type HeartbeatResponse struct { - RecoveryTimeStamp RecoveryTimeStamp -} - func New(ServerAddress string) *Pfcp { udpClient, err := network.NewUdp(ServerAddress) @@ -34,7 +25,7 @@ func New(ServerAddress string) *Pfcp { return &Pfcp{ServerAddress: ServerAddress, Udp: udpClient} } -func (pfcp *Pfcp) sendPfcpMessage(header PFCPHeader, payload []byte, messageType string) error { +func (pfcp *Pfcp) sendPfcpMessage(header messages.PFCPHeader, payload []byte, messageType string) error { message := serializeMessage(header, payload) if err := pfcp.Udp.Send(message); err != nil { log.Printf("Failed to send PFCP %s: %v\n", messageType, err) @@ -44,29 +35,18 @@ func (pfcp *Pfcp) sendPfcpMessage(header PFCPHeader, payload []byte, messageType return nil } -func serializeMessage(header PFCPHeader, payload []byte) []byte { - headerBytes := SerializePFCPHeader(header) - header.MessageLength = uint16(len(headerBytes) + len(payload)) +func serializeMessage(header messages.PFCPHeader, payload []byte) []byte { + header.MessageLength = uint16(4 + len(payload)) + headerBytes := messages.SerializePFCPHeader(header) return append(headerBytes, payload...) } -func (pfcp *Pfcp) SendHeartbeatRequest() error { - request := HeartbeatRequest{RecoveryTimeStamp: RecoveryTimeStamp(time.Now())} - requestBytes := request.ToBytes() - header := NewPFCPHeader(1, 1) - return pfcp.sendPfcpMessage(header, requestBytes, "Heartbeat Request") -} - -func (p *Pfcp) SendHeartbeatResponse() error { - response := HeartbeatRequest{RecoveryTimeStamp: RecoveryTimeStamp(time.Now())} - responseBytes := response.ToBytes() - header := NewPFCPHeader(2, 1) - return p.sendPfcpMessage(header, responseBytes, "Heartbeat Response") -} - -func (h HeartbeatRequest) ToBytes() []byte { - timestamp := time.Time(h.RecoveryTimeStamp).Unix() +func (pfcp *Pfcp) SendHeartbeatRequest() (messages.RecoveryTimeStamp, error) { + timestamp := time.Now().Unix() timeBytes := make([]byte, 8) binary.BigEndian.PutUint64(timeBytes, uint64(timestamp)) - return timeBytes + 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 945dcb3..d57a0dd 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -24,7 +24,7 @@ func TestGivenPfcpWhenSendHeartbeatRequestThenNoError(t *testing.T) { pfcpClient := client.New("127.0.0.1:8805") pfcpClient.Udp = mockSender - err := pfcpClient.SendHeartbeatRequest() + _, err := pfcpClient.SendHeartbeatRequest() if err != nil { t.Errorf("SendHeartbeatRequest failed: %v", err) } diff --git a/client/header.go b/client/header.go deleted file mode 100644 index 0cea269..0000000 --- a/client/header.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "bytes" - "encoding/binary" -) - -type PFCPHeader struct { - Version byte - MessageType byte - MessageLength uint16 - SEID uint64 - SequenceNumber uint32 - MessagePriority byte -} - -func SerializePFCPHeader(header PFCPHeader) []byte { - buf := new(bytes.Buffer) - binary.Write(buf, binary.BigEndian, header) - return buf.Bytes() -} - -func NewPFCPHeader(messageType byte, sequenceNumber uint32) PFCPHeader { - return PFCPHeader{ - Version: 1, - MessageType: messageType, - MessageLength: 0, - SEID: 0, - SequenceNumber: sequenceNumber, - MessagePriority: 0, - } -} diff --git a/main.go b/main.go index 4439efd..8060080 100644 --- a/main.go +++ b/main.go @@ -4,12 +4,29 @@ import ( "log" "github.com/dot-5g/pfcp/client" + "github.com/dot-5g/pfcp/messages" + "github.com/dot-5g/pfcp/server" ) 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) } } + +func RunServer() { + pfcpServer := server.New("localhost:8805") + pfcpServer.HeartbeatRequest(HandleHeartbeatRequest) + pfcpServer.HeartbeatResponse(HandleHeartbeatResponse) + pfcpServer.Run() +} + +func HandleHeartbeatRequest(h *messages.HeartbeatRequest) { + +} + +func HandleHeartbeatResponse(h *messages.HeartbeatResponse) { + +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..235f05f --- /dev/null +++ b/main_test.go @@ -0,0 +1,51 @@ +package main_test + +import ( + "sync" + "testing" + "time" + + "github.com/dot-5g/pfcp/client" + "github.com/dot-5g/pfcp/messages" + "github.com/dot-5g/pfcp/server" +) + +var ( + mu sync.Mutex + handlerCalled bool + receivedRecoveryTimestamp messages.RecoveryTimeStamp +) + +func HandleHeartbeatRequest(h *messages.HeartbeatRequest) { + mu.Lock() + defer mu.Unlock() + handlerCalled = true + receivedRecoveryTimestamp = h.RecoveryTimeStamp +} + +func TestGivenHandleHeartbeatRequestWhenRunThenHeartbeatRequestHandled(t *testing.T) { + pfcpServer := server.New("127.0.0.1:8805") + pfcpServer.HeartbeatRequest(HandleHeartbeatRequest) + + go pfcpServer.Run() + + time.Sleep(time.Second) + + pfcpClient := client.New("127.0.0.1:8805") + sentRecoveryTimeStamp, err := pfcpClient.SendHeartbeatRequest() + if err != nil { + t.Fatalf("Failed to send Heartbeat request: %v", err) + } + + time.Sleep(time.Second) + + mu.Lock() + if !handlerCalled { + t.Errorf("Heartbeat request handler was not called") + } + if receivedRecoveryTimestamp != sentRecoveryTimeStamp { + t.Errorf("Heartbeat request handler was called with wrong timestamp: %v", receivedRecoveryTimestamp) + } + mu.Unlock() + +} diff --git a/messages/header.go b/messages/header.go new file mode 100644 index 0000000..8ee99a0 --- /dev/null +++ b/messages/header.go @@ -0,0 +1,61 @@ +package messages + +import ( + "bytes" + "encoding/binary" +) + +type PFCPHeader struct { + Version byte + MessageType byte + MessageLength uint16 + SequenceNumber uint32 +} + +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) + firstOctet := (header.Version << 5) + buf.WriteByte(firstOctet) + + // Octet 2: Message Type (1 byte) + buf.WriteByte(header.MessageType) + + // Octets 3 and 4: Message Length (2 bytes) + binary.Write(buf, binary.BigEndian, header.MessageLength) + + // 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 + + // Octet 8: Spare (1 byte set to 0) + buf.WriteByte(0) + + 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 + MessageType: messageType, + MessageLength: 0, // To be set later + SequenceNumber: sequenceNumber, + } +} + +func ParsePFCPHeader(data []byte) PFCPHeader { + + header := PFCPHeader{} + header.Version = data[0] >> 5 + header.MessageType = data[1] + header.MessageLength = binary.BigEndian.Uint16(data[2:4]) + + seqNumBytes := make([]byte, 4) + copy(seqNumBytes, data[4:7]) + header.SequenceNumber = binary.BigEndian.Uint32(seqNumBytes) + + return header +} diff --git a/messages/heartbeat.go b/messages/heartbeat.go new file mode 100644 index 0000000..6651c50 --- /dev/null +++ b/messages/heartbeat.go @@ -0,0 +1,15 @@ +package messages + +import ( + "time" +) + +type RecoveryTimeStamp time.Time + +type HeartbeatRequest struct { + RecoveryTimeStamp RecoveryTimeStamp +} + +type HeartbeatResponse struct { + RecoveryTimeStamp RecoveryTimeStamp +} diff --git a/network/udpserver.go b/network/udpserver.go new file mode 100644 index 0000000..0ee5895 --- /dev/null +++ b/network/udpserver.go @@ -0,0 +1,46 @@ +package network + +import ( + "log" + "net" +) + +type UdpServer struct { + Handler func([]byte, net.Addr) +} + +func (udpServer *UdpServer) SetHandler(handler func([]byte, net.Addr)) { + udpServer.Handler = handler +} + +func NewUdpServer() *UdpServer { + return &UdpServer{} +} + +func (udpServer *UdpServer) Run(address string) error { + addr, err := net.ResolveUDPAddr("udp", address) + if err != nil { + return err + } + + conn, err := net.ListenUDP("udp", addr) + if err != nil { + return err + } + log.Printf("Listening on %s\n", addr) + + defer conn.Close() + + for { + buffer := make([]byte, 1024) + length, remoteAddr, err := conn.ReadFrom(buffer) + if err != nil { + log.Printf("Error reading from UDP: %v", err) + continue + } + + if udpServer.Handler != nil { + go udpServer.Handler(buffer[:length], remoteAddr) + } + } +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..a2969b1 --- /dev/null +++ b/server/server.go @@ -0,0 +1,76 @@ +package server + +import ( + "encoding/binary" + "log" + "net" + "time" + + "github.com/dot-5g/pfcp/messages" + "github.com/dot-5g/pfcp/network" +) + +type HandleHeartbeatRequest func(*messages.HeartbeatRequest) + +type HandleHeartbeatResponse func(*messages.HeartbeatResponse) + +type PfcpMessage struct { + Header messages.PFCPHeader + Message []byte +} + +type Server struct { + address string + udpServer *network.UdpServer + heartbeatRequestHandler HandleHeartbeatRequest + heartbeatResponseHandler HandleHeartbeatResponse +} + +func New(address string) *Server { + return &Server{ + address: address, + udpServer: network.NewUdpServer(), + } +} + +func (server *Server) Run() { + server.udpServer.SetHandler(server.handleUDPMessage) + server.udpServer.Run(server.address) +} + +func (server *Server) handleUDPMessage(data []byte, addr net.Addr) { + + pfcpMessage := ParseUDPMessage(data) + + 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.") + } + } +} + +func (server *Server) HeartbeatRequest(handler HandleHeartbeatRequest) { + server.heartbeatRequestHandler = handler +} + +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:]} +}