diff --git a/dataplane/standalone/saiserver/BUILD b/dataplane/standalone/saiserver/BUILD index 44003ffe..55c9e044 100644 --- a/dataplane/standalone/saiserver/BUILD +++ b/dataplane/standalone/saiserver/BUILD @@ -29,6 +29,7 @@ go_library( go_test( name = "saiserver_test", srcs = [ + "acl_test.go", "ports_test.go", "routing_test.go", "switch_test.go", diff --git a/dataplane/standalone/saiserver/acl.go b/dataplane/standalone/saiserver/acl.go index 407c701f..fed06f73 100644 --- a/dataplane/standalone/saiserver/acl.go +++ b/dataplane/standalone/saiserver/acl.go @@ -151,14 +151,14 @@ func (a *acl) CreateAclEntry(ctx context.Context, req *saipb.CreateAclEntryReque }, }, } - switch { - case req.GetFieldDstIp() != nil: + if req.GetFieldDstIp() != nil { aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_ADDR_DST}}, Bytes: req.GetFieldDstIp().GetDataIp(), Masks: req.GetFieldDstIp().GetMaskIp(), }) - case req.GetFieldInPort() != nil: + } + if req.GetFieldInPort() != nil { nid, ok := a.dataplane.PortIDToNID(fmt.Sprint(req.FieldInPort.GetDataOid())) if !ok { return nil, fmt.Errorf("unknown port with id: %v", req.FieldInPort.GetDataOid()) @@ -169,6 +169,99 @@ func (a *acl) CreateAclEntry(ctx context.Context, req *saipb.CreateAclEntryReque Masks: binary.BigEndian.AppendUint64(nil, math.MaxUint64), }) } + if req.GetFieldAclIpType() != nil { // Use the EtherType header to match against specific protocols. + fieldMask := &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_TYPE}}, + Bytes: binary.BigEndian.AppendUint16(nil, 0x0000), + Masks: binary.BigEndian.AppendUint16(nil, 0xFFFF), + } + switch t := req.GetFieldAclIpType().GetDataIpType(); t { + case saipb.AclIpType_ACL_IP_TYPE_ANY: + fieldMask.Masks = binary.BigEndian.AppendUint16(nil, 0x0000) // Match any EtherType. + case saipb.AclIpType_ACL_IP_TYPE_IPV4ANY: + fieldMask.Bytes = binary.BigEndian.AppendUint16(nil, 0x0800) // Match IPv4. + case saipb.AclIpType_ACL_IP_TYPE_IPV6ANY: + fieldMask.Bytes = binary.BigEndian.AppendUint16(nil, 0x86DD) // Match IPv6. + case saipb.AclIpType_ACL_IP_TYPE_ARP: + fieldMask.Bytes = binary.BigEndian.AppendUint16(nil, 0x0806) // Match ARP. + default: + return nil, status.Errorf(codes.InvalidArgument, "unspporrted ACL_IP_TYPE: %v", t) + } + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, fieldMask) + } + if req.GetFieldDscp() != nil { + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_QOS}}, + Bytes: []byte{byte(req.GetFieldDscp().GetDataUint())}, + Masks: []byte{byte(req.GetFieldDscp().GetMaskUint())}, + }) + } + if req.GetFieldDstIpv6Word3() != nil { // Word3 is supposed to match the 127:96 bits of the IP, assume the caller is masking this correctly put the whole IP in the table. + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_ADDR_DST}}, + Bytes: req.GetFieldDstIpv6Word3().GetDataIp(), + Masks: req.GetFieldDstIpv6Word3().GetMaskIp(), + }) + } + if req.GetFieldDstIpv6Word2() != nil { // Word2 is supposed to match the 95:64 bits of the IP, assume the caller is masking this correctly put the whole IP in the table. + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_ADDR_DST}}, + Bytes: req.GetFieldDstIpv6Word2().GetDataIp(), + Masks: req.GetFieldDstIpv6Word2().GetMaskIp(), + }) + } + if req.GetFieldDstMac() != nil { + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_DST}}, + Bytes: req.GetFieldDstMac().GetDataMac(), + Masks: req.GetFieldDstMac().GetMaskMac(), + }) + } + if req.GetFieldEtherType() != nil { + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_TYPE}}, + Bytes: binary.BigEndian.AppendUint16(nil, uint16(req.GetFieldEtherType().GetDataUint())), + Masks: binary.BigEndian.AppendUint16(nil, uint16(req.GetFieldEtherType().GetMaskUint())), + }) + } + if req.GetFieldIcmpv6Type() != nil { + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ICMP_TYPE}}, + Bytes: []byte{byte(req.GetFieldIcmpv6Type().GetDataUint())}, + Masks: []byte{byte(req.GetFieldIcmpv6Type().GetMaskUint())}, + }) + } + if req.GetFieldIpProtocol() != nil { + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_PROTO}}, + Bytes: []byte{byte(req.GetFieldIpProtocol().GetDataUint())}, + Masks: []byte{byte(req.GetFieldIpProtocol().GetMaskUint())}, + }) + } + if req.GetFieldL4DstPort() != nil { + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_L4_PORT_DST}}, + Bytes: binary.BigEndian.AppendUint16(nil, uint16(req.GetFieldL4DstPort().GetDataUint())), + Masks: binary.BigEndian.AppendUint16(nil, uint16(req.GetFieldL4DstPort().GetMaskUint())), + }) + } + if req.GetFieldSrcMac() != nil { + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_SRC}}, + Bytes: req.GetFieldSrcMac().GetDataMac(), + Masks: req.GetFieldSrcMac().GetMaskMac(), + }) + } + if req.GetFieldTtl() != nil { + aReq.EntryDesc.GetFlow().Fields = append(aReq.EntryDesc.GetFlow().Fields, &fwdpb.PacketFieldMaskedBytes{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_HOP}}, + Bytes: []byte{byte(req.GetFieldTtl().GetDataUint())}, + Masks: []byte{byte(req.GetFieldTtl().GetMaskUint())}, + }) + } + if len(aReq.EntryDesc.GetFlow().Fields) == 0 { + return nil, status.Error(codes.InvalidArgument, "either no fields or not unsupports fields in entry req") + } switch { case req.ActionSetVrf != nil: aReq.Actions = append(aReq.Actions, diff --git a/dataplane/standalone/saiserver/acl_test.go b/dataplane/standalone/saiserver/acl_test.go new file mode 100644 index 00000000..35871c7d --- /dev/null +++ b/dataplane/standalone/saiserver/acl_test.go @@ -0,0 +1,234 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package saiserver + +import ( + "context" + "encoding/binary" + "math" + "net/netip" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/openconfig/gnmi/errdiff" + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + + saipb "github.com/openconfig/lemming/dataplane/standalone/proto" + "github.com/openconfig/lemming/dataplane/standalone/saiserver/attrmgr" + fwdpb "github.com/openconfig/lemming/proto/forwarding" +) + +func TestCreateAclEntry(t *testing.T) { + tests := []struct { + desc string + req *saipb.CreateAclEntryRequest + wantErr string + want *fwdpb.TableEntryAddRequest + }{{ + desc: "table not member of a group", + wantErr: "FailedPrecondition", + req: &saipb.CreateAclEntryRequest{}, + }, { + desc: "no fields", + wantErr: "InvalidArgument", + req: &saipb.CreateAclEntryRequest{ + TableId: proto.Uint64(1), + }, + }, { + desc: "all fields", + req: &saipb.CreateAclEntryRequest{ + TableId: proto.Uint64(1), + FieldDstIp: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataIp{ + DataIp: []byte{127, 0, 0, 1}, + }, + Mask: &saipb.AclFieldData_MaskIp{ + MaskIp: []byte{255, 255, 255, 0}, + }, + }, + FieldInPort: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataOid{DataOid: 1}, + }, + FieldDscp: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataUint{DataUint: 10}, + Mask: &saipb.AclFieldData_MaskUint{MaskUint: 0xff}, + }, + FieldDstIpv6Word3: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataIp{ + DataIp: netip.MustParseAddr("cafe:beef::").AsSlice(), + }, + Mask: &saipb.AclFieldData_MaskIp{ + MaskIp: netip.MustParseAddr("ffff:ffff::").AsSlice(), + }, + }, + FieldDstIpv6Word2: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataIp{ + DataIp: netip.MustParseAddr("0:0:cafe:beef::").AsSlice(), + }, + Mask: &saipb.AclFieldData_MaskIp{ + MaskIp: netip.MustParseAddr("0:0:ffff:ffff::").AsSlice(), + }, + }, + FieldDstMac: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataMac{ + DataMac: []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}, + }, + Mask: &saipb.AclFieldData_MaskMac{ + MaskMac: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + }, + FieldEtherType: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataUint{DataUint: 0x800}, + Mask: &saipb.AclFieldData_MaskUint{MaskUint: 0xfff}, + }, + FieldIcmpv6Type: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataUint{DataUint: 0x01}, + Mask: &saipb.AclFieldData_MaskUint{MaskUint: 0xff}, + }, + FieldIpProtocol: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataUint{DataUint: 0x04}, + Mask: &saipb.AclFieldData_MaskUint{MaskUint: 0xFF}, + }, + FieldL4DstPort: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataUint{DataUint: 22}, + Mask: &saipb.AclFieldData_MaskUint{MaskUint: 0xffff}, + }, + FieldSrcMac: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataMac{ + DataMac: []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}, + }, + Mask: &saipb.AclFieldData_MaskMac{ + MaskMac: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + }, + FieldTtl: &saipb.AclFieldData{ + Data: &saipb.AclFieldData_DataUint{DataUint: 0x01}, + Mask: &saipb.AclFieldData_MaskUint{MaskUint: 0xff}, + }, + }, + want: &fwdpb.TableEntryAddRequest{ + ContextId: &fwdpb.ContextId{Id: "foo"}, + TableId: &fwdpb.TableId{ObjectId: &fwdpb.ObjectId{Id: "1"}}, + EntryDesc: &fwdpb.EntryDesc{ + Entry: &fwdpb.EntryDesc_Flow{ + Flow: &fwdpb.FlowEntryDesc{ + Id: 1, + Fields: []*fwdpb.PacketFieldMaskedBytes{{ + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_ADDR_DST}}, + Bytes: []byte{127, 0, 0, 1}, + Masks: []byte{255, 255, 255, 0}, + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_PACKET_PORT_INPUT}}, + Bytes: binary.BigEndian.AppendUint64(nil, 1), + Masks: binary.BigEndian.AppendUint64(nil, math.MaxUint64), + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_QOS}}, + Bytes: []byte{10}, + Masks: []byte{255}, + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_ADDR_DST}}, + Bytes: netip.MustParseAddr("cafe:beef::").AsSlice(), + Masks: netip.MustParseAddr("ffff:ffff::").AsSlice(), + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_ADDR_DST}}, + Bytes: netip.MustParseAddr("0:0:cafe:beef::").AsSlice(), + Masks: netip.MustParseAddr("0:0:ffff:ffff::").AsSlice(), + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_DST}}, + Bytes: []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}, + Masks: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_TYPE}}, + Bytes: []byte{0x8, 0x0}, + Masks: []byte{0x0f, 0xff}, + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ICMP_TYPE}}, + Bytes: []byte{0x01}, + Masks: []byte{0xff}, + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_PROTO}}, + Bytes: []byte{0x04}, + Masks: []byte{0xff}, + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_L4_PORT_DST}}, + Bytes: []byte{0, 22}, + Masks: []byte{0xff, 0xff}, + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_ETHER_MAC_SRC}}, + Bytes: []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}, + Masks: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, { + FieldId: &fwdpb.PacketFieldId{Field: &fwdpb.PacketField{FieldNum: fwdpb.PacketFieldNum_PACKET_FIELD_NUM_IP_HOP}}, + Bytes: []byte{0x01}, + Masks: []byte{0xff}, + }}, + }, + }, + }, + }, + }} + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + dplane := &fakeACLDataplaneAPI{} + c, a, stopFn := newTestACL(t, dplane) + a.tableToLocation[1] = tableLocation{ + groupID: "1", + bank: 0, + } + defer stopFn() + _, gotErr := c.CreateAclEntry(context.TODO(), tt.req) + if diff := errdiff.Check(gotErr, tt.wantErr); diff != "" { + t.Fatalf("CreateAclEntry() unexpected err: %s", diff) + } + if gotErr != nil { + return + } + if d := cmp.Diff(dplane.gotEntryAddReqs[0], tt.want, protocmp.Transform()); d != "" { + t.Errorf("CreateAclEntry() failed: diff(-got,+want)\n:%s", d) + } + }) + } +} + +func newTestACL(t testing.TB, api aclDataplaneAPI) (saipb.AclClient, *acl, func()) { + var a *acl + conn, _, stopFn := newTestServer(t, func(mgr *attrmgr.AttrMgr, srv *grpc.Server) { + a = newACL(mgr, api, srv) + }) + return saipb.NewAclClient(conn), a, stopFn +} + +type fakeACLDataplaneAPI struct { + gotEntryAddReqs []*fwdpb.TableEntryAddRequest +} + +func (f fakeACLDataplaneAPI) ID() string { + return "foo" +} + +func (f fakeACLDataplaneAPI) TableCreate(context.Context, *fwdpb.TableCreateRequest) (*fwdpb.TableCreateReply, error) { + return nil, nil +} + +func (f *fakeACLDataplaneAPI) TableEntryAdd(_ context.Context, req *fwdpb.TableEntryAddRequest) (*fwdpb.TableEntryAddReply, error) { + f.gotEntryAddReqs = append(f.gotEntryAddReqs, req) + return nil, nil +} + +func (f fakeACLDataplaneAPI) PortIDToNID(string) (uint64, bool) { + return 1, true +}