Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3eb4d3d
Rename host to bind and use IP
adamspofford-dfinity Feb 20, 2026
89c19c7
Use the bind correctly
adamspofford-dfinity Feb 20, 2026
256660c
Use first configured domain or embed IP as a domain if none
adamspofford-dfinity Feb 20, 2026
54a5c14
Fix descriptor and add test
adamspofford-dfinity Feb 20, 2026
642c310
Disallow ipv6 binds
adamspofford-dfinity Feb 21, 2026
c4c42a4
Remove all flexible handling
adamspofford-dfinity Feb 21, 2026
34a6fd9
consistency
adamspofford-dfinity Feb 23, 2026
02050fe
use v1.1
adamspofford-dfinity Feb 23, 2026
7d3de35
fix test?
adamspofford-dfinity Feb 23, 2026
284cfe5
fix domain test for xplat
adamspofford-dfinity Feb 27, 2026
fb772e3
Merge branch 'main' into spofford/bind
adamspofford-dfinity Feb 27, 2026
374d933
clean up
adamspofford-dfinity Feb 27, 2026
cfcbcd5
remove bind domain resolution
adamspofford-dfinity Feb 28, 2026
c3afdb6
Merge branch 'main' into spofford/bind
adamspofford-dfinity Feb 28, 2026
6e831bb
clean up
adamspofford-dfinity Feb 28, 2026
0536944
Add test
adamspofford-dfinity Feb 28, 2026
855732c
.
adamspofford-dfinity Feb 28, 2026
a152915
Revert IP change
adamspofford-dfinity Mar 2, 2026
ea1375c
Merge branch 'main' into spofford/bind
adamspofford-dfinity Mar 2, 2026
72894d3
no more need for ip host
adamspofford-dfinity Mar 2, 2026
315194b
.
adamspofford-dfinity Mar 2, 2026
69abb18
re-add fallback bind
adamspofford-dfinity Mar 2, 2026
4913883
fix domains check
adamspofford-dfinity Mar 2, 2026
ba9495f
Changelog
adamspofford-dfinity Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Unreleased

* feat: Added `bind` key to network gateway config to pick your network interface (previous documentation mentioned a `host` key, but it did not do anything)
* feat: check for Candid incompatibility when upgrading a canister
* feat: Add `bitcoind-addr` and `dogecoind-addr` options for managed networks to connect to Bitcoin and Dogecoin nodes
* feat: Init/call arg files now support raw binary without conversion to hex
Expand Down
45 changes: 45 additions & 0 deletions crates/icp-cli/tests/network_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,3 +829,48 @@ async fn network_gateway_responds_to_custom_domain() {
resp.status()
);
}

#[cfg(target_os = "linux")] // alternate loopback
#[tokio::test]
async fn network_gateway_binds_to_configured_interface() {
let ctx = TestContext::new();
let project_dir = ctx.create_project_dir("custom-bind");

write_string(
&project_dir.join("icp.yaml"),
indoc! {r#"
networks:
- name: bind-network
mode: managed
gateway:
port: 0
bind: 127.0.0.2
environments:
- name: bind-env
network: bind-network
"#},
)
.expect("failed to write project manifest");

let _guard = ctx.start_network_in(&project_dir, "bind-network").await;
ctx.ping_until_healthy(&project_dir, "bind-network");

let network = ctx.wait_for_network_descriptor(&project_dir, "bind-network");
let port = network.gateway_port;

let client = reqwest::Client::builder()
.build()
.expect("failed to build reqwest client");

let resp = client
.get(format!("http://127.0.0.2:{port}/api/v2/status"))
.send()
.await
.expect("request to custom interface failed");

assert!(
resp.status().is_success(),
"gateway should respond successfully on custom interface, got {}",
resp.status()
);
}
11 changes: 6 additions & 5 deletions crates/icp/src/context/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ use crate::{
Configuration, Gateway, Managed, ManagedLauncherConfig, ManagedMode, MockNetworkAccessor,
Port, access::NetworkAccess,
},
project::DEFAULT_LOCAL_NETWORK_URL,
store_id::{Access as IdAccess, mock::MockInMemoryIdStore},
};
use candid::Principal;
use std::collections::HashMap;

const DEFAULT_LOCAL_NETWORK_URL: &str = "http://localhost:8000";

#[tokio::test]
async fn test_get_identity_default() {
let ctx = Context::mocked();
Expand Down Expand Up @@ -601,7 +602,7 @@ async fn test_get_agent_defaults_inside_project_with_default_local() {
managed: Managed {
mode: ManagedMode::Launcher(Box::new(ManagedLauncherConfig {
gateway: Gateway {
host: "localhost".to_string(),
bind: "127.0.0.1".to_string(),
port: Port::Fixed(8000),
domains: vec![],
},
Expand Down Expand Up @@ -671,7 +672,7 @@ async fn test_get_agent_defaults_with_overridden_local_network() {
managed: Managed {
mode: ManagedMode::Launcher(Box::new(ManagedLauncherConfig {
gateway: Gateway {
host: "localhost".to_string(),
bind: "127.0.0.1".to_string(),
port: Port::Fixed(9000),
domains: vec![],
},
Expand Down Expand Up @@ -743,7 +744,7 @@ async fn test_get_agent_defaults_with_overridden_local_environment() {
managed: Managed {
mode: ManagedMode::Launcher(Box::new(ManagedLauncherConfig {
gateway: Gateway {
host: "localhost".to_string(),
bind: "127.0.0.1".to_string(),
port: Port::Fixed(8000),
domains: vec![],
},
Expand All @@ -765,7 +766,7 @@ async fn test_get_agent_defaults_with_overridden_local_environment() {
managed: Managed {
mode: ManagedMode::Launcher(Box::new(ManagedLauncherConfig {
gateway: Gateway {
host: "localhost".to_string(),
bind: "127.0.0.1".to_string(),
port: Port::Fixed(7000),
domains: vec![],
},
Expand Down
4 changes: 2 additions & 2 deletions crates/icp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ impl MockProjectLoader {
managed: Managed {
mode: ManagedMode::Launcher(Box::new(ManagedLauncherConfig {
gateway: Gateway {
host: "localhost".to_string(),
bind: "127.0.0.1".to_string(),
port: Port::Fixed(8000),
domains: vec![],
},
Expand All @@ -440,7 +440,7 @@ impl MockProjectLoader {
managed: Managed {
mode: ManagedMode::Launcher(Box::new(ManagedLauncherConfig {
gateway: Gateway {
host: "localhost".to_string(),
bind: "127.0.0.1".to_string(),
port: Port::Fixed(8001),
domains: vec![],
},
Expand Down
14 changes: 7 additions & 7 deletions crates/icp/src/manifest/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ impl From<RootKey> for String {
#[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema)]
pub struct Gateway {
/// Network interface for the gateway. Defaults to 127.0.0.1
pub host: Option<String>,
/// Domains the gateway should respond to. localhost is always included.
pub bind: Option<String>,
/// Domains the gateway should respond to. Automatically includes localhost if applicable.
pub domains: Option<Vec<String>>,
/// Port for the gateway to listen on. Defaults to 8000
pub port: Option<u16>,
Expand Down Expand Up @@ -282,20 +282,20 @@ mod tests {
}

#[test]
fn managed_network_with_host() {
fn managed_network_with_bind() {
assert_eq!(
validate_network_yaml(indoc! {r#"
name: my-network
mode: managed
gateway:
host: localhost
bind: 127.0.0.1
"#}),
NetworkManifest {
name: "my-network".to_string(),
configuration: Mode::Managed(Managed {
mode: Box::new(ManagedMode::Launcher {
gateway: Some(Gateway {
host: Some("localhost".to_string()),
bind: Some("127.0.0.1".to_string()),
domains: None,
port: None,
}),
Expand All @@ -319,15 +319,15 @@ mod tests {
name: my-network
mode: managed
gateway:
host: localhost
bind: 127.0.0.1
port: 8000
"#}),
NetworkManifest {
name: "my-network".to_string(),
configuration: Mode::Managed(Managed {
mode: Box::new(ManagedMode::Launcher {
gateway: Some(Gateway {
host: Some("localhost".to_string()),
bind: Some("127.0.0.1".to_string()),
domains: None,
port: Some(8000)
}),
Expand Down
2 changes: 1 addition & 1 deletion crates/icp/src/network/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub async fn get_managed_network_access(
.fail();
}
}
let http_gateway_url = Url::parse(&format!("http://localhost:{port}")).unwrap();
let http_gateway_url = Url::parse(&format!("http://{}:{port}", desc.gateway.host)).unwrap();
Ok(NetworkAccess {
root_key: desc.root_key,
api_url: http_gateway_url.clone(),
Expand Down
14 changes: 14 additions & 0 deletions crates/icp/src/network/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ use uuid::Uuid;

use crate::prelude::*;

fn default_gateway_host() -> String {
"localhost".to_string()
}

fn default_gateway_ip() -> String {
"127.0.0.1".to_string()
}

/// Gateway port configuration within a network descriptor.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
Expand All @@ -25,6 +33,12 @@ pub struct NetworkDescriptorGatewayPort {
pub fixed: bool,
/// The TCP port the gateway is listening on.
pub port: u16,
/// The host to use when constructing URLs to reach the gateway.
#[serde(default = "default_gateway_host")]
pub host: String,
/// The IP address to use when constructing URLs to reach the API.
#[serde(default = "default_gateway_ip")]
pub ip: String,
}

/// Runtime state of a running managed network, persisted as `descriptor.json`.
Expand Down
6 changes: 5 additions & 1 deletion crates/icp/src/network/managed/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ pub async fn spawn_network_launcher(
let mut cmd = tokio::process::Command::new(network_launcher_path);
cmd.args([
"--interface-version",
"1.0.0",
"1.1.0",
"--state-dir",
state_dir.as_str(),
]);
cmd.args(["--bind", &launcher_config.gateway.bind]);
if let Port::Fixed(port) = launcher_config.gateway.port {
cmd.args(["--gateway-port", &port.to_string()]);
}
Expand Down Expand Up @@ -206,6 +207,9 @@ pub fn launcher_settings_flags(config: &ManagedLauncherConfig) -> Vec<String> {
for domain in &gateway.domains {
flags.push(format!("--domain={domain}"));
}
if gateway.domains.is_empty() {
flags.push(format!("--domain={}", gateway.bind));
}
flags
}

Expand Down
21 changes: 14 additions & 7 deletions crates/icp/src/network/managed/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ async fn run_network_launcher(
(LaunchMode::NativeLauncher(launcher_config), fixed_ports)
}
};

let (mut guard, instance, gateway, locator) = network_root
.with_write(async |root| -> Result<_, RunNetworkLauncherError> {
// Acquire locks for all fixed ports and check they're not in use
Expand Down Expand Up @@ -191,6 +190,8 @@ async fn run_network_launcher(
let gateway = NetworkDescriptorGatewayPort {
port: instance.gateway_port,
fixed,
host: "localhost".to_string(),
ip: "127.0.0.1".to_string(),
};
Ok((ShutdownGuard::Container(guard), instance, gateway, locator))
}
Expand All @@ -212,9 +213,15 @@ async fn run_network_launcher(
&root.state_dir(),
)
.await?;
let host = match launcher_config.gateway.domains.first() {
Some(domain) => domain.clone(),
None => launcher_config.gateway.bind.to_string(),
};
let gateway = NetworkDescriptorGatewayPort {
port: instance.gateway_port,
fixed: matches!(launcher_config.gateway.port, Port::Fixed(_)),
ip: launcher_config.gateway.bind.clone(),
host,
};
Ok((ShutdownGuard::Process(child), instance, gateway, locator))
}
Expand All @@ -228,7 +235,7 @@ async fn run_network_launcher(
}

let (candid_ui_canister_id, proxy_canister_id) = initialize_network(
&format!("http://localhost:{}", instance.gateway_port)
&format!("http://{}:{}", gateway.host, gateway.port)
.parse()
.unwrap(),
&instance.root_key,
Expand Down Expand Up @@ -316,7 +323,7 @@ fn transform_native_launcher_to_container(config: &ManagedLauncherConfig) -> Man
let port_bindings: HashMap<String, Option<Vec<PortBinding>>> = [(
"4943/tcp".to_string(),
Some(vec![PortBinding {
host_ip: Some("127.0.0.1".to_string()),
host_ip: Some(config.gateway.bind.to_string()),
host_port: Some(port.to_string()),
}]),
)]
Expand Down Expand Up @@ -874,9 +881,9 @@ mod tests {
fn transform_native_launcher_default_config() {
let config = ManagedLauncherConfig {
gateway: Gateway {
host: "localhost".to_string(),
bind: "127.0.0.1".to_string(),
port: Port::Fixed(8000),
domains: vec![],
domains: vec!["localhost".to_string()],
},
artificial_delay_ms: None,
ii: false,
Expand All @@ -891,7 +898,7 @@ mod tests {
opts.image,
"ghcr.io/dfinity/icp-cli-network-launcher:latest"
);
assert!(opts.args.is_empty());
assert!(opts.args.iter().eq(["--domain=localhost"]));
assert!(opts.extra_hosts.is_empty());
assert!(opts.rm_on_exit);
assert_eq!(opts.status_dir, "/app/status");
Expand All @@ -908,7 +915,7 @@ mod tests {
fn transform_native_launcher_random_port() {
let config = ManagedLauncherConfig {
gateway: Gateway {
host: "localhost".to_string(),
bind: "127.0.0.1".to_string(),
port: Port::Random,
domains: vec![],
},
Expand Down
26 changes: 15 additions & 11 deletions crates/icp/src/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ impl<'de> Deserialize<'de> for Port {
}
}

fn default_host() -> String {
"localhost".to_string()
fn default_bind() -> String {
"127.0.0.1".to_string()
}

#[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)]
pub struct Gateway {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_bind")]
pub bind: String,

#[serde(default)]
pub port: Port,
Expand All @@ -68,7 +68,7 @@ pub struct Gateway {
impl Default for Gateway {
fn default() -> Self {
Self {
host: default_host(),
bind: default_bind(),
port: Default::default(),
domains: Default::default(),
}
Expand Down Expand Up @@ -125,7 +125,7 @@ impl ManagedMode {
pub fn default_for_port(port: u16) -> Self {
ManagedMode::Launcher(Box::new(ManagedLauncherConfig {
gateway: Gateway {
host: default_host(),
bind: default_bind(),
port: if port == 0 {
Port::Random
} else {
Expand Down Expand Up @@ -206,20 +206,24 @@ impl Default for Configuration {
impl From<ManifestGateway> for Gateway {
fn from(value: ManifestGateway) -> Self {
let ManifestGateway {
host,
bind,
domains,
port,
} = value;
let host = host.unwrap_or("localhost".to_string());
let bind = bind.unwrap_or("127.0.0.1".to_string());
let port = match port {
Some(0) => Port::Random,
Some(p) => Port::Fixed(p),
None => Port::Random,
};
let mut domains = domains.unwrap_or_default();
if bind == "127.0.0.1" || bind == "0.0.0.0" || bind == "::1" || bind == "::" {
domains.insert(0, "localhost".to_string());
}
Gateway {
host,
bind,
port,
domains: domains.unwrap_or_default(),
domains,
}
}
}
Expand Down Expand Up @@ -451,7 +455,7 @@ mod tests {
let mode = Mode::Managed(ManifestManaged {
mode: Box::new(ManifestManagedMode::Launcher {
gateway: Some(ManifestGateway {
host: None,
bind: None,
port: Some(8000),
domains: None,
}),
Expand Down
Loading
Loading