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, soAddressrejects them all. IsPublicsees through disguises. An internal IPv4 address can be wrapped inside an IPv6 one (NAT64, 6to4, IPv4-mapped);IsPublicunwraps those and judges the address inside. The documentation ranges (like203.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, so2001::ffaf:80ff:fffeis 127.0.0.1 — and the IETF protocol-assignments block192.0.0.0/24counts as internal beside the documentation ranges.- No substitute address, ever.
Addresshas nofallbackparameter, 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
falsefor junk too.IsInternalandIsPublicpartition the valid addresses, andIsIPv4andIsIPv6do the same; an unusable value fails all of them, so none is another's negation. PassthrowIfInvalid: truewhen the difference matters. IsRangeis for entry time,InRangesfor check time. A typo in an allow-list entry is silently a miss inInRanges, which is the safe behaviour when checking;IsRangerejects 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.