216 lines
10 KiB
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
|
||
|
|
}
|