package main import ( "context" "fmt" "net" "time" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/core" ) type networkSecurityGroupClient interface { UpdateNetworkSecurityGroupSecurityRules(context.Context, core.UpdateNetworkSecurityGroupSecurityRulesRequest) (core.UpdateNetworkSecurityGroupSecurityRulesResponse, error) } type ociReconciler struct { client networkSecurityGroupClient networkSecurityGroup string securityRule string wireguardDestinationPort int wireguardURL string recoveryTimeout time.Duration probeWireGuard func(context.Context, string, time.Duration) error } func newOCIReconciler(cfg config) (*ociReconciler, error) { provider := common.NewRawConfigurationProvider( cfg.ociTenancy, cfg.ociUser, cfg.ociRegion, cfg.ociFingerprint, cfg.ociPrivateKey, nil, ) client, err := core.NewVirtualNetworkClientWithConfigurationProvider(provider) if err != nil { return nil, fmt.Errorf("create OCI virtual network client: %w", err) } return &ociReconciler{ client: &client, networkSecurityGroup: cfg.networkSecurityGroup, securityRule: cfg.securityRule, wireguardDestinationPort: cfg.wireguardPort, wireguardURL: cfg.wireguardURL, recoveryTimeout: cfg.recoveryTimeout, probeWireGuard: waitForWireGuard, }, nil } func (r *ociReconciler) Reconcile(ctx context.Context, source net.IP) error { if !validPublicIPv4(source) { return fmt.Errorf("refuse invalid WAN source %q", source) } port := r.wireguardDestinationPort protocol := "17" description := "Home WireGuard peer" sourceCIDR := source.String() + "/32" response, err := r.client.UpdateNetworkSecurityGroupSecurityRules(ctx, core.UpdateNetworkSecurityGroupSecurityRulesRequest{ NetworkSecurityGroupId: common.String(r.networkSecurityGroup), UpdateNetworkSecurityGroupSecurityRulesDetails: core.UpdateNetworkSecurityGroupSecurityRulesDetails{ SecurityRules: []core.UpdateSecurityRuleDetails{{ Direction: core.UpdateSecurityRuleDetailsDirectionIngress, Id: common.String(r.securityRule), Protocol: common.String(protocol), Description: common.String(description), Source: common.String(sourceCIDR), SourceType: core.UpdateSecurityRuleDetailsSourceTypeCidrBlock, IsStateless: common.Bool(false), UdpOptions: &core.UdpOptions{DestinationPortRange: &core.PortRange{Min: common.Int(port), Max: common.Int(port)}}, }}, }, }) if err != nil { return fmt.Errorf("update WireGuard NSG source: %w", err) } if err := verifyUpdatedRule(response.SecurityRules, r.securityRule, sourceCIDR, port); err != nil { return err } if err := r.probeWireGuard(ctx, r.wireguardURL, r.recoveryTimeout); err != nil { return fmt.Errorf("verify WireGuard connectivity: %w", err) } return nil } func verifyUpdatedRule(rules []core.SecurityRule, id, source string, port int) error { if len(rules) != 1 { return fmt.Errorf("OCI returned %d updated rules, want 1", len(rules)) } rule := rules[0] if stringValue(rule.Id) != id || stringValue(rule.Source) != source || stringValue(rule.Protocol) != "17" || rule.Direction != core.SecurityRuleDirectionIngress || rule.SourceType != core.SecurityRuleSourceTypeCidrBlock || boolValue(rule.IsStateless) || rule.UdpOptions == nil || rule.UdpOptions.DestinationPortRange == nil || intValue(rule.UdpOptions.DestinationPortRange.Min) != port || intValue(rule.UdpOptions.DestinationPortRange.Max) != port { return fmt.Errorf("OCI returned an unexpected WireGuard rule") } return nil } func stringValue(value *string) string { if value == nil { return "" } return *value } func boolValue(value *bool) bool { return value != nil && *value } func intValue(value *int) int { if value == nil { return 0 } return *value }