Skip to main content
doc0blade47/honodocumentation home
Sign in

IP Restriction

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The IP restriction middleware restricts incoming HTTP requests by evaluating client IP addresses against configurable allow and deny rules. It processes static IP addresses, CIDR notation subnets for both IPv4 and IPv6, wildcard patterns, and custom validation functions.

Sources: src/middleware/ip-restriction/index.ts:26-38, src/middleware/ip-restriction/index.ts:83-87, src/middleware/ip-restriction/index.ts:172-175

Client addresses are retrieved via connection info helpers across multiple runtime adapters and normalized for comparison. The middleware converts dotted-decimal IPv4 and hexadecimal IPv6 addresses into bigint binary representations, handling IPv4-mapped IPv6 formats and subnet bitmasks for precise access control decisions.

Sources: src/middleware/ip-restriction/index.ts:10-18, src/middleware/ip-restriction/index.ts:104-114, src/middleware/ip-restriction/index.ts:131-139

Middleware Configuration and Public Interface

Public API Options and Types

The IP restriction middleware accepts a connection retrieval function and an options object containing configuration lists. The public interface defines rules that support wildcard matching, static addresses, CIDR notation subnets, and predicate functions src/middleware/ip-restriction/index.ts:23-38, src/middleware/ip-restriction/index.ts:172-183.

Option / TypeType DefinitionDescription
GetIPAddrGetConnInfo | ((c: Context) => string)Function to retrieve the client IP address from context or connection info.
IPRestrictionRulestring | ((addr: { addr: string; type: AddressType }) => boolean)Rule definition accepting a literal pattern, CIDR range, wildcard, or evaluation callback.
IPRestrictionRules{ denyList?: IPRestrictionRule[]; allowList?: IPRestrictionRule[] }Container object holding optional deny and allow rule lists.

Sources: src/middleware/ip-restriction/index.ts:23-38, src/middleware/ip-restriction/index.ts:172-183

Rule Formats and Matcher Compilation

Rule processing utilizes buildMatcher to compile rule arrays into optimized lookup sets and bitmasks src/middleware/ip-restriction/index.ts:51-120. The middleware evaluates incoming client requests against these compiled structures.

Warning

CIDR prefix validation strictly requires 1 to 3 decimal digits matching /^[0-9]{1,3}$/ and throws a TypeError if the parsed integer exceeds 32 for IPv4 or 128 for IPv6 src/middleware/ip-restriction/index.ts:41-50, src/middleware/ip-restriction/index.ts:98.

Sources: src/middleware/ip-restriction/index.ts:41-50, src/middleware/ip-restriction/index.ts:51-120

Client IP Extraction Across Runtimes

Connection Information Interface and Adapters

Hono extracts remote client addresses through runtime-specific connection info helpers adhering to the GetConnInfo type signature. Each adapter accesses underlying request context properties or platform-specific headers to populate the ConnInfo structure containing remote address, address type, port, and transport protocol src/helper/conninfo/types.ts:35-45.

Runtime / AdapterSource MechanismExtracted Properties
Bunserver.requestIP(c.req.raw)address, addressType (IPv4 / IPv6), port src/adapter/bun/conninfo.ts:11-41
Denoc.env.remoteAddraddress (hostname), port, transport src/adapter/deno/conninfo.ts:9-15
Cloudflare Workersc.req.header('cf-connecting-ip')address via header src/adapter/cloudflare-workers/conninfo.ts:3-6
Cloudflare Pagesc.req.header('cf-connecting-ip')address via header src/adapter/cloudflare-pages/conninfo.ts:22-25
Vercelc.req.header('x-real-ip')address via header src/adapter/vercel/conninfo.ts:3-7
Netlifyc.env.context?.ipaddress from Netlify function context src/adapter/netlify/conninfo.ts:53-56
AWS LambdaAPI Gateway v1/v2 or ALB x-forwarded-foraddress from requestContext.identity.sourceIp, requestContext.http.sourceIp, or trailing XFF entry src/adapter/aws-lambda/conninfo.ts:45-66
Lambda@Edgec.env.event.Records[0].cf.request.clientIpaddress from CloudFront event record src/adapter/lambda-edge/conninfo.ts:11-14

Sources: src/adapter/bun/conninfo.ts:11-41, src/adapter/deno/conninfo.ts:9-15, src/adapter/cloudflare-workers/conninfo.ts:3-6, src/adapter/cloudflare-pages/conninfo.ts:22-25, src/adapter/vercel/conninfo.ts:3-7, src/adapter/netlify/conninfo.ts:53-56, src/adapter/aws-lambda/conninfo.ts:45-66, src/adapter/lambda-edge/conninfo.ts:11-14

Specialized Adapter Extraction Behaviors

AWS Lambda handles multiple event payload structures by checking requestContext.identity for API Gateway REST APIs, requestContext.http for HTTP APIs and Function URLs, and falling back to parsing the comma-separated x-forwarded-for header where Application Load Balancers append the client IP to the end src/adapter/aws-lambda/conninfo.ts:50-65. Bun validates server existence and inspects server.requestIP, returning an empty remote object if the request is closed or uses a Unix domain socket src/adapter/bun/conninfo.ts:19-34.

Caution

In Bun, if the server instance is missing from the environment or server.requestIP is not a function, a TypeError is thrown immediately during connection info resolution src/adapter/bun/conninfo.ts:19-24.

Sources: src/adapter/aws-lambda/conninfo.ts:50-65, src/adapter/bun/conninfo.ts:19-34

IPv4 and Subnet Binary Conversion

Dotted-Decimal Parsing and Bitwise Construction

Dotted-decimal IPv4 strings are processed character by character using parseIPv4ToBinary, converting the string representation into a 32-bit unsigned integer represented as a bigint. The parser iterates through the specified string range, verifying character ASCII codes against digits (0-9), dots (.), and octet value bounds.

const parseIPv4ToBinary = (
  ipv4: string,
  start: number,
  end: number,
  onInvalid: () => never
): bigint => {
  let result = 0n
  let octets = 0
  let octet = 0
  let digits = 0
  let firstDigit = 0
 
  for (let i = start; i <= end; i++) {
    const code = i < end ? ipv4.charCodeAt(i) : CHAR_CODE_DOT
    if (code >= CHAR_CODE_0 && code <= CHAR_CODE_9) {
      if (digits === 0) {
        firstDigit = code
      } else if (firstDigit === CHAR_CODE_0) {
        onInvalid()
      }
      octet = octet * 10 + code - CHAR_CODE_0
      if (octet > 255) {
        onInvalid()
      }
      digits++
      continue
    }
 
    if (code !== CHAR_CODE_DOT || digits === 0 || octets === 4) {
      onInvalid()
    }
 
    result = (result << 8n) + BigInt(octet)
    octets++
    octet = 0
    digits = 0
  }
 
  if (octets !== 4) {
    onInvalid()
  }
 
  return result
}

Sources: src/utils/ipaddr.ts:83-126

Leading zeros within an octet (other than a standalone 0) trigger validation failures to prevent ambiguity. Each parsed octet is shifted left by 8 bits (result = (result << 8n) + BigInt(octet)) as the parser progresses across dots.

Warning

An octet value exceeding 255, failing to provide exactly four dot-separated octets, or containing leading zeros like 192.168.01.1 will immediately invoke the error handler and throw an invalid IP exception.

Sources: src/utils/ipaddr.ts:98-123

CIDR Subnet Prefix Parsing and Bitmasks

CIDR notation rules are parsed by matching expressions against IS_CIDR_NOTATION_REGEX = /\/[^/]*$/. The prefix length is extracted and validated against numeric constraints via parseCidrPrefix, ensuring the prefix integer does not exceed 32 for IPv4 rules.

const IS_CIDR_NOTATION_REGEX = /\/[^/]*$/
const parseCidrPrefix = (rule: string, prefix: string, max: number): number => {
  if (!/^[0-9]{1,3}$/.test(prefix)) {
    throw new TypeError(`Invalid rule: ${rule}`)
  }
  const parsedPrefix = parseInt(prefix)
  if (parsedPrefix > max) {
    throw new TypeError(`Invalid rule: ${rule}`)
  }
  return parsedPrefix
}

Sources: src/middleware/ip-restriction/index.ts:40-50

Once the prefix is verified, a bitmask is generated using bitwise shift operators. For an IPv4 CIDR range, the mask is constructed by shifting 1n left by the prefix length, subtracting 1n, and left-shifting the result by 32 - prefix.

const mask = ((1n << BigInt(prefix)) - 1n) << BigInt((isIPv4 ? 32 : 128) - prefix)
 
cidrRules.push([isIPv4, addr & mask, mask] as [boolean, bigint, bigint])

Sources: src/middleware/ip-restriction/index.ts:111-113

Execution Walkthrough and Sequence

When static rules or CIDR rules are compiled, IP strings pass through a deterministic function chain.

  1. registerStaticRule receives an IP rule string and invokes distinctRemoteAddr to classify it src/middleware/ip-restriction/index.ts:59-64.
  2. convertIPv4ToBinary is called for IPv4 addresses to parse the dotted-decimal string src/middleware/ip-restriction/index.ts:65.
  3. throwInvalidIPv4Address is invoked if parsing encounters malformed octets src/utils/ipaddr.ts:148-150.
  4. createInvalidIPAddressError generates the typed TypeError object carrying ERR_INVALID_IP_ADDRESS src/utils/ipaddr.ts:69-73.

Sources: src/middleware/ip-restriction/index.ts:59-65, src/utils/ipaddr.ts:69-73, src/utils/ipaddr.ts:148-150

Sources: src/middleware/ip-restriction/index.ts:59-65, src/utils/ipaddr.ts:69-77, src/utils/ipaddr.ts:148-150

IPv6 Address Normalization and Expansion

Zero-Compression and Section Expansion

Shortened IPv6 addresses containing double colons (::) are expanded to their full canonical 8-section representation by expandIPv6. The input string is first split by colons, and if the final section matches an IPv4 format via IPV4_REGEX, it is converted to binary and mapped back into hexadecimal IPv6 sections.

export const expandIPv6 = (ipV6: string): string => {
  const sections = ipV6.split(':')
  if (IPV4_REGEX.test(sections.at(-1) as string)) {
    sections.splice(
      -1,
      1,
      ...convertIPv6BinaryToString(convertIPv4ToBinary(sections.at(-1) as string)) // => ::7f00:0001
        .substring(2) // => 7f00:0001
        .split(':') // => ['7f00', '0001']
    )
  }

Sources: src/utils/ipaddr.ts:13-23

The function iterates through each section, padding non-empty nodes to four characters with leading zeros. When an empty string slot is encountered from zero compression, subsequent empty sections are spliced out, and the single slot is replaced with enough '0000' blocks to ensure the final array reaches exactly eight sections.

  for (let i = 0; i < sections.length; i++) {
    const node = sections[i]
    if (node !== '') {
      sections[i] = node.padStart(4, '0')
    } else {
      // Keep a single empty slot for `::` zero expansion.
      while (sections[i + 1] === '') {
        sections.splice(i + 1, 1)
      }
      sections[i] = new Array(8 - sections.length + 1).fill('0000').join(':')
    }
  }
  return sections.join(':')
}

Sources: src/utils/ipaddr.ts:24-37

Note

The zero-expansion block calculates missing segments dynamically using 8 - sections.length + 1, inserting the correct number of zero blocks to maintain standard IPv6 length compliance.

Sources: src/utils/ipaddr.ts:33

Execution Walkthrough and Sequence

When expanding and validating malformed addresses, execution proceeds through a strict error-handling call chain.

  1. expandIPv6 receives a shortened IPv6 address string and delegates nested IPv4-embedded segments to convertIPv4ToBinary src/utils/ipaddr.ts:13-19.
  2. convertIPv4ToBinary parses the trailing IPv4 portion and invokes throwInvalidIPv4Address if octet bounds or formatting fail src/utils/ipaddr.ts:148-150.
  3. throwInvalidIPv4Address immediately calls createInvalidIPAddressError with a formatted message src/utils/ipaddr.ts:75-77.
  4. createInvalidIPAddressError instantiates the typed TypeError with error code ERR_INVALID_IP_ADDRESS src/utils/ipaddr.ts:69-73.

Sources: src/utils/ipaddr.ts:13-19, src/utils/ipaddr.ts:69-77, src/utils/ipaddr.ts:148-150

Sources: src/utils/ipaddr.ts:13-19, src/utils/ipaddr.ts:69-77, src/utils/ipaddr.ts:148-150

Subnet Matching and Decision Evaluation

CIDR Rule Compilation and Mask Generation

The buildMatcher function processes configured IP rules to construct high-performance lookup structures. When a rule contains CIDR notation detected by IS_CIDR_NOTATION_REGEX, the prefix length is validated using parseCidrPrefix, ensuring it does not exceed 32 for IPv4 or 128 for IPv6. If the parsed prefix equals the maximum bit length, the rule is treated as a static address and registered directly.

const IS_CIDR_NOTATION_REGEX = /\/[^/]*$/
const parseCidrPrefix = (rule: string, prefix: string, max: number): number => {
  if (!/^[0-9]{1,3}$/.test(prefix)) {
    throw new TypeError(`Invalid rule: ${rule}`)
  }
  const parsedPrefix = parseInt(prefix)
  if (parsedPrefix > max) {
    throw new TypeError(`Invalid rule: ${rule}`)
  }
  return parsedPrefix
}

Sources: src/middleware/ip-restriction/index.ts:40-50

For subnet ranges, bitmasks are computed using BigInt shift operations. When an IPv6 rule represents an IPv4-mapped address with a prefix of 96 or greater, the rule is normalized into an IPv4 CIDR range by subtracting 96 from the prefix length.

if (IS_CIDR_NOTATION_REGEX.test(rule)) {
  const separatedRule = rule.split('/')
 
  const addrStr = separatedRule[0]
  const type = distinctRemoteAddr(addrStr)
  if (type === undefined) {
    throw new TypeError(`Invalid rule: ${rule}`)
  }
 
  let isIPv4 = type === 'IPv4'
  let prefix = parseCidrPrefix(rule, separatedRule[1], isIPv4 ? 32 : 128)
 
  if (isIPv4 ? prefix === 32 : prefix === 128) {
    // this rule is a static rule
    rule = addrStr
  } else {
    let addr = (isIPv4 ? convertIPv4ToBinary : convertIPv6ToBinary)(addrStr)
    if (type === 'IPv6' && isIPv4MappedIPv6(addr) && prefix >= 96) {
      isIPv4 = true
      addr = convertIPv4MappedIPv6ToIPv4(addr)
      prefix -= 96
    }
 
    const mask = ((1n << BigInt(prefix)) - 1n) << BigInt((isIPv4 ? 32 : 128) - prefix)
 
    cidrRules.push([isIPv4, addr & mask, mask] as [boolean, bigint, bigint])
    continue
  }

Sources: src/middleware/ip-restriction/index.ts:88-115

Warning

IPv4-mapped IPv6 CIDR rules with prefixes under 96 bits are evaluated under IPv6 rules, whereas prefixes of 96 bits or greater are shifted down and evaluated as IPv4 ranges to maintain correct binary mask alignment.

Sources: src/middleware/ip-restriction/index.ts:105-109

Evaluation Logic and Runtime Matching

The returned matcher evaluates incoming remote connection objects against compiled static sets, CIDR bitmasks, and custom callback functions in a sequential evaluation chain.

return (remote: {
  addr: string
  type: AddressType
  isIPv4: boolean
  binaryAddr?: bigint
}): boolean => {
  if (staticRules.has(remote.addr)) {
    return true
  }
  const remoteAddr = (remote.binaryAddr ||= (
    remote.isIPv4 ? convertIPv4ToBinary : convertIPv6ToBinary
  )(remote.addr))
  const remoteIPv4Addr =
    remote.isIPv4 || isIPv4MappedIPv6(remoteAddr)
      ? remote.isIPv4
        ? remoteAddr
        : convertIPv4MappedIPv6ToIPv4(remoteAddr)
      : undefined
  if ((remote.isIPv4 ? staticIPv4Rules : staticIPv6Rules).has(remoteAddr)) {
    return true
  }
  for (const [isIPv4, addr, mask] of cidrRules) {
    if (isIPv4) {
      if (remoteIPv4Addr === undefined) {
        continue
      }
      if ((remoteIPv4Addr & mask) === addr) {
        return true
      }
      continue
    }
    if (remote.isIPv4) {
      continue
    }
    if ((remoteAddr & mask) === addr) {
      return true
    }
  }
  for (const rule of functionRules) {
    if (rule({ addr: remote.addr, type: remote.type })) {
      return true
    }
  }
  return false
}

Sources: src/middleware/ip-restriction/index.ts:122-166

Lookup TargetInternal StructureMatching Mechanism
Static StringstaticRules (Set<string>)Direct string hash lookup against remote.addr
Static Binary IPv4staticIPv4Rules (Set<bigint>)BigInt lookup using convertIPv4ToBinary
Static Binary IPv6staticIPv6Rules (Set<bigint>)BigInt lookup using convertIPv6ToBinary
CIDR SubnetscidrRules ([boolean, bigint, bigint][])Bitwise AND evaluation: (remoteAddr & mask) === addr
Custom FunctionsfunctionRules (IPRestrictionRuleFunction[])Iterative execution of user-supplied predicate functions

Sources: src/middleware/ip-restriction/index.ts:54-58, src/middleware/ip-restriction/index.ts:128-164

Tip

Remote binary addresses are lazily evaluated and cached on the remote context object using logical assignment (remote.binaryAddr ||= ...), avoiding redundant string-to-bigint parsing during repeated middleware evaluations.

Sources: src/middleware/ip-restriction/index.ts:131-133

Rejection Handling and Custom Responses

Execution Flow and Denial Enforcement

When the IP restriction middleware processes an incoming request, access decisions are determined by evaluating the client's address against configured denyList and allowList rules. If a denyList is provided and the client IP matches any rule within it, or if an allowList is provided and the client IP fails to match any rule within it, access is denied and an HTTPException with status code 403 is thrown.

*
* @example
* ```ts
* import { Hono } from 'hono'
* import { ipRestriction } from 'hono/ip-restriction'
* import { getConnInfo } from 'hono/cloudflare-workers'
*
* const app = new Hono()
*
* app.use(
*   '*',
*   ipRestriction(getConnInfo, {
*     // Block a specific IP and an entire subnet
*     denyList: ['192.168.0.5', '10.0.0.0/8'],
*     // Only allow requests from localhost and a private range
*     allowList: ['127.0.0.1', '::1', '192.168.1.0/24'],
*   })
* )
*
* // With a custom error handler
* app.use(
*   '/admin/*',
*   ipRestriction(
*     getConnInfo,
*     { allowList: ['203.0.113.0/24'] },
*     (remote, c) => c.text(`Access denied for ${remote.addr}`, 403)
*   )
* )
*
* app.get('/', (c) => c.text('Hello!'))
* ```

Sources: src/middleware/ip-restriction/index.ts:186-216

Sources: src/middleware/ip-restriction/index.ts:186-216

Warning

If both a denyList and an allowList are configured, the denyList is evaluated first. A match in the deny list immediately triggers a 403 denial before the allow list is even checked.

Sources: src/middleware/ip-restriction/index.ts:188-198

Custom Block Responses and Error Handling

Developers can customize the response returned upon access denial by catching the HTTPException globally in Hono using app.onError(). Because the middleware relies on HTTPException from ../../http-exception, catching status 403 allows applications to return custom JSON payloads, HTML error pages, or plain text messages instead of the default framework error response.

import { HTTPException } from '../../http-exception'

Sources: src/middleware/ip-restriction/index.ts:8

Tip

To supply a custom block response body, handle HTTPException in your Hono application error handler and check err.status === 403 to render a tailored rejection message.

Sources: src/middleware/ip-restriction/index.ts:8

Was this page helpful?