From 1ae07a1ca23833709021a3ab9f42bf8c3f684481 Mon Sep 17 00:00:00 2001 From: Sefinek Date: Fri, 21 Mar 2025 10:15:16 +0100 Subject: [PATCH] Improved the service fetching the device's IP address --- config.default.js | 7 ++-- index.js | 40 +++++++++++---------- package-lock.json | 4 +-- package.json | 2 +- services/SefinekAPI.js | 2 +- services/fetchServerIP.js | 41 ---------------------- services/ipFetcher.js | 73 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 66 deletions(-) delete mode 100644 services/fetchServerIP.js create mode 100644 services/ipFetcher.js diff --git a/config.default.js b/config.default.js index 0848db3..ce4d736 100644 --- a/config.default.js +++ b/config.default.js @@ -5,10 +5,11 @@ exports.CONFIG = { CLOUDFLARE_API_KEY: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', // https://dash.cloudflare.com/profile/api-tokens ABUSEIPDB_API_KEY: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', // API key for reporting malicious IPs to AbuseIPDB RUN_ON_START: true, // Should the reporting function run immediately after the script starts? + IPv6_SUPPORT: true, // Specifies whether the device has been assigned an IPv6 address. }, CYCLES: { - // Schedule for running cron jobs for reporting to AbuseIPDB. + // CRON: Schedule for running cron jobs for reporting to AbuseIPDB. REPORT_SCHEDULE: '0 */2 * * *', // The minimum time (in hours) that must pass after reporting an IP address before it can be reported again. @@ -22,9 +23,9 @@ exports.CONFIG = { // Additional delay (in milliseconds) after each successful IP report to avoid overloading the AbuseIPDB API. SUCCESS_COOLDOWN: 20, - // Interval for refreshing your IP address (in minutes). + // CRON: Interval for refreshing your IP address. Default: every 6 hours // This ensures that WAF violations originating from your IP address are not reported to AbuseIPDB. - IP_REFRESH_INTERVAL: 8 * 60 * 1000, + IP_REFRESH_SCHEDULE: '0 */6 * * *', }, SEFINEK_API: { diff --git a/index.js b/index.js index 1f5de4b..407a605 100644 --- a/index.js +++ b/index.js @@ -5,19 +5,18 @@ const PAYLOAD = require('./services/payload.js'); const SefinekAPI = require('./services/SefinekAPI.js'); const headers = require('./utils/headers.js'); const { logToCSV, readReportedIPs } = require('./services/csv.js'); -const formatDelay = require('./utils/formatDelay.js'); -const fetchServerIP = require('./services/fetchServerIP.js'); +const { refreshServerIPs, getServerIPs } = require('./services/ipFetcher.js'); const getFilters = require('./services/getFilters.js'); const log = require('./utils/log.js'); -const fetchBlockedIPs = async whitelist => { +const fetchCloudflareEvents = async whitelist => { try { const { data, status } = await axios.post('https://api.cloudflare.com/client/v4/graphql', PAYLOAD(), { headers: headers.CLOUDFLARE }); const events = data?.data?.viewer?.zones?.[0]?.firewallEventsAdaptive; if (!events) throw new Error(`Failed to retrieve data from Cloudflare (status ${status}): ${JSON.stringify(data?.errors)}`); const isWhitelisted = event => - event.ip === fetchServerIP() || + getServerIPs().includes(event.ip) || whitelist.userAgents.some(ua => event.userAgent.includes(ua)) || whitelist.imgExtensions.some(ext => event.clientRequestPath.endsWith(ext)) || whitelist.domains.some(domain => event.clientRequestHTTPHost?.includes(domain)) || @@ -60,7 +59,7 @@ const reportIP = async (event, uri, country, hostname, endpoint, cycleErrorCount return false; } - if (event.clientIP === fetchServerIP()) { + if (getServerIPs().includes(event.clientIP)) { logToCSV(event.rayName, event.clientIP, country, hostname, endpoint, event.userAgent, event.action, 'YOUR_IP_ADDRESS'); log(0, `Your IP address (${event.clientIP}) was unexpectedly received from Cloudflare. URI: ${uri}`); return false; @@ -104,23 +103,27 @@ const reportIP = async (event, uri, country, hostname, endpoint, cycleErrorCount let cycleId = 1; const cron = async () => { + log(0, `======================== Reporting Cycle No. ${cycleId} ========================`); + + // Fetch cloudflare events const whitelist = await getFilters(); + const events = await fetchCloudflareEvents(whitelist); + if (!events) return log(1, 'No events fetched, skipping cycle...'); - log(0, `===================== Reporting Cycle No. ${cycleId} =====================`); - - const blockedIPEvents = await fetchBlockedIPs(whitelist); - if (!blockedIPEvents) return log(1, 'No events fetched, skipping cycle...'); - - const serverIP = fetchServerIP(); - if (!serverIP) log(1, `Server IP address is missing! Received: ${serverIP}`); + // IP + await refreshServerIPs(); + const ips = getServerIPs(); + if (!Array.isArray(ips)) return log(2, 'For some reason, \'ips\' is not an array'); + log(0, `Fetched ${getServerIPs()?.length} of your IP addresses`); + // Cycle let cycleProcessedCount = 0, cycleReportedCount = 0, cycleSkippedCount = 0; const cycleErrorCounts = { blocked: 0, otherErrors: 0 }; - for (const event of blockedIPEvents) { + for (const event of events) { cycleProcessedCount++; const ip = event.clientIP; - if (ip === serverIP) { + if (getServerIPs().includes(ip)) { log(0, `The IP address ${ip} belongs to this machine. Ignoring...`); cycleSkippedCount++; continue; @@ -164,11 +167,12 @@ const cron = async () => { new CronJob(CONFIG.SEFINEK_API.REPORT_SCHEDULE, SefinekAPI, null, true, 'UTC'); } - // Ready - process.send && process.send('ready'); - log(0, 'The integration is ready!'); - // AbuseIPDB new CronJob(CONFIG.CYCLES.REPORT_SCHEDULE, cron, null, true, 'UTC'); + + // Ready + process.send && process.send('ready'); + + // Run on start? if (CONFIG.MAIN.RUN_ON_START) await cron(); })(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e7907e4..c9b0ddd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cf-waf-to-abuseipdb", - "version": "1.5.1", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cf-waf-to-abuseipdb", - "version": "1.5.1", + "version": "1.6.0", "license": "MIT", "dependencies": { "axios": "^1.8.4", diff --git a/package.json b/package.json index 174f71e..bf7b2cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cf-waf-to-abuseipdb", - "version": "1.5.1", + "version": "1.6.0", "description": "Node.js script for automatically reporting incidents to AbuseIPDB using data obtained from Cloudflare WAF.", "homepage": "https://github.com/sefinek/Cloudflare-WAF-To-AbuseIPDB", "bugs": { diff --git a/services/SefinekAPI.js b/services/SefinekAPI.js index ab5274c..4e92b7d 100644 --- a/services/SefinekAPI.js +++ b/services/SefinekAPI.js @@ -1,7 +1,7 @@ const axios = require('./axios.js'); const { readReportedIPs, updateSefinekAPIInCSV } = require('./csv.js'); const log = require('../utils/log.js'); -const fetchServerIP = require('./fetchServerIP.js'); +const fetchServerIP = require('./ipFetcher.js'); const { SEFINEK_API } = require('../config.js').CONFIG; module.exports = async () => { diff --git a/services/fetchServerIP.js b/services/fetchServerIP.js deleted file mode 100644 index d83a870..0000000 --- a/services/fetchServerIP.js +++ /dev/null @@ -1,41 +0,0 @@ -const { networkInterfaces } = require('node:os'); -const { get } = require('./axios.js'); -const isLocalIP = require('../utils/isLocalIP.js'); -const log = require('../utils/log.js'); -const { CYCLES } = require('../config.js').CONFIG; - -const ipAddrList = new Set(); - -const fetchIPv4Address = async () => { - try { - const { data } = await get('https://api.sefinek.net/api/v2/ip'); - if (data?.success && data?.message) ipAddrList.add(data.message); - } catch (err) { - log(2, `Error fetching IPv4 address: ${err.message}`); - } -}; - -const fetchIPv6Address = () => { - try { - Object.values(networkInterfaces()).flat().forEach(({ address, internal }) => { - if (!internal && address && !isLocalIP(address)) ipAddrList.add(address); - }); - } catch (err) { - log(2, `Error fetching IPv6 address: ${err.message}`); - } -}; - -const fetchServerIPs = async () => { - ipAddrList.clear(); - await fetchIPv4Address(); - fetchIPv6Address(); -}; - -(async () => { - await fetchServerIPs(); - setInterval(fetchServerIPs, CYCLES.IP_REFRESH_INTERVAL); - - // console.debug(ipAddrList); -})(); - -module.exports = () => Array.from(ipAddrList); \ No newline at end of file diff --git a/services/ipFetcher.js b/services/ipFetcher.js new file mode 100644 index 0000000..2e59762 --- /dev/null +++ b/services/ipFetcher.js @@ -0,0 +1,73 @@ +const { networkInterfaces } = require('node:os'); +const https = require('node:https'); +const { CronJob } = require('cron'); +const { get } = require('./axios.js'); +const isLocalIP = require('../utils/isLocalIP.js'); +const log = require('../utils/log.js'); +const { CYCLES, IPv6_SUPPORT } = require('../config.js').CONFIG; + +const ipAddresses = new Set(); +let ipv6ErrorCount = 0, ipv6ErrorLogged = false; +const IPv6Failed = 'It looks like your ISP hasn\'t assigned you any IPv6 address. I won\'t attempt to fetch it again.'; + +const fetchIPAddress = async family => { + if (family === 6 && (ipv6ErrorLogged || !IPv6_SUPPORT)) return; + + try { + const { data } = await get('https://api.sefinek.net/api/v2/ip', { + httpsAgent: new https.Agent({ family }), + }); + + if (data?.success && data?.message) { + ipAddresses.add(data.message); + + if (family === 6) { + if (ipv6ErrorCount > 0) { + const IPv6Success = `Uh, it looks like IPv6 has started working! It only succeeded after ${ipv6ErrorCount} attempts.`; + log(0, IPv6Success); + } + + ipv6ErrorCount = 0; + } + } else { + log(2, `Unexpected API response: ${JSON.stringify(data)}`); + } + } catch (err) { + log(2, `Error fetching IPv${family} address: ${err.message}`); + + if (family === 6 && err.code === 'ENOENT') { + ipv6ErrorCount++; + + if (ipv6ErrorCount >= 6 && !ipv6ErrorLogged) { + ipv6ErrorLogged = true; + log(0, IPv6Failed); + } else { + await new Promise(resolve => setTimeout(resolve, 4000)); + await fetchIPAddress(6); + } + } + } +}; + +const fetchLocalIPs = () => { + for (const iface of Object.values(networkInterfaces()).flat()) { + if (iface && !iface.internal && iface.address && !isLocalIP(iface.address)) { + ipAddresses.add(iface.address); + } + } +}; + +const refreshServerIPs = async () => { + await Promise.all([fetchIPAddress(4), fetchIPAddress(6)]); + fetchLocalIPs(); +}; + +(async () => { + new CronJob(CYCLES.IP_REFRESH_SCHEDULE || '0 */6 * * *', refreshServerIPs, null, true, 'UTC'); + await refreshServerIPs(); +})(); + +module.exports = { + refreshServerIPs, + getServerIPs: () => [...ipAddresses], +}; \ No newline at end of file