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

216 lines
10 KiB
Go

package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/netip"
"os"
)
type config struct {
Admission admissionConfig `json:"admission"`
AdmissionHelper string `json:"admission_helper"`
GatewayRelayAddress string `json:"gateway_relay_address"`
HomeWireGuardPublicKey string `json:"home_wireguard_public_key"`
LogIngestHostname string `json:"log_ingest_hostname"`
LogIngestSecretOCID string `json:"log_ingest_secret_ocid"`
MailRelayAddress string `json:"mail_relay_address,omitempty"`
MonitoringSourceIPv4 []string `json:"monitoring_source_ipv4"`
PublicKeySecretOCID string `json:"public_key_secret_ocid"`
RateLimitMbit int `json:"rate_limit_mbit"`
RelayHTTPQuarantineEnabled bool `json:"relay_http_quarantine_enabled"`
RelayHTTPQuarantineMinutes int `json:"relay_http_quarantine_minutes"`
RelayHTTPSRecentMinutes int `json:"relay_https_recent_minutes"`
RelayMetricsPort int `json:"relay_metrics_port"`
RelayAuthPacketAggregateBurst int `json:"relay_auth_packet_aggregate_burst"`
RelayAuthPacketAggregateRate int `json:"relay_auth_packet_aggregate_rate_per_second"`
RelayAuthSYNAggregateBurst int `json:"relay_auth_syn_aggregate_burst"`
RelayAuthSYNAggregateRate int `json:"relay_auth_syn_aggregate_rate_per_second"`
RelayAuthSYNBurst int `json:"relay_auth_syn_burst"`
RelayAuthSYNRate int `json:"relay_auth_syn_rate_per_minute"`
RelaySMTPPacketAggregateBurst int `json:"relay_smtp_packet_aggregate_burst"`
RelaySMTPPacketAggregateRate int `json:"relay_smtp_packet_aggregate_rate_per_second"`
RelaySMTPSYNAggregateBurst int `json:"relay_smtp_syn_aggregate_burst"`
RelaySMTPSYNAggregateRate int `json:"relay_smtp_syn_aggregate_rate_per_second"`
RelaySMTPSYNBurst int `json:"relay_smtp_syn_burst"`
RelaySMTPSYNRate int `json:"relay_smtp_syn_rate_per_minute"`
RelayTrackerMaxSources int `json:"relay_tracker_max_sources"`
RelayTunnelAddress string `json:"relay_tunnel_address"`
RelayTunnelMTU int `json:"relay_tunnel_mtu"`
RelayUnmatchedHTTPBurst int `json:"relay_unmatched_http_burst"`
RelayUnmatchedHTTPRate int `json:"relay_unmatched_http_rate_per_hour"`
RelayWebPacketAggregateBurst int `json:"relay_web_packet_aggregate_burst"`
RelayWebPacketAggregateRate int `json:"relay_web_packet_aggregate_rate_per_second"`
RelayWebSYNAggregateBurst int `json:"relay_web_syn_aggregate_burst"`
RelayWebSYNAggregateRate int `json:"relay_web_syn_aggregate_rate_per_second"`
RelayWebSYNBurst int `json:"relay_web_syn_burst"`
RelayWebSYNRate int `json:"relay_web_syn_rate_per_minute"`
SMTPDeny *smtpDenyConfig `json:"smtp_deny,omitempty"`
SMTPDenyHelper string `json:"smtp_deny_helper,omitempty"`
WireGuardListenPort int `json:"wireguard_listen_port"`
WireGuardSeedSecretOCID string `json:"wireguard_seed_secret_ocid"`
mailConfigurationError string
}
type admissionConfig struct {
Bucket string `json:"bucket"`
MaximumAddresses uint64 `json:"maximum_addresses"`
MinimumAddresses uint64 `json:"minimum_addresses"`
MinimumIntervals int `json:"minimum_intervals"`
Namespace string `json:"namespace"`
Object string `json:"object"`
}
type smtpDenyConfig struct {
Bucket string `json:"bucket"`
MaximumAddresses uint64 `json:"maximum_addresses"`
MaximumIntervals int `json:"maximum_intervals"`
MaximumLifetimeSeconds int `json:"maximum_lifetime_seconds"`
MaximumObservationWindow int `json:"maximum_observation_window_seconds"`
MinimumObservations uint64 `json:"minimum_observations"`
Namespace string `json:"namespace"`
Object string `json:"object"`
}
func loadConfig(path string) (config, error) {
contents, err := os.ReadFile(path)
if err != nil {
return config{}, err
}
var value config
decoder := json.NewDecoder(bytes.NewReader(contents))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return config{}, err
}
if err := value.validate(); err != nil {
return config{}, err
}
if err := value.validateMail(); err != nil {
value.MailRelayAddress = ""
value.SMTPDeny = nil
value.SMTPDenyHelper = ""
value.mailConfigurationError = err.Error()
}
return value, nil
}
func (value config) validate() error {
required := map[string]string{
"admission.bucket": value.Admission.Bucket,
"admission.namespace": value.Admission.Namespace,
"admission.object": value.Admission.Object,
"admission_helper": value.AdmissionHelper,
"gateway_relay_address": value.GatewayRelayAddress,
"home_wireguard_public_key": value.HomeWireGuardPublicKey,
"log_ingest_hostname": value.LogIngestHostname,
"log_ingest_secret_ocid": value.LogIngestSecretOCID,
"public_key_secret_ocid": value.PublicKeySecretOCID,
"relay_tunnel_address": value.RelayTunnelAddress,
"wireguard_seed_secret_ocid": value.WireGuardSeedSecretOCID,
}
for name, candidate := range required {
if candidate == "" {
return fmt.Errorf("%s is required", name)
}
}
positive := map[string]int{
"admission.minimum_intervals": value.Admission.MinimumIntervals,
"rate_limit_mbit": value.RateLimitMbit,
"relay_http_quarantine_minutes": value.RelayHTTPQuarantineMinutes,
"relay_https_recent_minutes": value.RelayHTTPSRecentMinutes,
"relay_metrics_port": value.RelayMetricsPort,
"relay_tracker_max_sources": value.RelayTrackerMaxSources,
"relay_tunnel_mtu": value.RelayTunnelMTU,
"relay_unmatched_http_burst": value.RelayUnmatchedHTTPBurst,
"relay_unmatched_http_rate_per_hour": value.RelayUnmatchedHTTPRate,
"relay_web_packet_aggregate_burst": value.RelayWebPacketAggregateBurst,
"relay_web_packet_aggregate_rate_per_second": value.RelayWebPacketAggregateRate,
"relay_web_syn_aggregate_burst": value.RelayWebSYNAggregateBurst,
"relay_web_syn_aggregate_rate_per_second": value.RelayWebSYNAggregateRate,
"relay_web_syn_burst": value.RelayWebSYNBurst,
"relay_web_syn_rate_per_minute": value.RelayWebSYNRate,
"wireguard_listen_port": value.WireGuardListenPort,
}
for name, candidate := range positive {
if candidate <= 0 {
return fmt.Errorf("%s must be positive", name)
}
}
if value.RelayMetricsPort > 65535 {
return errors.New("relay_metrics_port must not exceed 65535")
}
if value.Admission.MinimumAddresses == 0 || value.Admission.MaximumAddresses < value.Admission.MinimumAddresses {
return errors.New("admission address bounds are invalid")
}
if len(value.MonitoringSourceIPv4) < 3 {
return errors.New("at least three monitoring source IPv4 addresses are required")
}
seen := make(map[netip.Addr]bool, len(value.MonitoringSourceIPv4))
for _, candidate := range value.MonitoringSourceIPv4 {
address, err := netip.ParseAddr(candidate)
if err != nil || !address.Is4() || address.String() != candidate || seen[address] {
return fmt.Errorf("invalid or duplicate monitoring source IPv4 address %q", candidate)
}
seen[address] = true
}
return nil
}
func (value config) validateMail() error {
if value.MailRelayAddress == "" && value.SMTPDeny == nil && value.SMTPDenyHelper == "" {
return nil
}
address, err := netip.ParseAddr(value.MailRelayAddress)
if err != nil || !address.Is4() || address.String() != value.MailRelayAddress || value.MailRelayAddress == value.GatewayRelayAddress {
return errors.New("mail_relay_address must be a distinct canonical IPv4 address")
}
if value.SMTPDeny == nil {
return errors.New("smtp_deny is required when mail relay is configured")
}
required := map[string]string{
"smtp_deny.bucket": value.SMTPDeny.Bucket,
"smtp_deny.namespace": value.SMTPDeny.Namespace,
"smtp_deny.object": value.SMTPDeny.Object,
"smtp_deny_helper": value.SMTPDenyHelper,
}
for name, candidate := range required {
if candidate == "" {
return fmt.Errorf("%s is required", name)
}
}
if value.SMTPDeny.MinimumObservations == 0 ||
value.SMTPDeny.MaximumAddresses == 0 ||
value.SMTPDeny.MaximumIntervals <= 0 ||
value.SMTPDeny.MaximumLifetimeSeconds <= 0 ||
value.SMTPDeny.MaximumObservationWindow <= 0 ||
value.SMTPDeny.MaximumObservationWindow > value.SMTPDeny.MaximumLifetimeSeconds {
return errors.New("smtp_deny policy bounds are invalid")
}
positive := map[string]int{
"relay_auth_packet_aggregate_burst": value.RelayAuthPacketAggregateBurst,
"relay_auth_packet_aggregate_rate_per_second": value.RelayAuthPacketAggregateRate,
"relay_auth_syn_aggregate_burst": value.RelayAuthSYNAggregateBurst,
"relay_auth_syn_aggregate_rate_per_second": value.RelayAuthSYNAggregateRate,
"relay_auth_syn_burst": value.RelayAuthSYNBurst,
"relay_auth_syn_rate_per_minute": value.RelayAuthSYNRate,
"relay_smtp_packet_aggregate_burst": value.RelaySMTPPacketAggregateBurst,
"relay_smtp_packet_aggregate_rate_per_second": value.RelaySMTPPacketAggregateRate,
"relay_smtp_syn_aggregate_burst": value.RelaySMTPSYNAggregateBurst,
"relay_smtp_syn_aggregate_rate_per_second": value.RelaySMTPSYNAggregateRate,
"relay_smtp_syn_burst": value.RelaySMTPSYNBurst,
"relay_smtp_syn_rate_per_minute": value.RelaySMTPSYNRate,
}
for name, candidate := range positive {
if candidate <= 0 {
return fmt.Errorf("%s must be positive", name)
}
}
if value.RelayAuthPacketAggregateBurst < value.RelayAuthPacketAggregateRate ||
value.RelaySMTPPacketAggregateBurst < value.RelaySMTPPacketAggregateRate {
return errors.New("mail packet aggregate bursts must cover at least one second")
}
return nil
}