一句话需求
- 现在是七月, 从三月开始我的一个网站一直受到几百个IP的流量攻击, 具体表现就是日志里面出现大量访问一个固定url网址的不带reffer的手机端的国内IP段的大量请求. 每秒请求超过50次.
- 一开始用宝塔面板的免费WAF nginx防火墙, 能防住, 但是效果不好, 依然会有大量额外的图片请求, 虽然不多.
- 事件的经过在没查明IP之前我是不想封的, 因为有些站群的操作手法就是克隆我的网站来引流到他们自己的网站, 这种手法会造成大量访问我IP的请求都是来自真实用户的手机. 然后根绝我长期采样观察发现这些虽然是真人IP但是应该是来自他们手机里面的后门, 所以流量对我没有意义, 可以直接封杀掉了.
- 定性之后就要封IP了. nginx那个防火墙不好用, 实际上请求还是请求了nginx的只是nginx返回了错误信息直接挡住了.所以nginx依然是有负载的.并且防不住变幻请求其他的鬼才知道的哪个页面.
- 我们要用linux自带的防火墙来直接封IP才能有效抵御住DDOS
- 宝塔面板的防火墙是
这是利用centos最新的系统级firewalld来封锁IP的高级防火墙, 比iptable更好.
- 从图片上的导入规则我们可以批量导入要封锁的IP地址
- 导入格式是
{"id": 1, "types": "drop", "address": "171.8.172.145", "brief": "", "addtime": "2021-07-28 17:26:50"}
需求落地:
- 分析nginx的web日志
- 提取要封锁的IP
- 生成JSON规则列表, 并保留历史记录, 下一次分析日志就只需要添加新规则
- 一次性导入即可
项目命名 digIP.js
文件夹结构
源码:
const time2MinuteOrHour = time => {
const now = new Date()
const pass = new Date(time)
const result = now - pass
if (parseInt(parseInt(result / 1000, 0) / 60, 0) < 60) {
return `${Math.ceil(result / 1000 / 60)}分钟前`
}
if (parseInt(parseInt(parseInt(result / 1000, 0) / 60, 0) / 60, 0) < 16) {
return `${Math.ceil(result / 1000 / 60 / 60)}小时前`
}
return time.replace(/T/, ' ').replace(/Z/, '').substring(0, 16)
}
const time2DateAndHM = originTime => {
const time = originTime.replace(/T/, ' ').replace(/Z/, '')
return `${time.substring(0, 10)} ${time.substring(11, 16)}`
}
const timestamp2day = timestamp => {
const interval = timestamp - Math.round(new Date() / 1000)
return parseInt(interval / (60 * 60 * 24), 0)
}
function pad(str, length = 2) {
str += ''
while (str.length < length) {
str = '0' + str
}
return str.slice(-length)
}
const parser = {
yyyy: dateObj => {
return pad(dateObj.year, 4)
},
yy: dateObj => {
return pad(dateObj.year)
},
MM: dateObj => {
return pad(dateObj.month)
},
M: dateObj => {
return dateObj.month
},
dd: dateObj => {
return pad(dateObj.day)
},
d: dateObj => {
return dateObj.day
},
hh: dateObj => {
return pad(dateObj.hour)
},
h: dateObj => {
return dateObj.hour
},
mm: dateObj => {
return pad(dateObj.minute)
},
m: dateObj => {
return dateObj.minute
},
ss: dateObj => {
return pad(dateObj.second)
},
s: dateObj => {
return dateObj.second
},
SSS: dateObj => {
return pad(dateObj.millisecond, 3)
},
S: dateObj => {
return dateObj.millisecond
}
}
function getDate(time) {
if (time instanceof Date) {
return time
}
switch (typeof time) {
case 'string':
return new Date(time.replace(/-/g, '/'))
default:
return new Date(time)
}
}
function formatDate(date, format = 'yyyy/MM/dd hh:mm:ss') {
if (!date && date !== 0) {
return '-'
}
date = getDate(date)
const dateObj = {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hour: date.getHours(),
minute: date.getMinutes(),
second: date.getSeconds(),
millisecond: date.getMilliseconds()
}
const tokenRegExp = /yyyy|yy|MM|M|dd|d|hh|h|mm|m|ss|s|SSS|SS|S/
let flag = true
let result = format
while (flag) {
flag = false
result = result.replace(tokenRegExp, function (matched) {
flag = true
return parser[matched](dateObj)
})
}
return result
}
function friendlyDate(
time,
{
locale = 'zh',
threshold = [60000, 3600000],
format = 'yyyy/MM/dd hh:mm:ss'
}
) {
if (!time && time !== 0) {
return '-'
}
const localeText = {
zh: {
year: '年',
month: '月',
day: '天',
hour: '小时',
minute: '分钟',
second: '秒',
ago: '前',
later: '后',
justNow: '刚刚',
soon: '马上',
template: '{num}{unit}{suffix}'
},
en: {
year: 'year',
month: 'month',
day: 'day',
hour: 'hour',
minute: 'minute',
second: 'second',
ago: 'ago',
later: 'later',
justNow: 'just now',
soon: 'soon',
template: '{num} {unit} {suffix}'
}
}
const text = localeText[locale] || localeText.zh
const date = getDate(time)
let ms = date.getTime() - Date.now()
const absMs = Math.abs(ms)
if (absMs < threshold[0]) {
return ms < 0 ? text.justNow : text.soon
}
if (absMs >= threshold[1]) {
return formatDate(date, format)
}
let num
let unit
let suffix = text.later
if (ms < 0) {
suffix = text.ago
ms = -ms
}
const seconds = Math.floor(ms / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
const months = Math.floor(days / 30)
const years = Math.floor(months / 12)
switch (true) {
case years > 0:
num = years
unit = text.year
break
case months > 0:
num = months
unit = text.month
break
case days > 0:
num = days
unit = text.day
break
case hours > 0:
num = hours
unit = text.hour
break
case minutes > 0:
num = minutes
unit = text.minute
break
default:
num = seconds
unit = text.second
break
}
if (locale === 'en') {
if (num === 1) {
num = 'a'
} else {
unit += 's'
}
}
return text.template
.replace(/{\s*num\s*}/g, num + '')
.replace(/{\s*unit\s*}/g, unit)
.replace(/{\s*suffix\s*}/g, suffix)
}
module.exports = {
formatDate
}
./utils/write-file.js
const fs = require('fs').promises
const path = require('path')
const writeFile = async (_path, file, flag = 'w+') => {
const relativePath = path.resolve(__dirname, _path)
const { dir } = path.parse(relativePath)
await fs.mkdir(path.resolve(__dirname, dir), { recursive: true })
return fs.writeFile(relativePath, file, { flag: 'w+' })
}
module.exports = writeFile
./utils/wirte-log.js
const fs = require('fs').promises
const writeLog = async (file, str, isAppend = true) => {
str && isAppend
? await fs.appendFile(file, str + '\n', 'utf-8')
: await fs.writeFile(file, str + '\n', 'utf-8')
}
module.exports = writeLog
./digIP.js 主程序代码
const fs = require('fs').promises
const path = require('path')
const writeLog = require('./utils/write-log.js')
const formatDate = require('./utils/datetime-format.js').formatDate
const config = {
newIpNewFile: true,
logFile: String.raw`F:\my_download\你的网站nginx访问日志文件(单行数据里面是空格分隔,其中IP是第一个数据).log`,
outputFile: path.resolve(__dirname, 'output-policy.json'),
policy: {
keyword: '/tag/特殊请求url格式/return%20false'
}
}
const getIPList = async () => {
let list = (await fs.readFile(config.logFile, { encoding: 'utf-8' })).split(
'\n'
)
console.log(`总计 ${list.length} 条记录`)
list = list.filter(item => ~item.indexOf(config.policy.keyword))
console.log(`找到 ${list.length} 条非法记录`)
list = list.map(item => item.split(' ')[0])
list = [...new Set(list)]
list = list.sort()
console.log(`本次攻击IP总计: ${list.length} 个`)
return list
}
const outputJSON = async (ipList, JsonInfo = 1, type = 'bt') =>
ipList.map((item, index, arr) => ({
id:
index +
(typeof JsonInfo === 'number'
? JsonInfo
: JsonInfo.length > 0
? JsonInfo.slice(-1)[0].id + 1
: 1),
types: 'drop',
address: item.trim(),
brief: '',
addtime: formatDate(Date.now(), 'yyyy-MM-dd hh:mm:ss')
}))
const getNewJsonDeltaBundle = async ipList => {
const oldJSON = JSON.parse(
await fs.readFile(config.outputFile, { encoding: 'utf-8' })
)
if (oldJSON.length > 0) {
const oldIPs = oldJSON.map(item => item.address)
return [ipList.filter(item => !oldIPs.includes(item)), oldJSON]
} else return [ipList, oldJSON]
}
;(async () => {
const timeElapseFlag = '🎈All Done'
console.time(timeElapseFlag)
const ipList = await getIPList()
let jsonList = await outputJSON(ipList)
try {
await fs.access(config.outputFile)
} catch (err) {
await writeLog(config.outputFile, JSON.stringify([]), false)
}
const [newIPList, oldJSON] = await getNewJsonDeltaBundle(ipList)
if (newIPList.length) {
console.log('写入新IP', newIPList)
if (config.newIpNewFile) {
jsonList = [...(await outputJSON(newIPList, oldJSON))]
await writeLog(
`${config.outputFile.slice(
0,
-5
)}_${Date.now()}${config.outputFile.slice(-5)}`,
JSON.stringify(jsonList),
false
)
}
jsonList = [...oldJSON, ...(await outputJSON(newIPList, oldJSON))]
} else {
console.log('没有找到需要添加的新IP')
}
await writeLog(config.outputFile, JSON.stringify(jsonList), false)
console.timeEnd(timeElapseFlag)
})()
代码执行
node digIP.js
- 初次运行
- - 多次运行
附上nginx的main节点日志格式
log_format main '$remote_addr - $remote_user [$time_local] requesthost:"$http_host"; "$request" requesttime:"$request_time"; $status $body_bytes_sent "$http_referer" - $request_body "$http_user_agent" "$http_x_forwarded_for"';
后记
生成的JSON文件用宝塔里面的系统防火墙IP屏蔽导入配置即可 流量刷一下就下去了 清空web日志 第二天再次观察还有几个漏网之鱼 再次执行程序得到新的配置文件 宝塔导入之 妥妥封死了
- 定性这些IP是封还是不封我是观察了几个月的包括他们的行为模式
- 封IP之后每天还是会有新的增长, 毕竟站群要吃饭嘛, 所以下一步会做全自动封锁脚本
- 自动封锁脚本都想好了, 上面这个拿到JSON策略文件->使用puppeteer自动登录宝塔后台并自动导入本地JSON规则, 这样就可以把以上全套代码部署到服务器实现无人值守每日自动封最新IP了.
- 这个过一阵子空了做
|