feat(wireguard): qualify service tunnel IDs by protocol

Sanitisation maps '-' onto the '_' separator, so "a-b/c" and "a/b-c"
shared one nftables chain, and TCP and UDP on the same port collided.
ServicePortIDs appends the protocol and, when sanitisation changed the
name or the ID grew too long, a hash of the raw name. It also returns
the previous port-only ID so existing chains can be migrated.

Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
This commit is contained in:
Marcel Fest
2026-09-07 09:54:18 +02:00
committed by GitHub
parent 3eb3838893
commit f48a458530
2 changed files with 72 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
package wireguard
import (
"crypto/sha256"
"fmt"
"strings"
"github.com/kube-vip/kube-vip/pkg/utils"
v1 "k8s.io/api/core/v1"
)
const maxServicePortIDLength = 50
// ServicePortIDs returns the protocol-qualified nftables identifier and the
// prior port-only identifier that must be removed during migration.
//
// Sanitisation maps '-' onto the '_' separator, so "a-b/c" and "a/b-c" would
// otherwise share a chain; a hash of the raw name keeps them distinct.
func ServicePortIDs(namespace, name string, port v1.ServicePort) (string, string) {
rawServiceID := fmt.Sprintf("%s_%s", namespace, name)
serviceID := utils.SanitizeServiceID(rawServiceID)
legacyID := fmt.Sprintf("%s_p%d", serviceID, port.Port)
protocol := port.Protocol
if protocol == "" {
protocol = v1.ProtocolTCP
}
suffix := fmt.Sprintf("_p%d_%s", port.Port, strings.ToLower(string(protocol)))
maxBase := maxServicePortIDLength - len(suffix)
if serviceID != rawServiceID || len(serviceID) > maxBase {
sum := sha256.Sum256([]byte(rawServiceID))
hash := fmt.Sprintf("_%x", sum[:8])
maxPrefix := maxBase - len(hash)
if len(serviceID) > maxPrefix {
serviceID = serviceID[:maxPrefix]
}
serviceID += hash
}
return serviceID + suffix, legacyID
}

View File

@@ -0,0 +1,33 @@
package wireguard
import (
"strings"
"testing"
v1 "k8s.io/api/core/v1"
)
func TestServicePortIDsKeepProtocolAndLongNamesDistinct(t *testing.T) {
udp := v1.ServicePort{Port: 53, Protocol: v1.ProtocolUDP}
udpID, legacyID := ServicePortIDs("default", "dns", udp)
if udpID != "default_dns_p53_udp" || legacyID != "default_dns_p53" {
t.Fatalf("ServicePortIDs() = %q, %q", udpID, legacyID)
}
tcpID, _ := ServicePortIDs("default", "dns", v1.ServicePort{Port: 53, Protocol: v1.ProtocolTCP})
if tcpID == udpID {
t.Fatal("TCP and UDP Services sharing a port received the same rule ID")
}
longPrefix := strings.Repeat("a", 63)
first, _ := ServicePortIDs(longPrefix, "first", udp)
second, _ := ServicePortIDs(longPrefix, "second", udp)
if len(first) > maxServicePortIDLength || first == second {
t.Fatalf("long Service IDs = %q, %q", first, second)
}
hyphenatedNamespace, _ := ServicePortIDs("a-b", "c", udp)
hyphenatedName, _ := ServicePortIDs("a", "b-c", udp)
if hyphenatedNamespace == hyphenatedName {
t.Fatalf("distinct Services received the same rule ID %q", hyphenatedNamespace)
}
}