ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
1 changed files with 145336 additions and 0 deletions
292
services/oci-l4-relay-guest/runtime_test.go
Normal file
292
services/oci-l4-relay-guest/runtime_test.go
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue