Files
oci-portal/internal/oci/ipv6.go
T

315 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"}
}