Skip to content

Commit

Permalink
Merge pull request #71 from flawedmatrix/dualstack-support
Browse files Browse the repository at this point in the history
Support for dualstack loadbalancer services
  • Loading branch information
thebsdbox authored Dec 31, 2023
2 parents e11cc57 + 1ca71bd commit 376a9ba
Show file tree
Hide file tree
Showing 6 changed files with 806 additions and 15 deletions.
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ The `kube-vip-cloud-provider` will only implement the `loadBalancer` functionali
- IP ranges [start address - end address]
- Multiple pools by CIDR per namespace
- Multiple IP ranges per namespace (handles overlapping ranges)
- Support for mixed IP families when specifying multiple pools or ranges
- Setting of static addresses through `--load-balancer-ip=x.x.x.x` or through annotations `kube-vip.io/loadbalancerIPs: x.x.x.x`
- Setting the special IP `0.0.0.0` for DHCP workflow.
- Support single stack IPv6 or IPv4
- Support for dualstack via the annotation: `kube-vip.io/loadbalancerIPs: 192.168.10.10,2001:db8::1`
- Support ascending and descending search order by setting search-order=desc

## Installing the `kube-vip-cloud-provider`
Expand Down Expand Up @@ -87,7 +89,46 @@ kubectl create configmap --namespace kube-system kubevip --from-literal range-gl

## Multiple pools or ranges

We can apply multiple pools or ranges by seperating them with commas.. i.e. `192.168.0.200/30,192.168.0.200/29` or `2001::12/127,2001::10/127` or `192.168.0.10-192.168.0.11,192.168.0.10-192.168.0.13` or `2001::10-2001::14,2001::20-2001::24`
We can apply multiple pools or ranges by seperating them with commas.. i.e. `192.168.0.200/30,192.168.0.200/29` or `2001::12/127,2001::10/127` or `192.168.0.10-192.168.0.11,192.168.0.10-192.168.0.13` or `2001::10-2001::14,2001::20-2001::24` or `192.168.0.200/30,2001::10/127`

## Dualstack Services

Suppose a pool in the configmap is as follows: `range-default: 192.168.0.10-192.168.0.11,2001::10-2001::11`
and there are no IPs currently in use.

Then by creating a service with the following spec (with `IPv6` specified first in `ipFamilies`):
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-service
labels:
app.kubernetes.io/name: MyApp
spec:
ipFamilyPolicy: PreferDualStack
ipFamilies:
- IPv6
- IPv4
selector:
app.kubernetes.io/name: MyApp
ports:
- protocol: TCP
port: 80
```
The service will receive the annotation `kube-vip.io/loadbalancerIPs:
2001::10,192.168.0.10` following the intent to prefer IPv6. Conversely, if
`IPv4` were specified first, then the IPv4 address will appear first in the
annotation.

With the `PreferDualStack` IP family policy, kube-vip-cloud-provider will make a
best effort to provide at least one IP in `loadBalancerIPs` as long as any IP family
in the pool has available addresses.

If `RequireDualStack` is specified, then kube-vip-cloud-provider will fail to
set the `kube-vip.io/loadbalancerIPs` annotation if it cannot find an available
address in each of both IP families for the pool.


## Special DHCP CIDR

Expand Down
66 changes: 63 additions & 3 deletions pkg/ipam/addressbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"go4.org/netipx"
)

// buildHostsFromCidr - Builds a IPSet constructed from the cidr
func buildHostsFromCidr(cidr string) (*netipx.IPSet, error) {
// parseCidr - Builds an IPSet constructed from the cidrs
func parseCidrs(cidr string) (*netipx.IPSet, error) {
// Split the ipranges (comma separated)
cidrs := strings.Split(cidr, ",")
if len(cidrs) == 0 {
Expand All @@ -23,6 +23,21 @@ func buildHostsFromCidr(cidr string) (*netipx.IPSet, error) {
if err != nil {
return nil, err
}
builder.AddPrefix(prefix)
}
return builder.IPSet()
}

// buildHostsFromCidr - Builds a IPSet constructed from the cidr and filters out
// the broadcast IP and network IP for IPv4 networks
func buildHostsFromCidr(cidr string) (*netipx.IPSet, error) {
unfilteredSet, err := parseCidrs(cidr)
if err != nil {
return nil, err
}

builder := &netipx.IPSetBuilder{}
for _, prefix := range unfilteredSet.Prefixes() {
if prefix.IsSingleIP() {
builder.Add(prefix.Addr())
continue
Expand All @@ -31,7 +46,6 @@ func buildHostsFromCidr(cidr string) (*netipx.IPSet, error) {
builder.AddPrefix(prefix)
continue
}

if r := netipx.RangeOfPrefix(prefix); r.IsValid() {
if prefix.Bits() == 31 {
// rfc3021 Using 31-Bit Prefixes on IPv4 Point-to-Point Links
Expand Down Expand Up @@ -77,3 +91,49 @@ func buildAddressesFromRange(ipRangeString string) (*netipx.IPSet, error) {

return builder.IPSet()
}

// SplitCIDRsByIPFamily splits the cidrs into separate lists of ipv4
// and ipv6 CIDRs
func SplitCIDRsByIPFamily(cidrs string) (ipv4 string, ipv6 string, err error) {
ipPools, err := parseCidrs(cidrs)
if err != nil {
return "", "", err
}
ipv4Cidrs := strings.Builder{}
ipv6Cidrs := strings.Builder{}
for _, prefix := range ipPools.Prefixes() {
cidrsToEdit := &ipv4Cidrs
if prefix.Addr().Is6() {
cidrsToEdit = &ipv6Cidrs
}
if cidrsToEdit.Len() > 0 {
cidrsToEdit.WriteByte(',')
}
_, _ = cidrsToEdit.WriteString(prefix.String())
}
return ipv4Cidrs.String(), ipv6Cidrs.String(), nil
}

// SplitRangesByIPFamily splits the ipRangeString into separate lists of ipv4
// and ipv6 ranges
func SplitRangesByIPFamily(ipRangeString string) (ipv4 string, ipv6 string, err error) {
ipPools, err := buildAddressesFromRange(ipRangeString)
if err != nil {
return "", "", err
}
ipv4Ranges := strings.Builder{}
ipv6Ranges := strings.Builder{}
for _, ipRange := range ipPools.Ranges() {
rangeToEdit := &ipv4Ranges
if ipRange.From().Is6() {
rangeToEdit = &ipv6Ranges
}
if rangeToEdit.Len() > 0 {
rangeToEdit.WriteByte(',')
}
_, _ = rangeToEdit.WriteString(ipRange.From().String())
_ = rangeToEdit.WriteByte('-')
_, _ = rangeToEdit.WriteString(ipRange.To().String())
}
return ipv4Ranges.String(), ipv6Ranges.String(), nil
}
22 changes: 18 additions & 4 deletions pkg/ipam/ipam.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ import (
"k8s.io/klog"
)

type OutOfIPsError struct {
namespace string
pool string
isCidr bool
}

func (e *OutOfIPsError) Error() string {
what := "range"
if e.isCidr {
what = "cidr"
}
return fmt.Sprintf("no addresses available in [%s] %s [%s]", e.namespace, what, e.pool)
}

// Manager - handles the addresses for each namespace/vip
var Manager []ipManager

Expand Down Expand Up @@ -46,7 +60,7 @@ func FindAvailableHostFromRange(namespace, ipRange string, inUseIPSet *netipx.IP

addr, err := FindFreeAddress(Manager[x].poolIPSet, inUseIPSet, descOrder)
if err != nil {
return "", fmt.Errorf("no addresses available in [%s] range [%s]", namespace, ipRange)
return "", &OutOfIPsError{namespace: namespace, pool: ipRange, isCidr: false}
}
return addr.String(), nil
}
Expand All @@ -67,7 +81,7 @@ func FindAvailableHostFromRange(namespace, ipRange string, inUseIPSet *netipx.IP

addr, err := FindFreeAddress(poolIPSet, inUseIPSet, descOrder)
if err != nil {
return "", fmt.Errorf("no addresses available in [%s] range [%s]", namespace, ipRange)
return "", &OutOfIPsError{namespace: namespace, pool: ipRange, isCidr: false}
}
return addr.String(), nil
}
Expand All @@ -91,7 +105,7 @@ func FindAvailableHostFromCidr(namespace, cidr string, inUseIPSet *netipx.IPSet,
}
addr, err := FindFreeAddress(Manager[x].poolIPSet, inUseIPSet, descOrder)
if err != nil {
return "", fmt.Errorf("no addresses available in [%s] cidr [%s]", namespace, cidr)
return "", &OutOfIPsError{namespace: namespace, pool: cidr, isCidr: true}
}
return addr.String(), nil

Expand All @@ -111,7 +125,7 @@ func FindAvailableHostFromCidr(namespace, cidr string, inUseIPSet *netipx.IPSet,

addr, err := FindFreeAddress(poolIPSet, inUseIPSet, descOrder)
if err != nil {
return "", fmt.Errorf("no addresses available in [%s] cidr [%s]", namespace, cidr)
return "", &OutOfIPsError{namespace: namespace, pool: cidr, isCidr: true}
}
return addr.String(), nil

Expand Down
Loading

0 comments on commit 376a9ba

Please sign in to comment.