Why static IP blocklists lose to ephemeral botnets and residential DHCP churn
Traditional IP threat intelligence relies on static, flat blocklists: CSV or text files of "bad IPv4 addresses" distributed once per day. In modern production security engineering, static blocklists fail in two opposite directions simultaneously:
- Too slow for cloud-native and proxy-rotated attacks: Automated credential-stuffing frameworks, CVE exploitation bots, and LLM scrapers spin up ephemeral cloud instances or cycle through residential proxy pools in minutes. By the time a flat daily blocklist is compiled and pushed to your firewall, the attacker has already rotated to a fresh
/24or a new set of residential proxy exits. - Too sticky for dynamic residential and cellular ISPs: When a compromised home IoT device or malware-infected laptop sits behind a consumer ISP router, its outbound attack traffic originates from a dynamically leased residential IPv4 address. When that home router reboots or the ISP rotates PPPoE/DHCP leases 24 hours later, that IPv4 address is assigned to a completely innocent household. Static blocklists that lack continuous time decay keep blocking that residential IP for weeks or months, generating endless customer lockouts.
To solve both problems, ipscope cleanly decouples structural network anonymity (security) from time-decayed hostile traffic velocity (security_extensions).
This engineering post explains how our global darknet and honeypot sensor mesh collects hostile packet telemetry, how we compute the calibrated 0 to 100 abuse_velocity_score using multi-ASN target entropy and exponential half-life decay, and how to wire security_extensions into your API gateway, WAF, and login endpoints.
Anatomy of the security_extensions block
When your API plan or custom database SKU includes the security_extensions intelligence family, every IP lookup returns three real-time activity metrics alongside the core security block:
{
"ip": "198.51.100.214",
"ip_version": 4,
"matched_network": "198.51.100.0/24",
"connection": {
"connection_type": "hosting",
"is_residential": false,
"is_mobile": false,
"is_business": false,
"is_hosting": true
},
"security": {
"is_vpn": false,
"is_hosting": true,
"is_tor_exit": false,
"is_proxy": false,
"anonymizer_type": "none",
"confidence": 0.99,
"reason_codes": [
"datacenter_asn_range"
]
},
"security_extensions": {
"abuse_velocity_score": 92,
"is_known_scanner": true,
"observation_recency_hours": 1
}
}Field definitions in security_extensions
| Field | Data Type | Range / Values | Operational Definition |
|---|---|---|---|
abuse_velocity_score | uint8 | 0 to 100 | Calibrated, time-decayed severity and breadth score of hostile scanning, brute-forcing, and exploit attempts observed from this IP. 0 indicates zero observed hostile telemetry. |
is_known_scanner | boolean | true or false | true when the IP exhibits systematic horizontal port sweeps (SYN / UDP scanning) or automated vulnerability enumeration across multiple independent /16 networks. |
observation_recency_hours | uint32 (nullable) | 0 to 720 (null if 0 score) | Elapsed hours since our sensor fleet last observed hostile or scanning packets from this IP address. |
How our sensor mesh captures hostile telemetry
Our telemetry ingestion pipeline aggregates packet and application-layer events from three distinct sensor tiers deployed across 45+ autonomous systems and all five RIR regions:
1. Passive darknet (network telescope) radiation sensors
We monitor unallocated and unrouted-to-application /24 and /23 IPv4 blocks and /48 IPv6 honeypot subnets that host zero legitimate public services (no DNS A/AAAA records, no web links, no TLS certificates in CT logs). Because no legitimate human browser or misconfigured client has any reason to send TCP SYN packets to unused darknet address space, every unsolicited packet arriving at a darknet sensor is, by definition, either:
- Horizontal internet-wide port scanning (
ZMap,Masscan,Nmap), - Automated worm / botnet propagation probing (
TCP/22,TCP/23,TCP/445,TCP/3389,TCP/6379,TCP/27017), or - Spoofed UDP reflection backscatter (which we filter out by requiring a completed 3-way TCP handshake or stateful protocol payload before attributing application-layer abuse).
2. High-interaction protocol honeypots (SSH, HTTP/TLS, SMTP, Redis, Database)
Completing a TCP 3-way handshake proves that the source IP is not spoofed (the sender had to receive our SYN-ACK sequence number and return a valid ACK). Behind our handshake listeners, lightweight protocol state machines classify the exact intent of the connection:
- Credential Brute-Forcing: Repeated SSH password/key authentication attempts, SMTP
AUTH LOGINdictionary attacks, PostgreSQL/MySQL/Redis authentication sweeps, and HTTPPOST /wp-login.php,/api/v1/login, or OAuth token endpoint stuffing. - Vulnerability & Path Exploitation: HTTP requests probing for exposed
.envfiles,.git/config, cloud metadata SSRF headers, Log4Shell/Spring4Shell payloads, PHPUnit RCE paths, and unauthenticated Kubernetes/Docker API endpoints. - TLS / JA4 Fingerprint Clustering: Captures the exact TLS ClientHello structure (
JA4) and HTTP/2 settings frame to correlate distributed botnet nodes executing the same automation binary across thousands of source IPs.
The mathematics of abuse_velocity_score: Breadth, Intent, and Exponential Decay
A common mistake in naive threat-scoring systems is scoring an IP based on raw packet count. If you score by raw packet count, a single developer who accidentally runs an aggressive nmap -p 1-65535 scan against one test server looks worse than a stealthy botnet node sending 1 login attempt per minute against 500 different banks.
To prevent single-target noise from skewing the score, the ipscope compiler calculates abuse_velocity_score across three normalized dimensions:
Score(ip, t) = round( 100 * (0.45 * Breadth + 0.35 * Intent + 0.20 * Cadence) * exp(-lambda * delta_t_hours) )Dimension 1: Target Breadth & ASN Entropy (Breadth, 45% weight)
Instead of counting total packets sent to a single sensor, Breadth measures the Shannon entropy and count of distinct destination /16 networks and distinct destination ASNs touched by the source IP:
- A source IP that sends 10,000 packets to a single destination IP receives a low breadth score because the activity is localized to one host.
- A source IP that hits 40 distinct
/16sensors across 25 different autonomous systems in North America, Europe, and Asia within 15 minutes approachesBreadth = 1.0and triggersis_known_scanner = true.
Dimension 2: Protocol Intent Severity (Intent, 35% weight)
Not all internet scanners have the same intent. Academic and commercial internet topology mapping projects (Censys, Shodan, Shadowserver) perform regular port sweeps and publish clear PTR reverse DNS records, WHOIS abuse contacts, and HTTP opt-out headers. Conversely, exploit botnets (Mirai, Mozi, credential-stuffing clusters) send active RCE payloads and dictionary attacks after completing the TCP handshake.
| Observed Behavior Tier | Intent Multiplier (Intent) | is_known_scanner | Typical Score Range (When Active) |
|---|---|---|---|
| Zero observed darknet/honeypot hits | 0.00 | false | 0 |
| Low-rate single-port probing / background noise | 0.25 | false | 10 to 29 |
| Identified benign research / topology scanner | 0.45 | true | 35 to 55 |
| Unattributed horizontal port / vulnerability scanner | 0.75 | true | 60 to 84 |
| Active credential brute-forcing or RCE exploit payloads | 1.00 | true | 85 to 100 |
Dimension 3: Cadence Burstiness (Cadence, 20% weight) and Exponential Time Decay (exp(-lambda * delta_t))
Every observation is timestamped to the second. Let delta_t_hours equal observation_recency_hours (the hours elapsed since the most recent confirmed TCP/application-layer hit from ip).
We apply an exponential decay factor exp(-lambda * delta_t_hours) calibrated to the underlying connection.connection_type:
- Residential and Mobile CGNAT (
is_residentialoris_mobile): Uses a fast decay half-life (half_life = 12 hours). If a compromised home device stops transmitting because the ISP rotated its DHCP lease, an88score drops to44after 12 hours,22after 24 hours, and< 6after 48 hours. - Dedicated Cloud / Datacenter Hosting (
is_hosting): Uses a standard decay half-life (half_life = 24 hours), because static cloud IPs rarely rotate to consumer households.
Practical score bands for WAF and API Gateway policies
When integrating abuse_velocity_score into your edge middleware, we recommend dividing the 0..100 scale into four operational action bands:
abuse_velocity_score Band | Classification | Recommended Edge / Gateway Action |
|---|---|---|
0 to 19 | Clean / Baseline | Normal traffic handling and standard rate limits. |
20 to 49 | Elevated Background Noise | Typical of shared NAT gateways or historical low-volume probing; log for telemetry, apply standard authenticated rate limits. |
50 to 74 | Active Reconnaissance / Suspected Automation | Require Proof-of-Work / Turnstile / CAPTCHA on unauthenticated endpoints; tighten per-IP rate limits by 4x. |
75 to 100 | High-Velocity Attacker / Exploit Botnet | Block unauthenticated mutations (/login, /signup, /password-reset, /checkout); drop scanner probes at the edge WAF before hitting origin databases. |
Production implementation: Adaptive rate limiting and scanner blocking in Node.js
Here is how to combine connection, security, and security_extensions in an Express, Fastify, or Next.js edge middleware to block active scanners and throttle brute-force bots without ever locking out shared 5G cellular CGNAT users:
import { IpScopeClient } from '@ipscope/sdk-node';
const ipscope = new IpScopeClient({
apiKey: process.env.IPSCOPE_KEY!,
maxCacheEntries: 16_384,
cacheTtlMs: 60_000,
});
export interface EdgeInspectionResult {
action: 'allow' | 'throttle' | 'challenge' | 'block';
rateLimitPerMinute: number;
reason: string;
}
export async function inspectIncomingRequest(
clientIp: string,
requestPath: string,
isAuthenticatedSession: boolean,
): Promise<EdgeInspectionResult> {
const res = await ipscope.lookupIp(clientIp);
const { connection, security, security_extensions } = res;
const velocity = security_extensions?.abuse_velocity_score ?? 0;
const isScanner = security_extensions?.is_known_scanner ?? false;
const recencyHours = security_extensions?.observation_recency_hours ?? 999;
const isSensitiveAuthRoute =
requestPath.startsWith('/api/auth/') ||
requestPath.startsWith('/api/checkout');
// 1. Drop active high-velocity scanners and exploit bots on unauthenticated requests
if (!isAuthenticatedSession && (velocity >= 80 || (isScanner && recencyHours <= 6))) {
return {
action: 'block',
rateLimitPerMinute: 0,
reason: `high_abuse_velocity:${velocity}:recency_${recencyHours}h`,
};
}
// 2. Challenge medium-high velocity IPs or open proxies hitting auth/checkout routes
if (isSensitiveAuthRoute && (velocity >= 50 || security.is_proxy || security.is_tor_exit)) {
return {
action: 'challenge',
rateLimitPerMinute: 10,
reason: `sensitive_route_challenge:velocity_${velocity}:type_${security.anonymizer_type}`,
};
}
// 3. Shared 4G/5G Mobile CGNAT gateways: allocate high aggregate concurrency bucket
// so 4,000 mobile users sharing one carrier IP do not starve each other
if (connection?.is_mobile && !security.is_vpn && !security.is_proxy) {
return {
action: 'allow',
rateLimitPerMinute: 1200,
reason: 'mobile_cgnat_shared_pool',
};
}
// 4. Commercial VPN or raw datacenter egress: tighter bucket on unauthenticated traffic
if (security.is_vpn || security.is_hosting) {
return {
action: velocity >= 35 ? 'throttle' : 'allow',
rateLimitPerMinute: isAuthenticatedSession ? 120 : 30,
reason: `hosting_or_vpn_egress:${security.anonymizer_type}`,
};
}
// 5. Clean residential or enterprise business broadband
return {
action: 'allow',
rateLimitPerMinute: 300,
reason: 'clean_access_network',
};
}Enforcing fail2ban-style SSH and admin protection in Go
For infrastructure teams protecting SSH bastions, Kubernetes API servers, or VPN concentrators, you can query security_extensions directly in Go (github.com/badele-llc/ipscope-go) or via a local .mmdb database to reject known internet scanners before spending CPU cycles on cryptographic key exchange:
package main
import (
"context"
"fmt"
"os"
"time"
ipscope "github.com/badele-llc/ipscope-go"
)
func ShouldAllowAdminIngress(ctx context.Context, client *ipscope.Client, remoteIP string) (bool, string) {
res, err := client.LookupIP(ctx, remoteIP)
if err != nil {
// Fail-open to local auth if network lookup times out
return true, "lookup_fallback"
}
if res.SecurityExtensions != nil {
if res.SecurityExtensions.IsKnownScanner && res.SecurityExtensions.AbuseVelocityScore >= 60 {
return false, fmt.Sprintf(
"blocked_scanner(score=%d,recency=%dh)",
res.SecurityExtensions.AbuseVelocityScore,
res.SecurityExtensions.ObservationRecencyHours,
)
}
}
if res.Security.IsTorExit || res.Security.IsProxy {
return false, fmt.Sprintf("blocked_anonymizer(%s)", res.Security.AnonymizerType)
}
return true, "allowed"
}
func main() {
client := ipscope.NewClient(os.Getenv("IPSCOPE_KEY"), ipscope.WithTimeout(2*time.Second))
allowed, reason := ShouldAllowAdminIngress(context.Background(), client, "198.51.100.214")
fmt.Printf("allowed=%v reason=%s\n", allowed, reason)
}Frequently asked questions (FAQ)
What is the difference between security.confidence and security_extensions.abuse_velocity_score?
They measure two completely orthogonal properties:
security.confidence(0.0to1.0) measures our structural certainty that an IP address belongs to an anonymizing service (such as a commercial VPN, SOCKS proxy, or Tor exit). A clean, privacy-focused commercial VPN server may havesecurity.is_vpn: true, security.confidence: 0.99while having anabuse_velocity_scoreof0if no hostile scanning or brute-force traffic has originated from it.security_extensions.abuse_velocity_score(0to100) measures the intensity, target breadth, and recency of hostile scanning or exploit traffic observed from that IP address, regardless of whether the IP is a VPN, a compromised cloud VM, or an infected home router.
How does observation_recency_hours help me avoid false positives on offline .mmdb databases?
If your infrastructure downloads the .mmdb database on a Weekly or Monthly schedule rather than Daily, you can inspect observation_recency_hours alongside the database's build_epoch to see how fresh the abuse observation was at compile time. For customers using security_extensions in offline .mmdb or .parquet deployments, we strongly recommend configuring your database subscription to the Daily (06:00 UTC) update cadence so exponential score decay and newly detected scanner IPs are refreshed every 24 hours.