Chio/Docs

Network Guards

Two guards govern outbound network access: EgressAllowlistGuard enforces a domain allowlist over ToolAction::NetworkEgress, and InternalNetworkGuard blocks SSRF targets (private ranges, cloud metadata endpoints, encoded IP literals). Both run on the same action variant; they apply in series so a request must clear both to reach the network.


EgressAllowlistGuard

Source: crates/chio-guards/src/egress_allowlist.rs. Guard name: egress-allowlist. Domain matching uses glob::Pattern against the lowercased host string.

Struct

crates/chio-guards/src/egress_allowlist.rs
pub struct EgressAllowlistGuard {
    allow_patterns: Vec<glob::Pattern>,
    block_patterns: Vec<glob::Pattern>,
}

impl EgressAllowlistGuard {
    pub fn new() -> Self;
    pub fn with_lists(
        allow: Vec<String>,
        block: Vec<String>,
    ) -> Result<Self, EgressAllowlistConfigError>;
    pub fn is_allowed(&self, domain: &str) -> bool;
}

Default allowlist

Verified from default_allow_patterns():

crates/chio-guards/src/egress_allowlist.rs
// Common AI/ML APIs
"*.openai.com"
"*.anthropic.com"
"api.github.com"

// Package registries
"*.npmjs.org"
"registry.npmjs.org"
"pypi.org"
"files.pythonhosted.org"
"crates.io"
"static.crates.io"

That is the entire default. Anything else is denied. The default blocklist is empty.

Configuration

KnobTypeDefaultPurpose
allow_patternsVec<String>9 globs (above)Allowed hosts. Domain must match at least one.
block_patternsVec<String>emptyHard blocks that override the allowlist. Checked first.

Algorithm

  1. Lowercase the input host.
  2. For each block pattern, deny on first match. Block list takes precedence over allow list.
  3. For each allow pattern, allow on first match.
  4. Default fall-through: deny.

Bare-domain wildcard quirk

Glob matching is literal: *.example.com matches api.example.com but not the bare example.com. Add the bare domain explicitly when you need it.

Failure modes

  • Invalid allow glob :: EgressAllowlistConfigError::InvalidAllowPattern.
  • Invalid block glob :: EgressAllowlistConfigError::InvalidBlockPattern.
  • Non-network actions return Verdict::Allow.
  • EgressAllowlistGuard::new() panics if the default config fails to compile (treated as a source-level invariant).

Example

chio.yaml
guards:
  egress_allowlist:
    allow_patterns:
      - "*.openai.com"
      - "api.anthropic.com"
      - "internal.corp.example"
    block_patterns:
      - "*.public-mirror.example"

InternalNetworkGuard

SSRF defense. Source: crates/chio-guards/src/internal_network.rs. Guard name: internal-network. Runs in addition to (not instead of) EgressAllowlistGuard.

Struct

crates/chio-guards/src/internal_network.rs
pub struct InternalNetworkGuard {
    extra_blocked_hosts: Vec<String>,
    dns_rebinding_detection: bool,
}

impl InternalNetworkGuard {
    pub fn new() -> Self;
    pub fn with_config(
        extra_blocked_hosts: Vec<String>,
        dns_rebinding_detection: bool,
    ) -> Self;
    pub fn check_host(&self, host: &str) -> Option<String>;
}

Configuration

KnobTypeDefaultPurpose
extra_blocked_hostsVec<String>emptyAdditional exact-match hostnames to block (case-insensitive).
dns_rebinding_detectionbooltrueWhen on, hostnames containing IP-octet substrings flag.

Blocked address classes

From is_private_ip and is_cloud_metadata_host:

ClassRange / valueSource
IPv4 loopback127.0.0.0/8RFC 1122
IPv4 private (Class A)10.0.0.0/8RFC 1918
IPv4 private (Class B)172.16.0.0/12RFC 1918
IPv4 private (Class C)192.168.0.0/16RFC 1918
IPv4 link-local169.254.0.0/16RFC 3927
IPv4 broadcast255.255.255.255RFC 919
IPv4 zero network0.0.0.0/8RFC 791
IPv6 loopback::1RFC 4291
IPv6 link-localfe80::/10RFC 4291
IPv6 unique-localfc00::/7RFC 4193
IPv6 unspecified::RFC 4291
IPv4-mapped IPv6::ffff:<ipv4> when ipv4 is privateRFC 4291
Cloud metadata IP169.254.169.254AWS / GCP / Azure
GCP metadata hostmetadata.google.internalGCP
Azure metadata hostmetadata.azure.comAzure
EC2 instance-datainstance-dataAWS
Internal TLDany host ending in .internalconventional
Kubernetes APIkubernetes.default.svc, kubernetes.defaultKubernetes

The .internal suffix is a coarse block

Any host whose name ends with .internal is blocked unconditionally. If your tenancy uses .internal for legitimate public-facing services, you must override this guard. The check is literal substring on the lowercased host.

Encoded-IP detection

From looks_like_encoded_ip. The guard tries to catch hostnames that obfuscate a private-IP literal:

EncodingExampleHeuristic
Hexadecimal0x7f000001Starts with 0x, all hex digits.
Decimal2130706433 (= 127.0.0.1)7 to 10 ASCII digits.
Octal-dotted0177.0.0.1Starts with 0, contains a dot, and at least one component starts with 0 and is > 1 char.

DNS-rebinding heuristics

From is_dns_rebinding_suspect. When a hostname is not itself a parseable IP but contains a substring that looks like a private IP, the guard flags it. The matched substrings:

crates/chio-guards/src/internal_network.rs
let suspicious_patterns = [
    "127-0-0-1",
    "127.0.0.1",
    "10-0-",
    "10.0.",
    "192-168-",
    "192.168.",
    "172-16-",
    "172.16.",
    "169-254-",
    "169.254.",
    "0x7f",   // hex-encoded 127
    "0177.",  // octal 127
];

So evil.127-0-0-1.attacker.com and evil.192-168-1.attacker.com flag. Disable this layer with with_config(_, false) if you accept the false-positive risk.

Algorithm

  1. Lowercase the host. Match cloud-metadata names first (literal table plus the .internal suffix).
  2. Compare against extra_blocked_hosts (case-insensitive equality).
  3. If dns_rebinding_detection is on and the host is not itself a parseable IP, run the suspicious- substring scan.
  4. Try to parse the host as an IpAddr. If it parses, run is_private_ip. If the host does not parse, run looks_like_encoded_ip.
  5. The first match wins. Otherwise allow.

Failure modes

  • Non-network actions return Verdict::Allow.
  • The guard does no DNS lookups. It cannot block an attacker-controlled hostname that resolves to a private IP at TCP-connect time. Pair it with a network-layer egress proxy that re-validates after DNS, or with a host-level firewall that enforces the same private-range policy.

Stacking the two guards

Both guards inspect ToolAction::NetworkEgress(host, _). Register them in the order below so an SSRF target is denied even when the egress allowlist is permissive:

rust
use chio_guards::{EgressAllowlistGuard, InternalNetworkGuard};

let mut pipeline = chio_guards::GuardPipeline::new();
pipeline.add(Box::new(InternalNetworkGuard::new()));   // SSRF first
pipeline.add(Box::new(EgressAllowlistGuard::new()));   // domain allowlist

Defense in depth

Even with a correctly configured allowlist, an attacker who controls a trusted hostname (DNS poisoning, dangling subdomain takeover) can point it at 169.254.169.254. Running InternalNetworkGuard before the allowlist closes that gap for IP literals; pairing with a proxy that revalidates after DNS resolution closes the resolved-host gap.

Next Steps

Network Guards · Chio Docs