mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-15 05:09:44 +08:00
* perf(net): set TCP_NODELAY on all tunnel and proxy TCP hops The sandbox tunnel added ~44 ms of latency to every small request/response because no socket in the path disabled Nagle's algorithm, so sub-MSS writes waited on delayed ACKs at each hop. Set TCP_NODELAY on every latency-sensitive TCP socket: - gateway: accepted connections on the public listener (gRPC relay frames and WS tunnel writes) - CLI: edge tunnel local accept + underlying WebSocket TCP stream, insecure TLS connector (tonic's default connector already does this), and service-forward accepted sockets - supervisor: direct-tcpip connect into the sandbox netns, TCP relay target dials, egress proxy accepted connections, and all upstream CONNECT/HTTP dials (via a new connect_upstream helper) Setting TCP_NODELAY on connect is best-effort: a failure only costs latency, so we log and continue rather than fail the connection. The sandbox SSH transport rides a unix domain socket and gRPC client channels use tonic defaults (nodelay on), so no change is needed there. Fixes #2219 Signed-off-by: Jim Meyer <jim@meyer4hire.com> * refactor(net): house TCP_NODELAY helper in a shared net module Address review feedback on the TCP_NODELAY change: - Move the shared set-nodelay helper out of supervisor_session into a new crate-private `net` module in openshell-supervisor-process, so ssh and supervisor_session no longer reach across modules through a pub(crate) item. - Make the best-effort comments at each call site terse and consistent. No behavior change; the benchmark ladder reproduces the same numbers. Signed-off-by: Jim Meyer <jim@meyer4hire.com> * refactor(net): consolidate TCP_NODELAY helpers into openshell_core::net Move the best-effort TCP_NODELAY helpers into the shared openshell_core::net module so every crate dials and configures sockets the same way: - Add set_tcp_nodelay_best_effort (accepted/existing streams) and connect_tcp_nodelay_best_effort (dial + set) with unit tests. - Migrate all call sites in openshell-cli, openshell-server, and the supervisor crates to the shared helpers. - Remove the crate-private net module from openshell-supervisor-process. - Document socket guidance in AGENTS.md (Network Sockets). Signed-off-by: Jim Meyer <jim@meyer4hire.com> * perf(net): set TCP_NODELAY on exec bridge and metadata server The gateway-side single-use SSH-over-relay loopback bridge and the sandbox IMDS metadata server were missed latency-sensitive TCP hops. Set TCP_NODELAY on the accepted client connection and both russh client dials of the exec bridge — interactive keystrokes and line-buffered PTY output are the most tinygram-heavy traffic in the system — and on the metadata server's accepted connections. Also log unrecognized MaybeTlsStream variants in the edge tunnel so a future TLS-backend change surfaces a silent TCP_NODELAY miss instead of skipping it quietly. Signed-off-by: Jim Meyer <jim@meyer4hire.com> * perf(net): set TCP_NODELAY on openshell-sdk socket paths The openshell-sdk crate landed on main with its own copies of the CLI's hand-rolled sockets, which the CLI and TUI are meant to consume. Give them the same treatment as the CLI equivalents: - edge_tunnel: the accepted local tunnel connection and the WebSocket's underlying TCP socket (plain and rustls variants). - transport: the dial in InsecureTlsConnector, tonic's custom-connector path. Only these hand-rolled sockets need it. Tonic's own connector defaults tcp_nodelay to true and applies it itself, so plain Endpoint::connect callers were already covered. Signed-off-by: Jim Meyer <jim@meyer4hire.com> --------- Signed-off-by: Jim Meyer <jim@meyer4hire.com>
757 lines
26 KiB
Rust
757 lines
26 KiB
Rust
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
//! Shared networking utilities for `OpenShell` crates.
|
||
//!
|
||
//! The IP-classification helpers enforce the always-blocked IP invariant
|
||
//! (loopback, link-local, unspecified) and the broader internal-IP
|
||
//! classification (adds RFC 1918 and ULA). They are used by:
|
||
//! - The sandbox proxy for runtime SSRF enforcement
|
||
//! - The mechanistic mapper for proposal filtering
|
||
//! - The gateway server for defense-in-depth validation on approval
|
||
//!
|
||
//! The socket tuning helpers ([`set_tcp_nodelay_best_effort`],
|
||
//! [`connect_tcp_nodelay_best_effort`]) help avoid the known latency
|
||
//! imposed by conflict between Nagle's algorithm and delayed ACK behaviors.
|
||
//!
|
||
|
||
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
|
||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||
use tokio::net::TcpStream;
|
||
|
||
/// Check if a hostname is a known cloud metadata hostname that resolves to an
|
||
/// always-blocked metadata service.
|
||
///
|
||
/// This is intentionally a static name check. Do not perform DNS resolution in
|
||
/// policy validation or proposal generation paths.
|
||
pub fn is_known_metadata_hostname(host: &str) -> bool {
|
||
let normalized = host.trim().trim_end_matches('.').to_ascii_lowercase();
|
||
matches!(normalized.as_str(), "metadata.google.internal")
|
||
}
|
||
|
||
/// Check if an IP address is link-local.
|
||
///
|
||
/// Covers IPv4 `169.254.0.0/16`, IPv6 `fe80::/10`, and IPv4-mapped IPv6
|
||
/// addresses whose embedded IPv4 address is link-local (`::ffff:169.254.x.x`).
|
||
///
|
||
/// This is a point-check helper used to build the always-blocked and
|
||
/// trusted-gateway exemption predicates. For CIDR-range overlap checks see
|
||
/// [`is_always_blocked_net`].
|
||
pub fn is_link_local_ip(ip: IpAddr) -> bool {
|
||
match ip {
|
||
IpAddr::V4(v4) => v4.is_link_local(),
|
||
IpAddr::V6(v6) => {
|
||
// fe80::/10 — IPv6 link-local
|
||
if (v6.segments()[0] & 0xffc0) == 0xfe80 {
|
||
return true;
|
||
}
|
||
// ::ffff:169.254.x.x — IPv4-mapped link-local
|
||
if let Some(v4) = v6.to_ipv4_mapped() {
|
||
return v4.is_link_local();
|
||
}
|
||
false
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Check if an IP address is always blocked regardless of policy.
|
||
///
|
||
/// Loopback, link-local, and unspecified addresses are never allowed even when
|
||
/// an endpoint has `allowed_ips` configured. This prevents proxy bypass
|
||
/// (loopback) and cloud metadata SSRF (link-local 169.254.x.x).
|
||
pub fn is_always_blocked_ip(ip: IpAddr) -> bool {
|
||
match ip {
|
||
IpAddr::V4(v4) => v4.is_loopback() || v4.is_link_local() || v4.is_unspecified(),
|
||
IpAddr::V6(v6) => {
|
||
if v6.is_loopback() || v6.is_unspecified() {
|
||
return true;
|
||
}
|
||
if is_link_local_ip(IpAddr::V6(v6)) {
|
||
return true;
|
||
}
|
||
// Check IPv4-mapped IPv6 (::ffff:x.x.x.x)
|
||
if let Some(v4) = v6.to_ipv4_mapped() {
|
||
return v4.is_loopback() || v4.is_unspecified();
|
||
}
|
||
false
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Check if a CIDR network overlaps any always-blocked range.
|
||
///
|
||
/// Returns `true` if the network contains or overlaps loopback (`127.0.0.0/8`),
|
||
/// link-local (`169.254.0.0/16`), unspecified (`0.0.0.0`), or their IPv6
|
||
/// equivalents. A CIDR like `0.0.0.0/0` is rejected because it contains
|
||
/// always-blocked addresses.
|
||
///
|
||
/// Used at policy load time and server-side approval to reject entries that
|
||
/// would be silently blocked at runtime by [`is_always_blocked_ip`].
|
||
pub fn is_always_blocked_net(net: IpNet) -> bool {
|
||
match net {
|
||
IpNet::V4(v4net) => {
|
||
let network = v4net.network();
|
||
let broadcast = v4net.broadcast();
|
||
|
||
// Check if the range overlaps 127.0.0.0/8 (loopback)
|
||
if broadcast >= Ipv4Addr::new(127, 0, 0, 0)
|
||
&& network <= Ipv4Addr::new(127, 255, 255, 255)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Check if the range overlaps 169.254.0.0/16 (link-local)
|
||
if broadcast >= Ipv4Addr::new(169, 254, 0, 0)
|
||
&& network <= Ipv4Addr::new(169, 254, 255, 255)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Check if the range contains 0.0.0.0 (unspecified)
|
||
if network == Ipv4Addr::UNSPECIFIED {
|
||
return true;
|
||
}
|
||
|
||
false
|
||
}
|
||
IpNet::V6(v6net) => {
|
||
// For IPv6, check the network address itself and representative
|
||
// addresses within the range.
|
||
let network = v6net.network();
|
||
|
||
// ::1 (loopback)
|
||
if v6net.contains(&Ipv6Addr::LOCALHOST) {
|
||
return true;
|
||
}
|
||
|
||
// :: (unspecified)
|
||
if v6net.contains(&Ipv6Addr::UNSPECIFIED) {
|
||
return true;
|
||
}
|
||
|
||
// fe80::/10 (link-local) — check overlap
|
||
if (network.segments()[0] & 0xffc0) == 0xfe80 {
|
||
return true;
|
||
}
|
||
// Also check if a broad prefix contains fe80::
|
||
if v6net.contains(&Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0)) {
|
||
return true;
|
||
}
|
||
|
||
// Check IPv4-mapped IPv6 addresses. The network-address check covers
|
||
// ranges whose first address is already in a blocked range
|
||
// (e.g. ::ffff:127.0.0.1/128, ::ffff:169.254.0.1/128). The
|
||
// containment checks below catch broader prefixes that only reach
|
||
// into a blocked range further in — e.g. ::ffff:168.0.0.0/103
|
||
// has a public network address but spans ::ffff:169.254.0.0.
|
||
if network
|
||
.to_ipv4_mapped()
|
||
.is_some_and(|v4| v4.is_loopback() || v4.is_link_local() || v4.is_unspecified())
|
||
{
|
||
return true;
|
||
}
|
||
if v6net.contains(&Ipv4Addr::LOCALHOST.to_ipv6_mapped())
|
||
|| v6net.contains(&Ipv4Addr::new(169, 254, 0, 0).to_ipv6_mapped())
|
||
|| v6net.contains(&Ipv4Addr::UNSPECIFIED.to_ipv6_mapped())
|
||
{
|
||
return true;
|
||
}
|
||
|
||
false
|
||
}
|
||
}
|
||
}
|
||
|
||
const RFC1918_10_NET: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(10, 0, 0, 0), 8);
|
||
const RFC1918_172_NET: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(172, 16, 0, 0), 12);
|
||
const RFC1918_192_NET: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(192, 168, 0, 0), 16);
|
||
const CGNAT_NET: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(100, 64, 0, 0), 10);
|
||
const IETF_PROTOCOL_ASSIGNMENTS_NET: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 0, 0), 24);
|
||
const TEST_NET_1: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(192, 0, 2, 0), 24);
|
||
const BENCHMARKING_NET: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(198, 18, 0, 0), 15);
|
||
const TEST_NET_2: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(198, 51, 100, 0), 24);
|
||
const TEST_NET_3: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::new(203, 0, 113, 0), 24);
|
||
const LIMITED_BROADCAST_NET: Ipv4Net = Ipv4Net::new_assert(Ipv4Addr::BROADCAST, 32);
|
||
const IPV6_ULA_NET: Ipv6Net = Ipv6Net::new_assert(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 0), 7);
|
||
|
||
const NON_HARD_INTERNAL_V4_NETS: [Ipv4Net; 10] = [
|
||
RFC1918_10_NET,
|
||
RFC1918_172_NET,
|
||
RFC1918_192_NET,
|
||
CGNAT_NET,
|
||
IETF_PROTOCOL_ASSIGNMENTS_NET,
|
||
TEST_NET_1,
|
||
BENCHMARKING_NET,
|
||
TEST_NET_2,
|
||
TEST_NET_3,
|
||
LIMITED_BROADCAST_NET,
|
||
];
|
||
|
||
fn ipv4_nets_intersect(left: Ipv4Net, right: Ipv4Net) -> bool {
|
||
left.network() <= right.broadcast() && right.network() <= left.broadcast()
|
||
}
|
||
|
||
fn ipv6_nets_intersect(left: Ipv6Net, right: Ipv6Net) -> bool {
|
||
left.network() <= right.broadcast() && right.network() <= left.broadcast()
|
||
}
|
||
|
||
fn ipv4_net_to_mapped_ipv6(net: Ipv4Net) -> Ipv6Net {
|
||
Ipv6Net::new_assert(net.network().to_ipv6_mapped(), 96_u8 + net.prefix_len())
|
||
}
|
||
|
||
/// Check if an IP address is internal (loopback, private RFC 1918, link-local,
|
||
/// or unspecified).
|
||
///
|
||
/// This is a broader check than [`is_always_blocked_ip`] — it includes RFC 1918
|
||
/// private ranges (`10/8`, `172.16/12`, `192.168/16`), CGNAT (`100.64.0.0/10`,
|
||
/// RFC 6598), other special-use ranges, and IPv6 ULA (`fc00::/7`) which are
|
||
/// allowable via `allowed_ips` but blocked by default without one.
|
||
///
|
||
/// Used by the proxy's default SSRF path and the mechanistic mapper to detect
|
||
/// when `allowed_ips` should be populated in proposals.
|
||
pub fn is_internal_ip(ip: IpAddr) -> bool {
|
||
match ip {
|
||
IpAddr::V4(v4) => is_internal_v4(v4),
|
||
IpAddr::V6(v6) => {
|
||
if v6.is_loopback() || v6.is_unspecified() {
|
||
return true;
|
||
}
|
||
if is_link_local_ip(IpAddr::V6(v6)) {
|
||
return true;
|
||
}
|
||
// fc00::/7 — IPv6 unique local addresses (ULA)
|
||
if (v6.segments()[0] & 0xfe00) == 0xfc00 {
|
||
return true;
|
||
}
|
||
// Check IPv4-mapped IPv6 (::ffff:x.x.x.x)
|
||
if let Some(v4) = v6.to_ipv4_mapped() {
|
||
return is_internal_v4(v4);
|
||
}
|
||
false
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Check if a CIDR network intersects any address range classified by
|
||
/// [`is_internal_ip`].
|
||
pub fn is_internal_net(net: IpNet) -> bool {
|
||
if is_always_blocked_net(net) {
|
||
return true;
|
||
}
|
||
|
||
match net {
|
||
IpNet::V4(net) => NON_HARD_INTERNAL_V4_NETS
|
||
.iter()
|
||
.any(|internal| ipv4_nets_intersect(net, *internal)),
|
||
IpNet::V6(net) => {
|
||
ipv6_nets_intersect(net, IPV6_ULA_NET)
|
||
|| NON_HARD_INTERNAL_V4_NETS
|
||
.iter()
|
||
.any(|internal| ipv6_nets_intersect(net, ipv4_net_to_mapped_ipv6(*internal)))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// IPv4 internal address check covering RFC 1918, CGNAT (RFC 6598), and other
|
||
/// special-use ranges that should never be reachable from sandbox egress.
|
||
fn is_internal_v4(v4: Ipv4Addr) -> bool {
|
||
// Prefer the stable `Ipv4Addr` predicates. `is_documentation()` (RFC 5737)
|
||
// covers all three TEST-NET ranges, including 192.0.2.0/24 (TEST-NET-1) which
|
||
// the previous manual checks missed.
|
||
if v4.is_loopback()
|
||
|| v4.is_private()
|
||
|| v4.is_link_local()
|
||
|| v4.is_unspecified()
|
||
|| v4.is_documentation()
|
||
|| v4.is_broadcast()
|
||
{
|
||
return true;
|
||
}
|
||
let octets = v4.octets();
|
||
// The ranges below have no stable std predicate yet, so keep them manual.
|
||
// 100.64.0.0/10 — CGNAT / shared address space (RFC 6598; `is_shared` is
|
||
// unstable). Commonly used by cloud VPC peering, Tailscale, and similar overlays.
|
||
if octets[0] == 100 && (octets[1] & 0xC0) == 64 {
|
||
return true;
|
||
}
|
||
// 192.0.0.0/24 — IETF protocol assignments (RFC 6890; no stable predicate).
|
||
if octets[0] == 192 && octets[1] == 0 && octets[2] == 0 {
|
||
return true;
|
||
}
|
||
// 198.18.0.0/15 — benchmarking (RFC 2544; `is_benchmarking` is unstable).
|
||
if octets[0] == 198 && (octets[1] & 0xFE) == 18 {
|
||
return true;
|
||
}
|
||
false
|
||
}
|
||
|
||
/// Enable `TCP_NODELAY` on a stream, logging (not returning) any failure.
|
||
///
|
||
/// Disabling Nagle's algorithm keeps small writes from waiting on delayed ACKs.
|
||
/// It's a latency optimization: if it fails the connection still works, just a
|
||
/// bit slower, so there is nothing for the caller to act on — we log and move on.
|
||
pub fn set_tcp_nodelay_best_effort(stream: &TcpStream) {
|
||
if let Err(e) = stream.set_nodelay(true) {
|
||
tracing::debug!(error = %e, "failed to set TCP_NODELAY");
|
||
}
|
||
}
|
||
|
||
/// Connect to `addrs`, then enable `TCP_NODELAY` on a best-effort basis, propagating
|
||
/// any errors from `TcpStream::connect`.
|
||
///
|
||
/// The returned stream is not *guaranteed* to have `TCP_NODELAY` set.
|
||
pub async fn connect_tcp_nodelay_best_effort(addrs: &[SocketAddr]) -> std::io::Result<TcpStream> {
|
||
let stream = TcpStream::connect(addrs).await?;
|
||
set_tcp_nodelay_best_effort(&stream);
|
||
Ok(stream)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
// -- is_known_metadata_hostname --
|
||
|
||
#[test]
|
||
fn test_known_metadata_hostname_accepts_gcp_variants() {
|
||
assert!(is_known_metadata_hostname("metadata.google.internal"));
|
||
assert!(is_known_metadata_hostname("METADATA.GOOGLE.INTERNAL"));
|
||
assert!(is_known_metadata_hostname("metadata.google.internal."));
|
||
}
|
||
|
||
#[test]
|
||
fn test_known_metadata_hostname_rejects_public_hosts() {
|
||
assert!(!is_known_metadata_hostname("api.github.com"));
|
||
assert!(!is_known_metadata_hostname(""));
|
||
}
|
||
|
||
// -- is_link_local_ip --
|
||
|
||
#[test]
|
||
fn test_link_local_ip_v4() {
|
||
assert!(is_link_local_ip(IpAddr::V4(Ipv4Addr::new(169, 254, 0, 1))));
|
||
assert!(is_link_local_ip(IpAddr::V4(Ipv4Addr::new(
|
||
169, 254, 169, 254
|
||
))));
|
||
assert!(is_link_local_ip(IpAddr::V4(Ipv4Addr::new(
|
||
169, 254, 255, 255
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_link_local_ip_v6_fe80() {
|
||
assert!(is_link_local_ip(IpAddr::V6(Ipv6Addr::new(
|
||
0xfe80, 0, 0, 0, 0, 0, 0, 1
|
||
))));
|
||
// Upper boundary of fe80::/10 (febf:...)
|
||
assert!(is_link_local_ip(IpAddr::V6(Ipv6Addr::new(
|
||
0xfebf, 0, 0, 0, 0, 0, 0, 1
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_link_local_ip_v6_mapped_v4() {
|
||
let mapped = Ipv4Addr::new(169, 254, 1, 2).to_ipv6_mapped();
|
||
assert!(is_link_local_ip(IpAddr::V6(mapped)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_link_local_ip_not_loopback() {
|
||
assert!(!is_link_local_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)));
|
||
assert!(!is_link_local_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_link_local_ip_not_unspecified() {
|
||
assert!(!is_link_local_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED)));
|
||
assert!(!is_link_local_ip(IpAddr::V6(Ipv6Addr::UNSPECIFIED)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_link_local_ip_not_rfc1918() {
|
||
assert!(!is_link_local_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
|
||
assert!(!is_link_local_ip(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1))));
|
||
assert!(!is_link_local_ip(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_link_local_ip_not_public() {
|
||
assert!(!is_link_local_ip(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))));
|
||
assert!(!is_link_local_ip(IpAddr::V6(Ipv6Addr::new(
|
||
0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_link_local_ip_not_v6_mapped_loopback() {
|
||
let mapped = Ipv4Addr::LOCALHOST.to_ipv6_mapped();
|
||
assert!(!is_link_local_ip(IpAddr::V6(mapped)));
|
||
}
|
||
|
||
// -- is_always_blocked_ip --
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_loopback_v4() {
|
||
assert!(is_always_blocked_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)));
|
||
assert!(is_always_blocked_ip(IpAddr::V4(Ipv4Addr::new(
|
||
127, 0, 0, 2
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_link_local_v4() {
|
||
assert!(is_always_blocked_ip(IpAddr::V4(Ipv4Addr::new(
|
||
169, 254, 169, 254
|
||
))));
|
||
assert!(is_always_blocked_ip(IpAddr::V4(Ipv4Addr::new(
|
||
169, 254, 0, 1
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_loopback_v6() {
|
||
assert!(is_always_blocked_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_link_local_v6() {
|
||
assert!(is_always_blocked_ip(IpAddr::V6(Ipv6Addr::new(
|
||
0xfe80, 0, 0, 0, 0, 0, 0, 1
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_unspecified_v4() {
|
||
assert!(is_always_blocked_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_unspecified_v6() {
|
||
assert!(is_always_blocked_ip(IpAddr::V6(Ipv6Addr::UNSPECIFIED)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_ipv4_mapped_v6_loopback() {
|
||
let v6 = Ipv4Addr::LOCALHOST.to_ipv6_mapped();
|
||
assert!(is_always_blocked_ip(IpAddr::V6(v6)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_ipv4_mapped_v6_link_local() {
|
||
let v6 = Ipv4Addr::new(169, 254, 169, 254).to_ipv6_mapped();
|
||
assert!(is_always_blocked_ip(IpAddr::V6(v6)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_allows_rfc1918() {
|
||
assert!(!is_always_blocked_ip(IpAddr::V4(Ipv4Addr::new(
|
||
10, 0, 0, 1
|
||
))));
|
||
assert!(!is_always_blocked_ip(IpAddr::V4(Ipv4Addr::new(
|
||
172, 16, 0, 1
|
||
))));
|
||
assert!(!is_always_blocked_ip(IpAddr::V4(Ipv4Addr::new(
|
||
192, 168, 0, 1
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_ip_allows_public() {
|
||
assert!(!is_always_blocked_ip(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))));
|
||
assert!(!is_always_blocked_ip(IpAddr::V6(Ipv6Addr::new(
|
||
0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888
|
||
))));
|
||
}
|
||
|
||
// -- is_always_blocked_net --
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_loopback_v4() {
|
||
let net: IpNet = "127.0.0.0/8".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_link_local_v4() {
|
||
let net: IpNet = "169.254.0.0/16".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_unspecified_v4() {
|
||
let net: IpNet = "0.0.0.0/32".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_loopback_v6() {
|
||
let net: IpNet = "::1/128".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_link_local_v6() {
|
||
let net: IpNet = "fe80::/10".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_ipv4_mapped_v6_loopback() {
|
||
let net: IpNet = "::ffff:127.0.0.1/128".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_allows_rfc1918() {
|
||
let net10: IpNet = "10.0.0.0/8".parse().unwrap();
|
||
let net172: IpNet = "172.16.0.0/12".parse().unwrap();
|
||
let net192: IpNet = "192.168.0.0/16".parse().unwrap();
|
||
assert!(!is_always_blocked_net(net10));
|
||
assert!(!is_always_blocked_net(net172));
|
||
assert!(!is_always_blocked_net(net192));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_allows_public() {
|
||
let net: IpNet = "8.8.8.0/24".parse().unwrap();
|
||
assert!(!is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_single_ip_loopback() {
|
||
let net: IpNet = "127.0.0.1/32".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_single_ip_metadata() {
|
||
let net: IpNet = "169.254.169.254/32".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_broad_cidr_containing_blocked() {
|
||
// 0.0.0.0/0 contains everything including unspecified, loopback, link-local
|
||
let net: IpNet = "0.0.0.0/0".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_v6_broad_containing_loopback() {
|
||
let net: IpNet = "::/0".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_v6_ipv4_mapped_loopback_single() {
|
||
let net: IpNet = "::ffff:127.0.0.1/128".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_v6_ipv4_mapped_link_local_single() {
|
||
let net: IpNet = "::ffff:169.254.0.1/128".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_v6_ipv4_mapped_broad_spans_link_local() {
|
||
// ::ffff:168.0.0.0/103 has a public network address (168.0.0.0) but
|
||
// the range covers 168.0.0.0–169.255.255.255, which includes the
|
||
// link-local block 169.254.0.0/16.
|
||
let net: IpNet = "::ffff:168.0.0.0/103".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_v6_ipv4_mapped_broad_spans_loopback() {
|
||
// ::ffff:64.0.0.0/98 has a public network address (64.0.0.0) but the
|
||
// range covers 64.0.0.0–127.255.255.255, which includes loopback.
|
||
let net: IpNet = "::ffff:64.0.0.0/98".parse().unwrap();
|
||
assert!(is_always_blocked_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_always_blocked_net_v6_ipv4_mapped_allows_public() {
|
||
// ::ffff:8.8.8.8/128 is a public address — should not be blocked.
|
||
let net: IpNet = "::ffff:8.8.8.8/128".parse().unwrap();
|
||
assert!(!is_always_blocked_net(net));
|
||
}
|
||
|
||
// -- is_internal_net --
|
||
|
||
#[test]
|
||
fn test_internal_net_rfc1918_contained_and_supernet() {
|
||
for cidr in ["10.1.0.0/16", "8.0.0.0/5"] {
|
||
assert!(is_internal_net(cidr.parse().unwrap()), "{cidr}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_net_cgnat_contained_and_supernet() {
|
||
for cidr in ["100.64.0.0/10", "100.0.0.0/9"] {
|
||
assert!(is_internal_net(cidr.parse().unwrap()), "{cidr}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_net_ipv4_special_use_ranges() {
|
||
for cidr in [
|
||
"192.0.0.0/24",
|
||
"192.0.2.0/24",
|
||
"198.18.0.0/15",
|
||
"198.51.100.0/24",
|
||
"203.0.113.0/24",
|
||
"255.255.255.255/32",
|
||
] {
|
||
assert!(is_internal_net(cidr.parse().unwrap()), "{cidr}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_net_ipv6_ula() {
|
||
for cidr in ["fc00::/7", "fd00::/8"] {
|
||
assert!(is_internal_net(cidr.parse().unwrap()), "{cidr}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_net_ipv4_mapped_cgnat_supernet() {
|
||
let net: IpNet = "::ffff:100.0.0.0/105".parse().unwrap();
|
||
assert!(is_internal_net(net));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_net_always_blocked() {
|
||
for cidr in ["127.0.0.0/8", "fe80::/10"] {
|
||
assert!(is_internal_net(cidr.parse().unwrap()), "{cidr}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_net_allows_large_public_ranges() {
|
||
for cidr in ["8.0.0.0/8", "::ffff:8.0.0.0/104"] {
|
||
assert!(!is_internal_net(cidr.parse().unwrap()), "{cidr}");
|
||
}
|
||
}
|
||
|
||
// -- is_internal_ip --
|
||
|
||
#[test]
|
||
fn test_internal_ip_rfc1918() {
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1))));
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_ip_loopback() {
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)));
|
||
assert!(is_internal_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_ip_link_local() {
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(
|
||
169, 254, 169, 254
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_ip_unspecified() {
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED)));
|
||
assert!(is_internal_ip(IpAddr::V6(Ipv6Addr::UNSPECIFIED)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_ip_v6_ula() {
|
||
assert!(is_internal_ip(IpAddr::V6(Ipv6Addr::new(
|
||
0xfc00, 0, 0, 0, 0, 0, 0, 1
|
||
))));
|
||
assert!(is_internal_ip(IpAddr::V6(Ipv6Addr::new(
|
||
0xfd00, 0, 0, 0, 0, 0, 0, 1
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_ip_allows_public() {
|
||
assert!(!is_internal_ip(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))));
|
||
assert!(!is_internal_ip(IpAddr::V6(Ipv6Addr::new(
|
||
0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_ip_ipv4_mapped_v6() {
|
||
let v6 = Ipv4Addr::new(10, 0, 0, 1).to_ipv6_mapped();
|
||
assert!(is_internal_ip(IpAddr::V6(v6)));
|
||
let v6_public = Ipv4Addr::new(8, 8, 8, 8).to_ipv6_mapped();
|
||
assert!(!is_internal_ip(IpAddr::V6(v6_public)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_ip_cgnat() {
|
||
// 100.64.0.0/10 — CGNAT / shared address space (RFC 6598)
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(100, 64, 0, 1))));
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(100, 100, 50, 3))));
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(
|
||
100, 127, 255, 255
|
||
))));
|
||
// Just outside the /10 boundary
|
||
assert!(!is_internal_ip(IpAddr::V4(Ipv4Addr::new(100, 128, 0, 1))));
|
||
assert!(!is_internal_ip(IpAddr::V4(Ipv4Addr::new(
|
||
100, 63, 255, 255
|
||
))));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_ip_special_use_ranges() {
|
||
// 192.0.0.0/24 — IETF protocol assignments
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(192, 0, 0, 1))));
|
||
// 198.18.0.0/15 — benchmarking
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(198, 18, 0, 1))));
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(198, 19, 255, 255))));
|
||
// 192.0.2.0/24 — TEST-NET-1 (RFC 5737), now covered via is_documentation()
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1))));
|
||
// 198.51.100.0/24 — TEST-NET-2
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1))));
|
||
// 203.0.113.0/24 — TEST-NET-3
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1))));
|
||
// 255.255.255.255 — limited broadcast (RFC 919), via is_broadcast()
|
||
assert!(is_internal_ip(IpAddr::V4(Ipv4Addr::BROADCAST)));
|
||
}
|
||
|
||
#[test]
|
||
fn test_internal_ip_ipv6_mapped_cgnat() {
|
||
let v6 = Ipv4Addr::new(100, 64, 0, 1).to_ipv6_mapped();
|
||
assert!(is_internal_ip(IpAddr::V6(v6)));
|
||
}
|
||
|
||
// -- tcp_nodelay helpers --
|
||
|
||
#[tokio::test]
|
||
async fn set_tcp_nodelay_best_effort_enables_nodelay() {
|
||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||
.await
|
||
.expect("bind");
|
||
let addr = listener.local_addr().expect("local addr");
|
||
let stream = TcpStream::connect(addr).await.expect("connect");
|
||
|
||
set_tcp_nodelay_best_effort(&stream);
|
||
assert!(stream.nodelay().expect("query TCP_NODELAY"));
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn connect_tcp_nodelay_best_effort_sets_nodelay() {
|
||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||
.await
|
||
.expect("bind");
|
||
let addr = listener.local_addr().expect("local addr");
|
||
|
||
let stream = connect_tcp_nodelay_best_effort(&[addr])
|
||
.await
|
||
.expect("connect");
|
||
assert!(stream.nodelay().expect("query TCP_NODELAY"));
|
||
}
|
||
}
|