Dev.to Security ๐Ÿ” Cybersecurity ๐Ÿ‘ 0 ๐Ÿ“– 6 min read

Your IPv4 and IPv6 Exits Can Disagree: Looking Up IPv6 on IP99

A dual-stack client has two public identities, not one. If your risk check only looks up the IPv4, you have answered half the question: the IPv6 on the same line can show a different city, and a site may have used a diff

A dual-stack client has two public identities, not one. If your risk check only looks up the IPv4, you have answered half the question: the IPv6 on the same line can show a different city, and a site may have used a different protocol. On IP99 by ThreatHunter, free, no-key GET /v1/ip/{ip} accepts both IPv4 and IPv6. GET /v1/ip (no address in the path) returns the caller's current public exit โ€” whichever protocol your client used to reach the API.

This walkthrough stays on that free profile. JSON samples were verified on 13 November 2026 UTC. Notes: IP99, API, What is my IP, IP location, IP risk score.

What the free lookup returns

risk.score is 0โ€“100 for freshness and strength of verifiable risk evidence, not attacker probability. risk.level follows the score (0 โ†’ none, 1โ€“29 โ†’ low, 30โ€“69 โ†’ medium, 70โ€“100 โ†’ high). Signals appear only when they hit. Branch on evidence_state (active / stale / none) before the score: 0 + none is a coverage gap, not a clean bill of health. hosting and mobile are network attributes, not risk verdicts โ€” official guidance is not to block on those slugs alone.

The path parameter is a bare address: an IPv4 or IPv6 literal, no port, no zone id.

Same resolver family, two protocols

Google Public DNS agrees with itself in this snapshot. IPv4 and IPv6 land in the same city, the same ASN, and the same empty evidence set:

curl -g https://ip99.com/v1/ip/8.8.8.8
curl -g https://ip99.com/v1/ip/2001:4860:4860::8888

Verified 13 November 2026 UTC (2001:4860:4860::8888, computed_at 2026-11-13T13:06:06Z):

{
  "ip": "2001:4860:4860::8888",
  "computed_at": "2026-11-13T13:06:06Z",
  "evidence_state": "none",
  "geo": { "continent": "NA", "country": "US", "region": "California", "city": "Mountain View", "tz": "America/Los_Angeles" },
  "network": { "asn": 15169, "usage_type": "IDC" },
  "risk": { "score": 0, "level": "none", "signals": ["hosting"] }
}

8.8.8.8 in the same minute is the same shape: Mountain View, ASN 15169, usage_type: IDC, hosting, score 0, evidence_state: none. Agreement is a possible outcome, not a rule. hosting here is infrastructure, not a reason to refuse the resolver.

Cloudflare's public resolver does not agree on geography in the same snapshot:

curl -g https://ip99.com/v1/ip/1.1.1.1
curl -g https://ip99.com/v1/ip/2606:4700:4700::1111

Both addresses are ASN 13335, usage_type: IDC, hosting, score 0, evidence_state: none. The IPv4 (computed_at 2026-11-13T13:06:12Z) is placed in Brisbane, Queensland, AU (Australia/Brisbane). The IPv6 (2026-11-13T13:06:10Z) is placed in San Francisco, California, US (America/Los_Angeles). That is two anycast exits, not a person moving house.

IP99's What is my IP page says this in product language: a dual-stack connection has a public IPv4 address and an IPv6 prefix, and a site may see whichever protocol its DNS prefers. If two services disagree about "your" address, check which protocol each one used. Geolocation places the block, not the building; city-level is the realistic accuracy for consumer addresses.

Treat the IPv4 and the IPv6 as two records. Do not copy geo, network, or risk from one onto the other.

How the path handles IPv6

A compressed literal and a fully expanded one resolve to the same profile. In this snapshot, 2001:4860:4860:0:0:0:0:8888 is returned as ip: "2001:4860:4860::8888". The unencoded literal is enough for curl -g (URL-encoding colons also works).

An IPv4-mapped address is answered as IPv4. GET /v1/ip/::ffff:8.8.8.8 returned the 8.8.8.8 profile (ip is 8.8.8.8, not the mapped form). Store the request string and the response ip separately.

Private, unique-local, loopback, and link-local addresses return HTTP 400 reserved_ip ("private or reserved address has no public risk profile"). Verified here: 192.168.1.1, ::1, fc00::1, and fe80::1. Official docs: those ranges are not on the public internet, so they have no public profile.

The documentation prefix is different. 2001:db8::1 returned HTTP 200, not 400:

{
  "ip": "2001:db8::1",
  "computed_at": "2026-11-13T13:06:22Z",
  "evidence_state": "none",
  "geo": { "continent": "", "country": "", "region": "reserve", "city": "reserve", "tz": "" },
  "network": { "usage_type": "BOGON" },
  "risk": { "score": 0, "level": "none" }
}

No ASN, empty country, usage_type: BOGON, and no risk.signals key. That is not a customer session; a missing asn is unknown, not zero.

Look up the exit you actually have

curl https://ip99.com/v1/ip

The ip field is the public address the API observed on that connection โ€” including a VPN or proxy exit if one is active, in which case the country is the exit's, not your line's. Official copy also notes that many home connections share one public IPv4, or one IPv6 prefix, behind the router, and that carrier-grade NAT means the address is not a person. To see both protocols, request once over IPv4 and once over IPv6. Re-query at decision time and store computed_at; scores decay, and the pair can change independently.

Don't ban a single IPv6 and call it done

A household IPv6 allocation is typically a prefix (commonly a /56 or /64), and devices may rotate the interface identifier (privacy extensions, RFC 8981). A hard ban of one IPv6 address is a narrow lever: it can miss the next address from the same prefix, or punish every device behind that prefix if you widen the ban blindly.

Free /v1/ip answers one address. It does not return share_tier, evidence[], or a prefix-aggregation field. Those details, including Pulse-only ipv6_range on evidence[].risk_type (not a risk.signals slug), live on GET /pulse/v1/ip/{ip} (professional). Historical replay with ?at= is a Pulse feature. Official docs list history_no_ipv6 when an access point has not enabled IPv6 history (the history index is IPv4 until that is turned on). Free /v1/ip rejects ?at= with 400 at_unsupported. Without a professional key, Pulse returns 403 professional_required.

Generic triage (not a contract): score 0 โ†’ do not stamp "clean"; 1โ€“29 โ†’ observe; 30โ€“69 โ†’ captcha / throttle / second factor; 70โ€“100 โ†’ block only when business cost allows, and not at building or prefix scale. Official order: evidence_state โ†’ score/level โ†’ signal type, then usage_type on the free profile (share_tier on Pulse). If hijacked_proxy is present, challenge the session; do not ban the account โ€” the address holder is the victim.

A practical branch for dual-stack traffic:

  1. Look up the IPv4 you observed and the IPv6 you observed. If you only have a self-check, call GET /v1/ip once per protocol.
  2. Keep two records. Never copy geo or score across families.
  3. Read evidence_state, then risk.signals, then network.usage_type and network.asn.
  4. On MOB or GTW, assume a wide NAT and prefer a step-up over a hard IP ban. Do not hard-ban a single consumer IPv6, and do not allowlist "it is IPv6."
  5. Never mark score 0 + evidence_state: none as "trusted clean."

Anonymous quota is 500 lookups/day per client IP (60/min); a free logged-in key raises the daily cap to 5,000/day. Check remaining with GET https://ip99.com/v1/me or the X-Daily-* headers. OpenAPI: https://ip99.com/openapi.json. Pricing: https://ip99.com/en/pricing.

Do not treat IP99 scores as identical to other ThreatHunter datasets โ€” this article only claims what free /v1/ip and the cited product pages document.

Try it

curl https://ip99.com/v1/ip
curl -g https://ip99.com/v1/ip/8.8.8.8
curl -g https://ip99.com/v1/ip/2001:4860:4860::8888
curl -g https://ip99.com/v1/ip/1.1.1.1
curl -g https://ip99.com/v1/ip/2606:4700:4700::1111
curl -g https://ip99.com/v1/ip/2001:db8::1
curl https://ip99.com/v1/me

IP99 is a ThreatHunter product. Use the free lookup on both families before you decide that an egress, a resolver, or a customer session is "the" address. Start at https://ip99.com; read What is my IP, IP location, and https://ip99.com/api.

๐Ÿ“ฐ Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.