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

655 lines
24 KiB
Go
Raw Permalink Normal View History

package main
import (
"context"
"crypto/ecdh"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
"time"
)
const (
webAllowStateDirectory = "/var/lib/oci-l4-relay/web-allow"
smtpDenyStateDirectory = "/var/lib/oci-l4-relay/smtp-deny"
interfaceName = "wg-oci0"
logIngestTokenPath = "/etc/oci-l4-relay/log-ingest.token"
telemetrySnapshotPath = "/run/oci-l4-relay-telemetry/current.json"
privateKeyPath = "/etc/oci-l4-relay/private.key"
logIngestGroup = "oci-l4-relay-logs"
)
func bootstrap(ctx context.Context, configuration config) error {
publicInterface, err := defaultInterface()
if err != nil {
return err
}
fmt.Println("bootstrap_stage=fail_closed_firewall")
if err := configureHost(ctx, configuration, publicInterface); err != nil {
return err
}
if err := restoreAdmission(ctx, configuration); err != nil {
fmt.Printf("bootstrap_stage=cached_admission cache_restored=false\n")
} else {
fmt.Printf("bootstrap_stage=cached_admission cache_restored=true\n")
}
if configuration.MailRelayAddress != "" {
if err := restoreSMTPDeny(ctx, configuration); err != nil {
fmt.Printf("bootstrap_stage=cached_smtp_deny cache_restored=false\n")
} else {
fmt.Printf("bootstrap_stage=cached_smtp_deny cache_restored=true\n")
}
} else if configuration.mailConfigurationError != "" {
fmt.Printf("bootstrap_stage=mail_configuration active=false reason=%q\n", configuration.mailConfigurationError)
}
if fileExists(privateKeyPath) {
if err := configureTunnel(ctx, configuration); err != nil {
fmt.Printf("bootstrap_stage=cached_tunnel cache_restored=false\n")
} else {
fmt.Printf("bootstrap_stage=cached_tunnel cache_restored=true\n")
}
}
fmt.Println("bootstrap_stage=instance_identity")
clients, err := newOCIClients()
if err != nil {
return err
}
if err := configureIdentityAndTunnel(ctx, clients, configuration); err != nil {
return err
}
fmt.Println("bootstrap_stage=wireguard_handshake")
if err := waitForHandshake(ctx, 60*time.Second); err != nil {
return err
}
fmt.Println("bootstrap_stage=fresh_admission")
if err := updateAdmissionWithClients(ctx, clients, configuration); err != nil {
return err
}
if configuration.MailRelayAddress != "" {
if err := updateSMTPDenyWithClients(ctx, clients, configuration); err != nil {
fmt.Println("bootstrap_stage=smtp_deny current_available=false")
} else {
fmt.Println("bootstrap_stage=smtp_deny current_available=true")
}
}
if err := updateLogCredentialWithClients(ctx, clients, configuration); err != nil {
fmt.Println("bootstrap_stage=log_ingest credential_available=false")
} else {
fmt.Println("bootstrap_stage=log_ingest credential_available=true")
}
fmt.Println("bootstrap_stage=complete")
return nil
}
func reconcile(ctx context.Context, configuration config) error {
clients, err := newOCIClients()
if err != nil {
return err
}
if err := configureIdentityAndTunnel(ctx, clients, configuration); err != nil {
return err
}
if err := waitForHandshake(ctx, 180*time.Second); err != nil {
return err
}
if err := updateAdmissionWithClients(ctx, clients, configuration); err != nil {
return err
}
if configuration.MailRelayAddress != "" {
if err := updateSMTPDenyWithClients(ctx, clients, configuration); err != nil {
fmt.Println("reconcile_stage=smtp_deny current_available=false")
} else {
fmt.Println("reconcile_stage=smtp_deny current_available=true")
}
}
if err := updateLogCredentialWithClients(ctx, clients, configuration); err != nil {
fmt.Println("reconcile_stage=log_ingest credential_available=false")
} else {
fmt.Println("reconcile_stage=log_ingest credential_available=true")
}
return nil
}
func configureHost(ctx context.Context, configuration config, publicInterface string) error {
if err := os.MkdirAll("/etc/oci-l4-relay", 0700); err != nil {
return err
}
groupID, err := logIngestGroupID()
if err != nil {
return err
}
if err := os.Chown("/etc/oci-l4-relay", 0, groupID); err != nil {
return err
}
if err := os.Chmod("/etc/oci-l4-relay", 0710); err != nil {
return err
}
if err := os.MkdirAll(webAllowStateDirectory, 0700); err != nil {
return err
}
if configuration.MailRelayAddress != "" {
if err := os.MkdirAll(smtpDenyStateDirectory, 0700); err != nil {
return err
}
}
if err := writeAtomic("/etc/sysctl.d/90-oci-l4-relay.conf", []byte("net.ipv4.ip_forward = 1\nnet.ipv4.tcp_syncookies = 1\nnet.netfilter.nf_conntrack_acct = 1\nnet.netfilter.nf_conntrack_max = 262144\nnet.netfilter.nf_conntrack_timestamp = 1\n"), 0644); err != nil {
return err
}
ruleset := renderRuleset(configuration, publicInterface)
if err := writeAtomic("/etc/sysconfig/nftables.conf", []byte(ruleset), 0600); err != nil {
return err
}
settings := []string{
"net.ipv4.ip_forward=1",
"net.ipv4.tcp_syncookies=1",
"net.netfilter.nf_conntrack_acct=1",
"net.netfilter.nf_conntrack_max=262144",
"net.netfilter.nf_conntrack_timestamp=1",
}
for _, setting := range settings {
if err := run(ctx, "/usr/sbin/sysctl", "--write", setting); err != nil {
return err
}
}
if err := run(ctx, "/usr/sbin/nft", "--check", "--file", "/etc/sysconfig/nftables.conf"); err != nil {
return err
}
return run(ctx, "/usr/sbin/nft", "--file", "/etc/sysconfig/nftables.conf")
}
func configureIdentityAndTunnel(ctx context.Context, clients ociClients, configuration config) error {
seed, err := clients.getSecret(ctx, configuration.WireGuardSeedSecretOCID)
if err != nil {
return fmt.Errorf("retrieve WireGuard seed: %w", err)
}
privateKey, publicKey, err := deriveWireGuardKey(seed)
for index := range seed {
seed[index] = 0
}
if err != nil {
return err
}
defer func() {
for index := range privateKey {
privateKey[index] = 0
}
}()
encodedPrivateKey := []byte(base64.StdEncoding.EncodeToString(privateKey) + "\n")
if err := writeAtomic(privateKeyPath, encodedPrivateKey, 0600); err != nil {
return err
}
for index := range encodedPrivateKey {
encodedPrivateKey[index] = 0
}
encodedPublicKey := base64.StdEncoding.EncodeToString(publicKey)
if err := clients.publishSecret(ctx, configuration.PublicKeySecretOCID, []byte(encodedPublicKey)); err != nil {
return fmt.Errorf("publish WireGuard public key: %w", err)
}
return configureTunnel(ctx, configuration)
}
func configureTunnel(ctx context.Context, configuration config) error {
if err := ensureWireGuardInterface(ctx); err != nil {
return err
}
if err := run(ctx, "/usr/bin/wg", "set", interfaceName, "listen-port", strconv.Itoa(configuration.WireGuardListenPort), "private-key", privateKeyPath, "peer", configuration.HomeWireGuardPublicKey, "allowed-ips", "0.0.0.0/0"); err != nil {
return err
}
commands := [][]string{
{"/usr/sbin/ip", "address", "replace", configuration.RelayTunnelAddress, "dev", interfaceName},
{"/usr/sbin/ip", "link", "set", "dev", interfaceName, "mtu", strconv.Itoa(configuration.RelayTunnelMTU), "up"},
{"/usr/sbin/ip", "route", "replace", configuration.GatewayRelayAddress + "/32", "dev", interfaceName},
}
if configuration.MailRelayAddress != "" {
commands = append(commands, []string{"/usr/sbin/ip", "route", "replace", configuration.MailRelayAddress + "/32", "dev", interfaceName})
}
for _, command := range commands {
if err := run(ctx, command[0], command[1:]...); err != nil {
return err
}
}
clearTrafficControl(ctx)
for _, command := range trafficControlCommands(configuration) {
if err := run(ctx, command[0], command[1:]...); err != nil {
return err
}
}
return nil
}
func clearTrafficControl(ctx context.Context) {
// Oracle Linux cannot replace an existing HTB root qdisc; deletion also clears partial bootstrap state.
_ = exec.CommandContext(ctx, "/usr/sbin/tc", "qdisc", "delete", "dev", interfaceName, "root").Run()
}
func trafficControlCommands(configuration config) [][]string {
rate := strconv.Itoa(configuration.RateLimitMbit) + "mbit"
webRate := strconv.Itoa(max(1, configuration.RateLimitMbit*40/100)) + "mbit"
smtpRate := strconv.Itoa(max(1, configuration.RateLimitMbit*10/100)) + "mbit"
authRate := strconv.Itoa(max(1, configuration.RateLimitMbit*20/100)) + "mbit"
defaultRate := strconv.Itoa(max(1, configuration.RateLimitMbit*10/100)) + "mbit"
commands := [][]string{
{"/usr/sbin/tc", "qdisc", "add", "dev", interfaceName, "root", "handle", "1:", "htb", "default", "40"},
{"/usr/sbin/tc", "class", "replace", "dev", interfaceName, "parent", "1:", "classid", "1:1", "htb", "rate", rate, "ceil", rate},
{"/usr/sbin/tc", "class", "replace", "dev", interfaceName, "parent", "1:1", "classid", "1:10", "htb", "rate", webRate, "ceil", rate},
{"/usr/sbin/tc", "class", "replace", "dev", interfaceName, "parent", "1:1", "classid", "1:20", "htb", "rate", smtpRate, "ceil", rate},
{"/usr/sbin/tc", "class", "replace", "dev", interfaceName, "parent", "1:1", "classid", "1:30", "htb", "rate", authRate, "ceil", rate},
{"/usr/sbin/tc", "class", "replace", "dev", interfaceName, "parent", "1:1", "classid", "1:40", "htb", "rate", defaultRate, "ceil", rate},
{"/usr/sbin/tc", "qdisc", "replace", "dev", interfaceName, "parent", "1:10", "handle", "10:", "fq_codel"},
{"/usr/sbin/tc", "qdisc", "replace", "dev", interfaceName, "parent", "1:20", "handle", "20:", "fq_codel"},
{"/usr/sbin/tc", "qdisc", "replace", "dev", interfaceName, "parent", "1:30", "handle", "30:", "fq_codel"},
{"/usr/sbin/tc", "qdisc", "replace", "dev", interfaceName, "parent", "1:40", "handle", "40:", "fq_codel"},
}
priority := 10
addFilter := func(address string, port int, classID string) {
commands = append(commands, []string{
"/usr/sbin/tc", "filter", "replace", "dev", interfaceName,
"protocol", "ip", "parent", "1:", "prio", strconv.Itoa(priority),
"flower", "dst_ip", address, "ip_proto", "tcp",
"dst_port", strconv.Itoa(port), "classid", classID,
})
priority++
}
addFilter(configuration.GatewayRelayAddress, 80, "1:10")
addFilter(configuration.GatewayRelayAddress, 443, "1:10")
if configuration.MailRelayAddress != "" {
addFilter(configuration.MailRelayAddress, 25, "1:20")
for _, port := range []int{465, 993} {
addFilter(configuration.MailRelayAddress, port, "1:30")
}
}
return commands
}
func ensureWireGuardInterface(ctx context.Context) error {
if exec.CommandContext(ctx, "/usr/sbin/ip", "link", "show", "dev", interfaceName).Run() == nil {
return nil
}
return run(ctx, "/usr/sbin/ip", "link", "add", "dev", interfaceName, "type", "wireguard")
}
func deriveWireGuardKey(seed []byte) ([]byte, []byte, error) {
digest := sha256.Sum256(seed)
privateKey := append([]byte(nil), digest[:]...)
privateKey[0] &= 248
privateKey[31] &= 127
privateKey[31] |= 64
key, err := ecdh.X25519().NewPrivateKey(privateKey)
if err != nil {
return nil, nil, err
}
return privateKey, key.PublicKey().Bytes(), nil
}
func updateAdmission(ctx context.Context, configuration config) error {
clients, err := newOCIClients()
if err != nil {
return err
}
return updateAdmissionWithClients(ctx, clients, configuration)
}
func updateSMTPDeny(ctx context.Context, configuration config) error {
clients, err := newOCIClients()
if err != nil {
return completeSMTPDenyRefresh(err)
}
return updateSMTPDenyWithClients(ctx, clients, configuration)
}
func updateLogCredential(ctx context.Context, configuration config) error {
clients, err := newOCIClients()
if err != nil {
return err
}
return updateLogCredentialWithClients(ctx, clients, configuration)
}
func updateLogCredentialWithClients(ctx context.Context, clients ociClients, configuration config) error {
token, err := clients.getSecret(ctx, configuration.LogIngestSecretOCID)
if err != nil {
return fmt.Errorf("retrieve log ingestion credential: %w", err)
}
defer func() {
for index := range token {
token[index] = 0
}
}()
if !validLogIngestToken(token) {
return errors.New("log ingestion credential is invalid")
}
groupID, err := logIngestGroupID()
if err != nil {
return err
}
return writeAtomicOwned(logIngestTokenPath, token, 0640, 0, groupID)
}
func logIngestGroupID() (int, error) {
group, err := user.LookupGroup(logIngestGroup)
if err != nil {
return 0, fmt.Errorf("look up log ingestion group: %w", err)
}
groupID, err := strconv.Atoi(group.Gid)
if err != nil {
return 0, fmt.Errorf("parse log ingestion group ID: %w", err)
}
return groupID, nil
}
func validLogIngestToken(token []byte) bool {
if len(token) < 32 || len(token) > 256 {
return false
}
for _, value := range token {
if !validLogIngestTokenByte(value) {
return false
}
}
return true
}
func validLogIngestTokenByte(value byte) bool {
switch {
case value >= 'a' && value <= 'z', value >= 'A' && value <= 'Z', value >= '0' && value <= '9':
return true
default:
return value == '-' || value == '_' || value == '.'
}
}
func updateAdmissionWithClients(ctx context.Context, clients ociClients, configuration config) error {
path, cleanup, err := clients.downloadAdmission(ctx, configuration)
if err != nil {
return err
}
defer cleanup()
publicInterface, err := defaultInterface()
if err != nil {
return err
}
return runAdmission(ctx, configuration, "apply", "--input", path, "--public-interface", publicInterface)
}
func restoreAdmission(ctx context.Context, configuration config) error {
publicInterface, err := defaultInterface()
if err != nil {
return err
}
return runAdmission(ctx, configuration, "restore", "--public-interface", publicInterface)
}
func runAdmission(ctx context.Context, configuration config, arguments ...string) error {
arguments = append(arguments,
"--state-directory", webAllowStateDirectory,
"--minimum-admitted-addresses", strconv.FormatUint(configuration.Admission.MinimumAddresses, 10),
"--maximum-admitted-addresses", strconv.FormatUint(configuration.Admission.MaximumAddresses, 10),
"--minimum-admitted-intervals", strconv.Itoa(configuration.Admission.MinimumIntervals),
)
for _, address := range configuration.MonitoringSourceIPv4 {
arguments = append(arguments, "--static-exception-ipv4", address)
}
return run(ctx, configuration.AdmissionHelper, arguments...)
}
func updateSMTPDenyWithClients(ctx context.Context, clients ociClients, configuration config) (operationErr error) {
defer func() {
operationErr = completeSMTPDenyRefresh(operationErr)
}()
path, cleanup, err := clients.downloadSMTPDeny(ctx, configuration)
if err != nil {
return errors.Join(err, restoreSMTPDeny(ctx, configuration))
}
defer cleanup()
publicInterface, err := defaultInterface()
if err != nil {
return err
}
return runSMTPDeny(ctx, configuration, "apply", "--input", path, "--public-interface", publicInterface)
}
func restoreSMTPDeny(ctx context.Context, configuration config) error {
publicInterface, err := defaultInterface()
if err != nil {
return err
}
return runSMTPDeny(ctx, configuration, "restore", "--public-interface", publicInterface)
}
func runSMTPDeny(ctx context.Context, configuration config, arguments ...string) error {
if configuration.SMTPDeny == nil || configuration.SMTPDenyHelper == "" {
return errors.New("SMTP deny configuration is inactive")
}
arguments = append(arguments,
"--state-directory", smtpDenyStateDirectory,
"--minimum-observations", strconv.FormatUint(configuration.SMTPDeny.MinimumObservations, 10),
"--maximum-addresses", strconv.FormatUint(configuration.SMTPDeny.MaximumAddresses, 10),
"--maximum-intervals", strconv.Itoa(configuration.SMTPDeny.MaximumIntervals),
"--maximum-lifetime", strconv.Itoa(configuration.SMTPDeny.MaximumLifetimeSeconds)+"s",
"--maximum-observation-window", strconv.Itoa(configuration.SMTPDeny.MaximumObservationWindow)+"s",
)
return run(ctx, configuration.SMTPDenyHelper, arguments...)
}
func waitForHandshake(ctx context.Context, maximumAge time.Duration) error {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
latest, err := latestHandshake(ctx)
if err == nil && latest > 0 && time.Since(time.Unix(latest, 0)) <= maximumAge {
return nil
}
select {
case <-ctx.Done():
return fmt.Errorf("wait for WireGuard handshake: %w", ctx.Err())
case <-ticker.C:
}
}
}
func latestHandshake(ctx context.Context) (int64, error) {
output, err := exec.CommandContext(ctx, "/usr/bin/wg", "show", interfaceName, "latest-handshakes").Output()
if err != nil {
return 0, err
}
fields := strings.Fields(string(output))
if len(fields) < 2 {
return 0, errors.New("WireGuard peer handshake is absent")
}
return strconv.ParseInt(fields[1], 10, 64)
}
func defaultInterface() (string, error) {
contents, err := os.ReadFile("/proc/net/route")
if err != nil {
return "", err
}
for _, line := range strings.Split(string(contents), "\n") {
fields := strings.Fields(line)
if len(fields) >= 4 && fields[1] == "00000000" {
flags, err := strconv.ParseUint(fields[3], 16, 16)
if err == nil && flags&1 != 0 {
return fields[0], nil
}
}
}
return "", errors.New("default IPv4 interface not found")
}
func writeAtomic(path string, contents []byte, mode os.FileMode) error {
return writeAtomicOwned(path, contents, mode, -1, -1)
}
func writeAtomicOwned(path string, contents []byte, mode os.FileMode, ownerID, groupID int) error {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
file, err := os.CreateTemp(filepath.Dir(path), ".write-")
if err != nil {
return err
}
temporaryPath := file.Name()
defer func() { _ = os.Remove(temporaryPath) }()
if err := file.Chmod(mode); err != nil {
_ = file.Close()
return err
}
if ownerID >= 0 || groupID >= 0 {
if err := file.Chown(ownerID, groupID); err != nil {
_ = file.Close()
return err
}
}
if _, err := file.Write(contents); err != nil {
_ = file.Close()
return err
}
if err := file.Sync(); err != nil {
_ = file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
return os.Rename(temporaryPath, path)
}
func run(ctx context.Context, path string, arguments ...string) error {
command := exec.CommandContext(ctx, path, arguments...)
output, err := command.CombinedOutput()
if err != nil {
return fmt.Errorf("%s: %w: %s", filepath.Base(path), err, strings.TrimSpace(string(output)))
}
return nil
}
type status struct {
ConntrackCount int64 `json:"conntrack_count"`
ConntrackMaximum int64 `json:"conntrack_maximum"`
HandshakeAgeSecond int64 `json:"handshake_age_seconds"`
NftablesActive bool `json:"nftables_active"`
SMTPDenyActive bool `json:"smtp_deny_active"`
SMTPDenyActiveSource string `json:"smtp_deny_active_source,omitempty"`
SMTPDenyBundleAgeSeconds int64 `json:"smtp_deny_bundle_age_seconds"`
SMTPDenyBundleRemainingSeconds int64 `json:"smtp_deny_bundle_remaining_seconds"`
SMTPDenyCached bool `json:"smtp_deny_cached"`
SMTPDenyPolicyValid bool `json:"smtp_deny_policy_valid"`
SMTPDenyRefreshCompletedAgeSeconds int64 `json:"smtp_deny_refresh_completed_age_seconds"`
SMTPDenyRefreshSuccessful bool `json:"smtp_deny_refresh_successful"`
SMTPDenyRemoteCurrentAgeSeconds int64 `json:"smtp_deny_remote_current_age_seconds"`
WebAllowActive bool `json:"web_allow_active"`
WebAllowCached bool `json:"web_allow_cached"`
WireGuardActive bool `json:"wireguard_active"`
}
type telemetryFile struct {
GeneratedAt time.Time `json:"generated_at"`
Status status `json:"status"`
}
func currentStatus(configuration config) status {
result := status{
SMTPDenyBundleAgeSeconds: -1,
SMTPDenyBundleRemainingSeconds: -1,
SMTPDenyRefreshCompletedAgeSeconds: -1,
SMTPDenyRemoteCurrentAgeSeconds: -1,
}
result.NftablesActive = exec.Command("/usr/sbin/nft", "list", "table", "inet", "oci_l4_relay").Run() == nil
result.WebAllowActive = exec.Command("/usr/sbin/nft", "list", "table", "inet", "oci_l4_relay_web_allow").Run() == nil
result.SMTPDenyActive = exec.Command("/usr/sbin/nft", "list", "table", "inet", "oci_l4_relay_smtp_deny").Run() == nil
result.WireGuardActive = exec.Command("/usr/sbin/ip", "link", "show", "dev", interfaceName).Run() == nil
result.WebAllowCached = fileExists(filepath.Join(webAllowStateDirectory, "last-good.json.gz"))
if configuration.MailRelayAddress != "" {
now := time.Now().UTC()
if active, err := readSMTPDenyActiveState(smtpDenyStateDirectory); err == nil {
result.SMTPDenyActiveSource = active.Source
result.SMTPDenyPolicyValid = active.validAt(now, time.Duration(configuration.SMTPDeny.MaximumLifetimeSeconds)*time.Second)
result.SMTPDenyCached = result.SMTPDenyPolicyValid && fileExists(filepath.Join(smtpDenyStateDirectory, "last-good.json.gz"))
if !active.BundleGeneratedAt.IsZero() {
result.SMTPDenyBundleAgeSeconds = max(0, int64(now.Sub(active.BundleGeneratedAt).Seconds()))
}
if !active.BundleExpiresAt.IsZero() {
result.SMTPDenyBundleRemainingSeconds = int64(active.BundleExpiresAt.Sub(now).Seconds())
}
if !active.LastRemoteCurrentAt.IsZero() {
result.SMTPDenyRemoteCurrentAgeSeconds = max(0, int64(now.Sub(active.LastRemoteCurrentAt).Seconds()))
}
}
if refresh, err := readSMTPDenyRefreshState(smtpDenyStateDirectory); err == nil && !refresh.CompletedAt.IsZero() && !refresh.CompletedAt.After(now.Add(5*time.Minute)) {
result.SMTPDenyRefreshCompletedAgeSeconds = max(0, int64(now.Sub(refresh.CompletedAt).Seconds()))
result.SMTPDenyRefreshSuccessful = refresh.Successful && refresh.ActiveSource == "current"
}
}
result.ConntrackCount = readInteger("/proc/sys/net/netfilter/nf_conntrack_count")
result.ConntrackMaximum = readInteger("/proc/sys/net/netfilter/nf_conntrack_max")
if latest, err := latestHandshake(context.Background()); err == nil && latest > 0 {
result.HandshakeAgeSecond = max(0, time.Now().Unix()-latest)
} else {
result.HandshakeAgeSecond = -1
}
return result
}
func reportStatus(configuration config, requireHealthy bool) error {
result := currentStatus(configuration)
encoded, err := json.Marshal(result)
if err != nil {
return err
}
fmt.Println(string(encoded))
mailPolicyUnhealthy := configuration.MailRelayAddress != "" && (!result.SMTPDenyActive || !result.SMTPDenyPolicyValid)
if requireHealthy && (!result.NftablesActive || !result.WebAllowActive || !result.WebAllowCached || !result.WireGuardActive || mailPolicyUnhealthy || result.HandshakeAgeSecond < 0 || result.HandshakeAgeSecond > 180) {
return errors.New("relay is unhealthy")
}
return nil
}
func reportTelemetry(ctx context.Context, configuration config) error {
result := currentStatus(configuration)
generatedAt := time.Now().UTC()
snapshot, err := json.Marshal(telemetryFile{GeneratedAt: generatedAt, Status: result})
if err != nil {
return err
}
if err := writeAtomic(telemetrySnapshotPath, append(snapshot, '\n'), 0644); err != nil {
return err
}
fmt.Printf("oci_l4_relay_telemetry timestamp=%s conntrack_count=%d conntrack_max=%d wireguard_handshake_age_seconds=%d web_allow_cached=%t smtp_deny_cached=%t smtp_deny_policy_valid=%t smtp_deny_active_source=%s smtp_deny_bundle_age_seconds=%d smtp_deny_bundle_remaining_seconds=%d smtp_deny_remote_current_age_seconds=%d smtp_deny_refresh_successful=%t smtp_deny_refresh_completed_age_seconds=%d nftables_active=%t wireguard_active=%t\n", generatedAt.Format(time.RFC3339), result.ConntrackCount, result.ConntrackMaximum, result.HandshakeAgeSecond, result.WebAllowCached, result.SMTPDenyCached, result.SMTPDenyPolicyValid, result.SMTPDenyActiveSource, result.SMTPDenyBundleAgeSeconds, result.SMTPDenyBundleRemainingSeconds, result.SMTPDenyRemoteCurrentAgeSeconds, result.SMTPDenyRefreshSuccessful, result.SMTPDenyRefreshCompletedAgeSeconds, result.NftablesActive, result.WireGuardActive)
for _, command := range [][]string{
{"/usr/sbin/nft", "-j", "list", "counters"},
{"/usr/sbin/tc", "-s", "-j", "qdisc", "show", "dev", interfaceName},
{"/usr/sbin/ip", "-s", "-j", "link", "show", "dev", interfaceName},
} {
output, err := exec.CommandContext(ctx, command[0], command[1:]...).Output()
if err != nil {
return err
}
fmt.Printf("oci_l4_relay_telemetry detail=%s\n", strings.TrimSpace(string(output)))
}
return nil
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func readInteger(path string) int64 {
contents, err := os.ReadFile(path)
if err != nil {
return -1
}
value, err := strconv.ParseInt(strings.TrimSpace(string(contents)), 10, 64)
if err != nil {
return -1
}
return value
}