import ipanon| Name | Type | Description |
|---|---|---|
Anonymizer |
class | Core anonymization engine |
Category |
enum | IP classification categories |
NetworkEntry |
dataclass | A network with match scope and host-bit boundary |
NetworkRegistry |
class | Subnet registry for host-bit locking |
PassThroughCollisionError |
exception | Raised on anonymized/pass-through IP collision |
classify_ip |
function | Classify an IP into Category A/B/C |
find_ips |
function | Find all IPs in a text string |
scan_and_replace |
function | Find and replace all IPs in text |
The core anonymization engine. Handles IP classification, permutation, remapping, caching, and pass-through logic.
Anonymizer(
salt: str | None = None,
remaps: dict[int, int] | None = None,
pass_through_prefixes: list[str] | None = None,
allow_pt_collisions: bool = False,
quiet: bool = False,
verbose: bool = False,
ignore_subnets: bool = False,
ignore_reserved: bool = False,
network_registry: NetworkRegistry | None = None,
)Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
salt |
str | None |
None |
Anonymization salt for deterministic output. If None, a random salt is generated and printed to stderr. |
remaps |
dict[int, int] | None |
None |
Map mixed first-octets to pure-public targets. Keys must be mixed octets (e.g., 172), values must be pure-public octets. |
pass_through_prefixes |
list[str] | None |
None |
CIDR strings for IPs that should not be anonymized (e.g., ["10.0.0.0/8"]). |
allow_pt_collisions |
bool |
False |
If True, downgrade pass-through collision errors to stderr warnings instead of raising PassThroughCollisionError. |
quiet |
bool |
False |
Suppress all warnings to stderr. |
verbose |
bool |
False |
Enable verbose warnings (e.g., short-prefix pass-through notices). |
ignore_subnets |
bool |
False |
Treat sub-/8 IPv4 Cat A ranges (172.16/12, 192.168/16, 100.64/10, 169.254/16) as public. Only 10.0.0.0/8 remains range-preserved. Cat B and IPv6 are unaffected. |
ignore_reserved |
bool |
False |
Remove ALL Cat A and Cat B handling. Every IP (including loopback, multicast, private) gets fully anonymized as public. Affects both IPv4 and IPv6. |
network_registry |
NetworkRegistry | None |
None |
If provided, enables subnet-aware host-bit locking. The registry determines which bits are permuted (network portion) and which are preserved (host portion). |
Raises:
ValueError— if a remap source is not a mixed first-octet, a remap target is not pure-public, or a remap target conflicts with a pass-through /8 exclusion.
Examples:
# Basic usage with auto-generated salt
anon = Anonymizer() # prints "Generated salt: ..." to stderr
# Deterministic with fixed salt
anon = Anonymizer(salt="my-secret")
# With remapping: route 172.x public IPs to the 42.x range
anon = Anonymizer(salt="s", remaps={172: 42})
# Preserve your monitoring subnet
anon = Anonymizer(salt="s", pass_through_prefixes=["10.0.0.0/8"])
# Anonymize everything, including private/reserved ranges
anon = Anonymizer(salt="s", ignore_reserved=True)Anonymize a single IP address or CIDR prefix string.
Parameters:
addr_str— IP address string, optionally with CIDR prefix (e.g.,"8.8.8.8","10.0.0.0/8","2001:db8::1/32").
Returns: The anonymized IP string. CIDR prefix notation is preserved if present.
Behavior by category:
- Cat A (range-preserved): Prefix bits are locked, remaining bits are permuted.
10.1.2.3stays in10.0.0.0/8. - Cat B (pass-through): Returned unchanged.
127.0.0.1→127.0.0.1. - Cat C (public): First octet is permuted within the pure-public pool, lower 24 bits are prefix-preserving permuted.
Special cases:
- Invalid IP strings are returned unchanged.
- Short prefixes (
< /8for IPv4,< /32for IPv6) are returned unchanged. - Results are cached: calling
anonymize("8.8.8.8")twice returns the same result without recomputation.
anon = Anonymizer(salt="test")
anon.anonymize("8.8.8.8") # Public → fully anonymized
anon.anonymize("10.1.2.3") # Private → stays in 10.0.0.0/8
anon.anonymize("127.0.0.1") # Loopback → "127.0.0.1"
anon.anonymize("192.168.1.0/24") # CIDR preserved → "192.168.x.0/24"
anon.anonymize("2001:db8::1") # IPv6 → fully anonymized
anon.anonymize("not-an-ip") # Invalid → "not-an-ip"Return the mapping of all IPs processed so far.
Returns: A dict mapping original IP strings to their anonymized equivalents.
anon = Anonymizer(salt="test")
anon.anonymize("8.8.8.8")
anon.anonymize("10.1.2.3")
anon.get_mapping()
# {"8.8.8.8": "143.57.192.12", "10.1.2.3": "10.187.42.5"}Scan text for all IPv4 and IPv6 addresses (with optional CIDR notation) and replace them with anonymized versions.
Parameters:
text— Input text containing IP addresses.anonymizer— AnAnonymizerinstance.
Returns: Text with all detected IPs replaced.
from ipanon import Anonymizer, scan_and_replace
anon = Anonymizer(salt="test")
text = "Server 8.8.8.8 rejected connection from 10.0.1.5"
result = scan_and_replace(text, anon)
# "Server 143.57.192.12 rejected connection from 10.187.42.5"Detection notes:
- Matches both IPv4 and IPv6 addresses, including compressed forms.
- Rejects version-number-like patterns (e.g.,
1.2.3.4adjacent to other dots). - CIDR notation (
/0through/32for IPv4,/0through/128for IPv6) is detected and preserved.
Find all valid IP addresses in text without replacing them.
Parameters:
text— Input text to scan.
Returns: A list of re.Match objects. Each match's group(0) is the full IP string (including optional CIDR prefix).
from ipanon import find_ips
matches = find_ips("Servers: 8.8.8.8 and 2001:db8::1/48")
[m.group(0) for m in matches]
# ["8.8.8.8", "2001:db8::1/48"]Classify an IP address using the default (unfiltered) range tables.
Parameters:
addr— Anipaddress.IPv4Addressoripaddress.IPv6Addressobject.
Returns: A tuple of (Category, locked_bits):
- Category A:
(Category.RANGE_PRESERVED, prefix_len)— e.g.,(Category.RANGE_PRESERVED, 8)for10.x.x.x - Category B:
(Category.PASS_THROUGH, 32)for IPv4,(Category.PASS_THROUGH, 128)for IPv6 - Category C:
(Category.PUBLIC, 0)
import ipaddress
from ipanon import classify_ip, Category
classify_ip(ipaddress.IPv4Address("10.1.2.3"))
# (Category.RANGE_PRESERVED, 8)
classify_ip(ipaddress.IPv4Address("127.0.0.1"))
# (Category.PASS_THROUGH, 32)
classify_ip(ipaddress.IPv4Address("8.8.8.8"))
# (Category.PUBLIC, 0)
classify_ip(ipaddress.IPv6Address("fe80::1"))
# (Category.RANGE_PRESERVED, 10)Enum for IP classification categories.
| Member | Description |
|---|---|
Category.RANGE_PRESERVED |
Category A — private/link-local ranges; prefix locked, host bits permuted |
Category.PASS_THROUGH |
Category B — loopback, multicast, documentation; returned unchanged |
Category.PUBLIC |
Category C — routable public IPs; fully anonymized |
Collects subnets and provides lowest-host-boundary lookup for host-bit locking.
NetworkRegistry()| Method | Description |
|---|---|
add(spec: str) |
Add a network spec (plain or range CIDR). Interface notation accepted. |
lookup(addr_str: str) -> int | None |
Find host-bit boundary for address. Returns None if no match. |
load_file(path: str) |
Load specs from file (one per line, # comments, blank lines ignored). |
load_from_text(text: str) |
Auto-collect CIDR patterns from text. |
entries() -> list[NetworkEntry] |
Return all entries. |
to_spec_list() -> list[str] |
Export entries as spec strings. |
warn_overlaps() |
Print warnings to stderr for redundant overlapping networks. |
from ipanon import Anonymizer, NetworkRegistry
# Create registry with range notation
registry = NetworkRegistry()
registry.add("10.0.0.0/8-24") # /8 scope, host boundary at /24
registry.add("192.168.1.0/29") # /29 scope and boundary
registry.add("2001:db8::/32-64") # IPv6
# Use with Anonymizer
anon = Anonymizer(salt="test", network_registry=registry)
anon.anonymize("10.1.2.65") # Last octet (65) preserved
anon.anonymize("10.1.2.65/29") # → "x.y.z.65/29"Dataclass representing a network with its match scope and host-bit boundary.
| Field | Type | Description |
|---|---|---|
network |
IPv4Network | IPv6Network |
Match scope (e.g., 10.0.0.0/8) |
host_boundary |
int |
Prefix length where host bits start (e.g., 24) |
Exception raised when an anonymized IP collides with a --pass-through prefix and allow_pt_collisions is False.
from ipanon import Anonymizer, PassThroughCollisionError
try:
anon = Anonymizer(salt="s", pass_through_prefixes=["8.0.0.0/8"])
anon.anonymize("1.2.3.4") # might collide with 8.x.x.x
except PassThroughCollisionError as e:
print(f"Collision: {e}")To downgrade collisions to warnings instead:
anon = Anonymizer(salt="s", pass_through_prefixes=["8.0.0.0/8"], allow_pt_collisions=True)