From 81fcacab1100a4152984a05158e9f8a509ab48dd Mon Sep 17 00:00:00 2001 From: Sanaei Date: Sun, 23 Aug 2026 21:51:39 +0200 Subject: [PATCH] chore(build): bump Go toolchain to 1.27.0 Go 1.27.0 shipped on 2026-08-19. Raise the go directive and the builder image so Docker and release builds pick it up; every CI job already reads the version from go.mod, and golangci-lint v2.13.1 release binaries are themselves built with go1.27.0, so the lint job needs no pin change. --- .github/claude/repo-context.md | 2 +- CLAUDE.md | 2 +- CONTRIBUTING.md | 2 +- Dockerfile | 2 +- bot_context_test.go | 4 ++-- docs/architecture.md | 2 +- go.mod | 10 +++++----- internal/pia/catalog_test.go | 2 +- internal/pia/validation.go | 2 +- internal/sub/json_service.go | 18 +++++++++--------- internal/sub/mutation_audit_test.go | 5 +++-- internal/sub/remote_routing.go | 4 ++-- internal/web/controller/api.go | 6 +++--- internal/web/job/ip_limit_allowlist.go | 9 ++++----- internal/web/network/auto_https_conn.go | 4 ++-- .../web/service/client_external_link_test.go | 10 +++------- internal/web/service/client_test.go | 11 +++++------ .../service/inbound_autorenew_calendar_test.go | 2 +- internal/web/service/setting_mtls_test.go | 2 +- internal/xray/geodata/query.go | 5 +---- 20 files changed, 48 insertions(+), 56 deletions(-) diff --git a/.github/claude/repo-context.md b/.github/claude/repo-context.md index fa3089a18..f18c00e45 100644 --- a/.github/claude/repo-context.md +++ b/.github/claude/repo-context.md @@ -15,7 +15,7 @@ question it already answers. 3x-ui is an open-source web control panel for managing Xray-core servers. -- Backend: Go 1.26, module `github.com/mhsanaei/3x-ui/v3`, Gin and GORM. +- Backend: Go 1.27, module `github.com/mhsanaei/3x-ui/v3`, Gin and GORM. - It runs Xray-core as a managed child process (`internal/xray/process.go`) and imports `github.com/xtls/xray-core` for config types and the gRPC stats/handler/router API. The release the panel BUNDLES is pinned in diff --git a/CLAUDE.md b/CLAUDE.md index 6ea3553dc..0bf942f3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ index, layering rules), read `docs/architecture.md` on demand — do not guess file locations when it can answer in one hop. ## Stack -- Backend: Go 1.26 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM. +- Backend: Go 1.27 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM. Runs Xray-core as a managed child process (`internal/xray/process.go`) and imports `github.com/xtls/xray-core` for config types + gRPC stats/handler/router API. MTProto inbounds run a second managed child — the `mtg-multi` binary diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79769bc05..a3217696d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for taking the time to contribute to 3x-ui. This guide gets a development ## Prerequisites -- **Go 1.26+** (the version pinned in `go.mod`) +- **Go 1.27+** (the version pinned in `go.mod`) - **Node.js 24 LTS** (the version pinned in `.nvmrc`) and npm 10+ (for the React frontend) - **Git** - **A C compiler** — required by the CGo SQLite driver (`github.com/mattn/go-sqlite3`). Linux and macOS already ship one; for Windows see below. diff --git a/Dockerfile b/Dockerfile index 2e889987c..0a156241f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN npm run build # ======================================================== # Stage: Builder # ======================================================== -FROM golang:1.26-alpine AS builder +FROM golang:1.27-alpine AS builder WORKDIR /app ARG TARGETARCH diff --git a/bot_context_test.go b/bot_context_test.go index 7944f1789..89d04676d 100644 --- a/bot_context_test.go +++ b/bot_context_test.go @@ -37,8 +37,8 @@ func section(t *testing.T, doc, from, to string) string { t.Fatalf("%s no longer contains the heading %q", botContextPath, from) } rest := doc[i+len(from):] - if j := strings.Index(rest, to); j >= 0 { - return rest[:j] + if before, _, ok := strings.Cut(rest, to); ok { + return before } return rest } diff --git a/docs/architecture.md b/docs/architecture.md index 59bb75c5b..52bf7aea1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -51,7 +51,7 @@ Two key ideas that explain most of the complexity: ## 2. Tech stack -**Backend (Go 1.26):** +**Backend (Go 1.27):** - Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip. - ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`). diff --git a/go.mod b/go.mod index 65da84e49..62279138d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mhsanaei/3x-ui/v3 -go 1.26.6 +go 1.27.0 require ( github.com/gin-contrib/gzip v1.2.6 @@ -13,10 +13,12 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 + github.com/klauspost/compress v1.19.2 github.com/mattn/go-sqlite3 v1.14.49 github.com/mymmrac/telego v1.11.1 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 + github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil/v4 v4.26.7 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e @@ -25,9 +27,11 @@ require ( github.com/xtls/xray-core v1.260327.1-0.20260728075948-5ca6f4b7d4dc go.uber.org/atomic v1.11.0 golang.org/x/crypto v0.55.0 + golang.org/x/net v0.58.0 golang.org/x/sys v0.47.0 golang.org/x/text v0.41.0 google.golang.org/grpc v1.83.0 + google.golang.org/protobuf v1.36.12 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/postgres v1.6.2 gorm.io/driver/sqlite v1.6.0 @@ -64,7 +68,6 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/juju/ratelimit v1.0.2 // indirect - github.com/klauspost/compress v1.19.2 github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/leodido/go-urn v1.5.0 // indirect github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect @@ -81,7 +84,6 @@ require ( github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.61.0 // indirect - github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/rogpeppe/go-internal v1.15.0 // indirect github.com/sagernet/sing v0.8.13 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect @@ -101,7 +103,6 @@ require ( golang.org/x/arch v0.30.0 // indirect golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect golang.org/x/mod v0.40.0 // indirect - golang.org/x/net v0.58.0 golang.org/x/sync v0.22.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.49.0 // indirect @@ -109,7 +110,6 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 // indirect golang.zx2c4.com/wireguard/windows v1.0.1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 // indirect - google.golang.org/protobuf v1.36.12 gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/internal/pia/catalog_test.go b/internal/pia/catalog_test.go index a39aa1f88..018add336 100644 --- a/internal/pia/catalog_test.go +++ b/internal/pia/catalog_test.go @@ -103,7 +103,7 @@ func TestCatalogCoalescesConcurrentRefresh(t *testing.T) { time.Sleep(time.Millisecond) } close(source.release) - for i := 0; i < 2; i++ { + for range 2 { if err := <-errc; err != nil { t.Fatal(err) } diff --git a/internal/pia/validation.go b/internal/pia/validation.go index da458dcb4..6e189149a 100644 --- a/internal/pia/validation.go +++ b/internal/pia/validation.go @@ -23,7 +23,7 @@ func validHostname(host string) bool { if host == "" || len(host) > 253 || net.ParseIP(host) != nil || strings.HasSuffix(host, ".") { return false } - for _, label := range strings.Split(host, ".") { + for label := range strings.SplitSeq(host, ".") { if label == "" || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { return false } diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index 92338c3cc..b50bccddb 100644 --- a/internal/sub/json_service.go +++ b/internal/sub/json_service.go @@ -387,10 +387,10 @@ func jsonMux(global, override string) string { } func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage { - outbound := Outbound{} + outbound := Outbound{ - outbound.Protocol = string(inbound.Protocol) - outbound.Tag = "proxy" + Protocol: string(inbound.Protocol), + Tag: "proxy"} if mux != "" { outbound.Mux = json_util.RawMessage(mux) } @@ -410,9 +410,9 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut } func (s *SubJsonService) genVless(subReq *SubService, inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage { - outbound := Outbound{} - outbound.Protocol = string(inbound.Protocol) - outbound.Tag = "proxy" + outbound := Outbound{ + Protocol: string(inbound.Protocol), + Tag: "proxy"} if mux != "" { outbound.Mux = json_util.RawMessage(mux) } @@ -490,10 +490,10 @@ func (s *SubJsonService) genServer(subReq *SubService, inbound *model.Inbound, s } func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, client model.Client, mux string) json_util.RawMessage { - outbound := Outbound{} + outbound := Outbound{ - outbound.Protocol = string(inbound.Protocol) - outbound.Tag = "proxy" + Protocol: string(inbound.Protocol), + Tag: "proxy"} if mux != "" { outbound.Mux = json_util.RawMessage(mux) diff --git a/internal/sub/mutation_audit_test.go b/internal/sub/mutation_audit_test.go index b5fe52946..dc3a3a7dd 100644 --- a/internal/sub/mutation_audit_test.go +++ b/internal/sub/mutation_audit_test.go @@ -25,8 +25,9 @@ func initMutDB(t *testing.T) { t.Cleanup(func() { _ = database.CloseDB() }) } +//go:fix inline func externalLinkEnabled(v bool) *bool { - return &v + return new(v) } // --- json_service.go:40 — rules are merged into routing only when non-empty --- @@ -312,7 +313,7 @@ func TestGetClientExternalLinksBySubId(t *testing.T) { if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://a", Remark: "first", SortIndex: 1}).Error; err != nil { t.Fatalf("seed link a: %v", err) } - if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://disabled", Remark: "disabled", Enable: externalLinkEnabled(false), SortIndex: 3}).Error; err != nil { + if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://disabled", Remark: "disabled", Enable: new(false), SortIndex: 3}).Error; err != nil { t.Fatalf("seed disabled link: %v", err) } if err := db.Create(&model.ClientExternalLink{ClientId: rec.Id, Kind: model.ExternalLinkKindLink, Value: "trojan://expired", Remark: "expired", ExpiryTime: time.Now().Add(-time.Hour).UnixMilli(), SortIndex: 4}).Error; err != nil { diff --git a/internal/sub/remote_routing.go b/internal/sub/remote_routing.go index c1c37eb57..dbfe9854e 100644 --- a/internal/sub/remote_routing.go +++ b/internal/sub/remote_routing.go @@ -370,8 +370,8 @@ func normalizeHappRouting(body []byte) (string, error) { payload := "" for _, prefix := range []string{"happ://routing/onadd/", "happ://routing/add/"} { - if strings.HasPrefix(text, prefix) { - payload = strings.TrimPrefix(text, prefix) + if after, ok := strings.CutPrefix(text, prefix); ok { + payload = after break } } diff --git a/internal/web/controller/api.go b/internal/web/controller/api.go index f189ae4e9..edab090cc 100644 --- a/internal/web/controller/api.go +++ b/internal/web/controller/api.go @@ -155,11 +155,11 @@ func (a *APIController) enforceTokenScope(c *gin.Context) { func relAPIPath(fullPath string) string { const marker = "/panel/api" - i := strings.Index(fullPath, marker) - if i < 0 { + _, after, ok := strings.Cut(fullPath, marker) + if !ok { return "" } - return fullPath[i+len(marker):] + return after } // initRouter sets up the API routes for inbounds, server, and other endpoints. diff --git a/internal/web/job/ip_limit_allowlist.go b/internal/web/job/ip_limit_allowlist.go index a9f3475ea..d463deb32 100644 --- a/internal/web/job/ip_limit_allowlist.go +++ b/internal/web/job/ip_limit_allowlist.go @@ -2,6 +2,7 @@ package job import ( "net/netip" + "slices" "strings" ) @@ -16,7 +17,7 @@ type ipLimitAllowlist struct { // skipped: the validator uses these same rules, so only a hand-edited DB differs. func parseIpLimitAllowlist(raw string) ipLimitAllowlist { var list ipLimitAllowlist - for _, field := range strings.Split(raw, ",") { + for field := range strings.SplitSeq(raw, ",") { field = strings.TrimSpace(field) if field == "" { continue @@ -52,10 +53,8 @@ func (l ipLimitAllowlist) contains(ip string) bool { return false } addr = addr.Unmap() - for _, allowed := range l.addrs { - if allowed == addr { - return true - } + if slices.Contains(l.addrs, addr) { + return true } for _, prefix := range l.prefixes { if prefix.Contains(addr) { diff --git a/internal/web/network/auto_https_conn.go b/internal/web/network/auto_https_conn.go index 3dee0954c..8e59d875d 100644 --- a/internal/web/network/auto_https_conn.go +++ b/internal/web/network/auto_https_conn.go @@ -46,8 +46,8 @@ func (c *AutoHttpsConn) readRequest() bool { } resp := http.Response{ Header: http.Header{}, - } - resp.StatusCode = http.StatusTemporaryRedirect + + StatusCode: http.StatusTemporaryRedirect} location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI) resp.Header.Set("Location", location) _ = resp.Write(c.Conn) diff --git a/internal/web/service/client_external_link_test.go b/internal/web/service/client_external_link_test.go index 814079e9c..a83b74dbf 100644 --- a/internal/web/service/client_external_link_test.go +++ b/internal/web/service/client_external_link_test.go @@ -7,10 +7,6 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) -func externalLinkBool(v bool) *bool { - return &v -} - func TestSetExternalLinksPersistsEnableState(t *testing.T) { setupBulkDB(t) db := database.GetDB() @@ -22,8 +18,8 @@ func TestSetExternalLinksPersistsEnableState(t *testing.T) { } if err := svc.SetExternalLinksForRecord(rec.Id, []ExternalLinkInput{ - {Kind: model.ExternalLinkKindLink, Value: "trojan://pw@example.com:443#on", Remark: "Primary", Enable: externalLinkBool(true), ExpiryTime: 1767225600000}, - {Kind: model.ExternalLinkKindSubscription, Value: "https://provider.example/sub", Remark: "Provider", Enable: externalLinkBool(false), NamePrefix: "[zjh] "}, + {Kind: model.ExternalLinkKindLink, Value: "trojan://pw@example.com:443#on", Remark: "Primary", Enable: new(true), ExpiryTime: 1767225600000}, + {Kind: model.ExternalLinkKindSubscription, Value: "https://provider.example/sub", Remark: "Provider", Enable: new(false), NamePrefix: "[zjh] "}, {Kind: model.ExternalLinkKindLink, Value: "trojan://pw@example.net:443#default"}, }); err != nil { t.Fatalf("set external links: %v", err) @@ -76,7 +72,7 @@ func TestSetExternalLinksPreservesFetchStatus(t *testing.T) { } if err := svc.SetExternalLinksForRecord(rec.Id, []ExternalLinkInput{ - {Kind: row.Kind, Value: row.Value, Remark: "new", Enable: externalLinkBool(true)}, + {Kind: row.Kind, Value: row.Value, Remark: "new", Enable: new(true)}, }); err != nil { t.Fatalf("set external links: %v", err) } diff --git a/internal/web/service/client_test.go b/internal/web/service/client_test.go index 811b6b284..1f04f4d2b 100644 --- a/internal/web/service/client_test.go +++ b/internal/web/service/client_test.go @@ -4,15 +4,14 @@ import ( "encoding/json" "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" ) func TestClientWithAttachmentsMarshalJSONIncludesExtras(t *testing.T) { c := ClientWithAttachments{ - ClientRecord: model.ClientRecord{Id: 1, Email: "alice@example.com"}, - InboundIds: []int{3, 5}, - Traffic: &xray.ClientTraffic{Email: "alice@example.com", Up: 1024, Down: 4096, Enable: true}, + Id: 1, Email: "alice@example.com", + InboundIds: []int{3, 5}, + Traffic: &xray.ClientTraffic{Email: "alice@example.com", Up: 1024, Down: 4096, Enable: true}, } out, err := json.Marshal(c) if err != nil { @@ -39,8 +38,8 @@ func TestClientWithAttachmentsMarshalJSONIncludesExtras(t *testing.T) { func TestClientWithAttachmentsMarshalJSONOmitsAbsentTraffic(t *testing.T) { c := ClientWithAttachments{ - ClientRecord: model.ClientRecord{Id: 1, Email: "bob@example.com"}, - InboundIds: nil, + Id: 1, Email: "bob@example.com", + InboundIds: nil, } out, err := json.Marshal(c) if err != nil { diff --git a/internal/web/service/inbound_autorenew_calendar_test.go b/internal/web/service/inbound_autorenew_calendar_test.go index 8eaffe61a..d48aa6b39 100644 --- a/internal/web/service/inbound_autorenew_calendar_test.go +++ b/internal/web/service/inbound_autorenew_calendar_test.go @@ -118,7 +118,7 @@ func TestAutoRenewClients_CalendarModeClampsShortMonths(t *testing.T) { func firstBillingMidnightAfter(t *testing.T, from time.Time, day int, loc *time.Location) time.Time { t.Helper() cur := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, loc) - for i := 0; i < 400; i++ { + for range 400 { cur = cur.AddDate(0, 0, 1) want := day if last := time.Date(cur.Year(), cur.Month()+1, 0, 0, 0, 0, 0, loc).Day(); want > last { diff --git a/internal/web/service/setting_mtls_test.go b/internal/web/service/setting_mtls_test.go index 841f0e56b..55d28c504 100644 --- a/internal/web/service/setting_mtls_test.go +++ b/internal/web/service/setting_mtls_test.go @@ -187,7 +187,7 @@ func TestEnsureMasterClientCertConcurrentFirstUseMintsOneCredential(t *testing.T } close(start) var first crypto.CertKeyPEM - for i := 0; i < callers; i++ { + for i := range callers { credential := <-results if err := <-errs; err != nil { t.Fatalf("caller %d: %v", i, err) diff --git a/internal/xray/geodata/query.go b/internal/xray/geodata/query.go index 129e177a1..16e006582 100644 --- a/internal/xray/geodata/query.go +++ b/internal/xray/geodata/query.go @@ -103,10 +103,7 @@ func sliceBounds(total, offset, limit int) (int, int) { if limit <= 0 || limit > MaxPageSize { limit = MaxPageSize } - to := offset + limit - if to > total { - to = total - } + to := min(offset+limit, total) return offset, to }