Relevant source files
The following files were used as context for generating this wiki page:
- src/middleware/ip-restriction/index.ts
- src/utils/ipaddr.ts
- src/middleware/secure-headers/secure-headers.ts
- src/adapter/aws-lambda/handler.ts
- src/adapter/vercel/conninfo.ts
- src/router/reg-exp-router/prepared-router.ts
- src/request.ts
- src/adapter/lambda-edge/conninfo.ts
- src/adapter/bun/conninfo.ts
- src/validator/validator.ts
- src/router/reg-exp-router/router.ts
- src/adapter/aws-lambda/conninfo.ts
- src/middleware/language/language.ts
- src/adapter/cloudflare-workers/conninfo.ts
- src/utils/url.ts
- src/adapter/netlify/conninfo.ts
- src/router/linear-router/router.ts
- src/middleware/jwk/keys.test.json
- src/adapter/cloudflare-pages/conninfo.ts
- src/helper/conninfo/types.ts
- src/utils/accept.ts
- src/router/pattern-router/router.ts
- src/utils/compress.ts
- src/adapter/deno/conninfo.ts
- src/utils/cookie.ts
- src/helper/conninfo/index.ts
- src/router/reg-exp-router/node.ts
- src/validator/utils.ts
- src/utils/headers.ts
- src/middleware/secure-headers/permissions-policy.ts
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 / Type | Type Definition | Description |
|---|---|---|
GetIPAddr | GetConnInfo | ((c: Context) => string) | Function to retrieve the client IP address from context or connection info. |
IPRestrictionRule | string | ((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 / Adapter | Source Mechanism | Extracted Properties |
|---|---|---|
| Bun | server.requestIP(c.req.raw) | address, addressType (IPv4 / IPv6), port src/adapter/bun/conninfo.ts:11-41 |
| Deno | c.env.remoteAddr | address (hostname), port, transport src/adapter/deno/conninfo.ts:9-15 |
| Cloudflare Workers | c.req.header('cf-connecting-ip') | address via header src/adapter/cloudflare-workers/conninfo.ts:3-6 |
| Cloudflare Pages | c.req.header('cf-connecting-ip') | address via header src/adapter/cloudflare-pages/conninfo.ts:22-25 |
| Vercel | c.req.header('x-real-ip') | address via header src/adapter/vercel/conninfo.ts:3-7 |
| Netlify | c.env.context?.ip | address from Netlify function context src/adapter/netlify/conninfo.ts:53-56 |
| AWS Lambda | API Gateway v1/v2 or ALB x-forwarded-for | address from requestContext.identity.sourceIp, requestContext.http.sourceIp, or trailing XFF entry src/adapter/aws-lambda/conninfo.ts:45-66 |
| Lambda@Edge | c.env.event.Records[0].cf.request.clientIp | address 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.
registerStaticRulereceives an IP rule string and invokesdistinctRemoteAddrto classify it src/middleware/ip-restriction/index.ts:59-64.convertIPv4ToBinaryis called for IPv4 addresses to parse the dotted-decimal string src/middleware/ip-restriction/index.ts:65.throwInvalidIPv4Addressis invoked if parsing encounters malformed octets src/utils/ipaddr.ts:148-150.createInvalidIPAddressErrorgenerates the typedTypeErrorobject carryingERR_INVALID_IP_ADDRESSsrc/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.
expandIPv6receives a shortened IPv6 address string and delegates nested IPv4-embedded segments toconvertIPv4ToBinarysrc/utils/ipaddr.ts:13-19.convertIPv4ToBinaryparses the trailing IPv4 portion and invokesthrowInvalidIPv4Addressif octet bounds or formatting fail src/utils/ipaddr.ts:148-150.throwInvalidIPv4Addressimmediately callscreateInvalidIPAddressErrorwith a formatted message src/utils/ipaddr.ts:75-77.createInvalidIPAddressErrorinstantiates the typedTypeErrorwith error codeERR_INVALID_IP_ADDRESSsrc/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 Target | Internal Structure | Matching Mechanism |
|---|---|---|
| Static String | staticRules (Set<string>) | Direct string hash lookup against remote.addr |
| Static Binary IPv4 | staticIPv4Rules (Set<bigint>) | BigInt lookup using convertIPv4ToBinary |
| Static Binary IPv6 | staticIPv6Rules (Set<bigint>) | BigInt lookup using convertIPv6ToBinary |
| CIDR Subnets | cidrRules ([boolean, bigint, bigint][]) | Bitwise AND evaluation: (remoteAddr & mask) === addr |
| Custom Functions | functionRules (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
Related
On This Page
OverviewMiddleware Configuration and Public InterfacePublic API Options and TypesRule Formats and Matcher CompilationClient IP Extraction Across RuntimesConnection Information Interface and AdaptersSpecialized Adapter Extraction BehaviorsIPv4 and Subnet Binary ConversionDotted-Decimal Parsing and Bitwise ConstructionCIDR Subnet Prefix Parsing and BitmasksExecution Walkthrough and SequenceIPv6 Address Normalization and ExpansionZero-Compression and Section ExpansionExecution Walkthrough and SequenceSubnet Matching and Decision EvaluationCIDR Rule Compilation and Mask GenerationEvaluation Logic and Runtime MatchingRejection Handling and Custom ResponsesExecution Flow and Denial EnforcementCustom Block Responses and Error HandlingRelated