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"} }