IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 宝塔面板批量封IP---node.js增量式封锁脚本 -> 正文阅读

[系统运维]宝塔面板批量封IP---node.js增量式封锁脚本

一句话需求

  • 现在是七月, 从三月开始我的一个网站一直受到几百个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

文件夹结构

在这里插入图片描述
源码:

/**
* ./utils/datetime-format.js
 * 计算指定时间到当前时间的时间间隔
 * @param {*} time 指定时间
 * @return 时间间隔或年月日时分
 *
 * example:
 * time2MinuteOrHour('2020-04-25T13:42:00.000Z')
 */
const time2MinuteOrHour = time => {
  const now = new Date()
  const pass = new Date(time)
  const result = now - pass
  // 分钟差小于60分钟
  if (parseInt(parseInt(result / 1000, 0) / 60, 0) < 60) {
    return `${Math.ceil(result / 1000 / 60)}分钟前`
  }
  // 小时差小于16小时
  if (parseInt(parseInt(parseInt(result / 1000, 0) / 60, 0) / 60, 0) < 16) {
    return `${Math.ceil(result / 1000 / 60 / 60)}小时前`
  }
  // 超过16个小时展示 年月日时分
  return time.replace(/T/, ' ').replace(/Z/, '').substring(0, 16)
}

/**
 * 时间转换为年月日时分
 * @param {*} originTime 原始时间
 * @return 年月日 时分
 *
 * example:
 * time2DateAndHM('2020-04-25T11:54:17+08:00')
 */
const time2DateAndHM = originTime => {
  const time = originTime.replace(/T/, ' ').replace(/Z/, '')
  return `${time.substring(0, 10)} ${time.substring(11, 16)}`
}

/**
 * 有效期(时间戳减去当前时间戳再转换为天)
 * @param {*} timestamp 时间戳
 * @return 天
 *
 * example:
 * timestamp2day(1589785128)
 */
const timestamp2day = timestamp => {
  const interval = timestamp - Math.round(new Date() / 1000)
  return parseInt(interval / (60 * 60 * 24), 0)
}

// yyyy-MM-dd hh:mm:ss.SSS 所有支持的类型
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
  }
}

// 这都n年了iOS依然不认识2020-12-12,需要转换为2020/12/12
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')

/**
 * Write file from path
 * @param {String} _path Relative path
 * @param {[String]} file File data
 */
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 })
  // console.log('file write to ', relativePath)
  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

/*
分析网站日志 main 组
提取非法IP列表
输出指定格式JSON
**********************使用放方法:
配置config里面的weblog路径地址, 必须是main路径或者IP在第一位(日志分隔符是空格), main配置格式见后
首次执行, 执行即可在当前目录下生成一个JSON文件, 复制这个文件内容到宝塔的系统防火墙导入IP界面即可(导入之前先删除老的JSON, 700个IP耗时半小时, 这个导入时间很长, 是宝塔的问题)
二次执行, 会生成全新JSON, 包含老的(这里要优化下, 提供只生成新IP的策略JSON)
之后对新weblog分析(含老日志或者全新日志都可以), 会自动对比老配置文件和新weblog, 自动去重已添加的IP并分配新的序列号

*/
const config = {
  newIpNewFile: true, // true 单独输出一份新IP输出为独立json文件; false 不单独输出一份
  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
}

// 生成封锁模板
// {"id": 1, "types": "drop", "address": "171.8.172.145", "brief": "", "addtime": "2021-07-28 17:26:50"}
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')
  }))

// 对比本地list 检测新的
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了.
  • 这个过一阵子空了做
  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2021-07-29 12:03:21  更:2021-07-29 12:04:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/27 10:41:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计