From 7526ba60df72fa65c2bde3be3c56af6f23ed42cf Mon Sep 17 00:00:00 2001 From: Louis Royer Date: Wed, 25 Sep 2024 10:29:20 +0200 Subject: [PATCH] Initial commit --- .github/dependabot.yml | 15 +++ .github/workflows/create-release.yml | 23 ++++ .github/workflows/main.yml | 28 ++++ .pre-commit-config.yaml | 8 ++ LICENSE | 21 +++ README.md | 5 + doc.go | 7 + encoding/args-mob-session.go | 142 ++++++++++++++++++++ encoding/doc.go | 8 ++ encoding/errors/doc.go | 7 + encoding/errors/errors.go | 15 +++ encoding/m-gtp4-ipv6-dst.go | 144 ++++++++++++++++++++ encoding/m-gtp4-ipv6-dst_test.go | 13 ++ encoding/m-gtp4-ipv6-src.go | 188 +++++++++++++++++++++++++++ encoding/m-gtp4-ipv6-src_test.go | 58 +++++++++ go.mod | 5 + go.sum | 2 + internal/utils/utils.go | 66 ++++++++++ internal/utils/utils_test.go | 54 ++++++++ 19 files changed, 809 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/create-release.yml create mode 100644 .github/workflows/main.yml create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 doc.go create mode 100644 encoding/args-mob-session.go create mode 100644 encoding/doc.go create mode 100644 encoding/errors/doc.go create mode 100644 encoding/errors/errors.go create mode 100644 encoding/m-gtp4-ipv6-dst.go create mode 100644 encoding/m-gtp4-ipv6-dst_test.go create mode 100644 encoding/m-gtp4-ipv6-src.go create mode 100644 encoding/m-gtp4-ipv6-src_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/utils/utils.go create mode 100644 internal/utils/utils_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a288f58 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + assignees: + - "louisroyer" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + assignees: + - "louisroyer" diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..c696cbc --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,23 @@ +name: Release +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get version number + id: version + run: echo "version=$(echo ${{ github.ref_name }} | cut -c2- -)" >> $GITHUB_OUTPUT + - name: Release + uses: ncipollo/release-action@v1 + with: + generateReleaseNotes: true + makeLatest: "legacy" + name: "Version ${{ steps.version.outputs.version }}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..297e435 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,28 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the "master" branch + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: +jobs: + go_build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + - name: Install depends + run: go get . + - name: Build + run: go build -v ./... + - name: Vet + run: go vet ./... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..416533b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending +exclude: ^.github/.*$ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0b734ba --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2023 Louis Royer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..111af85 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# RFC 9433 +This package provides functions to implement RFC 9433 (Segment Routing over IPv6 for the Mobile User Plane). + +## Licence +MIT diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..396323b --- /dev/null +++ b/doc.go @@ -0,0 +1,7 @@ +// Copyright 2023 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 rfc9433 provides functions to implement RFC9433 (Segment Routing over IPv6 for the Mobile User Plane). +package rfc9433 diff --git a/encoding/args-mob-session.go b/encoding/args-mob-session.go new file mode 100644 index 0000000..49c2bf9 --- /dev/null +++ b/encoding/args-mob-session.go @@ -0,0 +1,142 @@ +// Copyright 2023 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 rfc9433 + +import "encoding/binary" +import "github.com/nextmn/rfc9433/encoding/errors" + +const ( + // Field TEID + teidSizeByte = 4 // size of the field in bytes + teidSizeBit = teidSizeByte * 8 // size of the field in bits + teidPosByte = 1 // position of the field from the left in bytes + + // Field QFI + qfiSizeBit = 6 // size of the field + qfiPosBit = 2 // position from right of the byte in bits + qfiPosByte = 0 // position from left in bytes + qfiMask = (0xFF >> (8 - qfiSizeBit)) // mask (decoding: after shift to right; encoding before shift to left) + + // Field R + rSizeBit = 1 // size of the field + rPosBit = 1 // position from right of the byte in bits + rPosByte = 0 // position from left in bytes + rMask = (0xFF >> (8 - rSizeBit)) // mask (decoding: after shift to right; encoding before shift to left) + + // Field U + uSizeBit = 1 // size of the field + uPosBit = 0 // position from right of the byte in bits + uPosByte = 0 // position from left in bytes + uMask = (0xFF >> (8 - uSizeBit)) // mask (decoding: after shift to right; encoding before shift to left) +) + +// Args.Mob.Session as defined in RFC 9433, section 6.1: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | QFI |R|U| PDU Session ID | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |PDU Sess(cont')| +// +-+-+-+-+-+-+-+-+ +// Figure 8: Args.Mob.Session Format +type ArgsMobSession struct { + qfi uint8 // QoS Flow Identifier (6 bits) + r uint8 // Reflective QoS Indication (1 bit) + u uint8 // Unused and for future use (1 bit) + pduSessionID uint32 // Identifier of PDU Session. The GTP-U equivalent is TEID (32 bits) +} + +// NewArgsMobSession creates an ArgsMobSession. +func NewArgsMobSession(qfi uint8, r bool, u bool, pduSessionID uint32) *ArgsMobSession { + var ruint uint8 = 0 + if r { + ruint = 1 + } + var uuint uint8 = 0 + if u { + uuint = 1 + } + return &ArgsMobSession{ + qfi: qfi, + r: ruint, + u: uuint, + pduSessionID: pduSessionID, + } +} + +// ParseArgsMobSession parses given byte sequence as an ArgsMobSession. +func ParseArgsMobSession(b []byte) (*ArgsMobSession, error) { + a := &ArgsMobSession{} + if err := a.UnmarshalBinary(b); err != nil { + return nil, err + } + return a, nil +} + +// QFI returns the Qos Flow Identifier for this ArgsMobSession. +func (a *ArgsMobSession) QFI() uint8 { + return a.qfi +} + +// R returns the Reflective QoS Indication for this ArgsMobSession. +func (a *ArgsMobSession) R() bool { + if a.r == 0 { + return false + } + return true +} + +// U returns the U bit for this ArgsMobSession. +func (a *ArgsMobSession) U() bool { + if a.u == 0 { + return false + } + return true +} + +// PDUSessionID returns the PDU Session Identifier for this ArgsMobSession. The GTP-U equivalent is TEID. +func (a *ArgsMobSession) PDUSessionID() uint32 { + return a.pduSessionID +} + +// MarshalLen returns the serial length of ArgsMobSession. +func (a *ArgsMobSession) MarshalLen() int { + return 5 +} + +// Marshal returns the byte sequence generated from ArgsMobSession. +func (a *ArgsMobSession) Marshal() ([]byte, error) { + b := make([]byte, a.MarshalLen()) + if err := a.MarshalTo(b); err != nil { + return nil, err + } + return b, nil +} + +// MarshalTo puts the byte sequence in the byte array given as b. +func (a *ArgsMobSession) MarshalTo(b []byte) error { + if len(b) < a.MarshalLen() { + return errors.ErrTooShortToMarshal + } + b[qfiPosByte] |= (qfiMask & a.qfi) << qfiPosBit + b[rPosByte] |= (rMask & a.r) << rPosBit + b[uPosByte] |= (uMask & a.u) << uPosBit + binary.BigEndian.PutUint32(b[teidPosByte:teidPosByte+teidSizeByte], a.pduSessionID) + return nil +} + +// UnmarshalBinary sets the values retrieved from byte sequence in an ArgsMobSession. +func (a *ArgsMobSession) UnmarshalBinary(b []byte) error { + if len(b) < 5 { + return errors.ErrTooShortToParse + } + a.qfi = qfiMask & (b[qfiPosByte] >> qfiPosBit) + a.r = rMask & (b[rPosByte] >> rPosBit) + a.u = uMask & (b[uPosByte] >> uPosBit) + a.pduSessionID = binary.BigEndian.Uint32(b[teidPosByte : teidPosByte+teidSizeByte]) + return nil +} diff --git a/encoding/doc.go b/encoding/doc.go new file mode 100644 index 0000000..f7b0d7c --- /dev/null +++ b/encoding/doc.go @@ -0,0 +1,8 @@ +// Copyright 2023 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 encoding provides encoding and decoding of IPv6 Addresses +// used by RFC 9433 (Segment Routing over IPv6 for the Mobile User Plane). +package rfc9433 diff --git a/encoding/errors/doc.go b/encoding/errors/doc.go new file mode 100644 index 0000000..9d66439 --- /dev/null +++ b/encoding/errors/doc.go @@ -0,0 +1,7 @@ +// Copyright 2023 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 errors provides common encoding errors used by rfc9433 package. +package errors diff --git a/encoding/errors/errors.go b/encoding/errors/errors.go new file mode 100644 index 0000000..1c24bb9 --- /dev/null +++ b/encoding/errors/errors.go @@ -0,0 +1,15 @@ +// Copyright 2023 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 errors + +import "errors" + +var ( + ErrTooShortToMarshal = errors.New("too short to serialize") + ErrTooShortToParse = errors.New("too short to parse") + ErrPrefixLength = errors.New("wrong prefix length") + ErrOutOfRange = errors.New("out of range") +) diff --git a/encoding/m-gtp4-ipv6-dst.go b/encoding/m-gtp4-ipv6-dst.go new file mode 100644 index 0000000..e4dd83c --- /dev/null +++ b/encoding/m-gtp4-ipv6-dst.go @@ -0,0 +1,144 @@ +// Copyright 2023 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 rfc9433 + +import ( + "net/netip" + + "github.com/nextmn/rfc9433/encoding/errors" + "github.com/nextmn/rfc9433/internal/utils" +) + +// RFC 9433, section 6.6 (End.M.GTP4.E): +// The End.M.GTP.E SID in S has the following format: +// +// 0 127 +// +-----------------------+-------+----------------+---------+ +// | SRGW-IPv6-LOC-FUNC |IPv4DA |Args.Mob.Session|0 Padded | +// +-----------------------+-------+----------------+---------+ +// 128-a-b-c a b c +// Figure 9: End.M.GTP4.E SID Encoding +type MGTP4IPv6Dst struct { + prefix netip.Prefix // prefix in canonical form + ipv4 [4]byte + argsMobSession *ArgsMobSession +} + +// NewMGTP4IPv6Dst creates a new MGTP4IPv6Dst. +func NewMGTP4IPv6Dst(prefix netip.Prefix, ipv4 [4]byte, a *ArgsMobSession) *MGTP4IPv6Dst { + return &MGTP4IPv6Dst{ + prefix: prefix.Masked(), + ipv4: ipv4, + argsMobSession: a, + } +} + +// ParseMGTP4IPv6Dst parses a given byte sequence into a MGTP4IPv6Dst according to the given prefixLength. +func ParseMGTP4IPv6Dst(ipv6Addr [16]byte, prefixLength uint) (*MGTP4IPv6Dst, error) { + // prefix extraction + a := netip.AddrFrom16(ipv6Addr) + prefix := netip.PrefixFrom(a, int(prefixLength)).Masked() + + // ipv4 extraction + var ipv4 [4]byte + if src, err := utils.FromIPv6(ipv6Addr, prefixLength, 4); err != nil { + return nil, err + } else { + copy(ipv4[:], src[:4]) + } + + // argMobSession extraction + argsMobSessionSlice, err := utils.FromIPv6(ipv6Addr, prefixLength+8*4, 5) + argsMobSession, err := ParseArgsMobSession(argsMobSessionSlice) + if err != nil { + return nil, err + } + return &MGTP4IPv6Dst{ + prefix: prefix, + ipv4: ipv4, + argsMobSession: argsMobSession, + }, nil +} + +// IPv4 returns the IPv4 Address encoded in the MGTP4IPv6Dst. +func (e *MGTP4IPv6Dst) IPv4() netip.Addr { + return netip.AddrFrom4(e.ipv4) +} + +// ArgsMobSession returns the ArgsMobSession encoded in the MGTP4IPv6Dst. +func (e *MGTP4IPv6Dst) ArgsMobSession() *ArgsMobSession { + return e.argsMobSession +} + +// QFI returns the QFI encoded in the MGTP4IPv6Dst's ArgsMobSession. +func (e *MGTP4IPv6Dst) QFI() uint8 { + return e.argsMobSession.QFI() +} + +// R returns the R bit encoded in the MGTP4IPv6Dst's ArgsMobSession. +func (e *MGTP4IPv6Dst) R() bool { + return e.argsMobSession.R() +} + +// U returns the U bit encoded in the MGTP4IPv6Dst's ArgsMobSession. +func (e *MGTP4IPv6Dst) U() bool { + return e.argsMobSession.U() +} + +// PDUSessionID returns the PDUSessionID for this MGTP4IPv6Dst's ArgsMobSession. +func (a *MGTP4IPv6Dst) PDUSessionID() uint32 { + return a.argsMobSession.PDUSessionID() +} + +// Prefix returns the IPv6 Prefix for this MGTP4IPv6Dst. +func (e *MGTP4IPv6Dst) Prefix() netip.Prefix { + return e.prefix +} + +// MarshalLen returns the serial length of MGTP4IPv6Dst. +func (a *MGTP4IPv6Dst) MarshalLen() int { + return 16 +} + +// Marshal returns the byte sequence generated from MGTP4IPv6Dst. +func (a *MGTP4IPv6Dst) Marshal() ([]byte, error) { + b := make([]byte, a.MarshalLen()) + if err := a.MarshalTo(b); err != nil { + return nil, err + } + return b, nil +} + +// MarshalTo puts the byte sequence in the byte array given as b. +// warning: no caching is done, this result will be recomputed at each call +func (a *MGTP4IPv6Dst) MarshalTo(b []byte) error { + if len(b) < a.MarshalLen() { + return errors.ErrTooShortToMarshal + } + // init ipv6 with the prefix + prefix := a.prefix.Addr().As16() + copy(b, prefix[:]) + + ipv4 := netip.AddrFrom4(a.ipv4).AsSlice() + bits := a.prefix.Bits() + if bits == -1 { + return errors.ErrPrefixLength + } + + // add ipv4 + if err := utils.AppendToSlice(b, uint(bits), ipv4); err != nil { + return err + } + argsMobSessionB, err := a.argsMobSession.Marshal() + if err != nil { + return err + } + // add Args-Mob-Session + if err := utils.AppendToSlice(b, uint(bits+8*4), argsMobSessionB); err != nil { + return err + } + return nil +} diff --git a/encoding/m-gtp4-ipv6-dst_test.go b/encoding/m-gtp4-ipv6-dst_test.go new file mode 100644 index 0000000..01f389a --- /dev/null +++ b/encoding/m-gtp4-ipv6-dst_test.go @@ -0,0 +1,13 @@ +// Copyright 2023 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 rfc9433 + +import "net/netip" + +func ExampleMGTP4IPv6Dst() { + dst := NewMGTP4IPv6Dst(netip.MustParsePrefix("3fff::/20"), netip.MustParseAddr("203.0.113.1").As4(), NewArgsMobSession(0, false, false, 1)) + dst.Marshal() +} diff --git a/encoding/m-gtp4-ipv6-src.go b/encoding/m-gtp4-ipv6-src.go new file mode 100644 index 0000000..5e0fc66 --- /dev/null +++ b/encoding/m-gtp4-ipv6-src.go @@ -0,0 +1,188 @@ +// Copyright 2023 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 rfc9433 + +import ( + "encoding/binary" + "net/netip" + + "github.com/nextmn/rfc9433/encoding/errors" + "github.com/nextmn/rfc9433/internal/utils" +) + +const ( + // "IPv6 Length" field + ipv6LenEncodingSizeBit = 7 // size of the field in bits + ipv6LenEncodingPosBit = 0 // position from right of the byte in bits + ipv6LenEncodingPosByte = 15 // position from left in bytes + ipv6LenEncodingMask = (0xFF >> (8 - ipv6LenEncodingSizeBit)) // mask (decoding: after shift to right; encoding before shift to left) +) + +// RFC 9433, section 6.6 (End.M.GTP4.E): +// The IPv6 Source Address has the following format: +// +// 0 127 +// +----------------------+--------+--------------------------+ +// | Source UPF Prefix |IPv4 SA | any bit pattern(ignored) | +// +----------------------+--------+--------------------------+ +// 128-a-b a b +// Figure 10: IPv6 SA Encoding for End.M.GTP4.E +// +// With NextMN implementation, we choose to deviate from the RFC +// because RFC's proposal doesn't allow to retrieve +// the IPv4 SA without knowing the prefix length, +// which may be different for 2 packets issued from 2 different headends. +// +// To allow the endpoint to be stateless, we need to know the prefix. +// We propose to encode it on the 7 last bits of the IPv6 SA. +// +// The other option would have been to directly put the IPv4 SA at the end of the IPv6 SA (bytes 12 to 15), +// but this would imply matching on /128 if the IPv4 SA is used for source routing purpose, +// and thus breaking compatibility with future new patterns. +// +// We also introduce a new field that will carry the source UDP port to be used in the newly created GTP4 packet. +// +// This field is intended to help load balancing, as specified in [TS 129.281, section 4.4.2.0]: +// +// "For the GTP-U messages described below (other than the Echo Response message, see clause 4.4.2.2), the UDP Source Port +// or the Flow Label field (see IETF RFC 6437) should be set dynamically by the sending GTP-U entity to help +// balancing the load in the transport network". +// +// Since the headend has a better view than End.M.GTP4.E on +// the origin of the flows, and can be helped by the control plane, +// it makes sense to generate the source port number on headend side, +// and to carry it during transit through SR domain. +// +// Note: even with this proposal, the remaining space (73 bits) is bigger +// than what remains for LOC+FUNC in the SID (56 bits). +// +// 0 127 +// +----------------------+--------+----------------+--------------------------+---------------+ +// | Source UPF Prefix |IPv4 SA | UDP Source Port| any bit pattern(ignored) | Prefix length | +// +----------------------+--------+----------------+--------------------------+---------------+ +// 128-a-b'-c-7 a (32 bits) c (16 bits) b' 7 bits +// IPv6 SA Encoding for End.M.GTP4.E in NextMN +// +// [TS 129.281, section 4.4.2.0]: https://www.etsi.org/deliver/etsi_ts/129200_129299/129281/17.04.00_60/ts_129281v170400p.pdf#page=16 +type MGTP4IPv6Src struct { + prefix netip.Prefix // prefix in canonical form + ipv4 [4]byte + udp uint16 +} + +// NewMGTP4IPv6Src creates a nw MGTP4IPv6Src +func NewMGTP4IPv6Src(prefix netip.Prefix, ipv4 [4]byte, udpPortNumber uint16) *MGTP4IPv6Src { + return &MGTP4IPv6Src{ + prefix: prefix.Masked(), + ipv4: ipv4, + udp: udpPortNumber, + } +} + +// ParseMGTP4IPv6SrcNextMN parses a given IPv6 source address with NextMN bit pattern into a MGTP4IPv6Src +func ParseMGTP4IPv6SrcNextMN(addr [16]byte) (*MGTP4IPv6Src, error) { + // Prefix length extraction + prefixLen := uint(ipv6LenEncodingMask & (addr[ipv6LenEncodingPosByte] >> ipv6LenEncodingPosBit)) + if prefixLen == 0 { + // even if globally routable IPv6 Prefix size cannot currently be less than 32 (per ICANN policy), + // nothing prevent the use of such prefix with ULA (fc00::/7) + // or, in the future, a prefix from a currently not yet allocated address block. + return nil, errors.ErrPrefixLength + } + if prefixLen+8*4+16+ipv6LenEncodingSizeBit > 8*16 { + return nil, errors.ErrOutOfRange + } + + // udp port extraction + var udp [2]byte + if src, err := utils.FromIPv6(addr, prefixLen+8*4, 2); err != nil { + return nil, err + } else { + copy(udp[:], src[:2]) + } + + if r, err := ParseMGTP4IPv6Src(addr, prefixLen); err != nil { + return nil, err + } else { + r.udp = binary.BigEndian.Uint16([]byte{udp[0], udp[1]}) + return r, nil + } +} + +// ParseMGTP4IPv6SrcNextMN parses a given IPv6 source address without any specific bit pattern into a MGTP4IPv6Src +func ParseMGTP4IPv6Src(addr [16]byte, prefixLen uint) (*MGTP4IPv6Src, error) { + // prefix extraction + a := netip.AddrFrom16(addr) + prefix := netip.PrefixFrom(a, int(prefixLen)).Masked() + + // ipv4 extraction + var ipv4 [4]byte + if src, err := utils.FromIPv6(addr, prefixLen, 4); err != nil { + return nil, err + } else { + copy(ipv4[:], src[:4]) + } + + return &MGTP4IPv6Src{ + prefix: prefix, + ipv4: ipv4, + }, nil +} + +// IPv4 returns the IPv4 Address encoded in the MGTP4IPv6Src. +func (e *MGTP4IPv6Src) IPv4() netip.Addr { + return netip.AddrFrom4(e.ipv4) +} + +// UDPPortNumber returns the UDP Port Number encoded in the MGTP4IPv6Src (0 if not set). +func (e *MGTP4IPv6Src) UDPPortNumber() uint16 { + return e.udp +} + +// MarshalLen returns the serial length of MGTP4IPv6Src. +func (a *MGTP4IPv6Src) MarshalLen() int { + return 16 +} + +// Marshal returns the byte sequence generated from MGTP4IPv6Src. +func (a *MGTP4IPv6Src) Marshal() ([]byte, error) { + b := make([]byte, a.MarshalLen()) + if err := a.MarshalTo(b); err != nil { + return nil, err + } + return b, nil +} + +// MarshalTo puts the byte sequence in the byte array given as b. +// warning: no caching is done, this result will be recomputed at each call +func (a *MGTP4IPv6Src) MarshalTo(b []byte) error { + if len(b) < a.MarshalLen() { + return errors.ErrTooShortToMarshal + } + // init b with prefix + prefix := a.prefix.Addr().As16() + copy(b, prefix[:]) + + ipv4 := netip.AddrFrom4(a.ipv4).AsSlice() + udp := make([]byte, 2) + binary.BigEndian.PutUint16(udp, a.udp) + bits := a.prefix.Bits() + if bits == -1 { + return errors.ErrPrefixLength + } + + // add ipv4 + if err := utils.AppendToSlice(b, uint(bits), ipv4); err != nil { + return err + } + // add upd port + if err := utils.AppendToSlice(b, uint(bits+8*4), udp); err != nil { + return err + } + // add prefix length + b[ipv6LenEncodingPosByte] = byte(bits) + return nil +} diff --git a/encoding/m-gtp4-ipv6-src_test.go b/encoding/m-gtp4-ipv6-src_test.go new file mode 100644 index 0000000..1fa2c6e --- /dev/null +++ b/encoding/m-gtp4-ipv6-src_test.go @@ -0,0 +1,58 @@ +// Copyright 2023 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 rfc9433 + +import ( + "fmt" + "net/netip" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func ExampleMGTP4IPv6Src() { + src := NewMGTP4IPv6Src(netip.MustParsePrefix("3fff::/20"), netip.MustParseAddr("203.0.113.1").As4(), 1337) + src.Marshal() +} + +func TestMGTP4IPv6Src(t *testing.T) { + ip_addr := [16]byte{ + 0x20, 0x01, 0xDB, 0x08, + 192, 0, 2, 1, + 0x01, 0x23, + 0x55, 0x55, 0x55, 0x55, 0x55, + 32, + } + + e, err := ParseMGTP4IPv6SrcNextMN(ip_addr) + if err != nil { + t.Fatal(err) + } + if e.IPv4().Compare(netip.MustParseAddr("192.0.2.1")) != 0 { + t.Fatalf("Cannot extract ipv4 correctly: %s", e.IPv4()) + } + if e.UDPPortNumber() != 0x0123 { + t.Fatalf("Cannot extract udp port number correctly: %x", e.UDPPortNumber()) + } + ip_addr2 := NewMGTP4IPv6Src(netip.MustParsePrefix("fd00:1:1::/48"), [4]byte{10, 0, 4, 1}, 0x1234) + b, err := ip_addr2.Marshal() + if err != nil { + t.Fatal(err) + } + res2 := []byte{ + 0xfd, 0x00, 0x00, 0x01, 0x00, 0x01, + 10, 0, 4, 1, + 0x12, 0x34, + 0x00, 0x00, 0x00, + 48, + } + fmt.Println(b) + fmt.Println(res2) + if diff := cmp.Diff(b, res2); diff != "" { + t.Error(diff) + } + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a50f581 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/nextmn/rfc9433 + +go 1.22.7 + +require github.com/google/go-cmp v0.6.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5a8d551 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..ccdb80e --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,66 @@ +// Copyright 2023 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 utils + +import "github.com/nextmn/rfc9433/encoding/errors" + +// ipv6: Address to extract bits from +// startBit: offset in bits +// length: length of result in Bytes +func FromIPv6(ipv6 [16]byte, startBit uint, length uint) ([]byte, error) { + if uint(len(ipv6)) < length { + return nil, errors.ErrTooShortToParse + } + if startBit+uint(length*8) > 8*uint(len(ipv6)) { + return nil, errors.ErrOutOfRange + } + startByte := startBit / 8 + offset := startBit % 8 + ret := make([]byte, length) + if offset == 0 { + copy(ret, ipv6[startByte:startByte+length]) + return ret, nil + } + + // init left + for i, b := range ipv6[startByte : startByte+length] { + ret[i] = (b << offset) + } + // init right + for i, b := range ipv6[startByte+1 : startByte+length] { + ret[i] |= b >> (8 - offset) + } + return ret, nil +} + +// usage conditions : +// 1. slice must be large enough +// 2. every bit after endBit should be zero (no reset is performed in the function) +func AppendToSlice(slice []byte, endBit uint, appendThis []byte) error { + endByte := endBit / 8 + offset := endBit % 8 + isOffset := 0 + if offset > 0 { + isOffset = 1 + } + if isOffset+int(endByte)+len(appendThis) > len(slice) { + return errors.ErrTooShortToMarshal + } + if offset == 0 { + // concatenate slices + copy(slice[endByte:], appendThis[:]) + return nil + } + // add right part of bytes + for i, b := range appendThis { + slice[int(endByte)+i] |= b >> offset + } + // add left part of bytes + for i, b := range appendThis { + slice[int(endByte)+isOffset+i] |= b << (8 - offset) + } + return nil +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 0000000..2e4089e --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,54 @@ +// Copyright 2023 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 utils + +import ( + "net/netip" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestFromIPv6(t *testing.T) { + res, err := FromIPv6(netip.MustParseAddr("::ff:192.168.0.1").As16(), 128-8*4, 4) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(res, []byte{192, 168, 0, 1}); diff != "" { + t.Error(diff) + } + res, err = FromIPv6(netip.MustParseAddr("ff00::").As16(), 1, 1) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(res, []byte{0xFE}); diff != "" { + t.Error(diff) + } + res, err = FromIPv6(netip.MustParseAddr("ff55::").As16(), 2, 2) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(res, []byte{0xFD, 0x54}); diff != "" { + t.Error(diff) + } +} + +func TestAppendToSlice(t *testing.T) { + b1 := []byte{0xFF, 0x00, 0x00, 0x00} + if err := AppendToSlice(b1, 8, []byte{0x00, 0xAA}); err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(b1, []byte{0xFF, 0x00, 0xAA, 0x00}); diff != "" { + t.Error(diff) + } + b2 := []byte{0xE0, 0x00, 0x00, 0x00} + if err := AppendToSlice(b2, 3, []byte{0x00, 0xAA, 0xFF}); err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(b2, []byte{0xE0, 0x15, 0x5F, 0xE0}); diff != "" { + t.Error(diff) + } +}