diff --git a/go.mod b/go.mod index ede5b823..d503022e 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/antihax/optional v1.0.0 - github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/davecgh/go-spew v1.1.1 github.com/free5gc/aper v1.0.5 github.com/free5gc/nas v1.1.3 diff --git a/go.sum b/go.sum index 7c65e43d..fb7a8eb6 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UME github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= diff --git a/internal/context/upf.go b/internal/context/upf.go index 79cc3c02..32e1f7f8 100644 --- a/internal/context/upf.go +++ b/internal/context/upf.go @@ -604,21 +604,12 @@ func (upf *UPF) AddURR(urrID uint32, opts ...UrrOpt) (urr *URR, err error) { opt(urr) } - if urrId == 0 { - if URRID, err := upf.urrID(); err != nil { - } else { - urr.URRID = URRID - upf.urrPool.Store(urr.URRID, urr) - } - } else { - urr.URRID = urrId - upf.urrPool.Store(urr.URRID, urr) - } + upf.urrPool.Store(urr.URRID, urr) return urr, nil } func (upf *UPF) GetUUID() uuid.UUID { - return upf.uuid + return upf.ID } func (upf *UPF) GetQERById(qerId uint32) *QER { @@ -701,3 +692,21 @@ func (upf *UPF) IsAssociated() error { return nil } } + +func (upf *UPF) MatchedSelection(selection *UPFSelectionParams) bool { + for _, snssaiInfo := range upf.SNssaiInfos { + currentSnssai := snssaiInfo.SNssai + if currentSnssai.Equal(selection.SNssai) { + for _, dnnInfo := range snssaiInfo.DnnList { + if dnnInfo.Dnn == selection.Dnn { + if selection.Dnai == "" { + return true + } else if dnnInfo.ContainsDNAI(selection.Dnai) { + return true + } + } + } + } + } + return false +} diff --git a/internal/sbi/consumer/pcf_service_test.go b/internal/sbi/consumer/pcf_service_test.go index df119e9a..d137dc2b 100644 --- a/internal/sbi/consumer/pcf_service_test.go +++ b/internal/sbi/consumer/pcf_service_test.go @@ -27,6 +27,7 @@ var testConfig = factory.Config{ BindingIPv4: "127.0.0.1", Port: 8000, }, + UserPlaneInformation: &factory.UserPlaneInformation{}, }, } diff --git a/pkg/factory/config.go b/pkg/factory/config.go index 73fa7b94..bf183c91 100644 --- a/pkg/factory/config.go +++ b/pkg/factory/config.go @@ -464,17 +464,14 @@ func (u *UserPlaneInformation) Validate() (result bool, err error) { return str == "N3" || str == "N9" }) - // register CIDR to govalidator - govalidator.TagMap["cidr"] = govalidator.Validator(func(str string) bool { - return govalidator.IsCIDR(str) - }) - + // collect all validation errors for UserPlaneInformation + var validationErrors govalidator.Errors result = true - // validate struct field correctness recursively + // validate struct field correctness if ok, errStruct := govalidator.ValidateStruct(u); !ok { result = false - err = appendInvalid(errStruct) + validationErrors = append(validationErrors, appendInvalid(errStruct).(govalidator.Errors)...) } var upNodeNames []string @@ -484,19 +481,19 @@ func (u *UserPlaneInformation) Validate() (result bool, err error) { // call custom validation function (semantic validation) if ok, errSemantic := upNode.Validate(); !ok { result = false - err = appendInvalid(errSemantic) + validationErrors = append(validationErrors, errSemantic.(govalidator.Errors)...) } } for _, link := range u.Links { if !slices.Contains(upNodeNames, link.A) || !slices.Contains(upNodeNames, link.B) { result = false - err = appendInvalid(fmt.Errorf("Link %s--%s contains unknown node name, check 'userplaneInformation'", - link.A, link.B)) + validationErrors = append(validationErrors, + fmt.Errorf("Link %s--%s contains unknown node name", link.A, link.B)) } } - return result, err + return result, error(validationErrors) } // UPNode represent the user plane node @@ -521,6 +518,7 @@ type UPFConfig struct { } func (gNB *GNBConfig) Validate() (result bool, err error) { + // no semantic validation (yet) for GNBs return true, nil } @@ -529,15 +527,15 @@ func (gNB *GNBConfig) GetType() string { } func (upf *UPFConfig) Validate() (result bool, err error) { + var validationErrors govalidator.Errors result = true n3IfsNum := 0 n9IfsNum := 0 - for _, iface := range upf.Interfaces { if ok, errIface := iface.Validate(); !ok { result = false - err = appendInvalid(errIface) + validationErrors = append(validationErrors, errIface.(govalidator.Errors)...) } if iface.InterfaceType == "N3" { @@ -551,24 +549,26 @@ func (upf *UPFConfig) Validate() (result bool, err error) { if n3IfsNum == 0 && n9IfsNum == 0 { result = false - err = appendInvalid(fmt.Errorf("UPF %s must have a user plane interface (N3 or N9)", upf.NodeID)) + validationErrors = append(validationErrors, + fmt.Errorf("UPF %s must have a user plane interface (N3 or N9)", upf.NodeID)) } if n3IfsNum > 1 || n9IfsNum > 1 { result = false - err = appendInvalid(fmt.Errorf( - "UPF %s: There is currently no support for multiple N3/ N9 interfaces: N3 number(%d), N9 number(%d)", - upf.NodeID, n3IfsNum, n9IfsNum)) + validationErrors = append(validationErrors, + fmt.Errorf( + "UPF %s: There is currently no support for multiple N3/ N9 interfaces: N3 number(%d), N9 number(%d)", + upf.NodeID, n3IfsNum, n9IfsNum)) } for _, snssaiInfo := range upf.SNssaiInfos { if ok, errSNSSAI := snssaiInfo.Validate(); !ok { result = false - err = appendInvalid(errSNSSAI) + validationErrors = append(validationErrors, errSNSSAI.(govalidator.Errors)...) } } - return result, err + return result, error(validationErrors) } func (upf *UPFConfig) GetNodeID() string { @@ -586,14 +586,27 @@ type Interface struct { } func (i *Interface) Validate() (result bool, err error) { + var validationErrors govalidator.Errors result = true + + switch i.InterfaceType { + case "N3": + case "N9": + case "N6": + default: + result = false + validationErrors = append(validationErrors, + fmt.Errorf("Invalid interface type %s: must be N3 or N9.", i.InterfaceType)) + } for _, endpoint := range i.Endpoints { if ok := govalidator.IsHost(endpoint); !ok { result = false - err = appendInvalid(fmt.Errorf("Invalid endpoint: %s should be one of IPv4, IPv6, FQDN.", endpoint)) + validationErrors = append(validationErrors, + fmt.Errorf("Invalid endpoint: %s should be one of IPv4, IPv6, FQDN.", endpoint)) } } - return + + return result, error(validationErrors) } type SnssaiUpfInfoItem struct { @@ -602,20 +615,23 @@ type SnssaiUpfInfoItem struct { } func (s *SnssaiUpfInfoItem) Validate() (result bool, err error) { + var validationErrors govalidator.Errors result = true if s.SNssai != nil { if ok := (s.SNssai.Sst >= 0 && s.SNssai.Sst <= 255); !ok { result = false - err = appendInvalid(fmt.Errorf("Invalid sNssai.Sst: %s should be in range 0-255.", - strconv.Itoa(int(s.SNssai.Sst)))) + validationErrors = append(validationErrors, + fmt.Errorf("Invalid sNssai.Sst: %s should be in range 0-255.", + strconv.Itoa(int(s.SNssai.Sst)))) } if s.SNssai.Sd != "" { if ok := govalidator.StringMatches(s.SNssai.Sd, "^[0-9A-Fa-f]{6}$"); !ok { result = false - err = appendInvalid(fmt.Errorf( - "Invalid sNssai.Sd: %s should be 3 bytes hex string and in range 000000-FFFFFF.", s.SNssai.Sd)) + validationErrors = append(validationErrors, + fmt.Errorf("Invalid sNssai.Sd: %s should be 3 bytes hex string and in range 000000-FFFFFF.", + s.SNssai.Sd)) } } } @@ -623,11 +639,15 @@ func (s *SnssaiUpfInfoItem) Validate() (result bool, err error) { for _, dnnInfo := range s.DnnUpfInfoList { if ok, errDNNInfo := dnnInfo.Validate(); !ok { result = false - err = appendInvalid(errDNNInfo) + validationErrors = append(validationErrors, errDNNInfo.(govalidator.Errors)...) } } - return + if len(validationErrors) > 0 { + return result, error(validationErrors) + } + + return result, nil } type DnnUpfInfoItem struct { @@ -639,40 +659,77 @@ type DnnUpfInfoItem struct { } func (d *DnnUpfInfoItem) Validate() (result bool, err error) { + // collect all errors + var validationErrors govalidator.Errors result = true if len(d.Dnn) == 0 { result = false - err = appendInvalid(fmt.Errorf("Invalid DnnUpfInfoItem: dnn must not be empty.")) + validationErrors = append(validationErrors, + fmt.Errorf("Invalid DnnUpfInfoItem: dnn must not be empty.")) } if len(d.Pools) == 0 { result = false - err = appendInvalid(fmt.Errorf("Invalid DnnUpfInfoItem: requires at least one dynamic IP pool.")) + validationErrors = append(validationErrors, + fmt.Errorf("Invalid DnnUpfInfoItem: requires at least one dynamic IP pool.")) } var prefixes []netaddr.IPPrefix for _, pool := range d.Pools { - prefix, errCidrCheck := netaddr.ParseIPPrefix(pool.Cidr) - if errCidrCheck != nil { + // CIDR check + prefix, ok := netaddr.ParseIPPrefix(pool.Cidr) + if ok != nil { result = false - err = appendInvalid(fmt.Errorf("Invalid CIDR: %s.", pool.Cidr)) + validationErrors = append(validationErrors, fmt.Errorf("Invalid CIDR: %s.", pool.Cidr)) } else { prefixes = append(prefixes, prefix) } } - // check overlap with dynamic and static pools + // check overlap within dynamic pools for i := 0; i < len(prefixes); i++ { for j := i + 1; j < len(prefixes); j++ { if prefixes[i].Overlaps(prefixes[j]) { result = false - err = appendInvalid(fmt.Errorf("overlap detected between pools %s and %s", prefixes[i], prefixes[j])) + validationErrors = append(validationErrors, + fmt.Errorf("overlap detected between dynamic pools %s and %s", prefixes[i], prefixes[j])) } } } - return result, err + // check static pools CIDR and overlap with dynamic pools + var staticPrefixes []netaddr.IPPrefix + for _, staticPool := range d.StaticPools { + // CIDR check + staticPrefix, ok := netaddr.ParseIPPrefix(staticPool.Cidr) + if ok != nil { + result = false + validationErrors = append(validationErrors, fmt.Errorf("Invalid CIDR: %s.", staticPool.Cidr)) + } else { + staticPrefixes = append(staticPrefixes, staticPrefix) + for _, prefix := range prefixes { + if staticPrefix.Overlaps(prefix) { + result = false + validationErrors = append(validationErrors, + fmt.Errorf("overlap detected between static pool %s and dynamic pool %s", staticPrefix, prefix)) + } + } + } + } + + // check overlap within static pools + for i := 0; i < len(staticPrefixes); i++ { + for j := i + 1; j < len(staticPrefixes); j++ { + if staticPrefixes[i].Overlaps(staticPrefixes[j]) { + result = false + validationErrors = append(validationErrors, + fmt.Errorf("overlap detected between static pools %s and %s", staticPrefixes[i], staticPrefixes[j])) + } + } + } + + return result, error(validationErrors) } type UPLink struct { diff --git a/pkg/factory/config_test.go b/pkg/factory/config_test.go index a8c7215a..be2726a9 100644 --- a/pkg/factory/config_test.go +++ b/pkg/factory/config_test.go @@ -1,6 +1,7 @@ package factory_test import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -55,84 +56,124 @@ func baseUPF() *factory.UPFConfig { } } -func TestUPNodeConfigInterfaceValidation(t *testing.T) { +func baseUPI() *factory.UserPlaneInformation { + return &factory.UserPlaneInformation{ + UPNodes: map[string]factory.UPNodeConfigInterface{ + "gNB": baseGNB(), + "UPF1": baseUPF(), + }, + Links: []*factory.UPLink{ + { + A: "gNB", + B: "UPF1", + }, + }, + } +} + +func TestUserplaneInformationValidation(t *testing.T) { testcase := []struct { - Name string - UPNodeConfig factory.UPNodeConfigInterface - Valid bool + Name string + Upi *factory.UserPlaneInformation + Valid bool }{ { - Name: "Valid gNB", - UPNodeConfig: baseGNB(), - Valid: true, + Name: "Valid userPlaneInformation", + Upi: baseUPI(), + Valid: true, }, { - Name: "Valid UPF", - UPNodeConfig: baseUPF(), - Valid: true, + Name: "gNB with wrong type", + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["gNB"].(*factory.GNBConfig).Type = "xxx" + return config + }(), + Valid: false, }, { Name: "UPF with wrong type", - UPNodeConfig: func() *factory.UPFConfig { - config := baseUPF() - config.UPNodeConfig.Type = "xxx" + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).Type = "xxx" return config }(), Valid: false, }, { Name: "UPF with wrong NodeID", - UPNodeConfig: func() *factory.UPFConfig { - config := baseUPF() - config.NodeID = "foobar" + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).NodeID = "127.0.0.1/24" return config }(), Valid: false, }, { Name: "UPF with nil sNssaiUpfInfos", - UPNodeConfig: func() *factory.UPFConfig { - config := baseUPF() - config.SNssaiInfos = nil + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).SNssaiInfos = nil return config }(), Valid: false, }, { Name: "UPF with empty sNssaiUpfInfos", - UPNodeConfig: func() *factory.UPFConfig { - config := baseUPF() - config.SNssaiInfos = []*factory.SnssaiUpfInfoItem{} + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).SNssaiInfos = []*factory.SnssaiUpfInfoItem{} return config }(), Valid: false, }, { - Name: "UPF with nil interfaces", - UPNodeConfig: func() *factory.UPFConfig { - config := baseUPF() - config.Interfaces = nil + Name: "UPF with invalid pool cidr", + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).SNssaiInfos[0].DnnUpfInfoList[0].Pools = []*factory.UEIPPool{ + { + Cidr: "10.60.0.0", + }, + } return config }(), Valid: false, }, { - Name: "UPF with empty interfaces", - UPNodeConfig: func() *factory.UPFConfig { - config := baseUPF() - config.Interfaces = []*factory.Interface{} + Name: "UPF with overlapping dynamic pools in DnnUpfInfoItem.Pools", + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).SNssaiInfos[0].DnnUpfInfoList = []*factory.DnnUpfInfoItem{ + { + Dnn: "internet", + Pools: []*factory.UEIPPool{ + { + Cidr: "10.60.0.0/16", + }, + { + Cidr: "10.60.10.0/16", + }, + }, + }, + } return config }(), Valid: false, }, { - Name: "UPF with overlapping pools in DnnUpfInfoItem.Pools", - UPNodeConfig: func() *factory.UPFConfig { - config := baseUPF() - config.SNssaiInfos[0].DnnUpfInfoList = []*factory.DnnUpfInfoItem{ + Name: "UPF with overlapping static pools in DnnUpfInfoItem.Pools", + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).SNssaiInfos[0].DnnUpfInfoList = []*factory.DnnUpfInfoItem{ { Dnn: "internet", Pools: []*factory.UEIPPool{ + { + Cidr: "10.80.0.0/16", + }, + }, + StaticPools: []*factory.UEIPPool{ { Cidr: "10.60.0.0/16", }, @@ -148,9 +189,9 @@ func TestUPNodeConfigInterfaceValidation(t *testing.T) { }, { Name: "UPF with overlapping pools in DnnUpfInfoItem.Pools and DnnUpfInfoItem.StaticPools", - UPNodeConfig: func() *factory.UPFConfig { - config := baseUPF() - config.SNssaiInfos[0].DnnUpfInfoList = []*factory.DnnUpfInfoItem{ + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).SNssaiInfos[0].DnnUpfInfoList = []*factory.DnnUpfInfoItem{ { Dnn: "internet", Pools: []*factory.UEIPPool{ @@ -170,61 +211,105 @@ func TestUPNodeConfigInterfaceValidation(t *testing.T) { Valid: false, }, { - Name: "UPF without N3 interface", - UPNodeConfig: func() *factory.UPFConfig { - config := baseUPF() - config.Interfaces = []*factory.Interface{} + Name: "UPF with nil interfaces", + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).Interfaces = nil return config }(), Valid: false, }, - } - - for _, tc := range testcase { - t.Run(tc.Name, func(t *testing.T) { - ok, err := tc.UPNodeConfig.Validate() - require.Equal(t, tc.Valid, ok) - require.Nil(t, err) - }) - } -} - -func TestUserplaneInformationValidation(t *testing.T) { - testcase := []struct { - Name string - Upi *factory.UserPlaneInformation - Valid bool - }{ { - Name: "Valid userPlaneInformation", - Upi: &factory.UserPlaneInformation{ - UPNodes: map[string]factory.UPNodeConfigInterface{ - "gNB": baseGNB(), - "UPF1": baseUPF(), - }, - Links: []*factory.UPLink{ + Name: "UPF with empty interfaces", + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).Interfaces = []*factory.Interface{} + return config + }(), + Valid: false, + }, + { + Name: "UPF with invalid interface", + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).Interfaces = []*factory.Interface{ { - A: "gNB", - B: "UPF1", + InterfaceType: "N3", + Endpoints: []string{ + "127.0.0.8", + }, }, - }, - }, + { + InterfaceType: "N4", + Endpoints: []string{ + "127.0.0.89", + }, + }, + } + return config + }(), + Valid: false, + }, + { + Name: "UPF with only N9 interface", + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).Interfaces = []*factory.Interface{ + { + InterfaceType: "N9", + Endpoints: []string{ + "127.0.0.8", + }, + NetworkInstances: []string{ + "internet", + }, + }, + } + return config + }(), Valid: true, }, + { + Name: "UPF with two N3 interfaces", + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.UPNodes["UPF1"].(*factory.UPFConfig).Interfaces = []*factory.Interface{ + { + InterfaceType: "N3", + Endpoints: []string{ + "127.0.0.8", + }, + NetworkInstances: []string{ + "internet", + }, + }, + { + InterfaceType: "N3", + Endpoints: []string{ + "127.0.0.88", + }, + NetworkInstances: []string{ + "internet", + }, + }, + } + return config + }(), + Valid: false, + }, { Name: "Link with non-existing node", - Upi: &factory.UserPlaneInformation{ - UPNodes: map[string]factory.UPNodeConfigInterface{ - "gNB": baseGNB(), - "UPF1": baseUPF(), - }, - Links: []*factory.UPLink{ + Upi: func() *factory.UserPlaneInformation { + config := baseUPI() + config.Links = []*factory.UPLink{ { A: "gNB", B: "UPF", }, - }, - }, + } + config.UPNodes["UPF1"].(*factory.UPFConfig).Interfaces = []*factory.Interface{} + return config + }(), Valid: false, }, } @@ -233,7 +318,12 @@ func TestUserplaneInformationValidation(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { ok, err := tc.Upi.Validate() require.Equal(t, tc.Valid, ok) - require.Nil(t, err) + if !ok { + require.Error(t, err) + fmt.Println(err) + } else { + require.Nil(t, err) + } }) } } @@ -303,6 +393,11 @@ func TestSnssaiUpfInfoItem(t *testing.T) { }, DnnInfos: []*factory.DnnUpfInfoItem{ { + Pools: []*factory.UEIPPool{ + { + Cidr: "10.60.0.0/16", + }, + }, Dnn: "internet", }, }, @@ -314,6 +409,11 @@ func TestSnssaiUpfInfoItem(t *testing.T) { }, DnnInfos: []*factory.DnnUpfInfoItem{ { + Pools: []*factory.UEIPPool{ + { + Cidr: "10.60.0.0/16", + }, + }, Dnn: "internet2", }, }, @@ -328,6 +428,7 @@ func TestSnssaiUpfInfoItem(t *testing.T) { } ok, err := snssaiInfoItem.Validate() + fmt.Println("Error: ", err) require.True(t, ok) require.Nil(t, err) })