ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
1 changed files with 145336 additions and 0 deletions
329
services/oci-l4-relay/main_test.go
Normal file
329
services/oci-l4-relay/main_test.go
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/expr"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (function roundTripperFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return function(request)
|
||||
}
|
||||
|
||||
func TestRepeatElectionAfterTermFailure(t *testing.T) {
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
var attempts atomic.Int32
|
||||
repeatElection(ctx, time.Millisecond, 10*time.Millisecond, func(_ context.Context, cancelElection context.CancelFunc) bool {
|
||||
if attempts.Add(1) == 1 {
|
||||
cancelElection()
|
||||
return false
|
||||
}
|
||||
stop()
|
||||
return false
|
||||
})
|
||||
if attempts.Load() != 2 {
|
||||
t.Fatalf("expected two election attempts, got %d", attempts.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestElectionRetryDelay(t *testing.T) {
|
||||
base := 5 * time.Second
|
||||
maximum := 60 * time.Second
|
||||
for unhealthyTerms, expected := range map[int]time.Duration{
|
||||
0: base,
|
||||
1: base,
|
||||
2: 10 * time.Second,
|
||||
3: 20 * time.Second,
|
||||
4: 40 * time.Second,
|
||||
5: maximum,
|
||||
20: maximum,
|
||||
} {
|
||||
if actual := electionRetryDelay(base, maximum, unhealthyTerms); actual != expected {
|
||||
t.Fatalf("terms=%d: got %s, want %s", unhealthyTerms, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCIDRPreservesHostAddress(t *testing.T) {
|
||||
address, err := parseCIDR("10.153.238.225/30")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := address.String(); got != "10.153.238.225/30" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetricsTarget(t *testing.T) {
|
||||
tunnel, err := parseCIDR("10.153.238.225/30")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target, err := parseMetricsTarget("http://10.40.68.115:19100/metrics", tunnel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if target.String() != "http://10.40.68.115:19100/metrics" {
|
||||
t.Fatalf("unexpected target %q", target)
|
||||
}
|
||||
for _, value := range []string{
|
||||
"https://10.40.68.115:19100/metrics",
|
||||
"http://10.153.238.225:19100/metrics",
|
||||
"http://10.40.68.115:19100/other",
|
||||
"http://10.40.68.115:19100/metrics?query=true",
|
||||
"http://192.0.2.1:19100/metrics",
|
||||
} {
|
||||
if _, err := parseMetricsTarget(value, tunnel); err == nil {
|
||||
t.Fatalf("expected %q to fail", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsHandlerRequiresActiveOwner(t *testing.T) {
|
||||
target, err := url.Parse("http://10.40.68.115:19100/metrics")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := &http.Client{Transport: roundTripperFunc(func(request *http.Request) (*http.Response, error) {
|
||||
if request.URL.String() != target.String() {
|
||||
t.Fatalf("unexpected upstream request %q", request.URL)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"text/plain; version=0.0.4"}},
|
||||
Body: io.NopCloser(strings.NewReader("relay_metric 1\n")),
|
||||
}, nil
|
||||
})}
|
||||
instance := &relay{config: config{metricsTarget: target}, metricsClient: client}
|
||||
inactive := httptest.NewRecorder()
|
||||
instance.metricsHandler().ServeHTTP(inactive, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
||||
if inactive.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("inactive status = %d", inactive.Code)
|
||||
}
|
||||
instance.active.Store(true)
|
||||
active := httptest.NewRecorder()
|
||||
instance.metricsHandler().ServeHTTP(active, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
||||
if active.Code != http.StatusOK || active.Body.String() != "relay_metric 1\n" || active.Header().Get("Content-Type") != "text/plain; version=0.0.4" {
|
||||
t.Fatalf("unexpected active response: status=%d content-type=%q body=%q", active.Code, active.Header().Get("Content-Type"), active.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsEndpointFollowsOwner(t *testing.T) {
|
||||
client := fake.NewSimpleClientset(&discoveryv1.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "oci-l4-relay-metrics", Namespace: "kube-system"},
|
||||
AddressType: discoveryv1.AddressTypeIPv4,
|
||||
})
|
||||
owner := &relay{
|
||||
client: client,
|
||||
config: config{
|
||||
namespace: "kube-system",
|
||||
podName: "relay-owner",
|
||||
podUID: "owner-uid",
|
||||
podIP: "10.167.40.23",
|
||||
podNodeName: "node-a",
|
||||
metricsSliceName: "oci-l4-relay-metrics",
|
||||
},
|
||||
}
|
||||
if err := owner.publishMetricsEndpoint(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slice, err := client.DiscoveryV1().EndpointSlices("kube-system").Get(context.Background(), "oci-l4-relay-metrics", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !owner.metricsEndpointMatches(slice) {
|
||||
t.Fatalf("endpoint does not match owner: %#v", slice.Endpoints)
|
||||
}
|
||||
standby := &relay{client: client, config: config{namespace: "kube-system", podUID: "standby-uid", metricsSliceName: "oci-l4-relay-metrics"}}
|
||||
if err := standby.clearMetricsEndpoint(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slice, err = client.DiscoveryV1().EndpointSlices("kube-system").Get(context.Background(), "oci-l4-relay-metrics", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(slice.Endpoints) != 1 {
|
||||
t.Fatal("standby cleared the active owner's endpoint")
|
||||
}
|
||||
if err := owner.clearMetricsEndpoint(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slice, err = client.DiscoveryV1().EndpointSlices("kube-system").Get(context.Background(), "oci-l4-relay-metrics", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(slice.Endpoints) != 0 {
|
||||
t.Fatal("owner endpoint was not cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowedRuleContainsGatewayAndPort(t *testing.T) {
|
||||
rule := allowedRule(net.ParseIP("10.104.40.127"), 443)
|
||||
if len(rule) != 12 {
|
||||
t.Fatalf("got %d expressions", len(rule))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsReturnRuleIsBoundedToEstablishedPeerTraffic(t *testing.T) {
|
||||
rule := metricsReturnRule(net.ParseIP("10.153.238.225"), net.ParseIP("10.40.68.115"), 19100)
|
||||
if len(rule) != 16 {
|
||||
t.Fatalf("got %d expressions", len(rule))
|
||||
}
|
||||
comparison, ok := rule[14].(*expr.Cmp)
|
||||
if !ok || comparison.Op != expr.CmpOpNeq {
|
||||
t.Fatal("metrics return rule does not require a matching conntrack state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRelayTargets(t *testing.T) {
|
||||
targets, err := parseRelayTargets(`[{"address":"10.66.153.202","ports":[993,25,465]},{"address":"10.104.40.127","ports":[443,80]}]`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(targets) != 2 ||
|
||||
targets[0].address.String() != "10.104.40.127" ||
|
||||
targets[1].address.String() != "10.66.153.202" ||
|
||||
targets[1].ports[0] != 25 ||
|
||||
targets[1].ports[2] != 993 {
|
||||
t.Fatalf("unexpected normalized targets: %#v", targets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRelayTargetsRejectsAmbiguousMappings(t *testing.T) {
|
||||
for name, value := range map[string]string{
|
||||
"unknown field": `[{"address":"10.104.40.127","ports":[80],"name":"web"}]`,
|
||||
"duplicate port": `[{"address":"10.104.40.127","ports":[80]},{"address":"10.66.153.202","ports":[80]}]`,
|
||||
"unknown port": `[{"address":"10.104.40.127","ports":[22]}]`,
|
||||
"submission": `[{"address":"10.66.153.202","ports":[587]}]`,
|
||||
"duplicate IP": `[{"address":"10.104.40.127","ports":[80]},{"address":"10.104.40.127","ports":[443]}]`,
|
||||
"IPv6": `[{"address":"2001:db8::1","ports":[443]}]`,
|
||||
"trailing": `[{"address":"10.104.40.127","ports":[80]}] true`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := parseRelayTargets(value); err == nil {
|
||||
t.Fatal("expected target validation failure")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNetworks(t *testing.T) {
|
||||
networks, err := parseNetworks("10.63.81.107/24, 10.162.8.60/24")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(networks) != 2 || networks[0].String() != "10.63.81.107/24" || networks[1].String() != "10.162.8.60/24" {
|
||||
t.Fatalf("unexpected networks: %v", networks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallCountersFromRules(t *testing.T) {
|
||||
rule := func(packets uint64) *nftables.Rule {
|
||||
return &nftables.Rule{Exprs: []expr.Any{&expr.Counter{Packets: packets}}}
|
||||
}
|
||||
targets := []relayTarget{{address: net.ParseIP("10.104.40.127").To4(), ports: []uint16{80, 443}}}
|
||||
counters, err := firewallCountersFromRules(map[string][]*nftables.Rule{
|
||||
"prerouting": {{}, rule(1), rule(2), rule(3)},
|
||||
"output": {rule(4), rule(5)},
|
||||
}, targets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if counters.tunnelAllowed["10.104.40.127:80"] != 1 || counters.tunnelAllowed["10.104.40.127:443"] != 2 || counters.tunnelDenied != 3 || counters.outerAllowed != 4 || counters.outerDenied != 5 {
|
||||
t.Fatalf("unexpected counters: %+v", counters)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallCountersRequireEveryRuleCounter(t *testing.T) {
|
||||
withCounter := &nftables.Rule{Exprs: []expr.Any{&expr.Counter{}}}
|
||||
withoutCounter := &nftables.Rule{}
|
||||
_, err := firewallCountersFromRules(map[string][]*nftables.Rule{
|
||||
"prerouting": {{}, withCounter, withoutCounter, withCounter},
|
||||
"output": {withCounter, withCounter},
|
||||
}, []relayTarget{{address: net.ParseIP("10.104.40.127").To4(), ports: []uint16{80, 443}}})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing counter error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHealth(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "health")
|
||||
if err := writeHealth(path, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := checkHealth(path, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeHealth(path, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := checkHealth(path, time.Minute); err == nil || err.Error() != "relay reports unhealthy" {
|
||||
t.Fatalf("expected unhealthy state failure, got %v", err)
|
||||
}
|
||||
if err := writeHealth(path, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := time.Now().Add(-time.Hour)
|
||||
if err := os.Chtimes(path, old, old); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := checkHealth(path, time.Minute); err == nil {
|
||||
t.Fatal("expected stale health failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyReversePathFilterAcceptsEffectiveLooseMode(t *testing.T) {
|
||||
for name, values := range map[string]map[string]string{
|
||||
"interface": {"all": "0\n", interfaceName: "2\n"},
|
||||
"all": {"all": "2\n", interfaceName: "1\n"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for directoryName, value := range values {
|
||||
directory := filepath.Join(root, directoryName)
|
||||
if err := os.MkdirAll(directory, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(directory, "rp_filter"), []byte(value), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := verifyReversePathFilter(root, interfaceName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyReversePathFilterRejectsStrictMode(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for _, name := range []string{"all", interfaceName} {
|
||||
directory := filepath.Join(root, name)
|
||||
if err := os.MkdirAll(directory, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(directory, "rp_filter"), []byte("1\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := verifyReversePathFilter(root, interfaceName); err == nil {
|
||||
t.Fatal("expected strict reverse-path filtering to fail")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue