mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-09 11:14:30 +00:00
feat: support allocating public IPv6 addresses from a provider (#2162)
* feat: support allocating public IPv6 addresses from a provider Add a provider/leaser architecture for public IPv6 address allocation between nodes in the same network: - A node with `--ipv6-public-addr-provider` advertises a delegable public IPv6 prefix (auto-detected from kernel routes or manually configured via `--ipv6-public-addr-prefix`). - Other nodes with `--ipv6-public-addr-auto` request a /128 lease from the selected provider via a new RPC service (PublicIpv6AddrRpc). - Leases have a 30s TTL, renewed every 10s by the client routine. - The provider allocates addresses deterministically from its prefix using instance-UUID-based hashing to prefer stable assignments. - Routes to peer leases are installed on the TUN device, and each client's own /128 is assigned as its IPv6 address. Also includes netlink IPv6 route table inspection, integration tests, and event-driven route/address reconciliation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -402,6 +402,528 @@ async fn ping6_test(from_netns: &str, target_ip: &str, payload_size: Option<usiz
|
||||
code.code().unwrap() == 0
|
||||
}
|
||||
|
||||
fn run_cmd(program: &str, args: &[&str]) {
|
||||
let output = std::process::Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} {:?} failed: stdout={}, stderr={}",
|
||||
program,
|
||||
args,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn run_cmd_output(program: &str, args: &[&str]) -> String {
|
||||
let output = std::process::Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} {:?} failed: stdout={}, stderr={}",
|
||||
program,
|
||||
args,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
String::from_utf8(output.stdout).unwrap()
|
||||
}
|
||||
|
||||
fn run_ip(args: &[&str]) {
|
||||
run_cmd("ip", args);
|
||||
}
|
||||
|
||||
fn run_ip_in_ns(ns: &str, args: &[&str]) {
|
||||
let mut cmd = vec!["netns", "exec", ns, "ip"];
|
||||
cmd.extend_from_slice(args);
|
||||
run_cmd("ip", &cmd);
|
||||
}
|
||||
|
||||
fn run_ip_in_ns_output(ns: &str, args: &[&str]) -> String {
|
||||
let mut cmd = vec!["netns", "exec", ns, "ip"];
|
||||
cmd.extend_from_slice(args);
|
||||
run_cmd_output("ip", &cmd)
|
||||
}
|
||||
|
||||
fn run_sysctl_in_ns(ns: &str, assignment: &str) {
|
||||
run_cmd("ip", &["netns", "exec", ns, "sysctl", "-qw", assignment]);
|
||||
}
|
||||
|
||||
fn create_empty_netns(name: &str) {
|
||||
del_netns(name);
|
||||
run_ip(&["netns", "add", name]);
|
||||
run_ip(&["netns", "exec", name, "ip", "link", "set", "lo", "up"]);
|
||||
}
|
||||
|
||||
fn connect_ns_to_bridge(ns: &str, guest_if: &str, host_if: &str, bridge: &str) {
|
||||
let _ = std::process::Command::new("ip")
|
||||
.args(["link", "del", host_if])
|
||||
.status();
|
||||
run_ip(&[
|
||||
"link", "add", host_if, "type", "veth", "peer", "name", guest_if,
|
||||
]);
|
||||
run_ip(&["link", "set", guest_if, "netns", ns]);
|
||||
run_ip(&["link", "set", host_if, "up"]);
|
||||
run_cmd("brctl", &["addif", bridge, host_if]);
|
||||
run_ip(&["netns", "exec", ns, "ip", "link", "set", guest_if, "up"]);
|
||||
}
|
||||
|
||||
struct PublicIpv6Lab {
|
||||
extra_namespaces: [&'static str; 2],
|
||||
extra_bridges: [&'static str; 2],
|
||||
}
|
||||
|
||||
impl PublicIpv6Lab {
|
||||
const PROVIDER_NS: &'static str = "net_a";
|
||||
const CLIENT_NS: &'static str = "net_b";
|
||||
const UPSTREAM_NS: &'static str = "net_pubgw";
|
||||
const SERVER_NS: &'static str = "net_pubsrv";
|
||||
const WAN_BRIDGE: &'static str = "br_pubwan";
|
||||
const SERVER_BRIDGE: &'static str = "br_pubsrv";
|
||||
const PROVIDER_TUN: &'static str = "etpubv6p";
|
||||
const CLIENT_TUN: &'static str = "etpubv6c";
|
||||
const PROVIDER_PREFIX: &'static str = "2001:db8:100::/64";
|
||||
const PROVIDER_DEFAULT_FROM: &'static str = "2001:db8:100::/64";
|
||||
const PROVIDER_WAN_ADDR: &'static str = "2001:db8:ffff:1::2/64";
|
||||
const UPSTREAM_WAN_ADDR: &'static str = "2001:db8:ffff:1::1/64";
|
||||
const UPSTREAM_SERVER_ADDR: &'static str = "2001:db8:ffff:2::1/64";
|
||||
const SERVER_ADDR: &'static str = "2001:db8:ffff:2::100/64";
|
||||
const SERVER_IP: &'static str = "2001:db8:ffff:2::100";
|
||||
|
||||
fn setup() -> Self {
|
||||
prepare_linux_namespaces();
|
||||
|
||||
del_netns(Self::UPSTREAM_NS);
|
||||
del_netns(Self::SERVER_NS);
|
||||
let _ = std::process::Command::new("ip")
|
||||
.args(["link", "del", Self::WAN_BRIDGE])
|
||||
.status();
|
||||
let _ = std::process::Command::new("ip")
|
||||
.args(["link", "del", Self::SERVER_BRIDGE])
|
||||
.status();
|
||||
let _ = std::process::Command::new("brctl")
|
||||
.args(["delbr", Self::WAN_BRIDGE])
|
||||
.status();
|
||||
let _ = std::process::Command::new("brctl")
|
||||
.args(["delbr", Self::SERVER_BRIDGE])
|
||||
.status();
|
||||
|
||||
create_empty_netns(Self::UPSTREAM_NS);
|
||||
create_empty_netns(Self::SERVER_NS);
|
||||
prepare_bridge(Self::WAN_BRIDGE);
|
||||
prepare_bridge(Self::SERVER_BRIDGE);
|
||||
run_ip(&["link", "set", Self::WAN_BRIDGE, "up"]);
|
||||
run_ip(&["link", "set", Self::SERVER_BRIDGE, "up"]);
|
||||
|
||||
connect_ns_to_bridge(
|
||||
Self::PROVIDER_NS,
|
||||
"pubwan0",
|
||||
"veth_pubwan_p",
|
||||
Self::WAN_BRIDGE,
|
||||
);
|
||||
connect_ns_to_bridge(
|
||||
Self::UPSTREAM_NS,
|
||||
"upwan0",
|
||||
"veth_pubwan_u",
|
||||
Self::WAN_BRIDGE,
|
||||
);
|
||||
connect_ns_to_bridge(
|
||||
Self::UPSTREAM_NS,
|
||||
"upsrv0",
|
||||
"veth_pubsrv_u",
|
||||
Self::SERVER_BRIDGE,
|
||||
);
|
||||
connect_ns_to_bridge(
|
||||
Self::SERVER_NS,
|
||||
"srv0",
|
||||
"veth_pubsrv_s",
|
||||
Self::SERVER_BRIDGE,
|
||||
);
|
||||
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&["addr", "add", Self::PROVIDER_WAN_ADDR, "dev", "pubwan0"],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::UPSTREAM_NS,
|
||||
&["addr", "add", Self::UPSTREAM_WAN_ADDR, "dev", "upwan0"],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::UPSTREAM_NS,
|
||||
&["addr", "add", Self::UPSTREAM_SERVER_ADDR, "dev", "upsrv0"],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::SERVER_NS,
|
||||
&["addr", "add", Self::SERVER_ADDR, "dev", "srv0"],
|
||||
);
|
||||
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&["link", "add", "pubprefix0", "type", "dummy"],
|
||||
);
|
||||
run_ip_in_ns(Self::PROVIDER_NS, &["link", "set", "pubprefix0", "up"]);
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
Self::PROVIDER_PREFIX,
|
||||
"dev",
|
||||
"pubprefix0",
|
||||
],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::PROVIDER_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
"default",
|
||||
"from",
|
||||
Self::PROVIDER_DEFAULT_FROM,
|
||||
"via",
|
||||
"2001:db8:ffff:1::1",
|
||||
"dev",
|
||||
"pubwan0",
|
||||
],
|
||||
);
|
||||
|
||||
run_ip_in_ns(
|
||||
Self::SERVER_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
"default",
|
||||
"via",
|
||||
"2001:db8:ffff:2::1",
|
||||
"dev",
|
||||
"srv0",
|
||||
],
|
||||
);
|
||||
run_ip_in_ns(
|
||||
Self::UPSTREAM_NS,
|
||||
&[
|
||||
"-6",
|
||||
"route",
|
||||
"add",
|
||||
Self::PROVIDER_PREFIX,
|
||||
"via",
|
||||
"2001:db8:ffff:1::2",
|
||||
"dev",
|
||||
"upwan0",
|
||||
],
|
||||
);
|
||||
|
||||
run_sysctl_in_ns(Self::PROVIDER_NS, "net.ipv6.conf.all.forwarding=1");
|
||||
run_sysctl_in_ns(Self::UPSTREAM_NS, "net.ipv6.conf.all.forwarding=1");
|
||||
|
||||
Self {
|
||||
extra_namespaces: [Self::UPSTREAM_NS, Self::SERVER_NS],
|
||||
extra_bridges: [Self::WAN_BRIDGE, Self::SERVER_BRIDGE],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PublicIpv6Lab {
|
||||
fn drop(&mut self) {
|
||||
for ns in self.extra_namespaces {
|
||||
del_netns(ns);
|
||||
}
|
||||
for bridge in self.extra_bridges {
|
||||
let _ = std::process::Command::new("ip")
|
||||
.args(["link", "del", bridge])
|
||||
.status();
|
||||
let _ = std::process::Command::new("brctl")
|
||||
.args(["delbr", bridge])
|
||||
.status();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_public_ipv6_config(
|
||||
inst_name: &str,
|
||||
netns: &str,
|
||||
ipv4: &str,
|
||||
dev_name: &str,
|
||||
inst_id: uuid::Uuid,
|
||||
) -> TomlConfigLoader {
|
||||
let config = get_inst_config(inst_name, Some(netns), ipv4, "fd00::1/64");
|
||||
config.set_id(inst_id);
|
||||
config.set_ipv6(None);
|
||||
config.set_socks5_portal(None);
|
||||
config.set_network_identity(NetworkIdentity {
|
||||
network_name: "public_ipv6_auto_addr_test".to_string(),
|
||||
network_secret: Some("public_ipv6_auto_addr_secret".to_string()),
|
||||
network_secret_digest: None,
|
||||
});
|
||||
config.set_listeners(vec!["tcp://0.0.0.0:11010".parse().unwrap()]);
|
||||
let mut flags = config.get_flags();
|
||||
flags.dev_name = dev_name.to_string();
|
||||
config.set_flags(flags);
|
||||
config
|
||||
}
|
||||
|
||||
async fn init_public_ipv6_two_node(
|
||||
client_inst_id: uuid::Uuid,
|
||||
) -> (PublicIpv6Lab, Instance, Instance) {
|
||||
let lab = PublicIpv6Lab::setup();
|
||||
|
||||
let provider_cfg = get_public_ipv6_config(
|
||||
"provider_public_ipv6",
|
||||
PublicIpv6Lab::PROVIDER_NS,
|
||||
"10.144.144.1",
|
||||
PublicIpv6Lab::PROVIDER_TUN,
|
||||
uuid::Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap(),
|
||||
);
|
||||
provider_cfg.set_ipv6_public_addr_provider(true);
|
||||
|
||||
let client_cfg = get_public_ipv6_config(
|
||||
"client_public_ipv6",
|
||||
PublicIpv6Lab::CLIENT_NS,
|
||||
"10.144.144.2",
|
||||
PublicIpv6Lab::CLIENT_TUN,
|
||||
client_inst_id,
|
||||
);
|
||||
client_cfg.set_ipv6_public_addr_auto(true);
|
||||
|
||||
let mut provider = Instance::new(provider_cfg);
|
||||
let mut client = Instance::new(client_cfg);
|
||||
|
||||
provider.run().await.unwrap();
|
||||
client.run().await.unwrap();
|
||||
|
||||
provider
|
||||
.get_conn_manager()
|
||||
.add_connector(TcpTunnelConnector::new(
|
||||
"tcp://10.1.1.2:11010".parse().unwrap(),
|
||||
));
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
provider.get_peer_manager().list_routes().await.len() == 1
|
||||
&& client.get_peer_manager().list_routes().await.len() == 1
|
||||
},
|
||||
Duration::from_secs(8),
|
||||
)
|
||||
.await;
|
||||
|
||||
(lab, provider, client)
|
||||
}
|
||||
|
||||
async fn wait_for_public_ipv6_addr(inst: &Instance) -> cidr::Ipv6Inet {
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
inst.get_peer_manager()
|
||||
.get_my_public_ipv6_addr()
|
||||
.await
|
||||
.is_some()
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
inst.get_peer_manager()
|
||||
.get_my_public_ipv6_addr()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn wait_for_public_ipv6_route(inst: &Instance, target: cidr::Ipv6Inet) {
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
inst.get_peer_manager()
|
||||
.list_public_ipv6_routes()
|
||||
.await
|
||||
.contains(&target)
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn route_exists_in_ns(ns: &str, needle: &str) -> bool {
|
||||
run_ip_in_ns_output(ns, &["-6", "route", "show"])
|
||||
.lines()
|
||||
.any(|line| line.contains(needle))
|
||||
}
|
||||
|
||||
fn addr_exists_in_ns(ns: &str, dev: &str, needle: &str) -> bool {
|
||||
run_ip_in_ns_output(ns, &["-6", "addr", "show", "dev", dev]).contains(needle)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn public_ipv6_auto_addr_end_to_end() {
|
||||
let client_id = uuid::Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap();
|
||||
let (_lab, provider, client) = init_public_ipv6_two_node(client_id).await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
provider
|
||||
.get_global_ctx()
|
||||
.get_advertised_ipv6_public_addr_prefix()
|
||||
== Some(PublicIpv6Lab::PROVIDER_PREFIX.parse().unwrap())
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
|
||||
let leased = wait_for_public_ipv6_addr(&client).await;
|
||||
wait_for_public_ipv6_route(&provider, leased).await;
|
||||
|
||||
assert_eq!(
|
||||
provider
|
||||
.get_global_ctx()
|
||||
.config
|
||||
.get_ipv6_public_addr_prefix(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
provider
|
||||
.get_global_ctx()
|
||||
.get_advertised_ipv6_public_addr_prefix(),
|
||||
Some(PublicIpv6Lab::PROVIDER_PREFIX.parse().unwrap())
|
||||
);
|
||||
let provider_prefix = PublicIpv6Lab::PROVIDER_PREFIX
|
||||
.parse::<cidr::Ipv6Cidr>()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
provider
|
||||
.get_peer_manager()
|
||||
.get_my_info()
|
||||
.await
|
||||
.ipv6_public_addr_prefix,
|
||||
Some(
|
||||
cidr::Ipv6Inet::new(
|
||||
provider_prefix.first_address(),
|
||||
provider_prefix.network_length()
|
||||
)
|
||||
.unwrap()
|
||||
.into()
|
||||
)
|
||||
);
|
||||
let provider_info = provider
|
||||
.get_peer_manager()
|
||||
.get_local_public_ipv6_info()
|
||||
.await;
|
||||
let client_peer_id = client.get_peer_manager().get_my_info().await.peer_id;
|
||||
assert_eq!(
|
||||
provider_info.provider_prefix,
|
||||
Some(
|
||||
cidr::Ipv6Inet::new(
|
||||
provider_prefix.first_address(),
|
||||
provider_prefix.network_length()
|
||||
)
|
||||
.unwrap()
|
||||
.into()
|
||||
)
|
||||
);
|
||||
assert_eq!(provider_info.provider_leases.len(), 1);
|
||||
assert_eq!(provider_info.provider_leases[0].peer_id, client_peer_id);
|
||||
assert_eq!(
|
||||
provider_info.provider_leases[0].inst_id,
|
||||
client_id.to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
provider_info.provider_leases[0].leased_addr,
|
||||
Some(leased.into())
|
||||
);
|
||||
assert!(
|
||||
leased.address().segments()[0] & 0xfe00 != 0xfc00,
|
||||
"leased address should not be unique-local: {leased}"
|
||||
);
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
addr_exists_in_ns(
|
||||
PublicIpv6Lab::CLIENT_NS,
|
||||
PublicIpv6Lab::CLIENT_TUN,
|
||||
&leased.to_string(),
|
||||
) && route_exists_in_ns(
|
||||
PublicIpv6Lab::CLIENT_NS,
|
||||
&format!("default dev {}", PublicIpv6Lab::CLIENT_TUN),
|
||||
) && route_exists_in_ns(
|
||||
PublicIpv6Lab::PROVIDER_NS,
|
||||
&format!("{} dev {}", leased.address(), PublicIpv6Lab::PROVIDER_TUN),
|
||||
)
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async { ping6_test(PublicIpv6Lab::CLIENT_NS, PublicIpv6Lab::SERVER_IP, None).await },
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
ping6_test(
|
||||
PublicIpv6Lab::SERVER_NS,
|
||||
leased.address().to_string().as_str(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
|
||||
drop_insts(vec![provider, client]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn public_ipv6_auto_addr_reconnect_reuses_same_address() {
|
||||
let client_id = uuid::Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap();
|
||||
let (_lab, provider, client) = init_public_ipv6_two_node(client_id).await;
|
||||
let first = wait_for_public_ipv6_addr(&client).await;
|
||||
|
||||
drop_insts(vec![client]).await;
|
||||
|
||||
let client_cfg = get_public_ipv6_config(
|
||||
"client_public_ipv6_reconnect",
|
||||
PublicIpv6Lab::CLIENT_NS,
|
||||
"10.144.144.2",
|
||||
PublicIpv6Lab::CLIENT_TUN,
|
||||
client_id,
|
||||
);
|
||||
client_cfg.set_ipv6_public_addr_auto(true);
|
||||
let mut client = Instance::new(client_cfg);
|
||||
client.run().await.unwrap();
|
||||
provider
|
||||
.get_conn_manager()
|
||||
.add_connector(TcpTunnelConnector::new(
|
||||
"tcp://10.1.1.2:11010".parse().unwrap(),
|
||||
));
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
provider.get_peer_manager().list_routes().await.len() == 1
|
||||
&& client.get_peer_manager().list_routes().await.len() == 1
|
||||
},
|
||||
Duration::from_secs(8),
|
||||
)
|
||||
.await;
|
||||
|
||||
let second = wait_for_public_ipv6_addr(&client).await;
|
||||
assert_eq!(first, second);
|
||||
|
||||
wait_for_condition(
|
||||
|| async { ping6_test(PublicIpv6Lab::CLIENT_NS, PublicIpv6Lab::SERVER_IP, None).await },
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
|
||||
drop_insts(vec![provider, client]).await;
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
@@ -3077,7 +3599,15 @@ pub async fn config_patch_test() {
|
||||
};
|
||||
use crate::tunnel::common::tests::_tunnel_pingpong_netns_with_timeout;
|
||||
|
||||
let insts = init_three_node("udp").await;
|
||||
let insts = init_three_node_ex(
|
||||
"udp",
|
||||
|cfg| {
|
||||
cfg.set_ipv6(None);
|
||||
cfg
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
check_route(
|
||||
"10.144.144.2/24",
|
||||
@@ -3124,6 +3654,46 @@ pub async fn config_patch_test() {
|
||||
},
|
||||
);
|
||||
|
||||
// 测试1.1:修改公网 IPv6 provider 相关配置
|
||||
let public_prefix = "2001:db8:100::/64";
|
||||
let patch = InstanceConfigPatch {
|
||||
ipv6_public_addr_provider: Some(true),
|
||||
ipv6_public_addr_auto: Some(true),
|
||||
ipv6_public_addr_prefix: Some(public_prefix.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
insts[1]
|
||||
.get_config_patcher()
|
||||
.apply_patch(patch)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
insts[1]
|
||||
.get_global_ctx()
|
||||
.config
|
||||
.get_ipv6_public_addr_provider()
|
||||
);
|
||||
assert!(insts[1].get_global_ctx().config.get_ipv6_public_addr_auto());
|
||||
assert_eq!(
|
||||
insts[1]
|
||||
.get_global_ctx()
|
||||
.config
|
||||
.get_ipv6_public_addr_prefix(),
|
||||
Some(public_prefix.parse().unwrap())
|
||||
);
|
||||
assert!(
|
||||
insts[1]
|
||||
.get_global_ctx()
|
||||
.get_feature_flags()
|
||||
.ipv6_public_addr_provider
|
||||
);
|
||||
assert_eq!(
|
||||
insts[1]
|
||||
.get_global_ctx()
|
||||
.get_advertised_ipv6_public_addr_prefix(),
|
||||
Some(public_prefix.parse().unwrap())
|
||||
);
|
||||
|
||||
// 测试2: 端口转发
|
||||
let patch = InstanceConfigPatch {
|
||||
port_forwards: vec![PortForwardPatch {
|
||||
|
||||
Reference in New Issue
Block a user