quorate/services/oci-l4-relay-guest/runtime_test.go

292 lines
10 KiB
Go

package main
import (
"context"
"encoding/base64"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestDeriveWireGuardKey(t *testing.T) {
privateKey, publicKey, err := deriveWireGuardKey([]byte("stable seed"))
if err != nil {
t.Fatal(err)
}
if got := base64.StdEncoding.EncodeToString(privateKey); got != "WBlFzkJLD63HWvvdo3EpeSDetKKqWSlTFpSi/I8K93o=" {
t.Fatalf("private key = %q", got)
}
if got := base64.StdEncoding.EncodeToString(publicKey); got != "20/HiRcrrKinQNH2WEJk8QcuB+3fv9uOW8Ptofn3CCA=" {
t.Fatalf("public key = %q", got)
}
}
func TestAdmissionRefreshCommandIsSupported(t *testing.T) {
if err := runAdmissionOperation(context.TODO(), validConfig(), "invalid"); err == nil {
t.Fatal("unknown admission operation was accepted")
}
}
func TestRenderRuleset(t *testing.T) {
configuration := validConfig()
ruleset := renderRuleset(configuration, "ens3")
for _, expected := range []string{
`iifname "ens3" tcp dport { 80, 443, 465, 993 } counter name web_allow_denied drop`,
`iifname "ens3" tcp flags syn tcp dport { 80, 443 } ip saddr @syn_quarantine_v4`,
"set syn_quarantine_v4 {\n type ipv4_addr\n flags dynamic,timeout\n counter\n timeout 1m",
`iifname "wg-oci0" tcp dport 19100 accept`,
`limit rate over 1000/second burst 1000 packets counter name web_syn_aggregate_dropped`,
`limit rate over 20000/second burst 20000 packets counter name web_packet_aggregate_dropped`,
`dnat ip to 10.104.40.127`,
} {
if !strings.Contains(ruleset, expected) {
t.Errorf("ruleset does not contain %q", expected)
}
}
if strings.Contains(ruleset, "587") {
t.Fatal("ruleset exposes cluster-local submission")
}
if strings.Index(ruleset, "ip saddr @syn_quarantine_v4") > strings.Index(ruleset, "limit rate over 1000/second") {
t.Error("per-source quarantine must precede the aggregate SYN ceiling")
}
if strings.Contains(ruleset, "10.129.13.87") || strings.Contains(ruleset, "dport { 25, 465, 993 }") {
t.Fatal("mail forwarding is present while mail configuration is inactive")
}
}
func TestRenderRulesetMapsOnlyDeclaredMailPorts(t *testing.T) {
configuration := validConfig()
configuration.MailRelayAddress = "10.129.13.87"
ruleset := renderRuleset(configuration, "ens3")
for _, expected := range []string{
`iifname "ens3" oifname "wg-oci0" ip daddr 10.104.40.127 tcp dport { 80, 443 } accept`,
`iifname "ens3" oifname "wg-oci0" ip daddr 10.129.13.87 tcp dport { 25, 465, 993 } accept`,
`iifname "ens3" tcp dport { 80, 443 } dnat ip to 10.104.40.127`,
`iifname "ens3" tcp dport { 25, 465, 993 } dnat ip to 10.129.13.87`,
`tcp dport 25 meter smtp_syn_by_source`,
"set smtp_syn_quarantine_v4 {\n type ipv4_addr\n flags dynamic,timeout\n counter\n timeout 600s\n size 65536",
`tcp dport 25 limit rate over 500/second burst 500 packets counter name smtp_syn_aggregate_dropped`,
`tcp dport { 465, 993 } meter auth_syn_by_source`,
`tcp dport { 465, 993 } limit rate over 200/second burst 200 packets counter name auth_syn_aggregate_dropped`,
} {
if !strings.Contains(ruleset, expected) {
t.Errorf("ruleset does not contain %q", expected)
}
}
for _, excluded := range []string{"snat", "masquerade", "dport 80 dnat ip to 10.129.13.87", "dport 25 dnat ip to 10.104.40.127"} {
if strings.Contains(strings.ToLower(ruleset), excluded) {
t.Errorf("ruleset contains %q", excluded)
}
}
}
func TestRulesetLoadsWithIndependentMailBudgets(t *testing.T) {
if os.Getenv("RELAY_NFT_TEST") != "1" {
t.Skip("RELAY_NFT_TEST is not enabled")
}
configuration := validConfig()
configuration.MailRelayAddress = "10.129.13.87"
path := filepath.Join(t.TempDir(), "ruleset.nft")
if err := os.WriteFile(path, []byte(renderRuleset(configuration, "ens3")), 0600); err != nil {
t.Fatal(err)
}
if output, err := exec.Command("nft", "--check", "--file", path).CombinedOutput(); err != nil {
t.Fatalf("nft check: %v: %s", err, output)
}
if output, err := exec.Command("nft", "--file", path).CombinedOutput(); err != nil {
t.Fatalf("nft apply: %v: %s", err, output)
}
output, err := exec.Command("nft", "list", "chain", "inet", "oci_l4_relay", "ingress_guard").CombinedOutput()
if err != nil {
t.Fatalf("nft list: %v: %s", err, output)
}
ruleset := string(output)
for _, expected := range []string{
"smtp_syn_by_source",
"auth_syn_by_source",
"web_syn_by_source",
"smtp_packet_aggregate_dropped",
"auth_packet_aggregate_dropped",
"web_packet_aggregate_dropped",
} {
if !strings.Contains(ruleset, expected) {
t.Fatalf("active ruleset does not contain %q", expected)
}
}
}
func TestTrafficControlCommandsSeparateServiceClasses(t *testing.T) {
configuration := validConfig()
configuration.MailRelayAddress = "10.129.13.87"
commands := trafficControlCommands(configuration)
joined := make([]string, 0, len(commands))
for _, command := range commands {
joined = append(joined, strings.Join(command, " "))
}
output := strings.Join(joined, "\n")
for _, expected := range []string{
"classid 1:10 htb rate 60mbit",
"classid 1:20 htb rate 15mbit",
"classid 1:30 htb rate 30mbit",
"dst_ip 10.104.40.127 ip_proto tcp dst_port 443 classid 1:10",
"dst_ip 10.129.13.87 ip_proto tcp dst_port 25 classid 1:20",
"dst_ip 10.129.13.87 ip_proto tcp dst_port 993 classid 1:30",
} {
if !strings.Contains(output, expected) {
t.Fatalf("traffic-control commands do not contain %q", expected)
}
}
}
func TestTrafficControlActivation(t *testing.T) {
if os.Getenv("RELAY_NFT_TEST") != "1" {
t.Skip("RELAY_NFT_TEST is not enabled")
}
if output, err := exec.Command("ip", "link", "add", interfaceName, "type", "dummy").CombinedOutput(); err != nil {
t.Fatalf("create test interface: %v: %s", err, output)
}
configuration := validConfig()
configuration.MailRelayAddress = "10.129.13.87"
for attempt := 0; attempt < 2; attempt++ {
clearTrafficControl(context.Background())
for _, command := range trafficControlCommands(configuration) {
if output, err := exec.Command(command[0], command[1:]...).CombinedOutput(); err != nil {
t.Fatalf("attempt %d: %s: %v: %s", attempt+1, strings.Join(command, " "), err, output)
}
}
}
output, err := exec.Command("tc", "class", "show", "dev", interfaceName).CombinedOutput()
if err != nil {
t.Fatalf("list traffic-control classes: %v: %s", err, output)
}
for _, expected := range []string{"1:10", "1:20", "1:30", "1:40"} {
if !strings.Contains(string(output), expected) {
t.Fatalf("active traffic-control classes do not contain %s: %s", expected, output)
}
}
}
func TestLoadConfigRejectsUnknownFields(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(path, []byte(`{"unknown":true}`), 0600); err != nil {
t.Fatal(err)
}
if _, err := loadConfig(path); err == nil {
t.Fatal("loadConfig accepted an unknown field")
}
}
func TestMalformedMailConfigurationDisablesOnlyMail(t *testing.T) {
configuration := validConfig()
configuration.MailRelayAddress = "not-an-address"
configuration.SMTPDeny = &smtpDenyConfig{}
contents, err := json.Marshal(configuration)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(path, contents, 0600); err != nil {
t.Fatal(err)
}
loaded, err := loadConfig(path)
if err != nil {
t.Fatal(err)
}
if loaded.MailRelayAddress != "" || loaded.SMTPDeny != nil || loaded.mailConfigurationError == "" {
t.Fatal("malformed mail configuration remained active")
}
if loaded.GatewayRelayAddress != configuration.GatewayRelayAddress {
t.Fatal("malformed mail configuration changed the web relay")
}
}
func TestValidMailConfiguration(t *testing.T) {
configuration := validConfig()
configuration.MailRelayAddress = "10.129.13.87"
configuration.SMTPDeny = &smtpDenyConfig{
Bucket: "bucket",
MaximumAddresses: 4096,
MaximumIntervals: 1024,
MaximumLifetimeSeconds: 86400,
MaximumObservationWindow: 600,
MinimumObservations: 20,
Namespace: "namespace",
Object: "smtp-deny/v1/current.json.gz",
}
configuration.SMTPDenyHelper = "/usr/local/libexec/oci-l4-relay-smtp-deny"
if err := configuration.validateMail(); err != nil {
t.Fatal(err)
}
}
func validConfig() config {
return config{
Admission: admissionConfig{
Bucket: "bucket",
MaximumAddresses: 3500000000,
MinimumAddresses: 1000000000,
MinimumIntervals: 10000,
Namespace: "namespace",
Object: "web-allow/v1/current.json.gz",
},
AdmissionHelper: "/usr/local/libexec/oci-l4-relay-admission",
GatewayRelayAddress: "10.104.40.127",
HomeWireGuardPublicKey: "key",
LogIngestHostname: "relay-logs.quorate.dev",
LogIngestSecretOCID: "log-secret",
MonitoringSourceIPv4: []string{"192.0.2.1", "192.0.2.2", "192.0.2.3"},
PublicKeySecretOCID: "public-secret",
RateLimitMbit: 150,
RelayHTTPQuarantineEnabled: true,
RelayHTTPQuarantineMinutes: 30,
RelayHTTPSRecentMinutes: 10,
RelayMetricsPort: 19100,
RelayAuthPacketAggregateBurst: 5000,
RelayAuthPacketAggregateRate: 5000,
RelayAuthSYNAggregateBurst: 200,
RelayAuthSYNAggregateRate: 200,
RelayAuthSYNBurst: 15,
RelayAuthSYNRate: 30,
RelaySMTPPacketAggregateBurst: 10000,
RelaySMTPPacketAggregateRate: 10000,
RelaySMTPSYNAggregateBurst: 500,
RelaySMTPSYNAggregateRate: 500,
RelaySMTPSYNBurst: 20,
RelaySMTPSYNRate: 60,
RelayTrackerMaxSources: 65536,
RelayTunnelAddress: "10.207.54.68/30",
RelayTunnelMTU: 1420,
RelayUnmatchedHTTPBurst: 5,
RelayUnmatchedHTTPRate: 30,
RelayWebPacketAggregateBurst: 20000,
RelayWebPacketAggregateRate: 20000,
RelayWebSYNAggregateBurst: 1000,
RelayWebSYNAggregateRate: 1000,
RelayWebSYNBurst: 100,
RelayWebSYNRate: 120,
WireGuardListenPort: 51820,
WireGuardSeedSecretOCID: "seed-secret",
}
}
func TestValidLogIngestToken(t *testing.T) {
for _, test := range []struct {
name string
token string
valid bool
}{
{name: "valid", token: strings.Repeat("a", 48), valid: true},
{name: "short", token: strings.Repeat("a", 31)},
{name: "newline", token: strings.Repeat("a", 47) + "\n"},
{name: "space", token: strings.Repeat("a", 47) + " "},
} {
t.Run(test.name, func(t *testing.T) {
if got := validLogIngestToken([]byte(test.token)); got != test.valid {
t.Fatalf("validLogIngestToken() = %t", got)
}
})
}
}