diff --git a/pfcpiface/bess.go b/pfcpiface/bess.go index 05f3d98e0..80c8ccda0 100644 --- a/pfcpiface/bess.go +++ b/pfcpiface/bess.go @@ -711,45 +711,56 @@ func (b *bess) addPDR(ctx context.Context, done chan<- bool, p pdr) { break } - f := &pb.WildcardMatchCommandAddArg{ - Gate: uint64(p.needDecap), - Priority: int64(math.MaxUint32 - p.precedence), - Values: []*pb.FieldData{ - intEnc(uint64(p.srcIface)), /* src_iface */ - intEnc(uint64(p.tunnelIP4Dst)), /* tunnel_ipv4_dst */ - intEnc(uint64(p.tunnelTEID)), /* enb_teid */ - intEnc(uint64(p.appFilter.srcIP)), /* ueaddr ip*/ - intEnc(uint64(p.appFilter.dstIP)), /* inet ip */ - intEnc(uint64(p.appFilter.srcPort)), /* ue port */ - intEnc(uint64(p.appFilter.dstPort)), /* inet port */ - intEnc(uint64(p.appFilter.proto)), /* proto id */ - }, - Masks: []*pb.FieldData{ - intEnc(uint64(p.srcIfaceMask)), /* src_iface-mask */ - intEnc(uint64(p.tunnelIP4DstMask)), /* tunnel_ipv4_dst-mask */ - intEnc(uint64(p.tunnelTEIDMask)), /* enb_teid-mask */ - intEnc(uint64(p.appFilter.srcIPMask)), /* ueaddr ip-mask */ - intEnc(uint64(p.appFilter.dstIPMask)), /* inet ip-mask */ - intEnc(uint64(p.appFilter.srcPortMask)), /* ue port-mask */ - intEnc(uint64(p.appFilter.dstPortMask)), /* inet port-mask */ - intEnc(uint64(p.appFilter.protoMask)), /* proto id-mask */ - }, - Valuesv: []*pb.FieldData{ - intEnc(uint64(p.pdrID)), /* pdr-id */ - intEnc(p.fseID), /* fseid */ - intEnc(uint64(p.ctrID)), /* ctr_id */ - intEnc(uint64(qerID)), /* qer_id */ - intEnc(uint64(p.farID)), /* far_id */ - }, - } - - any, err = anypb.New(f) + // Translate port ranges into ternary rule(s) and insert them one-by-one. + portRules, err := CreatePortRangeCartesianProduct(p.appFilter.srcPortRange, p.appFilter.dstPortRange) if err != nil { - log.Println("Error marshalling the rule", f, err) + log.Errorln(err) return } - b.processPDR(ctx, any, upfMsgTypeAdd) + log.Tracef("PDR rules %+v", portRules) + + for _, r := range portRules { + f := &pb.WildcardMatchCommandAddArg{ + Gate: uint64(p.needDecap), + Priority: int64(math.MaxUint32 - p.precedence), + Values: []*pb.FieldData{ + intEnc(uint64(p.srcIface)), /* src_iface */ + intEnc(uint64(p.tunnelIP4Dst)), /* tunnel_ipv4_dst */ + intEnc(uint64(p.tunnelTEID)), /* enb_teid */ + intEnc(uint64(p.appFilter.srcIP)), /* ueaddr ip*/ + intEnc(uint64(p.appFilter.dstIP)), /* inet ip */ + intEnc(uint64(r.srcPort)), /* ue port */ + intEnc(uint64(r.dstPort)), /* inet port */ + intEnc(uint64(p.appFilter.proto)), /* proto id */ + }, + Masks: []*pb.FieldData{ + intEnc(uint64(p.srcIfaceMask)), /* src_iface-mask */ + intEnc(uint64(p.tunnelIP4DstMask)), /* tunnel_ipv4_dst-mask */ + intEnc(uint64(p.tunnelTEIDMask)), /* enb_teid-mask */ + intEnc(uint64(p.appFilter.srcIPMask)), /* ueaddr ip-mask */ + intEnc(uint64(p.appFilter.dstIPMask)), /* inet ip-mask */ + intEnc(uint64(r.srcMask)), /* ue port-mask */ + intEnc(uint64(r.dstMask)), /* inet port-mask */ + intEnc(uint64(p.appFilter.protoMask)), /* proto id-mask */ + }, + Valuesv: []*pb.FieldData{ + intEnc(uint64(p.pdrID)), /* pdr-id */ + intEnc(p.fseID), /* fseid */ + intEnc(uint64(p.ctrID)), /* ctr_id */ + intEnc(uint64(qerID)), /* qer_id */ + intEnc(uint64(p.farID)), /* far_id */ + }, + } + + any, err = anypb.New(f) + if err != nil { + log.Println("Error marshalling the rule", f, err) + return + } + + b.processPDR(ctx, any, upfMsgTypeAdd) + } done <- true }() } @@ -761,36 +772,45 @@ func (b *bess) delPDR(ctx context.Context, done chan<- bool, p pdr) { err error ) - f := &pb.WildcardMatchCommandDeleteArg{ - Values: []*pb.FieldData{ - intEnc(uint64(p.srcIface)), /* src_iface */ - intEnc(uint64(p.tunnelIP4Dst)), /* tunnel_ipv4_dst */ - intEnc(uint64(p.tunnelTEID)), /* enb_teid */ - intEnc(uint64(p.appFilter.srcIP)), /* ueaddr ip*/ - intEnc(uint64(p.appFilter.dstIP)), /* inet ip */ - intEnc(uint64(p.appFilter.srcPort)), /* ue port */ - intEnc(uint64(p.appFilter.dstPort)), /* inet port */ - intEnc(uint64(p.appFilter.proto)), /* proto id */ - }, - Masks: []*pb.FieldData{ - intEnc(uint64(p.srcIfaceMask)), /* src_iface-mask */ - intEnc(uint64(p.tunnelIP4DstMask)), /* tunnel_ipv4_dst-mask */ - intEnc(uint64(p.tunnelTEIDMask)), /* enb_teid-mask */ - intEnc(uint64(p.appFilter.srcIPMask)), /* ueaddr ip-mask */ - intEnc(uint64(p.appFilter.dstIPMask)), /* inet ip-mask */ - intEnc(uint64(p.appFilter.srcPortMask)), /* ue port-mask */ - intEnc(uint64(p.appFilter.dstPortMask)), /* inet port-mask */ - intEnc(uint64(p.appFilter.protoMask)), /* proto id-mask */ - }, - } - - any, err = anypb.New(f) + // Translate port ranges into ternary rule(s) and insert them one-by-one. + portRules, err := CreatePortRangeCartesianProduct(p.appFilter.srcPortRange, p.appFilter.dstPortRange) if err != nil { - log.Errorln("Error marshalling the rule", f, err) + log.Errorln(err) return } - b.processPDR(ctx, any, upfMsgTypeDel) + for _, r := range portRules { + f := &pb.WildcardMatchCommandDeleteArg{ + Values: []*pb.FieldData{ + intEnc(uint64(p.srcIface)), /* src_iface */ + intEnc(uint64(p.tunnelIP4Dst)), /* tunnel_ipv4_dst */ + intEnc(uint64(p.tunnelTEID)), /* enb_teid */ + intEnc(uint64(p.appFilter.srcIP)), /* ueaddr ip*/ + intEnc(uint64(p.appFilter.dstIP)), /* inet ip */ + intEnc(uint64(r.srcPort)), /* ue port */ + intEnc(uint64(r.dstPort)), /* inet port */ + intEnc(uint64(p.appFilter.proto)), /* proto id */ + }, + Masks: []*pb.FieldData{ + intEnc(uint64(p.srcIfaceMask)), /* src_iface-mask */ + intEnc(uint64(p.tunnelIP4DstMask)), /* tunnel_ipv4_dst-mask */ + intEnc(uint64(p.tunnelTEIDMask)), /* enb_teid-mask */ + intEnc(uint64(p.appFilter.srcIPMask)), /* ueaddr ip-mask */ + intEnc(uint64(p.appFilter.dstIPMask)), /* inet ip-mask */ + intEnc(uint64(r.srcMask)), /* ue port-mask */ + intEnc(uint64(r.dstMask)), /* inet port-mask */ + intEnc(uint64(p.appFilter.protoMask)), /* proto id-mask */ + }, + } + + any, err = anypb.New(f) + if err != nil { + log.Errorln("Error marshalling the rule", f, err) + return + } + + b.processPDR(ctx, any, upfMsgTypeDel) + } done <- true }() } diff --git a/pfcpiface/p4rt_translator.go b/pfcpiface/p4rt_translator.go index bc776e633..ed1898e93 100644 --- a/pfcpiface/p4rt_translator.go +++ b/pfcpiface/p4rt_translator.go @@ -518,15 +518,15 @@ func (t *P4rtTranslator) BuildApplicationsTableEntry(pdr pdr, internalAppID uint var ( appIP, appIPMask uint32 = 0, 0 - appPort uint16 = 0 + appPort portRange ) if pdr.srcIface == access { appIP, appIPMask = pdr.appFilter.dstIP, pdr.appFilter.dstIPMask - appPort = pdr.appFilter.dstPort + appPort = pdr.appFilter.dstPortRange } else if pdr.srcIface == core { appIP, appIPMask = pdr.appFilter.srcIP, pdr.appFilter.srcIPMask - appPort = pdr.appFilter.srcPort + appPort = pdr.appFilter.srcPortRange } appProto, appProtoMask := pdr.appFilter.proto, pdr.appFilter.protoMask @@ -538,8 +538,8 @@ func (t *P4rtTranslator) BuildApplicationsTableEntry(pdr pdr, internalAppID uint } } - if appPort != 0 { - if err := t.withRangeMatchField(entry, FieldAppL4Port, appPort, appPort); err != nil { + if !appPort.isWildcardMatch() { + if err := t.withRangeMatchField(entry, FieldAppL4Port, appPort.low, appPort.high); err != nil { return nil, err } } diff --git a/pfcpiface/parse-pdr.go b/pfcpiface/parse-pdr.go index b915932b1..f33a2cd84 100644 --- a/pfcpiface/parse-pdr.go +++ b/pfcpiface/parse-pdr.go @@ -1,29 +1,271 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 Intel Corporation +// Copyright 2022 Open Networking Foundation package main import ( "errors" "fmt" + "math" "net" log "github.com/sirupsen/logrus" "github.com/wmnsk/go-pfcp/ie" ) +// portRange encapsulates a L4 port range as seen in PDRs. A zero value portRange represents +// a wildcard match, but use of the dedicated new*PortRange() functions is encouraged. +type portRange struct { + low uint16 + high uint16 +} + +// newWildcardPortRange returns a portRange that matches on every possible port, i.e., implements +// no filtering. +func newWildcardPortRange() portRange { + return portRange{ + low: 0, + high: math.MaxUint16, + } +} + +// newExactMatchPortRange returns a portRange that matches on exactly the given port. +func newExactMatchPortRange(port uint16) portRange { + return portRange{ + low: port, + high: port, + } +} + +// newRangeMatchPortRange returns a portRange that matches on the given range [low, high]. +// low must be smaller than high. Creating exact and wildcard matches with this function is +// possible, but use of the dedicated functions is encouraged. +func newRangeMatchPortRange(low, high uint16) portRange { + if low > high { + return portRange{} + } + + return portRange{ + low: low, + high: high, + } +} + +func (pr portRange) String() string { + return fmt.Sprintf("{%v-%v}", pr.low, pr.high) +} + +// Width returns the number of ports covered by this portRange. +func (pr portRange) Width() uint16 { + // Need to handle the zero value. + if pr.isWildcardMatch() { + return math.MaxUint16 + } else { + return pr.high - pr.low + 1 + } +} + +func (pr portRange) isWildcardMatch() bool { + return pr.low == 0 && pr.high == math.MaxUint16 || + pr.low == 0 && pr.high == 0 +} + +func (pr portRange) isExactMatch() bool { + return pr.low == pr.high && pr.high != 0 +} + +func (pr portRange) isRangeMatch() bool { + return !pr.isExactMatch() && !pr.isWildcardMatch() +} + +// Returns portRange as an exact match, without checking if it is one. isExactMatch() must be true +// before calling asExactMatchUnchecked. +func (pr portRange) asExactMatchUnchecked() portRangeTernaryRule { + return portRangeTernaryRule{port: pr.low, mask: math.MaxUint16} +} + +// Return portRange as a trivial, single value and mask, ternary match. Will fail if conversion is +// not possible. +func (pr portRange) asTrivialTernaryMatch() (portRangeTernaryRule, error) { + if pr.isWildcardMatch() { + return portRangeTernaryRule{0, 0}, nil + } else if pr.isExactMatch() { + return pr.asExactMatchUnchecked(), nil + } + + return portRangeTernaryRule{}, ErrInvalidArgumentWithReason("asTrivialTernaryMatch", pr, "not trivially convertible") +} + +type RangeConversionStrategy int + +const ( + Exact RangeConversionStrategy = iota + Ternary +) + +// Returns portRange as a list of ternary matches that cover the same range. +func (pr portRange) asComplexTernaryMatches(strategy RangeConversionStrategy) ([]portRangeTernaryRule, error) { + rules := make([]portRangeTernaryRule, 0) + + // Fast path for exact and wildcard matches which are trivial. + if pr.isExactMatch() { + rules = append(rules, pr.asExactMatchUnchecked()) + return rules, nil + } + + if pr.isWildcardMatch() { + rules = append(rules, portRangeTernaryRule{0, 0}) + return rules, nil + } + + if strategy == Exact { + if pr.Width() > 100 { + return nil, ErrInvalidArgumentWithReason("asComplexTernaryMatches", pr, + "port range too wide for exact match strategy") + } + + for port := int(pr.low); port <= int(pr.high); port++ { + rules = append(rules, portRangeTernaryRule{uint16(port), math.MaxUint16}) + } + } else if strategy == Ternary { + // Adapted from https://stackoverflow.com/a/66959276 + const limit = math.MaxUint16 + maxPort := func(port, mask uint16) uint16 { + xid := limit - mask + nid := port & mask + return nid + xid + } + + portMask := func(port, end uint16) uint16 { + bit := uint16(1) + mask := uint16(limit) + testMask := uint16(limit) + netPort := port & limit + maximumPort := maxPort(netPort, limit) + + for netPort > 0 && maximumPort < end { + netPort = port & testMask + if netPort < port { + break + } + maximumPort = maxPort(netPort, testMask) + if maximumPort <= end { + mask = testMask + } + testMask -= bit + bit <<= 1 + } + return mask + } + + port := uint32(pr.low) // Promote to higher bit width for greater-equals check. + for port <= uint32(pr.high) { + mask := portMask(uint16(port), pr.high) + rules = append(rules, portRangeTernaryRule{uint16(port), mask}) + port = uint32(maxPort(uint16(port), mask)) + 1 + } + } else { + return nil, ErrInvalidArgument("asComplexTernaryMatches", strategy) + } + + return rules, nil +} + +type portRangeTernaryRule struct { + port, mask uint16 +} + +func (pf portRangeTernaryRule) String() string { + return fmt.Sprintf("{0b%b & 0b%b}", pf.port, pf.mask) +} + +type portRangeTernaryCartesianProduct struct { + srcPort, srcMask uint16 + dstPort, dstMask uint16 +} + +// CreatePortRangeCartesianProduct converts two port ranges into a list of ternary +// rules covering the same range. +func CreatePortRangeCartesianProduct(src, dst portRange) ([]portRangeTernaryCartesianProduct, error) { + // A single range rule can result in multiple ternary ones. To cover the same range of packets, + // we need to create the Cartesian product of src and dst rules. For now, we only allow one true + // range match to keep the complexity in check. + if src.isRangeMatch() && dst.isRangeMatch() { + return nil, ErrInvalidArgumentWithReason("CreatePortRangeCartesianProduct", + src, "src and dst ports cannot both be a range match") + } + + rules := make([]portRangeTernaryCartesianProduct, 0) + + if src.isRangeMatch() { + srcTernaryRules, err := src.asComplexTernaryMatches(Exact) + if err != nil { + return nil, err + } + + dstTernary, err := dst.asTrivialTernaryMatch() + if err != nil { + return nil, err + } + + for _, r := range srcTernaryRules { + p := portRangeTernaryCartesianProduct{ + srcPort: r.port, srcMask: r.mask, + dstPort: dstTernary.port, dstMask: dstTernary.mask, + } + rules = append(rules, p) + } + } else if dst.isRangeMatch() { + dstTernaryRules, err := dst.asComplexTernaryMatches(Exact) + if err != nil { + return nil, err + } + + srcTernary, err := src.asTrivialTernaryMatch() + if err != nil { + return nil, err + } + + for _, r := range dstTernaryRules { + p := portRangeTernaryCartesianProduct{ + srcPort: srcTernary.port, srcMask: srcTernary.mask, + dstPort: r.port, dstMask: r.mask, + } + rules = append(rules, p) + } + } else { + // Neither is range. Only one rule needed. + srcTernary, err := src.asTrivialTernaryMatch() + if err != nil { + return nil, err + } + + dstTernary, err := dst.asTrivialTernaryMatch() + if err != nil { + return nil, err + } + + p := portRangeTernaryCartesianProduct{ + dstPort: dstTernary.port, dstMask: dstTernary.mask, + srcPort: srcTernary.port, srcMask: srcTernary.mask, + } + rules = append(rules, p) + } + + return rules, nil +} + type applicationFilter struct { - srcIP uint32 - dstIP uint32 - srcPort uint16 - dstPort uint16 - proto uint8 - - srcIPMask uint32 - dstIPMask uint32 - srcPortMask uint16 - dstPortMask uint16 - protoMask uint8 + srcIP uint32 + dstIP uint32 + srcPortRange portRange + dstPortRange portRange + proto uint8 + + srcIPMask uint32 + dstIPMask uint32 + protoMask uint8 } type pdr struct { @@ -58,9 +300,9 @@ func needAllocIP(ueIPaddr *ie.UEIPAddressFields) bool { } func (af applicationFilter) String() string { - return fmt.Sprintf("ApplicationFilter(srcIP=%v/%x, dstIP=%v/%x, proto=%v/%x, srcPort=%v/%x, dstPort=%v/%x)", + return fmt.Sprintf("ApplicationFilter(srcIP=%v/%x, dstIP=%v/%x, proto=%v/%x, srcPort=%v, dstPort=%v)", af.srcIP, af.srcIPMask, af.dstIP, af.dstIPMask, af.proto, - af.protoMask, af.srcPort, af.srcPortMask, af.dstPort, af.dstPortMask) + af.protoMask, af.srcPortRange, af.dstPortRange) } func (p pdr) String() string { @@ -74,8 +316,8 @@ func (p pdr) String() string { func (p pdr) IsAppFilterEmpty() bool { return p.appFilter.proto == 0 && - ((p.IsUplink() && p.appFilter.dstIP == 0 && p.appFilter.dstPort == 0) || - (p.IsDownlink() && p.appFilter.srcIP == 0 && p.appFilter.srcPort == 0)) + ((p.IsUplink() && p.appFilter.dstIP == 0 && p.appFilter.dstPortRange.isWildcardMatch()) || + (p.IsDownlink() && p.appFilter.srcIP == 0 && p.appFilter.srcPortRange.isWildcardMatch())) } func (p pdr) IsUplink() bool { @@ -200,16 +442,8 @@ func (p *pdr) parsePDI(seid uint64, pdiIEs []*ie.IE, appPFDs map[string]appPFD, p.appFilter.dstIPMask = ipMask2int(ipf.dst.IPNet.Mask) p.appFilter.srcIP = ip2int(ipf.src.IPNet.IP) p.appFilter.srcIPMask = ipMask2int(ipf.src.IPNet.Mask) - - if ipf.dst.Port > 0 { - p.appFilter.dstPort = ipf.dst.Port - p.appFilter.dstPortMask = 0xffff - } - - if ipf.src.Port > 0 { - p.appFilter.srcPort = ipf.src.Port - p.appFilter.srcPortMask = 0xffff - } + p.appFilter.dstPortRange = ipf.dst.ports + p.appFilter.srcPortRange = ipf.src.ports break } @@ -246,35 +480,26 @@ func (p *pdr) parsePDI(seid uint64, pdiIEs []*ie.IE, appPFDs map[string]appPFD, p.appFilter.dstIPMask = ipMask2int(ipf.dst.IPNet.Mask) p.appFilter.srcIP = ip2int(ipf.src.IPNet.IP) p.appFilter.srcIPMask = ipMask2int(ipf.src.IPNet.Mask) - - if ipf.src.Port > 0 { - p.appFilter.srcPort, p.appFilter.srcPortMask = ipf.src.Port, 0xffff - } - - if ipf.dst.Port > 0 { - p.appFilter.dstPort, p.appFilter.dstPortMask = ipf.dst.Port, 0xffff - } + p.appFilter.dstPortRange = ipf.dst.ports + p.appFilter.srcPortRange = ipf.src.ports // FIXME: temporary workaround for SDF Filter, // remove once we meet spec compliance - p.appFilter.srcPort = p.appFilter.dstPort - p.appFilter.dstPort, p.appFilter.dstPortMask = 0, 0 // reset UE Port + p.appFilter.srcPortRange = p.appFilter.dstPortRange + p.appFilter.dstPortRange = newWildcardPortRange() } else if p.srcIface == access { p.appFilter.srcIP = ip2int(ipf.dst.IPNet.IP) p.appFilter.srcIPMask = ipMask2int(ipf.dst.IPNet.Mask) p.appFilter.dstIP = ip2int(ipf.src.IPNet.IP) p.appFilter.dstIPMask = ipMask2int(ipf.src.IPNet.Mask) - if ipf.dst.Port > 0 { - p.appFilter.srcPort, p.appFilter.srcPortMask = ipf.dst.Port, 0xffff - } - if ipf.src.Port > 0 { - p.appFilter.dstPort, p.appFilter.dstPortMask = ipf.src.Port, 0xffff - } + // Ports are flipped for access PDRs + p.appFilter.dstPortRange = ipf.src.ports + p.appFilter.srcPortRange = ipf.dst.ports // FIXME: temporary workaround for SDF Filter, // remove once we meet spec compliance - p.appFilter.dstPort = p.appFilter.srcPort - p.appFilter.srcPort, p.appFilter.srcPortMask = 0, 0 // reset UE Port + p.appFilter.dstPortRange = p.appFilter.srcPortRange + p.appFilter.srcPortRange = newWildcardPortRange() } } } diff --git a/pfcpiface/parse-pdr_test.go b/pfcpiface/parse-pdr_test.go new file mode 100644 index 000000000..c05bdaf52 --- /dev/null +++ b/pfcpiface/parse-pdr_test.go @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Open Networking Foundation + +package main + +import ( + "math" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_CreatePortRangeCartesianProduct(t *testing.T) { + type args struct { + src portRange + dst portRange + } + + tests := []struct { + name string + args args + want []portRangeTernaryCartesianProduct + wantErr bool + }{ + {name: "exact ranges", + args: args{src: newExactMatchPortRange(5000), dst: newExactMatchPortRange(80)}, + want: []portRangeTernaryCartesianProduct{{ + srcPort: 5000, + srcMask: math.MaxUint16, + dstPort: 80, + dstMask: math.MaxUint16, + }}, + wantErr: false}, + {name: "wildcard dst range", + args: args{src: newExactMatchPortRange(10), dst: newWildcardPortRange()}, + want: []portRangeTernaryCartesianProduct{{ + srcPort: 10, + srcMask: math.MaxUint16, + dstPort: 0, + dstMask: 0, + }}, + wantErr: false}, + {name: "true range src range", + args: args{src: newRangeMatchPortRange(1, 3), dst: newExactMatchPortRange(80)}, + want: []portRangeTernaryCartesianProduct{ + { + srcPort: 0x1, + srcMask: 0xffff, + dstPort: 80, + dstMask: math.MaxUint16, + }, + { + srcPort: 0x2, + srcMask: 0xffff, + dstPort: 80, + dstMask: math.MaxUint16, + }, + { + srcPort: 0x3, + srcMask: 0xffff, + dstPort: 80, + dstMask: math.MaxUint16, + }}, + wantErr: false}, + {name: "invalid double range", + args: args{src: newRangeMatchPortRange(10, 20), dst: newRangeMatchPortRange(80, 85)}, + want: nil, + wantErr: true}, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + got, err := CreatePortRangeCartesianProduct(tt.args.src, tt.args.dst) + if (err != nil) != tt.wantErr { + t.Errorf("CreatePortRangeCartesianProduct() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CreatePortRangeCartesianProduct() got = %v, want %v", got, tt.want) + } + }, + ) + } +} + +func Test_defaultPortRange(t *testing.T) { + t.Run("default constructed is wildcard", func(t *testing.T) { + assert.True(t, portRange{}.isWildcardMatch(), "default portRange is wildcard") + }) +} + +func Test_newWildcardPortRange(t *testing.T) { + tests := []struct { + name string + want portRange + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + if got := newWildcardPortRange(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newWildcardPortRange() = %v, want %v", got, tt.want) + } + }, + ) + } +} + +func Test_portRange_String(t *testing.T) { + tests := []struct { + name string + pr portRange + want string + }{ + // TODO: Add test cases. + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + if got := tt.pr.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }, + ) + } +} + +func Test_portRange_isExactMatch(t *testing.T) { + tests := []struct { + name string + pr portRange + want bool + }{ + // TODO: Add test cases. + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + if got := tt.pr.isExactMatch(); got != tt.want { + t.Errorf("isExactMatch() = %v, want %v", got, tt.want) + } + }, + ) + } +} + +func Test_portRange_isRangeMatch(t *testing.T) { + tests := []struct { + name string + pr portRange + want bool + }{ + // TODO: Add test cases. + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + if got := tt.pr.isRangeMatch(); got != tt.want { + t.Errorf("isRangeMatch() = %v, want %v", got, tt.want) + } + }, + ) + } +} + +func Test_portRange_isWildcardMatch(t *testing.T) { + tests := []struct { + name string + pr portRange + want bool + }{ + // TODO: Add test cases. + {name: "foo", pr: portRange{ + low: 0, + high: 0, + }, want: true}, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + if got := tt.pr.isWildcardMatch(); got != tt.want { + t.Errorf("isWildcardMatch() = %v, want %v", got, tt.want) + } + }, + ) + } +} + +// Perform a ternary match of value against rules. +func matchesTernary(value uint16, rules []portRangeTernaryRule) bool { + for _, r := range rules { + if (value & r.mask) == r.port { + return true + } + } + + return false +} + +func Test_portRange_asComplexTernaryMatches(t *testing.T) { + tests := []struct { + name string + pr portRange + strategy RangeConversionStrategy + wantErr bool + want []portRangeTernaryRule + }{ + {name: "Exact match port range", + pr: portRange{ + low: 8888, + high: 8888, + }, + want: []portRangeTernaryRule{ + {port: 8888, mask: 0xffff}, + }, + wantErr: false}, + {name: "wildcard port range", + pr: portRange{ + low: 0, + high: math.MaxUint16, + }, + want: []portRangeTernaryRule{ + {port: 0, mask: 0}, + }, + wantErr: false}, + {name: "Simplest port range", + pr: portRange{ + low: 0b0, // 0 + high: 0b1, // 1 + }, + //want: []portRangeTernaryRule{ + // {port: 0b0, mask: 0xfffe}, + //}, + wantErr: false}, + {name: "Simplest port range2", + pr: portRange{ + low: 0b01, // 1 + high: 0b10, // 2 + }, + //want: []portRangeTernaryRule{ + // {port: 0b01, mask: 0xffff}, + // {port: 0b10, mask: 0xffff}, + //}, + wantErr: false}, + {name: "Trivial ternary port range", + pr: portRange{ + low: 0x0100, // 256 + high: 0x01ff, // 511 + }, + strategy: Ternary, + //want: []portRangeTernaryRule{ + // {port: 0x0100, mask: 0xff00}, + //}, + wantErr: false}, + {name: "one to three range", + pr: portRange{ + low: 0b01, // 1 + high: 0b11, // 3 + }, + //want: []portRangeTernaryRule{ + // {port: 0b01, mask: 0xffff}, + // {port: 0b10, mask: 0xfffe}, + //}, + wantErr: false}, + {name: "True port range", + pr: portRange{ + low: 0b00010, // 2 + high: 0b11101, // 29 + }, + wantErr: false}, + {name: "Worst case port range", + pr: portRange{ + low: 1, + high: 65534, + }, + strategy: Ternary, + wantErr: false}, + {name: "low port filter", + pr: portRange{ + low: 0, + high: 1023, + }, + strategy: Ternary, + wantErr: false}, + {name: "some small app filter", + pr: portRange{ + low: 8080, + high: 8084, + }, + wantErr: false}, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + got, err := tt.pr.asComplexTernaryMatches(tt.strategy) + if (err != nil) != tt.wantErr { + t.Errorf("asComplexTernaryMatches() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.want != nil && !reflect.DeepEqual(got, tt.want) { + t.Errorf("asComplexTernaryMatches() got = %v, want %v", got, tt.want) + } + // Do exhaustive test over entire value range. + for port := 0; port <= math.MaxUint16; port++ { + expectMatch := port >= int(tt.pr.low) && port <= int(tt.pr.high) + if matchesTernary(uint16(port), got) != expectMatch { + mod := " " + if !expectMatch { + mod = " not " + } + t.Errorf("Expected port %v to%vmatch against rules %v from range %+v", port, mod, got, tt.pr) + } + } + }, + ) + } +} + +func Test_portRange_asTrivialTernaryMatch(t *testing.T) { + tests := []struct { + name string + pr portRange + wantPort uint16 + wantMask uint16 + wantErr bool + }{ + {name: "Wildcard range", pr: portRange{ + low: 0, + high: 0, + }, wantPort: 0, wantMask: 0, wantErr: false}, + {name: "Exact match range", pr: portRange{ + low: 100, + high: 100, + }, wantPort: 100, wantMask: 0xffff, wantErr: false}, + {name: "True range match fail", pr: portRange{ + low: 100, + high: 200, + }, wantPort: 0, wantMask: 0, wantErr: true}, + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + got, err := tt.pr.asTrivialTernaryMatch() + if (err != nil) != tt.wantErr { + t.Errorf("asTrivialTernaryMatch() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if got.port != tt.wantPort { + t.Errorf("asTrivialTernaryMatch() got = %v, want %v", got.port, tt.wantPort) + } + if got.mask != tt.wantMask { + t.Errorf("asTrivialTernaryMatch() got = %v, want %v", got.mask, tt.wantMask) + } + }, + ) + } +} + +func Test_portRange_Width(t *testing.T) { + tests := []struct { + name string + pr portRange + want uint16 + }{ + {name: "wildcard", pr: newWildcardPortRange(), want: math.MaxUint16}, + {name: "zero value", pr: portRange{}, want: math.MaxUint16}, + {name: "exact match", pr: newExactMatchPortRange(100), want: 1}, + {name: "range match", pr: newRangeMatchPortRange(10, 12), want: 3}, + {name: "range single match", pr: newRangeMatchPortRange(1000, 1000), want: 1}, + } + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + if got := tt.pr.Width(); got != tt.want { + t.Errorf("Width() = %v, want %v", got, tt.want) + } + }, + ) + } +} diff --git a/pfcpiface/parse-sdf.go b/pfcpiface/parse-sdf.go index 6362295bc..6cadef8aa 100644 --- a/pfcpiface/parse-sdf.go +++ b/pfcpiface/parse-sdf.go @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 Intel Corporation +// Copyright 2022 Open Networking Foundation package main @@ -21,7 +22,7 @@ var errBadFilterDesc = errors.New("unsupported Filter Description format") type endpoint struct { IPNet *net.IPNet - Port uint16 + ports portRange } func (ep *endpoint) parseNet(ipnet string) error { @@ -66,12 +67,11 @@ func (ep *endpoint) parsePort(port string) error { return err } - // TODO: support port ranges - if low != high { - return ErrInvalidArgumentWithReason("port", port, "port ranges are not supported yet") + if low > high { + return ErrInvalidArgumentWithReason("port", port, "invalid port range") } - ep.Port = uint16(low) + ep.ports = newRangeMatchPortRange(uint16(low), uint16(high)) return nil } @@ -82,10 +82,17 @@ type ipFilterRule struct { src, dst endpoint } +func newIpFilterRule() *ipFilterRule { + return &ipFilterRule{ + src: endpoint{ports: newWildcardPortRange()}, + dst: endpoint{ports: newWildcardPortRange()}, + } +} + func (ipf *ipFilterRule) String() string { return fmt.Sprintf("FlowDescription{action=%v, direction=%v, proto=%v, "+ "srcIP=%v, srcPort=%v, dstIP=%v, dstPort=%v}", - ipf.action, ipf.direction, ipf.proto, ipf.src.IPNet, ipf.src.Port, ipf.dst.IPNet, ipf.dst.Port) + ipf.action, ipf.direction, ipf.proto, ipf.src.IPNet, ipf.src.ports, ipf.dst.IPNet, ipf.dst.ports) } // "permit out ip from any to assigned" @@ -106,7 +113,7 @@ func parseFlowDesc(flowDesc, ueIP string) (*ipFilterRule, error) { }) parseLog.Debug("Parsing flow description") - ipf := &ipFilterRule{} + ipf := newIpFilterRule() fields := strings.Fields(flowDesc) diff --git a/pfcpiface/up4.go b/pfcpiface/up4.go index 383b61f92..a687da53e 100644 --- a/pfcpiface/up4.go +++ b/pfcpiface/up4.go @@ -50,7 +50,7 @@ const ( type application struct { appIP uint32 - appL4Port uint16 + appL4Port portRange appProto uint8 } @@ -573,7 +573,7 @@ func (up4 *UP4) allocateInternalApplicationID(app application) (uint8, error) { func (up4 *UP4) releaseInternalApplicationID(appFilter applicationFilter) { app := application{ appIP: appFilter.srcIP, - appL4Port: appFilter.srcPort, + appL4Port: appFilter.srcPortRange, appProto: appFilter.proto, } @@ -589,12 +589,12 @@ func (up4 *UP4) getOrAllocateInternalApplicationID(pdr pdr) (uint8, error) { if pdr.IsUplink() { app = application{ appIP: pdr.appFilter.dstIP, - appL4Port: pdr.appFilter.dstPort, + appL4Port: pdr.appFilter.dstPortRange, } } else if pdr.IsDownlink() { app = application{ appIP: pdr.appFilter.srcIP, - appL4Port: pdr.appFilter.srcPort, + appL4Port: pdr.appFilter.srcPortRange, } }