Skip to content
Draft
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
40 changes: 27 additions & 13 deletions api/vpc/v1beta1/vpc_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ type VPCSubnet struct {
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"`
// HostBGP is the flag to set this Subnet as dedicated to BGP speaking hosts advertising their VIPs within the subnet's IP range
HostBGP bool `json:"hostBGP,omitempty"`
}

// VPCDHCP defines the on-demand DHCP configuration for the subnet
Expand Down Expand Up @@ -272,7 +274,7 @@ func (vpc *VPC) Default() {
continue
}

if subnet.Gateway == "" {
if subnet.Gateway == "" && !subnet.HostBGP {
subnet.Gateway = cidr.Gateway.String()
}

Expand Down Expand Up @@ -383,6 +385,7 @@ func (vpc *VPC) Validate(ctx context.Context, kube kclient.Reader, fabricCfg *me

subnets := []*net.IPNet{}
vlans := map[uint16]bool{}
hostBGPSubnets := 0
for subnetName, subnetCfg := range vpc.Spec.Subnets {
if subnetCfg.Subnet == "" {
return nil, errors.Errorf("subnet %s: missing subnet", subnetName)
Expand All @@ -405,19 +408,30 @@ func (vpc *VPC) Validate(ctx context.Context, kube kclient.Reader, fabricCfg *me
}
}

if subnetCfg.Gateway == "" {
return nil, errors.Errorf("subnet %s: gateway is required", subnetName)
}
var gateway net.IP
if subnetCfg.HostBGP {
hostBGPSubnets++
if subnetCfg.VLAN != 0 {
return nil, errors.Errorf("subnet %s: vlan should not be set for hostBGP subnets", subnetName)
}
if subnetCfg.DHCP.Enable {
return nil, errors.Errorf("subnet %s: dhcp should not be enabled for hostBGP subnets", subnetName)
}
} else {
if subnetCfg.Gateway == "" {
return nil, errors.Errorf("subnet %s: gateway is required", subnetName)
}

gateway := net.ParseIP(subnetCfg.Gateway)
if !ipNet.Contains(gateway) {
return nil, errors.Errorf("subnet %s: gateway %s is not in the subnet", subnetName, subnetCfg.Gateway)
}
gateway = net.ParseIP(subnetCfg.Gateway)
if !ipNet.Contains(gateway) {
return nil, errors.Errorf("subnet %s: gateway %s is not in the subnet", subnetName, subnetCfg.Gateway)
}

if subnetCfg.VLAN == 0 {
return nil, errors.Errorf("subnet %s: vlan is required", subnetName)
if subnetCfg.VLAN == 0 {
return nil, errors.Errorf("subnet %s: vlan is required", subnetName)
}
vlans[subnetCfg.VLAN] = true
}
vlans[subnetCfg.VLAN] = true

subnets = append(subnets, ipNet)

Expand Down Expand Up @@ -597,7 +611,7 @@ func (vpc *VPC) Validate(ctx context.Context, kube kclient.Reader, fabricCfg *me
}
}

if len(vlans) != len(vpc.Spec.Subnets) {
if len(vlans) != len(vpc.Spec.Subnets)-hostBGPSubnets {
return nil, errors.Errorf("duplicate subnet VLANs")
}

Expand Down Expand Up @@ -690,7 +704,7 @@ func (vpc *VPC) Validate(ctx context.Context, kube kclient.Reader, fabricCfg *me
return nil, errors.Errorf("vpc subnet %s (%s) doesn't belong to the IPv4Namespace %s", subnetName, subnetCfg.Subnet, vpc.Spec.IPv4Namespace)
}

if !vlanNs.Spec.Contains(subnetCfg.VLAN) {
if !subnetCfg.HostBGP && !vlanNs.Spec.Contains(subnetCfg.VLAN) {
return nil, errors.Errorf("vpc subnet %s (%s) vlan %d doesn't belong to the VLANNamespace %s", subnetName, subnetCfg.Subnet, subnetCfg.VLAN, vpc.Spec.VLANNamespace)
}
}
Expand Down
12 changes: 8 additions & 4 deletions cmd/hhfctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,8 @@ func main() {
Required: true,
},
&cli.StringFlag{
Name: "vlan",
Usage: "vlan",
Required: true,
Name: "vlan",
Usage: "vlan",
},
&cli.BoolFlag{
Name: "dhcp",
Expand Down Expand Up @@ -208,6 +207,10 @@ func main() {
Name: "dhcp-advertised-routes",
Usage: "custom routes to advertise in dhcp, in the format prefix-gateway, e.g. 8.8.8.0/24-192.168.1.1",
},
&cli.BoolFlag{
Name: "host-bgp",
Usage: "mark the subnet as dedicated to BGP speakers",
},
printYamlFlag,
},
Before: func(_ *cli.Context) error {
Expand Down Expand Up @@ -244,7 +247,8 @@ func main() {
AdvertisedRoutes: advertisedRoutes,
},
},
Mode: vpcapi.VPCMode(cCtx.String("vpc-mode")),
Mode: vpcapi.VPCMode(cCtx.String("vpc-mode")),
HostBGP: cCtx.Bool("host-bgp"),
}), "failed to create vpc")
},
},
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/agent.githedgehog.com_agents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,11 @@ spec:
specified, the first IP (e.g. 10.0.0.1) in the subnet
is used as the gateway
type: string
hostBGP:
description: HostBGP is the flag to set this Subnet as
dedicated to BGP speaking hosts advertising their VIPs
within the subnet's IP range
type: boolean
isolated:
description: Isolated is the flag to enable isolated mode
for the subnet which means no access to and from the
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/vpc.githedgehog.com_vpcs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ spec:
the first IP (e.g. 10.0.0.1) in the subnet is used as the
gateway
type: string
hostBGP:
description: HostBGP is the flag to set this Subnet as dedicated
to BGP speaking hosts advertising their VIPs within the subnet's
IP range
type: boolean
isolated:
description: Isolated is the flag to enable isolated mode for
the subnet which means no access to and from the other subnets
Expand Down
1 change: 1 addition & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,7 @@ _Appears in:_
| `vlan` _integer_ | 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 | | |
| `hostBGP` _boolean_ | HostBGP is the flag to set this Subnet as dedicated to BGP speaking hosts advertising their VIPs within the subnet's IP range | | |



Expand Down
2 changes: 2 additions & 0 deletions pkg/agent/dozer/bcm/enforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,13 @@ const (

ActionWeightInterfaceSubinterfaceIPsDelete
ActionWeightVRFAttachedHostDelete
ActionWeightInterfaceSubinterfaceIPv6Delete
ActionWeightVRFInterfaceDelete
ActionWeightACLInterfaceDelete
ActionWeightInterfaceSubinterfaceDelete
ActionWeightInterfaceSubinterfaceUpdate
ActionWeightVRFInterfaceUpdate
ActionWeightInterfaceSubinterfaceIPv6Update
ActionWeightVRFAttachedHostUpdate
ActionWeightInterfaceSubinterfaceIPsUpdate

Expand Down
130 changes: 115 additions & 15 deletions pkg/agent/dozer/bcm/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -1778,14 +1778,16 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error {
return errors.Errorf("VPC %s subnet %s not found", vpcName, subnetName)
}

switch vpc.Mode {
case vpcapi.VPCModeL2VNI, vpcapi.VPCModeL3VNI:
if err := planVNIVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil {
return errors.Wrapf(err, "failed to plan VPC %s subnet %s", vpcName, subnetName)
}
case vpcapi.VPCModeL3Flat:
if err := planL3FlatVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil {
return errors.Wrapf(err, "failed to plan L3 VPC %s subnet %s", vpcName, subnetName)
if !subnet.HostBGP {
switch vpc.Mode {
case vpcapi.VPCModeL2VNI, vpcapi.VPCModeL3VNI:
if err := planVNIVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil {
return errors.Wrapf(err, "failed to plan VPC %s subnet %s", vpcName, subnetName)
}
case vpcapi.VPCModeL3Flat:
if err := planL3FlatVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil {
return errors.Wrapf(err, "failed to plan L3 VPC %s subnet %s", vpcName, subnetName)
}
}
}

Expand Down Expand Up @@ -1842,13 +1844,19 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error {
ifaces = append(ifaces, conn.Unbundled.Link.Switch.LocalPortName())
}

for _, iface := range ifaces {
if attach.NativeVLAN {
spec.Interfaces[iface].AccessVLAN = pointer.To(subnet.VLAN)
} else {
vlanStr := fmt.Sprintf("%d", subnet.VLAN)
if !slices.Contains(spec.Interfaces[iface].TrunkVLANs, vlanStr) {
spec.Interfaces[iface].TrunkVLANs = append(spec.Interfaces[iface].TrunkVLANs, vlanStr)
if subnet.HostBGP {
if err := planHostBGPSubnet(agent, spec, vpcName, vpc, subnetName, subnet, ifaces); err != nil {
return errors.Wrapf(err, "failed to plan HostBGP VPC %s subnet %s", vpcName, subnetName)
}
} else {
for _, iface := range ifaces {
if attach.NativeVLAN {
spec.Interfaces[iface].AccessVLAN = pointer.To(subnet.VLAN)
} else {
vlanStr := fmt.Sprintf("%d", subnet.VLAN)
if !slices.Contains(spec.Interfaces[iface].TrunkVLANs, vlanStr) {
spec.Interfaces[iface].TrunkVLANs = append(spec.Interfaces[iface].TrunkVLANs, vlanStr)
}
}
}
}
Expand Down Expand Up @@ -1877,6 +1885,11 @@ func planVPCs(agent *agentapi.Agent, spec *dozer.Spec) error {
return errors.Errorf("VPC %s subnet %s not found", vpcName, subnetName)
}

// no VLAN interface to configure on HostBGP subnets
if subnet.HostBGP {
continue
}

switch vpc.Mode {
case vpcapi.VPCModeL2VNI, vpcapi.VPCModeL3VNI:
if err := planVNIVPCSubnet(agent, spec, vpcName, vpc, subnetName, subnet); err != nil {
Expand Down Expand Up @@ -2495,6 +2508,85 @@ func addL3FlatVPCFilteringACLEntryiesForVPC(agent *agentapi.Agent, spec *dozer.S
return nil
}

func planHostBGPSubnet(agent *agentapi.Agent, spec *dozer.Spec, vpcName string, vpc vpcapi.VPCSpec, subnetName string, subnet *vpcapi.VPCSubnet, ifaceNames []string) error {
vrfName := vpcVrfName(vpcName)
_, err := iputil.ParseCIDR(subnet.Subnet)
if err != nil {
return errors.Wrapf(err, "failed to parse hostBGP subnet %s for VPC %s", subnet.Subnet, vpcName)
}

// create prefix list matching /32s in this subnet prefix
plName := vpcSubnetVIPsOnlyPrefixListName(vpcName, subnetName)
spec.PrefixLists[plName] = &dozer.SpecPrefixList{
Prefixes: map[uint32]*dozer.SpecPrefixListEntry{
10: {
Prefix: dozer.SpecPrefixListPrefix{
Prefix: subnet.Subnet,
Ge: 32,
Le: 32,
},
Action: dozer.SpecPrefixListActionPermit,
},
},
}

// create routemap filtering anything that is not the prefix list above
rmName := vpcSubnetVIPsOnlyRouteMapName(vpcName, subnetName)
spec.RouteMaps[rmName] = &dozer.SpecRouteMap{
Statements: map[string]*dozer.SpecRouteMapStatement{
"10": {
Conditions: dozer.SpecRouteMapConditions{MatchPrefixList: pointer.To(plName)},
Result: dozer.SpecRouteMapResultAccept,
},
},
}

vpcFilteringACL := vpcFilteringAccessListName(vpcName, subnetName)
spec.ACLs[vpcFilteringACL], err = buildVNIVPCFilteringACL(agent, vpcName, vpc, subnetName, subnet)
if err != nil {
return errors.Wrapf(err, "failed to plan VPC filtering ACL for VPC %s hostBGP subnet %s", vpcName, subnetName)
}

// for each of the attachment interfaces on the switch side, enslave them to the VPC VRF and enable IPv6 for BGP unnumbered
// then add an unnumbered BGP neighbor on that interface in the VPC VRF instance
// finally add the ACL to handle restricted subnets (this will be removed if it's empty)
for _, iface := range ifaceNames {
spec.VRFs[vrfName].Interfaces[iface] = &dozer.SpecVRFInterface{}
if spec.Interfaces[iface].Subinterfaces == nil {
spec.Interfaces[iface].Subinterfaces = map[uint32]*dozer.SpecSubinterface{
0: {},
}
}
subIf0, ok := spec.Interfaces[iface].Subinterfaces[0]
if !ok {
spec.Interfaces[iface].Subinterfaces[0] = &dozer.SpecSubinterface{}
subIf0 = spec.Interfaces[iface].Subinterfaces[0]
}
subIf0.Ipv6Enabled = pointer.To(true)

if spec.VRFs[vrfName].BGP == nil {
return errors.Errorf("VRF %s BGP not found when planning hostBGP for VPC %s subnet %s", vrfName, vpcName, subnetName)
}
if spec.VRFs[vrfName].BGP.Neighbors == nil {
spec.VRFs[vrfName].BGP.Neighbors = map[string]*dozer.SpecVRFBGPNeighbor{}
}
spec.VRFs[vrfName].BGP.Neighbors[iface] = &dozer.SpecVRFBGPNeighbor{
Enabled: pointer.To(true),
Description: pointer.To(fmt.Sprintf("HostBGP unnumbered %s", iface)),
PeerType: pointer.To(string(dozer.SpecVRFBGPNeighborPeerTypeExternal)),
ExtendedNexthop: pointer.To(true),
IPv4Unicast: pointer.To(true),
IPv4UnicastImportPolicies: []string{rmName},
}

spec.ACLInterfaces[iface] = &dozer.SpecACLInterface{
Ingress: pointer.To(vpcFilteringACL),
}
}

return nil
}

func planVNIVPCSubnet(agent *agentapi.Agent, spec *dozer.Spec, vpcName string, vpc vpcapi.VPCSpec, subnetName string, subnet *vpcapi.VPCSubnet) error {
vrfName := vpcVrfName(vpcName)

Expand Down Expand Up @@ -3239,6 +3331,14 @@ func vpcFilteringAccessListName(vpc string, subnet string) string {
return fmt.Sprintf("vpc-filtering--%s--%s", vpc, subnet)
}

func vpcSubnetVIPsOnlyPrefixListName(vpc string, subnet string) string {
return fmt.Sprintf("vips-only--%s--%s", vpc, subnet)
}

func vpcSubnetVIPsOnlyRouteMapName(vpc string, subnet string) string {
return fmt.Sprintf("vips-only--%s--%s", vpc, subnet)
}

func communityForVPC(agent *agentapi.Agent, vpc string) (string, error) {
baseParts := strings.Split(agent.Spec.Config.BaseVPCCommunity, ":")
if len(baseParts) != 2 {
Expand Down
26 changes: 26 additions & 0 deletions pkg/agent/dozer/bcm/spec_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ var specInterfaceSubinterfaceEnforcer = &DefaultValueEnforcer[uint32, *dozer.Spe
if err := specInterfaceSubinterfaceBaseEnforcer.Handle(basePath, idx, actual, desired, actions); err != nil {
return errors.Wrap(err, "failed to handle subinterface base")
}
ipv6Path := basePath + "/ipv6/config"
if err := specInterfaceSubinterfaceIPv6Enforcer.Handle(ipv6Path, idx, actual, desired, actions); err != nil {
return errors.Wrap(err, "failed to handle subinterface ipv6")
}

actualIPs, desiredIPs := ValueOrNil(actual, desired,
func(value *dozer.SpecSubinterface) map[string]*dozer.SpecInterfaceIP { return value.IPs })
Expand All @@ -248,6 +252,23 @@ var specInterfaceSubinterfaceEnforcer = &DefaultValueEnforcer[uint32, *dozer.Spe
},
}

var specInterfaceSubinterfaceIPv6Enforcer = &DefaultValueEnforcer[uint32, *dozer.SpecSubinterface]{
Summary: "Subinterface %d IPv6 Enable",
NoReplace: true,
UpdateWeight: ActionWeightInterfaceSubinterfaceIPv6Update,
DeleteWeight: ActionWeightInterfaceSubinterfaceIPv6Delete,
Marshal: func(idx uint32, value *dozer.SpecSubinterface) (ygot.ValidatedGoStruct, error) {
ipv6 := &oc.OpenconfigInterfaces_Interfaces_Interface_Subinterfaces_Subinterface_Ipv6{}
if value.Ipv6Enabled != nil {
ipv6.Config = &oc.OpenconfigInterfaces_Interfaces_Interface_Subinterfaces_Subinterface_Ipv6_Config{
Enabled: value.Ipv6Enabled,
}
}

return ipv6, nil
},
}

var specInterfaceSubinterfaceBaseEnforcer = &DefaultValueEnforcer[uint32, *dozer.SpecSubinterface]{
Summary: "Subinterface Base %d",
NoReplace: true, // TODO check if it'll work correctly
Expand Down Expand Up @@ -612,6 +633,11 @@ func unmarshalOCInterfaces(agent *agentapi.Agent, ocVal *oc.OpenconfigInterfaces
}
}

// only set this if it exitsts and it is true
if sub.Ipv6 != nil && sub.Ipv6.Config != nil && sub.Ipv6.Config.Enabled != nil && *sub.Ipv6.Config.Enabled {
subIface.Ipv6Enabled = sub.Ipv6.Config.Enabled
}

iface.Subinterfaces[id] = subIface
}
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/agent/dozer/bcm/spec_vrf.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ var specVRFBGPNeighborEnforcer = &DefaultValueEnforcer[string, *dozer.SpecVRFBGP
PeerAs: remoteAS,
PeerType: peerType,
DisableEbgpConnectedRouteCheck: value.DisableConnectedCheck,
CapabilityExtendedNexthop: value.ExtendedNexthop,
},
AfiSafis: &oc.OpenconfigNetworkInstance_NetworkInstances_NetworkInstance_Protocols_Protocol_Bgp_Neighbors_Neighbor_AfiSafis{
AfiSafi: map[oc.E_OpenconfigBgpTypes_AFI_SAFI_TYPE]*oc.OpenconfigNetworkInstance_NetworkInstances_NetworkInstance_Protocols_Protocol_Bgp_Neighbors_Neighbor_AfiSafis_AfiSafi{ //nolint:exhaustive,nolintlint
Expand Down Expand Up @@ -890,6 +891,7 @@ func unmarshalOCVRFs(ocVal *oc.OpenconfigNetworkInstance_NetworkInstances) (map[
L2VPNEVPNAllowOwnAS: l2VPNEVPNAllowOwnAS,
BFDProfile: bfdProfile,
DisableConnectedCheck: neighbor.Config.DisableEbgpConnectedRouteCheck,
ExtendedNexthop: neighbor.Config.CapabilityExtendedNexthop,
}
if neighbor.Transport != nil && neighbor.Transport.Config != nil {
bgp.Neighbors[neighborName].UpdateSource = neighbor.Transport.Config.LocalAddress
Expand Down
Loading
Loading