Compare commits

...

40 Commits

Author SHA1 Message Date
Daniel Finneran
28bec598f5 Merge pull request #1360 from p-strusiewiczsurmacki-mobica/fix/redundant-delete-svcelection
Fixed service deletion when service leader election is enabled
2025-12-19 09:13:37 +00:00
Patryk Strusiewicz-Surmacki
200d0d960c Fixed service deletion when service leader election is enabled
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-12-18 17:16:39 +01:00
Daniel Finneran
6e0f2132ca Merge pull request #1359 from kube-vip/repeat-of-prev-PR-to-move-error-to-warn 2025-12-17 19:55:31 +00:00
Daniel Finneran
19152ccd15 Merge pull request #1358 from kube-vip/bump-Makefile 2025-12-17 19:55:15 +00:00
Daniel Finneran
741d4a63e0 Move an Error to a warning 2025-12-17 18:04:56 +00:00
Daniel Finneran
badd66c4e8 Update Makefile 2025-12-17 18:02:56 +00:00
Daniel Finneran
fbb0717cb4 Merge pull request #1357 from p-strusiewiczsurmacki-mobica/fix/service-port-security
Fixed service port security rules for iptables
2025-12-17 18:02:29 +00:00
Marcel Fest
414ba0c6a6 Merge branch 'main' into fix/service-port-security 2025-12-17 18:09:11 +01:00
Daniel Finneran
c4054fa86d Merge pull request #1356 from p-strusiewiczsurmacki-mobica/fix/service-dns
Fixed service DNS resolve
2025-12-17 16:06:39 +00:00
Patryk Strusiewicz-Surmacki
5bfec6e426 Fixed service port security rules
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-12-17 12:49:06 +01:00
Daniel Finneran
075639996e Merge pull request #1354 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.20.11
Bump anchore/sbom-action from 0.20.10 to 0.20.11
2025-12-16 14:16:50 +00:00
Daniel Finneran
d90b7ae20e Merge pull request #1353 from kube-vip/dependabot/github_actions/actions/upload-artifact-6
Bump actions/upload-artifact from 5 to 6
2025-12-16 14:16:32 +00:00
Daniel Finneran
b200e9a3f7 Merge pull request #1352 from kube-vip/dependabot/go_modules/k8s.io/client-go-0.34.3
Bump k8s.io/client-go from 0.34.1 to 0.34.3
2025-12-16 14:16:17 +00:00
dependabot[bot]
e6c48839a7 Bump anchore/sbom-action from 0.20.10 to 0.20.11
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.20.10 to 0.20.11.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.20.10...v0.20.11)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.20.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 12:58:25 +00:00
dependabot[bot]
53a86b6164 Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 12:58:13 +00:00
dependabot[bot]
1988a69d1b Bump k8s.io/client-go from 0.34.1 to 0.34.3
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.34.1 to 0.34.3.
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.34.1...v0.34.3)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-version: 0.34.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 12:58:06 +00:00
Daniel Finneran
3b97bb7360 Merge pull request #1351 from kube-vip/dependabot/go_modules/k8s.io/api-0.34.3
Bump k8s.io/api from 0.34.2 to 0.34.3
2025-12-16 12:54:44 +00:00
Daniel Finneran
09800c9be6 Merge pull request #1346 from kube-vip/dependabot/go_modules/github.com/florianl/go-conntrack-0.5.0
Bump github.com/florianl/go-conntrack from 0.4.0 to 0.5.0
2025-12-16 12:54:32 +00:00
Patryk Strusiewicz-Surmacki
c8e0a72be6 Fixed service DNS resolve
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-12-15 17:48:03 +01:00
Daniel Finneran
002a83fa65 Merge pull request #1347 from kube-vip/dependabot/docker/alpine-3.23.0
Bump alpine from 3.22.2 to 3.23.0
2025-12-15 16:42:42 +00:00
Daniel Finneran
9ad84e3ae6 Merge pull request #1345 from kube-vip/dependabot/go_modules/github.com/spf13/cobra-1.10.2
Bump github.com/spf13/cobra from 1.10.1 to 1.10.2
2025-12-15 16:10:36 +00:00
Daniel Finneran
ad50b9c3ef Merge pull request #1344 from kube-vip/dependabot/go_modules/testing/e2e/e2e/github.com/sirupsen/logrus-1.9.1
Bump github.com/sirupsen/logrus from 1.9.0 to 1.9.1 in /testing/e2e/e2e
2025-12-15 16:10:10 +00:00
dependabot[bot]
d98a6ef660 Bump k8s.io/api from 0.34.2 to 0.34.3
Bumps [k8s.io/api](https://github.com/kubernetes/api) from 0.34.2 to 0.34.3.
- [Commits](https://github.com/kubernetes/api/compare/v0.34.2...v0.34.3)

---
updated-dependencies:
- dependency-name: k8s.io/api
  dependency-version: 0.34.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 15:33:29 +00:00
dependabot[bot]
c83a8cea2f Bump github.com/florianl/go-conntrack from 0.4.0 to 0.5.0
Bumps [github.com/florianl/go-conntrack](https://github.com/florianl/go-conntrack) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/florianl/go-conntrack/releases)
- [Commits](https://github.com/florianl/go-conntrack/compare/v0.4.0...v0.5.0)

---
updated-dependencies:
- dependency-name: github.com/florianl/go-conntrack
  dependency-version: 0.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 15:32:50 +00:00
dependabot[bot]
08966e9bed Bump github.com/spf13/cobra from 1.10.1 to 1.10.2
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 15:32:32 +00:00
dependabot[bot]
6bf0e37282 Bump github.com/sirupsen/logrus from 1.9.0 to 1.9.1 in /testing/e2e/e2e
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.9.0...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  dependency-version: 1.9.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 15:32:00 +00:00
dependabot[bot]
ea1d6136be Bump alpine from 3.22.2 to 3.23.0
Bumps alpine from 3.22.2 to 3.23.0.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: 3.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 15:31:50 +00:00
Daniel Finneran
ff3e85c6a6 Merge pull request #1355 from kube-vip/golang_bump
Bump to golang to fix stdlib vuln
2025-12-15 15:30:49 +00:00
Dan Finneran
38a6fdb457 bump alpine release for build
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-12-15 14:57:21 +00:00
Dan Finneran
dfd1b1da19 path up other ci complaints.
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-12-15 14:53:21 +00:00
Dan Finneran
aecc0264b0 upgrade lint config
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-12-15 12:02:41 +00:00
Dan Finneran
a35849d4f8 bump ci workflow
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-12-15 11:57:46 +00:00
Dan Finneran
4c3a2d57ee Bump to golang to fix stdlib vuln
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-12-15 11:53:23 +00:00
Daniel Finneran
48c99ca320 Merge pull request #1342 from kube-vip/dependabot/github_actions/docker/metadata-action-5.10.0
Bump docker/metadata-action from 5.9.0 to 5.10.0
2025-12-15 11:50:31 +00:00
Daniel Finneran
83d1a9e455 Merge pull request #1341 from kube-vip/dependabot/go_modules/github.com/google/go-containerregistry-0.20.7
Bump github.com/google/go-containerregistry from 0.20.6 to 0.20.7
2025-12-15 11:50:18 +00:00
Daniel Finneran
11649c9e79 Merge pull request #1350 from ivucica/ivucica-upnp-lease-duration
Support specifying UPNP lease duration as annotation on a service.
2025-12-15 11:50:06 +00:00
Ivan Vučica
b31de5ca61 Support specifying UPNP lease duration as annotation on a service.
The annotation `kube-vip.io/upnp-lease-duration` takes in a duration where the string is parseable by `time.ParseDuration`.

There is no new configuration option that would allow setting this globally, since this new feature is most helpful in limited cases (such as incorrect implementations that have trouble with certain values).

There is a minimal test added for the value returned by the new private helper `upnpLeaseDurationForService`. Due to a lack of dependency injection in `service.Processor`'s `upnpMap`, it is infeasible to add a more complete test at this time, to check what would be passed, exactly, into `AddPinholeCtx` and `AddPortMapping`.

Signed-off-by: Ivan Vučica <ivan@vucica.net>
Signed-off-by: Ivan Vučica <ivucica@google.com>
Fixes: #1349
2025-12-12 12:22:37 +00:00
Daniel Finneran
18544a5d54 Merge pull request #1343 from kube-vip/error_warn
Move this error to a warning
2025-12-01 15:56:14 +00:00
dependabot[bot]
099d8f759b Bump docker/metadata-action from 5.9.0 to 5.10.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.9.0 to 5.10.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5.9.0...v5.10.0)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 5.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 02:14:54 +00:00
dependabot[bot]
e99fa71ed6 Bump github.com/google/go-containerregistry from 0.20.6 to 0.20.7
Bumps [github.com/google/go-containerregistry](https://github.com/google/go-containerregistry) from 0.20.6 to 0.20.7.
- [Release notes](https://github.com/google/go-containerregistry/releases)
- [Commits](https://github.com/google/go-containerregistry/compare/v0.20.6...v0.20.7)

---
updated-dependencies:
- dependency-name: github.com/google/go-containerregistry
  dependency-version: 0.20.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 02:03:39 +00:00
43 changed files with 1574 additions and 581 deletions

View File

@@ -26,6 +26,6 @@ jobs:
with:
ref: ${{ github.ref_name }}
- name: Anchore SBOM Action
uses: anchore/sbom-action@v0.20.10
uses: anchore/sbom-action@v0.20.11
with:
format: cyclonedx-json

View File

@@ -41,7 +41,7 @@ jobs:
run: sudo chmod -R 755 /tmp/kube-vip-test-${{ matrix.mode }}*
if: matrix.mode== 'bgp' && always()
- name: Save logs
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: e2e-test-logs-${{ matrix.mode }}-${{ steps.date.outputs.date }}
path: /tmp/kube-vip-test-${{ matrix.mode }}*
@@ -64,7 +64,7 @@ jobs:
- name: Run tests
run: DOCKERTAG=action E2E_KEEP_LOGS=true make service-tests
- name: Save logs
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: services-test-logs-${{ steps.date.outputs.date }}
path: /tmp/kube-vip-service-tests*

View File

@@ -10,7 +10,7 @@ jobs:
- name: Init
run: sudo apt-get update && sudo apt-get install -y build-essential && sudo sysctl fs.inotify.max_user_instances=8192 && sudo sysctl fs.inotify.max_user_watches=524288
- name: Install golangci-lint
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.8
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.7.2
- name: Checkout code
uses: actions/checkout@v6
- name: Install Go

View File

@@ -28,7 +28,7 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Metadata
uses: docker/metadata-action@v5.9.0
uses: docker/metadata-action@v5.10.0
id: metadata
with:
labels: |

View File

@@ -1,13 +1,38 @@
run:
timeout: 10m
version: "2"
linters:
enable:
- bodyclose
- gofmt
- goimports
- revive
- gosec
- misspell
- unconvert
- unparam
- bodyclose
- gosec
- misspell
- unconvert
- unparam
settings:
misspell:
ignore-rules:
- creater
staticcheck:
checks:
- all
# Disable QF1008 to retain embedded fields for better readability.
- "-QF1008"
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:experimental
FROM golang:1.25.4-alpine3.22 as dev
FROM golang:1.25.5-alpine3.23 as dev
RUN apk add --no-cache git ca-certificates make
RUN adduser -D appuser
COPY . /src/

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:experimental
FROM golang:1.25.4-alpine3.22 as dev
FROM golang:1.25.5-alpine3.23 as dev
RUN apk add --no-cache git make
RUN adduser -D appuser
COPY . /src/
@@ -11,7 +11,7 @@ RUN --mount=type=cache,sharing=locked,id=gomod,target=/go/pkg/mod/cache \
--mount=type=cache,sharing=locked,id=goroot,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux make build
FROM alpine:3.22.2
FROM alpine:3.23.0
# Update pkgs and add iptables
RUN apk upgrade && \
apk add --no-cache iptables iptables-legacy

View File

@@ -5,7 +5,7 @@ TARGET := kube-vip
.DEFAULT_GOAL := $(TARGET)
# These will be provided to the target
VERSION := v1.0.2
VERSION := v1.0.3
BUILD := `git rev-parse HEAD`

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
@@ -133,6 +134,7 @@ func init() {
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableNodeLabeling, "enableNodeLabeling", false, fmt.Sprintf("Enable leader node labeling with %q, defaults to false", kubevip.HasIP))
kubeVipCmd.PersistentFlags().StringVar(&initConfig.ServicesLeaseName, "servicesLeaseName", "plndr-svcs-lock", "Name of the lease that is used for leader election for services (in arp mode)")
kubeVipCmd.PersistentFlags().StringVar(&initConfig.DNSMode, "dnsMode", "first", "Name of the mode that DNS lookup will be performed (first, ipv4, ipv6, dual)")
kubeVipCmd.PersistentFlags().StringVar(&initConfig.DHCPMode, "dhcpMode", "", "Mode DHCP resolving will use to obtain IP addresses (ipv4, ipv6, dual)")
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.DisableServiceUpdates, "disableServiceUpdates", false, "If true, kube-vip will process services as usual, but will not update service's Status.LoadBalancer.Ingress slice")
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableEndpoints, "enableEndpoints", false, "If enabled, kube-vip will only advertise services, but will use the (deprecated since v1.33) endpoints for IP addresses")
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.LoInterfaceGlobalScope, "loInterfaceGlobalScope", false, "If true, kube-vip will set global scope when using the lo interface, otherwise a host scope will be used by default")
@@ -483,23 +485,23 @@ func GenerateCidrRange(address string, dnsMode string) (string, error) {
ip := net.ParseIP(a)
if ip == nil {
// we probably are a DNS name
ips, err := utils.LookupHost(a, dnsMode)
ips, err := utils.LookupHost(a, dnsMode, true)
if len(ips) == 0 || err != nil {
return "", fmt.Errorf("invalid IP address: %s from [%s], %v", a, address, err)
}
for _, addr := range ips {
ip = net.ParseIP(addr)
if ip.To4() != nil {
cidrs = append(cidrs, "32")
cidrs = append(cidrs, strconv.Itoa(vip.DefaultMaskIPv4))
} else {
cidrs = append(cidrs, "128")
cidrs = append(cidrs, strconv.Itoa(vip.DefaultMaskIPv6))
}
}
} else {
if ip.To4() != nil {
cidrs = append(cidrs, "32")
cidrs = append(cidrs, strconv.Itoa(vip.DefaultMaskIPv4))
} else {
cidrs = append(cidrs, "128")
cidrs = append(cidrs, strconv.Itoa(vip.DefaultMaskIPv6))
}
}
}

View File

@@ -19,7 +19,7 @@ func main() {
var errorOccurred bool
for {
p := make([]byte, 2048)
conn, err := net.Dial("udp", fmt.Sprintf("%s:%d", *address, *port))
conn, err := net.Dial("udp", net.JoinHostPort(*address, fmt.Sprint(port)))
if err != nil {
if !errorOccurred {
errorTime = time.Now()

20
go.mod
View File

@@ -1,14 +1,14 @@
module github.com/kube-vip/kube-vip
go 1.24.4
go 1.25.5
require (
github.com/cloudflare/ipvs v0.11.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/docker/docker v28.5.2+incompatible
github.com/florianl/go-conntrack v0.4.0
github.com/florianl/go-conntrack v0.5.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.6
github.com/google/go-containerregistry v0.20.7
github.com/google/nftables v0.3.0
github.com/gookit/slog v0.6.0
github.com/huin/goupnp v1.3.0
@@ -21,7 +21,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/vishvananda/netlink v1.3.1
go.etcd.io/etcd/api/v3 v3.6.6
@@ -34,9 +34,9 @@ require (
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.10
k8s.io/api v0.34.2
k8s.io/apimachinery v0.34.2
k8s.io/client-go v0.34.1
k8s.io/api v0.34.3
k8s.io/apimachinery v0.34.3
k8s.io/client-go v0.34.3
k8s.io/klog/v2 v2.130.1
sigs.k8s.io/kind v0.30.0
sigs.k8s.io/yaml v1.6.0
@@ -136,13 +136,13 @@ require (
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect

40
go.sum
View File

@@ -59,8 +59,8 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/florianl/go-conntrack v0.4.0 h1:TlYkxytdwgVayfU0cKwkHurQA0Rd1ZSEBRckRYDUu18=
github.com/florianl/go-conntrack v0.4.0/go.mod h1:iPDx4oIats2T7X7Jm3PFyRCJM1GfZhJaSHOWROYOrE8=
github.com/florianl/go-conntrack v0.5.0 h1:1+749T6cgOgnqXyBtv42fcc1GZK1M9U5P6eCX6yZBGo=
github.com/florianl/go-conntrack v0.5.0/go.mod h1:iPDx4oIats2T7X7Jm3PFyRCJM1GfZhJaSHOWROYOrE8=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
@@ -107,8 +107,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
@@ -164,8 +164,8 @@ github.com/k-sone/critbitgo v1.4.0 h1:l71cTyBGeh6X5ATh6Fibgw3+rtNT80BA0uNNWgkPrb
github.com/k-sone/critbitgo v1.4.0/go.mod h1:7E6pyoyADnFxlUBEKcnfS49b7SUAQGMK+OAp/UQvo0s=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -282,8 +282,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
@@ -373,8 +373,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -397,8 +397,8 @@ golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -453,8 +453,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -491,12 +491,12 @@ gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=

View File

@@ -4,10 +4,12 @@ import (
"context"
"fmt"
"net"
"strconv"
//nolint
"github.com/kube-vip/kube-vip/pkg/kubevip"
"github.com/kube-vip/kube-vip/pkg/vip"
api "github.com/osrg/gobgp/v3/api"
"github.com/kube-vip/kube-vip/pkg/utils"
@@ -74,11 +76,11 @@ func (b *Server) AddPeer(peer kubevip.BGPPeer) (err error) {
return fmt.Errorf("failed to get MP-BGP addresses: %w", err)
}
mask := "128"
mask := strconv.Itoa(vip.DefaultMaskIPv6)
address := ipv4Address
family := api.Family_AFI_IP
if utils.IsIPv4(p.Conf.NeighborAddress) {
mask = "32"
mask = strconv.Itoa(vip.DefaultMaskIPv4)
address = ipv6Address
family = api.Family_AFI_IP6
}
@@ -128,7 +130,7 @@ func (b *Server) getPath(ip net.IP) (path *api.Path) {
//nolint
nlri, _ := anypb.New(&api.IPAddressPrefix{
Prefix: ip.String(),
PrefixLen: 32,
PrefixLen: vip.DefaultMaskIPv4,
})
//nolint
@@ -148,7 +150,7 @@ func (b *Server) getPath(ip net.IP) (path *api.Path) {
//nolint
nlri, _ := anypb.New(&api.IPAddressPrefix{
Prefix: ip.String(),
PrefixLen: 128,
PrefixLen: vip.DefaultMaskIPv6,
})
v6Family := &api.Family{

View File

@@ -54,9 +54,9 @@ func startNetworking(c *kubevip.Config, intfMgr *networkinterface.Manager) ([]vi
networks := []vip.Network{}
for _, addr := range addresses {
network, err := vip.NewConfig(addr, c.Interface, c.LoInterfaceGlobalScope, c.VIPSubnet, c.DDNS, c.RoutingTableID,
c.RoutingTableType, c.RoutingProtocol, c.DNSMode, c.LoadBalancerForwardingMethod, c.IptablesBackend,
c.EnableLoadBalancer, c.EnableServiceSecurity, intfMgr)
network, err := vip.NewConfig(addr, c.Interface, c.LoInterfaceGlobalScope, c.VIPSubnet, c.DDNS, c.DHCPMode,
c.RequireDualStack, c.IsDualStack, c.RoutingTableID, c.RoutingTableType, c.RoutingProtocol, c.DNSMode,
c.LoadBalancerForwardingMethod, c.IptablesBackend, c.EnableLoadBalancer, c.EnableServiceSecurity, intfMgr)
if err != nil {
return nil, err
}

View File

@@ -13,8 +13,8 @@ import (
// dnsUpdater already have the functionality to keep trying resolve the IP
// and update the VIP configuration if it changes
func (cluster *Cluster) StartDDNS(ctx context.Context, network vip.Network) error {
ddnsMgr := vip.NewDDNSManager(ctx, network)
ip, err := ddnsMgr.Start()
ddnsMgr := vip.NewDDNSManager(network)
ip, err := ddnsMgr.Start(ctx)
if err != nil {
return err
}

View File

@@ -8,8 +8,10 @@ import (
"net"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
log "log/slog"
@@ -276,9 +278,28 @@ func (cluster *Cluster) StartLoadBalancerService(ctx context.Context, c *kubevip
var arpWG sync.WaitGroup
log.Debug("StartLoadBalancerService")
log.Debug("StartLoadBalancerService", "networks", len(cluster.Network))
for i := range cluster.Network {
network := cluster.Network[i]
if network.IsDDNS() {
ddnsReady := make(chan struct{})
go func() {
ctxDDNS, ddnsCancel := context.WithCancel(ctx)
defer ddnsCancel()
// start the DDNS if requested
log.Debug("(svcs) start DDNS", "name", network.DNSName())
if err := cluster.StartDDNS(ctxDDNS, cluster.Network[i]); err != nil {
log.Error("failed to start DDNS", "err", err)
}
close(ddnsReady)
<-cluster.stop
}()
<-ddnsReady
}
log.Debug("current ip to process", "ip", network.IP(), "mask", c.VIPSubnet)
if err := network.SetMask(c.VIPSubnet); err != nil {
log.Error("failed to set mask", "subnet", c.VIPSubnet, "err", err)
@@ -322,6 +343,20 @@ func (cluster *Cluster) StartLoadBalancerService(ctx context.Context, c *kubevip
}
go func() {
for i := range cluster.Network {
network := cluster.Network[i]
ctxDNS, dnsCancel := context.WithCancel(ctx)
defer dnsCancel()
// start the dns updater if address is dns
if network.IsDNS() {
log.Info("(svcs) starting the DNS updater", "address", network.DNSName(), "ip", network.IP())
ipUpdater := vip.NewIPUpdater(network)
ipUpdater.Run(ctxDNS)
}
}
<-cluster.stop
// Stop the Arp context if it is running
cancelArp()
@@ -387,16 +422,14 @@ func (cluster *Cluster) StartLoadBalancerService(ctx context.Context, c *kubevip
// Layer2Update, handles the creation of the
func (cluster *Cluster) layer2Update(ctx context.Context, network vip.Network, c *kubevip.Config, arpWG *sync.WaitGroup) {
defer arpWG.Done()
log.Info("layer 2 broadcaster starting")
var ndp *vip.NdpResponder
var err error
ipString := network.IP()
if utils.IsIPv6(ipString) {
if network.IPisLinkLocal() {
log.Error("layer2 is link-local can't use NDP", "address", ipString)
} else {
ndp, err = vip.NewNDPResponder(network.Interface())
ndp, err = waitNDPResponder(ctx, network.Interface())
if err != nil {
log.Error("failed to create new NDP Responder", "error", err)
} else {
@@ -407,6 +440,7 @@ func (cluster *Cluster) layer2Update(ctx context.Context, network vip.Network, c
}
}
log.Info("layer 2 broadcaster starting", "IP", network.IP(), "device", network.Interface())
log.Debug("layer 2 update", "ip", ipString, "interface", network.Interface(), "ms", c.ArpBroadcastRate)
arpInstance := arp.NewInstance(network, ndp)
@@ -416,3 +450,30 @@ func (cluster *Cluster) layer2Update(ctx context.Context, network vip.Network, c
log.Debug("ending layer 2 update", "ip", ipString, "interface", network.Interface(), "ms", c.ArpBroadcastRate)
cluster.arpMgr.RemoveOnLeadershipLoss(arpInstance)
}
func waitNDPResponder(ctx context.Context, ifaceName string) (*vip.NdpResponder, error) {
ndp, err := vip.NewNDPResponder(ifaceName)
if err != nil && strings.Contains(err.Error(), "no such device") {
log.Warn("unable to create NDP responder at first try", "interface", ifaceName, "err", err)
ndpCreateCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
ticker := time.NewTicker(time.Second)
for {
select {
case <-ndpCreateCtx.Done():
return nil, fmt.Errorf("failed to create NDP responder for interface %q: %w", ifaceName, ndpCreateCtx.Err())
case <-ticker.C:
ndp, err = vip.NewNDPResponder(ifaceName)
if err != nil {
log.Warn("unable to create NDP responder on retry", "interface", ifaceName, "err", err)
} else {
return ndp, nil
}
}
}
} else if err != nil {
return nil, fmt.Errorf("unable to create NDP responder for interface %q: %w", ifaceName, err)
}
return ndp, nil
}

View File

@@ -37,5 +37,5 @@ func FindIPAddress(addrName string) (string, string, error) {
}
}
return "", "", fmt.Errorf("Unknown interface [%s]", addrName)
return "", "", fmt.Errorf("unknown interface [%s]", addrName)
}

View File

@@ -1,13 +1,14 @@
package instance
import (
"context"
"fmt"
"net"
"slices"
"strconv"
"strings"
"time"
"log/slog"
log "log/slog"
"github.com/vishvananda/netlink"
@@ -31,18 +32,24 @@ type Instance struct {
Clusters []*cluster.Cluster
// Service uses DHCP
IsDHCP bool
IsDHCPv4 bool
IsDHCPv6 bool
DHCPInterface string
DHCPInterfaceHwaddr string
DHCPInterfaceIP string
DHCPInterfaceIPv4 string
DHCPInterfaceIPv6 string
DHCPHostname string
DHCPClient *vip.DHCPClient
DHCPv4Client vip.DHCPClient
DHCPv6Client vip.DHCPClient
// External Gateway IP the service is forwarded from
UPNPGatewayIPs []string
// Kubernetes service mapping
ServiceSnapshot *v1.Service
dnsAddresses []string
}
type Port struct {
@@ -50,12 +57,14 @@ type Port struct {
Type string
}
func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterface.Manager, arpMgr *arp.Manager) (*Instance, error) {
instanceAddresses, _ := FetchServiceAddresses(svc)
func NewInstance(ctx context.Context, svc *v1.Service, config *kubevip.Config, intfMgr *networkinterface.Manager, arpMgr *arp.Manager) (*Instance, error) {
instanceAddresses, instanceHostnames := FetchServiceAddresses(svc)
log.Info("NewInstance used", "instanceAddresses", instanceAddresses, "instanceHostnames", instanceHostnames)
var newVips []*kubevip.Config
var link netlink.Link
var err error
var dnsAddresses []string
for _, address := range instanceAddresses {
// Detect if we're using a specific interface for services
@@ -131,7 +140,7 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
if cidrs[0] != "" && cidrs[0] != kubevip.Auto {
subnet = cidrs[0]
} else {
subnet = "32"
subnet = strconv.Itoa(vip.DefaultMaskIPv4)
}
}
} else {
@@ -144,7 +153,7 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
if len(cidrs) > 1 && cidrs[1] != "" && cidrs[1] != kubevip.Auto {
subnet = cidrs[1]
} else {
subnet = "128"
subnet = strconv.Itoa(vip.DefaultMaskIPv6)
}
}
}
@@ -164,6 +173,7 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
ArpBroadcastRate: config.ArpBroadcastRate,
EnableServiceSecurity: config.EnableServiceSecurity,
DNSMode: config.DNSMode,
DHCPMode: config.DHCPMode,
DisableServiceUpdates: config.DisableServiceUpdates,
EnableServicesElection: config.EnableServicesElection,
PreserveVIPOnLeadershipLoss: config.PreserveVIPOnLeadershipLoss,
@@ -173,22 +183,78 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
})
}
for _, hostname := range instanceHostnames {
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
// If it is still blank then use the
if svcInterface == "" {
switch config.ServicesInterface {
case "":
svcInterface = config.Interface
default:
svcInterface = config.ServicesInterface
}
}
if link == nil {
if link, err = netlink.LinkByName(svcInterface); err != nil {
return nil, fmt.Errorf("failed to get interface %s: %w", svcInterface, err)
}
if link == nil {
return nil, fmt.Errorf("failed to get interface %s", svcInterface)
}
}
// Generate new Virtual IP configuration
newVips = append(newVips, &kubevip.Config{
VIP: hostname,
Interface: svcInterface,
SingleNode: true,
EnableARP: config.EnableARP,
EnableBGP: config.EnableBGP,
VIPSubnet: config.VIPSubnet,
EnableRoutingTable: config.EnableRoutingTable,
RoutingTableID: config.RoutingTableID,
RoutingTableType: config.RoutingTableType,
RoutingProtocol: config.RoutingProtocol,
ArpBroadcastRate: config.ArpBroadcastRate,
EnableServiceSecurity: config.EnableServiceSecurity,
DNSMode: config.DNSMode,
DHCPMode: config.DHCPMode,
DisableServiceUpdates: config.DisableServiceUpdates,
EnableServicesElection: config.EnableServicesElection,
KubernetesLeaderElection: kubevip.KubernetesLeaderElection{
EnableLeaderElection: config.EnableLeaderElection,
},
})
}
// Create new service
instance := &Instance{
//UID: instanceUID,
//VIPs: instanceAddresses,
ServiceSnapshot: svc,
dnsAddresses: dnsAddresses,
}
// for _, port := range svc.Spec.Ports {
// instance.ExternalPorts = append(instance.ExternalPorts, Port{
// Port: uint16(port.Port), //nolint
// Type: string(port.Protocol),
// })
// }
if svc.Annotations != nil {
instance.DHCPInterfaceHwaddr = svc.Annotations[kubevip.HwAddrKey]
instance.DHCPInterfaceIP = svc.Annotations[kubevip.RequestedIP]
requestedIP := svc.Annotations[kubevip.RequestedIP]
if requestedIP != "" {
requestedIPs := strings.Split(requestedIP, ",")
if len(requestedIPs) > 2 {
return nil, fmt.Errorf("annotation %q cannot request more than one IPv4 and one Ipv6 address", kubevip.RequestedIP)
}
for _, ip := range requestedIPs {
netip := net.ParseIP(ip)
if netip.To4() != nil {
instance.DHCPInterfaceIPv4 = ip
} else {
instance.DHCPInterfaceIPv6 = ip
}
}
}
instance.DHCPHostname = svc.Annotations[kubevip.LoadbalancerHostname]
}
@@ -212,29 +278,82 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
// Create Add configuration to the new service
instance.VIPConfigs = newVips
// If this was purposely created with the address 0.0.0.0,
// If this was purposely created with the address '0.0.0.0', or '::'
// we will create a macvlan on the main interface and a DHCP client
// TODO: Consider how best to handle DHCP with multiple addresses
if len(instanceAddresses) == 1 && instanceAddresses[0] == "0.0.0.0" {
err := instance.startDHCP()
if err != nil {
return nil, err
}
select {
case err := <-instance.DHCPClient.ErrorChannel():
return nil, fmt.Errorf("error starting DHCP for %s/%s: error: %s",
instance.ServiceSnapshot.Namespace, instance.ServiceSnapshot.Name, err)
case ip := <-instance.DHCPClient.IPChannel():
instance.VIPConfigs[0].Interface = instance.DHCPInterface
instance.VIPConfigs[0].VIP = ip
instance.DHCPInterfaceIP = ip
}
if len(instanceAddresses) > 2 && (slices.Contains(instanceAddresses, "0.0.0.0") || slices.Contains(instanceAddresses, "::")) {
return nil, fmt.Errorf("DHCP cannot be used if more than 2 addresses (one IPv4 and one IPv6) were specified")
}
for i := range instance.VIPConfigs {
if instance.VIPConfigs[i].VIP == "0.0.0.0" {
err := instance.startDHCP(ctx, i)
if err != nil {
return nil, err
}
select {
case err := <-instance.DHCPv4Client.ErrorChannel():
return nil, fmt.Errorf("error starting DHCPv4 for %s/%s: error: %s",
instance.ServiceSnapshot.Namespace, instance.ServiceSnapshot.Name, err)
case ip := <-instance.DHCPv4Client.IPChannel():
instance.VIPConfigs[i].Interface = instance.DHCPInterface
instance.VIPConfigs[i].VIP = ip
instance.DHCPInterfaceIPv4 = ip
}
}
if instance.VIPConfigs[i].VIP == "::" {
err := instance.startDHCP(ctx, i)
if err != nil {
return nil, err
}
select {
case err := <-instance.DHCPv6Client.ErrorChannel():
return nil, fmt.Errorf("error starting DHCPv6 for %s/%s: error: %s",
instance.ServiceSnapshot.Namespace, instance.ServiceSnapshot.Name, err)
case ip := <-instance.DHCPv6Client.IPChannel():
instance.VIPConfigs[i].Interface = instance.DHCPInterface
instance.VIPConfigs[i].VIP = ip
instance.DHCPInterfaceIPv6 = ip
}
}
for _, vipConfig := range instance.VIPConfigs {
c, err := cluster.InitCluster(vipConfig, false, intfMgr, arpMgr)
ddnsAnnotation, exists := svc.Annotations[kubevip.ServiceDDNS]
if exists {
instance.VIPConfigs[i].DDNS, err = strconv.ParseBool(ddnsAnnotation)
if err != nil {
log.Error("Failed to add service", "err", err)
return nil, err
}
}
if len(svc.Spec.IPFamilies) > 0 {
if len(svc.Spec.IPFamilies) > 1 {
instance.VIPConfigs[i].DHCPMode = utils.DualFamily
instance.VIPConfigs[i].DNSMode = utils.DualFamily
switch *svc.Spec.IPFamilyPolicy {
case v1.IPFamilyPolicyRequireDualStack:
instance.VIPConfigs[i].IsDualStack = true
instance.VIPConfigs[i].RequireDualStack = true
case v1.IPFamilyPolicyPreferDualStack:
instance.VIPConfigs[i].IsDualStack = true
instance.VIPConfigs[i].RequireDualStack = false
default:
instance.VIPConfigs[i].IsDualStack = false
instance.VIPConfigs[i].RequireDualStack = false
}
} else {
if strings.EqualFold(string(svc.Spec.IPFamilies[0]), utils.IPv4Family) {
instance.VIPConfigs[i].DHCPMode = strings.ToLower(utils.IPv4Family)
instance.VIPConfigs[i].DNSMode = strings.ToLower(utils.IPv4Family)
} else {
instance.VIPConfigs[i].DHCPMode = strings.ToLower(utils.IPv6Family)
instance.VIPConfigs[i].DNSMode = strings.ToLower(utils.IPv6Family)
}
}
}
c, err := cluster.InitCluster(instance.VIPConfigs[i], false, intfMgr, arpMgr)
if err != nil {
log.Error("Failed to add Service %s/%s", svc.Namespace, svc.Name)
log.Error("failed to add service", "err", err)
return nil, err
}
@@ -243,7 +362,7 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
}
instance.Clusters = append(instance.Clusters, c)
log.Info("(svcs) adding VIP", "ip", vipConfig.VIP, "interface", vipConfig.Interface, "namespace", svc.Namespace, "name", svc.Name)
log.Info("(svcs) adding VIP", "ip", instance.VIPConfigs[i].VIP, "interface", instance.VIPConfigs[i].Interface, "namespace", svc.Namespace, "name", svc.Name)
}
return instance, nil
@@ -306,11 +425,11 @@ func getAutoInterfaceName(link netlink.Link, defaultInterface string) string {
return link.Attrs().Name
}
func (i *Instance) startDHCP() error {
if len(i.VIPConfigs) != 1 {
return fmt.Errorf("DHCP requires exactly 1 VIP config, got: %v", len(i.VIPConfigs))
func (i *Instance) startDHCP(ctx context.Context, index int) error {
if len(i.VIPConfigs) > 2 {
return fmt.Errorf("DHCP can be used with 2 VIP config maximally, got: %v", len(i.VIPConfigs))
}
parent, err := netlink.LinkByName(i.VIPConfigs[0].Interface)
parent, err := netlink.LinkByName(i.VIPConfigs[index].Interface)
if err != nil {
return fmt.Errorf("error finding VIP Interface, for building DHCP Link : %v", err)
}
@@ -361,34 +480,61 @@ func (i *Instance) startDHCP() error {
log.Info("Using existing macvlan interface for DHCP", "interface", interfaceName)
}
// Default rp_filter setting (https://github.com/kube-vip/kube-vip/issues/1170)
rpfilterSetting := "0"
var initRebootFlag bool
ip := net.ParseIP(i.VIPConfigs[index].VIP)
// Check if we need to set an override rp_filter value for the interface
if i.ServiceSnapshot.Annotations[kubevip.RPFilter] != "" {
// Check the rp_filter value
rpFilter, err := strconv.Atoi(i.ServiceSnapshot.Annotations[kubevip.RPFilter])
if err != nil {
slog.Error("[DHCP] unable to process rp_filter", "value", rpFilter)
} else {
if rpFilter >= 0 && rpFilter < 3 { // Ensure the value is 0,1,2
rpfilterSetting = i.ServiceSnapshot.Annotations[kubevip.RPFilter]
var client vip.DHCPClient
if ip.To4() != nil {
// Default rp_filter setting (https://github.com/kube-vip/kube-vip/issues/1170)
rpfilterSetting := "0"
// Check if we need to set an override rp_filter value for the interface
if i.ServiceSnapshot.Annotations[kubevip.RPFilter] != "" {
// Check the rp_filter value
rpFilter, err := strconv.Atoi(i.ServiceSnapshot.Annotations[kubevip.RPFilter])
if err != nil {
log.Error("[DHCP] unable to process rp_filter", "value", rpFilter)
} else {
slog.Error("[DHCP] rp_filter value not within range 0-2", "value", rpFilter)
if rpFilter >= 0 && rpFilter < 3 { // Ensure the value is 0,1,2
rpfilterSetting = i.ServiceSnapshot.Annotations[kubevip.RPFilter]
} else {
log.Error("[DHCP] rp_filter value not within range 0-2", "value", rpFilter)
}
}
}
}
err = sysctl.WriteProcSys("/proc/sys/net/ipv4/conf/"+interfaceName+"/rp_filter", rpfilterSetting)
if err != nil {
slog.Error("[DHCP] unable to write rp_filter", "value", rpfilterSetting, "err", err)
}
var initRebootFlag bool
if i.DHCPInterfaceIP != "" {
initRebootFlag = true
}
err = sysctl.WriteProcSys("/proc/sys/net/ipv4/conf/"+interfaceName+"/rp_filter", rpfilterSetting)
if err != nil {
log.Error("[DHCP] unable to write rp_filter", "value", rpfilterSetting, "err", err)
}
client := vip.NewDHCPClient(iface, initRebootFlag, i.DHCPInterfaceIP)
if i.DHCPInterfaceIPv4 != "" {
initRebootFlag = true
}
client = vip.NewDHCPv4Client(iface, initRebootFlag, i.DHCPInterfaceIPv4)
// Add the client so that we can call it to stop function
i.DHCPv4Client = client
// Set that DHCPv4 is enabled
i.IsDHCPv4 = true
} else {
if i.DHCPInterfaceIPv6 != "" {
initRebootFlag = true
}
client, err = vip.NewDHCPv6Client(iface, parent, initRebootFlag, i.DHCPInterfaceIPv6)
if err != nil {
return fmt.Errorf("unable to create client: %w", err)
}
// Add the client so that we can call it to stop function
i.DHCPv6Client = client
// Set that DHCPv6 is enabled
i.IsDHCPv6 = true
}
// Add hostname to dhcp client if annotated
if i.DHCPHostname != "" {
@@ -396,15 +542,15 @@ func (i *Instance) startDHCP() error {
client.WithHostName(i.DHCPHostname)
}
go client.Start()
go func() {
if err := client.Start(ctx); err != nil {
log.Error("[instance] DHCP client error: %w")
}
}()
// Set that DHCP is enabled
i.IsDHCP = true
// Set the name of the interface so that it can be removed on Service deletion
i.DHCPInterface = interfaceName
i.DHCPInterfaceHwaddr = iface.HardwareAddr.String()
// Add the client so that we can call it to stop function
i.DHCPClient = client
return nil
}

View File

@@ -57,7 +57,14 @@ const (
// Enable UPNP on a Service
UpnpEnabled = "kube-vip.io/forwardUPNP"
// Set the UPNP lease duration for a specific service using duration format (e.g., "30s", "1h")
UpnpLeaseDuration = "kube-vip.io/upnp-lease-duration"
RPFilter = "kube-vip.io/rp_filter" // Set the return path filter for a specific service interface
// Name of the service lease object
ServiceLease = "kube-vip.io/leaseName"
// Enable DDNS for the service
ServiceDDNS = "kube-vip.io/ddns"
)

View File

@@ -11,6 +11,7 @@ import (
"strings"
"github.com/kube-vip/kube-vip/pkg/detector"
"github.com/kube-vip/kube-vip/pkg/utils"
"sigs.k8s.io/yaml"
)
@@ -379,6 +380,18 @@ func ParseEnvironment(c *Config) error {
c.DNSMode = env
}
// DHCP mode
env = os.Getenv(dhcpMode)
if env != "" {
c.DHCPMode = env
} else {
if c.DNSMode != "first" {
c.DHCPMode = c.DNSMode
} else {
c.DHCPMode = strings.ToLower(utils.IPv4Family)
}
}
// Disable updates for services (status.LoadBalancer.Ingress will not be updated)
env = os.Getenv(disableServiceUpdates)
if env != "" {
@@ -878,6 +891,11 @@ func mergeConfigValues(baseConfig, fileConfig *Config) {
baseConfig.DNSMode = fileConfig.DNSMode
}
// DHCP configuration
if baseConfig.DHCPMode == "" && fileConfig.DHCPMode != "" {
baseConfig.DHCPMode = fileConfig.DHCPMode
}
// Health check configuration
if baseConfig.HealthCheckPort == 0 && fileConfig.HealthCheckPort != 0 {
baseConfig.HealthCheckPort = fileConfig.HealthCheckPort

View File

@@ -212,6 +212,9 @@ const (
// dnsMode defines mode that DNS lookup will be performed with (first, ipv4, ipv6, dual)
dnsMode = "dns_mode"
// dhcpMode defines mode that DHCP lookup will be performed with (ipv4, ipv6, dual)
dhcpMode = "dhcp_mode"
// disableServiceUpdates disables service updating
disableServiceUpdates = "disable_service_updates"

View File

@@ -170,7 +170,7 @@ func GenerateRoleBinding(rolebinding bool, saCfg *applyCoreV1.ServiceAccountAppl
func generatePodSpec(c *Config, image, imageVersion string, inCluster bool) *corev1.Pod {
imageRef, err := name.NewTag(image, name.WeakValidation, name.WithDefaultTag(imageVersion))
if err != nil {
panic(fmt.Errorf("Cannot parse %q: %w", image, err))
panic(fmt.Errorf("cannot parse %q: %w", image, err))
}
command := "manager"
@@ -255,6 +255,17 @@ func generatePodSpec(c *Config, image, imageVersion string, inCluster bool) *cor
newEnvironment = append(newEnvironment, dnsModeSelector...)
}
if c.DHCPMode != "" {
// build environment variables
dhcpModeSelector := []corev1.EnvVar{
{
Name: dhcpMode,
Value: c.DHCPMode,
},
}
newEnvironment = append(newEnvironment, dhcpModeSelector...)
}
// If we're doing the hybrid mode
if c.EnableControlPlane {
cp := []corev1.EnvVar{

View File

@@ -158,6 +158,16 @@ type Config struct {
// DNSMode, this will set the mode DSN lookup will be performed (first, ipv4, ipv6, dual)
DNSMode string `yaml:"dnsDualStackMode"`
// IsDualStack reports if service is DualStack.
IsDualStack bool
// RequireDualStack defines if DualStack is required for the service. Based on service's Spec.ipFamilyPolicy field.
RequireDualStack bool
// DNSMode, this will set the mode DHCP lookup will be performed for DDNS (ipv4, ipv6, dual). By default will be the same as DNSMode.
// If DNSMode is 'first', IPv4 will be used.
DHCPMode string `yaml:"dhcpDualStackMode"`
// DisableServiceUpdates, if true, kube-vip will only advertise service, but it will not update service's Status.LoadBalancer.Ingress slice
DisableServiceUpdates bool `yaml:"disableServiceUpdates"`

View File

@@ -17,6 +17,7 @@ import (
"github.com/kube-vip/kube-vip/pkg/backend"
"github.com/kube-vip/kube-vip/pkg/sysctl"
"github.com/kube-vip/kube-vip/pkg/utils"
"github.com/kube-vip/kube-vip/pkg/vip"
"github.com/vishvananda/netlink"
)
@@ -90,9 +91,9 @@ func NewIPVSLB(address string, port uint16, forwardingMethod string, backendHeal
}
}
netMask := netmask.MaskFrom(31, 32) // For ipv4
netMask := netmask.MaskFrom(31, vip.DefaultMaskIPv4) // For ipv4
if family == ipvs.INET6 {
netMask = netmask.MaskFrom(128, 128) // For ipv6
netMask = netmask.MaskFrom(128, vip.DefaultMaskIPv6) // For ipv6
}
// Generate out API Server LoadBalancer instance

View File

@@ -1,6 +1,7 @@
package networkinterface
import (
log "log/slog"
"sync"
"github.com/vishvananda/netlink"
@@ -23,6 +24,12 @@ func NewManager() *Manager {
func (m *Manager) Get(intf netlink.Link) *Link {
if l, ok := m.interfaces[intf.Attrs().Name]; ok {
updated, err := netlink.LinkByName(l.Intf.Attrs().Name)
if err != nil {
log.Error("failed to get interface %q: %w", l.Intf.Attrs().Name, err)
return nil
}
l.Intf = updated
return l
}
result := &Link{

View File

@@ -269,9 +269,9 @@ func (p *Processor) configureEgress(vipIP, podIP, namespace, serviceUUID string,
}
}
mask := "/32"
mask := fmt.Sprintf("/%d", vip.DefaultMaskIPv4)
if !utils.IsIPv4(podIP) {
mask = "/128"
mask = fmt.Sprintf("/%d", vip.DefaultMaskIPv6)
}
if allowedNetworks != "" {

View File

@@ -68,8 +68,6 @@ func (p *Processor) StartServicesLeaderElection(ctx context.Context, service *v1
return fmt.Errorf("failed to get context for service %q with UID %q: nil context", service.Name, service.UID)
}
svcCtx.IsActive = true
svcLease, isNew := p.leaseMgr.Add(service)
// this service is sharing lease
if !isNew {
@@ -83,23 +81,30 @@ func (p *Processor) StartServicesLeaderElection(ctx context.Context, service *v1
<-svcLease.Started
if lease.UsesCommon(service) {
svcCtx.IsActive = true
if err := p.SyncServices(ctx, service); err != nil {
log.Error("service sync", "err", err)
log.Error("service sync", "err", err, "uid", service.UID)
svcLease.Cancel()
}
// just block until context is cancelled
<-ctx.Done()
if svcCtx.IsActive {
if err := p.deleteService(service.UID); err != nil {
log.Error("service deletion", "err", err)
}
}
// wait for service context to finish
<-svcCtx.Ctx.Done()
if svcCtx.IsActive {
if err := p.deleteService(service.UID); err != nil {
log.Error("service deletion", "uid", service.UID, "err", err)
}
}
// wait for leaderelection to be finished
<-svcLease.Ctx.Done()
// Mark this service is inactive
svcCtx.IsActive = false
// wait for leaderelection to be finished
<-svcLease.Ctx.Done()
return nil
}
// start the leader election code loop
@@ -119,16 +124,18 @@ func (p *Processor) StartServicesLeaderElection(ctx context.Context, service *v1
OnStartedLeading: func(ctx context.Context) {
// Mark this service as active (as we've started leading)
// we run this in background as it's blocking
svcCtx.IsActive = true
if err := p.SyncServices(ctx, service); err != nil {
log.Error("service sync", "err", err)
log.Error("service sync", "uid", service.UID, "err", err)
svcLease.Cancel()
}
close(svcLease.Started)
},
OnStoppedLeading: func() {
// we can do cleanup here
log.Info("leadership lost", "service", service.Name, "leader", p.config.NodeName)
log.Info("leadership lost", "service", service.Name, "uid", service.UID, "leader", p.config.NodeName)
if svcCtx.IsActive {
log.Debug("DELETING LEADER", "uid", service.UID)
if err := p.deleteService(service.UID); err != nil {
log.Error("service deletion", "err", err)
}
@@ -142,10 +149,10 @@ func (p *Processor) StartServicesLeaderElection(ctx context.Context, service *v1
// I just got the lock
return
}
log.Info("new leader", "leader", identity)
log.Info("new leader", "leader", identity, "service", service.Name, "uid", service.UID)
},
},
})
log.Info("stopping leader election", "service", service.Name)
log.Info("stopping leader election", "service", service.Name, "uid", service.UID)
return nil
}

View File

@@ -116,7 +116,7 @@ func (p *Processor) AddOrModify(ctx context.Context, event watch.Event, serviceF
svcAddresses, svcHostnames := instance.FetchServiceAddresses(svc)
// We only care about LoadBalancer services that have been allocated an address
if len(svcAddresses) <= 0 {
if len(svcAddresses) <= 0 && len(svcHostnames) <= 0 {
return true, nil
}
@@ -134,7 +134,7 @@ func (p *Processor) AddOrModify(ctx context.Context, event watch.Event, serviceF
// The modified event should only be triggered if the service has been modified (i.e. moved somewhere else)
if event.Type == watch.Modified {
i := instance.FindServiceInstance(svc, p.ServiceInstances)
shouldGarbageCollect := true
shouldGarbageCollect := false
if i != nil {
originalServiceAddresses, originalServiceHostnames := instance.FetchServiceAddresses(i.ServiceSnapshot)
shouldGarbageCollect =
@@ -143,7 +143,13 @@ func (p *Processor) AddOrModify(ctx context.Context, event watch.Event, serviceF
// Service hostnames changed
!reflect.DeepEqual(originalServiceHostnames, svcHostnames) ||
// ExternalTrafficPolicy changed
svc.Spec.ExternalTrafficPolicy != i.ServiceSnapshot.Spec.ExternalTrafficPolicy
svc.Spec.ExternalTrafficPolicy != i.ServiceSnapshot.Spec.ExternalTrafficPolicy ||
// IP stack configuration changed
!reflect.DeepEqual(svc.Spec.IPFamilies, i.ServiceSnapshot.Spec.IPFamilies) ||
*svc.Spec.IPFamilyPolicy != *i.ServiceSnapshot.Spec.IPFamilyPolicy ||
// DDNS was disabled/enabled
svc.Annotations[kubevip.ServiceDDNS] != i.ServiceSnapshot.Annotations[kubevip.ServiceDDNS]
}
if shouldGarbageCollect {
for _, addr := range svcAddresses {
@@ -276,7 +282,10 @@ func (p *Processor) AddOrModify(ctx context.Context, event watch.Event, serviceF
log.Error(err.Error())
}
}
svcCtx.IsActive = true
if !p.config.EnableServicesElection {
log.Debug("Service now active", "name", svc.Name, "uid", svc.UID)
svcCtx.IsActive = true
}
}
return false, nil
@@ -312,15 +321,17 @@ func (p *Processor) Delete(event watch.Event) (bool, error) {
}
}
// If this is an active service then and additional leaderElection will handle stopping
err = p.deleteService(svc.UID)
if err != nil {
log.Error(err.Error())
if svcCtx.IsActive && !p.config.EnableServicesElection {
// If this is an active service then and additional leaderElection will handle stopping
err = p.deleteService(svc.UID)
if err != nil {
log.Error(err.Error())
}
svcCtx.IsActive = false
}
// Calls the cancel function of the context
log.Warn("(svcs) The load balancer was deleted, cancelling context")
svcCtx.IsActive = false
svcCtx.Cancel()
log.Warn("(svcs) waiting for load balancer to finish")
<-svcCtx.Ctx.Done()

View File

@@ -3,6 +3,7 @@ package services
import (
"context"
"fmt"
"net"
"slices"
"strings"
"time"
@@ -34,10 +35,13 @@ const (
ActionDelete ServiceInstanceAction = "delete"
ActionAdd ServiceInstanceAction = "add"
ActionNone ServiceInstanceAction = "none"
// Default UPNP lease requested is 3600 seconds.
defaultUPNPLeaseDuration = 1 * time.Hour
)
func (p *Processor) SyncServices(ctx context.Context, svc *v1.Service) error {
log.Debug("[STARTING] Service Sync", "namespace", svc.Namespace, "name", svc.Name)
log.Debug("[STARTING] Service Sync", "namespace", svc.Namespace, "name", svc.Name, "uid", svc.UID)
// Iterate through the synchronising services
@@ -83,16 +87,30 @@ func (p *Processor) getServiceInstanceAction(svc *v1.Service) ServiceInstanceAct
if instance != nil && instance.ServiceSnapshot.UID == svc.UID {
for _, address := range addresses {
// handle the case where the service instance needs to be deleted
if instance.IsDHCP {
if address != "0.0.0.0" && address != "::" {
if instance.IsDHCPv4 {
if address != "0.0.0.0" {
return ActionDelete
}
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(statusAddresses, instance.DHCPInterfaceIP) {
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(statusAddresses, instance.DHCPInterfaceIPv4) {
return ActionDelete
}
} else {
if address == "0.0.0.0" {
return ActionDelete
}
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(statusAddresses, address) {
return ActionDelete
}
}
if !instance.IsDHCP {
if address == "0.0.0.0" || address == "::" {
if instance.IsDHCPv6 {
if address != "::" {
return ActionDelete
}
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(statusAddresses, instance.DHCPInterfaceIPv6) {
return ActionDelete
}
} else {
if address == "::" {
return ActionDelete
}
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(statusAddresses, address) {
@@ -108,7 +126,7 @@ func (p *Processor) getServiceInstanceAction(svc *v1.Service) ServiceInstanceAct
}
}
if len(addresses) > 0 || len(hostnames) > 0 {
log.Debug("no matching service instance found", "service", svc.Name, "namespace", svc.Namespace, "addresses", addresses, "hostnames", hostnames)
log.Debug("no matching service instance found", "service", svc.Name, "namespace", svc.Namespace, "uid", svc.UID, "addresses", addresses, "hostnames", hostnames)
return ActionAdd // If no matching instance is found, we need to add a new service instance
}
return ActionNone
@@ -137,31 +155,72 @@ func (p *Processor) addService(ctx context.Context, svc *v1.Service) error {
startTime := time.Now()
newService, err := instance.NewInstance(svc, p.config, p.intfMgr, p.arpMgr)
newService, err := instance.NewInstance(ctx, svc, p.config, p.intfMgr, p.arpMgr)
if err != nil {
return err
}
for x := range newService.VIPConfigs {
log.Debug("starting loadbalancer for service", "name", svc.Name, "namespace", svc.Namespace)
log.Debug("starting loadbalancer for service", "name", svc.Name, "namespace", svc.Namespace, "uid", svc.UID)
newService.Clusters[x].StartLoadBalancerService(ctx, newService.VIPConfigs[x], p.bgpServer, svc.Name, p.CountRouteReferences)
}
p.upnpMap(ctx, newService)
if newService.IsDHCP && len(newService.VIPConfigs) == 1 {
if newService.IsDHCPv4 {
go func() {
for ip := range newService.DHCPClient.IPChannel() {
log.Debug("IP changed", "ip", ip)
newService.VIPConfigs[0].VIP = ip
newService.DHCPInterfaceIP = ip
if !p.config.DisableServiceUpdates {
if err := p.updateStatus(newService); err != nil {
log.Warn("updating svc", "err", err)
}
index := -1
for i := range newService.VIPConfigs {
ip := net.ParseIP(newService.VIPConfigs[i].VIP)
if ip.To4() != nil {
index = i
break
}
}
log.Debug("IP update channel closed, stopping")
if index == -1 {
log.Error("unable to find proper VIPConfig for the DHCPv4")
} else {
for ip := range newService.DHCPv4Client.IPChannel() {
log.Debug("IP changed", "ip", ip)
newService.VIPConfigs[index].VIP = ip
newService.DHCPInterfaceIPv4 = ip
if !p.config.DisableServiceUpdates {
if err := p.updateStatus(newService); err != nil {
log.Warn("updating svc", "err", err)
}
}
}
log.Debug("IPv4 update channel closed, stopping")
}
}()
}
if newService.IsDHCPv6 {
go func() {
index := -1
for i := range newService.VIPConfigs {
ip := net.ParseIP(newService.VIPConfigs[i].VIP)
if ip.To4() == nil {
index = i
break
}
}
if index == -1 {
log.Error("unable to find proper VIPConfig for the DHCPv6")
} else {
for ip := range newService.DHCPv4Client.IPChannel() {
log.Debug("IP changed", "ip", ip)
newService.VIPConfigs[index].VIP = ip
newService.DHCPInterfaceIPv6 = ip
if !p.config.DisableServiceUpdates {
if err := p.updateStatus(newService); err != nil {
log.Warn("updating svc", "err", err)
}
}
}
log.Debug("IPv6 update channel closed, stopping")
}
}()
}
@@ -270,7 +329,7 @@ func (p *Processor) deleteService(uid types.UID) error {
var serviceInstance *instance.Instance
found := false
for x := range p.ServiceInstances {
log.Debug("[service] lookup", "target UID", uid, "found UID ", p.ServiceInstances[x].ServiceSnapshot.UID, "name", p.ServiceInstances[x].ServiceSnapshot.Name, "namespace", p.ServiceInstances[x].ServiceSnapshot.Namespace)
log.Debug("[service] lookup", "target UID", uid, "found UID", p.ServiceInstances[x].ServiceSnapshot.UID, "name", p.ServiceInstances[x].ServiceSnapshot.Name, "namespace", p.ServiceInstances[x].ServiceSnapshot.Namespace)
// Add the running services to the new array
if p.ServiceInstances[x].ServiceSnapshot.UID != uid {
updatedInstances = append(updatedInstances, p.ServiceInstances[x])
@@ -295,7 +354,7 @@ func (p *Processor) deleteService(uid types.UID) error {
}
}
// Determine if this this VIP is shared with other loadbalancers
// Determine if this VIP is shared with other loadbalancers
shared := false
vipSet := make(map[string]interface{})
for x := range updatedInstances {
@@ -314,8 +373,15 @@ func (p *Processor) deleteService(uid types.UID) error {
for x := range serviceInstance.Clusters {
serviceInstance.Clusters[x].Stop()
}
if serviceInstance.IsDHCP {
serviceInstance.DHCPClient.Stop()
if serviceInstance.IsDHCPv4 || serviceInstance.IsDHCPv6 {
if serviceInstance.IsDHCPv4 {
serviceInstance.DHCPv4Client.Stop()
}
if serviceInstance.IsDHCPv6 {
serviceInstance.DHCPv6Client.Stop()
}
macvlan, err := netlink.LinkByName(serviceInstance.DHCPInterface)
if err != nil {
return fmt.Errorf("[service] error finding VIP Interface: %v", err)
@@ -358,6 +424,76 @@ func (p *Processor) deleteService(uid types.UID) error {
return nil
}
// upnpLeaseDurationForService determines the UPNP lease duration for a given service, based on its annotations.
//
// The default lease duration is set to 1 hour, maintaining the default of 3600 seconds that was previously passed. If
// the service has an annotation of [kubevip.UpnpLeaseDuration], the function attempts to parse its value as a
// [time.Duration] using [time.ParseDuration].
//
// If parsing is successful, the lease duration is updated accordingly; otherwise, a warning is logged and the default
// duration is retained.
//
// Overriding the default lease duration can be useful for services that require longer or shorter UPNP port mappings,
// or for buggy UPNP implementations that may not handle renewals correctly. At least one router's implementation
// completely times out the mapping very shortly after creation if it is set to 3600 or 7200, but works fine if 0 is
// used.
//
// This function must therefore explicitly permit duration of 0, and callers and the underlying library must pass that
// value in XML correctly. A duration of 0 indicates to the UPNP gateway that the mapping should be permanent.
//
// It may be useful to update this function to read a global configuration option as well. This helper could also take
// in v1.Service instead of [instance.Instance], but the latter is more convenient for callers.
//
// Example where 0 was observed to stay on the problematic router: miniupnpc's test client, upnpc v2.2.4.
func upnpLeaseDurationForService(s *instance.Instance) time.Duration {
if s == nil || s.ServiceSnapshot == nil || s.ServiceSnapshot.Annotations == nil {
// No warning output. No annotation is unusual but perfectly ok.
return defaultUPNPLeaseDuration
}
// Constant is named `UpnpLeaseDuration` for consistency with `UpnpEnabled`. According to Go naming conventions
// regarding use of acronyms, `UPNPLeaseDuration` would be preferred. Cleanup of both of these is left for a future
// refactor as these are public symbols and might be used elsewhere in the ecosystem.
val, ok := s.ServiceSnapshot.Annotations[kubevip.UpnpLeaseDuration]
if !ok {
// No warning output. No annotation is common and perfectly ok.
return defaultUPNPLeaseDuration
}
if val == "" {
log.Warn("[UPNP] Lease duration annotation is empty, using default of 1 hour", "service", s.ServiceSnapshot.Name)
return defaultUPNPLeaseDuration
}
parsed, err := time.ParseDuration(val)
if err != nil {
log.Warn("[UPNP] Unable to parse lease duration from annotation, using default of 1 hour", "service", s.ServiceSnapshot.Name, "err", err)
return defaultUPNPLeaseDuration
}
if parsed < 0 {
log.Warn("[UPNP] Lease duration from annotation is negative, using default of 1 hour", "service", s.ServiceSnapshot.Name)
return defaultUPNPLeaseDuration
}
return parsed
}
// upnpLeaseDurationForServiceSec returns the UPNP lease duration for a service in uint32 seconds, as expected by the
// helper library. This is a convenience wrapper around [upnpLeaseDurationForService], and in case
// upnpLeaseDurationForService returns a duration that maps to a negative value of seconds or invalid float of seconds,
// it will return the default lease duration in seconds instead. (Technically, it will check for a reasonable range of
// seconds, e.g. ~10 years-ish.)
func upnpLeaseDurationForServiceSec(s *instance.Instance) uint32 {
duration := upnpLeaseDurationForService(s)
seconds := duration.Seconds()
// Check if within range.
if seconds >= 0 && seconds <= float64(10*365*24*60*60) {
return uint32(seconds)
}
return uint32(defaultUPNPLeaseDuration.Seconds())
}
// Set up UPNP forwards for a service
// We first try to use the more modern Pinhole API introduced in UPNPv2 and fall back to UPNPv2 Port Forwarding if no forward was successful
func (p *Processor) upnpMap(ctx context.Context, s *instance.Instance) {
@@ -374,6 +510,10 @@ func (p *Processor) upnpMap(ctx context.Context, s *instance.Instance) {
gateways := upnp.GetGatewayClients(ctx)
// Determine desired UPNP TTL / "lease duration". Passed into the library as integer seconds from now, as the
// underlying XML API wants integer seconds.
leaseDurationSec := upnpLeaseDurationForServiceSec(s)
// Reset Gateway IPs to remove stale addresses
s.UPNPGatewayIPs = make([]string, 0)
@@ -384,9 +524,9 @@ func (p *Processor) upnpMap(ctx context.Context, s *instance.Instance) {
forwardSucessful := false
if gw.WANIPv6FirewallControlClient != nil {
log.Info("[UPNP] Adding map", "vip", vip, "port", port.Port, "service", s.ServiceSnapshot.Name, "gateway", gw.WANIPv6FirewallControlClient.Location)
log.Info("[UPNP] Adding map", "vip", vip, "port", port.Port, "service", s.ServiceSnapshot.Name, "gateway", gw.WANIPv6FirewallControlClient.Location, "leaseDurationSec", leaseDurationSec)
pinholeID, pinholeErr := gw.WANIPv6FirewallControlClient.AddPinholeCtx(ctx, "0.0.0.0", uint16(port.Port), vip, uint16(port.Port), upnp.MapProtocolToIANA(string(port.Protocol)), 3600) //nolint TODO
pinholeID, pinholeErr := gw.WANIPv6FirewallControlClient.AddPinholeCtx(ctx, "0.0.0.0", uint16(port.Port), vip, uint16(port.Port), upnp.MapProtocolToIANA(string(port.Protocol)), leaseDurationSec) //nolint TODO
if pinholeErr == nil {
forwardSucessful = true
log.Info("[UPNP] Service should be accessible externally", "port", port.Port, "pinhold ID", pinholeID)
@@ -397,16 +537,16 @@ func (p *Processor) upnpMap(ctx context.Context, s *instance.Instance) {
}
// Fallback to PortForward
if !forwardSucessful {
log.Info("[UPNP] Adding map", "vip", vip, "port", port.Port, "service", s.ServiceSnapshot.Name)
log.Info("[UPNP] Adding map", "vip", vip, "port", port.Port, "service", s.ServiceSnapshot.Name, "leaseDurationSec", leaseDurationSec)
portMappingErr := gw.ConnectionClient.AddPortMapping("0.0.0.0", uint16(port.Port), strings.ToUpper(string(port.Protocol)), uint16(port.Port), vip, true, s.ServiceSnapshot.Name, 3600) //nolint TODO
portMappingErr := gw.ConnectionClient.AddPortMapping("0.0.0.0", uint16(port.Port), strings.ToUpper(string(port.Protocol)), uint16(port.Port), vip, true, s.ServiceSnapshot.Name, leaseDurationSec) //nolint TODO
if portMappingErr == nil {
ip, err := gw.ConnectionClient.GetExternalIPAddress()
if err != nil {
// Log the error but continue on the off chance the mapping was successful
log.Error("[UPNP] Unable to get external IP address from gateway", "port", port.Port, "err", err)
log.Error("[UPNP] Unable to get external IP address from gateway", "service", s.ServiceSnapshot.Name, "port", port.Port, "err", err)
} else {
log.Info("[UPNP] Service should be accessible externally", "port", port.Port, "externalip", ip)
log.Info("[UPNP] Service should be accessible externally", "service", s.ServiceSnapshot.Name, "port", port.Port, "externalip", ip)
}
forwardSucessful = true
} else {
@@ -431,6 +571,7 @@ func (p *Processor) upnpMap(ctx context.Context, s *instance.Instance) {
}
func (p *Processor) updateStatus(i *instance.Instance) error {
log.Debug("updating status")
// let's retry status update every 10ms for 30s
retryConfig := wait.Backoff{
Steps: 3000,
@@ -457,9 +598,19 @@ func (p *Processor) updateStatus(i *instance.Instance) error {
// Add the current host
currentServiceCopy.Annotations[kubevip.VipHost] = p.config.NodeName
}
if i.DHCPInterfaceHwaddr != "" || i.DHCPInterfaceIP != "" {
if i.DHCPInterfaceHwaddr != "" || i.DHCPInterfaceIPv4 != "" || i.DHCPInterfaceIPv6 != "" {
currentServiceCopy.Annotations[kubevip.HwAddrKey] = i.DHCPInterfaceHwaddr
currentServiceCopy.Annotations[kubevip.RequestedIP] = i.DHCPInterfaceIP
dhcpInterfaceIP := ""
if i.DHCPInterfaceIPv4 != "" {
dhcpInterfaceIP = i.DHCPInterfaceIPv4
if i.DHCPInterfaceIPv6 != "" {
dhcpInterfaceIP += ","
}
}
if i.DHCPInterfaceIPv6 != "" {
dhcpInterfaceIP += i.DHCPInterfaceIPv6
}
currentServiceCopy.Annotations[kubevip.RequestedIP] = dhcpInterfaceIP
}
if currentService.Annotations["development.kube-vip.io/synthetic-api-server-error-on-update"] == "true" {
@@ -487,7 +638,7 @@ func (p *Processor) updateStatus(i *instance.Instance) error {
for _, c := range i.VIPConfigs {
if !utils.IsIP(c.VIP) {
ips, err := utils.LookupHost(c.VIP, p.config.DNSMode)
ips, err := utils.LookupHost(c.VIP, c.DNSMode, *i.ServiceSnapshot.Spec.IPFamilyPolicy == v1.IPFamilyPolicyRequireDualStack)
if err != nil {
return err
}

View File

@@ -0,0 +1,68 @@
package services
import (
"testing"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/kube-vip/kube-vip/pkg/instance"
)
// Test_upnpLeaseDurationForService tests whether the default lease duration is used, and whether the annotation
// overrides it correctly.
//
// For simplicity, this table driven test does not cover all edge cases (passing in nil instance, nil service snapshot,
// nil annotations, etc).
func Test_upnpLeaseDurationForService(t *testing.T) {
const annotation = "kube-vip.io/upnp-lease-duration" // Validating value of kubevip.UpnpLeaseDuration.
tcs := []struct {
name string
annotations map[string]string
want int // in seconds
}{
{
name: "No annotation uses default",
annotations: map[string]string{},
want: 3600,
},
{
name: "Valid annotation overrides default",
annotations: map[string]string{
annotation: "2h",
},
want: 7200,
},
{
name: "Valid short annotation overrides default",
annotations: map[string]string{
annotation: "30m",
},
want: 1800,
},
{
name: "Invalid annotation uses default",
annotations: map[string]string{
annotation: "invalid-duration",
},
want: 3600,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
i := &instance.Instance{
ServiceSnapshot: &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Annotations: tc.annotations,
},
},
}
gotDuration := upnpLeaseDurationForService(i)
got := int(gotDuration.Seconds())
if got != tc.want {
t.Errorf("upnpLeaseDurationForService(%+v) = %v, want %v", tc.annotations, got, tc.want)
}
})
}
}

View File

@@ -2,6 +2,7 @@ package utils
import (
"fmt"
"log/slog"
"net"
"strings"
@@ -15,7 +16,7 @@ const (
)
// LookupHost resolves dnsName and return an IP or an error
func LookupHost(dnsName, dnsMode string) ([]string, error) {
func LookupHost(dnsName, dnsMode string, requireDualStack bool) ([]string, error) {
result, err := net.LookupHost(dnsName)
if err != nil {
return nil, err
@@ -26,7 +27,7 @@ func LookupHost(dnsName, dnsMode string) ([]string, error) {
addrs := []string{}
switch dnsMode {
case strings.ToLower(IPv4Family), strings.ToLower(IPv6Family), DualFamily:
a, err := getIPbyFamily(result, dnsMode)
a, err := getIPbyFamily(result, dnsMode, requireDualStack)
if err != nil {
return nil, err
}
@@ -38,7 +39,7 @@ func LookupHost(dnsName, dnsMode string) ([]string, error) {
return addrs, nil
}
func getIPbyFamily(addresses []string, family string) ([]string, error) {
func getIPbyFamily(addresses []string, family string, requireDualStack bool) ([]string, error) {
var checkers []func(string) bool
families := []string{}
if family == DualFamily || family == strings.ToLower(IPv4Family) {
@@ -54,11 +55,19 @@ func getIPbyFamily(addresses []string, family string) ([]string, error) {
for i, c := range checkers {
addr, err := getIPbyChecker(addresses, c)
if err != nil {
if len(checkers) > 1 && !requireDualStack {
slog.Warn("no address found", "family", families[i])
continue
}
return nil, fmt.Errorf("error getting %s address: %w", families[i], err)
}
addrs = append(addrs, addr)
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no addresses found")
}
return addrs, nil
}

View File

@@ -17,7 +17,7 @@ import (
"golang.org/x/sys/unix"
v1 "k8s.io/api/core/v1"
"github.com/kube-vip/kube-vip/pkg/iptables"
iptables "github.com/kube-vip/kube-vip/pkg/iptables"
"github.com/kube-vip/kube-vip/pkg/kubevip"
"github.com/kube-vip/kube-vip/pkg/utils"
@@ -28,8 +28,9 @@ const (
defaultValidLft = 60
iptablesComment = "%s kube-vip load balancer IP"
iptablesCommentMarkRule = "kube-vip load balancer IP set mark for masquerade"
defaultMaskIPv6 = 128
defaultMaskIPv4 = 32
DefaultMaskIPv4 = 32
DefaultMaskIPv6 = 128
)
// Network is an interface that enable managing operations for a given IP
@@ -56,6 +57,8 @@ type Network interface {
SetHasEndpoints(value bool)
HasEndpoints() bool
ARPName() string
GetPossibleSubnets() string
DHCPFamily() string
}
// network - This allows network configuration
@@ -82,12 +85,18 @@ type network struct {
ipvsEnabled bool
hasEndpoints bool
possibleSubnets string
// used by DHCP to get address of proper family
dhcpFamily string
}
// NewConfig will attempt to provide an interface to the kernel network configuration
func NewConfig(address string, iface string, loGlobalScope bool, subnet string, isDDNS bool, tableID int, tableType int,
routingProtocol int, dnsMode, forwardMethod, iptablesBackend string,
ipvsEnabled, enableSecurity bool, intfMgr *networkinterface.Manager) ([]Network, error) {
func NewConfig(address string, iface string, loGlobalScope bool, subnet string, isDDNS bool,
dhcpMode string, requireDualStack, isDualStack bool, tableID int, tableType int, routingProtocol int,
dnsMode, forwardMethod, iptablesBackend string, ipvsEnabled, enableSecurity bool,
intfMgr *networkinterface.Manager) ([]Network, error) {
networks := []Network{}
link, err := netlink.LinkByName(iface)
@@ -106,6 +115,8 @@ func NewConfig(address string, iface string, loGlobalScope bool, subnet string,
forwardMethod: forwardMethod,
iptablesBackend: iptablesBackend,
ipvsEnabled: ipvsEnabled,
enableSecurity: enableSecurity,
possibleSubnets: subnet,
}
subnet, err = SelectSubnet(address, subnet)
@@ -138,35 +149,50 @@ func NewConfig(address string, iface string, loGlobalScope bool, subnet string,
} else {
// try to resolve the address
log.Debug("looking up host", "address", address, "dnsMode", dnsMode)
ips, err := utils.LookupHost(address, dnsMode)
if err != nil {
ips, err := utils.LookupHost(address, dnsMode, requireDualStack)
if (dnsMode == utils.DualFamily && isDDNS && isDualStack) || err != nil {
// return early for ddns if no IP is allocated for the domain
// when leader starts, should do get IP from DHCP for the domain
if isDDNS {
result := &network{
link: networkLink,
routeTable: tableID,
routingTableType: tableType,
routingProtocol: routingProtocol,
forwardMethod: forwardMethod,
iptablesBackend: iptablesBackend,
isDDNS: isDDNS,
dnsName: address,
ipvsEnabled: ipvsEnabled,
enableSecurity: enableSecurity,
address: &netlink.Addr{ // create placeholder for the address
IPNet: &net.IPNet{}, // that will be added later in the process
Peer: &net.IPNet{},
},
log.Info("isDDNS true", "dhcpMode", dhcpMode)
if strings.EqualFold(dhcpMode, utils.IPv4Family) || strings.EqualFold(dhcpMode, utils.DualFamily) {
result := &network{
link: networkLink,
routeTable: tableID,
routingTableType: tableType,
routingProtocol: routingProtocol,
forwardMethod: forwardMethod,
iptablesBackend: iptablesBackend,
isDDNS: isDDNS,
dnsName: address,
ipvsEnabled: ipvsEnabled,
enableSecurity: enableSecurity,
possibleSubnets: subnet,
dhcpFamily: utils.IPv4Family,
}
networks = append(networks, result)
}
// set address as deprecated so it isn't used as source address according to RFC 3484
result.address.PreferedLft = 0
if strings.EqualFold(dhcpMode, utils.IPv6Family) || strings.EqualFold(dhcpMode, utils.DualFamily) {
result := &network{
link: networkLink,
routeTable: tableID,
routingTableType: tableType,
routingProtocol: routingProtocol,
forwardMethod: forwardMethod,
iptablesBackend: iptablesBackend,
isDDNS: isDDNS,
dnsName: address,
ipvsEnabled: ipvsEnabled,
enableSecurity: enableSecurity,
possibleSubnets: subnet,
dhcpFamily: utils.IPv6Family,
}
// Also set ValidLft so the netlink library actually sets them
result.address.ValidLft = math.MaxInt
networks = append(networks, result)
}
networks = append(networks, result)
return networks, nil
}
return nil, err
@@ -184,15 +210,12 @@ func NewConfig(address string, iface string, loGlobalScope bool, subnet string,
dnsName: address,
ipvsEnabled: ipvsEnabled,
enableSecurity: enableSecurity,
possibleSubnets: subnet,
}
// we're able to resolve store this as the initial IP
subnets := Split(subnet)
s := subnet
if len(subnets) > 1 {
s = selectSubnet(ip, subnets)
s, err := SelectSubnet(ip, subnet)
if err != nil {
return nil, fmt.Errorf("failed to select subnet: %w", err)
}
if result.address, err = netlink.ParseAddr(fmt.Sprintf("%s/%s", ip, s)); err != nil {
@@ -204,6 +227,11 @@ func NewConfig(address string, iface string, loGlobalScope bool, subnet string,
// set address as deprecated so it isn't used as source address according to RFC 3484
result.address.PreferedLft = 0
result.dhcpFamily = strings.ToLower(utils.IPv6Family)
if net.ParseIP(ip).To4() != nil {
result.dhcpFamily = strings.ToLower(utils.IPv4Family)
}
networks = append(networks, result)
}
@@ -356,7 +384,7 @@ func (configurator *network) AddIP(precheck bool, skipDAD bool) (bool, error) {
}
if err := netlink.AddrReplace(configurator.link.Intf, configurator.address); err != nil {
return false, errors.Wrap(err, "could not add ip")
return false, errors.Wrap(err, fmt.Sprintf("could not add ip to device %q", configurator.link.Intf.Attrs().Name))
}
if err := configurator.configureIPTables(); err != nil {
@@ -367,13 +395,13 @@ func (configurator *network) AddIP(precheck bool, skipDAD bool) (bool, error) {
}
func (configurator *network) configureIPTables() error {
if configurator.enableSecurity && !configurator.ignoreSecurity {
if configurator.enableSecurity && !configurator.ignoreSecurity && len(configurator.ports) > 0 {
if err := configurator.addIptablesRulesToLimitTrafficPorts(); err != nil {
return errors.Wrap(err, "could not add iptables rules to limit traffic ports")
}
}
// It seems that masquerading is only reuired with IPv4 for IPVS to work.
// It seems that masquerading is only required with IPv4 for IPVS to work.
if configurator.ipvsEnabled && configurator.forwardMethod == "masquerade" && configurator.address.IP.To4() != nil {
if err := configurator.addIptablesRulesForMasquerade(); err != nil {
return errors.Wrap(err, "could not add iptables rules for masquerade")
@@ -384,12 +412,17 @@ func (configurator *network) configureIPTables() error {
}
func (configurator *network) addIptablesRulesToLimitTrafficPorts() error {
ipt, err := iptables.New()
vip := configurator.address.IP.String()
opt := iptables.IPFamily(iptables.ProtocolIPv4)
if utils.IsIPv6(vip) {
opt = iptables.IPFamily(iptables.ProtocolIPv6)
}
ipt, err := iptables.New(opt)
if err != nil {
return errors.Wrap(err, "could not create iptables client")
}
vip := configurator.address.IP.String()
comment := fmt.Sprintf(iptablesComment, configurator.serviceName)
if err := insertCommonIPTablesRules(ipt, vip, comment); err != nil {
return fmt.Errorf("could not add common iptables rules: %w", err)
@@ -675,6 +708,25 @@ func (configurator *network) SetIP(ip string) error {
if strings.Contains("/", ip) {
return fmt.Errorf("ip should not contain CIDR notation got: %s", ip)
}
if configurator.address == nil {
log.Debug("possible", "subnets", configurator.possibleSubnets)
subnet, err := SelectSubnet(ip, configurator.possibleSubnets)
if err != nil {
return fmt.Errorf("unable to select subnet for IP %q from %q: %w", ip, subnet, err)
}
// Check if the subnet needs overriding
cidr, err := utils.FormatIPWithSubnetMask(ip, subnet)
if err != nil {
return errors.Wrapf(err, "2 could not format address %q with subnetMask %q", ip, subnet)
}
configurator.address, err = netlink.ParseAddr(cidr)
if err != nil {
return errors.Wrapf(err, "could not parse address %q", cidr)
}
}
ones, _ := configurator.address.Mask.Size()
cidr, err := utils.FormatIPWithSubnetMask(ip, strconv.Itoa(ones))
if err != nil {
@@ -804,11 +856,17 @@ func (configurator *network) SetMask(mask string) error {
selectedMask := mask
var err error
if mask == "" {
return fmt.Errorf("no mask provided")
}
if configurator.IP() != "" {
selectedMask, err = SelectSubnet(configurator.IP(), mask)
if err != nil {
return fmt.Errorf("failed to select mask %q: %w", mask, err)
}
} else if len(strings.Split(mask, ",")) > 1 {
return fmt.Errorf("cannot select mask from %q when IP address is unknown", mask)
}
m, err := strconv.Atoi(selectedMask)
@@ -816,19 +874,18 @@ func (configurator *network) SetMask(mask string) error {
return err
}
size := defaultMaskIPv4
size := DefaultMaskIPv4
family := utils.IPv4Family
if configurator.IP() != "" {
if utils.IsIPv6(configurator.IP()) {
size = defaultMaskIPv6
size = DefaultMaskIPv6
family = utils.IPv6Family
}
if m > size {
return fmt.Errorf("provided CIDR mask '%d' is greater than the highest mask value for the %s family (%d)", m, family, size)
}
}
toSet := net.CIDRMask(m, size)
@@ -857,6 +914,14 @@ func (configurator *network) ARPName() string {
return fmt.Sprintf("%s-%s", configurator.CIDR(), configurator.Interface())
}
func (configurator *network) GetPossibleSubnets() string {
return configurator.possibleSubnets
}
func (configurator *network) DHCPFamily() string {
return configurator.dhcpFamily
}
// SelectSubnet formats an IP address with the appropriate CIDR based on the input.
// The input SubnetMasks can be "32,128" (dual-stack), "32", "128" (SingleStack).
func SelectSubnet(rawIP string, subnetMasks string) (string, error) {

View File

@@ -2,7 +2,7 @@ package vip
import (
"context"
"net"
"fmt"
"time"
log "log/slog"
@@ -14,42 +14,41 @@ import (
// for the dDNSHostName
// will return the IP allocated
type DDNSManager interface {
Start() (string, error)
Start(ctx context.Context) (string, error)
}
type ddnsManager struct {
ctx context.Context
network Network
}
// NewDDNSManager returns a newly created Dynamic DNS manager
func NewDDNSManager(ctx context.Context, network Network) DDNSManager {
func NewDDNSManager(network Network) DDNSManager {
return &ddnsManager{
ctx: ctx,
network: network,
}
}
// Start will start the dhcpclient routine to keep the lease
// and return the IP it got from DHCP
func (ddns *ddnsManager) Start() (string, error) {
interfaceName := ddns.network.Interface()
iface, err := net.InterfaceByName(interfaceName)
func (ddns *ddnsManager) Start(ctx context.Context) (string, error) {
client, err := NewDHCPClient(ddns.network)
if err != nil {
return "", err
return "", fmt.Errorf("unable to create DHCP client: %w", err)
}
client := NewDHCPClient(iface, false, "")
client.WithHostName(ddns.network.DDNSHostName())
go client.Start()
go func() {
if err := client.Start(ctx); err != nil {
log.Error("[ddns] DHCP client error: %w")
}
}()
log.Info("waiting for ip from dhcp")
ip, timeout := "", time.After(1*time.Minute)
select {
case <-ddns.ctx.Done():
case <-ctx.Done():
client.Stop()
return "", errors.New("context cancelled")
case <-timeout:
@@ -78,7 +77,7 @@ func (ddns *ddnsManager) Start() (string, error) {
log.Info("got address from dhcp", "ip", ip)
}
}
}(ddns.ctx)
}(ctx)
return ip, nil
}

View File

@@ -1,289 +1,39 @@
package vip
// DHCP client implementation that refers to https://www.rfc-editor.org/rfc/rfc2131.html
import (
"context"
"fmt"
"net"
"time"
"strings"
log "log/slog"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
"github.com/jpillora/backoff"
"github.com/kube-vip/kube-vip/pkg/utils"
)
const dhcpClientPort = "68"
const defaultDHCPRenew = time.Hour
const maxBackoffAttempts = 3
// DHCPClient is responsible for maintaining ipv4 lease for one specified interface
type DHCPClient struct {
iface *net.Interface
ddnsHostName string
lease *nclient4.Lease
initRebootFlag bool
requestedIP net.IP
stopChan chan struct{} // used as a signal to release the IP and stop the dhcp client daemon
releasedChan chan struct{} // indicate that the IP has been released
errorChan chan error // indicates there was an error on the IP request
ipChan chan string
type DHCPClient interface {
ErrorChannel() chan error
IPChannel() chan string
Start(ctx context.Context) error
Stop()
WithHostName(hostname string) DHCPClient
}
// NewDHCPClient returns a new DHCP Client.
func NewDHCPClient(iface *net.Interface, initRebootFlag bool, requestedIP string) *DHCPClient {
return &DHCPClient{
iface: iface,
stopChan: make(chan struct{}),
releasedChan: make(chan struct{}),
errorChan: make(chan error),
initRebootFlag: initRebootFlag,
requestedIP: net.ParseIP(requestedIP),
ipChan: make(chan string),
}
}
func (c *DHCPClient) WithHostName(hostname string) *DHCPClient {
c.ddnsHostName = hostname
return c
}
// Stop state-transition process and close dhcp client
func (c *DHCPClient) Stop() {
close(c.ipChan)
close(c.stopChan)
<-c.releasedChan
}
// Gets the IPChannel for consumption
func (c *DHCPClient) IPChannel() chan string {
return c.ipChan
}
// Gets the ErrorChannel for consumption
func (c *DHCPClient) ErrorChannel() chan error {
return c.errorChan
}
// Start state-transition process of dhcp client
//
// -------- -------
//
// | | +-------------------------->| |<-------------------+
// | INIT- | | +-------------------->| INIT | |
// | REBOOT |DHCPNAK/ +---------->| |<---+ |
// | |Restart| | ------- | |
//
// -------- | DHCPNAK/ | | |
// | Discard offer | -/Send DHCPDISCOVER |
//
// -/Send DHCPREQUEST | | |
//
// | | | DHCPACK v | |
// ----------- | (not accept.)/ ----------- | |
//
// | | | Send DHCPDECLINE | | |
// | REBOOTING | | | | SELECTING |<----+ |
// | | | / | | |DHCPOFFER/ |
//
// ----------- | / ----------- | |Collect |
// | | / | | | replies |
//
// DHCPACK/ | / +----------------+ +-------+ |
// Record lease, set| | v Select offer/ |
// timers T1, T2 ------------ send DHCPREQUEST | |
//
// | +----->| | DHCPNAK, Lease expired/ |
// | | | REQUESTING | Halt network |
// DHCPOFFER/ | | | |
// Discard ------------ | |
// | | | | ----------- |
// | +--------+ DHCPACK/ | | |
// | Record lease, set -----| REBINDING | |
// | timers T1, T2 / | | |
// | | DHCPACK/ ----------- |
// | v Record lease, set ^ |
// +----------------> ------- /timers T1,T2 | |
// +----->| |<---+ | |
// | | BOUND |<---+ | |
// DHCPOFFER, DHCPACK, | | | T2 expires/ DHCPNAK/
// DHCPNAK/Discard ------- | Broadcast Halt network
// | | | | DHCPREQUEST |
// +-------+ | DHCPACK/ | |
// T1 expires/ Record lease, set | |
// Send DHCPREQUEST timers T1, T2 | |
// to leasing server | | |
// | ---------- | |
// | | |------------+ |
// +->| RENEWING | |
// | |----------------------------+
// ----------
// Figure: State-transition diagram for DHCP clients
func (c *DHCPClient) Start() {
lease := c.requestWithBackoff()
c.initRebootFlag = false
c.lease = lease
// Set up two ticker to renew/rebind regularly
t1Timeout := c.lease.ACK.IPAddressLeaseTime(defaultDHCPRenew) / 2
t2Timeout := (c.lease.ACK.IPAddressLeaseTime(defaultDHCPRenew) / 8) * 7
log.Debug("dhcp timeouts", "timeout1", t1Timeout, "timeoute2", t2Timeout)
t1, t2 := time.NewTicker(t1Timeout), time.NewTicker(t2Timeout)
for {
select {
case <-t1.C:
// renew is a unicast request of the IP renewal
// A point on renew is: the library does not return the right message (NAK)
// on renew error due to IP Change, but instead it returns a different error
// This way there's not much to do other than log and continue, as the renew error
// may be an offline server, or may be an incorrect package match
lease, err := c.renew()
if err == nil {
c.lease = lease
log.Info("renew", "lease", lease)
t2.Reset(t2Timeout)
} else {
log.Error("renew failed", "err", err)
}
case <-t2.C:
// rebind is just like a request, but forcing to provide a new IP address
lease, err := c.request(true)
if err == nil {
c.lease = lease
log.Info("rebind", "lease", lease)
} else {
if _, ok := err.(*nclient4.ErrNak); !ok {
t1.Stop()
t2.Stop()
log.Error("rebind failed", "err", err)
return
}
log.Warn("ip may have changed", "ip", c.lease.ACK.YourIPAddr, "err", err)
c.initRebootFlag = false
c.lease = c.requestWithBackoff()
}
t1.Reset(t1Timeout)
t2.Reset(t2Timeout)
case <-c.stopChan:
// release is a unicast request of the IP release.
if err := c.release(); err != nil {
log.Error("release lease failed", "lease", lease, "err", err)
} else {
log.Info("release", "lease", lease)
}
t1.Stop()
t2.Stop()
close(c.releasedChan)
return
}
}
}
// --------------------------------------------------------
// | |INIT-REBOOT | RENEWING |REBINDING |
// --------------------------------------------------------
// |broad/unicast |broadcast | unicast |broadcast |
// |server-ip |MUST NOT | MUST NOT |MUST NOT |
// |requested-ip |MUST | MUST NOT |MUST NOT |
// |ciaddr |zero | IP address |IP address|
// --------------------------------------------------------
func (c *DHCPClient) requestWithBackoff() *nclient4.Lease {
backoff := backoff.Backoff{
Factor: 2,
Jitter: true,
Min: 10 * time.Second,
Max: 1 * time.Minute,
func NewDHCPClient(network Network) (DHCPClient, error) {
interfaceName := network.Interface()
iface, err := net.InterfaceByName(interfaceName)
if err != nil {
return nil, fmt.Errorf("failed to get interface %q: %w", interfaceName, err)
}
var lease *nclient4.Lease
var err error
var client DHCPClient
for {
log.Debug("trying to get a new IP", "attempt", backoff.Attempt())
lease, err = c.request(false)
if strings.EqualFold(network.DHCPFamily(), utils.IPv6Family) {
client, err = NewDHCPv6Client(iface, nil, false, "")
if err != nil {
dur := backoff.Duration()
if backoff.Attempt() > maxBackoffAttempts-1 {
errMsg := fmt.Errorf("failed to get an IP address after %d attempts, error %s, giving up", maxBackoffAttempts, err.Error())
log.Error(errMsg.Error())
c.errorChan <- errMsg
c.Stop()
return nil
}
log.Error("request failed", "err", err.Error(), "waiting", dur)
time.Sleep(dur)
continue
return nil, fmt.Errorf("failed to create DHCP client: %w", err)
}
backoff.Reset()
break
} else {
client = NewDHCPv4Client(iface, false, "")
}
if c.ipChan != nil {
log.Debug("using channel")
c.ipChan <- lease.ACK.YourIPAddr.String()
}
return lease
}
func (c *DHCPClient) request(rebind bool) (*nclient4.Lease, error) {
dhclient, err := nclient4.New(c.iface.Name)
if err != nil {
return nil, fmt.Errorf("create a client for iface %s failed, error: %w", c.iface.Name, err)
}
defer dhclient.Close()
modifiers := make([]dhcpv4.Modifier, 0)
if c.ddnsHostName != "" {
modifiers = append(modifiers,
dhcpv4.WithOption(dhcpv4.OptHostName(c.ddnsHostName)),
dhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte(c.ddnsHostName))),
)
}
// if initRebootFlag is set, this means we have an IP already set on c.requestedIP that should be used
if c.initRebootFlag {
log.Debug("init-reboot", "ip", c.requestedIP)
modifiers = append(modifiers, dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(c.requestedIP)))
}
// if this is a rebind, then the IP we should set is the one that already exists in lease
if rebind {
log.Debug("rebinding", "ip", c.lease.ACK.YourIPAddr)
modifiers = append(modifiers, dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(c.lease.ACK.YourIPAddr)))
}
return dhclient.Request(context.TODO(), modifiers...)
}
func (c *DHCPClient) release() error {
dhclient, err := nclient4.New(c.iface.Name)
if err != nil {
return fmt.Errorf("create release client failed, error: %w, iface: %s, server ip: %v", err, c.iface.Name, c.lease.ACK.ServerIPAddr)
}
defer dhclient.Close()
// TODO modify lease
return dhclient.Release(c.lease)
}
func (c *DHCPClient) renew() (*nclient4.Lease, error) {
// renew needs a unicast client. This is due to some servers (like dnsmasq) require the exact request coming from the vip interface
dhclient, err := nclient4.New(c.iface.Name,
nclient4.WithUnicast(&net.UDPAddr{IP: c.lease.ACK.YourIPAddr, Port: nclient4.ClientPort}))
if err != nil {
return nil, fmt.Errorf("create renew client failed, error: %w, server ip: %v", err, c.lease.ACK.ServerIPAddr)
}
defer dhclient.Close()
return dhclient.Renew(context.TODO(), c.lease)
return client, nil
}

292
pkg/vip/dhcpv4.go Normal file
View File

@@ -0,0 +1,292 @@
package vip
// DHCP client implementation that refers to https://www.rfc-editor.org/rfc/rfc2131.html
import (
"context"
"fmt"
"net"
"time"
log "log/slog"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
"github.com/jpillora/backoff"
)
const dhcpClientPort = "68"
const defaultDHCPRenew = time.Hour
const maxBackoffAttempts = 3
// DHCPv4Client is responsible for maintaining ipv4 lease for one specified interface
type DHCPv4Client struct {
iface *net.Interface
ddnsHostName string
lease *nclient4.Lease
initRebootFlag bool
requestedIP net.IP
stopChan chan struct{} // used as a signal to release the IP and stop the dhcp client daemon
releasedChan chan struct{} // indicate that the IP has been released
errorChan chan error // indicates there was an error on the IP request
ipChan chan string
}
// NewDHCPv4Client returns a new DHCP Client.
func NewDHCPv4Client(iface *net.Interface, initRebootFlag bool, requestedIP string) *DHCPv4Client {
return &DHCPv4Client{
iface: iface,
stopChan: make(chan struct{}),
releasedChan: make(chan struct{}),
errorChan: make(chan error),
initRebootFlag: initRebootFlag,
requestedIP: net.ParseIP(requestedIP),
ipChan: make(chan string),
}
}
func (c *DHCPv4Client) WithHostName(hostname string) DHCPClient {
c.ddnsHostName = hostname
return c
}
// Stop state-transition process and close dhcp client
func (c *DHCPv4Client) Stop() {
close(c.ipChan)
close(c.stopChan)
<-c.releasedChan
}
// Gets the IPChannel for consumption
func (c *DHCPv4Client) IPChannel() chan string {
return c.ipChan
}
// Gets the ErrorChannel for consumption
func (c *DHCPv4Client) ErrorChannel() chan error {
return c.errorChan
}
// Start state-transition process of dhcp client
//
// -------- -------
//
// | | +-------------------------->| |<-------------------+
// | INIT- | | +-------------------->| INIT | |
// | REBOOT |DHCPNAK/ +---------->| |<---+ |
// | |Restart| | ------- | |
//
// -------- | DHCPNAK/ | | |
// | Discard offer | -/Send DHCPDISCOVER |
//
// -/Send DHCPREQUEST | | |
//
// | | | DHCPACK v | |
// ----------- | (not accept.)/ ----------- | |
//
// | | | Send DHCPDECLINE | | |
// | REBOOTING | | | | SELECTING |<----+ |
// | | | / | | |DHCPOFFER/ |
//
// ----------- | / ----------- | |Collect |
// | | / | | | replies |
//
// DHCPACK/ | / +----------------+ +-------+ |
// Record lease, set| | v Select offer/ |
// timers T1, T2 ------------ send DHCPREQUEST | |
//
// | +----->| | DHCPNAK, Lease expired/ |
// | | | REQUESTING | Halt network |
// DHCPOFFER/ | | | |
// Discard ------------ | |
// | | | | ----------- |
// | +--------+ DHCPACK/ | | |
// | Record lease, set -----| REBINDING | |
// | timers T1, T2 / | | |
// | | DHCPACK/ ----------- |
// | v Record lease, set ^ |
// +----------------> ------- /timers T1,T2 | |
// +----->| |<---+ | |
// | | BOUND |<---+ | |
// DHCPOFFER, DHCPACK, | | | T2 expires/ DHCPNAK/
// DHCPNAK/Discard ------- | Broadcast Halt network
// | | | | DHCPREQUEST |
// +-------+ | DHCPACK/ | |
// T1 expires/ Record lease, set | |
// Send DHCPREQUEST timers T1, T2 | |
// to leasing server | | |
// | ---------- | |
// | | |------------+ |
// +->| RENEWING | |
// | |----------------------------+
// ----------
// Figure: State-transition diagram for DHCP clients
func (c *DHCPv4Client) Start(ctx context.Context) error {
dhcpContext, cancel := context.WithCancel(ctx)
defer cancel()
lease := c.requestWithBackoff(dhcpContext)
c.initRebootFlag = false
c.lease = lease
// Set up two ticker to renew/rebind regularly
t1Timeout := c.lease.ACK.IPAddressLeaseTime(defaultDHCPRenew) / 2
t2Timeout := (c.lease.ACK.IPAddressLeaseTime(defaultDHCPRenew) / 8) * 7
log.Debug("[DHCPv4] timeouts", "timeout1", t1Timeout, "timeoute2", t2Timeout)
t1, t2 := time.NewTicker(t1Timeout), time.NewTicker(t2Timeout)
for {
select {
case <-t1.C:
// renew is a unicast request of the IP renewal
// A point on renew is: the library does not return the right message (NAK)
// on renew error due to IP Change, but instead it returns a different error
// This way there's not much to do other than log and continue, as the renew error
// may be an offline server, or may be an incorrect package match
lease, err := c.renew(dhcpContext)
if err == nil {
c.lease = lease
log.Info("[DHCPv4] renew", "lease", lease)
t2.Reset(t2Timeout)
} else {
log.Error("[DHCPv4] renew failed", "err", err)
}
case <-t2.C:
// rebind is just like a request, but forcing to provide a new IP address
lease, err := c.request(dhcpContext, true)
if err == nil {
c.lease = lease
log.Info("[DHCPv4] rebind", "lease", lease)
} else {
if _, ok := err.(*nclient4.ErrNak); !ok {
t1.Stop()
t2.Stop()
log.Error("[DHCPv4] rebind failed", "err", err)
}
log.Warn("[DHCPv4] ip may have changed", "ip", c.lease.ACK.YourIPAddr, "err", err)
c.initRebootFlag = false
c.lease = c.requestWithBackoff(dhcpContext)
}
t1.Reset(t1Timeout)
t2.Reset(t2Timeout)
case <-c.stopChan:
// release is a unicast request of the IP release.
var err error
if err = c.release(); err != nil {
log.Error("[DHCPv4] release lease failed", "lease", lease, "err", err)
} else {
log.Info("[DHCPv4] release", "lease", lease)
}
t1.Stop()
t2.Stop()
close(c.releasedChan)
return err
}
}
}
// --------------------------------------------------------
// | |INIT-REBOOT | RENEWING |REBINDING |
// --------------------------------------------------------
// |broad/unicast |broadcast | unicast |broadcast |
// |server-ip |MUST NOT | MUST NOT |MUST NOT |
// |requested-ip |MUST | MUST NOT |MUST NOT |
// |ciaddr |zero | IP address |IP address|
// --------------------------------------------------------
func (c *DHCPv4Client) requestWithBackoff(ctx context.Context) *nclient4.Lease {
backoff := backoff.Backoff{
Factor: 2,
Jitter: true,
Min: 10 * time.Second,
Max: 1 * time.Minute,
}
var lease *nclient4.Lease
var err error
for {
log.Debug("[DHCPv4] trying to get a new IP", "attempt", backoff.Attempt())
lease, err = c.request(ctx, false)
if err != nil {
dur := backoff.Duration()
if backoff.Attempt() > maxBackoffAttempts-1 {
errMsg := fmt.Errorf("failed to get an IP address after %d attempts, error %s, giving up", maxBackoffAttempts, err.Error())
log.Error(errMsg.Error())
c.errorChan <- errMsg
c.Stop()
return nil
}
log.Error("[DHCPv4] request failed", "err", err.Error(), "waiting", dur)
time.Sleep(dur)
continue
}
backoff.Reset()
break
}
if c.ipChan != nil {
log.Debug("[DHCPv4] using channel")
c.ipChan <- lease.ACK.YourIPAddr.String()
}
return lease
}
func (c *DHCPv4Client) request(ctx context.Context, rebind bool) (*nclient4.Lease, error) {
dhclient, err := nclient4.New(c.iface.Name)
if err != nil {
return nil, fmt.Errorf("create a client for iface %s failed, error: %w", c.iface.Name, err)
}
defer dhclient.Close()
modifiers := make([]dhcpv4.Modifier, 0)
if c.ddnsHostName != "" {
modifiers = append(modifiers,
dhcpv4.WithOption(dhcpv4.OptHostName(c.ddnsHostName)),
dhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte(c.ddnsHostName))),
)
}
// if initRebootFlag is set, this means we have an IP already set on c.requestedIP that should be used
if c.initRebootFlag {
log.Debug("[DHCPv4] init-reboot", "ip", c.requestedIP)
modifiers = append(modifiers, dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(c.requestedIP)))
}
// if this is a rebind, then the IP we should set is the one that already exists in lease
if rebind {
log.Debug("[DHCPv4] rebinding", "ip", c.lease.ACK.YourIPAddr)
modifiers = append(modifiers, dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(c.lease.ACK.YourIPAddr)))
}
return dhclient.Request(ctx, modifiers...)
}
func (c *DHCPv4Client) release() error {
dhclient, err := nclient4.New(c.iface.Name)
if err != nil {
return fmt.Errorf("create release client failed, error: %w, iface: %s, server ip: %v", err, c.iface.Name, c.lease.ACK.ServerIPAddr)
}
defer dhclient.Close()
// TODO modify lease
return dhclient.Release(c.lease)
}
func (c *DHCPv4Client) renew(ctx context.Context) (*nclient4.Lease, error) {
// renew needs a unicast client. This is due to some servers (like dnsmasq) require the exact request coming from the vip interface
dhclient, err := nclient4.New(c.iface.Name,
nclient4.WithUnicast(&net.UDPAddr{IP: c.lease.ACK.YourIPAddr, Port: nclient4.ClientPort}))
if err != nil {
return nil, fmt.Errorf("create renew client failed, error: %w, server ip: %v", err, c.lease.ACK.ServerIPAddr)
}
defer dhclient.Close()
return dhclient.Renew(ctx, c.lease)
}

387
pkg/vip/dhcpv6.go Normal file
View File

@@ -0,0 +1,387 @@
package vip
import (
"context"
"fmt"
log "log/slog"
"net"
"sync/atomic"
"time"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
"github.com/insomniacslk/dhcp/iana"
"github.com/jpillora/backoff"
"github.com/vishvananda/netlink"
)
var dhcpv6ClientManager *DHCPv6ClientManager
func init() {
dhcpv6ClientManager = NewDHCPv6ClientManager()
}
type DHCPv6ClientManager struct {
clients map[string]*DHCPv6InternalClient
}
func NewDHCPv6ClientManager() *DHCPv6ClientManager {
return &DHCPv6ClientManager{
clients: map[string]*DHCPv6InternalClient{},
}
}
func (m *DHCPv6ClientManager) Get(iface string) *DHCPv6InternalClient {
c, exists := m.clients[iface]
if !exists {
return nil
}
return c
}
func (m *DHCPv6ClientManager) Add(iface string) (*DHCPv6InternalClient, error) {
c := m.Get(iface)
if c != nil {
c.references.Add(1)
return c, nil
}
client, err := NewDHCPv6InternalClient(iface)
if err != nil {
return nil, err
}
m.clients[iface] = client
return client, nil
}
func (m *DHCPv6ClientManager) Delete(iface string) {
c := m.Get(iface)
if c != nil {
c.references.Add(-1)
ref := c.references.Load()
if ref < 1 {
c.client.Close()
delete(m.clients, iface)
}
}
}
type DHCPv6InternalClient struct {
client *nclient6.Client
references *atomic.Int32
}
func NewDHCPv6InternalClient(iface string) (*DHCPv6InternalClient, error) {
client, err := nclient6.New(iface)
if err != nil {
return nil, fmt.Errorf("failed to create DHCPv6 client for interface %q: %w", iface, err)
}
ref := &atomic.Int32{}
ref.Store(1)
return &DHCPv6InternalClient{
client: client,
references: ref,
}, nil
}
type DHCPv6Client struct {
iface *net.Interface
ddnsHostName string
initRebootFlag bool
requestedIP net.IP
stopChan chan struct{} // used as a signal to release the IP and stop the dhcp client daemon
releasedChan chan struct{} // indicate that the IP has been released
errorChan chan error // indicates there was an error on the IP request
ipChan chan string
ic *DHCPv6InternalClient
addr *dhcpv6.OptIAAddress
}
// NewDHCPv6Client returns a new DHCP6 Client.
func NewDHCPv6Client(iface *net.Interface, parent netlink.Link, initRebootFlag bool, requestedIP string) (*DHCPv6Client, error) {
name := iface.Name
if parent != nil {
name = parent.Attrs().Name
}
client, err := dhcpv6ClientManager.Add(name)
if err != nil {
return nil, fmt.Errorf("failed to create DHCPv6 client: %w", err)
}
return &DHCPv6Client{
iface: iface,
stopChan: make(chan struct{}),
releasedChan: make(chan struct{}),
errorChan: make(chan error),
initRebootFlag: initRebootFlag,
requestedIP: net.ParseIP(requestedIP),
ipChan: make(chan string),
ic: client,
}, nil
}
func (c *DHCPv6Client) WithHostName(hostname string) DHCPClient {
c.ddnsHostName = hostname
return c
}
// Stop state-transition process and close dhcp client
func (c *DHCPv6Client) Stop() {
close(c.ipChan)
close(c.stopChan)
<-c.releasedChan
dhcpv6ClientManager.Delete(c.iface.Name)
}
// Gets the IPChannel for consumption
func (c *DHCPv6Client) IPChannel() chan string {
return c.ipChan
}
// Gets the ErrorChannel for consumption
func (c *DHCPv6Client) ErrorChannel() chan error {
return c.errorChan
}
func (c *DHCPv6Client) Start(ctx context.Context) error {
dhcpContext, cancel := context.WithCancel(ctx)
defer cancel()
// REQUEST WITH BACKOFF ACTION
addr, err := c.requestWithBackoff(dhcpContext)
if err != nil {
return fmt.Errorf("DHCPv6 client failed: %w", err)
}
c.addr = addr
c.initRebootFlag = false
// Set up two ticker to renew/rebind regularly
t1Timeout := c.addr.PreferredLifetime / 2
t2Timeout := (c.addr.ValidLifetime / 8) * 7
log.Debug("[DHCPv6] timeouts", "timeout1", t1Timeout, "timeoute2", t2Timeout)
t1, t2 := time.NewTicker(t1Timeout), time.NewTicker(t2Timeout)
for {
select {
case <-t1.C:
// renew is a unicast request of the IP renewal
// A point on renew is: the library does not return the right message (NAK)
// on renew error due to IP Change, but instead it returns a different error
// This way there's not much to do other than log and continue, as the renew error
// may be an offline server, or may be an incorrect package match
addr, err := c.renew(dhcpContext)
if err == nil {
c.addr = addr
log.Info("[DHCPv6] renew", "addr", addr.IPv6Addr.String())
t2.Reset(t2Timeout)
} else {
log.Error("[DHCPv6] renew failed", "err", err)
}
case <-t2.C:
// rebind is just like a request, but forcing to provide a new IP address
addr, err := c.request(dhcpContext, true)
if err == nil {
c.addr = addr
log.Info("[DHCPv6] rebind", "lease", addr)
} else {
log.Warn("[DHCPv6] ip may have changed", "ip", addr.IPv6Addr.String(), "err", err)
c.initRebootFlag = false
c.addr, err = c.requestWithBackoff(dhcpContext)
log.Error("[DHCPv6] rebind failed", "err", err)
}
t1.Reset(t1Timeout)
t2.Reset(t2Timeout)
case <-c.stopChan:
dhcpStopContext, cancel := context.WithCancel(context.Background())
defer cancel()
// IP address release.
var err error
if err = c.release(dhcpStopContext); err != nil {
log.Error("[DHCPv6] release failed", "err", err)
} else {
log.Info("[DHCPv6] released", "address", c.addr.String())
}
t1.Stop()
t2.Stop()
close(c.releasedChan)
return err
}
}
}
func (c *DHCPv6Client) requestWithBackoff(ctx context.Context) (*dhcpv6.OptIAAddress, error) {
backoff := backoff.Backoff{
Factor: 2,
Jitter: true,
Min: 10 * time.Second,
Max: 1 * time.Minute,
}
var err error
var addr *dhcpv6.OptIAAddress
for {
log.Debug("[DHCPv6] trying to get a new IP", "attempt", backoff.Attempt())
addr, err = c.request(ctx, false)
if err != nil {
dur := backoff.Duration()
if backoff.Attempt() > maxBackoffAttempts-1 {
errMsg := fmt.Errorf("failed to get an IP address after %d attempts, error %s, giving up", maxBackoffAttempts, err.Error())
log.Error(errMsg.Error())
c.errorChan <- errMsg
c.Stop()
return nil, fmt.Errorf("failed to get IPv6 address: %w", err)
}
log.Error("[DHCPv6] request failed", "err", err.Error(), "waiting", dur)
time.Sleep(dur)
continue
}
backoff.Reset()
break
}
if c.ipChan != nil {
log.Debug("[DHCPv6] using channel")
c.ipChan <- addr.IPv6Addr.String()
}
return addr, nil
}
func (c *DHCPv6Client) request(ctx context.Context, rebind bool) (*dhcpv6.OptIAAddress, error) {
modifiers := []dhcpv6.Modifier{}
modifiers = append(modifiers, dhcpv6.WithClientID(&dhcpv6.DUIDEN{EnterpriseNumber: 1, EnterpriseIdentifier: []byte(c.ddnsHostName)}))
modifiers = append(modifiers, dhcpv6.WithFQDN(4, c.ddnsHostName))
// if initRebootFlag is set, this means we have an IP already set on c.requestedIP that should be used
if c.initRebootFlag {
log.Debug("[DHCPv6] init-reboot", "ip", c.requestedIP)
addr := dhcpv6.OptIAAddress{
IPv6Addr: c.requestedIP,
}
modifiers = append(modifiers, dhcpv6.WithIANA(addr))
} else if rebind {
if c.addr == nil {
return nil, fmt.Errorf("unable to rebind - current IP unknown")
}
log.Debug("[DHCPv6] rebinding", "ip", c.addr.IPv6Addr)
modifiers = append(modifiers, dhcpv6.WithIANA(*c.addr))
}
var reply *dhcpv6.Message
if rebind || c.initRebootFlag {
request, err := dhcpv6.NewMessage(modifiers...)
if err != nil {
return nil, fmt.Errorf("failed to create rebind message: %w", err)
}
request.MessageType = dhcpv6.MessageTypeRebind
reply, err = c.ic.client.SendAndRead(ctx, c.ic.client.RemoteAddr(), request, nil)
if err != nil {
return nil, fmt.Errorf("rebind error: %w", err)
}
} else {
adv, err := c.ic.client.Solicit(ctx, modifiers...)
if err != nil {
return nil, fmt.Errorf("solicit error: %w", err)
}
request, err := dhcpv6.NewRequestFromAdvertise(adv, modifiers...)
if err != nil {
return nil, fmt.Errorf("unable to create request message: %w", err)
}
request.MessageType = dhcpv6.MessageTypeAdvertise
reply, err = c.ic.client.Request(ctx, request, modifiers...)
if err != nil {
return nil, fmt.Errorf("request error: %w", err)
}
}
if reply == nil {
return nil, fmt.Errorf("invalid request")
}
return getAddress(reply.Options.IANA())
}
func (c *DHCPv6Client) renew(ctx context.Context) (*dhcpv6.OptIAAddress, error) {
modifiers := []dhcpv6.Modifier{}
modifiers = append(modifiers, dhcpv6.WithClientID(&dhcpv6.DUIDEN{EnterpriseNumber: 1, EnterpriseIdentifier: []byte(c.ddnsHostName)}))
modifiers = append(modifiers, dhcpv6.WithFQDN(4, c.ddnsHostName))
modifiers = append(modifiers, dhcpv6.WithOption(&dhcpv6.OptionGeneric{OptionCode: dhcpv6.OptionUnicast}))
adv, err := c.ic.client.Solicit(ctx, modifiers...)
if err != nil {
return nil, fmt.Errorf("solicit error: %w", err)
}
request, err := dhcpv6.NewRequestFromAdvertise(adv)
if err != nil {
return nil, fmt.Errorf("failed to create request message: %w", err)
}
request.MessageType = dhcpv6.MessageTypeRenew
reply, err := c.ic.client.SendAndRead(ctx, c.ic.client.RemoteAddr(), request, nil)
if err != nil {
return nil, fmt.Errorf("failed to send renew: %w", err)
}
return getAddress(reply.Options.IANA())
}
func (c *DHCPv6Client) release(ctx context.Context) error {
modifiers := []dhcpv6.Modifier{}
modifiers = append(modifiers, dhcpv6.WithClientID(&dhcpv6.DUIDEN{EnterpriseNumber: 1, EnterpriseIdentifier: []byte(c.ddnsHostName)}))
modifiers = append(modifiers, dhcpv6.WithFQDN(4, c.ddnsHostName))
adv, err := c.ic.client.Solicit(ctx, modifiers...)
if err != nil {
return fmt.Errorf("solicit error: %w", err)
}
request, err := dhcpv6.NewRequestFromAdvertise(adv)
if err != nil {
return fmt.Errorf("failed to create release message: %w", err)
}
request.MessageType = dhcpv6.MessageTypeRelease
reply, err := c.ic.client.SendAndRead(ctx, c.ic.client.RemoteAddr(), request, nil)
if err != nil {
return fmt.Errorf("failed to send release: %w", err)
}
if reply.Options.Status().StatusCode != iana.StatusSuccess {
return fmt.Errorf("release failed with code %d: %s", reply.Options.Status().StatusCode, reply.Options.Status().StatusMessage)
}
return nil
}
func getAddress(iana []*dhcpv6.OptIANA) (*dhcpv6.OptIAAddress, error) {
if len(iana) < 1 {
return nil, fmt.Errorf("failed to get IANA")
}
if len(iana) < 1 {
return nil, fmt.Errorf("failed to get addresses data")
}
return iana[0].Options.Addresses()[0], nil
}

View File

@@ -39,20 +39,20 @@ func (d *ipUpdater) Run(ctx context.Context) {
mode = "ipv6"
}
ip, err := utils.LookupHost(d.vip.DNSName(), mode)
ip, err := utils.LookupHost(d.vip.DNSName(), mode, true)
if err != nil {
log.Warn("cannot lookup", "name", d.vip.DNSName(), "err", err)
// fallback to renewing the existing IP
ip = []string{d.vip.IP()}
}
log.Debug("setting IP", "address", ip)
log.Debug("(ipUpdater) setting IP", "address", ip)
if err := d.vip.SetIP(ip[0]); err != nil {
log.Error("setting IP", "address", ip, "err", err)
}
// Normal VIP addition for DNS, use skipDAD=false for normal DAD process
if _, err := d.vip.AddIP(false, false); err != nil {
if _, err := d.vip.AddIP(true, false); err != nil {
log.Error("error adding virtual IP", "err", err)
}

View File

@@ -21,13 +21,13 @@ type NdpResponder struct {
func NewNDPResponder(ifaceName string) (*NdpResponder, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("failed to get interface %q: %v", ifaceName, err)
return nil, fmt.Errorf("failed to get interface %q: %w", ifaceName, err)
}
// Use link-local address as the source IPv6 address for NDP communications.
conn, _, err := ndp.Listen(iface, ndp.LinkLocal)
if err != nil {
return nil, fmt.Errorf("creating NDP responder for %q: %s", iface.Name, err)
return nil, fmt.Errorf("creating NDP responder for %s: %w", iface.Name, err)
}
ret := &NdpResponder{

View File

@@ -5,13 +5,11 @@ import (
"crypto/rand"
"fmt"
"net"
"strconv"
"strings"
"syscall"
log "log/slog"
"github.com/kube-vip/kube-vip/pkg/utils"
"github.com/pkg/errors"
"github.com/vishvananda/netlink"
)
@@ -144,22 +142,3 @@ func GetNonLinkLocalIP(iface *netlink.Link, family int) (string, error) {
return "", fmt.Errorf("failed to find non-local IP on interface: %s", (*iface).Attrs().Name)
}
func selectSubnet(address string, subnets []string) string {
subnet := ""
if utils.IsIPv4(address) {
if subnets[0] != "" {
subnet = subnets[0]
} else {
subnet = strconv.Itoa(defaultMaskIPv4)
}
} else {
if len(subnets) > 1 && subnets[1] != "" {
subnet = subnets[1]
} else {
subnet = strconv.Itoa(defaultMaskIPv6)
}
}
return subnet
}

View File

@@ -1,7 +1,3 @@
module github.com/kube-vip/kube-vip/testing/e2e/servicesClient
go 1.19
require github.com/sirupsen/logrus v1.9.0
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect

View File

@@ -1,15 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -402,7 +402,7 @@ var _ = Describe("kube-vip BGP mode", Ordered, func() {
)
})
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv4 over IPv6 - fixed nexthop", Ordered, func() {
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv4 over IPv6 - auto_sourceif nexthop, auto source interface", Ordered, func() {
var (
cpVIP string
clusterName string
@@ -457,7 +457,7 @@ var _ = Describe("kube-vip BGP mode", Ordered, func() {
)
})
Describe("kube-vip IPv4 services BGP mode functionality", Ordered, func() {
Describe("kube-vip IPv4 services BGP mode functionality, with service election", Ordered, func() {
var (
cpVIP string
clusterName string
@@ -521,7 +521,7 @@ var _ = Describe("kube-vip BGP mode", Ordered, func() {
)
})
Describe("kube-vip IPv6 services BGP mode functionality", Ordered, func() {
Describe("kube-vip IPv6 services BGP mode functionality, with service election", Ordered, func() {
var (
cpVIP string
clusterName string
@@ -583,7 +583,7 @@ var _ = Describe("kube-vip BGP mode", Ordered, func() {
)
})
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv6 over IPv4 - fixed nexthop", Ordered, func() {
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv6 over IPv4 - fixed nexthop, with service election", Ordered, func() {
var (
cpVIP string
clusterName string
@@ -645,7 +645,7 @@ var _ = Describe("kube-vip BGP mode", Ordered, func() {
)
})
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv4 over IPv6 - fixed nexthop", Ordered, func() {
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv4 over IPv6 - fixed nexthop, with service election", Ordered, func() {
var (
cpVIP string
clusterName string
@@ -707,7 +707,7 @@ var _ = Describe("kube-vip BGP mode", Ordered, func() {
)
})
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv6 over IPv4 - auto_sourceif nexthop", Ordered, func() {
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv6 over IPv4 - auto_sourceif nexthop, with service election", Ordered, func() {
var (
cpVIP string
clusterName string
@@ -770,7 +770,7 @@ var _ = Describe("kube-vip BGP mode", Ordered, func() {
)
})
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv4 over IPv6 - fixed nexthop", Ordered, func() {
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv4 over IPv6 - auto_sourceif nexthop, with service election", Ordered, func() {
var (
cpVIP string
clusterName string

View File

@@ -25,7 +25,7 @@ func GetLogs(ctx context.Context, client kubernetes.Interface, tempDirPath strin
return nil
}
intCtx, cancel := context.WithTimeout(ctx, time.Second*10)
intCtx, cancel := context.WithTimeout(ctx, time.Second*20)
defer cancel()
path := filepath.Join(tempDirPath, "pods.json")
@@ -93,7 +93,8 @@ func GetLogs(ctx context.Context, client kubernetes.Interface, tempDirPath strin
LabelSelector: "app=kube-vip",
}
kvpods, err := client.CoreV1().Pods("").List(intCtx, listOptions)
var kvpods *corev1.PodList
kvpods, err = client.CoreV1().Pods("").List(intCtx, listOptions)
if err != nil {
return fmt.Errorf("failed to list pods: %w", err)
}