package cloud import ( "context" "crypto/sha256" "encoding/hex" "fmt" "slices" "sort" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/core" "quorate.dev/services/short/config" ) type Network interface { ListCpes(context.Context, core.ListCpesRequest) (core.ListCpesResponse, error) CreateCpe(context.Context, core.CreateCpeRequest) (core.CreateCpeResponse, error) DeleteCpe(context.Context, core.DeleteCpeRequest) (core.DeleteCpeResponse, error) ListIPSecConnections(context.Context, core.ListIPSecConnectionsRequest) (core.ListIPSecConnectionsResponse, error) CreateIPSecConnection(context.Context, core.CreateIPSecConnectionRequest) (core.CreateIPSecConnectionResponse, error) UpdateIPSecConnection(context.Context, core.UpdateIPSecConnectionRequest) (core.UpdateIPSecConnectionResponse, error) DeleteIPSecConnection(context.Context, core.DeleteIPSecConnectionRequest) (core.DeleteIPSecConnectionResponse, error) ListIPSecConnectionTunnels(context.Context, core.ListIPSecConnectionTunnelsRequest) (core.ListIPSecConnectionTunnelsResponse, error) UpdateIPSecConnectionTunnel(context.Context, core.UpdateIPSecConnectionTunnelRequest) (core.UpdateIPSecConnectionTunnelResponse, error) GetIPSecConnectionTunnelSharedSecret(context.Context, core.GetIPSecConnectionTunnelSharedSecretRequest) (core.GetIPSecConnectionTunnelSharedSecretResponse, error) } type Client struct { API Network Config config.Cloud } type State struct { Bundle config.Bundle Peers []string Ready bool } func New(c config.Cloud, key string) (*Client, error) { p := common.NewRawConfigurationProvider(c.Tenancy, c.User, c.Region, c.Fingerprint, key, nil) api, err := core.NewVirtualNetworkClientWithConfigurationProvider(p) if err != nil { return nil, err } return &Client{API: &api, Config: c}, nil } func token(kind, drg, wan string) *string { sum := sha256.Sum256([]byte(kind + ":" + drg + ":" + wan)) value := hex.EncodeToString(sum[:]) return &value } func create[T any](retry *string, call func(*string) (T, error)) (T, error) { for { result, err := call(retry) service, ok := common.IsServiceError(err) if !ok || service.GetHTTPStatusCode() != 409 || service.GetCode() != "InvalidatedRetryToken" { return result, err } // Deterministic successors let another leader replay a create whose response was lost. sum := sha256.Sum256([]byte(*retry)) retry = common.String(hex.EncodeToString(sum[:])) } } func owned(tags map[string]string) bool { return tags["owner"] == "short" } func (c *Client) Reconcile(ctx context.Context, wan string) (State, error) { state := State{Bundle: config.Bundle{WAN: wan}} var cpes []core.Cpe var page *string for { r, err := c.API.ListCpes(ctx, core.ListCpesRequest{CompartmentId: &c.Config.Compartment, Page: page}) if err != nil { return state, err } for _, p := range r.Items { if owned(p.FreeformTags) { cpes = append(cpes, p) } } if r.OpcNextPage == nil { break } page = r.OpcNextPage } var current *core.Cpe for i := range cpes { if *cpes[i].IpAddress == wan { if current != nil { return state, fmt.Errorf("duplicate owned CPE for WAN") } current = &cpes[i] } } if current == nil { r, err := create(token("cpe", c.Config.DRG, wan), func(retry *string) (core.CreateCpeResponse, error) { return c.API.CreateCpe(ctx, core.CreateCpeRequest{OpcRetryToken: retry, CreateCpeDetails: core.CreateCpeDetails{ CompartmentId: &c.Config.Compartment, IpAddress: &wan, DisplayName: common.String("short"), FreeformTags: map[string]string{"owner": "short"}, }}) }) if err != nil { return state, err } current = &r.Cpe } var connections []core.IpSecConnection page = nil for { r, err := c.API.ListIPSecConnections(ctx, core.ListIPSecConnectionsRequest{CompartmentId: &c.Config.Compartment, DrgId: &c.Config.DRG, Page: page}) if err != nil { return state, err } for _, p := range r.Items { if owned(p.FreeformTags) { connections = append(connections, p) } } if r.OpcNextPage == nil { break } page = r.OpcNextPage } var connection *core.IpSecConnection for i := range connections { p := &connections[i] if *p.CpeId == *current.Id && p.LifecycleState != core.IpSecConnectionLifecycleStateTerminated && p.LifecycleState != core.IpSecConnectionLifecycleStateTerminating { if connection != nil { return state, fmt.Errorf("duplicate owned IPsec connection") } connection = p } } if connection == nil { r, err := create(token("ipsec", c.Config.DRG, wan), func(retry *string) (core.CreateIPSecConnectionResponse, error) { return c.API.CreateIPSecConnection(ctx, core.CreateIPSecConnectionRequest{OpcRetryToken: retry, CreateIpSecConnectionDetails: core.CreateIpSecConnectionDetails{ CompartmentId: &c.Config.Compartment, CpeId: current.Id, DrgId: &c.Config.DRG, DisplayName: common.String("short"), FreeformTags: map[string]string{"owner": "short"}, StaticRoutes: []string{c.Config.Address + "/32"}, CpeLocalIdentifier: &wan, CpeLocalIdentifierType: core.CreateIpSecConnectionDetailsCpeLocalIdentifierTypeIpAddress, TunnelConfiguration: []core.CreateIpSecConnectionTunnelDetails{tunnel("short-0"), tunnel("short-1")}, }}) }) if err != nil { return state, err } connection = &r.IpSecConnection } routes := []string{c.Config.Address + "/32"} routeReady := slices.Equal(connection.StaticRoutes, routes) if connection.LifecycleState == core.IpSecConnectionLifecycleStateAvailable && !routeReady { if _, err := c.API.UpdateIPSecConnection(ctx, core.UpdateIPSecConnectionRequest{ IpscId: connection.Id, UpdateIpSecConnectionDetails: core.UpdateIpSecConnectionDetails{StaticRoutes: routes}, }); err != nil { return state, err } } allUp := false for _, p := range append(connectionsWithout(connections, *connection.Id), *connection) { if p.LifecycleState != core.IpSecConnectionLifecycleStateAvailable { continue } r, err := c.API.ListIPSecConnectionTunnels(ctx, core.ListIPSecConnectionTunnelsRequest{IpscId: p.Id}) if err != nil { return state, err } isCurrent := *p.Id == *connection.Id up := len(r.Items) == 2 for _, head := range r.Items { if head.VpnIp != nil { state.Peers = append(state.Peers, *head.VpnIp+"/32") } if head.LifecycleState != core.IpSecConnectionTunnelLifecycleStateAvailable { up = false continue } if head.VpnIp == nil { up = false continue } if isCurrent && head.OracleCanInitiate != core.IpSecConnectionTunnelOracleCanInitiateInitiatorOrResponder { _, err := c.API.UpdateIPSecConnectionTunnel(ctx, core.UpdateIPSecConnectionTunnelRequest{ IpscId: p.Id, TunnelId: head.Id, UpdateIpSecConnectionTunnelDetails: core.UpdateIpSecConnectionTunnelDetails{ OracleInitiation: core.UpdateIpSecConnectionTunnelDetailsOracleInitiationInitiatorOrResponder, }, }) if err != nil { return state, err } } if head.Status != core.IpSecConnectionTunnelStatusUp { up = false } if isCurrent { secret, err := c.API.GetIPSecConnectionTunnelSharedSecret(ctx, core.GetIPSecConnectionTunnelSharedSecretRequest{IpscId: p.Id, TunnelId: head.Id}) if err != nil { return state, err } state.Bundle.Peers = append(state.Bundle.Peers, config.Peer{ID: *head.Id, Address: *head.VpnIp, Secret: *secret.SharedSecret}) } } if isCurrent { allUp = up } } sort.Slice(state.Bundle.Peers, func(i, j int) bool { return state.Bundle.Peers[i].ID < state.Bundle.Peers[j].ID }) sort.Strings(state.Peers) state.Ready = len(state.Bundle.Peers) == 2 && routeReady if state.Ready && allUp { for _, p := range connections { if *p.Id != *connection.Id && p.LifecycleState != core.IpSecConnectionLifecycleStateTerminating && p.LifecycleState != core.IpSecConnectionLifecycleStateTerminated { if _, err := c.API.DeleteIPSecConnection(ctx, core.DeleteIPSecConnectionRequest{IpscId: p.Id}); err != nil { return state, err } } } for _, p := range cpes { if *p.Id == *current.Id { continue } used := false for _, conn := range connections { if *conn.CpeId == *p.Id && conn.LifecycleState != core.IpSecConnectionLifecycleStateTerminated { used = true } } if !used { if _, err := c.API.DeleteCpe(ctx, core.DeleteCpeRequest{CpeId: p.Id}); err != nil { return state, err } } } } return state, nil } func connectionsWithout(all []core.IpSecConnection, id string) []core.IpSecConnection { var result []core.IpSecConnection for _, p := range all { if *p.Id != id { result = append(result, p) } } return result } func tunnel(name string) core.CreateIpSecConnectionTunnelDetails { return core.CreateIpSecConnectionTunnelDetails{ DisplayName: &name, Routing: core.CreateIpSecConnectionTunnelDetailsRoutingStatic, IkeVersion: core.CreateIpSecConnectionTunnelDetailsIkeVersionV2, OracleInitiation: core.CreateIpSecConnectionTunnelDetailsOracleInitiationInitiatorOrResponder, NatTranslationEnabled: core.CreateIpSecConnectionTunnelDetailsNatTranslationEnabledAuto, PhaseOneConfig: &core.PhaseOneConfigDetails{IsCustomPhaseOneConfig: common.Bool(true), EncryptionAlgorithm: core.PhaseOneConfigDetailsEncryptionAlgorithm256Cbc, AuthenticationAlgorithm: core.PhaseOneConfigDetailsAuthenticationAlgorithmSha2256, DiffieHelmanGroup: core.PhaseOneConfigDetailsDiffieHelmanGroupGroup14, LifetimeInSeconds: common.Int(28800)}, PhaseTwoConfig: &core.PhaseTwoConfigDetails{IsCustomPhaseTwoConfig: common.Bool(true), EncryptionAlgorithm: core.PhaseTwoConfigDetailsEncryptionAlgorithm256Gcm, IsPfsEnabled: common.Bool(true), PfsDhGroup: core.PhaseTwoConfigDetailsPfsDhGroupGroup14, LifetimeInSeconds: common.Int(3600)}, DpdConfig: &core.DpdConfig{DpdMode: core.DpdConfigDpdModeInitiateAndRespond, DpdTimeoutInSec: common.Int(20)}, } }