Skip to content

CkIP

Job: read IP addresses strictly, check them against allow-lists, and tell internal addresses from public ones before your app talks to them.

An IP address identifies a machine on a network — "203.0.113.9" is the IPv4 style, "2001:db8::1" the newer IPv6 style. CkIP reads them more strictly than .NET itself: shorthand like "127.1" is rejected here even though the framework would accept it, because those odd spellings are exactly how attackers sneak internal addresses past security checks. A value that cannot be used answers the method's rejection value — "" from Address, false from the yes/no checks, your fallback from Version. Only Mask always answers something, because a log line should never go blank.

Quick look

using SafeCoder.CommonKit;

CkIP.Address("203.0.113.9");    // -> "203.0.113.9"
CkIP.Address("127.1");          // -> ""  (shorthand — rejected on purpose)
CkIP.IsPublic("8.8.8.8");       // -> true
CkIP.IsPublic("10.0.0.1");      // -> false  (private network)
CkIP.Mask("203.0.113.9");       // -> "203.0.113.***"
CkIP.Version("2001:db8::1");    // -> 6
CkIP.IsInternal("::ffff:10.0.0.1");   // -> true   (a private address in an IPv6 coat)
CkIP.IsRange("10.0.0.0/33");          // -> false  (the allow-list entry is a typo)
CkIP.SameAddress("2001:DB8::1", "2001:db8:0:0:0:0:0:1");   // -> true
CkIP.Anonymize("203.0.113.9");   // -> "203.0.113.0"
CkIP.Explain("127.1");           // -> "not four dot-separated numbers"

Methods

Address

Clean and validate an address.

string Address(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering "", when the value cannot be used.

Returns

When Default With throwIfInvalid: true
The value is a valid address The normalized address: "203.0.113.9"; IPv6 canonical: "2001:db8::1" The normalized address: "203.0.113.9"; IPv6 canonical: "2001:db8::1"
Rejected: missing, oversized, shorthand, a bare number, a leading-zero octet, a zone index, brackets or a port around the address ([::1]:8080, 1.2.3.4:80), or not a real address Empty string Throws CkIPException — carries Input, Expected and Reason

Non-empty in, empty out means rejected — check for "" at the call site.

The strict reader. IPv4 must be exactly four dot-separated plain numbers, each 0–255, with no leading zeros. IPv6 must parse, and comes back in its one canonical form, the RFC 5952 shape: lowercase hex, leading zeros dropped, the longest run of two or more zero groups compressed to ::, an IPv4-mapped address in dotted form (::ffff:10.0.0.1), brackets dropped. Anything else answers "" — and there is deliberately no fallback parameter here, because an address says who called, and a made-up address is worse than an empty one.

CkIP.Address("203.0.113.9");             // -> "203.0.113.9"
CkIP.Address("2001:DB8:0:0:0:0:0:1");    // -> "2001:db8::1"  (canonical form)
CkIP.Address("127.1");                   // -> ""  (shorthand for loopback — rejected)
CkIP.Address("2130706433");              // -> ""  (bare number — rejected)
CkIP.Address("01.2.3.4");                // -> ""  (leading zero reads as octal)
CkIP.Address("fe80::1%eth0");            // -> ""  (zone index — rejected)

InRanges

Is the address in your allow-list?

bool InRanges(
    string? input = null,
    params string[] ranges
)
Parameter Type Default What it does
input string? null The raw address.
ranges params string[] — The allowed ranges — CIDR notation or bare addresses, which match exactly.

Returns

When You get
The address is valid and inside at least one range true
No match — an unusable address, an unreadable range, an empty list, or a genuine miss false — and it never throws

No throwIfInvalid here — C# wants the params list last — so validate with Address first when a bad address must be told from a miss.

Answers whether an address falls inside any of the ranges you pass — the IP version of CkEmail's InDomains. A range uses CIDR notation: "203.0.113.0/24" means "every address whose first 24 bits match 203.0.113.0" — the bigger the number after the slash, the smaller the range. A bare address is also allowed and matches exactly. false means no match — for a bad address, an empty list, or a genuine miss alike.

CkIP.InRanges(
    input: "203.0.113.9",
    ranges: "203.0.113.0/24"
);   // -> true

CkIP.InRanges(
    input: "11.0.0.1",
    ranges: "10.0.0.0/8"
);   // -> false  (just outside the range)

CkIP.InRanges(
    input: "::ffff:10.0.0.1",
    ranges: "10.0.0.0/8"
);   // -> true  (an IPv4 address in an IPv6 coat is judged as the IPv4)

CkIP.InRanges(
    input: "203.0.113.9",
    ranges: new[] { "10.0.0.0/8", "203.0.113.9" }
);   // -> true  (a bare address matches exactly)

A range that cannot be read simply never matches — it must not quietly turn into "allow everything".

IsPublic

Internal or public?

bool IsPublic(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering false, when the value is not a usable address.

Returns

When Default With throwIfInvalid: true
The value is a valid public address true true
The value is a valid internal address — loopback, private, link-local, carrier-grade NAT, unique-local, multicast, reserved false false
Rejected: missing, oversized, or not a usable address false Throws CkIPException — carries Input, Expected and Reason

A bare false covers internal and unreadable alike — the flag is how you tell them apart.

Answers true only for a real public address. Loopback (the machine talking to itself), the private office ranges, link-local, carrier-grade NAT, the IPv6 private ranges, multicast and the reserved ranges all answer false. This is the same classification CkWeb's IsPublicHost applies to a URL's host before your app POSTs anywhere.

CkIP.IsPublic("8.8.8.8");           // -> true
CkIP.IsPublic("10.0.0.1");          // -> false  (private)
CkIP.IsPublic("100.64.0.1");        // -> false  (carrier-grade NAT)
CkIP.IsPublic("fd00::1");           // -> false  (IPv6 unique-local)
CkIP.IsPublic("192.0.2.55");        // -> false  (documentation range)
CkIP.IsPublic("64:ff9b::7f00:1");   // -> false  (loopback hidden inside NAT64)

An unusable value also answers false — pass throwIfInvalid: true when you need to tell "internal" and "unreadable" apart.

Mask

Safe to show in a log.

string Mask(string? input = null)
Parameter Type Default What it does
input string? null The raw address.

Returns

When You get
The value is a valid address The masked address — network readable, host starred: "203.0.113.***"; IPv6: "2001:db8:0:***"; an IPv4 address wrapped as IPv6 (::ffff:203.0.113.9, how a dual-stack socket reports it) masks as the IPv4 address it is: "203.0.113.***"
Missing or invalid The all-stars placeholder: "***.***.***.***" — never empty, and it never throws

The display never goes blank — a log line always shows something mask-shaped.

A display mask: the network part stays readable, the host part is starred. It never answers empty and never throws — junk masks as the all-stars placeholder, so a log line always shows something mask-shaped.

CkIP.Mask("203.0.113.9");          // -> "203.0.113.***"
CkIP.Mask("2001:db8::1");          // -> "2001:db8:0:***"
CkIP.Mask("::ffff:203.0.113.9");   // -> "203.0.113.***"  (an IPv4 address, however a socket wrapped it)
CkIP.Mask("junk");                 // -> "***.***.***.***"

Version

IPv4 or IPv6?

int Version(
    string? input = null,
    int fallback = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
fallback int 0 What comes back when the value cannot be used — 0 by default, which no IP version is.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering the fallback, when the value cannot be used.

Returns

When Default With throwIfInvalid: true
The value is a valid address The family as a number: 4 for IPv4, 6 for IPv6 The family as a number: 4 for IPv4, 6 for IPv6
Rejected: missing, oversized, or not a usable address The fallback (default 0) Throws CkIPException — carries Input, Expected and Reason

The default fallback of 0 is safe to test against — no IP version is 0.

The address family as a number: 4 or 6, or the fallback (default 0, which no IP version is) when the value cannot be read.

CkIP.Version("203.0.113.9");    // -> 4
CkIP.Version("::1");            // -> 6
CkIP.Version("junk");           // -> 0

CkIP.Version(
    input: "junk",
    fallback: -1
);   // -> -1

IsAddress

A strict address, of this family?

bool IsAddress(
    string? input = null,
    int version = 0,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
version int 0 4 or 6 to demand that family; 0 accepts either.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering false, when the value is not a usable address of the requested family, or the version is not 0, 4 or 6.

Returns

When Default With throwIfInvalid: true
A strict address, and of the requested family when one is named true true
A valid address of the other family false Throws — the reason says which family it is
Rejected: not a usable address — shorthand, a leading zero, a zone index, junk false Throws CkIPException — carries Input, Expected and Reason

The yes-or-no form of Address, with Version's answer folded in: "127.1", a leading-zero octet and a zone index still fail. An IPv4-mapped IPv6 address (::ffff:10.0.0.1) is IPv6 here, as Version says; see IsMapped. When the value is going to be stored, call Address instead and check for "" — it hands you the canonical spelling.

CkIP.IsAddress("203.0.113.9");               // -> true
CkIP.IsAddress("127.1");                     // -> false  (shorthand)
CkIP.IsAddress("2001:db8::1", version: 4);   // -> false  (the wrong family)

IsIPv4

A strict IPv4 address?

bool IsIPv4(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering false, when the value is not a usable IPv4 address.

Returns

When Default With throwIfInvalid: true
Four plain-digit octets, as Address reads them true true
A valid IPv6 address, the mapped form included false Throws — "an IPv6 address, not IPv4"
Rejected: not a usable address — shorthand, a leading zero, a zone index, junk false Throws CkIPException — carries Input, Expected and Reason

The same as IsAddress(input, version: 4), spelled the way validation code reads.

CkIP.IsIPv4("203.0.113.9");       // -> true
CkIP.IsIPv4("::ffff:10.0.0.1");   // -> false  (IPv6, even though it wraps an IPv4)

IsIPv6

A valid IPv6 address?

bool IsIPv6(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering false, when the value is not a usable IPv6 address.

Returns

When Default With throwIfInvalid: true
A valid IPv6 address, brackets and the mapped form included true true
A valid IPv4 address false Throws — "an IPv4 address, not IPv6"
Rejected: not a usable address — shorthand, a leading zero, a zone index, junk false Throws CkIPException — carries Input, Expected and Reason

The same as IsAddress(input, version: 6). A zone index (%eth0) still fails.

CkIP.IsIPv6("[2001:DB8::1]");   // -> true
CkIP.IsIPv6("fe80::1%eth0");    // -> false  (a zone index)

IsInternal

From inside the network?

bool IsInternal(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering false, when the value is not a usable address.

Returns

When Default With throwIfInvalid: true
A valid address IsPublic refuses: loopback, private, link-local, carrier-grade NAT, unique-local, multicast, unspecified, the documentation and benchmarking ranges true true
A valid public address false false
Rejected: not a usable address — shorthand, a leading zero, a zone index, junk false Throws CkIPException — carries Input, Expected and Reason

The positive form of IsPublic's classification, with the IPv6 disguises of an internal IPv4 address unwrapped first. IsPublic's false also means "unreadable"; both methods answer false for junk, so neither is the other's negation, and the flag tells the two apart.

CkIP.IsInternal("10.0.0.1");          // -> true
CkIP.IsInternal("::ffff:10.0.0.1");   // -> true   (unwrapped)
CkIP.IsInternal("8.8.8.8");           // -> false
CkIP.IsInternal("junk");              // -> false  (and so is IsPublic)

IsLoopback

The machine itself?

bool IsLoopback(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering false, when the value is not a usable address.

Returns

When Default With throwIfInvalid: true
127.0.0.0/8, ::1, or the mapped ::ffff:127.0.0.1 true true
Any other valid address false false
Rejected: not a usable address — shorthand, a leading zero, a zone index, junk false Throws CkIPException — carries Input, Expected and Reason

For "admin endpoints answer only locally", and for spotting a misconfigured proxy that reports every caller as itself.

CkIP.IsLoopback("127.1.2.3");   // -> true   (the whole /8)
CkIP.IsLoopback("::1");         // -> true
CkIP.IsLoopback("10.0.0.1");    // -> false

IsMapped

An IPv4 address in an IPv6 coat?

bool IsMapped(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering false, when the value is not a usable address.

Returns

When Default With throwIfInvalid: true
An IPv4-mapped IPv6 address, ::ffff:a.b.c.d true true
Any other valid address false false
Rejected: not a usable address — shorthand, a leading zero, a zone index, junk false Throws CkIPException — carries Input, Expected and Reason

For intake rules that refuse the disguised form outright rather than unwrapping it, as IsPublic and InRanges do. Only the RFC 4291 mapped form counts: the NAT64 and 6to4 embeddings are other things, which IsPublic also sees through and which InRanges can refuse by their prefixes, 64:ff9b::/96 and 2002::/16.

CkIP.IsMapped("::ffff:10.0.0.1");   // -> true
CkIP.IsMapped("64:ff9b::a00:1");    // -> false  (NAT64, another embedding)

IsRange

Something InRanges will honour?

bool IsRange(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw range or address.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering false, when the value is not a usable range.

Returns

When Default With throwIfInvalid: true
CIDR notation with a valid prefix length, or a bare strict address true true
Rejected: a prefix over the family's bits, junk after the slash, a non-strict address false Throws CkIPException — carries Input, Expected and Reason

For the screen where allow-list entries are typed in. InRanges ignores an unreadable range silently, which is safe at check time but hides the typo at entry time; this catches it there. Host bits past the prefix are ignored, as InRanges ignores them, so 203.0.113.5/24 is a range.

CkIP.IsRange("203.0.113.0/24");   // -> true
CkIP.IsRange("10.0.0.1");         // -> true   (a bare address matches exactly)
CkIP.IsRange("10.0.0.0/33");      // -> false  (over 32)

SameAddress

The same host, whatever the spelling?

bool SameAddress(
    string? a = null,
    string? b = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
a string? null The first value.
b string? null The second value.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says which value is not a usable address and why, instead of answering false.

Returns

When Default With throwIfInvalid: true
Both are valid addresses and the same one true true
Both are valid, and differ false false
Rejected: either is not a usable address false Throws CkIPException — carries Input, Expected and Reason

"2001:DB8::1", "[2001:db8:0:0:0:0:0:1]" and the compressed form are one address, and an IPv4-mapped IPv6 address equals the IPv4 it wraps, as InRanges already treats it. For matching a caller against a stored address. Two values that are not addresses are never the same.

CkIP.SameAddress("2001:DB8::1", "2001:db8:0:0:0:0:0:1");   // -> true
CkIP.SameAddress("::ffff:10.0.0.1", "10.0.0.1");           // -> true
CkIP.SameAddress("10.0.0.1", "127.1");                     // -> false  (shorthand is not an address)

Anonymize

The address you may keep.

string Anonymize(
    string? input = null,
    bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
Parameter Type Default What it does
input string? null The raw address.
throwIfInvalid bool CkSettings.DefaultThrowIfInvalid (false) Pass true to throw a CkIPException that says why, instead of answering "", when the value is not a usable address.

Returns

When Default With throwIfInvalid: true
A strict address The address with its host part zeroed: the last octet of an IPv4, the last 80 bits of an IPv6 The address
Rejected: not a usable address "" Throws CkIPException — carries Input, Expected and Reason

The truncation analytics tools apply under the GDPR, so a log line no longer identifies one machine. Mask is for display; this is for storage. An IPv4-mapped IPv6 address is treated as the IPv4 it wraps.

CkIP.Anonymize("203.0.113.9");            // -> "203.0.113.0"
CkIP.Anonymize("2001:db8:abcd:1234::1");   // -> "2001:db8:abcd::"

Explain

Why not — in words, without an exception.

string Explain(
    string? input = null
)
Parameter Type Default What it does
input string? null The raw address.

Returns

When You get
The value passes the reader ""
It does not The reason, the same words the exception would carry — an answer, not a failure; it never throws

The form-friendly twin of throwIfInvalid: the message to show beside the field, without a try and catch. Every class with a reader has one, on the reader's own rules, so the reason is exactly what the reader would say.

CkIP.Explain("127.1");     // -> "not four dot-separated numbers"
CkIP.Explain("01.2.3.4");  // -> "a leading zero reads as octal"

Good to know

  • Stricter than .NET on purpose. The framework's own parser happily reads "127.1" and even the bare number "2130706433" as loopback. Those shapes — plus leading-zero octets like "01.2.3.4" and zone indexes like "%eth0" — are classic tricks for slipping an internal address past a security filter, so Address rejects them all.
  • IsPublic sees through disguises. An internal IPv4 address can be wrapped inside an IPv6 one (NAT64, 6to4, IPv4-mapped); IsPublic unwraps those and judges the address inside. The documentation ranges (like 203.0.113.x) and the benchmarking ranges also count as internal — real traffic never comes from them. Teredo (2001:0::/32) is unwrapped too — the client's address rides in the last four bytes with every bit flipped, so 2001::ffaf:80ff:fffe is 127.0.0.1 — and the IETF protocol-assignments block 192.0.0.0/24 counts as internal beside the documentation ranges.
  • No substitute address, ever. Address has no fallback parameter, unlike most CommonKit methods. An invented address would say who called when nobody knows.
  • An unreadable range never matches. In InRanges, a typo in a CIDR range makes that range match nothing — it can never accidentally mean "allow all".
  • The validators answer false for junk too. IsInternal and IsPublic partition the valid addresses, and IsIPv4 and IsIPv6 do the same; an unusable value fails all of them, so none is another's negation. Pass throwIfInvalid: true when the difference matters.
  • IsRange is for entry time, InRanges for check time. A typo in an allow-list entry is silently a miss in InRanges, which is the safe behaviour when checking; IsRange rejects it when the entry is typed in.
  • Non-empty in, empty out means rejected. The standard CommonKit signal for Address — check for "" where it matters.

If you want errors instead

Pass throwIfInvalid: true to Address, IsPublic, Version or any of the validators, and a value that cannot be used raises a CkIPException (from SafeCoder.CommonKit.Exceptions) instead of answering the rejection value. The exception carries the Input and a short Reason, flattened into a one-line message.

CkIP.Address(
    input: "127.1",
    throwIfInvalid: true
);   // throws CkIPException — the message says why

Two methods take no throwIfInvalid. Mask never fails by design. InRanges cannot have one — C# requires its params range list to be the last parameter — so when you need to tell a bad address from a miss, validate first with Address and its throwIfInvalid.