V2.0.1: listen override fix, sing-box core fetch, probe settings page + fast report, spx removal, core mem env
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Docs CI / build (push) Waiting to run
Docs Deploy (GitHub Pages) / build (push) Waiting to run
Docs Deploy (GitHub Pages) / deploy (push) Blocked by required conditions
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Docs CI / build (push) Waiting to run
Docs Deploy (GitHub Pages) / build (push) Waiting to run
Docs Deploy (GitHub Pages) / deploy (push) Blocked by required conditions
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
- 0: sing-box gen overrides fragment listen/port from the inbound row (empty listen -> "::"), Update/Add recompute the stored fragment; non-loopback E2E against the real core binary. - 1: agent fetches the sing-box core from the Gitea release when missing, self-checks it, and only then latches the switch. - 2: node list shows the sing-box core version (heartbeat field, persisted on the node row), xray version as fallback. - 3: public-page fallback returns 404 to anonymous visitors when the page is disabled. - 4: settings restart hint bar removed, dead i18n keys cleaned. - 5: two PUT calls missing JSON headers fixed (audit: only those two), batch probe-task creation endpoint + UI. - 6: cf-probe retirement card and coverage pipeline removed. - 7: generated vless links no longer carry spx (panel export + sub); parser still accepts spx; golden fixtures updated. - probe batch: dedicated probe settings tab (public-page settings, tasks, per-node meta), retention_days editable, per-node traffic calibration + stat-interface pinning, runtime metrics split into a lightweight fast report (default 3s, 1-60s adjustable). - 8: sing-box core processes start with GOMEMLIMIT=32MiB, GOGC=50. - 9: dead /getTwoFactorEnable docs entries removed (frontend call and contract entry already gone at base; verified by grep).
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
)
|
||||
|
||||
// ITEM V2.0.1 ①: agents upgraded through the panel's one-click
|
||||
// update swap only their own binary — a node installed before the
|
||||
// V2 switchover can hold no sing-box core at all, and applying the
|
||||
// master's sing-box config there would strand it (the switch retires
|
||||
// xray first). Before any switch is attempted, a missing core is
|
||||
// fetched from the same Gitea release install-agent.sh installs
|
||||
// from (the NexusGate repo's dedicated sing-box-v1.13.21 release),
|
||||
// self-checked, and only then does the switch proceed. Any failure
|
||||
// leaves the node exactly as it was: xray keeps running, the fetch
|
||||
// retries on a later poll (throttled), and the failure is visible in
|
||||
// the agent log and — through the still-empty singboxVersion — on
|
||||
// the master's node list.
|
||||
|
||||
// singboxCoreVersion is the pinned self-built core release, the
|
||||
// same pin install-agent.sh carries (SINGBOX_VER).
|
||||
const singboxCoreVersion = "1.13.21"
|
||||
|
||||
// singboxCoreBaseURL is the release download base. A package
|
||||
// variable so tests can point it at a local server.
|
||||
var singboxCoreBaseURL = "https://git.881218.xyz/rogue/NexusGate/releases/download/sing-box-v" + singboxCoreVersion
|
||||
|
||||
// singboxFetchBackoff suppresses immediate re-downloads after a
|
||||
// failed fetch: a node whose route to the release host is broken
|
||||
// must not hammer it every config poll.
|
||||
const singboxFetchBackoff = 10 * time.Minute
|
||||
|
||||
// singboxCoreAssetName is the release asset for this agent's
|
||||
// architecture (the release publishes linux amd64/arm64 only, the
|
||||
// same set install-agent.sh supports).
|
||||
func singboxCoreAssetName() (string, error) {
|
||||
switch runtime.GOARCH {
|
||||
case "amd64", "arm64":
|
||||
return "nexusgate-sing-box-linux-" + runtime.GOARCH, nil
|
||||
}
|
||||
return "", fmt.Errorf("no sing-box core asset for architecture %s", runtime.GOARCH)
|
||||
}
|
||||
|
||||
// singboxCoreReady reports whether the node can run sing-box: the
|
||||
// core is on disk, or a fetch just installed it. Callers on the
|
||||
// config path use it as the switch gate.
|
||||
func (a *Agent) singboxCoreReady() bool {
|
||||
if singboxBinaryPath() != "" {
|
||||
return true
|
||||
}
|
||||
_ = a.fetchSingboxCoreThrottled()
|
||||
return singboxBinaryPath() != ""
|
||||
}
|
||||
|
||||
// fetchSingboxCoreThrottled runs one fetch attempt unless the last
|
||||
// failure is still inside the backoff window. A failed attempt is
|
||||
// logged once (here, not per poll) and stays visible on the master
|
||||
// through the agent's still-empty singboxVersion telemetry.
|
||||
func (a *Agent) fetchSingboxCoreThrottled() error {
|
||||
a.sbFetchMu.Lock()
|
||||
defer a.sbFetchMu.Unlock()
|
||||
if !a.sbFetchFailedAt.IsZero() && time.Since(a.sbFetchFailedAt) < singboxFetchBackoff {
|
||||
return fmt.Errorf("sing-box core fetch in backoff after failure at %s", a.sbFetchFailedAt.Format(time.RFC3339))
|
||||
}
|
||||
if err := a.fetchSingboxCore(); err != nil {
|
||||
a.sbFetchFailedAt = time.Now()
|
||||
logger.Warningf("sing-box core fetch failed, staying on the current runtime (retry after backoff): %v", err)
|
||||
return err
|
||||
}
|
||||
a.sbFetchFailedAt = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchSingboxCore downloads the pinned core into the bin folder,
|
||||
// self-checks it (a runnable `version`, mirroring the install
|
||||
// script's closing self-check), and only then moves it into place
|
||||
// and writes the install-time version record the telemetry reads.
|
||||
func (a *Agent) fetchSingboxCore() error {
|
||||
asset, err := singboxCoreAssetName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
binDir := config.GetBinFolderPath()
|
||||
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create bin folder: %w", err)
|
||||
}
|
||||
dest := filepath.Join(binDir, singboxBinaryName())
|
||||
tmp := dest + ".download"
|
||||
_ = os.Remove(tmp)
|
||||
|
||||
url := strings.TrimSuffix(singboxCoreBaseURL, "/") + "/" + asset
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var transport http.RoundTripper
|
||||
if a.httpClient != nil {
|
||||
transport = a.httpClient.Transport
|
||||
}
|
||||
client := &http.Client{Transport: transport, Timeout: 5 * time.Minute}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download sing-box core: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("download sing-box core: status %d", resp.StatusCode)
|
||||
}
|
||||
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create sing-box core download: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("download sing-box core: %w", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("download sing-box core: %w", err)
|
||||
}
|
||||
|
||||
// Self-check before anything is moved into place: the binary
|
||||
// must actually run on this node (right architecture, not a
|
||||
// truncated or error-page download).
|
||||
vctx, vcancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
out, verr := exec.CommandContext(vctx, tmp, "version").CombinedOutput()
|
||||
vcancel()
|
||||
if verr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("sing-box core self-check failed: %v (%s)", verr, strings.TrimSpace(string(out)))
|
||||
}
|
||||
if len(strings.TrimSpace(string(out))) == 0 {
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("sing-box core self-check failed: version produced no output")
|
||||
}
|
||||
|
||||
if err := os.Rename(tmp, dest); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("install sing-box core: %w", err)
|
||||
}
|
||||
// The version record is what telemetry reports for the self-built
|
||||
// core (its `version` output carries no stamp) — same convention
|
||||
// as install-agent.sh.
|
||||
_ = os.WriteFile(filepath.Join(binDir, singboxVersionRecord), []byte(singboxCoreVersion+"\n"), 0o644)
|
||||
logger.Infof("sing-box core %s fetched from the release and self-checked; ready to switch", singboxCoreVersion)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user