mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-26 14:56:39 +00:00
Add ASN-based resource rule matching
- Add MaxMind ASN database integration - Implement ASN lookup and matching in resource rule verification - Add curated list of 100+ major ASNs (cloud, ISP, CDN, mobile carriers) - Add ASN dropdown selector in resource rules UI with search functionality - Support custom ASN input for unlisted ASNs - Add 'ALL ASNs' special case handling (AS0) - Cache ASN lookups with 5-minute TTL for performance - Update validation schemas to support ASN match type This allows administrators to create resource access rules based on Autonomous System Numbers, similar to existing country-based rules. Useful for restricting access by ISP, cloud provider, or mobile carrier.
This commit is contained in:
committed by
Owen Schwartz
parent
1f4ebf1907
commit
8d2955475b
@@ -29,6 +29,7 @@ import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { getCountryCodeForIp } from "@server/lib/geoip";
|
||||
import { getAsnForIp } from "@server/lib/asn";
|
||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
@@ -128,6 +129,10 @@ export async function verifyResourceSession(
|
||||
? await getCountryCodeFromIp(clientIp)
|
||||
: undefined;
|
||||
|
||||
const ipAsn = clientIp
|
||||
? await getAsnFromIp(clientIp)
|
||||
: undefined;
|
||||
|
||||
let cleanHost = host;
|
||||
// if the host ends with :port, strip it
|
||||
if (cleanHost.match(/:[0-9]{1,5}$/)) {
|
||||
@@ -216,7 +221,8 @@ export async function verifyResourceSession(
|
||||
resource.resourceId,
|
||||
clientIp,
|
||||
path,
|
||||
ipCC
|
||||
ipCC,
|
||||
ipAsn
|
||||
);
|
||||
|
||||
if (action == "ACCEPT") {
|
||||
@@ -910,7 +916,8 @@ async function checkRules(
|
||||
resourceId: number,
|
||||
clientIp: string | undefined,
|
||||
path: string | undefined,
|
||||
ipCC?: string
|
||||
ipCC?: string,
|
||||
ipAsn?: number
|
||||
): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> {
|
||||
const ruleCacheKey = `rules:${resourceId}`;
|
||||
|
||||
@@ -954,6 +961,12 @@ async function checkRules(
|
||||
(await isIpInGeoIP(ipCC, rule.value))
|
||||
) {
|
||||
return rule.action as any;
|
||||
} else if (
|
||||
clientIp &&
|
||||
rule.match == "ASN" &&
|
||||
(await isIpInAsn(ipAsn, rule.value))
|
||||
) {
|
||||
return rule.action as any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1090,6 +1103,52 @@ async function isIpInGeoIP(
|
||||
return ipCountryCode?.toUpperCase() === checkCountryCode.toUpperCase();
|
||||
}
|
||||
|
||||
async function isIpInAsn(
|
||||
ipAsn: number | undefined,
|
||||
checkAsn: string
|
||||
): Promise<boolean> {
|
||||
// Handle "ALL" special case
|
||||
if (checkAsn === "ALL" || checkAsn === "AS0") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ipAsn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize the check ASN - remove "AS" prefix if present and convert to number
|
||||
const normalizedCheckAsn = checkAsn.toUpperCase().replace(/^AS/, "");
|
||||
const checkAsnNumber = parseInt(normalizedCheckAsn, 10);
|
||||
|
||||
if (isNaN(checkAsnNumber)) {
|
||||
logger.warn(`Invalid ASN format in rule: ${checkAsn}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const match = ipAsn === checkAsnNumber;
|
||||
logger.debug(
|
||||
`ASN check: IP ASN ${ipAsn} ${match ? "matches" : "does not match"} rule ASN ${checkAsnNumber}`
|
||||
);
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
async function getAsnFromIp(ip: string): Promise<number | undefined> {
|
||||
const asnCacheKey = `asn:${ip}`;
|
||||
|
||||
let cachedAsn: number | undefined = cache.get(asnCacheKey);
|
||||
|
||||
if (!cachedAsn) {
|
||||
cachedAsn = await getAsnForIp(ip); // do it locally
|
||||
// Cache for longer since IP ASN doesn't change frequently
|
||||
if (cachedAsn) {
|
||||
cache.set(asnCacheKey, cachedAsn, 300); // 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
return cachedAsn;
|
||||
}
|
||||
|
||||
async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
||||
const geoIpCacheKey = `geoip:${ip}`;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const createResourceRuleSchema = z.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]),
|
||||
value: z.string().min(1),
|
||||
priority: z.int(),
|
||||
enabled: z.boolean().optional()
|
||||
|
||||
@@ -25,7 +25,7 @@ const updateResourceRuleParamsSchema = z.strictObject({
|
||||
const updateResourceRuleSchema = z
|
||||
.strictObject({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]).optional(),
|
||||
value: z.string().min(1).optional(),
|
||||
priority: z.int(),
|
||||
enabled: z.boolean().optional()
|
||||
|
||||
Reference in New Issue
Block a user