批量删除节点,一键屏蔽ip

This commit is contained in:
2026-06-19 20:37:19 +08:00
parent b56cfb7b1e
commit 2afd890de4
6 changed files with 328 additions and 1 deletions
+42
View File
@@ -1,7 +1,10 @@
package web
import (
"net"
mqtt "github.com/mochi-mqtt/server/v2"
"github.com/mochi-mqtt/server/v2/packets"
mqttforwardpkg "meshtastic_mqtt_server/internal/mqttforward"
storepkg "meshtastic_mqtt_server/internal/store"
@@ -11,6 +14,11 @@ import (
// 实现一般由 main 包传入(持有真正的 mqtt.Server / 写队列 / 统计器)。
type MQTTStatusProvider interface {
Status() AdminMQTTStatus
// DisconnectClient 立即踢掉指定的 MQTT 客户端。clientID 不存在时返回 false。
DisconnectClient(clientID string) bool
// LookupClientRemoteHost 根据 clientID 查询当前连接的远端主机(不带端口),
// 用于把该 IP 加入屏蔽表。clientID 不存在时返回空字符串与 false。
LookupClientRemoteHost(clientID string) (string, bool)
}
// MQTTRuntimeStatus 把 mqtt.Server / 写队列 / 转发统计三个上下文打包成
@@ -127,3 +135,37 @@ func mqttClientInfo(c *mqtt.Client) mqttClientInfoView {
RemoteAddr: c.Net.Remote,
}
}
// DisconnectClient 实现 MQTTStatusProvider:发送 Disconnect 报文并关闭连接。
// 使用 ErrAdministrativeAction 作为断开理由,便于日志区分。
func (m MQTTRuntimeStatus) DisconnectClient(clientID string) bool {
if m.Server == nil || clientID == "" {
return false
}
client, ok := m.Server.Clients.Get(clientID)
if !ok || client == nil {
return false
}
_ = m.Server.DisconnectClient(client, packets.ErrAdministrativeAction)
return true
}
// LookupClientRemoteHost 实现 MQTTStatusProvider:根据 clientID 查询当前连接的远端 IP。
// Net.Remote 通常是 "host:port",这里只返回主机部分;解析失败时回退为整个值。
func (m MQTTRuntimeStatus) LookupClientRemoteHost(clientID string) (string, bool) {
if m.Server == nil || clientID == "" {
return "", false
}
client, ok := m.Server.Clients.Get(clientID)
if !ok || client == nil {
return "", false
}
remote := client.Net.Remote
if remote == "" {
return "", false
}
if host, _, err := net.SplitHostPort(remote); err == nil && host != "" {
return host, true
}
return remote, true
}
+61
View File
@@ -249,6 +249,67 @@ func registerAdminRoutes(r gin.IRouter, store *storepkg.Store, sessions *auth.Ma
status.MessagesDropped = discardCount
c.JSON(http.StatusOK, status)
})
// 一键断开 MQTT 客户端并把它的远端 IP 加入屏蔽表。reason 由前端传入;写库前先查 IP 避免连接断开后查不到。
protected.POST("/mqtt/clients/:client_id/disconnect-and-block", func(c *gin.Context) {
if mqttStatus == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "mqtt server not available"})
return
}
clientID := c.Param("client_id")
if clientID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid client id"})
return
}
var req struct {
Reason string `json:"reason"`
}
// 请求体允许为空——若没传 reason 就走默认占位。
_ = c.ShouldBindJSON(&req)
reason := strings.TrimSpace(req.Reason)
if reason == "" {
reason = "manual disconnect from admin dashboard"
}
host, ok := mqttStatus.LookupClientRemoteHost(clientID)
if !ok || host == "" {
c.JSON(http.StatusNotFound, gin.H{"error": "mqtt client not found"})
return
}
// 先写屏蔽规则,再断开连接:万一断开后客户端立刻重连,新规则已经生效。
var ipRule *storepkg.IPBlockingRecord
row, err := store.CreateIPBlocking(host, reason, true)
switch {
case err == nil:
ipRule = row
case errors.Is(err, storepkg.ErrBlockingAlreadyExists):
// 已经存在则忽略——只确保是启用状态。
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if blocking != nil {
if err := blocking.Reload(store); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
disconnected := mqttStatus.DisconnectClient(clientID)
resp := gin.H{
"status": "ok",
"client_id": clientID,
"ip_value": host,
"disconnected": disconnected,
}
if ipRule != nil {
resp["ip_rule_id"] = ipRule.ID
resp["ip_rule_created"] = true
} else {
resp["ip_rule_created"] = false
}
c.JSON(http.StatusOK, resp)
})
protected.GET("/users", func(c *gin.Context) {
users, err := store.ListUsers()
if err != nil {