mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
351a01ff0c
An interval renewal turned the client's reset days into milliseconds with no bound, so a value that wraps int64 (a node reports it unchecked) sent the catch-up loop stepping through time for seconds per client while the traffic writer held the database. Intervals above 36500 days no longer renew, and every client write refuses them.
145 lines
4.3 KiB
Go
145 lines
4.3 KiB
Go
package service
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
)
|
|
|
|
// nextCalendarRenewal returns the next renewal strictly after from, at midnight
|
|
// in loc; a missing day clamps to the month's last, so the 31st comes back (#6106).
|
|
func nextCalendarRenewal(from time.Time, day int, loc *time.Location) time.Time {
|
|
if loc == nil {
|
|
loc = time.UTC
|
|
}
|
|
if day < 1 {
|
|
day = 1
|
|
}
|
|
if day > 31 {
|
|
day = 31
|
|
}
|
|
local := from.In(loc)
|
|
|
|
candidate := calendarDay(local.Year(), local.Month(), day, loc)
|
|
if !candidate.After(local) {
|
|
year, month := local.Year(), local.Month()+1
|
|
if month > time.December {
|
|
year, month = year+1, time.January
|
|
}
|
|
candidate = calendarDay(year, month, day, loc)
|
|
}
|
|
return candidate
|
|
}
|
|
|
|
// Clamped rather than normalized: time.Date rolls 31 February into March, which
|
|
// is the drift this mode exists to avoid.
|
|
func calendarDay(year int, month time.Month, day int, loc *time.Location) time.Time {
|
|
last := daysInMonth(year, month)
|
|
if day > last {
|
|
day = last
|
|
}
|
|
return time.Date(year, month, day, 0, 0, 0, 0, loc)
|
|
}
|
|
|
|
func daysInMonth(year int, month time.Month) int {
|
|
return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
|
|
}
|
|
|
|
// Advance by calendar dates, not 168 hours; a repeated midnight must not spend
|
|
// another weekly allowance on the same local date.
|
|
func nextWeeklyRenewal(from time.Time, weekday int, loc *time.Location) time.Time {
|
|
if loc == nil {
|
|
loc = time.UTC
|
|
}
|
|
local := from.In(loc)
|
|
days := (min(7, max(1, weekday))%7 - int(local.Weekday()) + 7) % 7
|
|
if days == 0 {
|
|
days = 7
|
|
}
|
|
date := time.Date(local.Year(), local.Month(), local.Day()+days, 0, 0, 0, 0, time.UTC)
|
|
// A corrupt or unusual zone must not stall the single traffic writer.
|
|
// Returning from lets the catch-up forward-progress guard fail closed.
|
|
for range 8 {
|
|
candidate, exists := localCalendarDateStart(date, loc)
|
|
if exists && candidate.After(local) {
|
|
return candidate
|
|
}
|
|
date = date.AddDate(0, 0, 7)
|
|
}
|
|
return from
|
|
}
|
|
|
|
// Find the first valid instant of a local date: Date can pick a repeated
|
|
// midnight or normalize a nonexistent midnight into the preceding day.
|
|
func localCalendarDateStart(date time.Time, loc *time.Location) (time.Time, bool) {
|
|
localDate := func(at time.Time) time.Time {
|
|
local := at.In(loc)
|
|
return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, time.UTC)
|
|
}
|
|
candidate := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, loc)
|
|
if localDate(candidate).Equal(date) && localDate(candidate.Add(-time.Second)).Before(date) {
|
|
return candidate, true
|
|
}
|
|
low, high := candidate.Unix()-86400, candidate.Unix()+86400
|
|
for low < high {
|
|
mid := low + (high-low)/2
|
|
if localDate(time.Unix(mid, 0)).Before(date) {
|
|
low = mid + 1
|
|
} else {
|
|
high = mid
|
|
}
|
|
}
|
|
start := time.Unix(low, 0).In(loc)
|
|
return start, localDate(start).Equal(date)
|
|
}
|
|
|
|
// maxClientResetDays bounds an interval renewal at a century: a larger one, which a node can
|
|
// report, wraps int64 once turned into milliseconds.
|
|
const maxClientResetDays = 36500
|
|
|
|
func nextClientRenewal(expiry int64, reset, day, weekday int, loc *time.Location) int64 {
|
|
if day > 0 {
|
|
return nextCalendarRenewal(time.UnixMilli(expiry), day, loc).UnixMilli()
|
|
}
|
|
if weekday > 0 {
|
|
return nextWeeklyRenewal(time.UnixMilli(expiry), weekday, loc).UnixMilli()
|
|
}
|
|
if reset > maxClientResetDays {
|
|
return expiry
|
|
}
|
|
return expiry + int64(reset)*86400000
|
|
}
|
|
|
|
func canonicalRenewalExpiry(expiry int64, reset, day, weekday int, loc *time.Location) int64 {
|
|
if day > 0 || weekday > 0 {
|
|
boundary := nextClientRenewal(expiry, reset, day, weekday, loc)
|
|
if expiry >= boundary-1000 && expiry < boundary {
|
|
return boundary
|
|
}
|
|
}
|
|
return expiry
|
|
}
|
|
|
|
func catchUpClientRenewal(traffic *xray.ClientTraffic, now int64, loc *time.Location) (int64, int) {
|
|
if traffic.ResetDay <= 0 && traffic.ResetWeekday <= 0 && traffic.Reset <= 0 {
|
|
return traffic.ExpiryTime, 0
|
|
}
|
|
expiry := canonicalRenewalExpiry(traffic.ExpiryTime, traffic.Reset, traffic.ResetDay, traffic.ResetWeekday, loc)
|
|
renewals := 0
|
|
for expiry < now {
|
|
if traffic.ResetMax > 0 && traffic.ResetCount+renewals >= traffic.ResetMax {
|
|
break
|
|
}
|
|
next := nextClientRenewal(expiry, traffic.Reset, traffic.ResetDay, traffic.ResetWeekday, loc)
|
|
if next <= expiry {
|
|
return traffic.ExpiryTime, 0
|
|
}
|
|
expiry = next
|
|
renewals++
|
|
}
|
|
if renewals == 0 {
|
|
return traffic.ExpiryTime, 0
|
|
}
|
|
return expiry, renewals
|
|
}
|