Skip to content

refactor: extract SOCKS proxy resolution into a cache - #11619

Open
schneiderstefan wants to merge 1 commit into
masterfrom
stschnei/socks-proxy
Open

schneiderstefan wants to merge 1 commit into
masterfrom
stschnei/socks-proxy

Conversation

@schneiderstefan

Copy link
Copy Markdown
Contributor

CanisterHttpPoolManagerImpl::get_socks_proxy_addrs resolved the API boundary nodes that outcalls may be proxied through. That is registry policy rather than consensus logic, and an upcoming second caller (HTTP outcalls from composite queries) needs the same answer.

Move it into a new ic-https-outcalls-socks-proxy crate as a SocksProxyCache. In addition to the refactor, this commit also introduces caching for repeated lookups under the same registry version.

The pool manager builds its own provider from the registry_client and subnet_type it already receives, so its constructor is unchanged, and it resolves the same addresses as before. Only the logging thins out: the per-node warnings now appear once per registry version rather than on every on_state_change. The failure metrics still tick on every call, which is why the resolution hands its failures back to the caller instead of recording them itself.

`CanisterHttpPoolManagerImpl::get_socks_proxy_addrs` resolved the API
boundary nodes that outcalls may be proxied through. That is registry
policy rather than consensus logic, and an upcoming second caller (HTTP
outcalls from composite queries) needs the same answer.

Move it into a new `ic-https-outcalls-socks-proxy` crate as a
`SocksProxyCache`. In addition to the refactor, this commit also
introduces caching for repeated lookups under the same registry version.

The pool manager builds its own provider from the `registry_client` and
`subnet_type` it already receives, so its constructor is unchanged, and it
resolves the same addresses as before. Only the logging thins out: the
per-node warnings now appear once per registry version rather than on every
`on_state_change`. The failure metrics still tick on every call, which is
why the resolution hands its failures back to the caller instead of
recording them itself.
@schneiderstefan
schneiderstefan requested a review from a team as a code owner September 18, 2026 11:15
@zeropath-ai

zeropath-ai Bot commented Sep 18, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to b2d74c2.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/https_outcalls/socks_proxy/Cargo.toml
    Add socks_proxy crate dependencies and integration points
► rs/https_outcalls/socks_proxy/BUILD.bazel
    Declare socks_proxy library and tests in Bazel
► rs/https_outcalls/socks_proxy/src/lib.rs
    Implement SOCKS proxy resolution logic and cache, with tests
► rs/https_outcalls/socks_proxy/src/lib.rs
    Define SocksProxyCache and memoization behavior
► rs/https_outcalls/consensus/BUILD.bazel
    Include socks_proxy as a dependency for consensus tests
► rs/https_outcalls/consensus/Cargo.toml
    Add ic-https-outcalls-socks-proxy as a path dependency
► rs/https_outcalls/consensus/src/pool_manager.rs
    Use SocksProxyCache and add socks_proxy to dependencies
► rs/https_outcalls/consensus/src/metrics.rs
    Add CanisterHttpPoolManagerMetrics struct derivation for compatibility
► rs/https_outcalls/consensus/src/metrics.rs
    Add CanisterHttpPoolManagerMetrics field op_duration
► rs/https_outcalls/consensus/src/pool_manager.rs
    Replace direct subnet_type field with socks_proxy cache integration
► rs/https_outcalls/consensus/src/pool_manager.rs
    Initialize SocksProxyCache in constructor and use addrs()

no_op_logger(),
);

let first = cache.addrs();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's assert the lookup succeeds, e.g., by comparing the number of nodes in the lookup

Comment on lines +427 to +446
data_provider
.add(
&make_api_boundary_node_record_key(node_id),
version,
Some(ApiBoundaryNodeRecord::default()),
)
.unwrap();
data_provider
.add(
&make_node_record_key(node_id),
version,
Some(NodeRecord {
http: Some(ConnectionEndpoint {
ip_addr: format!("2001:db8::{i}"),
port: 8080,
}),
..Default::default()
}),
)
.unwrap();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be refactored into a test helper shared with registry_with_boundary_nodes

}

#[test]
fn returns_no_proxies_when_the_registry_is_unreadable() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could also assert that the errors are observed (in metrics)

Comment on lines +260 to +265
let ip_addr = &nodes
.iter()
.find(|(id, _)| id == node_id)
.expect("unknown boundary node id")
.1;
expected_addr(ip_addr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be also factored out into a shared helper shared with skips_boundary_nodes_without_http_endpoint

Comment on lines +254 to +255
/// other subnet type through the app ones. The two sets are disjoint, which
/// is what makes this test able to tell them apart.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this test relies on disjointess: it just asserts that socks_proxy_addrs_at looks up the proxies based on the subnet type in the registry, any two unequal sets would confirm that.


/// Never fails: an unreadable registry yields no proxies, degrading an
/// outcall to a direct attempt rather than failing it.
pub fn addrs(&self) -> Vec<String> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn addrs(&self) -> Vec<String> {
pub fn addrs(&self) -> Arc<Vec<String>> {

then we don't need to clone the content

Comment on lines +98 to +99
/// a lookup per boundary node. Failures are reported on every call, memoized or
/// not, so a persistent one keeps being visible.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can there be no transient failures that we wouldn't want to memoize but rather retry?

None
})
})
.map(|http_info| format!("socks5h://[{0}]:{SOCKS_PROXY_PORT}", http_info.ip_addr))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

format!("socks5h://[{0}]:{SOCKS_PROXY_PORT}", http_info.ip_addr) could be a helper reused by tests instead of redefined there

.filter_map(|node_id| socks_proxy_addr_of(registry_client, registry_version, node_id, log))
.collect();
if addrs.len() != eligible {
errors.push(errors::SOCKS_PROXY_ADDRS_UNRESOLVED);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also warn! here?

log.clone(),
);

let mut actual = pool_manager.get_socks_proxy_addrs();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new test does not go through CanisterHttpPoolManagerImpl and thus its wiring (subnet type, metrics updates) is not covered by tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants