1 unstable release
| 0.1.0 | Jan 31, 2026 |
|---|
#125 in Internationalization (i18n)
27KB
214 lines
IP Flag (ipflag)
IP Flag is a tiny, resolver-pluggable Rust crate that turns IP addresses into a human-friendly country display (flag + country code). It is designed for communities, forums, comment sections, moderation logs, analytics dashboards, and anywhere an IP string is hard to read at a glance.
The core idea is simple:
- IP Flag does display.
- IP Flag does not ship geolocation data.
- IP Flag does not call external APIs.
- IP Flag does not require updates.
If you want real “IP → country” resolution, you provide it via a resolver implementation (or via a separate extension crate). This keeps the core maintenance-free and makes the ecosystem extensible.
Why IP Flag exists
Raw IP addresses are noisy and not human-friendly.
In real community moderation, the question is often not “what is the exact IP?”, but “what pattern do we see across many comments?” A country flag or country code makes it easy to scan a thread and quickly notice clusters and anomalies.
IP Flag does not make claims about intent or manipulation. It only helps humans see patterns more clearly.
What IP Flag does (and does not)
IP Flag does
- Parse IPv4/IPv6
- Classify IP scope:
Private(LAN / internal)Special(loopback, multicast, documentation ranges, etc.)Public(eligible for resolution)
- if
Public, call your resolver and display:- Flag emoji (🇰🇷, 🇺🇸, 🇷🇺, ...)
- Country code (KR, US, RU, ...)
- Provide formatting helpers for common UI output styles
IP Flag does not
- Bundle MaxMind or any GeoIP database
- Contact third-party APIs
- Track, store, or log anything
- Decide what is “suspicious”
- Maintain an ever-changing list of countries or names
Installation
Add the crate to your Cargo.toml:
ipflag = "0.1.0"
Then import what you need:
tag_ip/tag_addrto generate tagsIpResolverto plug in your own resolution logicNoopResolverfor “display-only” usage without resolution
Core usage
Use IP Flag by itself (no resolver data)
This mode is still useful because it cleanly categorizes internal/special addresses and gives you a consistent display output.
Example:
use ipflag::{tag_ip, NoopResolver};
fn main() {
let t1 = tag_ip(&NoopResolver, "192.168.0.10").unwrap();
println!("{}", t1); // 🏠 PRIVATE
let t2 = tag_ip(&NoopResolver, "127.0.0.1").unwrap();
println!("{}", t2); // ⚙️ SPECIAL
let t3 = tag_ip(&NoopResolver, "8.8.8.8").unwrap();
println!("{}", t3); // 🌐 UNKNOWN
}
What you get here is:
- A reliable “this is internal” / “this is special” / “this is public but unknown” signal
- A stable output format suitable for UI and logs
- Zero dependencies and zero update requirements
2) Provide a resolver (real country flags)
To show real countries, you implement one trait: IpResolver.
This resolver can be anything:
- a MaxMind reader
- an IP-to-country map from your own data source
- a corporate CDN header mapping
- a custom enterprise threat intel source
- a quick prototype rule-based resolver
Example resolver (hard-coded for demonstration):
use ipflag::{CountryCode, IpResolver, tag_ip};
use std::net::IpAddr;
struct DemoResolver;
impl IpResolver for DemoResolver {
type Error = core::convert::Infallible;
fn resolve(&self, _ip: IpAddr) -> Result<Option<CountryCode>, Self::Error> {
Ok(CountryCode::new("KR"))
}
}
fn main() {
let tag = tag_ip(&DemoResolver, "8.8.8.8").unwrap();
println!("{}", tag); // 🇰🇷 KR
}
How resolution works
IP Flag calls your resolver only for Public IPs.
- Private IPs never call the resolver → always
🏠 PRIVATE - Special IPs never call the resolver → always
⚙️ SPECIAL - Public IPs call the resolver:
- resolver returns
Some(CountryCode)→IpTag::Country(code)→ displays🇰🇷 KR - resolver returns None →
IpTag::Unknown→ displays🌐 UNKNOWN
- resolver returns
This is important for correctness, performance, and privacy. It prevents “fake resolution” for private and special ranges and keeps your resolver focused on real public IPs only.
Output formats (UI-friendly)
IpTag implements Display, so you can directly print it:
println!("{}", tag); // default output
If you want different display styles, use TagFormat:
use ipflag::{TagFormat};
println!("{}", tag.format(TagFormat::FlagAndCode)); // "🇰🇷 KR"
println!("{}", tag.format(TagFormat::FlagOnly)); // "🇰🇷"
println!("{}", tag.format(TagFormat::CodeOnly)); // "KR"
For non-country tags, the default output is:
🏠 PRIVATE⚙️ SPECIAL🌐 UNKNOWN
This is intentional: it keeps log output consistent and predictable.
Minimal “community comment display” example
A typical usage is to append the tag next to a comment’s “anonymous” label:
use ipflag::{tag_ip, NoopResolver, TagFormat};
fn display_anonymous(ip: &str) -> String {
let tag = tag_ip(&NoopResolver, ip).unwrap();
format!("Anonymous {}", tag.format(TagFormat::FlagOnly))
}
fn main() {
println!("{}", display_anonymous("192.168.1.10")); // Anonymous 🏠
println!("{}", display_anonymous("8.8.8.8")); // Anonymous 🌐
}
In production, replace NoopResolver with your real resolver implementation.
Privacy and security notes
- IP Flag does not perform network requests.
- IP Flag does not store IPs.
- IP Flag does not log anything.
- Your resolver may, depending on your implementation.
In community settings, you should consider privacy laws and policies. A common approach is masking IPs in UI and logs while still using flags/codes for pattern visibility.
Non-goals
- “Detect manipulation” or “label someone as suspicious”
- Provide absolute truth about user identity or nationality
- Replace moderation judgment
- Provide a global geolocation database
IP Flag is a display utility. The interpretation is yours.
Contributing
Contributions that keep the core minimal and stable are welcome:
- Bug fixes
- Better IPv4/IPv6 scope classification edge cases
- Output formatting improvements
- More examples and documentation
Anything that adds bundled data sources or external network calls should live in extension crates, not in the core.