Network Guard API

kavalai.net decides which addresses a request to a model-chosen URL may reach. The bundled http_request and crawl_url tools use it by default, and PublicOnlyTransport gives the same protection to any httpx client in your own tools. It is not re-exported from kavalai, because it needs httpx and import kavalai has to work without it.

For what the guard covers and where it stops, read the Outbound requests section of Safety; for the tools, Bundled tools.

Copyright 2026 OÜ KAVAL AI (registry code 17393877)

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Outbound requests to URLs a model chooses: the guard against SSRF.

A bundled web tool fetches whatever URL a model composes, and a model can be talked into composing http://169.254.169.254/ or http://localhost:5432/. This module refuses such targets.

  • is_public_address() decides whether one IP address is globally routable unicast.

  • ensure_public_url() checks a URL: scheme, authority, host name, and every address the host resolves to. It is a pre-check — by itself it is defeated by DNS rebinding, because the client that fetches the URL resolves the name a second time and may receive a different answer.

  • PublicOnlyTransport closes that gap for httpx. Its network backend resolves the host, checks every address and then connects to the address it checked, so no second lookup takes place. TLS still verifies the certificate for the host name and sends it as SNI, and the Host header is the one in the URL. Each redirect hop is a new request through the same transport, so it is checked in the same way.

The module is not imported by kavalai/__init__.py: it needs httpx, and import kavalai has to work under Pyodide without it.

kavalai.net.Resolver

An ASCII host name to the IP addresses it resolves to, in preference order.

An empty list means the name does not resolve. resolve_host() is the default; tests pass their own.

alias of Callable[[str], Awaitable[list[str]]]

kavalai.net.METADATA_ADDRESSES = frozenset({IPv4Address('100.100.100.200'), IPv4Address('168.63.129.16'), IPv4Address('169.254.169.254'), IPv4Address('169.254.170.2'), IPv6Address('fd00:ec2::254')})

Cloud metadata and platform endpoints.

AWS, GCP, Azure, Oracle and DigitalOcean serve instance metadata at 169.254.169.254, ECS task credentials at 169.254.170.2, Alibaba at 100.100.100.200 and AWS over IPv6 at fd00:ec2::254. All of these fall in ranges refused anyway; they are listed so the refusal does not depend on the interpreter’s address tables. Azure’s platform endpoint 168.63.129.16 is a global address, so without this entry it would be allowed.

kavalai.net.BLOCKED_NETWORKS = (IPv4Network('0.0.0.0/8'), IPv4Network('100.64.0.0/10'), IPv4Network('192.0.0.0/24'), IPv4Network('198.18.0.0/15'), IPv6Network('fec0::/10'), IPv6Network('3fff::/20'), IPv6Network('5f00::/16'))

Networks refused in addition to what ipaddress calls non-global.

0.0.0.0/8 (reaches the local host on Linux), carrier-grade NAT 100.64.0.0/10, IETF protocol assignments 192.0.0.0/24, benchmarking 198.18.0.0/15, deprecated IPv6 site-local fec0::/10 (global to Python 3.12), IPv6 documentation 3fff::/20 (RFC 9637, global to Python 3.12) and SRv6 segment identifiers 5f00::/16.

exception kavalai.net.UnsafeUrlError[source]

Bases: ValueError

A URL, or an address its host resolves to, is not a public target.

kavalai.net.is_public_address(address: str | IPv4Address | IPv6Address) bool[source]

Whether address is globally routable unicast.

Refuses loopback, private, link-local, multicast, reserved, unspecified, carrier-grade NAT, benchmarking and documentation ranges, 0.0.0.0/8 and the cloud metadata endpoints in METADATA_ADDRESSES.

An IPv4-mapped address (::ffff:a.b.c.d) and a NAT64 address (64:ff9b::a.b.c.d) are judged as the IPv4 address they reach. An IPv6 address that tunnels to an IPv4 one — IPv4-compatible, 6to4, Teredo — is refused when that IPv4 address is not public, and is otherwise judged as an IPv6 address.

Parameters:
address: str | IPv4Address | IPv6Address

An address as text or as an ipaddress object. A zone index (fe80::1%eth0) is ignored.

Raises:

ValueErroraddress is not an IP address.

kavalai.net.parse_ip_literal(host: str) IPv4Address | IPv6Address | None[source]

The address host spells out, or None when it is a name.

Accepts IPv6 with or without brackets and IPv4 in every form the C library’s inet_aton accepts: dotted, decimal, octal, hex and the short forms (127.1). One trailing dot is ignored.

async kavalai.net.resolve_host(host: str) list[str][source]

host’s addresses from the system resolver, in its preference order.

Returns an empty list when the name does not resolve.

async kavalai.net.ensure_public_url(url: str, *, resolver: Callable[[str], Awaitable[list[str]]] | None = None) str[source]

Return url unchanged when it points at a public host; raise otherwise.

Refuses a scheme other than http/https; credentials, a backslash or whitespace in the URL, which different URL parsers read differently; localhost and *.localhost; a host whose first label is metadata; *.internal and *.local; and a host with any address for which is_public_address() is false. IP literals are read in every encoding (http://2130706433/ is 127.0.0.1) and never sent to the resolver; non-ASCII names are IDNA-encoded first.

This is a pre-check: the client that fetches the URL resolves the name again. Use PublicOnlyTransport where the connection itself is under the SDK’s control.

Parameters:
url: str

The absolute URL to check.

resolver: Callable[[str], Awaitable[list[str]]] | None = None

Resolves a host name; resolve_host() by default.

Raises:

UnsafeUrlError – The URL or one of its addresses is not public, or the host does not resolve.

class kavalai.net.PublicOnlyTransport(*, resolver: Callable[[str], Awaitable[list[str]]] | None = None, network_backend: AsyncNetworkBackend | None = None, verify: Any = True, cert: Any = None, trust_env: bool = True, http1: bool = True, http2: bool = False, limits: Limits | None = None, retries: int = 0)[source]

Bases: AsyncHTTPTransport

An httpx transport that connects only to public addresses.

Every connection resolves its host once, refuses it unless every address is public (is_public_address()), and dials the address it checked, so a DNS answer that changes between the check and the connection cannot redirect it. The certificate is still verified against the host name, which is also sent as SNI. Redirects are followed by httpx as new requests through this transport, so each hop is checked. A refusal raises UnsafeUrlError from the request.

verify, cert, trust_env, http1, http2, limits and retries mean what they mean for httpx.AsyncHTTPTransport. There is no proxy: through a proxy the proxy resolves the name, so the guard could not apply. The constructor builds its own connection pool rather than calling the parent’s, because that one offers no way to choose the network backend.

Parameters:
resolver: Callable[[str], Awaitable[list[str]]] | None = None

Resolves a host name; resolve_host() by default.

network_backend: AsyncNetworkBackend | None = None

The httpcore backend that opens the socket to the vetted address; httpcore.AnyIOBackend by default.