feat: remove speed limit tunnel binding and add migration cleanup

- Remove tunnel binding UI from speed limit page (no more Select component)
- Remove /api/v1/speed-limit/tunnels route alias
- Simplify CreateSpeedLimit/UpdateSpeedLimit to not accept tunnel parameters
- Add schema migration v4 to clear historical tunnel_id/tunnel_name bindings
- Update contract tests to verify tunnel binding is ignored
- Add limiter sync failure tests for forward-level rate limiting
This commit is contained in:
sagitchu
2026-02-27 19:30:02 +08:00
parent db3577afa9
commit 6e8406f439
14 changed files with 491 additions and 559 deletions
@@ -1028,52 +1028,46 @@ func asBool(v interface{}, def bool) bool {
}
}
func (h *Handler) sendLimiterConfig(limiterID int64, speedMbps int, tunnelID int64) error {
rate := float64(speedMbps) / 8.0
limitStr := fmt.Sprintf("$ %.1fMB %.1fMB", rate, rate)
payload := map[string]interface{}{
"name": strconv.FormatInt(limiterID, 10),
"limits": []string{limitStr},
}
nodes, err := h.tunnelEntryNodeIDs(tunnelID)
if err != nil {
return err
}
for _, nodeID := range nodes {
_, _ = h.sendNodeCommand(nodeID, "AddLimiters", payload, false, false)
}
return nil
}
func (h *Handler) sendDeleteLimiterConfig(limiterID int64, tunnelID int64) error {
payload := map[string]interface{}{
"limiter": strconv.FormatInt(limiterID, 10),
}
nodes, err := h.tunnelEntryNodeIDs(tunnelID)
if err != nil {
return err
}
for _, nodeID := range nodes {
_, _ = h.sendNodeCommand(nodeID, "DeleteLimiters", payload, false, true)
}
return nil
}
func (h *Handler) ensureLimiterOnNode(nodeID int64, limiterID int64, speed int) error {
rate := float64(speed) / 8.0
limitStr := fmt.Sprintf("$ %.1fMB %.1fMB", rate, rate)
payload := map[string]interface{}{
"name": strconv.FormatInt(limiterID, 10),
"limits": []string{limitStr},
}
if _, err := h.sendNodeCommand(nodeID, "AddLimiters", payload, false, false); err != nil {
if err := h.upsertLimiterOnNode(nodeID, limiterID, speed); err != nil {
return fmt.Errorf("限速规则下发失败: %w", err)
}
return nil
}
func buildLimiterAddPayload(limiterID int64, speed int) (string, map[string]interface{}) {
rate := float64(speed) / 8.0
limitStr := fmt.Sprintf("$ %.1fMB %.1fMB", rate, rate)
name := strconv.FormatInt(limiterID, 10)
return name, map[string]interface{}{
"name": name,
"limits": []string{limitStr},
}
}
func buildLimiterUpdatePayload(name string, data map[string]interface{}) map[string]interface{} {
return map[string]interface{}{
"limiter": name,
"data": data,
}
}
func (h *Handler) upsertLimiterOnNode(nodeID int64, limiterID int64, speed int) error {
name, addPayload := buildLimiterAddPayload(limiterID, speed)
if _, err := h.sendNodeCommand(nodeID, "AddLimiters", addPayload, false, false); err != nil {
if !isAlreadyExistsMessage(err.Error()) {
return err
}
payload := map[string]interface{}{
"name": name,
"limits": addPayload["limits"],
}
if _, updateErr := h.sendNodeCommand(nodeID, "UpdateLimiters", buildLimiterUpdatePayload(name, payload), false, false); updateErr != nil {
return updateErr
}
}
return nil
}
@@ -1472,7 +1472,7 @@ func (h *Handler) releasePeerShareForwardRuntimeServices(share *repo.PeerShare,
func isFederationRuntimeCommandAllowed(commandType string) bool {
switch strings.ToLower(strings.TrimSpace(commandType)) {
case "addservice", "updateservice", "deleteservice", "pauseservice", "resumeservice", "addchains", "deletechains", "addlimiters", "deletelimiters", "tcpping", "reload":
case "addservice", "updateservice", "deleteservice", "pauseservice", "resumeservice", "addchains", "deletechains", "addlimiters", "updatelimiters", "deletelimiters", "tcpping", "reload":
return true
default:
return false
@@ -159,7 +159,6 @@ func (h *Handler) Register(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/speed-limit/create", h.speedLimitCreate)
mux.HandleFunc("/api/v1/speed-limit/update", h.speedLimitUpdate)
mux.HandleFunc("/api/v1/speed-limit/delete", h.speedLimitDelete)
mux.HandleFunc("/api/v1/speed-limit/tunnels", h.tunnelList)
mux.HandleFunc("/api/v1/tunnel/user/tunnel", h.userTunnelVisibleList)
mux.HandleFunc("/api/v1/tunnel/user/list", h.userTunnelList)
mux.HandleFunc("/api/v1/group/tunnel/list", h.tunnelGroupList)
+2 -38
View File
@@ -1659,28 +1659,13 @@ func (h *Handler) speedLimitCreate(w http.ResponseWriter, r *http.Request) {
speed := asInt(req["speed"], 100)
var tunnelID *int64
var tunnelName string
if tid := asInt64(req["tunnelId"], 0); tid > 0 {
tunnelID = &tid
tunnelName = h.repo.GetTunnelNameByID(tid)
if tunnelName == "" {
response.WriteJSON(w, response.ErrDefault("隧道不存在"))
return
}
}
now := time.Now().UnixMilli()
id, err := h.repo.CreateSpeedLimit(name, speed, tunnelID, tunnelName, now, asInt(req["status"], 1))
_, err := h.repo.CreateSpeedLimit(name, speed, now, asInt(req["status"], 1))
if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
if tunnelID != nil && *tunnelID > 0 {
_ = h.sendLimiterConfig(id, speed, *tunnelID)
}
response.WriteJSON(w, response.OKEmpty())
}
@@ -1705,26 +1690,11 @@ func (h *Handler) speedLimitUpdate(w http.ResponseWriter, r *http.Request) {
speed := asInt(req["speed"], 100)
var tunnelID *int64
var tunnelName string
if tid := asInt64(req["tunnelId"], 0); tid > 0 {
tunnelID = &tid
tunnelName = h.repo.GetTunnelNameByID(tid)
if tunnelName == "" {
response.WriteJSON(w, response.ErrDefault("隧道不存在"))
return
}
}
if err := h.repo.UpdateSpeedLimit(id, name, speed, tunnelID, tunnelName, asInt(req["status"], 1), time.Now().UnixMilli()); err != nil {
if err := h.repo.UpdateSpeedLimit(id, name, speed, asInt(req["status"], 1), time.Now().UnixMilli()); err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
if tunnelID != nil && *tunnelID > 0 {
_ = h.sendLimiterConfig(id, speed, *tunnelID)
}
response.WriteJSON(w, response.OKEmpty())
}
@@ -1734,17 +1704,11 @@ func (h *Handler) speedLimitDelete(w http.ResponseWriter, r *http.Request) {
return
}
tunnelID := h.repo.GetSpeedLimitTunnelID(id)
if err := h.repo.DeleteSpeedLimit(id); err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
if tunnelID.Valid && tunnelID.Int64 > 0 {
_ = h.sendDeleteLimiterConfig(id, tunnelID.Int64)
}
response.WriteJSON(w, response.OKEmpty())
}