diff --git a/api/agent/v1alpha2/catalog_types.go b/api/agent/v1alpha2/catalog_types.go index d56e69d0..66fb82a4 100644 --- a/api/agent/v1alpha2/catalog_types.go +++ b/api/agent/v1alpha2/catalog_types.go @@ -48,8 +48,8 @@ type CatalogSpec struct { LoopbackWorkaroundVLANs map[string]uint16 `json:"loopbackWorkaroundVLANs,omitempty"` // ExternalIDs stores external name -> ID, unique per switch ExternalIDs map[string]uint16 `json:"externalIDs,omitempty"` - // ExternalPeeringPrefixIDs stores external peering prefix -> ID, unique per switch - ExternalPeeringPrefixIDs map[string]uint32 `json:"externalPeeringPrefixIDs,omitempty"` + // SubnetIDs stores subnet -> ID, unique per switch + SubnetIDs map[string]uint32 `json:"subnetIDs,omitempty"` } // CatalogStatus defines the observed state of Catalog diff --git a/api/agent/v1alpha2/zz_generated.deepcopy.go b/api/agent/v1alpha2/zz_generated.deepcopy.go index 6d37f80c..bb5e1dc8 100644 --- a/api/agent/v1alpha2/zz_generated.deepcopy.go +++ b/api/agent/v1alpha2/zz_generated.deepcopy.go @@ -446,8 +446,8 @@ func (in *CatalogSpec) DeepCopyInto(out *CatalogSpec) { (*out)[key] = val } } - if in.ExternalPeeringPrefixIDs != nil { - in, out := &in.ExternalPeeringPrefixIDs, &out.ExternalPeeringPrefixIDs + if in.SubnetIDs != nil { + in, out := &in.SubnetIDs, &out.SubnetIDs *out = make(map[string]uint32, len(*in)) for key, val := range *in { (*out)[key] = val diff --git a/api/vpc/v1alpha2/vpc_types.go b/api/vpc/v1alpha2/vpc_types.go index d4fa4834..5be03e49 100644 --- a/api/vpc/v1alpha2/vpc_types.go +++ b/api/vpc/v1alpha2/vpc_types.go @@ -19,6 +19,7 @@ package v1alpha2 import ( "context" "net" + "strconv" "github.com/pkg/errors" wiringapi "go.githedgehog.com/fabric/api/wiring/v1alpha2" @@ -40,10 +41,18 @@ import ( type VPCSpec struct { // Subnets is the list of VPC subnets to configure Subnets map[string]*VPCSubnet `json:"subnets,omitempty"` - // IPv4Namespace is the name of the IPv4Namespace this VPC belongs to + // IPv4Namespace is the name of the IPv4Namespace this VPC belongs to (if not specified, "default" is used) IPv4Namespace string `json:"ipv4Namespace,omitempty"` - // VLANNamespace is the name of the VLANNamespace this VPC belongs to + // VLANNamespace is the name of the VLANNamespace this VPC belongs to (if not specified, "default" is used) VLANNamespace string `json:"vlanNamespace,omitempty"` + // DefaultIsolated sets default bahivour for isolated mode for the subnets (disabled by default) + DefaultIsolated bool `json:"defaultIsolated,omitempty"` + // DefaultRestricted sets default bahivour for restricted mode for the subnets (disabled by default) + DefaultRestricted bool `json:"defaultRestricted,omitempty"` + // Permit defines a list of the access policies between the subnets within the VPC - each policy is a list of subnets that have access to each other. + // It's applied on top of the subnet isolation flag and if subnet isn't isolated it's not required to have it in a permit list while if vpc is marked + // as isolated it's required to have it in a permit list to have access to other subnets. + Permit [][]string `json:"permit,omitempty"` } // VPCSubnet defines the VPC subnet configuration @@ -54,6 +63,10 @@ type VPCSubnet struct { DHCP VPCDHCP `json:"dhcp,omitempty"` // VLAN is the VLAN ID for the subnet, should belong to the VLANNamespace and be unique within the namespace VLAN string `json:"vlan,omitempty"` + // Isolated is the flag to enable isolated mode for the subnet which means no access to and from the other subnets within the VPC + Isolated *bool `json:"isolated,omitempty"` + // Restricted is the flag to enable restricted mode for the subnet which means no access between hosts within the subnet itself + Restricted *bool `json:"restricted,omitempty"` } // VPCDHCP defines the on-demand DHCP configuration for the subnet @@ -110,6 +123,22 @@ func init() { SchemeBuilder.Register(&VPC{}, &VPCList{}) } +func (vpc *VPCSpec) IsSubnetIsolated(subnetName string) bool { + if subnet, ok := vpc.Subnets[subnetName]; ok && subnet.Isolated != nil { + return *subnet.Isolated + } + + return vpc.DefaultIsolated +} + +func (vpc *VPCSpec) IsSubnetRestricted(subnetName string) bool { + if subnet, ok := vpc.Subnets[subnetName]; ok && subnet.Restricted != nil { + return *subnet.Restricted + } + + return vpc.DefaultRestricted +} + func (vpc *VPC) Default() { if vpc.Spec.IPv4Namespace == "" { vpc.Spec.IPv4Namespace = "default" @@ -146,6 +175,7 @@ func (vpc *VPC) Validate(ctx context.Context, client validation.Client, reserved } subnets := []*net.IPNet{} + vlans := map[string]bool{} for subnetName, subnetCfg := range vpc.Spec.Subnets { if subnetCfg.Subnet == "" { return nil, errors.Errorf("subnet %s: missing subnet", subnetName) @@ -165,6 +195,7 @@ func (vpc *VPC) Validate(ctx context.Context, client validation.Client, reserved if subnetCfg.VLAN == "" { return nil, errors.Errorf("subnet %s: vlan is required", subnetName) } + vlans[subnetCfg.VLAN] = true subnets = append(subnets, ipNet) @@ -224,13 +255,35 @@ func (vpc *VPC) Validate(ctx context.Context, client validation.Client, reserved } } + if len(vlans) != len(vpc.Spec.Subnets) { + return nil, errors.Errorf("duplicate subnet VLANs") + } + if err := iputil.VerifyNoOverlap(subnets); err != nil { return nil, errors.Wrapf(err, "failed to verify no overlap subnets") } + for permitIdx, permit := range vpc.Spec.Permit { + if len(permit) < 2 { + return nil, errors.Errorf("each permit policy must have at least 2 subnets in it") + } + + subnets := map[string]bool{} + for _, subnetName := range permit { + if _, ok := vpc.Spec.Subnets[subnetName]; !ok { + return nil, errors.Errorf("permit policy #%d: subnet %s not found", permitIdx, subnetName) + } + + subnets[subnetName] = true + } + + if len(subnets) != len(permit) { + return nil, errors.Errorf("permit policy #%d: duplicate subnets", permitIdx) + } + } + if client != nil { - // TODO check VLANs - // TODO Can we rely on Validation webhook for croll VPC subnet? if not - main VPC subnet validation should happen in the VPC controller + // TODO Can we rely on Validation webhook for cross VPC subnet? if not - main VPC subnet validation should happen in the VPC controller ipNs := &IPv4Namespace{} err := client.Get(ctx, types.NamespacedName{Name: vpc.Spec.IPv4Namespace, Namespace: vpc.Namespace}, ipNs) @@ -241,6 +294,15 @@ func (vpc *VPC) Validate(ctx context.Context, client validation.Client, reserved return nil, errors.Wrapf(err, "failed to get IPv4Namespace %s", vpc.Spec.IPv4Namespace) // TODO replace with some internal error to not expose to the user } + vlanNs := &wiringapi.VLANNamespace{} + err = client.Get(ctx, types.NamespacedName{Name: vpc.Spec.VLANNamespace, Namespace: vpc.Namespace}, vlanNs) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, errors.Errorf("VLANNamespace %s not found", vpc.Spec.VLANNamespace) + } + return nil, errors.Wrapf(err, "failed to get VLANNamespace %s", vpc.Spec.VLANNamespace) // TODO replace with some internal error to not expose to the user + } + for subnetName, subnetCfg := range vpc.Spec.Subnets { _, vpcSubnet, err := net.ParseCIDR(subnetCfg.Subnet) if err != nil { @@ -263,6 +325,14 @@ func (vpc *VPC) Validate(ctx context.Context, client validation.Client, reserved if !ok { return nil, errors.Errorf("vpc subnet %s (%s) doesn't belong to the IPv4Namespace %s", subnetName, subnetCfg.Subnet, vpc.Spec.IPv4Namespace) } + + vlanRaw, err := strconv.ParseUint(subnetCfg.VLAN, 10, 16) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse subnet %s (%s) VLAN %s", subnetName, subnetCfg.Subnet, subnetCfg.VLAN) + } + if !vlanNs.Spec.Contains(uint16(vlanRaw)) { + return nil, errors.Errorf("vpc subnet %s (%s) vlan %s doesn't belong to the VLANNamespace %s", subnetName, subnetCfg.Subnet, subnetCfg.VLAN, vpc.Spec.VLANNamespace) + } } vpcs := &VPCList{} @@ -294,6 +364,31 @@ func (vpc *VPC) Validate(ctx context.Context, client validation.Client, reserved } } } + + vpcs = &VPCList{} + err = client.List(ctx, vpcs, map[string]string{ + LabelVLANNS: vpc.Spec.VLANNamespace, + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to list VPCs") // TODO replace with some internal error to not expose to the user + } + + for _, other := range vpcs.Items { + if other.Name == vpc.Name { + continue + } + if other.Spec.VLANNamespace != vpc.Spec.VLANNamespace { + continue + } + + for _, otherSubnet := range other.Spec.Subnets { + for _, subnet := range vpc.Spec.Subnets { + if subnet.VLAN == otherSubnet.VLAN { + return nil, errors.Errorf("vlan %s is already used by other VPC", subnet.VLAN) + } + } + } + } } return nil, nil diff --git a/api/vpc/v1alpha2/vpcpeering_types.go b/api/vpc/v1alpha2/vpcpeering_types.go index 54eee8b5..1e259d3b 100644 --- a/api/vpc/v1alpha2/vpcpeering_types.go +++ b/api/vpc/v1alpha2/vpcpeering_types.go @@ -90,17 +90,20 @@ func init() { func (s *VPCPeeringSpec) VPCs() (string, string, error) { vpcs := []string{} - for _, permit := range s.Permit { + for idx, permit := range s.Permit { + if len(permit) != 2 { + return "", "", errors.Errorf("each permit policy must have exactly 2 VPCs (idx %d)", idx) + } + for vpc := range permit { - if slices.Contains(vpcs, vpc) { - return "", "", errors.Errorf("vpc peering must have 2 unique VPCs") + if !slices.Contains(vpcs, vpc) { + vpcs = append(vpcs, vpc) } - vpcs = append(vpcs, vpc) } } if len(vpcs) != 2 { - return "", "", errors.Errorf("vpc peering must have exactly 2 VPCs") + return "", "", errors.Errorf("VPCPeering must have exactly 2 VPCs") } sort.Strings(vpcs) @@ -131,7 +134,7 @@ func (peering *VPCPeering) Validate(ctx context.Context, client validation.Clien return nil, errors.Errorf("vpc peering is not allowed") } - vpc1, vpc2, err := peering.Spec.VPCs() + vpc1Name, vpc2Name, err := peering.Spec.VPCs() if err != nil { return nil, err } @@ -142,21 +145,11 @@ func (peering *VPCPeering) Validate(ctx context.Context, client validation.Clien } } - // TODO remove when subnet filtering is supported - if len(peering.Spec.Permit) != 1 { - return nil, errors.Errorf("permit must have exactly 1 entry") - } - if len(peering.Spec.Permit[0][vpc1].Subnets) != 0 || len(peering.Spec.Permit[0][vpc2].Subnets) != 0 { - return nil, errors.Errorf("subnets are not supported yet") - } - - // TODO validate overlaps in subnets - if client != nil { other := &VPCPeeringList{} err := client.List(ctx, other, map[string]string{ - ListLabelVPC(vpc1): ListLabelValue, - ListLabelVPC(vpc2): ListLabelValue, + ListLabelVPC(vpc1Name): ListLabelValue, + ListLabelVPC(vpc2Name): ListLabelValue, }) if err != nil && !apierrors.IsNotFound(err) { return nil, errors.Wrapf(err, "failed to list VPC peerings") // TODO replace with some internal error to not expose to the user @@ -164,7 +157,7 @@ func (peering *VPCPeering) Validate(ctx context.Context, client validation.Clien ipv4Namespaces := []string{} vlanNamespaces := []string{} - for _, vpcName := range []string{vpc1, vpc2} { + for _, vpcName := range []string{vpc1Name, vpc2Name} { vpc := &VPC{} err := client.Get(ctx, types.NamespacedName{Name: vpcName, Namespace: peering.Namespace}, vpc) if err != nil { @@ -204,6 +197,43 @@ func (peering *VPCPeering) Validate(ctx context.Context, client validation.Clien return nil, errors.Wrapf(err, "failed to list switch groups") // TODO replace with some internal error to not expose to the user } } + + vpc1 := &VPC{} + err = client.Get(ctx, types.NamespacedName{Name: vpc1Name, Namespace: peering.Namespace}, vpc1) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, errors.Errorf("VPC %s not found", vpc1Name) + } + + return nil, errors.Wrapf(err, "failed to get VPC %s", vpc1Name) // TODO replace with some internal error to not expose to the user + } + + vpc2 := &VPC{} + err = client.Get(ctx, types.NamespacedName{Name: vpc2Name, Namespace: peering.Namespace}, vpc2) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, errors.Errorf("VPC %s not found", vpc2Name) + } + + return nil, errors.Wrapf(err, "failed to get VPC %s", vpc2Name) // TODO replace with some internal error to not expose to the user + } + + for _, permit := range peering.Spec.Permit { + for vpcName, vpcPeer := range permit { + vpc := vpc1 + if vpcName == vpc2Name { + vpc = vpc2 + } else if vpcName != vpc1Name { + return nil, errors.Errorf("unexpected VPC %s in permit", vpcName) + } + + for _, subnet := range vpcPeer.Subnets { + if vpc.Spec.Subnets == nil || vpc.Spec.Subnets[subnet] == nil { + return nil, errors.Errorf("subnet %s not found in VPC %s", subnet, vpcName) + } + } + } + } } return nil, nil diff --git a/api/vpc/v1alpha2/zz_generated.deepcopy.go b/api/vpc/v1alpha2/zz_generated.deepcopy.go index fd450861..475cb84b 100644 --- a/api/vpc/v1alpha2/zz_generated.deepcopy.go +++ b/api/vpc/v1alpha2/zz_generated.deepcopy.go @@ -814,6 +814,17 @@ func (in *VPCSpec) DeepCopyInto(out *VPCSpec) { (*out)[key] = outVal } } + if in.Permit != nil { + in, out := &in.Permit, &out.Permit + *out = make([][]string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCSpec. @@ -845,6 +856,16 @@ func (in *VPCStatus) DeepCopy() *VPCStatus { func (in *VPCSubnet) DeepCopyInto(out *VPCSubnet) { *out = *in in.DHCP.DeepCopyInto(&out.DHCP) + if in.Isolated != nil { + in, out := &in.Isolated, &out.Isolated + *out = new(bool) + **out = **in + } + if in.Restricted != nil { + in, out := &in.Restricted, &out.Restricted + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCSubnet. diff --git a/api/wiring/v1alpha2/vlannamespace_types.go b/api/wiring/v1alpha2/vlannamespace_types.go index 93c6567c..e865d414 100644 --- a/api/wiring/v1alpha2/vlannamespace_types.go +++ b/api/wiring/v1alpha2/vlannamespace_types.go @@ -70,6 +70,16 @@ func init() { SchemeBuilder.Register(&VLANNamespace{}, &VLANNamespaceList{}) } +func (ns *VLANNamespaceSpec) Contains(vlan uint16) bool { + for _, r := range ns.Ranges { + if vlan >= r.From && vlan <= r.To { + return true + } + } + + return false +} + func (ns *VLANNamespaceSpec) Labels() map[string]string { // TODO return map[string]string{} diff --git a/config/crd/bases/agent.githedgehog.com_agents.yaml b/config/crd/bases/agent.githedgehog.com_agents.yaml index a38bd8d8..b74db288 100644 --- a/config/crd/bases/agent.githedgehog.com_agents.yaml +++ b/config/crd/bases/agent.githedgehog.com_agents.yaml @@ -113,13 +113,6 @@ spec: description: ExternalIDs stores external name -> ID, unique per switch type: object - externalPeeringPrefixIDs: - additionalProperties: - format: int32 - type: integer - description: ExternalPeeringPrefixIDs stores external peering - prefix -> ID, unique per switch - type: object irbVLANs: additionalProperties: type: integer @@ -145,6 +138,12 @@ spec: description: PortChannelIDs stores Connection name -> PortChannel ID, unique per redundancy group (or switch) type: object + subnetIDs: + additionalProperties: + format: int32 + type: integer + description: SubnetIDs stores subnet -> ID, unique per switch + type: object vpcSubnetVNIs: additionalProperties: additionalProperties: @@ -1110,10 +1109,31 @@ spec: description: VPCSpec defines the desired state of VPC. At least one subnet is required. properties: + defaultIsolated: + description: DefaultIsolated sets default bahivour for isolated + mode for the subnets (disabled by default) + type: boolean + defaultRestricted: + description: DefaultRestricted sets default bahivour for restricted + mode for the subnets (disabled by default) + type: boolean ipv4Namespace: description: IPv4Namespace is the name of the IPv4Namespace - this VPC belongs to + this VPC belongs to (if not specified, "default" is used) type: string + permit: + description: Permit defines a list of the access policies between + the subnets within the VPC - each policy is a list of subnets + that have access to each other. It's applied on top of the + subnet isolation flag and if subnet isn't isolated it's not + required to have it in a permit list while if vpc is marked + as isolated it's required to have it in a permit list to have + access to other subnets. + items: + items: + type: string + type: array + type: array subnets: additionalProperties: description: VPCSubnet defines the VPC subnet configuration @@ -1143,6 +1163,16 @@ spec: specified, DHCP server will be disabled type: string type: object + isolated: + description: Isolated is the flag to enable isolated mode + for the subnet which means no access to and from the + other subnets within the VPC + type: boolean + restricted: + description: Restricted is the flag to enable restricted + mode for the subnet which means no access between hosts + within the subnet itself + type: boolean subnet: description: Subnet is the subnet CIDR block, such as "10.0.0.0/24", should belong to the IPv4Namespace and @@ -1158,7 +1188,7 @@ spec: type: object vlanNamespace: description: VLANNamespace is the name of the VLANNamespace - this VPC belongs to + this VPC belongs to (if not specified, "default" is used) type: string type: object type: object diff --git a/config/crd/bases/agent.githedgehog.com_catalogs.yaml b/config/crd/bases/agent.githedgehog.com_catalogs.yaml index 92d78048..042a0fcc 100644 --- a/config/crd/bases/agent.githedgehog.com_catalogs.yaml +++ b/config/crd/bases/agent.githedgehog.com_catalogs.yaml @@ -46,13 +46,6 @@ spec: type: integer description: ExternalIDs stores external name -> ID, unique per switch type: object - externalPeeringPrefixIDs: - additionalProperties: - format: int32 - type: integer - description: ExternalPeeringPrefixIDs stores external peering prefix - -> ID, unique per switch - type: object irbVLANs: additionalProperties: type: integer @@ -78,6 +71,12 @@ spec: description: PortChannelIDs stores Connection name -> PortChannel ID, unique per redundancy group (or switch) type: object + subnetIDs: + additionalProperties: + format: int32 + type: integer + description: SubnetIDs stores subnet -> ID, unique per switch + type: object vpcSubnetVNIs: additionalProperties: additionalProperties: diff --git a/config/crd/bases/vpc.githedgehog.com_vpcs.yaml b/config/crd/bases/vpc.githedgehog.com_vpcs.yaml index af24617d..1015da67 100644 --- a/config/crd/bases/vpc.githedgehog.com_vpcs.yaml +++ b/config/crd/bases/vpc.githedgehog.com_vpcs.yaml @@ -57,10 +57,30 @@ spec: spec: description: Spec is the desired state of the VPC properties: + defaultIsolated: + description: DefaultIsolated sets default bahivour for isolated mode + for the subnets (disabled by default) + type: boolean + defaultRestricted: + description: DefaultRestricted sets default bahivour for restricted + mode for the subnets (disabled by default) + type: boolean ipv4Namespace: description: IPv4Namespace is the name of the IPv4Namespace this VPC - belongs to + belongs to (if not specified, "default" is used) type: string + permit: + description: Permit defines a list of the access policies between + the subnets within the VPC - each policy is a list of subnets that + have access to each other. It's applied on top of the subnet isolation + flag and if subnet isn't isolated it's not required to have it in + a permit list while if vpc is marked as isolated it's required to + have it in a permit list to have access to other subnets. + items: + items: + type: string + type: array + type: array subnets: additionalProperties: description: VPCSubnet defines the VPC subnet configuration @@ -89,6 +109,16 @@ spec: DHCP server will be disabled type: string type: object + isolated: + description: Isolated is the flag to enable isolated mode for + the subnet which means no access to and from the other subnets + within the VPC + type: boolean + restricted: + description: Restricted is the flag to enable restricted mode + for the subnet which means no access between hosts within + the subnet itself + type: boolean subnet: description: Subnet is the subnet CIDR block, such as "10.0.0.0/24", should belong to the IPv4Namespace and be unique within the @@ -103,7 +133,7 @@ spec: type: object vlanNamespace: description: VLANNamespace is the name of the VLANNamespace this VPC - belongs to + belongs to (if not specified, "default" is used) type: string type: object status: diff --git a/docs/api.md b/docs/api.md index 2d332421..2f849c73 100644 --- a/docs/api.md +++ b/docs/api.md @@ -611,8 +611,11 @@ _Appears in:_ | Field | Description | | --- | --- | | `subnets` _object (keys:string, values:[VPCSubnet](#vpcsubnet))_ | Subnets is the list of VPC subnets to configure | -| `ipv4Namespace` _string_ | IPv4Namespace is the name of the IPv4Namespace this VPC belongs to | -| `vlanNamespace` _string_ | VLANNamespace is the name of the VLANNamespace this VPC belongs to | +| `ipv4Namespace` _string_ | IPv4Namespace is the name of the IPv4Namespace this VPC belongs to (if not specified, "default" is used) | +| `vlanNamespace` _string_ | VLANNamespace is the name of the VLANNamespace this VPC belongs to (if not specified, "default" is used) | +| `defaultIsolated` _boolean_ | DefaultIsolated sets default bahivour for isolated mode for the subnets (disabled by default) | +| `defaultRestricted` _boolean_ | DefaultRestricted sets default bahivour for restricted mode for the subnets (disabled by default) | +| `permit` _string array array_ | Permit defines a list of the access policies between the subnets within the VPC - each policy is a list of subnets that have access to each other. It's applied on top of the subnet isolation flag and if subnet isn't isolated it's not required to have it in a permit list while if vpc is marked as isolated it's required to have it in a permit list to have access to other subnets. | #### VPCStatus @@ -640,6 +643,8 @@ _Appears in:_ | `subnet` _string_ | Subnet is the subnet CIDR block, such as "10.0.0.0/24", should belong to the IPv4Namespace and be unique within the namespace | | `dhcp` _[VPCDHCP](#vpcdhcp)_ | DHCP is the on-demand DHCP configuration for the subnet | | `vlan` _string_ | VLAN is the VLAN ID for the subnet, should belong to the VLANNamespace and be unique within the namespace | +| `isolated` _boolean_ | Isolated is the flag to enable isolated mode for the subnet which means no access to and from the other subnets within the VPC | +| `restricted` _boolean_ | Restricted is the flag to enable restricted mode for the subnet which means no access between hosts within the subnet itself | diff --git a/hack/tools.mk b/hack/tools.mk index 05f281da..b3d7c519 100644 --- a/hack/tools.mk +++ b/hack/tools.mk @@ -110,7 +110,7 @@ envtest-k8s: envtest ## Download envtest assets if necessary. # TODO: Install specific version KUBEVIOUS_INSTALL_SCRIPT ?= "https://get.kubevious.io/cli.sh" .PHONY: kubevious -kubevious: $(KUBEVIOUS) ## Download kustomize locally if necessary. +kubevious: $(KUBEVIOUS) kustomize ## Download kustomize locally if necessary. $(KUBEVIOUS): $(LOCALBIN) test -s $(LOCALBIN)/kubevious || { curl -Ss $(KUBEVIOUS_INSTALL_SCRIPT) | bash -s -- $(LOCALBIN); } diff --git a/pkg/agent/dozer/bcm/plan.go b/pkg/agent/dozer/bcm/plan.go index 600d30bc..2e3dca38 100644 --- a/pkg/agent/dozer/bcm/plan.go +++ b/pkg/agent/dozer/bcm/plan.go @@ -1386,7 +1386,7 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error { return errors.Errorf("VPC %s subnet %s not found", vpcName, subnetName) } - err := planVPCSubnet(agent, spec, vpcName, subnetName, subnet) + err := planVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet) if err != nil { return errors.Wrapf(err, "failed to plan VPC %s subnet %s", vpcName, subnetName) } @@ -1473,7 +1473,7 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error { return errors.Errorf("VPC %s subnet %s not found", vpcName, subnetName) } - err := planVPCSubnet(agent, spec, vpcName, subnetName, subnet) + err := planVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet) if err != nil { return errors.Wrapf(err, "failed to plan VPC %s subnet %s for configuredSubnets", vpcName, subnetName) } @@ -1581,6 +1581,10 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error { Result: dozer.SpecRouteMapResultReject, } + if err := extendVPCFilteringACL(agent, spec, vpc1Name, vpc2Name, peeringName, vpc1, vpc2, peering); err != nil { + return errors.Wrapf(err, "failed to extend VPC filtering ACL for VPC peering %s", peeringName) + } + vrf1Name := vpcVrfName(vpc1Name) vrf2Name := vpcVrfName(vpc2Name) @@ -1665,10 +1669,39 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error { } } + // cleanup empty (only a single permit) ACLs for all VPC/subnets + for vpcName, vpc := range agent.Spec.VPCs { + for subnetName, subnet := range vpc.Subnets { + aclName := vpcFilteringAccessListName(vpcName, subnetName) + if acl, ok := spec.ACLs[aclName]; ok { + if len(acl.Entries) == 1 { + delete(spec.ACLs, aclName) + + // TODO dedup + vlanRaw, err := strconv.ParseUint(subnet.VLAN, 10, 16) + if err != nil { + return errors.Wrapf(err, "failed to parse subnet VLAN %s for VPC %s", subnet.VLAN, vpcName) + } + subnetIface := vlanName(uint16(vlanRaw)) + + if aclIface, ok := spec.ACLInterfaces[subnetIface]; ok { + if aclIface.Ingress != nil && *aclIface.Ingress == aclName { + aclIface.Ingress = nil + + if aclIface.Egress == nil { + delete(spec.ACLInterfaces, subnetIface) + } + } + } + } + } + } + } + return nil } -func planVPCSubnet(agent *agentapi.Agent, spec *dozer.Spec, vpcName, subnetName string, subnet *vpcapi.VPCSubnet) error { +func planVPCSubnet(agent *agentapi.Agent, spec *dozer.Spec, vpcName string, vpc vpcapi.VPCSpec, subnetName string, subnet *vpcapi.VPCSubnet) error { vrfName := vpcVrfName(vpcName) vlanRaw, err := strconv.ParseUint(subnet.VLAN, 10, 16) @@ -1694,6 +1727,16 @@ func planVPCSubnet(agent *agentapi.Agent, spec *dozer.Spec, vpcName, subnetName spec.VRFs[vrfName].Interfaces[subnetIface] = &dozer.SpecVRFInterface{} + vpcFilteringACL := vpcFilteringAccessListName(vpcName, subnetName) + spec.ACLInterfaces[subnetIface] = &dozer.SpecACLInterface{ + Ingress: stringPtr(vpcFilteringACL), + } + + spec.ACLs[vpcFilteringACL], err = buildVPCFilteringACL(agent, vpcName, vpc, subnetName, subnet) + if err != nil { + return errors.Wrapf(err, "failed to plan VPC filtering ACL for VPC %s subnet %s", vpcName, subnetName) + } + if agent.IsSpineLeaf() { spec.SuppressVLANNeighs[subnetIface] = &dozer.SpecSuppressVLANNeigh{} @@ -1734,6 +1777,161 @@ func planVPCSubnet(agent *agentapi.Agent, spec *dozer.Spec, vpcName, subnetName return nil } +func buildVPCFilteringACL(agent *agentapi.Agent, vpcName string, vpc vpcapi.VPCSpec, subnetName string, subnet *vpcapi.VPCSubnet) (*dozer.SpecACL, error) { + acl := &dozer.SpecACL{ + Entries: map[uint32]*dozer.SpecACLEntry{ + 65535: { + Action: dozer.SpecACLEntryActionAccept, + }, + }, + } + + if vpc.IsSubnetRestricted(subnetName) { + acl.Entries[1] = &dozer.SpecACLEntry{ + DestinationAddress: stringPtr(subnet.Subnet), + Action: dozer.SpecACLEntryActionDrop, + } + } + + denySubnets := map[string]bool{} + + for otherSubnetName, otherSubnet := range vpc.Subnets { + if otherSubnetName == subnetName { + continue + } + + if vpc.IsSubnetIsolated(otherSubnetName) { + denySubnets[otherSubnet.Subnet] = true + } + } + + for permitIdx, permitPolicy := range vpc.Permit { + if !slices.Contains(permitPolicy, subnetName) { + continue + } + + for _, otherSubnetName := range permitPolicy { + if otherSubnetName == subnetName { + continue + } + + if otherSubnet, ok := vpc.Subnets[otherSubnetName]; ok { + delete(denySubnets, otherSubnet.Subnet) + } else { + return nil, errors.Errorf("permit policy #%d: subnet %s not found in VPC %s", permitIdx, otherSubnetName, vpcName) + } + } + } + + for subnet := range denySubnets { + subnetID := agent.Spec.Catalog.SubnetIDs[subnet] + if subnetID == 0 { + return nil, errors.Errorf("no subnet id found for vpc %s subnet %s", vpcName, subnet) + } + if subnetID < 100 { + return nil, errors.Errorf("subnet id for vpc %s subnet %s is too small", vpcName, subnet) + } + if subnetID >= 65000 { + return nil, errors.Errorf("subnet id for vpc %s subnet %s is too large", vpcName, subnet) + } + + acl.Entries[subnetID] = &dozer.SpecACLEntry{ + DestinationAddress: stringPtr(subnet), + Action: dozer.SpecACLEntryActionDrop, + } + } + + return acl, nil +} + +func extendVPCFilteringACL(agent *agentapi.Agent, spec *dozer.Spec, vpc1Name, vpc2Name, vpcPeeringName string, vpc1, vpc2 vpcapi.VPCSpec, vpcPeering vpcapi.VPCPeeringSpec) error { + vpc1Deny := map[string]map[string]bool{} + vpc2Deny := map[string]map[string]bool{} + + for vpc1SubnetName := range vpc1.Subnets { + for vpc2SubnetName := range vpc2.Subnets { + if vpc1Deny[vpc1SubnetName] == nil { + vpc1Deny[vpc1SubnetName] = map[string]bool{} + } + if vpc2Deny[vpc2SubnetName] == nil { + vpc2Deny[vpc2SubnetName] = map[string]bool{} + } + + vpc1Deny[vpc1SubnetName][vpc2SubnetName] = true + vpc2Deny[vpc2SubnetName][vpc1SubnetName] = true + } + } + + for _, permitPolicy := range vpcPeering.Permit { + vpc1Subnets := permitPolicy[vpc1Name].Subnets + if len(vpc1Subnets) == 0 { + for subnetName := range vpc1.Subnets { + vpc1Subnets = append(vpc1Subnets, subnetName) + } + } + + vpc2Subnets := permitPolicy[vpc2Name].Subnets + if len(vpc2Subnets) == 0 { + for subnetName := range vpc2.Subnets { + vpc2Subnets = append(vpc2Subnets, subnetName) + } + } + + for _, vpc1SubnetName := range vpc1Subnets { + for _, vpc2SubnetName := range vpc2Subnets { + delete(vpc1Deny[vpc1SubnetName], vpc2SubnetName) + delete(vpc2Deny[vpc2SubnetName], vpc1SubnetName) + } + } + } + + if err := addVPCFilteringACLEntryiesForVPC(agent, spec, vpc1Name, vpc2Name, vpc1, vpc2, vpc1Deny); err != nil { + return errors.Wrapf(err, "failed to add VPC filtering ACL entries for VPC %s", vpc1Name) + } + if err := addVPCFilteringACLEntryiesForVPC(agent, spec, vpc2Name, vpc1Name, vpc2, vpc1, vpc2Deny); err != nil { + return errors.Wrapf(err, "failed to add VPC filtering ACL entries for VPC %s", vpc2Name) + } + + return nil +} + +func addVPCFilteringACLEntryiesForVPC(agent *agentapi.Agent, spec *dozer.Spec, vpc1Name, vpc2Name string, vpc1, vpc2 vpcapi.VPCSpec, vpc1Deny map[string]map[string]bool) error { + for vpc1SubnetName, vpc1SubnetDeny := range vpc1Deny { + for vpc2SubnetName, deny := range vpc1SubnetDeny { + if !deny { + continue + } + + vpc2Subnet, ok := vpc2.Subnets[vpc2SubnetName] + if !ok { + return errors.Errorf("VPC %s subnet %s not found", vpc2Name, vpc2SubnetName) + } + + subnetID := agent.Spec.Catalog.SubnetIDs[vpc2Subnet.Subnet] + // TODO dedup + if subnetID == 0 { + return errors.Errorf("no subnet id found for vpc %s subnet %s", vpc2Name, vpc2SubnetName) + } + if subnetID < 100 { + return errors.Errorf("subnet id for vpc %s subnet %s is too small", vpc2Name, vpc2SubnetName) + } + if subnetID >= 65000 { + return errors.Errorf("subnet id for vpc %s subnet %s is too large", vpc2Name, vpc2SubnetName) + } + + aclName := vpcFilteringAccessListName(vpc1Name, vpc1SubnetName) + if spec.ACLs[aclName] != nil { + spec.ACLs[aclName].Entries[subnetID] = &dozer.SpecACLEntry{ + DestinationAddress: stringPtr(vpc2Subnet.Subnet), + Action: dozer.SpecACLEntryActionDrop, + } + } + } + } + + return nil +} + func planExternalPeerings(agent *agentapi.Agent, spec *dozer.Spec) error { attachedVPCs := map[string]bool{} for _, attach := range agent.Spec.VPCAttachments { @@ -1784,7 +1982,7 @@ func planExternalPeerings(agent *agentapi.Agent, spec *dozer.Spec) error { if !attachedVPCs[vpcName] { prefixes := map[uint32]*dozer.SpecPrefixListEntry{} for _, prefix := range peering.Permit.External.Prefixes { - idx := agent.Spec.Catalog.ExternalPeeringPrefixIDs[prefix.Prefix] + idx := agent.Spec.Catalog.SubnetIDs[prefix.Prefix] if idx == 0 { return errors.Errorf("no external peering prefix id for prefix %s in peering %s", prefix.Prefix, name) } @@ -2081,6 +2279,10 @@ func ipnsSubnetsPrefixListName(ipns string) string { return fmt.Sprintf("ipns-subnets--%s", ipns) } +func vpcFilteringAccessListName(vpc string, subnet string) string { + return fmt.Sprintf("vpc-filtering--%s--%s", vpc, subnet) +} + func communityForVPC(agent *agentapi.Agent, vpc string) (string, error) { baseParts := strings.Split(agent.Spec.Config.BaseVPCCommunity, ":") if len(baseParts) != 2 { diff --git a/pkg/ctrl/agent/agent_ctrl.go b/pkg/ctrl/agent/agent_ctrl.go index 5e5b23e4..5c66eff0 100644 --- a/pkg/ctrl/agent/agent_ctrl.go +++ b/pkg/ctrl/agent/agent_ctrl.go @@ -524,14 +524,19 @@ func (r *AgentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl externalsReq[name] = true } - externalPeeringPrefixReq := map[string]bool{} + subnetsReq := map[string]bool{} + for _, vpc := range vpcs { + for _, subnet := range vpc.Subnets { + subnetsReq[subnet.Subnet] = true + } + } for _, peering := range externalPeerings { for _, prefix := range peering.Permit.External.Prefixes { - externalPeeringPrefixReq[prefix.Prefix] = true + subnetsReq[prefix.Prefix] = true } } - err = r.LibMngr.CatalogForSwitch(ctx, r.Client, cat, sw.Name, loWorkaroundLinks, loWorkaroundReqs, externalsReq, externalPeeringPrefixReq) + err = r.LibMngr.CatalogForSwitch(ctx, r.Client, cat, sw.Name, loWorkaroundLinks, loWorkaroundReqs, externalsReq, subnetsReq) if err != nil { return ctrl.Result{}, errors.Wrapf(err, "error getting switch catalog") } diff --git a/pkg/manager/librarian/librarian.go b/pkg/manager/librarian/librarian.go index e62a5e16..8d45e7e3 100644 --- a/pkg/manager/librarian/librarian.go +++ b/pkg/manager/librarian/librarian.go @@ -259,7 +259,7 @@ func (m *Manager) CatalogForRedundancyGroup(ctx context.Context, kube client.Cli return nil } -func (m *Manager) CatalogForSwitch(ctx context.Context, kube client.Client, ret *agentapi.CatalogSpec, swName string, loWorkaroundLinks []string, loWorkaroundReqs, externals, externalPeeringPrefixes map[string]bool) error { +func (m *Manager) CatalogForSwitch(ctx context.Context, kube client.Client, ret *agentapi.CatalogSpec, swName string, loWorkaroundLinks []string, loWorkaroundReqs, externals, subnets map[string]bool) error { m.mutex.Lock() defer m.mutex.Unlock() @@ -302,11 +302,11 @@ func (m *Manager) CatalogForSwitch(ctx context.Context, kube client.Client, ret { a := Allocator[uint32]{ - Values: NewNextFreeValueFromRanges([][2]uint32{{10, 4294967295}}, 1), + Values: NewNextFreeValueFromRanges([][2]uint32{{100, 64999}}, 1), } - cat.Spec.ExternalPeeringPrefixIDs, err = a.Allocate(cat.Spec.ExternalPeeringPrefixIDs, externalPeeringPrefixes) + cat.Spec.SubnetIDs, err = a.Allocate(cat.Spec.SubnetIDs, subnets) if err != nil { - return errors.Wrapf(err, "failed to allocate external peering prefix IDs for %s", key) + return errors.Wrapf(err, "failed to allocate subnet IDs for %s", key) } } @@ -329,9 +329,9 @@ func (m *Manager) CatalogForSwitch(ctx context.Context, kube client.Client, ret } } - ret.ExternalPeeringPrefixIDs = cat.Spec.ExternalPeeringPrefixIDs - for prefix := range externalPeeringPrefixes { - if _, exists := ret.ExternalPeeringPrefixIDs[prefix]; !exists { + ret.SubnetIDs = cat.Spec.SubnetIDs + for prefix := range subnets { + if _, exists := ret.SubnetIDs[prefix]; !exists { return errors.Errorf("failed to find external peering prefix ID for %s", prefix) } } diff --git a/pkg/webhook/vpcattachment/vpcattachment_webhook.go b/pkg/webhook/vpcattachment/vpcattachment_webhook.go index fd21a588..84d99c72 100644 --- a/pkg/webhook/vpcattachment/vpcattachment_webhook.go +++ b/pkg/webhook/vpcattachment/vpcattachment_webhook.go @@ -3,10 +3,8 @@ package vpcattachment import ( "context" - "github.com/pkg/errors" vpcapi "go.githedgehog.com/fabric/api/vpc/v1alpha2" "go.githedgehog.com/fabric/pkg/manager/validation" - "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -64,11 +62,11 @@ func (w *VPCAttachmentWebhook) ValidateCreate(ctx context.Context, obj runtime.O func (w *VPCAttachmentWebhook) ValidateUpdate(ctx context.Context, oldObj runtime.Object, newObj runtime.Object) (warnings admission.Warnings, err error) { newAttach := newObj.(*vpcapi.VPCAttachment) - oldAttach := oldObj.(*vpcapi.VPCAttachment) + // oldAttach := oldObj.(*vpcapi.VPCAttachment) - if !equality.Semantic.DeepEqual(oldAttach.Spec, newAttach.Spec) { - return nil, errors.Errorf("vpc attachment is immutable") - } + // if !equality.Semantic.DeepEqual(oldAttach.Spec, newAttach.Spec) { + // return nil, errors.Errorf("vpc attachment is immutable") + // } warns, err := newAttach.Validate(ctx, w.Validation) if err != nil { diff --git a/pkg/webhook/vpcpeering/vpcpeering_webhook.go b/pkg/webhook/vpcpeering/vpcpeering_webhook.go index 5d264d10..914c661e 100644 --- a/pkg/webhook/vpcpeering/vpcpeering_webhook.go +++ b/pkg/webhook/vpcpeering/vpcpeering_webhook.go @@ -3,11 +3,9 @@ package vpcpeering import ( "context" - "github.com/pkg/errors" vpcapi "go.githedgehog.com/fabric/api/vpc/v1alpha2" "go.githedgehog.com/fabric/pkg/manager/config" "go.githedgehog.com/fabric/pkg/manager/validation" - "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -67,11 +65,11 @@ func (w *VPCPeeringWebhook) ValidateCreate(ctx context.Context, obj runtime.Obje func (w *VPCPeeringWebhook) ValidateUpdate(ctx context.Context, oldObj runtime.Object, newObj runtime.Object) (warnings admission.Warnings, err error) { newPeering := newObj.(*vpcapi.VPCPeering) - oldPeering := oldObj.(*vpcapi.VPCPeering) + // oldPeering := oldObj.(*vpcapi.VPCPeering) - if !equality.Semantic.DeepEqual(oldPeering.Spec.Permit, newPeering.Spec.Permit) { - return nil, errors.Errorf("vpc peering permit list is immutable") - } + // if !equality.Semantic.DeepEqual(oldPeering.Spec.Permit, newPeering.Spec.Permit) { + // return nil, errors.Errorf("vpc peering permit list is immutable") + // } warns, err := newPeering.Validate(ctx, w.Validation, w.Cfg.VPCPeeringDisabled) if err != nil {