20260723:sip攻击解决,1. 基于 Redis 的IP速率限制 2. 自动黑名单机制

This commit is contained in:
zhugongping 2026-07-23 10:43:08 +08:00
parent 516f20374c
commit 9d0f392bd7
10 changed files with 314 additions and 22 deletions

View File

@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""本地 SIP 攻击模拟"""
import socket
def send_sip_register(target_ip, target_port, sip_id, local_ip="127.0.0.1", local_port=50600):
message = f"""REGISTER sip:{target_ip} SIP/2.0\r
Via: SIP/2.0/UDP {local_ip}:{local_port};rport;branch=z9hG4bK-test-{sip_id}\r
From: <sip:{sip_id}@{target_ip}>;tag=12345\r
To: <sip:{sip_id}@{target_ip}>\r
Call-ID: test-{sip_id}@{local_ip}\r
CSeq: 1 REGISTER\r
Contact: <sip:{sip_id}@{local_ip}:{local_port}>\r
Max-Forwards: 70\r
User-Agent: Python-Test\r
Expires: 3600\r
Content-Length: 0\r
\r
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(2)
try:
sock.sendto(message.encode(), (target_ip, target_port))
response, _ = sock.recvfrom(4096)
return response.decode()
except socket.timeout:
return "Timeout"
finally:
sock.close()
IP_POOL = ("1.113.12.33","1.113.12.34","1.113.12.35")
if __name__ == "__main__":
TARGET_IP = "127.0.0.1" # 本地测试
TARGET_PORT = 5061 # SIP端口
# 模拟攻击:连续发送20个请求
for i in range(100):
sip_id = str(7000 + i)
response = send_sip_register(TARGET_IP, TARGET_PORT, sip_id)
print(f"[{sip_id}] {response.split(chr(13))[0] if response else 'No response'}")

View File

@ -144,6 +144,12 @@ sip:
log: true log: true
zlmRecordPath: /opt/media/bin/www zlmRecordPath: /opt/media/bin/www
mp4_max_second: 30 # 视频录像时长,单位秒 mp4_max_second: 30 # 视频录像时长,单位秒
# SIP安全防护配置
security:
enabled: true # 是否启用安全防护
rate-limit: 30 # 每分钟最大请求数
block-threshold: 50 # 触发封禁的请求次数
block-duration: 30 # 封禁时长(分钟)
# 日志配置 # 日志配置
logging: logging:

View File

@ -139,6 +139,12 @@ sip:
log: false log: false
zlmRecordPath: /opt/media/bin/www zlmRecordPath: /opt/media/bin/www
mp4MaxSecond: 30 # 视频录像时长,单位秒 mp4MaxSecond: 30 # 视频录像时长,单位秒
# SIP安全防护配置
security:
enabled: true # 是否启用安全防护
rate-limit: 10 # 每分钟最大请求数(生产环境更严格)
block-threshold: 20 # 触发封禁的请求次数
block-duration: 10080 # 封禁时长(分钟)一个星期
# 日志配置 # 日志配置
logging: logging:

View File

@ -78,10 +78,10 @@ emqx:
ApiSecret: tMbf5EEZFb08sut88i4tZscxTc9CE3gaRQewSOTRNvWL ApiSecret: tMbf5EEZFb08sut88i4tZscxTc9CE3gaRQewSOTRNvWL
# sip 配置 # sip 配置
sip: sip:
enabled: false # 是否启用视频监控SIP,true为启用 enabled: true # 是否启用视频监控SIP,true为启用
## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP ## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP
## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP ## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP
ip: 47.121.180.229 # 容器部署容器IP:47.121.180.229;云服务器部署写公网IP;本地部署127.0.0.1;总原则:能被摄像机访问的地址 ip: 127.0.0.1 # 容器部署容器IP:47.121.180.229;云服务器部署写公网IP;本地部署127.0.0.1;总原则:能被摄像机访问的地址
port: 5061 # SIP端口(保持默认) port: 5061 # SIP端口(保持默认)
domain: 3402000000 # 由省级、市级、区级、基层编号组成 domain: 3402000000 # 由省级、市级、区级、基层编号组成
id: 34020000002000000001 # 同上,另外增加编号,(可保持默认) id: 34020000002000000001 # 同上,另外增加编号,(可保持默认)
@ -89,6 +89,12 @@ sip:
log: false log: false
zlmRecordPath: /opt/media/bin/www zlmRecordPath: /opt/media/bin/www
mp4MaxSecond: 30 # 视频录像时长,单位秒 mp4MaxSecond: 30 # 视频录像时长,单位秒
# SIP安全防护配置
security:
enabled: true # 是否启用安全防护
rate-limit: 20 # 每分钟最大请求数(生产环境更严格)
block-threshold: 30 # 触发封禁的请求次数
block-duration: 60 # 封禁时长(分钟)
# 日志配置 # 日志配置
logging: logging:

View File

@ -6,12 +6,14 @@ import com.fastbee.common.core.page.TableDataInfo;
import com.fastbee.common.utils.poi.ExcelUtil; import com.fastbee.common.utils.poi.ExcelUtil;
import com.fastbee.iot.domain.SipRelation; import com.fastbee.iot.domain.SipRelation;
import com.fastbee.iot.service.ISipRelationService; import com.fastbee.iot.service.ISipRelationService;
import com.fastbee.sip.service.ISipSecurityService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.List; import java.util.List;
@ -28,6 +30,9 @@ public class SipRelationController extends BaseController {
@Autowired @Autowired
private ISipRelationService sipRelationService; private ISipRelationService sipRelationService;
@Resource
private ISipSecurityService sipSecurityService;
/** /**
* 查询监控设备关联列表 * 查询监控设备关联列表
*/ */
@ -98,4 +103,14 @@ public class SipRelationController extends BaseController {
public AjaxResult remove(@PathVariable Long[] ids) { public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(sipRelationService.deleteSipRelationByIds(ids)); return toAjax(sipRelationService.deleteSipRelationByIds(ids));
} }
// ================================= 解禁IP =================================
@PreAuthorize("@ss.hasPermi('iot:relation:edit')")
@GetMapping("/unblockIp")
@ApiOperation("删除监控设备关联")
public AjaxResult unblockIp(@RequestParam("ip") String ip) {
sipSecurityService.unblockIp(ip);
return success();
}
} }

View File

@ -63,6 +63,12 @@
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
</dependency> </dependency>
<!-- Redis缓存 -->
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>fastbee-common</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -16,6 +16,7 @@ import com.fastbee.sip.service.IMqttService;
import com.fastbee.sip.service.ISipConfigService; import com.fastbee.sip.service.ISipConfigService;
import com.fastbee.sip.service.ISipDeviceChannelService; import com.fastbee.sip.service.ISipDeviceChannelService;
import com.fastbee.sip.service.ISipDeviceService; import com.fastbee.sip.service.ISipDeviceService;
import com.fastbee.sip.service.ISipSecurityService;
import com.fastbee.sip.util.DigestAuthUtil; import com.fastbee.sip.util.DigestAuthUtil;
import gov.nist.javax.sip.address.AddressImpl; import gov.nist.javax.sip.address.AddressImpl;
import gov.nist.javax.sip.address.SipUri; import gov.nist.javax.sip.address.SipUri;
@ -23,11 +24,11 @@ import gov.nist.javax.sip.header.Expires;
import gov.nist.javax.sip.header.SIPDateHeader; import gov.nist.javax.sip.header.SIPDateHeader;
import gov.nist.javax.sip.message.SIPRequest; import gov.nist.javax.sip.message.SIPRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.sip.InvalidArgumentException; import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent; import javax.sip.RequestEvent;
@ -70,42 +71,70 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi
@Autowired @Autowired
private SIPSender sipSender; private SIPSender sipSender;
@Autowired
private ISipSecurityService sipSecurityService;
@Override @Override
public void processMsg(RequestEvent evt) { public void processMsg(RequestEvent evt) {
try { try {
log.info("收到注册请求,开始处理");
Request request = evt.getRequest(); Request request = evt.getRequest();
SIPRequest Sipreq = (SIPRequest) evt.getRequest(); SIPRequest sipReq = (SIPRequest) evt.getRequest();
Response response; Response response;
// 注册标志 0:未携带授权头或者密码错误 1:注册成功 2:注销成功
int registerFlag; // ========== 安全防护检查 ==========
// 提取客户端IP
ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
String clientIp = viaHeader.getReceived();
if (StringUtils.isEmpty(clientIp)) {
clientIp = viaHeader.getHost();
}
// 检查IP是否在黑名单中
if (sipSecurityService.isBlocked(clientIp)) {
log.debug("[SIP安全] IP {} 已封禁,拒绝REGISTER请求", clientIp);
return; // 直接丢弃,不响应
}
// 检查速率限制
FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME); FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);
AddressImpl address = (AddressImpl) fromHeader.getAddress(); AddressImpl address = (AddressImpl) fromHeader.getAddress();
SipUri uri = (SipUri) address.getURI(); SipUri uri = (SipUri) address.getURI();
String sipId = uri.getUser(); String sipId = uri.getUser();
if (sipSecurityService.checkAndRecord(clientIp, sipId)) {
log.debug("[SIP安全] IP {} 请求超限,拒绝请求,sipId: {}", clientIp, sipId);
return; // 直接丢弃,让攻击者超时等待
}
log.debug("收到注册请求,开始处理,sipId: {}, IP: {}", sipId, clientIp);
// ========== 安全防护检查结束 ==========
// 注册标志 0:未携带授权头或者密码错误 1:注册成功 2:注销成功
int registerFlag;
//取默认Sip配置 //取默认Sip配置
SipConfig sipConfig = sipConfigService.selectSipConfigBydeviceSipId(sipId); SipConfig sipConfig = sipConfigService.selectSipConfigBydeviceSipId(sipId);
if (sipConfig != null) { if (sipConfig != null) {
AuthorizationHeader authorhead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); AuthorizationHeader authorHead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
// 校验密码是否正确 // 校验密码是否正确
if (authorhead == null && !ObjectUtils.isEmpty(sipConfig.getPassword())) { if (authorHead == null && !ObjectUtils.isEmpty(sipConfig.getPassword())) {
log.info("未携带授权头 回复401,sipId:{}", sipId); log.debug("未携带授权头 回复401,sipId:{},IP:{}", sipId, clientIp);
response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request); response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request);
new DigestAuthUtil().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain()); new DigestAuthUtil().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain());
sipSender.transmitRequest(Sipreq.getLocalAddress().getHostAddress(), response); sipSender.transmitRequest(sipReq.getLocalAddress().getHostAddress(), response);
return; return;
} }
boolean pcheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request, boolean preCheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request,
sipConfig.getPassword()); sipConfig.getPassword());
boolean syscheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request, boolean sysCheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request,
sysSipConfig.getPassword()); sysSipConfig.getPassword());
if (!pcheck && !syscheck) { if (!preCheck && !sysCheck) {
// 注册失败 // 注册失败
response = getMessageFactory().createResponse(Response.FORBIDDEN, request); response = getMessageFactory().createResponse(Response.FORBIDDEN, request);
response.setReasonPhrase("wrong password"); response.setReasonPhrase("wrong password");
log.info("[注册请求] 密码/SIP服务器ID错误, 回复403 sipId:" + sipId); log.info("[注册请求] 密码/SIP服务器ID错误, 回复403 sipId:{}", sipId);
getServerTransaction(evt).sendResponse(response); getServerTransaction(evt).sendResponse(response);
return; return;
} }
@ -122,7 +151,7 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi
response.addHeader(request.getHeader(ContactHeader.NAME)); response.addHeader(request.getHeader(ContactHeader.NAME));
// 添加Expires头 // 添加Expires头
response.addHeader(request.getExpires()); response.addHeader(request.getExpires());
ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); //ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
String received = viaHeader.getReceived(); String received = viaHeader.getReceived();
int rPort = viaHeader.getRPort(); int rPort = viaHeader.getRPort();
// 本地模拟设备 received 为空 rPort 为 -1 // 本地模拟设备 received 为空 rPort 为 -1
@ -153,20 +182,20 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi
} }
device.setTransport(isTcp ? "TCP" : "UDP"); device.setTransport(isTcp ? "TCP" : "UDP");
} }
sipSender.transmitRequest(Sipreq.getLocalAddress().getHostAddress(), response); sipSender.transmitRequest(sipReq.getLocalAddress().getHostAddress(), response);
// 注册成功 // 注册成功
if (registerFlag == 1) { if (registerFlag == 1) {
log.info("注册成功! sipId:" + device.getDeviceSipId()); log.info("注册成功! sipId:{}", device.getDeviceSipId());
device.setRegistertime(DateUtils.getNowDate()); device.setRegistertime(DateUtils.getNowDate());
sipDeviceService.updateDevice(device); sipDeviceService.updateDevice(device);
List<SipDeviceChannel> channels = sipDeviceChannelService.selectSipDeviceChannelByDeviceSipId(device.getDeviceSipId()); List<SipDeviceChannel> channels = sipDeviceChannelService.selectSipDeviceChannelByDeviceSipId(device.getDeviceSipId());
if (channels.size() > 0) { if (!channels.isEmpty()) {
Device iotdev = deviceService.selectDeviceBySerialNumber(device.getDeviceSipId()); Device iotdev = deviceService.selectDeviceBySerialNumber(device.getDeviceSipId());
if (iotdev == null) { if (iotdev == null) {
//添加到统一设备管理 默认添加到admin账号下 //添加到统一设备管理 默认添加到admin账号下
int result = deviceService.insertDeviceAuto(device.getDeviceSipId(), 1L, sipConfig.getProductId()); int result = deviceService.insertDeviceAuto(device.getDeviceSipId(), 1L, sipConfig.getProductId());
if (result == 1) { if (result == 1) {
log.info("-----------sip设备认证成功,并自动添加设备到系统,SipId:" + device.getDeviceSipId() + "---------------"); log.info("-----------sip设备认证成功,并自动添加设备到系统,SipId:{}---------------", device.getDeviceSipId());
} }
iotdev = new Device(); iotdev = new Device();
} }
@ -177,10 +206,10 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi
// 重新注册更新设备和通道,以免设备替换或更新后信息无法更新 // 重新注册更新设备和通道,以免设备替换或更新后信息无法更新
onRegister(device); onRegister(device);
} else { } else {
log.info("未找到改设备! deviceId:" + device.getDeviceId()); log.info("未找到改设备! deviceId:{}", device.getDeviceId());
} }
} else if (registerFlag == 2) { } else if (registerFlag == 2) {
log.info("注销成功! deviceId:" + device.getDeviceId()); log.info("注销成功! deviceId:{}", device.getDeviceId());
mqttService.publishStatus(device, 4); mqttService.publishStatus(device, 4);
} }

View File

@ -0,0 +1,60 @@
package com.fastbee.sip.service;
/**
* SIP安全防护服务接口
*
* @author George
* @date 2026/7/23
*/
public interface ISipSecurityService {
/**
* Redis key前缀 - IP封禁黑名单
*/
String BLOCK_PREFIX = "sip:block:";
/**
* Redis key前缀 - IP请求计数
*/
String COUNT_PREFIX = "sip:count:";
/**
* 检查IP是否被封禁
*
* @param ip 客户端IP
* @return true=已封禁, false=未封禁
*/
boolean isBlocked(String ip);
/**
* 记录请求并检查是否超过速率限制
*
* @param ip 客户端IP
* @param sipId SIP设备ID
* @return true=超过限制触发封禁, false=正常
*/
boolean checkAndRecord(String ip, String sipId);
/**
* 手动封禁IP
*
* @param ip 客户端IP
* @param minutes 封禁时长(分钟)
*/
void blockIp(String ip, int minutes);
/**
* 解除IP封禁
*
* @param ip 客户端IP
*/
void unblockIp(String ip);
/**
* 获取IP请求计数
*
* @param ip 客户端IP
* @return 当前计数
*/
long getRequestCount(String ip);
}

View File

@ -0,0 +1,121 @@
package com.fastbee.sip.service.impl;
import com.fastbee.common.core.redis.RedisCache;
import com.fastbee.sip.service.ISipSecurityService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* SIP安全防护服务实现
* 基于Redis实现IP速率限制和自动黑名单
*
* @author George
* @date 2026/7/23
*/
@Slf4j
@Service
public class SipSecurityServiceImpl implements ISipSecurityService {
@Autowired
private RedisCache redisCache;
/**
* 速率限制:每分钟最大请求数
*/
@Value("${sip.security.rate-limit:20}")
private int rateLimit;
/**
* 触发封禁的失败次数阈值
*/
@Value("${sip.security.block-threshold:30}")
private int blockThreshold;
/**
* 默认封禁时长(分钟)
*/
@Value("${sip.security.block-duration:120}")
private int blockDuration;
@Override
public boolean isBlocked(String ip) {
if (ip == null || ip.isEmpty()) {
return false;
}
String key = BLOCK_PREFIX + ip;
return Boolean.TRUE.equals(redisCache.hasKey(key));
}
@Override
public boolean checkAndRecord(String ip, String sipId) {
if (ip == null || ip.isEmpty()) {
return false;
}
// 1. 检查是否已封禁
if (isBlocked(ip)) {
log.warn("[SIP安全] IP {} 已在黑名单中,拒绝请求", ip);
return true;
}
// 2. 记录请求计数(1分钟窗口)
String countKey = COUNT_PREFIX + ip;
long count = redisCache.incr(countKey, 1);
// 首次访问设置过期时间
if (count == 1) {
redisCache.expire(countKey, 60, TimeUnit.SECONDS);
}
// 3. 检查是否超过速率限制
if (count > rateLimit) {
log.warn("[SIP安全] IP {} 请求频率超限,当前: {}/min,限制: {}/min", ip, count, rateLimit);
// 超过阈值则自动封禁
if (count >= blockThreshold) {
blockIp(ip, blockDuration);
log.warn("[SIP安全] IP {} 触发自动封禁,时长: {}分钟", ip, blockDuration);
}
return true;
}
return false;
}
@Override
public void blockIp(String ip, int minutes) {
if (ip == null || ip.isEmpty()) {
return;
}
String key = BLOCK_PREFIX + ip;
redisCache.setCacheObject(key, System.currentTimeMillis(), minutes, TimeUnit.MINUTES);
log.info("[SIP安全] 封禁IP: {},时长: {}分钟", ip, minutes);
}
@Override
public void unblockIp(String ip) {
if (ip == null || ip.isEmpty()) {
return;
}
String key = BLOCK_PREFIX + ip;
redisCache.deleteObject(key);
log.info("[SIP安全] 解封IP: {}", ip);
}
@Override
public long getRequestCount(String ip) {
if (ip == null || ip.isEmpty()) {
return 0;
}
String countKey = COUNT_PREFIX + ip;
Object obj = redisCache.getCacheObject(countKey);
if (obj == null) {
return 0;
}
return Long.parseLong(obj.toString());
}
}