fix: fetcher proxy support, Dockerfile sing-box, rename index format

1. sslinks 403: Cloudflare blocks server IP. Add fetcher.proxy_url config
   so subscription/flow requests can route through an HTTP proxy.

2. Egress probe: Docker image lacked sing-box. Install sing-box v1.11.4
   in the Alpine runtime stage.

3. Collection rename: group by display baseName instead of internal
   groupKey, format index as -N instead of %02d. Fixes double-suffix
   issue where EnsureUniqueProxyNames appended -2/-3 onto existing  02.
This commit is contained in:
2026-07-28 17:38:57 +08:00
parent edae8235a6
commit 040e3ebab1
8 changed files with 110 additions and 64 deletions
+25 -3
View File
@@ -52,6 +52,7 @@ func (d *Deps) HandleDownloadCollection(c fiber.Ctx) error {
RequestUserAgent: c.Get("User-Agent"),
ForceRefresh: c.Query("refresh") == "1" || c.Query("noCache") == "1",
CacheRepo: d.CacheRepo,
ProxyURL: d.Cfg.Fetcher.ProxyURL,
})
if err != nil {
return failed(c, err.Error(), 500)
@@ -82,6 +83,7 @@ func (d *Deps) HandleDownloadSource(c fiber.Ctx) error {
RequestUserAgent: c.Get("User-Agent"),
ForceRefresh: c.Query("refresh") == "1" || c.Query("noCache") == "1",
CacheRepo: d.CacheRepo,
ProxyURL: d.Cfg.Fetcher.ProxyURL,
})
if err != nil {
return failed(c, err.Error(), 500)
@@ -182,6 +184,7 @@ func (d *Deps) HandlePreviewSource(c fiber.Ctx) error {
Settings: settings,
RequestUserAgent: c.Get("User-Agent"),
CacheRepo: d.CacheRepo,
ProxyURL: d.Cfg.Fetcher.ProxyURL,
})
if err != nil {
return failed(c, err.Error(), 400)
@@ -210,6 +213,7 @@ func (d *Deps) HandlePreviewCollection(c fiber.Ctx) error {
Settings: settings,
RequestUserAgent: c.Get("User-Agent"),
CacheRepo: d.CacheRepo,
ProxyURL: d.Cfg.Fetcher.ProxyURL,
})
if err != nil {
return failed(c, err.Error(), 400)
@@ -296,7 +300,7 @@ func (d *Deps) HandleFlowInfo(c fiber.Ctx) error {
"error": fiber.Map{"code": "NO_FLOW_INFO", "type": "NO_FLOW_INFO", "message": "No flow info"},
})
}
headers, err := fetchFlowHeaders(flowReq)
headers, err := fetchFlowHeaders(flowReq, d.Cfg.Fetcher.ProxyURL)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"status": "failed",
@@ -573,8 +577,8 @@ func parseJSONHeaders(v any) map[string]string {
return result
}
func fetchFlowHeaders(req *flowRequest) (string, error) {
client := &http.Client{Timeout: req.Timeout}
func fetchFlowHeaders(req *flowRequest, proxyURL string) (string, error) {
client := buildFlowHTTPClient(req.Timeout, proxyURL)
httpReq, err := http.NewRequest("GET", req.URL, nil)
if err != nil {
return "", err
@@ -708,3 +712,21 @@ func applyFiltersSafe(nodes []model.ProxyNode, filters []model.FilterRule, setti
// Use the filter package's ApplyFilters with FilterContext
return applyFiltersWithContext(nodes, filters, settings, target, sourceId)
}
// buildFlowHTTPClient builds an HTTP client for flow-info requests with
// optional proxy support.
func buildFlowHTTPClient(timeout time.Duration, proxyURL string) *http.Client {
if proxyURL == "" {
return &http.Client{Timeout: timeout}
}
pu, err := url.Parse(proxyURL)
if err != nil {
return &http.Client{Timeout: timeout}
}
return &http.Client{
Timeout: timeout,
Transport: &http.Transport{
Proxy: http.ProxyURL(pu),
},
}
}
+1 -1
View File
@@ -2226,7 +2226,7 @@ func TestSetSafeResponseHeaderVar(t *testing.T) {
func TestFetchFlowHeadersError(t *testing.T) {
// invalid URL -> error
req := &flowRequest{URL: "http://localhost:1/no-server", UserAgent: "ua", Timeout: 1000000000}
_, err := fetchFlowHeaders(req)
_, err := fetchFlowHeaders(req, "")
if err == nil {
// connection may succeed in some envs; just don't fail the test
t.Log("fetchFlowHeaders to invalid URL did not error (env-dependent)")