315 lines
12 KiB
Go
315 lines
12 KiB
Go
package oci
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/netip"
|
||
"time"
|
||
|
||
"github.com/oracle/oci-go-sdk/v65/core"
|
||
)
|
||
|
||
// ipv6AnyCIDR 是 IPv6 默认路由与放行目标。
|
||
const ipv6AnyCIDR = "::/0"
|
||
|
||
// IPv6Step 是一键启用 IPv6 过程中的一个步骤结果。
|
||
type IPv6Step struct {
|
||
Step string `json:"step"`
|
||
Status string `json:"status"` // done / skipped / failed
|
||
Detail string `json:"detail,omitempty"`
|
||
}
|
||
|
||
// EnableVCNIPv6 实现 Client:为 VCN 一键启用 IPv6。依次执行:
|
||
// 1. VCN 分配 Oracle GUA /56(已有则跳过);
|
||
// 2. 每个无 IPv6 的子网从 /56 中分配一个 /64;
|
||
// 3. 默认路由表补 ::/0 → Internet Gateway(无 IGW 时自动创建);
|
||
// 4. 默认安全列表补 egress ::/0 放行。
|
||
//
|
||
// 步骤级失败记录在返回的步骤列表中,不中断后续无依赖的步骤。
|
||
func (c *RealClient) EnableVCNIPv6(ctx context.Context, cred Credentials, region, vcnID string) ([]IPv6Step, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
vcnResp, err := vn.GetVcn(ctx, core.GetVcnRequest{VcnId: &vcnID})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("get vcn %s: %w", vcnID, err)
|
||
}
|
||
// 子网 / IGW 的关联查询须用 VCN 实际所在 compartment,传租户根会漏查
|
||
vcnCompartment := *hostCompartment(cred, deref(vcnResp.CompartmentId))
|
||
var steps []IPv6Step
|
||
vcnCidr, step := ensureVCNIPv6Cidr(ctx, vn, vcnResp.Vcn)
|
||
steps = append(steps, step)
|
||
steps = append(steps, ensureSubnetsIPv6(ctx, vn, vcnCompartment, vcnID, vcnCidr)...)
|
||
steps = append(steps, ensureIPv6Route(ctx, vn, vcnCompartment, vcnResp.Vcn))
|
||
steps = append(steps, ensureIPv6Egress(ctx, vn, vcnResp.Vcn))
|
||
return orEmpty(steps), nil
|
||
}
|
||
|
||
// ensureVCNIPv6Cidr 确保 VCN 拥有 IPv6 GUA,返回其 /56 前缀。
|
||
func ensureVCNIPv6Cidr(ctx context.Context, vn core.VirtualNetworkClient, vcn core.Vcn) (string, IPv6Step) {
|
||
if len(vcn.Ipv6CidrBlocks) > 0 {
|
||
return vcn.Ipv6CidrBlocks[0], IPv6Step{Step: "vcn-ipv6-cidr", Status: "skipped", Detail: vcn.Ipv6CidrBlocks[0]}
|
||
}
|
||
enabled := true
|
||
_, err := vn.AddIpv6VcnCidr(ctx, core.AddIpv6VcnCidrRequest{
|
||
VcnId: vcn.Id,
|
||
AddVcnIpv6CidrDetails: core.AddVcnIpv6CidrDetails{IsOracleGuaAllocationEnabled: &enabled},
|
||
})
|
||
if err != nil {
|
||
return "", IPv6Step{Step: "vcn-ipv6-cidr", Status: "failed", Detail: err.Error()}
|
||
}
|
||
cidr, err := waitVCNIPv6Cidr(ctx, vn, *vcn.Id)
|
||
if err != nil {
|
||
return "", IPv6Step{Step: "vcn-ipv6-cidr", Status: "failed", Detail: err.Error()}
|
||
}
|
||
return cidr, IPv6Step{Step: "vcn-ipv6-cidr", Status: "done", Detail: cidr}
|
||
}
|
||
|
||
// waitVCNIPv6Cidr 轮询等待 Oracle GUA 分配完成(异步操作)。
|
||
func waitVCNIPv6Cidr(ctx context.Context, vn core.VirtualNetworkClient, vcnID string) (string, error) {
|
||
for i := 0; i < 30; i++ {
|
||
resp, err := vn.GetVcn(ctx, core.GetVcnRequest{VcnId: &vcnID})
|
||
if err != nil {
|
||
return "", fmt.Errorf("poll vcn ipv6 cidr: %w", err)
|
||
}
|
||
if len(resp.Ipv6CidrBlocks) > 0 {
|
||
return resp.Ipv6CidrBlocks[0], nil
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
return "", fmt.Errorf("poll vcn ipv6 cidr: %w", ctx.Err())
|
||
case <-time.After(2 * time.Second):
|
||
}
|
||
}
|
||
return "", fmt.Errorf("poll vcn ipv6 cidr: timed out")
|
||
}
|
||
|
||
// ensureSubnetsIPv6 为每个还没有 IPv6 的子网从 VCN /56 中分配 /64;
|
||
// compartmentID 须是 VCN 实际所在 compartment,否则列不到子网。
|
||
func ensureSubnetsIPv6(ctx context.Context, vn core.VirtualNetworkClient, compartmentID, vcnID, vcnCidr string) []IPv6Step {
|
||
if vcnCidr == "" {
|
||
return []IPv6Step{{Step: "subnet-ipv6-cidr", Status: "failed", Detail: "vcn has no ipv6 cidr"}}
|
||
}
|
||
subnets, err := listSubnets(ctx, vn, compartmentID, vcnID)
|
||
if err != nil {
|
||
return []IPv6Step{{Step: "subnet-ipv6-cidr", Status: "failed", Detail: err.Error()}}
|
||
}
|
||
used := make([]string, 0, len(subnets))
|
||
for _, s := range subnets {
|
||
if s.Ipv6CidrBlock != "" {
|
||
used = append(used, s.Ipv6CidrBlock)
|
||
}
|
||
}
|
||
var steps []IPv6Step
|
||
for _, s := range subnets {
|
||
if s.Ipv6CidrBlock != "" {
|
||
steps = append(steps, IPv6Step{Step: "subnet-ipv6-cidr", Status: "skipped", Detail: s.DisplayName + " " + s.Ipv6CidrBlock})
|
||
continue
|
||
}
|
||
steps = append(steps, addSubnetIPv6(ctx, vn, s, vcnCidr, &used))
|
||
}
|
||
return orEmpty(steps)
|
||
}
|
||
|
||
func addSubnetIPv6(ctx context.Context, vn core.VirtualNetworkClient, s Subnet, vcnCidr string, used *[]string) IPv6Step {
|
||
cidr, err := nextSubnetIPv6CIDR(vcnCidr, *used)
|
||
if err != nil {
|
||
return IPv6Step{Step: "subnet-ipv6-cidr", Status: "failed", Detail: s.DisplayName + ": " + err.Error()}
|
||
}
|
||
_, err = vn.AddIpv6SubnetCidr(ctx, core.AddIpv6SubnetCidrRequest{
|
||
SubnetId: &s.ID,
|
||
AddSubnetIpv6CidrDetails: core.AddSubnetIpv6CidrDetails{Ipv6CidrBlock: &cidr},
|
||
})
|
||
if err != nil {
|
||
return IPv6Step{Step: "subnet-ipv6-cidr", Status: "failed", Detail: s.DisplayName + ": " + err.Error()}
|
||
}
|
||
*used = append(*used, cidr)
|
||
return IPv6Step{Step: "subnet-ipv6-cidr", Status: "done", Detail: s.DisplayName + " " + cidr}
|
||
}
|
||
|
||
// nextSubnetIPv6CIDR 从 VCN 的 /56 前缀中选出未被占用的 /64:
|
||
// /56 的前 7 字节固定,第 8 字节 0~255 即 256 个可用 /64。
|
||
func nextSubnetIPv6CIDR(vcnCidr string, used []string) (string, error) {
|
||
prefix, err := netip.ParsePrefix(vcnCidr)
|
||
if err != nil {
|
||
return "", fmt.Errorf("parse vcn ipv6 cidr %s: %w", vcnCidr, err)
|
||
}
|
||
base := prefix.Addr().As16()
|
||
taken := make(map[byte]bool, len(used))
|
||
for _, u := range used {
|
||
p, err := netip.ParsePrefix(u)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
b := p.Addr().As16()
|
||
if [7]byte(b[:7]) == [7]byte(base[:7]) {
|
||
taken[b[7]] = true
|
||
}
|
||
}
|
||
for i := 0; i <= 255; i++ {
|
||
if taken[byte(i)] {
|
||
continue
|
||
}
|
||
candidate := base
|
||
candidate[7] = base[7] | byte(i)
|
||
return netip.PrefixFrom(netip.AddrFrom16(candidate), 64).String(), nil
|
||
}
|
||
return "", fmt.Errorf("no free /64 left in %s", vcnCidr)
|
||
}
|
||
|
||
// ensureIPv6Route 在默认路由表补 ::/0 → IGW;没有 IGW 时自动创建。
|
||
// compartmentID 须是 VCN 实际所在 compartment,IGW 查找与创建都在其中进行。
|
||
func ensureIPv6Route(ctx context.Context, vn core.VirtualNetworkClient, compartmentID string, vcn core.Vcn) IPv6Step {
|
||
rtResp, err := vn.GetRouteTable(ctx, core.GetRouteTableRequest{RtId: vcn.DefaultRouteTableId})
|
||
if err != nil {
|
||
return IPv6Step{Step: "route-ipv6-default", Status: "failed", Detail: err.Error()}
|
||
}
|
||
for _, rule := range rtResp.RouteRules {
|
||
if deref(rule.Destination) == ipv6AnyCIDR {
|
||
return IPv6Step{Step: "route-ipv6-default", Status: "skipped", Detail: "::/0 route exists"}
|
||
}
|
||
}
|
||
igwID, err := ensureInternetGateway(ctx, vn, compartmentID, *vcn.Id)
|
||
if err != nil {
|
||
return IPv6Step{Step: "route-ipv6-default", Status: "failed", Detail: err.Error()}
|
||
}
|
||
dest := ipv6AnyCIDR
|
||
rules := append(rtResp.RouteRules, core.RouteRule{
|
||
Destination: &dest,
|
||
DestinationType: core.RouteRuleDestinationTypeCidrBlock,
|
||
NetworkEntityId: &igwID,
|
||
})
|
||
_, err = vn.UpdateRouteTable(ctx, core.UpdateRouteTableRequest{
|
||
RtId: vcn.DefaultRouteTableId,
|
||
UpdateRouteTableDetails: core.UpdateRouteTableDetails{RouteRules: rules},
|
||
})
|
||
if err != nil {
|
||
return IPv6Step{Step: "route-ipv6-default", Status: "failed", Detail: err.Error()}
|
||
}
|
||
return IPv6Step{Step: "route-ipv6-default", Status: "done", Detail: "::/0 -> " + igwID}
|
||
}
|
||
|
||
// ensureInternetGateway 返回 VCN 已有的 IGW,没有则创建一个;
|
||
// compartmentID 须是 VCN 实际所在 compartment,否则查不到已有 IGW。
|
||
func ensureInternetGateway(ctx context.Context, vn core.VirtualNetworkClient, compartmentID, vcnID string) (string, error) {
|
||
listResp, err := vn.ListInternetGateways(ctx, core.ListInternetGatewaysRequest{
|
||
CompartmentId: &compartmentID,
|
||
VcnId: &vcnID,
|
||
})
|
||
if err != nil {
|
||
return "", fmt.Errorf("list internet gateways: %w", err)
|
||
}
|
||
if len(listResp.Items) > 0 {
|
||
return deref(listResp.Items[0].Id), nil
|
||
}
|
||
enabled := true
|
||
name := "oci-portal-igw"
|
||
createResp, err := vn.CreateInternetGateway(ctx, core.CreateInternetGatewayRequest{
|
||
CreateInternetGatewayDetails: core.CreateInternetGatewayDetails{
|
||
CompartmentId: &compartmentID,
|
||
VcnId: &vcnID,
|
||
IsEnabled: &enabled,
|
||
DisplayName: &name,
|
||
},
|
||
})
|
||
if err != nil {
|
||
return "", fmt.Errorf("create internet gateway: %w", err)
|
||
}
|
||
return deref(createResp.InternetGateway.Id), nil
|
||
}
|
||
|
||
// AddInstanceIpv6 实现 Client:为实例主 VNIC 分配一个 IPv6 地址并返回。
|
||
// address 为空时由子网 /64 自动分配;子网未启用 IPv6 时 OCI 返回错误。
|
||
func (c *RealClient) AddInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) (string, error) {
|
||
vnic, err := c.primaryVnic(ctx, cred, region, instanceID)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return c.AddVnicIpv6(ctx, cred, region, deref(vnic.Id), address)
|
||
}
|
||
|
||
// AddVnicIpv6 实现 Client:为指定 VNIC 分配一个 IPv6 地址并返回(多网卡场景)。
|
||
func (c *RealClient) AddVnicIpv6(ctx context.Context, cred Credentials, region, vnicID, address string) (string, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
details := core.CreateIpv6Details{VnicId: &vnicID}
|
||
if address != "" {
|
||
details.IpAddress = &address
|
||
}
|
||
resp, err := vn.CreateIpv6(ctx, core.CreateIpv6Request{CreateIpv6Details: details})
|
||
if err != nil {
|
||
return "", fmt.Errorf("create ipv6 on vnic %s: %w", vnicID, err)
|
||
}
|
||
return deref(resp.IpAddress), nil
|
||
}
|
||
|
||
// DeleteInstanceIpv6 实现 Client:取消分配实例上的指定 IPv6 地址。
|
||
// 遍历实例 VNIC 找到地址对应的 Ipv6 对象后删除;地址不存在时报错。
|
||
func (c *RealClient) DeleteInstanceIpv6(ctx context.Context, cred Credentials, region, instanceID, address string) error {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
vnics, _, err := c.instanceVnics(ctx, cred, region, instanceID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
for _, vnic := range vnics {
|
||
resp, err := vn.ListIpv6s(ctx, core.ListIpv6sRequest{VnicId: vnic.Id})
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, ip := range resp.Items {
|
||
if !sameIPv6(deref(ip.IpAddress), address) {
|
||
continue
|
||
}
|
||
if _, err := vn.DeleteIpv6(ctx, core.DeleteIpv6Request{Ipv6Id: ip.Id}); err != nil {
|
||
return fmt.Errorf("delete ipv6 %s: %w", address, err)
|
||
}
|
||
return nil
|
||
}
|
||
}
|
||
return fmt.Errorf("delete ipv6: address %s not found on instance %s", address, instanceID)
|
||
}
|
||
|
||
// sameIPv6 规范化比较两个 IPv6 地址(兼容大小写与缩写形式差异)。
|
||
func sameIPv6(a, b string) bool {
|
||
pa, errA := netip.ParseAddr(a)
|
||
pb, errB := netip.ParseAddr(b)
|
||
if errA != nil || errB != nil {
|
||
return a == b
|
||
}
|
||
return pa == pb
|
||
}
|
||
|
||
// ensureIPv6Egress 在默认安全列表补 egress ::/0 全协议放行。
|
||
func ensureIPv6Egress(ctx context.Context, vn core.VirtualNetworkClient, vcn core.Vcn) IPv6Step {
|
||
slResp, err := vn.GetSecurityList(ctx, core.GetSecurityListRequest{SecurityListId: vcn.DefaultSecurityListId})
|
||
if err != nil {
|
||
return IPv6Step{Step: "seclist-ipv6-egress", Status: "failed", Detail: err.Error()}
|
||
}
|
||
for _, rule := range slResp.EgressSecurityRules {
|
||
if deref(rule.Destination) == ipv6AnyCIDR {
|
||
return IPv6Step{Step: "seclist-ipv6-egress", Status: "skipped", Detail: "::/0 egress exists"}
|
||
}
|
||
}
|
||
dest := ipv6AnyCIDR
|
||
protocol := "all"
|
||
rules := append(slResp.EgressSecurityRules, core.EgressSecurityRule{
|
||
Protocol: &protocol,
|
||
Destination: &dest,
|
||
DestinationType: core.EgressSecurityRuleDestinationTypeCidrBlock,
|
||
})
|
||
_, err = vn.UpdateSecurityList(ctx, core.UpdateSecurityListRequest{
|
||
SecurityListId: vcn.DefaultSecurityListId,
|
||
UpdateSecurityListDetails: core.UpdateSecurityListDetails{EgressSecurityRules: rules},
|
||
})
|
||
if err != nil {
|
||
return IPv6Step{Step: "seclist-ipv6-egress", Status: "failed", Detail: err.Error()}
|
||
}
|
||
return IPv6Step{Step: "seclist-ipv6-egress", Status: "done", Detail: "egress ::/0 allowed"}
|
||
}
|