fix(sub): fetch operator-chosen private and proxied external subscriptions again

Guarding the external subscription fetch against a provider's redirects
also refused the URL the operator configured when it names a LAN or
loopback address, and the guarded transport dropped the host's proxy
setting, so a panel merging another panel on its own network, or one
reaching the Internet only through a proxy, silently lost those links.
A provider listing more than 1000 links was refused whole as well.

A URL that names a private address itself is now fetched, while a
hostname still is not, since the provider controls what its DNS answers;
public providers stay held to public addresses through every redirect.
A fetch the host's proxy setting covers goes through that proxy, and a
long list is cut to its first 1000 links instead of refused.
This commit is contained in:
MHSanaei
2026-11-01 10:30:26 +01:00
parent 255c658468
commit 4cddcff12b
2 changed files with 80 additions and 8 deletions
+37 -3
View File
@@ -4,7 +4,9 @@ import (
"context"
"encoding/base64"
"io"
"net"
"net/http"
"net/url"
"slices"
"strings"
"sync"
@@ -40,6 +42,35 @@ var subscriptionHTTPClient = &http.Client{
},
}
// subscriptionProxy is the host's proxy setting: the proxy is the operator's and does the
// dialling, so a proxied fetch goes through it as the default transport did.
var subscriptionProxy = http.ProxyFromEnvironment
var proxiedSubscriptionHTTPClient = &http.Client{
Timeout: 6 * time.Second,
Transport: &http.Transport{
Proxy: func(r *http.Request) (*url.URL, error) { return subscriptionProxy(r) },
TLSHandshakeTimeout: 5 * time.Second,
MaxResponseHeaderBytes: 64 << 10,
IdleConnTimeout: 90 * time.Second,
},
}
// subscriptionClientFor picks the proxied client when the host's proxy setting covers req.
func subscriptionClientFor(req *http.Request) *http.Client {
if proxyURL, err := subscriptionProxy(req); err == nil && proxyURL != nil {
return proxiedSubscriptionHTTPClient
}
return subscriptionHTTPClient
}
// operatorChosePrivateHost reports a URL naming a private address itself: the operator picked
// it. A hostname does not count, since the provider controls what its DNS answers.
func operatorChosePrivateHost(u *url.URL) bool {
ip := net.ParseIP(u.Hostname())
return ip != nil && netsafe.IsBlockedIP(ip)
}
type subscriptionCacheEntry struct {
links []string
fetchedAt time.Time
@@ -161,13 +192,16 @@ func doFetchSubscriptionLinks(rawURL string) ([]string, error) {
if err != nil {
return nil, service.WithoutRequestURL(err)
}
// A provider on the operator's own network is fetched; the hops a public provider picks by
// redirecting stay held to public addresses.
req = req.WithContext(netsafe.ContextWithAllowPrivate(req.Context(), operatorChosePrivateHost(req.URL)))
// Some providers gate the link body on a known client User-Agent.
req.Header.Set("User-Agent", externalSubUserAgent())
// A 3x-ui donor with an HWID limit answers 404 when the header is empty (#6559).
if hwid := service.ExternalSubscriptionHwid(); hwid != "" {
req.Header.Set("X-HWID", hwid)
}
resp, err := subscriptionHTTPClient.Do(req)
resp, err := subscriptionClientFor(req).Do(req)
if err != nil {
return nil, service.WithoutRequestURL(err)
}
@@ -184,7 +218,8 @@ func doFetchSubscriptionLinks(rawURL string) ([]string, error) {
}
links := decodeSubscriptionBody(body)
if len(links) > subscriptionMaxLinks {
return nil, errSubscriptionTooManyLinks
logger.Warningf("sub: an external subscription lists %d share links; serving the first %d", len(links), subscriptionMaxLinks)
links = links[:subscriptionMaxLinks]
}
return links, nil
}
@@ -192,7 +227,6 @@ func doFetchSubscriptionLinks(rawURL string) ([]string, error) {
var (
errBadStatus = &subError{"non-2xx subscription response"}
errSubscriptionBodyTooLarge = &subError{"subscription response body exceeds size limit"}
errSubscriptionTooManyLinks = &subError{"subscription lists more than 1000 share links"}
)
// externalSubUserAgent returns the panel setting for external subscription