quorate/services/short/cloud/oci.go

256 lines
9.7 KiB
Go
Raw Permalink Normal View History

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)},
}
}