256 lines
9.7 KiB
Go
256 lines
9.7 KiB
Go
|
|
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)},
|
||
|
|
}
|
||
|
|
}
|