Загрузка...

We limit the user that consumes too much traffic | 3x-UI Panel

Thread in Go created by krisssss Jun 8, 2025. 302 views

  1. krisssss
    krisssss Topic starter Jun 8, 2025 Banned 10,787 Dec 24, 2024
    Ограничиваем юзера который потребляет слишком много трафика
    писал для https://github.com/MHSanaei/3x-ui
    Гитхаб: https://github.com/hellcat443/Hellcat-XUI-Sentinel


    Code
    package main

    import (
    "crypto/tls"
    "encoding/json"
    "flag"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/http/cookiejar"
    "net/url"
    "os"
    "strings"
    "time"
    )

    type ServerConfig struct {
    BaseURL string `json:"baseUrl"`
    Username string `json:"username"`
    Password string `json:"password"`
    InboundID int `json:"inboundId"`
    }

    type ClientStat struct {
    ID int `json:"id"`
    Email string `json:"email"`
    Up uint64 `json:"up"`
    Down uint64 `json:"down"`
    Enable bool `json:"enable"`
    ExpiryTime int64 `json:"expiryTime"`
    }

    type ListResponse struct {
    Success bool `json:"success"`
    Msg string `json:"msg"`
    Obj []struct {
    ID int `json:"id"`
    ClientStats []ClientStat `json:"clientStats"`
    Settings string `json:"settings"`
    } `json:"obj"`
    }

    var (
    thresholdBytes uint64
    intervalSeconds *int
    bannedUsers map[string]bool
    )

    const (
    configFile = "config.json"
    usageDataFile = "prev_usage.json"
    banFile = "ban.json"
    logFile = "monitor.log"
    )

    func init() {
    mb := flag.Uint64("threshold", 10, "Traffic limit MB")
    intervalSeconds = flag.Int("interval", 60, "Check interval sec")
    flag.Parse()
    thresholdBytes = *mb * 1024 * 1024
    f, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
    log.Fatalf("open log: %v", err)
    }
    log.SetOutput(io.MultiWriter(f, os.Stdout))
    bannedUsers = loadBanned()
    }

    func main() {
    cfgs, err := loadConfig()
    if err != nil {
    log.Fatal(err)
    }
    prev := loadPrevUsage()
    for name, cfg := range cfgs {
    go monitorServer(name, cfg, prev)
    }
    select {}
    }

    func loadConfig() (map[string]ServerConfig, error) {
    b, err := os.ReadFile(configFile)
    if err != nil {
    return nil, err
    }
    m := map[string]ServerConfig{}
    err = json.Unmarshal(b, &m)
    return m, err
    }

    func loadPrevUsage() map[string]uint64 {
    m := map[string]uint64{}
    if b, err := os.ReadFile(usageDataFile); err == nil {
    _ = json.Unmarshal(b, &m)
    }
    return m
    }

    func savePrevUsage(m map[string]uint64) {
    b, err := json.Marshal(m)
    if err != nil {
    log.Printf("marshal prev: %v", err)
    return
    }
    _ = os.WriteFile(usageDataFile, b, 0644)
    }

    func loadBanned() map[string]bool {
    m := map[string]bool{}
    if b, err := os.ReadFile(banFile); err == nil {
    _ = json.Unmarshal(b, &m)
    }
    return m
    }

    func saveBanned(m map[string]bool) {
    b, err := json.MarshalIndent(m, "", " ")
    if err != nil {
    log.Printf("marshal banned: %v", err)
    return
    }
    _ = os.WriteFile(banFile, b, 0644)
    }

    func newClient() *http.Client {
    jar, _ := cookiejar.New(nil)
    return &http.Client{
    Jar: jar,
    Transport: &http.Transport{
    TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    },
    Timeout: 30 * time.Second,
    }
    }

    func login(c *http.Client, cfg ServerConfig) error {
    endpoint := strings.TrimRight(cfg.BaseURL, "/") + "/login"
    form := url.Values{
    "username": {cfg.Username},
    "password": {cfg.Password},
    "twoFactorCode": {""},
    }
    resp, err := c.PostForm(endpoint, form)
    if err != nil {
    return err
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound {
    b, _ := io.ReadAll(resp.Body)
    return fmt.Errorf("login status %d: %s", resp.StatusCode, b)
    }
    return nil
    }

    func fetchClientStats(c *http.Client, cfg ServerConfig) ([]ClientStat, string, error) {
    endpoint := strings.TrimRight(cfg.BaseURL, "/") + "/panel/inbound/list"
    resp, err := c.Post(endpoint, "application/json", nil)
    if err != nil {
    return nil, "", err
    }
    defer resp.Body.Close()
    var lr ListResponse
    if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
    return nil, "", err
    }
    if !lr.Success {
    return nil, "", fmt.Errorf(lr.Msg)
    }
    for _, inb := range lr.Obj {
    if inb.ID == cfg.InboundID {
    return inb.ClientStats, inb.Settings, nil
    }
    }
    return nil, "", fmt.Errorf("inbound %d not found", cfg.InboundID)
    }

    func changeClientEnable(c *http.Client, cfg ServerConfig, uuid string, stat ClientStat, enable bool) error {
    clientData := map[string]interface{}{
    "id": uuid,
    "flow": "xtls-rprx-vision",
    "email": stat.Email,
    "limitIp": 0,
    "totalGB": 0,
    "expiryTime": 0,
    "enable": enable,
    "tgId": "",
    "subId": "",
    "comment": "",
    "reset": 0,
    }

    wrapped := map[string]interface{}{
    "clients": []interface{}{clientData},
    }

    jsonBytes, _ := json.Marshal(wrapped)
    form := url.Values{
    "id": {fmt.Sprint(cfg.InboundID)},
    "settings": {string(jsonBytes)},
    }

    url := strings.TrimRight(cfg.BaseURL, "/") + "/panel/inbound/updateClient/" + uuid
    log.Printf("POST %s — %s", url, stat.Email)

    req, err := http.NewRequest("POST", url, strings.NewReader(form.Encode()))
    if err != nil {
    return err
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    resp, err := c.Do(req)
    if err != nil {
    return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
    body, _ := io.ReadAll(resp.Body)
    return fmt.Errorf("update failed: %s", body)
    }
    return nil
    }

    func restartPanel(c *http.Client, cfg ServerConfig) error {
    url := strings.TrimRight(cfg.BaseURL, "/") + "/panel/setting/restartPanel"
    req, err := http.NewRequest("POST", url, nil)
    if err != nil {
    return err
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Set("X-Requested-With", "XMLHttpRequest")
    resp, err := c.Do(req)
    if err != nil {
    return err
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
    body, _ := io.ReadAll(resp.Body)
    return fmt.Errorf("restart failed: %s", body)
    }
    return nil
    }

    func monitorServer(name string, cfg ServerConfig, prev map[string]uint64) {
    client := newClient()
    for {
    if err := login(client, cfg); err != nil {
    log.Printf("[%s] login err: %v", name, err)
    time.Sleep(10 * time.Second)
    continue
    }

    stats, settingsJSON, err := fetchClientStats(client, cfg)
    if err != nil {
    log.Printf("[%s] fetch stats err: %v", name, err)
    time.Sleep(10 * time.Second)
    continue
    }

    uuidMap := map[string]string{}
    var parsed struct {
    Clients []struct {
    Email string `json:"email"`
    ID string `json:"id"`
    } `json:"clients"`
    }
    if err := json.Unmarshal([]byte(settingsJSON), &parsed); err != nil {
    log.Printf("[%s] failed to parse settings JSON: %v", name, err)
    } else {
    for _, client := range parsed.Clients {
    uuidMap[client.Email] = client.ID
    log.Printf("[%s] loaded UUID for %s: %s", name, client.Email, client.ID)
    }
    }

    for _, c := range stats {
    if !c.Enable || bannedUsers[c.Email] {
    continue
    }
    total := c.Up + c.Down
    key := fmt.Sprintf("%s|%s|%d", name, c.Email, c.ID)
    delta := total - prev[key]
    log.Printf("[%s] %s (ID:%d) ∆ %d", name, c.Email, c.ID, delta)

    if delta > thresholdBytes {
    log.Printf("[%s] %s exceeded", name, c.Email)
    uuid := uuidMap[c.Email]
    if uuid == "" {
    log.Printf("[%s] no uuid for %s", name, c.Email)
    } else {
    if err := changeClientEnable(client, cfg, uuid, c, false); err != nil {
    log.Printf("[%s] disable err: %v", name, err)
    } else {
    log.Printf("[%s] %s banned", name, c.Email)
    bannedUsers[c.Email] = true
    saveBanned(bannedUsers)
    restartPanel(client, cfg)
    }
    go func(u string, cc ClientStat) {
    time.Sleep(1 * time.Hour)
    login(client, cfg)
    changeClientEnable(client, cfg, u, cc, true)
    }(uuid, c)
    }
    }
    prev[key] = total
    }
    savePrevUsage(prev)
    time.Sleep(time.Duration(*intervalSeconds) * time.Second)
    }
    }
     
    1. View previous comments (2)
    2. Элейна
    3. Temmie
      ЯБылНоКем, можно ещё проще. Вырубить вай фай у себя
    4. Temmie
    5. View the next comments (1)
  2. thecashmere
    почему не взять rate limiter?
     
    1. krisssss Topic starter
      thecashmere, самый надежный способ я описал выше , так придумали создатели панели)
  3. yoona
    yoona Jun 11, 2025 58 Sep 10, 2017
    В этот раз я твой код рефакторить не хочу, поэтому немного текста.
    Файлики меняем на SQLite (и не просто так, ибо умрешь на вечной сериализации на больших объемах), опять же выкидываем Flag, с этой либой слишком невозможно работать увы (и неудобно запускать в кластерах, но учитывая контекст аля CLI - в целом окей, но ради устранения привычки, стоило бы заменить на аналог)
    Зачем писать **** в файл вручную, если можно сделать `./bin > data.log`? И не используй глобальный инстанс логгера, и вообще замени на структурный слог
    Бесконечный `select{}` меняем на `NotifyContext`
    endpoint string -> endpoint url.URL
    Везде прокидываем контекст, и прокидываем не затычки

    monitorServer дробить дробить и еще раз дробить, тут текстом даже не расписать, но очень большая вложенность
     
    1. View previous comments (1)
    2. yoona
      krisssss, примеры должны быть показательными и качественными, а это уже скорее наброски-наброска POC, но даже такие вещи зачастую в паблик без рефакторинга не уходят, а просто имеют минимальный функционал
    3. krisssss Topic starter
    4. yoona
      krisssss, тогда тут мои комменты немного невалидны, и тут совсем иначе делать придется, вообще с самого начала до самого конца полностью иначе
Loading...
Top