Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subnet filtering support #361

Merged
merged 9 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/agent/v1alpha2/catalog_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions api/agent/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

103 changes: 99 additions & 4 deletions api/vpc/v1alpha2/vpc_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package v1alpha2
import (
"context"
"net"
"strconv"

"github.com/pkg/errors"
wiringapi "go.githedgehog.com/fabric/api/wiring/v1alpha2"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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{}
Expand Down Expand Up @@ -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
Expand Down
68 changes: 49 additions & 19 deletions api/vpc/v1alpha2/vpcpeering_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -142,29 +145,19 @@ 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
}

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 {
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions api/vpc/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions api/wiring/v1alpha2/vlannamespace_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
Loading
Loading