Merge pull request #1554 from lbohdanl/feat/vlan-support

This commit is contained in:
Daniel Finneran
2026-05-22 18:28:57 +01:00
committed by GitHub
4 changed files with 172 additions and 8 deletions

View File

@@ -44,6 +44,10 @@ type Instance struct {
DHCPv4Client vip.DHCPClient
DHCPv6Client vip.DHCPClient
// Service use Vlan
IsVLAN bool
VLANInterface string
// External Gateway IP the service is forwarded from
UPNPGatewayIPs []string
@@ -72,10 +76,31 @@ func NewInstance(ctx context.Context, svc *v1.Service, config *kubevip.Config,
var err error
var dnsAddresses []string
// Create new service
instance := &Instance{
ServiceSnapshot: svc,
dnsAddresses: dnsAddresses,
}
for _, address := range instanceAddresses {
// Detect if we're using a specific interface for services
var svcInterface string
svcInterface = svc.Annotations[kubevip.ServiceInterface] // If the service has a specific interface defined, then use it
svcInterface = svc.Annotations[kubevip.ServiceVlan]
if svcInterface != "" {
parent, tag, err := utils.ParseVLANInterface(svcInterface)
if err != nil {
log.Error("failed to validate VLAN", "err", err)
}
if err := instance.addVLAN(parent, tag); err != nil {
log.Error("failed to create VLAN", "err", err)
}
} else {
// If no vlan defined use specific interface from annotation
svcInterface = svc.Annotations[kubevip.ServiceInterface]
}
if svcInterface == kubevip.Auto {
link, err = autoFindInterface(address)
if err != nil {
@@ -194,7 +219,21 @@ func NewInstance(ctx context.Context, svc *v1.Service, config *kubevip.Config,
log.Info("hostname", "addr", hostname)
// Detect if we're using a specific interface for services
var svcInterface string
svcInterface = svc.Annotations[kubevip.ServiceInterface] // If the service has a specific interface defined, then use it
svcInterface = svc.Annotations[kubevip.ServiceVlan]
if svcInterface != "" {
parent, tag, err := utils.ParseVLANInterface(svcInterface)
if err != nil {
log.Error("failed to validate VLAN", "err", err)
}
if err := instance.addVLAN(parent, tag); err != nil {
log.Error("failed to create VLAN", "err", err)
}
} else {
// If no vlan defined use specific interface from annotation
svcInterface = svc.Annotations[kubevip.ServiceInterface]
}
// If it is still blank then use the
if svcInterface == "" {
@@ -239,12 +278,6 @@ func NewInstance(ctx context.Context, svc *v1.Service, config *kubevip.Config,
})
}
// Create new service
instance := &Instance{
ServiceSnapshot: svc,
dnsAddresses: dnsAddresses,
}
if svc.Annotations != nil {
instance.DHCPInterfaceHwaddr = svc.Annotations[kubevip.HwAddrKey]
requestedIP := svc.Annotations[kubevip.RequestedIP]
@@ -432,6 +465,57 @@ func getAutoInterfaceName(link netlink.Link, defaultInterface string) string {
return link.Attrs().Name
}
func (i *Instance) addVLAN(parentInterface string, tag int) error {
var parent netlink.Link
interfaceName := fmt.Sprintf("%s.%d", parentInterface, tag)
iface, err := netlink.LinkByName(interfaceName)
if err != nil {
// check if parent interface doesnt exist
parent, err = netlink.LinkByName(parentInterface)
if err != nil {
return fmt.Errorf("error finding VLAN parent interface %s: %v", parentInterface, err)
}
log.Info("Creating new VLAN interface", "interface", interfaceName)
vlan := &netlink.Vlan{
LinkAttrs: netlink.LinkAttrs{
Name: interfaceName,
ParentIndex: parent.Attrs().Index,
},
VlanId: tag,
VlanProtocol: netlink.VLAN_PROTOCOL_8021Q,
}
err = netlink.LinkAdd(vlan)
if err != nil {
return fmt.Errorf("could not add VLAN %s: %v", interfaceName, err)
}
err = netlink.LinkSetUp(vlan)
if err != nil {
return fmt.Errorf("could not bring up VLAN interface [%s] : %v", interfaceName, err)
}
_, err = net.InterfaceByName(interfaceName)
if err != nil {
return fmt.Errorf("error finding new VLAN interface by name [%v]", err)
}
} else {
log.Info("Using existing VLAN interface", "interface", interfaceName)
if err := utils.ValidateVLANInterface(iface, parent, tag); err != nil {
return err
}
}
i.VLANInterface = interfaceName
i.IsVLAN = true
return nil
}
func (i *Instance) startDHCP(ctx context.Context, index int, backoffAttempts uint, wg *sync.WaitGroup) error {
if len(i.VIPConfigs) > 2 {
return fmt.Errorf("DHCP can be used with 2 VIP config maximally, got: %v", len(i.VIPConfigs))

View File

@@ -52,6 +52,9 @@ const (
// Define an interface name to bind the address of the LoadBalancer to
ServiceInterface = "kube-vip.io/serviceInterface"
// Specify VLAN subinterface for service (e.g. eth0.200)
ServiceVlan = "kube-vip.io/serviceVLAN"
ServiceSecurityIgnore = "kube-vip.io/ignore-service-security"
// Enable UPNP on a Service

View File

@@ -431,6 +431,19 @@ func (p *Processor) deleteService(ctx context.Context, uid types.UID) error {
for x := range serviceInstance.Clusters {
serviceInstance.Clusters[x].Stop()
}
if serviceInstance.IsVLAN {
vlan, err := netlink.LinkByName(serviceInstance.VLANInterface)
if err != nil {
return fmt.Errorf("[service] error finding VLAN Interface: %v", err)
}
err = netlink.LinkDel(vlan)
if err != nil {
return fmt.Errorf("[service] error deleting VLAN interface : %v", err)
}
}
if serviceInstance.IsDHCPv4 || serviceInstance.IsDHCPv6 {
if serviceInstance.IsDHCPv4 {
serviceInstance.DHCPv4Client.Stop()

64
pkg/utils/vlan.go Normal file
View File

@@ -0,0 +1,64 @@
package utils
import (
"fmt"
"strconv"
"strings"
"github.com/vishvananda/netlink"
)
// ParseVLANInterface does vlan name validation
// and splits it into the parent interface and tag
func ParseVLANInterface(name string) (string, int, error) {
vlanParts := strings.Split(name, ".")
if len(vlanParts) != 2 {
return "", 0, fmt.Errorf("invalid VLAN name %s, expected format: eth1.300", name)
}
parent := vlanParts[0]
if parent == "" {
return "", 0, fmt.Errorf("parent interface for VLAN is empty")
}
tagStr := vlanParts[1]
tag, err := strconv.Atoi(tagStr)
if err != nil {
return "", 0, fmt.Errorf("invalid VLAN tag %s", tagStr)
}
// according to IEEE 802.1Q
if tag < 1 || tag > 4094 {
return "", 0, fmt.Errorf("VLAN tag must be between 1 and 4094, got %d", tag)
}
return parent, tag, nil
}
// ValidateVLANInterface validates vlans parent index and expected vlan tag
func ValidateVLANInterface(link netlink.Link, parent netlink.Link, expectedID int) error {
vlan, ok := link.(*netlink.Vlan)
if !ok {
return fmt.Errorf("link %s exists but is not a VLAN interface", link.Attrs().Name)
}
if vlan.ParentIndex != parent.Attrs().Index {
return fmt.Errorf(
"VLAN interface %s has parent index %d, expected %d",
link.Attrs().Name,
vlan.ParentIndex,
parent.Attrs().Index,
)
}
if vlan.VlanId != expectedID {
return fmt.Errorf(
"VLAN interface %s has id %d, expected %d",
link.Attrs().Name,
vlan.VlanId,
expectedID,
)
}
return nil
}