Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ loop:
if err := c.StopDNSListener(); err != nil {
log.Warnf("[Reload] Failed to stop old DNS listener: %v", err)
}

log.Warnln("[Reload] Load new control plane")
newC, err := newControlPlane(log, obj, dnsCache, newConf, externGeoDataDirs)
if err != nil {
Expand Down Expand Up @@ -405,7 +405,7 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
Timeout: 30 * time.Second,
}
for _, sub := range conf.Subscription {
tag, nodes, err := subscription.ResolveSubscription(log, &client, filepath.Dir(cfgFile), string(sub))
tag, nodes, err := subscription.ResolveSubscription(log, &client, filepath.Dir(cfgFile), string(sub), conf.Global.SubscriptionUserAgent)
if err != nil {
log.Warnf(`failed to resolve subscription "%v": %v`, sub, err)
resolvingfailed = true
Expand Down
41 changes: 39 additions & 2 deletions common/subscription/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,24 @@ func ResolveFile(u *url.URL, configDir string) (b []byte, err error) {
return bytes.TrimSpace(b), err
}

func ResolveSubscription(log *logrus.Logger, client *http.Client, configDir string, subscription string) (tag string, nodes []string, err error) {
// parseUAFromFragment extracts the ua parameter from URL fragment.
// URL fragment format: #ua=xxx or #ua=xxx&other=yyy
// The value is URL-decoded to handle encoded characters (e.g., %20 for space).
func parseUAFromFragment(fragment string) string {
for _, part := range strings.Split(fragment, "&") {
if strings.HasPrefix(part, "ua=") {
ua := strings.TrimPrefix(part, "ua=")
// URL decode the value to handle encoded characters
if decoded, err := url.QueryUnescape(ua); err == nil {
return decoded
}
return ua
}
}
return ""
}

func ResolveSubscription(log *logrus.Logger, client *http.Client, configDir string, subscription string, globalUA string) (tag string, nodes []string, err error) {
/// Get tag.
tag, subscription = common.GetTagFromLinkLikePlaintext(subscription)

Expand All @@ -149,6 +166,24 @@ func ResolveSubscription(log *logrus.Logger, client *http.Client, configDir stri
return tag, nil, fmt.Errorf("failed to parse subscription \"%v\": %w", subscription, err)
}
log.Debugf("ResolveSubscription: %v", subscription)

// Extract per-subscription UA from fragment before clearing it
subscriptionUA := parseUAFromFragment(u.Fragment)

// Determine User-Agent: subscription-level > global > default
defaultUA := fmt.Sprintf("dae/%v (like v2rayA/1.0 WebRequestHelper) (like v2rayN/1.0 WebRequestHelper)", config.Version)
userAgent := defaultUA
if globalUA != "" {
userAgent = globalUA
}
if subscriptionUA != "" {
userAgent = subscriptionUA
}

// Clear fragment from URL after extracting UA (fragment should not be sent to server)
u.Fragment = ""
subscription = u.String()

var (
b []byte
req *http.Request
Expand All @@ -173,11 +208,13 @@ func ResolveSubscription(log *logrus.Logger, client *http.Client, configDir stri
break
default:
}

req, err = http.NewRequest("GET", subscription, nil)
if err != nil {
return "", nil, err
}
req.Header.Set("User-Agent", fmt.Sprintf("dae/%v (like v2rayA/1.0 WebRequestHelper) (like v2rayN/1.0 WebRequestHelper)", config.Version))

req.Header.Set("User-Agent", userAgent)
resp, err = client.Do(req)
if err != nil {
if persistToFile {
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type Global struct {
BandwidthMaxTx string `mapstructure:"bandwidth_max_tx" default:"0"`
BandwidthMaxRx string `mapstructure:"bandwidth_max_rx" default:"0"`
UDPHopInterval time.Duration `mapstructure:"udphop_interval" default:"30s"`
SubscriptionUserAgent string `mapstructure:"subscription_user_agent"`
}

type Utls struct {
Expand Down
1 change: 1 addition & 0 deletions config/desc.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ var GlobalDesc = Desc{
"tls_implementation": "TLS implementation. \"tls\" is to use Go's crypto/tls. \"utls\" is to use uTLS, which can imitate browser's Client Hello.",
"utls_imitate": "The Client Hello ID for uTLS to imitate. This takes effect only if tls_implementation is utls. See more: https://github.com/daeuniverse/dae/blob/331fa23c16/component/outbound/transport/tls/utls.go#L17",
"mptcp": "Enable Multipath TCP. If is true, dae will try to use MPTCP to connect all nodes, but it will only take effects when the node supports MPTCP. It can use for load balance and failover to multiple interfaces and IPs.",
"subscription_user_agent": "Custom User-Agent for fetching subscriptions. If not set, dae will use its default User-Agent. Per-subscription User-Agent can be set via URL fragment (e.g., https://example.com/sub#ua=V2rayA).",
}

var DnsDesc = Desc{
Expand Down
9 changes: 9 additions & 0 deletions example.dae
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ global {
# For IPv6 DNS servers, the format is "[ipv6]:port", such as "[2001:4860:4860::8888]:53".
# Fallback resolver will always be available, even if you do not specify it in the configuration file.
# fallback_resolver: '8.8.8.8:53'

# Custom User-Agent for fetching subscriptions. If not set, dae will use its default User-Agent.
# Some subscription providers return different node formats based on User-Agent.
# You can also set per-subscription User-Agent via URL fragment (e.g., https://example.com/sub#ua=V2rayA).
# Per-subscription User-Agent takes priority over this global setting.
# subscription_user_agent: 'V2rayA/2.2.0'
}

# Subscriptions defined here will be resolved as nodes and merged as a part of the global node pool.
Expand All @@ -153,6 +159,9 @@ subscription {
# This file will serve as a fallback when fetching the subscription via a link fails.
# It will be updated automatically once the fetch is successful.
persist_sub: 'https-file://www.example.com/persist_sub/link'

# Per-subscription User-Agent via URL fragment (overrides global subscription_user_agent).
# sub_with_ua: 'https://example.com/sub#ua=V2rayA/2.2.0'
}

# Nodes defined here will be merged as a part of the global node pool.
Expand Down
Loading