mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-21 01:38:32 +00:00

cmd/containerboot,kube/ingressservices: proxy VIPService TCP/UDP traffic to cluster Services This PR is part of the work to implement HA for Kubernetes Operator's network layer proxy. Adds logic to containerboot to monitor mounted ingress firewall configuration rules and update iptables/nftables rules as the config changes. Also adds new shared types for the ingress configuration. The implementation is intentionally similar to that for HA for egress proxy. Updates tailscale/tailscale#15895 Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk> Signed-off-by: Irbe Krumina <irbe@tailscale.com>
224 lines
6.8 KiB
Go
224 lines
6.8 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"net/netip"
|
|
"testing"
|
|
|
|
"tailscale.com/kube/ingressservices"
|
|
"tailscale.com/util/linuxfw"
|
|
)
|
|
|
|
func TestSyncIngressConfigs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
currentConfigs *ingressservices.Configs
|
|
currentStatus *ingressservices.Status
|
|
wantServices map[string]struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
}
|
|
}{
|
|
{
|
|
name: "add_new_rules_when_no_existing_config",
|
|
currentConfigs: &ingressservices.Configs{
|
|
"svc:foo": makeServiceConfig("100.64.0.1", "10.0.0.1", "", ""),
|
|
},
|
|
currentStatus: nil,
|
|
wantServices: map[string]struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
}{
|
|
"svc:foo": makeWantService("100.64.0.1", "10.0.0.1"),
|
|
},
|
|
},
|
|
{
|
|
name: "add_multiple_services",
|
|
currentConfigs: &ingressservices.Configs{
|
|
"svc:foo": makeServiceConfig("100.64.0.1", "10.0.0.1", "", ""),
|
|
"svc:bar": makeServiceConfig("100.64.0.2", "10.0.0.2", "", ""),
|
|
"svc:baz": makeServiceConfig("100.64.0.3", "10.0.0.3", "", ""),
|
|
},
|
|
currentStatus: nil,
|
|
wantServices: map[string]struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
}{
|
|
"svc:foo": makeWantService("100.64.0.1", "10.0.0.1"),
|
|
"svc:bar": makeWantService("100.64.0.2", "10.0.0.2"),
|
|
"svc:baz": makeWantService("100.64.0.3", "10.0.0.3"),
|
|
},
|
|
},
|
|
{
|
|
name: "add_both_ipv4_and_ipv6_rules",
|
|
currentConfigs: &ingressservices.Configs{
|
|
"svc:foo": makeServiceConfig("100.64.0.1", "10.0.0.1", "2001:db8::1", "2001:db8::2"),
|
|
},
|
|
currentStatus: nil,
|
|
wantServices: map[string]struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
}{
|
|
"svc:foo": makeWantService("2001:db8::1", "2001:db8::2"),
|
|
},
|
|
},
|
|
{
|
|
name: "add_ipv6_only_rules",
|
|
currentConfigs: &ingressservices.Configs{
|
|
"svc:ipv6": makeServiceConfig("", "", "2001:db8::10", "2001:db8::20"),
|
|
},
|
|
currentStatus: nil,
|
|
wantServices: map[string]struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
}{
|
|
"svc:ipv6": makeWantService("2001:db8::10", "2001:db8::20"),
|
|
},
|
|
},
|
|
{
|
|
name: "delete_all_rules_when_config_removed",
|
|
currentConfigs: nil,
|
|
currentStatus: &ingressservices.Status{
|
|
Configs: ingressservices.Configs{
|
|
"svc:foo": makeServiceConfig("100.64.0.1", "10.0.0.1", "", ""),
|
|
"svc:bar": makeServiceConfig("100.64.0.2", "10.0.0.2", "", ""),
|
|
},
|
|
PodIPv4: "10.0.0.2", // Current pod IPv4
|
|
PodIPv6: "2001:db8::2", // Current pod IPv6
|
|
},
|
|
wantServices: map[string]struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
}{},
|
|
},
|
|
{
|
|
name: "add_remove_modify",
|
|
currentConfigs: &ingressservices.Configs{
|
|
"svc:foo": makeServiceConfig("100.64.0.1", "10.0.0.2", "", ""), // Changed cluster IP
|
|
"svc:new": makeServiceConfig("100.64.0.4", "10.0.0.4", "", ""),
|
|
},
|
|
currentStatus: &ingressservices.Status{
|
|
Configs: ingressservices.Configs{
|
|
"svc:foo": makeServiceConfig("100.64.0.1", "10.0.0.1", "", ""),
|
|
"svc:bar": makeServiceConfig("100.64.0.2", "10.0.0.2", "", ""),
|
|
"svc:baz": makeServiceConfig("100.64.0.3", "10.0.0.3", "", ""),
|
|
},
|
|
PodIPv4: "10.0.0.2", // Current pod IPv4
|
|
PodIPv6: "2001:db8::2", // Current pod IPv6
|
|
},
|
|
wantServices: map[string]struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
}{
|
|
"svc:foo": makeWantService("100.64.0.1", "10.0.0.2"),
|
|
"svc:new": makeWantService("100.64.0.4", "10.0.0.4"),
|
|
},
|
|
},
|
|
{
|
|
name: "update_with_outdated_status",
|
|
currentConfigs: &ingressservices.Configs{
|
|
"svc:web": makeServiceConfig("100.64.0.10", "10.0.0.10", "", ""),
|
|
"svc:web-ipv6": {
|
|
IPv6Mapping: &ingressservices.Mapping{
|
|
TailscaleServiceIP: netip.MustParseAddr("2001:db8::10"),
|
|
ClusterIP: netip.MustParseAddr("2001:db8::20"),
|
|
},
|
|
},
|
|
"svc:api": makeServiceConfig("100.64.0.20", "10.0.0.20", "", ""),
|
|
},
|
|
currentStatus: &ingressservices.Status{
|
|
Configs: ingressservices.Configs{
|
|
"svc:web": makeServiceConfig("100.64.0.10", "10.0.0.10", "", ""),
|
|
"svc:web-ipv6": {
|
|
IPv6Mapping: &ingressservices.Mapping{
|
|
TailscaleServiceIP: netip.MustParseAddr("2001:db8::10"),
|
|
ClusterIP: netip.MustParseAddr("2001:db8::20"),
|
|
},
|
|
},
|
|
"svc:old": makeServiceConfig("100.64.0.30", "10.0.0.30", "", ""),
|
|
},
|
|
PodIPv4: "10.0.0.1", // Outdated pod IP
|
|
PodIPv6: "2001:db8::1", // Outdated pod IP
|
|
},
|
|
wantServices: map[string]struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
}{
|
|
"svc:web": makeWantService("100.64.0.10", "10.0.0.10"),
|
|
"svc:web-ipv6": makeWantService("2001:db8::10", "2001:db8::20"),
|
|
"svc:api": makeWantService("100.64.0.20", "10.0.0.20"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var nfr linuxfw.NetfilterRunner = linuxfw.NewFakeNetfilterRunner()
|
|
|
|
ep := &ingressProxy{
|
|
nfr: nfr,
|
|
podIPv4: "10.0.0.2", // Current pod IPv4
|
|
podIPv6: "2001:db8::2", // Current pod IPv6
|
|
}
|
|
|
|
err := ep.syncIngressConfigs(tt.currentConfigs, tt.currentStatus)
|
|
if err != nil {
|
|
t.Fatalf("syncIngressConfigs failed: %v", err)
|
|
}
|
|
|
|
fake := nfr.(*linuxfw.FakeNetfilterRunner)
|
|
gotServices := fake.GetServiceState()
|
|
if len(gotServices) != len(tt.wantServices) {
|
|
t.Errorf("got %d services, want %d", len(gotServices), len(tt.wantServices))
|
|
}
|
|
for svc, want := range tt.wantServices {
|
|
got, ok := gotServices[svc]
|
|
if !ok {
|
|
t.Errorf("service %s not found", svc)
|
|
continue
|
|
}
|
|
if got.TailscaleServiceIP != want.TailscaleServiceIP {
|
|
t.Errorf("service %s: got TailscaleServiceIP %v, want %v", svc, got.TailscaleServiceIP, want.TailscaleServiceIP)
|
|
}
|
|
if got.ClusterIP != want.ClusterIP {
|
|
t.Errorf("service %s: got ClusterIP %v, want %v", svc, got.ClusterIP, want.ClusterIP)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func makeServiceConfig(tsIP, clusterIP string, tsIP6, clusterIP6 string) ingressservices.Config {
|
|
cfg := ingressservices.Config{}
|
|
if tsIP != "" && clusterIP != "" {
|
|
cfg.IPv4Mapping = &ingressservices.Mapping{
|
|
TailscaleServiceIP: netip.MustParseAddr(tsIP),
|
|
ClusterIP: netip.MustParseAddr(clusterIP),
|
|
}
|
|
}
|
|
if tsIP6 != "" && clusterIP6 != "" {
|
|
cfg.IPv6Mapping = &ingressservices.Mapping{
|
|
TailscaleServiceIP: netip.MustParseAddr(tsIP6),
|
|
ClusterIP: netip.MustParseAddr(clusterIP6),
|
|
}
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func makeWantService(tsIP, clusterIP string) struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
} {
|
|
return struct {
|
|
TailscaleServiceIP netip.Addr
|
|
ClusterIP netip.Addr
|
|
}{
|
|
TailscaleServiceIP: netip.MustParseAddr(tsIP),
|
|
ClusterIP: netip.MustParseAddr(clusterIP),
|
|
}
|
|
}
|