Skip to content

Commit 1dcba93

Browse files
committed
Add Cloudflare CIDR auto-load toggle
1 parent 4552f78 commit 1dcba93

File tree

2 files changed

+69
-14
lines changed

2 files changed

+69
-14
lines changed

server/server.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import xss from 'xss';
1414
import helmet from 'helmet';
1515
import client from 'prom-client';
1616
import cors from 'cors';
17-
import { limiter, challengeLimiter, validateInput, loadApiKeys, validateDeviceToken, extractDomain, getClientIp } from './utils.js';
17+
import { limiter, challengeLimiter, validateInput, loadApiKeys, validateDeviceToken, extractDomain, getClientIp, initializeTrustedProxyCidrs } from './utils.js';
1818

1919
dotenv.config();
2020

@@ -594,6 +594,8 @@ app.use((req, res, next) => {
594594
res.status(404).send('Not found');
595595
});
596596

597+
await initializeTrustedProxyCidrs();
598+
597599
app.listen(port, () => {
598600
console.log(`Nickel-Auth (v${version}) running on port: ${port}`);
599601
});

server/utils.js

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import rateLimit, { ipKeyGenerator } from 'express-rate-limit';
2+
import axios from 'axios';
23
import fs from 'fs';
34
import net from 'node:net';
45
import { authCache } from './server.js';
56

67
const maxLimitReq = parseInt(process.env.RATE_LIMIT) || 50;
78
const bannedIpsFile = './banned-ips.log';
89
const trustedProxyCidrsRaw = String(process.env.TRUSTED_PROXY_CIDRS || '').trim();
9-
const trustedProxyAllowlist = new net.BlockList();
10-
let trustedProxyRulesLoaded = false;
10+
const useCloudflareIps = String(process.env.USE_CLOUDFLARE_IPS || 'false').toLowerCase() === 'true';
11+
const cloudflareIpv4Url = process.env.CLOUDFLARE_IPV4_URL || 'https://www.cloudflare.com/ips-v4';
12+
const cloudflareIpv6Url = process.env.CLOUDFLARE_IPV6_URL || 'https://www.cloudflare.com/ips-v6';
13+
let trustedProxyAllowlist = new net.BlockList();
14+
let trustedProxyCidrs = [];
15+
let trustedProxyInitialized = false;
1116

1217
function sanitizeIpValue(value) {
1318
if (typeof value !== 'string') return '';
@@ -23,35 +28,83 @@ function sanitizeIpValue(value) {
2328
return net.isIP(cleaned) ? cleaned : '';
2429
}
2530

26-
function loadTrustedProxyRules() {
27-
if (trustedProxyRulesLoaded) return;
28-
trustedProxyRulesLoaded = true;
29-
if (!trustedProxyCidrsRaw) return;
30-
31-
const rules = trustedProxyCidrsRaw
31+
function parseCidrs(raw) {
32+
if (!raw || typeof raw !== 'string') return [];
33+
return raw
3234
.split(/[\s,]+/)
3335
.map((entry) => entry.trim())
3436
.filter(Boolean);
37+
}
3538

36-
for (const rule of rules) {
39+
function applyTrustedProxyCidrs(entries, sourceLabel) {
40+
trustedProxyAllowlist = new net.BlockList();
41+
trustedProxyCidrs = [];
42+
const deduped = new Set(entries);
43+
44+
for (const rule of deduped) {
3745
const [address, prefix] = rule.split('/');
3846
const sanitizedAddress = sanitizeIpValue(address);
3947
const prefixNum = Number(prefix);
4048
const ipVersion = net.isIP(sanitizedAddress);
49+
const maxPrefix = ipVersion === 4 ? 32 : 128;
4150

42-
if (!sanitizedAddress || Number.isNaN(prefixNum) || !Number.isInteger(prefixNum) || !ipVersion) {
43-
console.warn(`Skipping invalid TRUSTED_PROXY_CIDRS entry: ${rule}`);
51+
if (
52+
!sanitizedAddress ||
53+
Number.isNaN(prefixNum) ||
54+
!Number.isInteger(prefixNum) ||
55+
!ipVersion ||
56+
prefixNum < 0 ||
57+
prefixNum > maxPrefix
58+
) {
59+
console.warn(`Skipping invalid trusted proxy CIDR from ${sourceLabel}: ${rule}`);
4460
continue;
4561
}
4662

4763
const type = ipVersion === 4 ? 'ipv4' : 'ipv6';
4864
trustedProxyAllowlist.addSubnet(sanitizedAddress, prefixNum, type);
65+
trustedProxyCidrs.push(`${sanitizedAddress}/${prefixNum}`);
66+
}
67+
68+
return trustedProxyCidrs;
69+
}
70+
71+
function logTrustedProxyCidrs(sourceLabel) {
72+
if (!trustedProxyCidrs.length) {
73+
console.warn('No trusted proxy CIDRs configured; forwarded headers will be ignored.');
74+
return;
4975
}
76+
77+
console.log(`Trusted proxy CIDRs loaded from ${sourceLabel} (${trustedProxyCidrs.length}):`);
78+
trustedProxyCidrs.forEach((cidr) => console.log(` - ${cidr}`));
79+
}
80+
81+
export async function initializeTrustedProxyCidrs() {
82+
if (trustedProxyInitialized) return trustedProxyCidrs;
83+
trustedProxyInitialized = true;
84+
85+
if (useCloudflareIps) {
86+
try {
87+
const [ipv4Res, ipv6Res] = await Promise.all([
88+
axios.get(cloudflareIpv4Url, { timeout: 5000, responseType: 'text' }),
89+
axios.get(cloudflareIpv6Url, { timeout: 5000, responseType: 'text' })
90+
]);
91+
const cloudflareCidrs = [...parseCidrs(ipv4Res.data), ...parseCidrs(ipv6Res.data)];
92+
applyTrustedProxyCidrs(cloudflareCidrs, 'Cloudflare');
93+
logTrustedProxyCidrs('Cloudflare');
94+
return trustedProxyCidrs;
95+
} catch (error) {
96+
console.error(`Failed to fetch Cloudflare IP ranges: ${error.message}`);
97+
console.warn('Falling back to TRUSTED_PROXY_CIDRS.');
98+
}
99+
}
100+
101+
applyTrustedProxyCidrs(parseCidrs(trustedProxyCidrsRaw), 'TRUSTED_PROXY_CIDRS');
102+
logTrustedProxyCidrs('TRUSTED_PROXY_CIDRS');
103+
return trustedProxyCidrs;
50104
}
51105

52106
function isTrustedProxyIp(ip) {
53-
loadTrustedProxyRules();
54-
if (!trustedProxyCidrsRaw) return false;
107+
if (!trustedProxyCidrs.length) return false;
55108
const sanitizedIp = sanitizeIpValue(ip);
56109
if (!sanitizedIp) return false;
57110
const type = net.isIP(sanitizedIp) === 4 ? 'ipv4' : 'ipv6';

0 commit comments

Comments
 (0)