diff --git a/.github/update.log b/.github/update.log
index becb45fb39..a62f727a95 100644
--- a/.github/update.log
+++ b/.github/update.log
@@ -1135,3 +1135,4 @@ Update On Thu Sep 25 20:42:08 CEST 2025
Update On Fri Sep 26 20:35:20 CEST 2025
Update On Sat Sep 27 20:33:29 CEST 2025
Update On Sun Sep 28 20:32:41 CEST 2025
+Update On Mon Sep 29 20:38:41 CEST 2025
diff --git a/clash-meta/component/resolver/local.go b/clash-meta/component/resolver/service.go
similarity index 73%
rename from clash-meta/component/resolver/local.go
rename to clash-meta/component/resolver/service.go
index e8505118b4..8b8f115842 100644
--- a/clash-meta/component/resolver/local.go
+++ b/clash-meta/component/resolver/service.go
@@ -6,15 +6,15 @@ import (
D "github.com/miekg/dns"
)
-var DefaultLocalServer LocalServer
+var DefaultService Service
-type LocalServer interface {
+type Service interface {
ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error)
}
// ServeMsg with a dns.Msg, return resolve dns.Msg
func ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) {
- if server := DefaultLocalServer; server != nil {
+ if server := DefaultService; server != nil {
return server.ServeMsg(ctx, msg)
}
diff --git a/clash-meta/config/config.go b/clash-meta/config/config.go
index bcd3cc7ad2..dec0955b2d 100644
--- a/clash-meta/config/config.go
+++ b/clash-meta/config/config.go
@@ -146,6 +146,7 @@ type DNS struct {
PreferH3 bool
IPv6 bool
IPv6Timeout uint
+ UseHosts bool
UseSystemHosts bool
NameServer []dns.NameServer
Fallback []dns.NameServer
@@ -157,7 +158,6 @@ type DNS struct {
CacheAlgorithm string
CacheMaxSize int
FakeIPRange *fakeip.Pool
- Hosts *trie.DomainTrie[resolver.HostValue]
NameServerPolicy []dns.Policy
ProxyServerNameserver []dns.NameServer
DirectNameServer []dns.NameServer
@@ -680,7 +680,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
}
config.Hosts = hosts
- dnsCfg, err := parseDNS(rawCfg, hosts, ruleProviders)
+ dnsCfg, err := parseDNS(rawCfg, ruleProviders)
if err != nil {
return nil, err
}
@@ -1341,7 +1341,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro
return policy, nil
}
-func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) {
+func parseDNS(rawCfg *RawConfig, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) {
cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
@@ -1357,6 +1357,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
PreferH3: cfg.PreferH3,
IPv6Timeout: cfg.IPv6Timeout,
IPv6: cfg.IPv6,
+ UseHosts: cfg.UseHosts,
UseSystemHosts: cfg.UseSystemHosts,
EnhancedMode: cfg.EnhancedMode,
CacheAlgorithm: cfg.CacheAlgorithm,
@@ -1490,10 +1491,6 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
}
}
- if cfg.UseHosts {
- dnsCfg.Hosts = hosts
- }
-
return dnsCfg, nil
}
diff --git a/clash-meta/context/dns.go b/clash-meta/context/dns.go
index 1cc2067d8d..15143102ca 100644
--- a/clash-meta/context/dns.go
+++ b/clash-meta/context/dns.go
@@ -2,10 +2,10 @@ package context
import (
"context"
+
"github.com/metacubex/mihomo/common/utils"
"github.com/gofrs/uuid/v5"
- "github.com/miekg/dns"
)
const (
@@ -17,17 +17,15 @@ const (
type DNSContext struct {
context.Context
- id uuid.UUID
- msg *dns.Msg
- tp string
+ id uuid.UUID
+ tp string
}
-func NewDNSContext(ctx context.Context, msg *dns.Msg) *DNSContext {
+func NewDNSContext(ctx context.Context) *DNSContext {
return &DNSContext{
Context: ctx,
- id: utils.NewUUIDV4(),
- msg: msg,
+ id: utils.NewUUIDV4(),
}
}
diff --git a/clash-meta/dns/enhancer.go b/clash-meta/dns/enhancer.go
index 9ea3ae84ac..36923ca624 100644
--- a/clash-meta/dns/enhancer.go
+++ b/clash-meta/dns/enhancer.go
@@ -12,6 +12,7 @@ type ResolverEnhancer struct {
mode C.DNSMode
fakePool *fakeip.Pool
mapping *lru.LruCache[netip.Addr, string]
+ useHosts bool
}
func (h *ResolverEnhancer) FakeIPEnabled() bool {
@@ -103,7 +104,13 @@ func (h *ResolverEnhancer) StoreFakePoolState() {
}
}
-func NewEnhancer(cfg Config) *ResolverEnhancer {
+type EnhancerConfig struct {
+ EnhancedMode C.DNSMode
+ Pool *fakeip.Pool
+ UseHosts bool
+}
+
+func NewEnhancer(cfg EnhancerConfig) *ResolverEnhancer {
var fakePool *fakeip.Pool
var mapping *lru.LruCache[netip.Addr, string]
@@ -116,5 +123,6 @@ func NewEnhancer(cfg Config) *ResolverEnhancer {
mode: cfg.EnhancedMode,
fakePool: fakePool,
mapping: mapping,
+ useHosts: cfg.UseHosts,
}
}
diff --git a/clash-meta/dns/local.go b/clash-meta/dns/local.go
deleted file mode 100644
index 37b5d41b04..0000000000
--- a/clash-meta/dns/local.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package dns
-
-import (
- "context"
-
- D "github.com/miekg/dns"
-)
-
-type LocalServer struct {
- handler handler
-}
-
-// ServeMsg implement resolver.LocalServer ResolveMsg
-func (s *LocalServer) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) {
- return handlerWithContext(ctx, s.handler, msg)
-}
-
-func NewLocalServer(resolver *Resolver, mapper *ResolverEnhancer) *LocalServer {
- return &LocalServer{handler: NewHandler(resolver, mapper)}
-}
diff --git a/clash-meta/dns/middleware.go b/clash-meta/dns/middleware.go
index e6461e91a3..502a37e5e5 100644
--- a/clash-meta/dns/middleware.go
+++ b/clash-meta/dns/middleware.go
@@ -7,22 +7,22 @@ import (
"github.com/metacubex/mihomo/common/lru"
"github.com/metacubex/mihomo/component/fakeip"
- R "github.com/metacubex/mihomo/component/resolver"
+ "github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
- "github.com/metacubex/mihomo/context"
+ icontext "github.com/metacubex/mihomo/context"
"github.com/metacubex/mihomo/log"
D "github.com/miekg/dns"
)
type (
- handler func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error)
+ handler func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error)
middleware func(next handler) handler
)
-func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middleware {
+func withHosts(mapping *lru.LruCache[netip.Addr, string]) middleware {
return func(next handler) handler {
- return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
+ return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0]
if !isIPRequest(q) {
@@ -36,7 +36,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew
rr.Target = domain + "."
resp.Answer = append([]D.RR{rr}, resp.Answer...)
}
- record, ok := hosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA)
+ record, ok := resolver.DefaultHosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA)
if !ok {
if record != nil && record.IsDomain {
// replace request domain
@@ -88,7 +88,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew
return next(ctx, r)
}
- ctx.SetType(context.DNSTypeHost)
+ ctx.SetType(icontext.DNSTypeHost)
msg.SetRcode(r, D.RcodeSuccess)
msg.Authoritative = true
msg.RecursionAvailable = true
@@ -99,7 +99,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew
func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware {
return func(next handler) handler {
- return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
+ return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0]
if !isIPRequest(q) {
@@ -149,7 +149,7 @@ func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware {
func withFakeIP(fakePool *fakeip.Pool) middleware {
return func(next handler) handler {
- return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
+ return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0]
host := strings.TrimRight(q.Name, ".")
@@ -173,7 +173,7 @@ func withFakeIP(fakePool *fakeip.Pool) middleware {
msg := r.Copy()
msg.Answer = []D.RR{rr}
- ctx.SetType(context.DNSTypeFakeIP)
+ ctx.SetType(icontext.DNSTypeFakeIP)
setMsgTTL(msg, 1)
msg.SetRcode(r, D.RcodeSuccess)
msg.Authoritative = true
@@ -185,8 +185,8 @@ func withFakeIP(fakePool *fakeip.Pool) middleware {
}
func withResolver(resolver *Resolver) handler {
- return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
- ctx.SetType(context.DNSTypeRaw)
+ return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) {
+ ctx.SetType(icontext.DNSTypeRaw)
q := r.Question[0]
@@ -218,11 +218,11 @@ func compose(middlewares []middleware, endpoint handler) handler {
return h
}
-func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
- middlewares := []middleware{}
+func newHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
+ var middlewares []middleware
- if resolver.hosts != nil {
- middlewares = append(middlewares, withHosts(R.NewHosts(resolver.hosts), mapper.mapping))
+ if mapper.useHosts {
+ middlewares = append(middlewares, withHosts(mapper.mapping))
}
if mapper.mode == C.DNSFakeIP {
diff --git a/clash-meta/dns/resolver.go b/clash-meta/dns/resolver.go
index f5f69c5f4a..f7d4d42968 100644
--- a/clash-meta/dns/resolver.go
+++ b/clash-meta/dns/resolver.go
@@ -9,7 +9,6 @@ import (
"github.com/metacubex/mihomo/common/arc"
"github.com/metacubex/mihomo/common/lru"
"github.com/metacubex/mihomo/common/singleflight"
- "github.com/metacubex/mihomo/component/fakeip"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/trie"
C "github.com/metacubex/mihomo/constant"
@@ -40,7 +39,6 @@ type result struct {
type Resolver struct {
ipv6 bool
ipv6Timeout time.Duration
- hosts *trie.DomainTrie[resolver.HostValue]
main []dnsClient
fallback []dnsClient
fallbackDomainFilters []C.DomainMatcher
@@ -452,11 +450,8 @@ type Config struct {
DirectFollowPolicy bool
IPv6 bool
IPv6Timeout uint
- EnhancedMode C.DNSMode
FallbackIPFilter []C.IpMatcher
FallbackDomainFilter []C.DomainMatcher
- Pool *fakeip.Pool
- Hosts *trie.DomainTrie[resolver.HostValue]
Policy []Policy
CacheAlgorithm string
CacheMaxSize int
@@ -530,7 +525,6 @@ func NewResolver(config Config) (rs Resolvers) {
ipv6: config.IPv6,
main: cacheTransform(config.Main),
cache: config.newCache(),
- hosts: config.Hosts,
ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond,
}
r.defaultResolver = defaultResolver
@@ -541,7 +535,6 @@ func NewResolver(config Config) (rs Resolvers) {
ipv6: config.IPv6,
main: cacheTransform(config.ProxyServer),
cache: config.newCache(),
- hosts: config.Hosts,
ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond,
}
}
@@ -551,7 +544,6 @@ func NewResolver(config Config) (rs Resolvers) {
ipv6: config.IPv6,
main: cacheTransform(config.DirectServer),
cache: config.newCache(),
- hosts: config.Hosts,
ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond,
}
}
diff --git a/clash-meta/dns/server.go b/clash-meta/dns/server.go
index caf1c2891a..b1224c6212 100644
--- a/clash-meta/dns/server.go
+++ b/clash-meta/dns/server.go
@@ -1,13 +1,12 @@
package dns
import (
- stdContext "context"
- "errors"
+ "context"
"net"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/sockopt"
- "github.com/metacubex/mihomo/context"
+ "github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/log"
D "github.com/miekg/dns"
@@ -21,39 +20,32 @@ var (
)
type Server struct {
- handler handler
+ service resolver.Service
tcpServer *D.Server
udpServer *D.Server
}
// ServeDNS implement D.Handler ServeDNS
func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) {
- msg, err := handlerWithContext(stdContext.Background(), s.handler, r)
+ msg, err := s.service.ServeMsg(context.Background(), r)
if err != nil {
- D.HandleFailed(w, r)
+ m := new(D.Msg)
+ m.SetRcode(r, D.RcodeServerFailure)
+ // does not matter if this write fails
+ w.WriteMsg(m)
return
}
msg.Compress = true
w.WriteMsg(msg)
}
-func handlerWithContext(stdCtx stdContext.Context, handler handler, msg *D.Msg) (*D.Msg, error) {
- if len(msg.Question) == 0 {
- return nil, errors.New("at least one question is required")
- }
-
- ctx := context.NewDNSContext(stdCtx, msg)
- return handler(ctx, msg)
+func (s *Server) SetService(service resolver.Service) {
+ s.service = service
}
-func (s *Server) SetHandler(handler handler) {
- s.handler = handler
-}
-
-func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) {
- if addr == address && resolver != nil {
- handler := NewHandler(resolver, mapper)
- server.SetHandler(handler)
+func ReCreateServer(addr string, service resolver.Service) {
+ if addr == address && service != nil {
+ server.SetService(service)
return
}
@@ -67,10 +59,10 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) {
server.udpServer = nil
}
- server.handler = nil
+ server.service = nil
address = ""
- if addr == "" {
+ if addr == "" || service == nil {
return
}
@@ -87,8 +79,7 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) {
}
address = addr
- handler := NewHandler(resolver, mapper)
- server = &Server{handler: handler}
+ server = &Server{service: service}
go func() {
p, err := inbound.ListenPacket("udp", addr)
diff --git a/clash-meta/dns/service.go b/clash-meta/dns/service.go
new file mode 100644
index 0000000000..4a7c1bb2ea
--- /dev/null
+++ b/clash-meta/dns/service.go
@@ -0,0 +1,29 @@
+package dns
+
+import (
+ "context"
+ "errors"
+
+ "github.com/metacubex/mihomo/component/resolver"
+ icontext "github.com/metacubex/mihomo/context"
+ D "github.com/miekg/dns"
+)
+
+type Service struct {
+ handler handler
+}
+
+// ServeMsg implement [resolver.Service] ResolveMsg
+func (s *Service) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) {
+ if len(msg.Question) == 0 {
+ return nil, errors.New("at least one question is required")
+ }
+
+ return s.handler(icontext.NewDNSContext(ctx), msg)
+}
+
+var _ resolver.Service = (*Service)(nil)
+
+func NewService(resolver *Resolver, mapper *ResolverEnhancer) *Service {
+ return &Service{handler: newHandler(resolver, mapper)}
+}
diff --git a/clash-meta/hub/executor/executor.go b/clash-meta/hub/executor/executor.go
index fcf176e0e0..041e6fc36b 100644
--- a/clash-meta/hub/executor/executor.go
+++ b/clash-meta/hub/executor/executor.go
@@ -240,20 +240,18 @@ func updateDNS(c *config.DNS, generalIPv6 bool) {
if !c.Enable {
resolver.DefaultResolver = nil
resolver.DefaultHostMapper = nil
- resolver.DefaultLocalServer = nil
+ resolver.DefaultService = nil
resolver.ProxyServerHostResolver = nil
resolver.DirectHostResolver = nil
- dns.ReCreateServer("", nil, nil)
+ dns.ReCreateServer("", nil)
return
}
- cfg := dns.Config{
+
+ r := dns.NewResolver(dns.Config{
Main: c.NameServer,
Fallback: c.Fallback,
IPv6: c.IPv6 && generalIPv6,
IPv6Timeout: c.IPv6Timeout,
- EnhancedMode: c.EnhancedMode,
- Pool: c.FakeIPRange,
- Hosts: c.Hosts,
FallbackIPFilter: c.FallbackIPFilter,
FallbackDomainFilter: c.FallbackDomainFilter,
Default: c.DefaultNameserver,
@@ -263,19 +261,23 @@ func updateDNS(c *config.DNS, generalIPv6 bool) {
DirectFollowPolicy: c.DirectFollowPolicy,
CacheAlgorithm: c.CacheAlgorithm,
CacheMaxSize: c.CacheMaxSize,
- }
-
- r := dns.NewResolver(cfg)
- m := dns.NewEnhancer(cfg)
+ })
+ m := dns.NewEnhancer(dns.EnhancerConfig{
+ EnhancedMode: c.EnhancedMode,
+ Pool: c.FakeIPRange,
+ UseHosts: c.UseHosts,
+ })
// reuse cache of old host mapper
if old := resolver.DefaultHostMapper; old != nil {
m.PatchFrom(old.(*dns.ResolverEnhancer))
}
+ s := dns.NewService(r.Resolver, m)
+
resolver.DefaultResolver = r
resolver.DefaultHostMapper = m
- resolver.DefaultLocalServer = dns.NewLocalServer(r.Resolver, m)
+ resolver.DefaultService = s
resolver.UseSystemHosts = c.UseSystemHosts
if r.ProxyResolver.Invalid() {
@@ -290,7 +292,7 @@ func updateDNS(c *config.DNS, generalIPv6 bool) {
resolver.DirectHostResolver = r.Resolver
}
- dns.ReCreateServer(c.Listen, r.Resolver, m)
+ dns.ReCreateServer(c.Listen, s)
}
func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) {
diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json
index 697c6fcc7e..6dd2385c13 100644
--- a/clash-nyanpasu/manifest/version.json
+++ b/clash-nyanpasu/manifest/version.json
@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.14",
- "mihomo_alpha": "alpha-f45c6f5",
+ "mihomo_alpha": "alpha-f7bd8b8",
"clash_rs": "v0.9.0",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.0-alpha+sha.2784d7a"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
}
},
- "updated_at": "2025-09-26T22:21:10.718Z"
+ "updated_at": "2025-09-28T22:20:43.982Z"
}
diff --git a/lede/README.md b/lede/README.md
index a45cd91b41..5361b16163 100644
--- a/lede/README.md
+++ b/lede/README.md
@@ -39,7 +39,7 @@ ArmSoM-Sige 系列:软路由、单板计算机、小型服务器与智能家
## 编译命令
-1. 首先装好 Linux 系统,推荐 Debian 或 Ubuntu LTS
+1. 首先装好 Linux 系统,推荐 Debian 或 Ubuntu LTS 22/24
2. 安装编译依赖
@@ -50,7 +50,7 @@ ArmSoM-Sige 系列:软路由、单板计算机、小型服务器与智能家
bzip2 ccache clang cmake cpio curl device-tree-compiler flex gawk gcc-multilib g++-multilib gettext \
genisoimage git gperf haveged help2man intltool libc6-dev-i386 libelf-dev libfuse-dev libglib2.0-dev \
libgmp3-dev libltdl-dev libmpc-dev libmpfr-dev libncurses5-dev libncursesw5-dev libpython3-dev \
- libreadline-dev libssl-dev libtool llvm lrzsz msmtp ninja-build p7zip p7zip-full patch pkgconf \
+ libreadline-dev libssl-dev libtool llvm lrzsz libnsl-dev ninja-build p7zip p7zip-full patch pkgconf \
python3 python3-pyelftools python3-setuptools qemu-utils rsync scons squashfs-tools subversion \
swig texinfo uglifyjs upx-ucl unzip vim wget xmlto xxd zlib1g-dev
```
diff --git a/lede/package/network/services/dnsmasq/Makefile b/lede/package/network/services/dnsmasq/Makefile
index aabac5d3ee..d2fe1e51e7 100644
--- a/lede/package/network/services/dnsmasq/Makefile
+++ b/lede/package/network/services/dnsmasq/Makefile
@@ -1,5 +1,5 @@
#
-# Copyright (C) 2006-2016 OpenWrt.org
+# Copyright (C) 2006-2022 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dnsmasq
PKG_UPSTREAM_VERSION:=2.91
PKG_VERSION:=$(subst test,~~test,$(subst rc,~rc,$(PKG_UPSTREAM_VERSION)))
-PKG_RELEASE:=2
+PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION).tar.xz
PKG_SOURCE_URL:=https://thekelleys.org.uk/dnsmasq/
@@ -24,7 +24,6 @@ PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(BUILD_VARIANT)/$(PKG_NAME)-$(PKG_UPSTR
PKG_INSTALL:=1
PKG_BUILD_PARALLEL:=1
-PKG_BUILD_FLAGS:=lto
PKG_ASLR_PIE_REGULAR:=1
PKG_CONFIG_DEPENDS:= CONFIG_PACKAGE_dnsmasq_$(BUILD_VARIANT)_dhcp \
CONFIG_PACKAGE_dnsmasq_$(BUILD_VARIANT)_dhcpv6 \
@@ -109,16 +108,16 @@ define Package/dnsmasq-full/config
default n
config PACKAGE_dnsmasq_full_auth
bool "Build with the facility to act as an authoritative DNS server."
- default y
+ default n
config PACKAGE_dnsmasq_full_ipset
bool "Build with IPset support."
default y
config PACKAGE_dnsmasq_full_nftset
bool "Build with Nftset support."
- default y
+ default n
config PACKAGE_dnsmasq_full_conntrack
bool "Build with Conntrack support."
- default y
+ default n
config PACKAGE_dnsmasq_full_noid
bool "Build with NO_ID. (hide *.bind pseudo domain)"
default n
@@ -134,6 +133,9 @@ endef
Package/dnsmasq-dhcpv6/conffiles = $(Package/dnsmasq/conffiles)
Package/dnsmasq-full/conffiles = $(Package/dnsmasq/conffiles)
+TARGET_CFLAGS += -flto
+TARGET_LDFLAGS += -flto=jobserver
+
COPTS = -DHAVE_UBUS -DHAVE_POLL_H \
$(if $(CONFIG_IPV6),,-DNO_IPV6)
diff --git a/lede/package/network/services/dnsmasq/files/dhcp-script.sh b/lede/package/network/services/dnsmasq/files/dhcp-script.sh
index f0c8b50902..470097bf6b 100755
--- a/lede/package/network/services/dnsmasq/files/dhcp-script.sh
+++ b/lede/package/network/services/dnsmasq/files/dhcp-script.sh
@@ -8,15 +8,6 @@ json_init
json_add_array env
hotplugobj=""
-oldIFS=$IFS
-IFS=$'\n'
-for var in $(env); do
- if [ "${var}" != "${var#DNSMASQ_}" ]; then
- json_add_string "" "${var%%=*}=${var#*=}"
- fi
-done
-IFS=$oldIFS
-
case "$1" in
add | del | old | arp-add | arp-del)
json_add_string "" "MACADDR=$2"
diff --git a/lede/package/network/services/dnsmasq/files/dhcp.conf b/lede/package/network/services/dnsmasq/files/dhcp.conf
index d5b9dfa018..3f054f5feb 100644
--- a/lede/package/network/services/dnsmasq/files/dhcp.conf
+++ b/lede/package/network/services/dnsmasq/files/dhcp.conf
@@ -10,7 +10,7 @@ config dnsmasq
option domain 'lan'
option expandhosts 1
option nonegcache 0
- option cachesize 1000
+ option cachesize 8192
option authoritative 1
option readethers 1
option leasefile '/tmp/dhcp.leases'
diff --git a/lede/package/network/services/dnsmasq/files/dnsmasq.init b/lede/package/network/services/dnsmasq/files/dnsmasq.init
index 6f31636a0d..7fa50803f9 100755
--- a/lede/package/network/services/dnsmasq/files/dnsmasq.init
+++ b/lede/package/network/services/dnsmasq/files/dnsmasq.init
@@ -12,7 +12,6 @@ ADD_WAN_FQDN=0
ADD_LOCAL_FQDN=""
BASECONFIGFILE="/var/etc/dnsmasq.conf"
-EXTRACONFFILE="extraconfig.conf"
BASEHOSTFILE="/tmp/hosts/dhcp"
TRUSTANCHORSFILE="/usr/share/dnsmasq/trust-anchors.conf"
TIMEVALIDFILE="/var/state/dnsmasqsec"
@@ -20,7 +19,7 @@ BASEDHCPSTAMPFILE="/var/run/dnsmasq"
DHCPBOGUSHOSTNAMEFILE="/usr/share/dnsmasq/dhcpbogushostname.conf"
RFC6761FILE="/usr/share/dnsmasq/rfc6761.conf"
DHCPSCRIPT="/usr/lib/dnsmasq/dhcp-script.sh"
-DHCPSCRIPT_DEPENDS="/usr/share/libubox/jshn.sh /usr/bin/jshn /bin/ubus /usr/bin/env"
+DHCPSCRIPT_DEPENDS="/usr/share/libubox/jshn.sh /usr/bin/jshn /bin/ubus"
DNSMASQ_DHCP_VER=4
@@ -34,7 +33,6 @@ dnsmasq_ignore_opt() {
[ "${dnsmasq_features#* DNSSEC }" = "$dnsmasq_features" ] || dnsmasq_has_dnssec=1
[ "${dnsmasq_features#* TFTP }" = "$dnsmasq_features" ] || dnsmasq_has_tftp=1
[ "${dnsmasq_features#* ipset }" = "$dnsmasq_features" ] || dnsmasq_has_ipset=1
- [ "${dnsmasq_features#* nftset }" = "$dnsmasq_features" ] || dnsmasq_has_nftset=1
fi
case "$opt" in
@@ -57,8 +55,6 @@ dnsmasq_ignore_opt() {
[ -z "$dnsmasq_has_tftp" ] ;;
ipset)
[ -z "$dnsmasq_has_ipset" ] ;;
- nftset)
- [ -z "$dnsmasq_has_nftset" ] ;;
*)
return 1
esac
@@ -69,7 +65,7 @@ xappend() {
local opt="${value%%=*}"
if ! dnsmasq_ignore_opt "$opt"; then
- echo "$value" >>"$CONFIGFILE_TMP"
+ echo "$value" >>$CONFIGFILE_TMP
fi
}
@@ -173,6 +169,10 @@ append_address() {
xappend "--address=$1"
}
+append_ipset() {
+ xappend "--ipset=$1"
+}
+
append_connmark_allowlist() {
xappend "--connmark-allowlist=$1"
}
@@ -205,12 +205,8 @@ ismounted() {
return 1
}
-append_extramount() {
- ismounted "$1" || append EXTRA_MOUNT "$1"
-}
-
append_addnhosts() {
- append_extramount "$1"
+ ismounted "$1" || append EXTRA_MOUNT "$1"
xappend "--addn-hosts=$1"
}
@@ -226,14 +222,6 @@ append_interface_name() {
xappend "--interface-name=$1,$2"
}
-append_filter_rr() {
- xappend "--filter-rr=$1"
-}
-
-append_cache_rr() {
- xappend "--cache-rr=$1"
-}
-
filter_dnsmasq() {
local cfg="$1" func="$2" match_cfg="$3" found_cfg
@@ -362,7 +350,7 @@ dhcp_host_add() {
config_get_bool dns "$cfg" dns 0
[ "$dns" = "1" ] && [ -n "$ip" ] && [ -n "$name" ] && {
- echo "$ip $name${DOMAIN:+.$DOMAIN}" >> "$HOSTFILE_TMP"
+ echo "$ip $name${DOMAIN:+.$DOMAIN}" >> $HOSTFILE_TMP
}
config_get mac "$cfg" mac
@@ -511,13 +499,14 @@ dhcp_boot_add() {
[ -n "$serveraddress" ] && [ ! -n "$servername" ] && return 0
- xappend "--dhcp-boot=${networkid:+tag:$networkid,}${filename}${servername:+,$servername}${serveraddress:+,$serveraddress}"
+ xappend "--dhcp-boot=${networkid:+net:$networkid,}${filename}${servername:+,$servername}${serveraddress:+,$serveraddress}"
config_get_bool force "$cfg" force 0
dhcp_option_add "$cfg" "$networkid" "$force"
}
+
dhcp_add() {
local cfg="$1"
local dhcp6range="::"
@@ -548,13 +537,8 @@ dhcp_add() {
# Do not support non-static interfaces for now
[ static = "$proto" ] || return 0
- ipaddr="${subnet%%/*}"
- prefix_or_netmask="${subnet##*/}"
-
# Override interface netmask with dhcp config if applicable
- config_get netmask "$cfg" netmask
-
- [ -n "$netmask" ] && prefix_or_netmask="$netmask"
+ config_get netmask "$cfg" netmask "${subnet##*/}"
#check for an already active dhcp server on the interface, unless 'force' is set
config_get_bool force "$cfg" force 0
@@ -570,8 +554,6 @@ dhcp_add() {
config_get leasetime "$cfg" leasetime 12h
config_get options "$cfg" options
config_get_bool dynamicdhcp "$cfg" dynamicdhcp 1
- config_get_bool dynamicdhcpv4 "$cfg" dynamicdhcpv4 $dynamicdhcp
- config_get_bool dynamicdhcpv6 "$cfg" dynamicdhcpv6 $dynamicdhcp
config_get dhcpv4 "$cfg" dhcpv4
config_get dhcpv6 "$cfg" dhcpv6
@@ -596,30 +578,25 @@ dhcp_add() {
nettag="${networkid:+set:${networkid},}"
- # make sure the DHCP range is not empty
- if [ "$dhcpv4" != "disabled" ]; then
- unset START
- unset END
- unset NETMASK
- ipcalc "$ipaddr/$prefix_or_netmask" "$start" "$limit"
-
- if [ -z "$START" ] || [ -z "$END" ] || [ -z "$NETMASK" ]; then
- logger -t dnsmasq \
- "unable to set dhcp-range for dhcp uci config section '$cfg'" \
- "on interface '$ifname', please check your config"
- else
- [ "$dynamicdhcpv4" = "0" ] && END="static"
- xappend "--dhcp-range=$tags$nettag$START,$END,$NETMASK,$leasetime${options:+ $options}"
- fi
+ if [ "$limit" -gt 0 ] ; then
+ limit=$((limit-1))
fi
- if [ "$dynamicdhcpv6" = "0" ] ; then
+ eval "$(ipcalc.sh "${subnet%%/*}" $netmask $start $limit)"
+
+ if [ "$dynamicdhcp" = "0" ] ; then
+ END="static"
dhcp6range="::,static"
else
dhcp6range="::1000,::ffff"
fi
+ if [ "$dhcpv4" != "disabled" ] ; then
+ xappend "--dhcp-range=$tags$nettag$START,$END,$NETMASK,$leasetime${options:+ $options}"
+ fi
+
+
if [ $DNSMASQ_DHCP_VER -eq 6 ] && [ "$ra" = "server" ] ; then
# Note: dnsmasq cannot just be a DHCPv6 server (all-in-1)
# and let some other machine(s) send RA pointing to it.
@@ -732,7 +709,7 @@ dhcp_domain_add() {
record="${record:+$record }$name"
done
- echo "$ip $record" >> "$HOSTFILE_TMP"
+ echo "$ip $record" >> $HOSTFILE_TMP
}
dhcp_srv_add() {
@@ -806,29 +783,6 @@ dhcp_hostrecord_add() {
xappend "--host-record=$record"
}
-dhcp_dnsrr_add() {
- #This adds arbitrary resource record types (of IN class) whose optional data must be hex
- local cfg="$1"
- local rrname rrnumber hexdata
-
- config_get rrname "$cfg" rrname
- [ -n "$rrname" ] || return 0
-
- config_get rrnumber "$cfg" rrnumber
- [ -n "$rrnumber" ] && [ "$rrnumber" -gt 0 ] || return 0
-
- config_get hexdata "$cfg" hexdata
-
- # dnsmasq accepts colon XX:XX:.., space XX XX .., or contiguous XXXX.. hex forms or mixtures thereof
- if [ -n "${hexdata//[0-9a-fA-F\:\ ]/}" ]; then
- # is invalid hex literal
- echo "dnsmasq: \"$hexdata\" is malformed hexadecimal (separate hex with colon, space or not at all)." >&2
- return 1
- fi
-
- xappend "--dns-rr=${rrname},${rrnumber}${hexdata:+,$hexdata}"
-}
-
dhcp_relay_add() {
local cfg="$1"
local local_addr server_addr interface
@@ -850,61 +804,30 @@ dhcp_relay_add() {
dnsmasq_ipset_add() {
local cfg="$1"
- local ipsets nftsets domains
+ local ipsets
add_ipset() {
ipsets="${ipsets:+$ipsets,}$1"
}
- add_nftset() {
- local IFS=,
- for set in $1; do
- local fam="$family"
- [ -n "$fam" ] || fam=$(echo "$set" | sed -nre \
- 's#^.*[^0-9]([46])$|^.*[-_]([46])[-_].*$|^([46])[^0-9].*$#\1\2\3#p')
- [ -n "$fam" ] || \
- fam=$(nft -t list set "$table_family" "$table" "$set" 2>&1 | sed -nre \
- 's#^\t\ttype .*\bipv([46])_addr\b.*$#\1#p')
-
- [ -n "$fam" ] || \
- logger -t dnsmasq "Cannot infer address family from non-existent nftables set '$set'"
-
- nftsets="${nftsets:+$nftsets,}${fam:+$fam#}$table_family#$table#$set"
- done
- }
-
add_domain() {
- # leading '/' is expected
- domains="$domains/$1"
+ xappend "--ipset=/$1/$ipsets"
}
- config_get table "$cfg" table 'fw4'
- config_get table_family "$cfg" table_family 'inet'
- if [ "$table_family" = "ip" ] ; then
- family="4"
- elif [ "$table_family" = "ip6" ] ; then
- family="6"
- else
- config_get family "$cfg" family
- fi
-
config_list_foreach "$cfg" "name" add_ipset
- config_list_foreach "$cfg" "name" add_nftset
- config_list_foreach "$cfg" "domain" add_domain
- if [ -z "$ipsets" ] || [ -z "$nftsets" ] || [ -z "$domains" ]; then
+ if [ -z "$ipsets" ]; then
return 0
fi
- xappend "--ipset=$domains/$ipsets"
- xappend "--nftset=$domains/$nftsets"
+ config_list_foreach "$cfg" "domain" add_domain
}
dnsmasq_start()
{
local cfg="$1"
- local disabled user_dhcpscript logfacility
- local resolvfile resolvdir localuse=1
+ local disabled user_dhcpscript
+ local resolvfile resolvdir localuse=0
config_get_bool disabled "$cfg" disabled 0
[ "$disabled" -gt 0 ] && return 0
@@ -923,13 +846,13 @@ dnsmasq_start()
# before we can call xappend
umask u=rwx,g=rx,o=rx
mkdir -p /var/run/dnsmasq/
- mkdir -p "$(dirname "$CONFIGFILE")"
+ mkdir -p $(dirname $CONFIGFILE)
mkdir -p "$HOSTFILE_DIR"
mkdir -p /var/lib/misc
chown dnsmasq:dnsmasq /var/run/dnsmasq
- echo "# auto-generated config file from /etc/config/dhcp" > "$CONFIGFILE_TMP"
- echo "# auto-generated config file from /etc/config/dhcp" > "$HOSTFILE_TMP"
+ echo "# auto-generated config file from /etc/config/dhcp" > $CONFIGFILE_TMP
+ echo "# auto-generated config file from /etc/config/dhcp" > $HOSTFILE_TMP
local dnsmasqconffile="/etc/dnsmasq.${cfg}.conf"
if [ ! -r "$dnsmasqconffile" ]; then
@@ -1015,14 +938,11 @@ dnsmasq_start()
append_bool "$cfg" rapidcommit "--dhcp-rapid-commit"
append_bool "$cfg" scriptarp "--script-arp"
- # deprecate or remove filter-X in favor of filter-rr?
append_bool "$cfg" filter_aaaa "--filter-AAAA"
append_bool "$cfg" filter_a "--filter-A"
- config_list_foreach "$cfg" filter_rr append_filter_rr
- config_list_foreach "$cfg" cache_rr append_cache_rr
append_parm "$cfg" logfacility "--log-facility"
- config_get logfacility "$cfg" "logfacility"
+
append_parm "$cfg" cachesize "--cache-size"
append_parm "$cfg" dnsforwardmax "--dns-forward-max"
append_parm "$cfg" port "--port"
@@ -1037,6 +957,7 @@ dnsmasq_start()
config_list_foreach "$cfg" "server" append_server
config_list_foreach "$cfg" "rev_server" append_rev_server
config_list_foreach "$cfg" "address" append_address
+ config_list_foreach "$cfg" "ipset" append_ipset
local connmark_allowlist_enable
config_get connmark_allowlist_enable "$cfg" connmark_allowlist_enable 0
@@ -1060,14 +981,7 @@ dnsmasq_start()
config_list_foreach "$cfg" "addnhosts" append_addnhosts
config_list_foreach "$cfg" "bogusnxdomain" append_bogusnxdomain
append_parm "$cfg" "leasefile" "--dhcp-leasefile" "/tmp/dhcp.leases"
-
- local serversfile
- config_get serversfile "$cfg" "serversfile"
- [ -n "$serversfile" ] && {
- xappend "--servers-file=$serversfile"
- append EXTRA_MOUNT "$serversfile"
- }
-
+ append_parm "$cfg" "serversfile" "--servers-file"
append_parm "$cfg" "tftp_root" "--tftp-root"
append_parm "$cfg" "dhcp_boot" "--dhcp-boot"
append_parm "$cfg" "local_ttl" "--local-ttl"
@@ -1104,7 +1018,7 @@ dnsmasq_start()
config_get resolvfile "$cfg" resolvfile /tmp/resolv.conf.d/resolv.conf.auto
[ -n "$resolvfile" ] && [ ! -e "$resolvfile" ] && touch "$resolvfile"
xappend "--resolv-file=$resolvfile"
- [ "$resolvfile" != "/tmp/resolv.conf.d/resolv.conf.auto" ] && localuse=0
+ [ "$resolvfile" = "/tmp/resolv.conf.d/resolv.conf.auto" ] && localuse=1
resolvdir="$(dirname "$resolvfile")"
fi
config_get_bool localuse "$cfg" localuse "$localuse"
@@ -1153,9 +1067,6 @@ dnsmasq_start()
[ "$addmac" = "1" ] && addmac=
xappend "--add-mac${addmac:+="$addmac"}"
}
- append_bool "$cfg" stripmac "--strip-mac"
- append_parm "$cfg" addsubnet "--add-subnet"
- append_bool "$cfg" stripsubnet "--strip-subnet"
dhcp_option_add "$cfg" "" 0
dhcp_option_add "$cfg" "" 2
@@ -1169,7 +1080,7 @@ dnsmasq_start()
[ ! -d "$dnsmasqconfdir" ] && mkdir -p $dnsmasqconfdir
xappend "--user=dnsmasq"
xappend "--group=dnsmasq"
- echo >> "$CONFIGFILE_TMP"
+ echo >> $CONFIGFILE_TMP
config_get_bool enable_tftp "$cfg" enable_tftp 0
[ "$enable_tftp" -gt 0 ] && {
@@ -1178,7 +1089,7 @@ dnsmasq_start()
}
config_foreach filter_dnsmasq host dhcp_host_add "$cfg"
- echo >> "$CONFIGFILE_TMP"
+ echo >> $CONFIGFILE_TMP
config_get_bool dhcpbogushostname "$cfg" dhcpbogushostname 1
[ "$dhcpbogushostname" -gt 0 ] && {
@@ -1197,13 +1108,12 @@ dnsmasq_start()
config_foreach filter_dnsmasq match dhcp_match_add "$cfg"
config_foreach filter_dnsmasq domain dhcp_domain_add "$cfg"
config_foreach filter_dnsmasq hostrecord dhcp_hostrecord_add "$cfg"
- config_foreach filter_dnsmasq dnsrr dhcp_dnsrr_add "$cfg"
[ -n "$BOOT" ] || config_foreach filter_dnsmasq relay dhcp_relay_add "$cfg"
- echo >> "$CONFIGFILE_TMP"
+ echo >> $CONFIGFILE_TMP
config_foreach filter_dnsmasq srvhost dhcp_srv_add "$cfg"
config_foreach filter_dnsmasq mxhost dhcp_mx_add "$cfg"
- echo >> "$CONFIGFILE_TMP"
+ echo >> $CONFIGFILE_TMP
config_get_bool boguspriv "$cfg" boguspriv 1
[ "$boguspriv" -gt 0 ] && {
@@ -1225,16 +1135,16 @@ dnsmasq_start()
fi
- echo >> "$CONFIGFILE_TMP"
+ echo >> $CONFIGFILE_TMP
config_foreach filter_dnsmasq cname dhcp_cname_add "$cfg"
- echo >> "$CONFIGFILE_TMP"
+ echo >> $CONFIGFILE_TMP
- echo >> "$CONFIGFILE_TMP"
+ echo >> $CONFIGFILE_TMP
config_foreach filter_dnsmasq ipset dnsmasq_ipset_add "$cfg"
- echo >> "$CONFIGFILE_TMP"
+ echo >> $CONFIGFILE_TMP
- mv -f "$CONFIGFILE_TMP" "$CONFIGFILE"
- mv -f "$HOSTFILE_TMP" "$HOSTFILE"
+ mv -f $CONFIGFILE_TMP $CONFIGFILE
+ mv -f $HOSTFILE_TMP $HOSTFILE
[ "$localuse" -gt 0 ] && {
rm -f /tmp/resolv.conf
@@ -1248,30 +1158,18 @@ dnsmasq_start()
done
}
- config_list_foreach "$cfg" addnmount append_extramount
-
procd_open_instance $cfg
procd_set_param command $PROG -C $CONFIGFILE -k -x /var/run/dnsmasq/dnsmasq."${cfg}".pid
procd_set_param file $CONFIGFILE
[ -n "$user_dhcpscript" ] && procd_set_param env USER_DHCPSCRIPT="$user_dhcpscript"
procd_set_param respawn
- local instance_ifc instance_netdev
- config_get instance_ifc "$cfg" interface
- [ -n "$instance_ifc" ] && network_get_device instance_netdev "$instance_ifc" &&
- [ -n "$instance_netdev" ] && procd_set_param netdev $instance_netdev
-
procd_add_jail dnsmasq ubus log
procd_add_jail_mount $CONFIGFILE $DHCPBOGUSHOSTNAMEFILE $DHCPSCRIPT $DHCPSCRIPT_DEPENDS
procd_add_jail_mount $EXTRA_MOUNT $RFC6761FILE $TRUSTANCHORSFILE
procd_add_jail_mount $dnsmasqconffile $dnsmasqconfdir $resolvdir $user_dhcpscript
procd_add_jail_mount /etc/passwd /etc/group /etc/TZ /etc/hosts /etc/ethers
procd_add_jail_mount_rw /var/run/dnsmasq/ $leasefile
- case "$logfacility" in */*)
- [ ! -e "$logfacility" ] && touch "$logfacility"
- procd_add_jail_mount_rw "$logfacility"
- esac
- [ -e "$hostsfile" ] && procd_add_jail_mount $hostsfile
procd_close_instance
}
@@ -1279,12 +1177,12 @@ dnsmasq_start()
dnsmasq_stop()
{
local cfg="$1"
- local noresolv resolvfile localuse=1
+ local noresolv resolvfile localuse=0
config_get_bool noresolv "$cfg" noresolv 0
config_get resolvfile "$cfg" "resolvfile"
- [ "$noresolv" = 0 ] && [ "$resolvfile" != "/tmp/resolv.conf.d/resolv.conf.auto" ] && localuse=0
+ [ "$noresolv" = 0 ] && [ "$resolvfile" = "/tmp/resolv.conf.d/resolv.conf.auto" ] && localuse=1
config_get_bool localuse "$cfg" localuse "$localuse"
[ "$localuse" -gt 0 ] && ln -sf "/tmp/resolv.conf.d/resolv.conf.auto" /tmp/resolv.conf
@@ -1293,11 +1191,10 @@ dnsmasq_stop()
add_interface_trigger()
{
- local interface ifname ignore
+ local interface ignore
config_get interface "$1" interface
config_get_bool ignore "$1" ignore 0
- network_get_device ifname "$interface" || ignore=0
[ -n "$interface" ] && [ $ignore -eq 0 ] && procd_add_interface_trigger "interface.*" "$interface" /etc/init.d/dnsmasq reload
}
diff --git a/lede/package/network/services/dnsmasq/patches/200-ubus_dns.patch b/lede/package/network/services/dnsmasq/patches/200-ubus_dns.patch
index a1a668818e..21e9e57c9c 100644
--- a/lede/package/network/services/dnsmasq/patches/200-ubus_dns.patch
+++ b/lede/package/network/services/dnsmasq/patches/200-ubus_dns.patch
@@ -275,4 +275,4 @@
+
void ubus_event_bcast(const char *type, const char *mac, const char *ip, const char *name, const char *interface)
{
- struct ubus_context *ubus = (struct ubus_context *)daemon->ubus;
+ struct ubus_context *ubus = (struct ubus_context *)daemon->ubus;
\ No newline at end of file
diff --git a/mihomo/component/resolver/local.go b/mihomo/component/resolver/service.go
similarity index 73%
rename from mihomo/component/resolver/local.go
rename to mihomo/component/resolver/service.go
index e8505118b4..8b8f115842 100644
--- a/mihomo/component/resolver/local.go
+++ b/mihomo/component/resolver/service.go
@@ -6,15 +6,15 @@ import (
D "github.com/miekg/dns"
)
-var DefaultLocalServer LocalServer
+var DefaultService Service
-type LocalServer interface {
+type Service interface {
ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error)
}
// ServeMsg with a dns.Msg, return resolve dns.Msg
func ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) {
- if server := DefaultLocalServer; server != nil {
+ if server := DefaultService; server != nil {
return server.ServeMsg(ctx, msg)
}
diff --git a/mihomo/config/config.go b/mihomo/config/config.go
index bcd3cc7ad2..dec0955b2d 100644
--- a/mihomo/config/config.go
+++ b/mihomo/config/config.go
@@ -146,6 +146,7 @@ type DNS struct {
PreferH3 bool
IPv6 bool
IPv6Timeout uint
+ UseHosts bool
UseSystemHosts bool
NameServer []dns.NameServer
Fallback []dns.NameServer
@@ -157,7 +158,6 @@ type DNS struct {
CacheAlgorithm string
CacheMaxSize int
FakeIPRange *fakeip.Pool
- Hosts *trie.DomainTrie[resolver.HostValue]
NameServerPolicy []dns.Policy
ProxyServerNameserver []dns.NameServer
DirectNameServer []dns.NameServer
@@ -680,7 +680,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
}
config.Hosts = hosts
- dnsCfg, err := parseDNS(rawCfg, hosts, ruleProviders)
+ dnsCfg, err := parseDNS(rawCfg, ruleProviders)
if err != nil {
return nil, err
}
@@ -1341,7 +1341,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro
return policy, nil
}
-func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) {
+func parseDNS(rawCfg *RawConfig, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) {
cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
@@ -1357,6 +1357,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
PreferH3: cfg.PreferH3,
IPv6Timeout: cfg.IPv6Timeout,
IPv6: cfg.IPv6,
+ UseHosts: cfg.UseHosts,
UseSystemHosts: cfg.UseSystemHosts,
EnhancedMode: cfg.EnhancedMode,
CacheAlgorithm: cfg.CacheAlgorithm,
@@ -1490,10 +1491,6 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
}
}
- if cfg.UseHosts {
- dnsCfg.Hosts = hosts
- }
-
return dnsCfg, nil
}
diff --git a/mihomo/context/dns.go b/mihomo/context/dns.go
index 1cc2067d8d..15143102ca 100644
--- a/mihomo/context/dns.go
+++ b/mihomo/context/dns.go
@@ -2,10 +2,10 @@ package context
import (
"context"
+
"github.com/metacubex/mihomo/common/utils"
"github.com/gofrs/uuid/v5"
- "github.com/miekg/dns"
)
const (
@@ -17,17 +17,15 @@ const (
type DNSContext struct {
context.Context
- id uuid.UUID
- msg *dns.Msg
- tp string
+ id uuid.UUID
+ tp string
}
-func NewDNSContext(ctx context.Context, msg *dns.Msg) *DNSContext {
+func NewDNSContext(ctx context.Context) *DNSContext {
return &DNSContext{
Context: ctx,
- id: utils.NewUUIDV4(),
- msg: msg,
+ id: utils.NewUUIDV4(),
}
}
diff --git a/mihomo/dns/enhancer.go b/mihomo/dns/enhancer.go
index 9ea3ae84ac..36923ca624 100644
--- a/mihomo/dns/enhancer.go
+++ b/mihomo/dns/enhancer.go
@@ -12,6 +12,7 @@ type ResolverEnhancer struct {
mode C.DNSMode
fakePool *fakeip.Pool
mapping *lru.LruCache[netip.Addr, string]
+ useHosts bool
}
func (h *ResolverEnhancer) FakeIPEnabled() bool {
@@ -103,7 +104,13 @@ func (h *ResolverEnhancer) StoreFakePoolState() {
}
}
-func NewEnhancer(cfg Config) *ResolverEnhancer {
+type EnhancerConfig struct {
+ EnhancedMode C.DNSMode
+ Pool *fakeip.Pool
+ UseHosts bool
+}
+
+func NewEnhancer(cfg EnhancerConfig) *ResolverEnhancer {
var fakePool *fakeip.Pool
var mapping *lru.LruCache[netip.Addr, string]
@@ -116,5 +123,6 @@ func NewEnhancer(cfg Config) *ResolverEnhancer {
mode: cfg.EnhancedMode,
fakePool: fakePool,
mapping: mapping,
+ useHosts: cfg.UseHosts,
}
}
diff --git a/mihomo/dns/local.go b/mihomo/dns/local.go
deleted file mode 100644
index 37b5d41b04..0000000000
--- a/mihomo/dns/local.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package dns
-
-import (
- "context"
-
- D "github.com/miekg/dns"
-)
-
-type LocalServer struct {
- handler handler
-}
-
-// ServeMsg implement resolver.LocalServer ResolveMsg
-func (s *LocalServer) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) {
- return handlerWithContext(ctx, s.handler, msg)
-}
-
-func NewLocalServer(resolver *Resolver, mapper *ResolverEnhancer) *LocalServer {
- return &LocalServer{handler: NewHandler(resolver, mapper)}
-}
diff --git a/mihomo/dns/middleware.go b/mihomo/dns/middleware.go
index e6461e91a3..502a37e5e5 100644
--- a/mihomo/dns/middleware.go
+++ b/mihomo/dns/middleware.go
@@ -7,22 +7,22 @@ import (
"github.com/metacubex/mihomo/common/lru"
"github.com/metacubex/mihomo/component/fakeip"
- R "github.com/metacubex/mihomo/component/resolver"
+ "github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
- "github.com/metacubex/mihomo/context"
+ icontext "github.com/metacubex/mihomo/context"
"github.com/metacubex/mihomo/log"
D "github.com/miekg/dns"
)
type (
- handler func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error)
+ handler func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error)
middleware func(next handler) handler
)
-func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middleware {
+func withHosts(mapping *lru.LruCache[netip.Addr, string]) middleware {
return func(next handler) handler {
- return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
+ return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0]
if !isIPRequest(q) {
@@ -36,7 +36,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew
rr.Target = domain + "."
resp.Answer = append([]D.RR{rr}, resp.Answer...)
}
- record, ok := hosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA)
+ record, ok := resolver.DefaultHosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA)
if !ok {
if record != nil && record.IsDomain {
// replace request domain
@@ -88,7 +88,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew
return next(ctx, r)
}
- ctx.SetType(context.DNSTypeHost)
+ ctx.SetType(icontext.DNSTypeHost)
msg.SetRcode(r, D.RcodeSuccess)
msg.Authoritative = true
msg.RecursionAvailable = true
@@ -99,7 +99,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew
func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware {
return func(next handler) handler {
- return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
+ return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0]
if !isIPRequest(q) {
@@ -149,7 +149,7 @@ func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware {
func withFakeIP(fakePool *fakeip.Pool) middleware {
return func(next handler) handler {
- return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
+ return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0]
host := strings.TrimRight(q.Name, ".")
@@ -173,7 +173,7 @@ func withFakeIP(fakePool *fakeip.Pool) middleware {
msg := r.Copy()
msg.Answer = []D.RR{rr}
- ctx.SetType(context.DNSTypeFakeIP)
+ ctx.SetType(icontext.DNSTypeFakeIP)
setMsgTTL(msg, 1)
msg.SetRcode(r, D.RcodeSuccess)
msg.Authoritative = true
@@ -185,8 +185,8 @@ func withFakeIP(fakePool *fakeip.Pool) middleware {
}
func withResolver(resolver *Resolver) handler {
- return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
- ctx.SetType(context.DNSTypeRaw)
+ return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) {
+ ctx.SetType(icontext.DNSTypeRaw)
q := r.Question[0]
@@ -218,11 +218,11 @@ func compose(middlewares []middleware, endpoint handler) handler {
return h
}
-func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
- middlewares := []middleware{}
+func newHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
+ var middlewares []middleware
- if resolver.hosts != nil {
- middlewares = append(middlewares, withHosts(R.NewHosts(resolver.hosts), mapper.mapping))
+ if mapper.useHosts {
+ middlewares = append(middlewares, withHosts(mapper.mapping))
}
if mapper.mode == C.DNSFakeIP {
diff --git a/mihomo/dns/resolver.go b/mihomo/dns/resolver.go
index f5f69c5f4a..f7d4d42968 100644
--- a/mihomo/dns/resolver.go
+++ b/mihomo/dns/resolver.go
@@ -9,7 +9,6 @@ import (
"github.com/metacubex/mihomo/common/arc"
"github.com/metacubex/mihomo/common/lru"
"github.com/metacubex/mihomo/common/singleflight"
- "github.com/metacubex/mihomo/component/fakeip"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/trie"
C "github.com/metacubex/mihomo/constant"
@@ -40,7 +39,6 @@ type result struct {
type Resolver struct {
ipv6 bool
ipv6Timeout time.Duration
- hosts *trie.DomainTrie[resolver.HostValue]
main []dnsClient
fallback []dnsClient
fallbackDomainFilters []C.DomainMatcher
@@ -452,11 +450,8 @@ type Config struct {
DirectFollowPolicy bool
IPv6 bool
IPv6Timeout uint
- EnhancedMode C.DNSMode
FallbackIPFilter []C.IpMatcher
FallbackDomainFilter []C.DomainMatcher
- Pool *fakeip.Pool
- Hosts *trie.DomainTrie[resolver.HostValue]
Policy []Policy
CacheAlgorithm string
CacheMaxSize int
@@ -530,7 +525,6 @@ func NewResolver(config Config) (rs Resolvers) {
ipv6: config.IPv6,
main: cacheTransform(config.Main),
cache: config.newCache(),
- hosts: config.Hosts,
ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond,
}
r.defaultResolver = defaultResolver
@@ -541,7 +535,6 @@ func NewResolver(config Config) (rs Resolvers) {
ipv6: config.IPv6,
main: cacheTransform(config.ProxyServer),
cache: config.newCache(),
- hosts: config.Hosts,
ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond,
}
}
@@ -551,7 +544,6 @@ func NewResolver(config Config) (rs Resolvers) {
ipv6: config.IPv6,
main: cacheTransform(config.DirectServer),
cache: config.newCache(),
- hosts: config.Hosts,
ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond,
}
}
diff --git a/mihomo/dns/server.go b/mihomo/dns/server.go
index caf1c2891a..b1224c6212 100644
--- a/mihomo/dns/server.go
+++ b/mihomo/dns/server.go
@@ -1,13 +1,12 @@
package dns
import (
- stdContext "context"
- "errors"
+ "context"
"net"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/sockopt"
- "github.com/metacubex/mihomo/context"
+ "github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/log"
D "github.com/miekg/dns"
@@ -21,39 +20,32 @@ var (
)
type Server struct {
- handler handler
+ service resolver.Service
tcpServer *D.Server
udpServer *D.Server
}
// ServeDNS implement D.Handler ServeDNS
func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) {
- msg, err := handlerWithContext(stdContext.Background(), s.handler, r)
+ msg, err := s.service.ServeMsg(context.Background(), r)
if err != nil {
- D.HandleFailed(w, r)
+ m := new(D.Msg)
+ m.SetRcode(r, D.RcodeServerFailure)
+ // does not matter if this write fails
+ w.WriteMsg(m)
return
}
msg.Compress = true
w.WriteMsg(msg)
}
-func handlerWithContext(stdCtx stdContext.Context, handler handler, msg *D.Msg) (*D.Msg, error) {
- if len(msg.Question) == 0 {
- return nil, errors.New("at least one question is required")
- }
-
- ctx := context.NewDNSContext(stdCtx, msg)
- return handler(ctx, msg)
+func (s *Server) SetService(service resolver.Service) {
+ s.service = service
}
-func (s *Server) SetHandler(handler handler) {
- s.handler = handler
-}
-
-func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) {
- if addr == address && resolver != nil {
- handler := NewHandler(resolver, mapper)
- server.SetHandler(handler)
+func ReCreateServer(addr string, service resolver.Service) {
+ if addr == address && service != nil {
+ server.SetService(service)
return
}
@@ -67,10 +59,10 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) {
server.udpServer = nil
}
- server.handler = nil
+ server.service = nil
address = ""
- if addr == "" {
+ if addr == "" || service == nil {
return
}
@@ -87,8 +79,7 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) {
}
address = addr
- handler := NewHandler(resolver, mapper)
- server = &Server{handler: handler}
+ server = &Server{service: service}
go func() {
p, err := inbound.ListenPacket("udp", addr)
diff --git a/mihomo/dns/service.go b/mihomo/dns/service.go
new file mode 100644
index 0000000000..4a7c1bb2ea
--- /dev/null
+++ b/mihomo/dns/service.go
@@ -0,0 +1,29 @@
+package dns
+
+import (
+ "context"
+ "errors"
+
+ "github.com/metacubex/mihomo/component/resolver"
+ icontext "github.com/metacubex/mihomo/context"
+ D "github.com/miekg/dns"
+)
+
+type Service struct {
+ handler handler
+}
+
+// ServeMsg implement [resolver.Service] ResolveMsg
+func (s *Service) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) {
+ if len(msg.Question) == 0 {
+ return nil, errors.New("at least one question is required")
+ }
+
+ return s.handler(icontext.NewDNSContext(ctx), msg)
+}
+
+var _ resolver.Service = (*Service)(nil)
+
+func NewService(resolver *Resolver, mapper *ResolverEnhancer) *Service {
+ return &Service{handler: newHandler(resolver, mapper)}
+}
diff --git a/mihomo/hub/executor/executor.go b/mihomo/hub/executor/executor.go
index fcf176e0e0..041e6fc36b 100644
--- a/mihomo/hub/executor/executor.go
+++ b/mihomo/hub/executor/executor.go
@@ -240,20 +240,18 @@ func updateDNS(c *config.DNS, generalIPv6 bool) {
if !c.Enable {
resolver.DefaultResolver = nil
resolver.DefaultHostMapper = nil
- resolver.DefaultLocalServer = nil
+ resolver.DefaultService = nil
resolver.ProxyServerHostResolver = nil
resolver.DirectHostResolver = nil
- dns.ReCreateServer("", nil, nil)
+ dns.ReCreateServer("", nil)
return
}
- cfg := dns.Config{
+
+ r := dns.NewResolver(dns.Config{
Main: c.NameServer,
Fallback: c.Fallback,
IPv6: c.IPv6 && generalIPv6,
IPv6Timeout: c.IPv6Timeout,
- EnhancedMode: c.EnhancedMode,
- Pool: c.FakeIPRange,
- Hosts: c.Hosts,
FallbackIPFilter: c.FallbackIPFilter,
FallbackDomainFilter: c.FallbackDomainFilter,
Default: c.DefaultNameserver,
@@ -263,19 +261,23 @@ func updateDNS(c *config.DNS, generalIPv6 bool) {
DirectFollowPolicy: c.DirectFollowPolicy,
CacheAlgorithm: c.CacheAlgorithm,
CacheMaxSize: c.CacheMaxSize,
- }
-
- r := dns.NewResolver(cfg)
- m := dns.NewEnhancer(cfg)
+ })
+ m := dns.NewEnhancer(dns.EnhancerConfig{
+ EnhancedMode: c.EnhancedMode,
+ Pool: c.FakeIPRange,
+ UseHosts: c.UseHosts,
+ })
// reuse cache of old host mapper
if old := resolver.DefaultHostMapper; old != nil {
m.PatchFrom(old.(*dns.ResolverEnhancer))
}
+ s := dns.NewService(r.Resolver, m)
+
resolver.DefaultResolver = r
resolver.DefaultHostMapper = m
- resolver.DefaultLocalServer = dns.NewLocalServer(r.Resolver, m)
+ resolver.DefaultService = s
resolver.UseSystemHosts = c.UseSystemHosts
if r.ProxyResolver.Invalid() {
@@ -290,7 +292,7 @@ func updateDNS(c *config.DNS, generalIPv6 bool) {
resolver.DirectHostResolver = r.Resolver
}
- dns.ReCreateServer(c.Listen, r.Resolver, m)
+ dns.ReCreateServer(c.Listen, s)
}
func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) {
diff --git a/openwrt-packages/adguardhome/Makefile b/openwrt-packages/adguardhome/Makefile
index d672bb213a..eef2c1d5b1 100644
--- a/openwrt-packages/adguardhome/Makefile
+++ b/openwrt-packages/adguardhome/Makefile
@@ -6,12 +6,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=adguardhome
-PKG_VERSION:=0.107.66
+PKG_VERSION:=0.107.67
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/AdGuardHome/tar.gz/v$(PKG_VERSION)?
-PKG_HASH:=823ccfed64b1472e9f4a23867e892a681642b2b3a6e64e285dbe22a57a384d84
+PKG_HASH:=0d74004fd17c8f185174fa09deb130ad48e2f46e946eb9fa8c66ce186d2af9cf
PKG_BUILD_DIR:=$(BUILD_DIR)/AdGuardHome-$(PKG_VERSION)
PKG_LICENSE:=GPL-3.0-only
@@ -58,7 +58,7 @@ define Download/adguardhome-frontend
URL:=https://github.com/AdguardTeam/AdGuardHome/releases/download/v$(PKG_VERSION)/
URL_FILE:=AdGuardHome_frontend.tar.gz
FILE:=$(FRONTEND_FILE)
- HASH:=18ead3a9a0c710a05d63a3f967795709120a8f50e8938462860022ada3c950e4
+ HASH:=8709396e05f812f3e2085a64074384b6363fe1871b9bbb7e8f9886c1aa64b579
endef
define Build/Prepare
diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js
index bcfa13b060..be786084a8 100644
--- a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js
+++ b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js
@@ -356,11 +356,11 @@ const vless_flow = [
/* Prototype */
const CBIGridSection = form.GridSection.extend({
modaltitle(/* ... */) {
- return loadModalTitle.call(this, ...this.hm_modaltitle || [null,null], ...arguments)
+ return loadModalTitle.call(this, ...this.hm_modaltitle || [null,null], ...arguments);
},
sectiontitle(/* ... */) {
- return loadDefaultLabel.call(this, ...arguments);
+ return loadDefaultLabel.apply(this, arguments);
},
renderSectionAdd(extra_class) {
@@ -1184,28 +1184,6 @@ function textvalue2Value(section_id) {
return this.vallist[i];
}
-function validatePresetIDs(disoption_list, section_id) {
- let node;
- let hm_prefmt = glossary[this.section.sectiontype].prefmt;
- let preset_ids = [
- 'fchomo_direct_list',
- 'fchomo_proxy_list',
- 'fchomo_china_list',
- 'fchomo_gfw_list'
- ];
-
- if (preset_ids.map((v) => hm_prefmt.format(v)).includes(section_id)) {
- disoption_list.forEach(([typ, opt]) => {
- node = this.section.getUIElement(section_id, opt)?.node;
- (typ ? node?.querySelector(typ) : node)?.setAttribute(typ === 'textarea' ? 'readOnly' : 'disabled', '');
- });
-
- this.map.findElement('id', 'cbi-fchomo-' + section_id)?.lastChild.querySelector('.cbi-button-remove')?.remove();
- }
-
- return true;
-}
-
function validateAuth(section_id, value) {
if (!value)
return true;
@@ -1270,59 +1248,6 @@ function validateCommonPort(section_id, value) {
return true;
}
-function validateJson(section_id, value) {
- if (!value)
- return true;
-
- try {
- let obj = JSON.parse(value.trim());
- if (!obj)
- return _('Expecting: %s').format(_('valid JSON format'));
- }
- catch(e) {
- return _('Expecting: %s').format(_('valid JSON format'));
- }
-
- return true;
-}
-
-function validateMTLSClientAuth(type_option, section_id, value) {
- // If `client-auth-type` is set to "verify-if-given" or "require-and-verify", `client-auth-cert` must not be empty.
- const auth_type = this.section.getOption(type_option).formvalue(section_id);
- //this.section.getUIElement('tls_client_auth_type').getValue();
- if (!value && ["verify-if-given", "require-and-verify"].includes(auth_type))
- return _('Expecting: %s').format(_('non-empty value'));
-
- return true;
-}
-
-function validateBase64Key(length, section_id, value) {
- /* Thanks to luci-proto-wireguard */
- if (value)
- if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
- return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
-
- return true;
-}
-
-function validateShadowsocksPassword(encmode, section_id, value) {
- let length = shadowsocks_cipher_length[encmode];
- if (typeof length !== 'undefined') {
- length = Math.ceil(length/3)*4;
- if (encmode.match(/^2022-/)) {
- return validateBase64Key(length, section_id, value);
- } else {
- if (length === 0 && !value)
- return _('Expecting: %s').format(_('non-empty value'));
- if (length !== 0 && value.length !== length)
- return _('Expecting: %s').format(_('valid key length with %d characters').format(length));
- }
- } else
- return true;
-
- return true;
-}
-
function validateBytesize(section_id, value) {
if (!value)
return true;
@@ -1342,18 +1267,27 @@ function validateTimeDuration(section_id, value) {
return true;
}
-function validateUniqueValue(section_id, value) {
+function validateJson(section_id, value) {
if (!value)
- return _('Expecting: %s').format(_('non-empty value'));
+ return true;
- let duplicate = false;
- uci.sections(this.config, this.section.sectiontype, (res) => {
- if (res['.name'] !== section_id)
- if (res[this.option] === value)
- duplicate = true;
- });
- if (duplicate)
- return _('Expecting: %s').format(_('unique value'));
+ try {
+ let obj = JSON.parse(value.trim());
+ if (!obj)
+ return _('Expecting: %s').format(_('valid JSON format'));
+ }
+ catch(e) {
+ return _('Expecting: %s').format(_('valid JSON format'));
+ }
+
+ return true;
+}
+
+function validateUUID(section_id, value) {
+ if (!value)
+ return true;
+ else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null)
+ return _('Expecting: %s').format(_('valid uuid'));
return true;
}
@@ -1374,11 +1308,77 @@ function validateUrl(section_id, value) {
return true;
}
-function validateUUID(section_id, value) {
- if (!value)
+function validateBase64Key(length, section_id, value) {
+ /* Thanks to luci-proto-wireguard */
+ if (value)
+ if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
+ return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
+
+ return true;
+}
+
+function validateMTLSClientAuth(type_option, section_id, value) {
+ // If `client-auth-type` is set to "verify-if-given" or "require-and-verify", `client-auth-cert` must not be empty.
+ const auth_type = this.section.getOption(type_option).formvalue(section_id);
+ //this.section.getUIElement('tls_client_auth_type').getValue();
+ if (!value && ["verify-if-given", "require-and-verify"].includes(auth_type))
+ return _('Expecting: %s').format(_('non-empty value'));
+
+ return true;
+}
+
+function validatePresetIDs(disoption_list, section_id) {
+ let node;
+ let hm_prefmt = glossary[this.section.sectiontype].prefmt;
+ let preset_ids = [
+ 'fchomo_direct_list',
+ 'fchomo_proxy_list',
+ 'fchomo_china_list',
+ 'fchomo_gfw_list'
+ ];
+
+ if (preset_ids.map((v) => hm_prefmt.format(v)).includes(section_id)) {
+ disoption_list.forEach(([typ, opt]) => {
+ node = this.section.getUIElement(section_id, opt)?.node;
+ (typ ? node?.querySelector(typ) : node)?.setAttribute(typ === 'textarea' ? 'readOnly' : 'disabled', '');
+ });
+
+ this.map.findElement('id', 'cbi-fchomo-' + section_id)?.lastChild.querySelector('.cbi-button-remove')?.remove();
+ }
+
+ return true;
+}
+
+function validateShadowsocksPassword(encmode, section_id, value) {
+ let length = shadowsocks_cipher_length[encmode];
+ if (typeof length !== 'undefined') {
+ length = Math.ceil(length/3)*4;
+ if (encmode.match(/^2022-/)) {
+ return validateBase64Key.call(this, length, section_id, value);
+ } else {
+ if (length === 0 && !value)
+ return _('Expecting: %s').format(_('non-empty value'));
+ if (length !== 0 && value.length !== length)
+ return _('Expecting: %s').format(_('valid key length with %d characters').format(length));
+ }
+ } else
return true;
- else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null)
- return _('Expecting: %s').format(_('valid uuid'));
+
+ return true;
+}
+
+function validateUniqueValue(section_id, value) {
+ if (!value)
+ return _('Expecting: %s').format(_('non-empty value'));
+
+ let duplicate = false;
+ uci.sections(this.config, this.section.sectiontype, (res) => {
+ if (res['.name'] !== section_id)
+ if (res[this.option] === value)
+ duplicate = true;
+ });
+ if (duplicate)
+ return _('Expecting: %s').format(_('unique value'));
return true;
}
@@ -1563,6 +1563,7 @@ return baseclass.extend({
getFeatures,
getServiceStatus,
getClashAPI,
+ // load
loadDefaultLabel,
loadModalTitle,
loadProxyGroupLabel,
@@ -1570,6 +1571,7 @@ return baseclass.extend({
loadProviderLabel,
loadRulesetLabel,
loadSubRuleGroup,
+ // render
renderStatus,
updateStatus,
getDashURL,
@@ -1578,20 +1580,23 @@ return baseclass.extend({
handleReload,
handleRemoveIdles,
textvalue2Value,
- validatePresetIDs,
+ // validate
validateAuth,
validateAuthUsername,
validateAuthPassword,
validateCommonPort,
- validateJson,
- validateMTLSClientAuth,
- validateBase64Key,
- validateShadowsocksPassword,
validateBytesize,
validateTimeDuration,
- validateUniqueValue,
- validateUrl,
+ validateJson,
validateUUID,
+ validateUrl,
+ // validate with bind this
+ validateBase64Key,
+ validateMTLSClientAuth,
+ validatePresetIDs,
+ validateShadowsocksPassword,
+ validateUniqueValue,
+ // file operations
lsDir,
readFile,
writeFile,
diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js
index 65dcf6b669..3719e6bd7b 100644
--- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js
+++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js
@@ -658,8 +658,8 @@ function renderRules(s, uciconfig) {
o.load = function(section_id) {
return form.DummyValue.prototype.load.call(this, section_id) || new RulesEntry().toString('json');
}
- o.write = L.bind(form.AbstractValue.prototype.write, o);
- o.remove = L.bind(form.AbstractValue.prototype.remove, o);
+ o.write = form.AbstractValue.prototype.write;
+ o.remove = form.AbstractValue.prototype.remove;
o.editable = true;
o = s.option(form.ListValue, 'type', _('Type'));
@@ -894,7 +894,7 @@ return view.extend({
/* General fields */
so = ss.taboption('field_general', form.Value, 'label', _('Label'));
- so.load = L.bind(hm.loadDefaultLabel, so);
+ so.load = hm.loadDefaultLabel;
so.validate = function(section_id, value) {
if (value.match(/[,]/))
return _('Expecting: %s').format(_('not included ","'));
@@ -975,14 +975,14 @@ return view.extend({
hm.health_checkurls.forEach((res) => {
so.value.apply(so, res);
})
- so.validate = L.bind(hm.validateUrl, so);
+ so.validate = hm.validateUrl;
so.depends({type: 'select', '!reverse': true});
so.modalonly = true;
so = ss.taboption('field_health', form.Value, 'interval', _('Health check interval'),
_('In seconds. %s will be used if empty.').format('600'));
so.placeholder = '600';
- so.validate = L.bind(hm.validateTimeDuration, so);
+ so.validate = hm.validateTimeDuration;
so.depends({type: 'select', '!reverse': true});
so.modalonly = true;
@@ -1128,14 +1128,14 @@ return view.extend({
/* Import mihomo config end */
so = ss.option(form.Value, 'label', _('Label'));
- so.load = L.bind(hm.loadDefaultLabel, so);
- so.validate = L.bind(hm.validateUniqueValue, so);
+ so.load = hm.loadDefaultLabel;
+ so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.option(form.Flag, 'enabled', _('Enable'));
so.default = so.enabled;
so.editable = true;
- so.validate = function(section_id, value) {
+ so.validate = function(/* ... */) {
let n = 0;
return hm.validatePresetIDs.call(this, [
@@ -1224,8 +1224,8 @@ return view.extend({
/* Import mihomo config end */
so = ss.option(form.Value, 'label', _('Label'));
- so.load = L.bind(hm.loadDefaultLabel, so);
- so.validate = L.bind(hm.validateUniqueValue, so);
+ so.load = hm.loadDefaultLabel;
+ so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.option(form.Flag, 'enabled', _('Enable'));
@@ -1235,7 +1235,7 @@ return view.extend({
so = ss.option(form.Value, 'group', _('Sub rule group'));
so.value('sub-rule1');
so.rmempty = false;
- so.validate = L.bind(hm.validateAuthUsername, so);
+ so.validate = hm.validateAuthUsername;
so.editable = true;
renderRules(ss, data[0]);
@@ -1259,28 +1259,28 @@ return view.extend({
so = ss.option(form.MultiValue, 'boot_server', _('Bootstrap DNS server'),
_('Used to resolve the domain of the DNS server. Must be IP.'));
so.default = 'default-dns';
- so.load = L.bind(loadDNSServerLabel, so);
- so.validate = L.bind(validateNameserver, so);
+ so.load = loadDNSServerLabel;
+ so.validate = validateNameserver;
so.rmempty = false;
so = ss.option(form.MultiValue, 'bootnode_server', _('Bootstrap DNS server (Node)'),
_('Used to resolve the domain of the Proxy node.'));
so.default = 'default-dns';
- so.load = L.bind(loadDNSServerLabel, so);
- so.validate = L.bind(validateNameserver, so);
+ so.load = loadDNSServerLabel;
+ so.validate = validateNameserver;
so.rmempty = false;
so = ss.option(form.MultiValue, 'default_server', _('Default DNS server'));
so.description = uci.get(data[0], so.section.section, 'fallback_server') ? _('Final DNS server (For non-poisoned domains)') : _('Final DNS server');
so.default = 'default-dns';
- so.load = L.bind(loadDNSServerLabel, so);
- so.validate = L.bind(validateNameserver, so);
+ so.load = loadDNSServerLabel;
+ so.validate = validateNameserver;
so.rmempty = false;
so = ss.option(form.MultiValue, 'fallback_server', _('Fallback DNS server'));
so.description = uci.get(data[0], so.section.section, 'fallback_server') ? _('Final DNS server (For poisoned domains)') : _('Fallback DNS server');
- so.load = L.bind(loadDNSServerLabel, so);
- so.validate = L.bind(validateNameserver, so);
+ so.load = loadDNSServerLabel;
+ so.validate = validateNameserver;
so.onchange = function(ev, section_id, value) {
let ddesc = this.section.getUIElement(section_id, 'default_server').node.nextSibling;
let fdesc = ev.target.nextSibling;
@@ -1358,8 +1358,8 @@ return view.extend({
/* Import mihomo config end */
so = ss.option(form.Value, 'label', _('Label'));
- so.load = L.bind(hm.loadDefaultLabel, so);
- so.validate = L.bind(hm.validateUniqueValue, so);
+ so.load = hm.loadDefaultLabel;
+ so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.option(form.Flag, 'enabled', _('Enable'));
@@ -1367,8 +1367,8 @@ return view.extend({
so.editable = true;
so = ss.option(form.DummyValue, 'address', _('Address'));
- so.write = L.bind(form.AbstractValue.prototype.write, so);
- so.remove = L.bind(form.AbstractValue.prototype.remove, so);
+ so.write = form.AbstractValue.prototype.write;
+ so.remove = form.AbstractValue.prototype.remove;
so.editable = true;
so = ss.option(form.Value, 'addr', _('Address'));
@@ -1580,14 +1580,14 @@ return view.extend({
/* Import mihomo config end */
so = ss.option(form.Value, 'label', _('Label'));
- so.load = L.bind(hm.loadDefaultLabel, so);
- so.validate = L.bind(hm.validateUniqueValue, so);
+ so.load = hm.loadDefaultLabel;
+ so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.option(form.Flag, 'enabled', _('Enable'));
so.default = so.enabled;
so.editable = true;
- so.validate = function(section_id, value) {
+ so.validate = function(/* ... */) {
return hm.validatePresetIDs.call(this, [
['select', 'type'],
['', 'rule_set']
@@ -1628,8 +1628,8 @@ return view.extend({
so = ss.option(form.MultiValue, 'server', _('DNS server'));
so.value('default-dns');
so.default = 'default-dns';
- so.load = L.bind(loadDNSServerLabel, so);
- so.validate = L.bind(validateNameserver, so);
+ so.load = loadDNSServerLabel;
+ so.validate = validateNameserver;
so.rmempty = false;
so.editable = true;
diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js
index 4f3589f936..11796df5b3 100644
--- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js
+++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js
@@ -304,7 +304,7 @@ return view.extend({
.format('https://raw.githubusercontent.com/fcshark-org/openwrt-fchomo/refs/heads/initialpack/initial.tgz'));
so.inputstyle = 'action';
so.inputtitle = _('Upload...');
- so.onclick = L.bind(hm.uploadInitialPack, so);
+ so.onclick = hm.uploadInitialPack;
}
so = ss.option(form.Flag, 'auto_update', _('Auto update'),
@@ -424,12 +424,12 @@ return view.extend({
so = ss.option(form.Value, 'keep_alive_interval', _('TCP-Keep-Alive interval'),
_('In seconds. %s will be used if empty.').format('30'));
so.placeholder = '30';
- so.validate = L.bind(hm.validateTimeDuration, so);
+ so.validate = hm.validateTimeDuration;
so = ss.option(form.Value, 'keep_alive_idle', _('TCP-Keep-Alive idle timeout'),
_('In seconds. %s will be used if empty.').format('600'));
so.placeholder = '600';
- so.validate = L.bind(hm.validateTimeDuration, so);
+ so.validate = hm.validateTimeDuration;
/* Global Authentication */
o = s.taboption('general', form.SectionValue, '_global', form.NamedSection, 'global', 'fchomo', _('Global Authentication'));
@@ -438,7 +438,7 @@ return view.extend({
so = ss.option(form.DynamicList, 'authentication', _('User Authentication'));
so.datatype = 'list(string)';
so.placeholder = 'user1:pass1';
- so.validate = L.bind(hm.validateAuth, so);
+ so.validate = hm.validateAuth;
so = ss.option(form.DynamicList, 'skip_auth_prefixes', _('No Authentication IP ranges'));
so.datatype = 'list(cidr)';
@@ -514,7 +514,7 @@ return view.extend({
_('Aging time of NAT map maintained by client.') +
_('In seconds. %s will be used if empty.').format('300'));
so.placeholder = '300';
- so.validate = L.bind(hm.validateTimeDuration, so);
+ so.validate = hm.validateTimeDuration;
so = ss.option(form.Flag, 'tun_endpoint_independent_nat', _('Endpoint-Independent NAT'),
_('Performance may degrade slightly, so it is not recommended to enable on when it is not needed.'));
@@ -573,7 +573,7 @@ return view.extend({
}
}
so.renderWidget = function(section_id, option_index, cfgvalue) {
- let node = hm.TextValue.prototype.renderWidget.apply(this, arguments);
+ let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
const cbid = this.cbid(section_id) + '._outer_sni';
node.appendChild(E('div', { 'class': 'control-group' }, [
@@ -826,7 +826,7 @@ return view.extend({
if (!res[0].match(/_udpport$/))
so.value.apply(so, res);
})
- so.validate = L.bind(hm.validateCommonPort, so);
+ so.validate = hm.validateCommonPort;
so = ss.taboption('routing_control', hm.RichMultiValue, 'routing_udpport', _('Routing ports') + ' (UDP)',
_('Specify target ports to be proxied. Multiple ports must be separated by commas.'));
@@ -835,7 +835,7 @@ return view.extend({
if (!res[0].match(/_tcpport$/))
so.value.apply(so, res);
})
- so.validate = L.bind(hm.validateCommonPort, so);
+ so.validate = hm.validateCommonPort;
so = ss.taboption('routing_control', form.ListValue, 'routing_mode', _('Routing mode'),
_('Routing mode of the traffic enters mihomo via firewall rules.'));
diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js
index aa6a2b12f2..cccc952581 100644
--- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js
+++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js
@@ -84,7 +84,7 @@ function getRuntimeLog(name, option_index, section_id, in_table) {
);
let log;
- poll.add(L.bind(function() {
+ poll.add(function() {
return fs.read_direct(String.format('%s/%s.log', hm_dir, filename), 'text')
.then((res) => {
log = E('pre', { 'wrap': 'pre' }, [
@@ -104,7 +104,7 @@ function getRuntimeLog(name, option_index, section_id, in_table) {
dom.content(log_textarea, log);
});
- }));
+ });
return E([
E('style', [ css ]),
diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js
index 223141c1c6..b448e03061 100644
--- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js
+++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js
@@ -143,8 +143,8 @@ return view.extend({
ss.tab('field_dial', _('Dial fields'));
so = ss.taboption('field_general', form.Value, 'label', _('Label'));
- so.load = L.bind(hm.loadDefaultLabel, so);
- so.validate = L.bind(hm.validateUniqueValue, so);
+ so.load = hm.loadDefaultLabel;
+ so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.taboption('field_general', form.Flag, 'enabled', _('Enable'));
@@ -170,19 +170,19 @@ return view.extend({
/* HTTP / SOCKS fields */
/* hm.validateAuth */
so = ss.taboption('field_general', form.Value, 'username', _('Username'));
- so.validate = L.bind(hm.validateAuthUsername, so);
+ so.validate = hm.validateAuthUsername;
so.depends({type: /^(http|socks5|mieru|ssh)$/});
so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'password', _('Password'));
so.password = true;
- so.validate = L.bind(hm.validateAuthPassword, so);
+ so.validate = hm.validateAuthPassword;
so.depends({type: /^(http|socks5|mieru|trojan|anytls|hysteria2|tuic|ssh)$/});
so.modalonly = true;
so = ss.taboption('field_general', hm.TextValue, 'headers', _('HTTP header'));
so.placeholder = '{\n "User-Agent": [\n "Clash/v1.18.0",\n "mihomo/1.18.3"\n ],\n "Authorization": [\n //"token 1231231"\n ]\n}';
- so.validate = L.bind(hm.validateJson, so);
+ so.validate = hm.validateJson;
so.depends('type', 'http');
so.modalonly = true;
@@ -288,7 +288,7 @@ return view.extend({
so = ss.taboption('field_general', form.Value, 'snell_psk', _('Pre-shared key'));
so.password = true;
so.rmempty = false;
- so.validate = L.bind(hm.validateAuthPassword, so);
+ so.validate = hm.validateAuthPassword;
so.depends('type', 'snell');
so.modalonly = true;
@@ -303,7 +303,7 @@ return view.extend({
/* TUIC fields */
so = ss.taboption('field_general', form.Value, 'uuid', _('UUID'));
so.rmempty = false;
- so.validate = L.bind(hm.validateUUID, so);
+ so.validate = hm.validateUUID;
so.depends('type', 'tuic');
so.modalonly = true;
@@ -401,14 +401,14 @@ return view.extend({
so = ss.taboption('field_general', form.Value, 'anytls_idle_session_check_interval', _('Idle session check interval'),
_('In seconds.'));
so.placeholder = '30';
- so.validate = L.bind(hm.validateTimeDuration, so);
+ so.validate = hm.validateTimeDuration;
so.depends('type', 'anytls');
so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'anytls_idle_session_timeout', _('Idle session timeout'),
_('In seconds.'));
so.placeholder = '30';
- so.validate = L.bind(hm.validateTimeDuration, so);
+ so.validate = hm.validateTimeDuration;
so.depends('type', 'anytls');
so.modalonly = true;
@@ -421,7 +421,7 @@ return view.extend({
/* VMess / VLESS fields */
so = ss.taboption('field_general', form.Value, 'vmess_uuid', _('UUID'));
so.rmempty = false;
- so.validate = L.bind(hm.validateUUID, so);
+ so.validate = hm.validateUUID;
so.depends({type: /^(vmess|vless)$/});
so.modalonly = true;
@@ -630,13 +630,13 @@ return view.extend({
}
so = ss.taboption('field_vless_encryption', form.Value, 'vless_encryption_encryption', _('encryption'));
- so.renderWidget = function(section_id, option_index, cfgvalue) {
+ so.renderWidget = function(/* ... */) {
let node = form.Value.prototype.renderWidget.apply(this, arguments);
node.firstChild.style.width = '30em';
return node;
- },
+ }
so.rmempty = false;
so.depends('vless_encryption', '1');
so.modalonly = true;
@@ -904,7 +904,7 @@ return view.extend({
so = ss.taboption('field_transport', hm.TextValue, 'transport_http_headers', _('HTTP header'));
so.placeholder = '{\n "Host": "example.com",\n "Connection": [\n "keep-alive"\n ]\n}';
- so.validate = L.bind(hm.validateJson, so);
+ so.validate = hm.validateJson;
so.depends({transport_enabled: '1', transport_type: /^(http|ws)$/});
so.modalonly = true;
@@ -1162,8 +1162,8 @@ return view.extend({
/* General fields */
so = ss.taboption('field_general', form.Value, 'label', _('Label'));
- so.load = L.bind(hm.loadDefaultLabel, so);
- so.validate = L.bind(hm.validateUniqueValue, so);
+ so.load = hm.loadDefaultLabel;
+ so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.taboption('field_general', form.Flag, 'enabled', _('Enable'));
@@ -1216,7 +1216,7 @@ return view.extend({
so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'url', _('Provider URL'));
- so.validate = L.bind(hm.validateUrl, so);
+ so.validate = hm.validateUrl;
so.rmempty = false;
so.depends('type', 'http');
so.modalonly = true;
@@ -1224,13 +1224,13 @@ return view.extend({
so = ss.taboption('field_general', form.Value, 'size_limit', _('Size limit'),
_('In bytes. %s will be used if empty.').format('0'));
so.placeholder = '0';
- so.validate = L.bind(hm.validateBytesize, so);
+ so.validate = hm.validateBytesize;
so.depends('type', 'http');
so = ss.taboption('field_general', form.Value, 'interval', _('Update interval'),
_('In seconds. %s will be used if empty.').format('86400'));
so.placeholder = '86400';
- so.validate = L.bind(hm.validateTimeDuration, so);
+ so.validate = hm.validateTimeDuration;
so.depends('type', 'http');
so = ss.taboption('field_general', form.ListValue, 'proxy', _('Proxy group'),
@@ -1240,14 +1240,14 @@ return view.extend({
so.value.apply(so, res);
})
so.load = L.bind(hm.loadProxyGroupLabel, so, hm.preset_outbound.direct);
- so.textvalue = L.bind(hm.textvalue2Value, so);
+ so.textvalue = hm.textvalue2Value;
//so.editable = true;
so.depends('type', 'http');
so = ss.taboption('field_general', hm.TextValue, 'header', _('HTTP header'),
_('Custom HTTP header.'));
so.placeholder = '{\n "User-Agent": [\n "Clash/v1.18.0",\n "mihomo/1.18.3"\n ],\n "Accept": [\n //"application/vnd.github.v3.raw"\n ],\n "Authorization": [\n //"token 1231231"\n ]\n}';
- so.validate = L.bind(hm.validateJson, so);
+ so.validate = hm.validateJson;
so.depends('type', 'http');
so.modalonly = true;
@@ -1267,7 +1267,7 @@ return view.extend({
_('For format see %s.')
.format('https://wiki.metacubex.one/config/proxy-providers/#overrideproxy-name', _('override.proxy-name')));
so.placeholder = '{"pattern": "IPLC-(.*?)倍", "target": "iplc x $1"}';
- so.validate = L.bind(hm.validateJson, so);
+ so.validate = hm.validateJson;
so.depends({type: 'inline', '!reverse': true});
so.modalonly = true;
@@ -1360,7 +1360,7 @@ return view.extend({
hm.health_checkurls.forEach((res) => {
so.value.apply(so, res);
})
- so.validate = L.bind(hm.validateUrl, so);
+ so.validate = hm.validateUrl;
so.retain = true;
so.depends({type: 'inline', '!reverse': true});
so.modalonly = true;
@@ -1368,7 +1368,7 @@ return view.extend({
so = ss.taboption('field_health', form.Value, 'health_interval', _('Health check interval'),
_('In seconds. %s will be used if empty.').format('600'));
so.placeholder = '600';
- so.validate = L.bind(hm.validateTimeDuration, so);
+ so.validate = hm.validateTimeDuration;
so.depends({type: 'inline', '!reverse': true});
so.modalonly = true;
@@ -1414,7 +1414,7 @@ return view.extend({
so.modalonly = true;
so = ss.option(form.DummyValue, '_update');
- so.cfgvalue = L.bind(hm.renderResDownload, so);
+ so.cfgvalue = hm.renderResDownload;
so.editable = true;
so.modalonly = false;
/* Provider END */
@@ -1435,8 +1435,8 @@ return view.extend({
ss.hm_lowcase_only = true;
so = ss.option(form.Value, 'label', _('Label'));
- so.load = L.bind(hm.loadDefaultLabel, so);
- so.validate = L.bind(hm.validateUniqueValue, so);
+ so.load = hm.loadDefaultLabel;
+ so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.option(form.Flag, 'enabled', _('Enable'));
@@ -1447,7 +1447,7 @@ return view.extend({
so.value('node', _('Proxy Node'));
so.value('provider', _('Provider'));
so.default = 'node';
- so.textvalue = L.bind(hm.textvalue2Value, so);
+ so.textvalue = hm.textvalue2Value;
so = ss.option(form.DummyValue, '_value', _('Value'));
so.load = function(section_id) {
diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js
index b5db959a41..e7617790cd 100644
--- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js
+++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js
@@ -188,7 +188,7 @@ return view.extend({
o.placeholder = 'http(s)://github.com/ACL4SSR/ACL4SSR/raw/refs/heads/master/Clash/Providers/BanAD.yaml?fmt=yaml&behav=classical&rawq=good%3Djob#BanAD\n' +
'file:///example.txt?fmt=text&behav=domain&fill=LmNuCg#CN%20TLD\n' +
'inline://LSAnLmhrJwoK?behav=domain#HK%20TLD\n';
- o.handleFn = L.bind(function(textarea) {
+ o.handleFn = function(textarea) {
let input_links = textarea.getValue().trim().split('\n');
let imported_count = 0;
@@ -216,7 +216,7 @@ return view.extend({
return this.save();
else
return ui.hideModal();
- }, o);
+ }
return o.render();
}
@@ -246,14 +246,14 @@ return view.extend({
/* Import mihomo config and Import rule-set links and Remove idle files end */
o = s.option(form.Value, 'label', _('Label'));
- o.load = L.bind(hm.loadDefaultLabel, o);
- o.validate = L.bind(hm.validateUniqueValue, o);
+ o.load = hm.loadDefaultLabel;
+ o.validate = hm.validateUniqueValue;
o.modalonly = true;
o = s.option(form.Flag, 'enabled', _('Enable'));
o.default = o.enabled;
o.editable = true;
- o.validate = function(section_id, value) {
+ o.validate = function(/* ... */) {
return hm.validatePresetIDs.call(this, [
['select', 'type'],
['select', 'behavior'],
@@ -345,7 +345,7 @@ return view.extend({
o.modalonly = true;
o = s.option(form.Value, 'url', _('Rule set URL'));
- o.validate = L.bind(hm.validateUrl, o);
+ o.validate = hm.validateUrl;
o.rmempty = false;
o.depends('type', 'http');
o.modalonly = true;
@@ -353,13 +353,13 @@ return view.extend({
o = s.option(form.Value, 'size_limit', _('Size limit'),
_('In bytes. %s will be used if empty.').format('0'));
o.placeholder = '0';
- o.validate = L.bind(hm.validateBytesize, o);
+ o.validate = hm.validateBytesize;
o.depends('type', 'http');
o = s.option(form.Value, 'interval', _('Update interval'),
_('In seconds. %s will be used if empty.').format('259200'));
o.placeholder = '259200';
- o.validate = L.bind(hm.validateTimeDuration, o);
+ o.validate = hm.validateTimeDuration;
o.depends('type', 'http');
o = s.option(form.ListValue, 'proxy', _('Proxy group'),
@@ -369,12 +369,12 @@ return view.extend({
o.value.apply(o, res);
})
o.load = L.bind(hm.loadProxyGroupLabel, o, hm.preset_outbound.direct);
- o.textvalue = L.bind(hm.textvalue2Value, o);
+ o.textvalue = hm.textvalue2Value;
//o.editable = true;
o.depends('type', 'http');
o = s.option(form.DummyValue, '_update');
- o.cfgvalue = L.bind(hm.renderResDownload, o);
+ o.cfgvalue = hm.renderResDownload;
o.editable = true;
o.modalonly = false;
/* Rule set END */
diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js
index 98aefaf899..437af04433 100644
--- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js
+++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js
@@ -13,7 +13,7 @@ const CBIDummyCopyValue = form.Value.extend({
readonly: true,
renderWidget: function(section_id, option_index, cfgvalue) {
- let node = form.Value.prototype.renderWidget.apply(this, arguments);
+ let node = form.Value.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
node.classList.add('control-group');
node.firstChild.style.width = '30em';
@@ -199,8 +199,8 @@ return view.extend({
/* General fields */
o = s.taboption('field_general', form.Value, 'label', _('Label'));
- o.load = L.bind(hm.loadDefaultLabel, o);
- o.validate = L.bind(hm.validateUniqueValue, o);
+ o.load = hm.loadDefaultLabel;
+ o.validate = hm.validateUniqueValue;
o.modalonly = true;
o = s.taboption('field_general', form.Flag, 'enabled', _('Enable'));
@@ -227,7 +227,7 @@ return view.extend({
o.datatype = 'or(port, portrange)';
//o.placeholder = '1080,2079-2080,3080'; // @fw4 does not support port lists with commas
o.rmempty = false;
- //o.validate = L.bind(hm.validateCommonPort, o); // @fw4 does not support port lists with commas
+ //o.validate = hm.validateCommonPort; // @fw4 does not support port lists with commas
// @dev: Features under development
// @rule
@@ -236,13 +236,13 @@ return view.extend({
/* HTTP / SOCKS fields */
/* hm.validateAuth */
o = s.taboption('field_general', form.Value, 'username', _('Username'));
- o.validate = L.bind(hm.validateAuthUsername, o);
+ o.validate = hm.validateAuthUsername;
o.depends({type: /^(http|socks|mixed|trojan|anytls|hysteria2)$/});
o.modalonly = true;
o = s.taboption('field_general', hm.GenValue, 'password', _('Password'));
o.password = true;
- o.validate = L.bind(hm.validateAuthPassword, o);
+ o.validate = hm.validateAuthPassword;
o.rmempty = false;
o.depends({type: /^(http|socks|mixed|trojan|anytls|hysteria2)$/, username: /.+/});
o.depends({type: /^(tuic)$/, uuid: /.+/});
@@ -308,7 +308,7 @@ return view.extend({
/* Tuic fields */
o = s.taboption('field_general', hm.GenValue, 'uuid', _('UUID'));
o.rmempty = false;
- o.validate = L.bind(hm.validateUUID, o);
+ o.validate = hm.validateUUID;
o.depends('type', 'tuic');
o.modalonly = true;
@@ -330,14 +330,14 @@ return view.extend({
o = s.taboption('field_general', form.Value, 'tuic_max_idle_time', _('Idle timeout'),
_('In seconds.'));
o.default = '15000';
- o.validate = L.bind(hm.validateTimeDuration, o);
+ o.validate = hm.validateTimeDuration;
o.depends('type', 'tuic');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'tuic_authentication_timeout', _('Auth timeout'),
_('In seconds.'));
o.default = '1000';
- o.validate = L.bind(hm.validateTimeDuration, o);
+ o.validate = hm.validateTimeDuration;
o.depends('type', 'tuic');
o.modalonly = true;
@@ -372,7 +372,7 @@ return view.extend({
/* VMess / VLESS fields */
o = s.taboption('field_general', hm.GenValue, 'vmess_uuid', _('UUID'));
o.rmempty = false;
- o.validate = L.bind(hm.validateUUID, o);
+ o.validate = hm.validateUUID;
o.depends({type: /^(vmess|vless)$/});
o.modalonly = true;
@@ -573,7 +573,7 @@ return view.extend({
}
}
o.renderWidget = function(section_id, option_index, cfgvalue) {
- let node = hm.TextValue.prototype.renderWidget.apply(this, arguments);
+ let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
const cbid = this.cbid(section_id) + '._keytype_select';
const selected = this.hm_options.type;
@@ -608,7 +608,7 @@ return view.extend({
return JSON.stringify(new VlessEncryption(uci.get(data[0], section_id, 'vless_encryption_hmpayload'))['keypairs'], null, 2);
}
o.validate = function(section_id, value) {
- let result = hm.validateJson.apply(this, arguments);
+ let result = hm.validateJson.call(this, section_id, value);
if (result === true) {
let keypairs = JSON.parse(value.trim());
@@ -715,8 +715,8 @@ return view.extend({
o = s.taboption('field_tls', form.Value, 'tls_client_auth_cert_path', _('Client Auth Certificate path') + _(' (mTLS)'),
_('The %s public key, in PEM format.').format(_('Client')));
o.value('/etc/fchomo/certs/client_publickey.pem');
- o.validate = function(section_id, value) {
- return hm.validateMTLSClientAuth.call(this, 'tls_client_auth_type', section_id, value);
+ o.validate = function(/* ... */) {
+ return hm.validateMTLSClientAuth.call(this, 'tls_client_auth_type', ...arguments);
}
o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/});
o.modalonly = true;
@@ -745,7 +745,7 @@ return view.extend({
}
}
o.renderWidget = function(section_id, option_index, cfgvalue) {
- let node = hm.TextValue.prototype.renderWidget.apply(this, arguments);
+ let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
const cbid = this.cbid(section_id) + '._outer_sni';
node.appendChild(E('div', { 'class': 'control-group' }, [
diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile
index d90f8a67cf..dc0a9769a3 100644
--- a/small/v2ray-geodata/Makefile
+++ b/small/v2ray-geodata/Makefile
@@ -30,7 +30,7 @@ define Download/geosite
HASH:=1a7dad0ceaaf1f6d12fef585576789699bd1c6ea014c887c04b94cb9609350e9
endef
-GEOSITE_IRAN_VER:=202509220041
+GEOSITE_IRAN_VER:=202509290038
GEOSITE_IRAN_FILE:=iran.dat.$(GEOSITE_IRAN_VER)
define Download/geosite-ir
URL:=https://github.com/bootmortis/iran-hosted-domains/releases/download/$(GEOSITE_IRAN_VER)/
diff --git a/youtube-dl/.github/workflows/ci.yml b/youtube-dl/.github/workflows/ci.yml
index 8234e0ccb6..c7a8fff844 100644
--- a/youtube-dl/.github/workflows/ci.yml
+++ b/youtube-dl/.github/workflows/ci.yml
@@ -122,12 +122,12 @@ jobs:
ytdl-test-set: ${{ fromJSON(needs.select.outputs.test-set) }}
run-tests-ext: [sh]
include:
- - os: windows-2019
+ - os: windows-2022
python-version: 3.4
python-impl: cpython
ytdl-test-set: ${{ contains(needs.select.outputs.test-set, 'core') && 'core' || 'nocore' }}
run-tests-ext: bat
- - os: windows-2019
+ - os: windows-2022
python-version: 3.4
python-impl: cpython
ytdl-test-set: ${{ contains(needs.select.outputs.test-set, 'download') && 'download' || 'nodownload' }}
diff --git a/youtube-dl/youtube_dl/__init__.py b/youtube-dl/youtube_dl/__init__.py
index 3c1272e7b0..202f2c9b9a 100644
--- a/youtube-dl/youtube_dl/__init__.py
+++ b/youtube-dl/youtube_dl/__init__.py
@@ -409,6 +409,8 @@ def _real_main(argv=None):
'include_ads': opts.include_ads,
'default_search': opts.default_search,
'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
+ 'youtube_player_js_version': opts.youtube_player_js_version,
+ 'youtube_player_js_variant': opts.youtube_player_js_variant,
'encoding': opts.encoding,
'extract_flat': opts.extract_flat,
'mark_watched': opts.mark_watched,
diff --git a/youtube-dl/youtube_dl/downloader/common.py b/youtube-dl/youtube_dl/downloader/common.py
index 91e691776b..8354030a9f 100644
--- a/youtube-dl/youtube_dl/downloader/common.py
+++ b/youtube-dl/youtube_dl/downloader/common.py
@@ -11,6 +11,7 @@ from ..utils import (
decodeArgument,
encodeFilename,
error_to_compat_str,
+ float_or_none,
format_bytes,
shell_quote,
timeconvert,
@@ -367,14 +368,27 @@ class FileDownloader(object):
})
return True
- min_sleep_interval = self.params.get('sleep_interval')
- if min_sleep_interval:
- max_sleep_interval = self.params.get('max_sleep_interval', min_sleep_interval)
- sleep_interval = random.uniform(min_sleep_interval, max_sleep_interval)
+ min_sleep_interval, max_sleep_interval = (
+ float_or_none(self.params.get(interval), default=0)
+ for interval in ('sleep_interval', 'max_sleep_interval'))
+
+ sleep_note = ''
+ available_at = info_dict.get('available_at')
+ if available_at:
+ forced_sleep_interval = available_at - int(time.time())
+ if forced_sleep_interval > min_sleep_interval:
+ sleep_note = 'as required by the site'
+ min_sleep_interval = forced_sleep_interval
+ if forced_sleep_interval > max_sleep_interval:
+ max_sleep_interval = forced_sleep_interval
+
+ sleep_interval = random.uniform(
+ min_sleep_interval, max_sleep_interval or min_sleep_interval)
+
+ if sleep_interval > 0:
self.to_screen(
- '[download] Sleeping %s seconds...' % (
- int(sleep_interval) if sleep_interval.is_integer()
- else '%.2f' % sleep_interval))
+ '[download] Sleeping %.2f seconds %s...' % (
+ sleep_interval, sleep_note))
time.sleep(sleep_interval)
return self.real_download(filename, info_dict)
diff --git a/youtube-dl/youtube_dl/extractor/youtube.py b/youtube-dl/youtube_dl/extractor/youtube.py
index b31798729e..0b802351d2 100644
--- a/youtube-dl/youtube_dl/extractor/youtube.py
+++ b/youtube-dl/youtube_dl/extractor/youtube.py
@@ -1,5 +1,4 @@
# coding: utf-8
-
from __future__ import unicode_literals
import collections
@@ -110,7 +109,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
'INNERTUBE_CONTEXT': {
'client': {
'clientName': 'MWEB',
- 'clientVersion': '2.20250311.03.00',
+ 'clientVersion': '2.2.20250925.01.00',
# mweb previously did not require PO Token with this UA
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)',
},
@@ -124,23 +123,36 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
'client': {
'clientName': 'TVHTML5',
'clientVersion': '7.20250312.16.00',
- 'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
+ # See: https://github.com/youtube/cobalt/blob/main/cobalt/browser/user_agent/user_agent_platform_info.cc#L506
+ 'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/25.lts.30.1034943-gold (unlike Gecko), Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)',
},
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 7,
'SUPPORTS_COOKIES': True,
},
+
'web': {
'INNERTUBE_CONTEXT': {
'client': {
'clientName': 'WEB',
- 'clientVersion': '2.20250312.04.00',
+ 'clientVersion': '2.20250925.01.00',
+ 'userAgent': 'Mozilla/5.0',
},
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
'REQUIRE_PO_TOKEN': True,
'SUPPORTS_COOKIES': True,
},
+ # Safari UA returns pre-merged video+audio 144p/240p/360p/720p/1080p HLS formats
+ 'web_safari': {
+ 'INNERTUBE_CONTEXT': {
+ 'client': {
+ 'clientName': 'WEB',
+ 'clientVersion': '2.20250925.01.00',
+ 'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)',
+ },
+ },
+ },
}
def _login(self):
@@ -419,10 +431,15 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
T(compat_str)))
def _extract_ytcfg(self, video_id, webpage):
- return self._parse_json(
- self._search_regex(
- r'ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;', webpage, 'ytcfg',
- default='{}'), video_id, fatal=False) or {}
+ ytcfg = self._search_json(
+ r'ytcfg\.set\s*\(', webpage, 'ytcfg', video_id,
+ end_pattern=r'\)\s*;', default={})
+
+ traverse_obj(ytcfg, (
+ 'INNERTUBE_CONTEXT', 'client', 'configInfo',
+ T(lambda x: x.pop('appInstallData', None))))
+
+ return ytcfg
def _extract_video(self, renderer):
video_id = renderer['videoId']
@@ -694,7 +711,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
r'/(?P[a-zA-Z0-9_-]{8,})/player(?:_ias(?:_tce)?\.vflset(?:/[a-zA-Z]{2,3}_[a-zA-Z]{2,3})?|-plasma-ias-(?:phone|tablet)-[a-z]{2}_[A-Z]{2}\.vflset)/base\.js$',
r'\b(?Pvfl[a-zA-Z0-9_-]{6,})\b.*?\.js$',
)
- _SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'vtt')
+ _SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
_GEO_BYPASS = False
@@ -1587,7 +1604,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
_PLAYER_JS_VARIANT_MAP = (
('main', 'player_ias.vflset/en_US/base.js'),
+ ('tcc', 'player_ias_tcc.vflset/en_US/base.js'),
('tce', 'player_ias_tce.vflset/en_US/base.js'),
+ ('es5', 'player_es5.vflset/en_US/base.js'),
+ ('es6', 'player_es6.vflset/en_US/base.js'),
('tv', 'tv-player-ias.vflset/tv-player-ias.js'),
('tv_es6', 'tv-player-es6.vflset/tv-player-es6.js'),
('phone', 'player-plasma-ias-phone-en_US.vflset/base.js'),
@@ -1605,6 +1625,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
self._code_cache = {}
self._player_cache = {}
+ def _get_player_js_version(self):
+ player_js_version = self.get_param('youtube_player_js_version') or '20348@0004de42'
+ sts_hash = self._search_regex(
+ ('^actual$(^)?(^)?', r'^([0-9]{5,})@([0-9a-f]{8,})$'),
+ player_js_version, 'player_js_version', group=(1, 2), default=None)
+ if sts_hash:
+ return sts_hash
+ self.report_warning(
+ 'Invalid player JS version "{0}" specified. '
+ 'It should be "{1}" or in the format of {2}'.format(
+ player_js_version, 'actual', 'SignatureTimeStamp@Hash'), only_once=True)
+ return None, None
+
# *ytcfgs, webpage=None
def _extract_player_url(self, *ytcfgs, **kw_webpage):
if ytcfgs and not isinstance(ytcfgs[0], dict):
@@ -1615,19 +1648,43 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
webpage or '', 'player URL', fatal=False)
if player_url:
ytcfgs = ytcfgs + ({'PLAYER_JS_URL': player_url},)
- return traverse_obj(
+ player_url = traverse_obj(
ytcfgs, (Ellipsis, 'PLAYER_JS_URL'), (Ellipsis, 'WEB_PLAYER_CONTEXT_CONFIGS', Ellipsis, 'jsUrl'),
get_all=False, expected_type=lambda u: urljoin('https://www.youtube.com', u))
+ player_id_override = self._get_player_js_version()[1]
+
+ requested_js_variant = self.get_param('youtube_player_js_variant') or 'main'
+ variant_js = next(
+ (v for k, v in self._PLAYER_JS_VARIANT_MAP if k == requested_js_variant),
+ None)
+ if variant_js:
+ player_id = player_id_override or self._extract_player_info(player_url)
+ original_url = player_url
+ player_url = '/s/player/{0}/{1}'.format(player_id, variant_js)
+ if original_url != player_url:
+ self.write_debug(
+ 'Forcing "{0}" player JS variant for player {1}\n'
+ ' original url = {2}'.format(
+ requested_js_variant, player_id, original_url),
+ only_once=True)
+ elif requested_js_variant != 'actual':
+ self.report_warning(
+ 'Invalid player JS variant name "{0}" requested. '
+ 'Valid choices are: {1}'.format(
+ requested_js_variant, ','.join(k for k, _ in self._PLAYER_JS_VARIANT_MAP)),
+ only_once=True)
+
+ return urljoin('https://www.youtube.com', player_url)
+
def _download_player_url(self, video_id, fatal=False):
res = self._download_webpage(
'https://www.youtube.com/iframe_api',
note='Downloading iframe API JS', video_id=video_id, fatal=fatal)
player_version = self._search_regex(
r'player\\?/([0-9a-fA-F]{8})\\?/', res or '', 'player version', fatal=fatal,
- default=NO_DEFAULT if res else None)
- if player_version:
- return 'https://www.youtube.com/s/player/{0}/player_ias.vflset/en_US/base.js'.format(player_version)
+ default=NO_DEFAULT if res else None) or None
+ return player_version and 'https://www.youtube.com/s/player/{0}/player_ias.vflset/en_US/base.js'.format(player_version)
def _signature_cache_id(self, example_sig):
""" Return a string representation of a signature """
@@ -2014,9 +2071,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=False):
"""
Extract signatureTimestamp (sts)
+
Required to tell API what sig/player version is in use.
"""
- sts = traverse_obj(ytcfg, 'STS', expected_type=int)
+ sts = traverse_obj(
+ (self._get_player_js_version(), ytcfg),
+ (0, 0),
+ (1, 'STS'),
+ expected_type=int_or_none)
+
if sts:
return sts
@@ -2163,8 +2226,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
video_id = self._match_id(url)
base_url = self.http_scheme() + '//www.youtube.com/'
webpage_url = base_url + 'watch?v=' + video_id
+ ua = traverse_obj(self._INNERTUBE_CLIENTS, (
+ 'web', 'INNERTUBE_CONTEXT', 'client', 'userAgent'))
+ headers = {'User-Agent': ua} if ua else None
webpage = self._download_webpage(
- webpage_url + '&bpctr=9999999999&has_verified=1', video_id, fatal=False)
+ webpage_url + '&bpctr=9999999999&has_verified=1', video_id,
+ headers=headers, fatal=False)
player_response = None
player_url = None
@@ -2174,12 +2241,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
video_id, 'initial player response')
is_live = traverse_obj(player_response, ('videoDetails', 'isLive'))
+ fetched_timestamp = None
if False and not player_response:
player_response = self._call_api(
'player', {'videoId': video_id}, video_id)
if True or not player_response:
origin = 'https://www.youtube.com'
pb_context = {'html5Preference': 'HTML5_PREF_WANTS'}
+ fetched_timestamp = int(time.time())
player_url = self._extract_player_url(webpage)
ytcfg = self._extract_ytcfg(video_id, webpage or '')
@@ -2246,6 +2315,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
hls = traverse_obj(
(player_response, api_player_response),
(Ellipsis, 'streamingData', 'hlsManifestUrl', T(url_or_none)))
+ fetched_timestamp = int(time.time())
if len(hls) == 2 and not hls[0] and hls[1]:
player_response['streamingData']['hlsManifestUrl'] = hls[1]
else:
@@ -2257,13 +2327,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
player_response['videoDetails'] = video_details
def is_agegated(playability):
- if not isinstance(playability, dict):
- return
+ # playability: dict
+ if not playability:
+ return False
if playability.get('desktopLegacyAgeGateReason'):
return True
- reasons = filter(None, (playability.get(r) for r in ('status', 'reason')))
+ reasons = traverse_obj(playability, (('status', 'reason'),))
AGE_GATE_REASONS = (
'confirm your age', 'age-restricted', 'inappropriate', # reason
'age_verification_required', 'age_check_required', # status
@@ -2321,15 +2392,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
trailer_video_id, self.ie_key(), trailer_video_id)
def get_text(x):
- if not x:
- return
- text = x.get('simpleText')
- if text and isinstance(text, compat_str):
- return text
- runs = x.get('runs')
- if not isinstance(runs, list):
- return
- return ''.join([r['text'] for r in runs if isinstance(r.get('text'), compat_str)])
+ return ''.join(traverse_obj(
+ x, (('simpleText',),), ('runs', Ellipsis, 'text'),
+ expected_type=compat_str))
search_meta = (
(lambda x: self._html_search_meta(x, webpage, default=None))
@@ -2412,6 +2477,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
lower = lambda s: s.lower()
+ if is_live:
+ fetched_timestamp = None
+ elif fetched_timestamp is not None:
+ # Handle preroll waiting period
+ preroll_sleep = self.get_param('youtube_preroll_sleep')
+ preroll_sleep = int_or_none(preroll_sleep, default=6)
+ fetched_timestamp += preroll_sleep
+
for fmt in streaming_formats:
if fmt.get('targetDurationSec'):
continue
@@ -2508,6 +2581,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'downloader_options': {'http_chunk_size': CHUNK_SIZE}, # No longer useful?
})
+ if fetched_timestamp:
+ dct['available_at'] = fetched_timestamp
+
formats.append(dct)
def process_manifest_format(f, proto, client_name, itag, all_formats=False):
@@ -2525,6 +2601,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
if f.get('source_preference') is None:
f['source_preference'] = -1
+ # Deprioritize since its pre-merged m3u8 formats may have lower quality audio streams
+ if client_name == 'web_safari' and proto == 'hls' and not is_live:
+ f['source_preference'] -= 1
+
if itag in ('616', '235'):
f['format_note'] = join_nonempty(f.get('format_note'), 'Premium', delim=' ')
f['source_preference'] += 100
@@ -2541,15 +2621,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
hls_manifest_url = streaming_data.get('hlsManifestUrl')
if hls_manifest_url:
- for f in self._extract_m3u8_formats(
+ formats.extend(
+ f for f in self._extract_m3u8_formats(
hls_manifest_url, video_id, 'mp4',
- entry_protocol='m3u8_native', live=is_live, fatal=False):
+ entry_protocol='m3u8_native', live=is_live, fatal=False)
if process_manifest_format(
- f, 'hls', None, self._search_regex(
- r'/itag/(\d+)', f['url'], 'itag', default=None)):
- formats.append(f)
+ f, 'hls', None, self._search_regex(
+ r'/itag/(\d+)', f['url'], 'itag', default=None)))
- if self._downloader.params.get('youtube_include_dash_manifest', True):
+ if self.get_param('youtube_include_dash_manifest', True):
dash_manifest_url = streaming_data.get('dashManifestUrl')
if dash_manifest_url:
for f in self._extract_mpd_formats(
@@ -2576,7 +2656,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
playability_status,
lambda x: x['errorScreen']['playerErrorMessageRenderer'],
dict) or {}
- reason = get_text(pemr.get('reason')) or playability_status.get('reason')
+ reason = get_text(pemr.get('reason')) or playability_status.get('reason') or ''
subreason = pemr.get('subreason')
if subreason:
subreason = clean_html(get_text(subreason))
@@ -2588,7 +2668,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
self.raise_geo_restricted(
subreason, countries)
reason += '\n' + subreason
+
if reason:
+ if 'sign in' in reason.lower():
+ self.raise_login_required(remove_end(reason, 'This helps protect our community. Learn more'))
+ elif traverse_obj(playability_status, ('errorScreen', 'playerCaptchaViewModel', T(dict))):
+ reason += '. YouTube is requiring a captcha challenge before playback'
raise ExtractorError(reason, expected=True)
self._sort_formats(formats)
@@ -2691,6 +2776,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
for fmt in self._SUBTITLE_FORMATS:
query.update({
'fmt': fmt,
+ # xosf=1 causes undesirable text position data for vtt, json3 & srv* subtitles
+ # See: https://github.com/yt-dlp/yt-dlp/issues/13654
+ 'xosf': []
})
lang_subs.append({
'ext': fmt,
@@ -2732,7 +2820,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
for d_k, s_ks in [('start', ('start', 't')), ('end', ('end',))]:
d_k += '_time'
if d_k not in info and k in s_ks:
- info[d_k] = parse_duration(query[k][0])
+ info[d_k] = parse_duration(v[0])
if video_description:
# Youtube Music Auto-generated description
@@ -2761,6 +2849,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
initial_data = self._call_api(
'next', {'videoId': video_id}, video_id, fatal=False)
+ initial_sdcr = None
if initial_data:
chapters = self._extract_chapters_from_json(
initial_data, video_id, duration)
@@ -2780,9 +2869,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
for next_num, content in enumerate(contents, start=1):
mmlir = content.get('macroMarkersListItemRenderer') or {}
start_time = chapter_time(mmlir)
- end_time = chapter_time(try_get(
- contents, lambda x: x[next_num]['macroMarkersListItemRenderer'])) \
- if next_num < len(contents) else duration
+ end_time = (traverse_obj(
+ contents, (next_num, 'macroMarkersListItemRenderer', T(chapter_time)))
+ if next_num < len(contents) else duration)
if start_time is None or end_time is None:
continue
chapters.append({
@@ -2888,12 +2977,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
info['track'] = mrr_contents_text
# this is not extraction but spelunking!
- carousel_lockups = traverse_obj(
- initial_data,
- ('engagementPanels', Ellipsis, 'engagementPanelSectionListRenderer',
- 'content', 'structuredDescriptionContentRenderer', 'items', Ellipsis,
- 'videoDescriptionMusicSectionRenderer', 'carouselLockups', Ellipsis),
- expected_type=dict) or []
+ initial_sdcr = traverse_obj(initial_data, (
+ 'engagementPanels', Ellipsis, 'engagementPanelSectionListRenderer',
+ 'content', 'structuredDescriptionContentRenderer', T(dict)),
+ get_all=False)
+ carousel_lockups = traverse_obj(initial_sdcr, (
+ 'items', Ellipsis, 'videoDescriptionMusicSectionRenderer',
+ 'carouselLockups', Ellipsis, T(dict))) or []
# try to reproduce logic from metadataRowContainerRenderer above (if it still is)
fields = (('ALBUM', 'album'), ('ARTIST', 'artist'), ('SONG', 'track'), ('LICENSES', 'license'))
# multiple_songs ?
@@ -2918,6 +3008,23 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
self.mark_watched(video_id, player_response)
+ # Fallbacks for missing metadata
+ if initial_sdcr:
+ if info.get('description') is None:
+ info['description'] = traverse_obj(initial_sdcr, (
+ 'items', Ellipsis, 'expandableVideoDescriptionBodyRenderer',
+ 'attributedDescriptionBodyText', 'content', T(compat_str)),
+ get_all=False)
+ # videoDescriptionHeaderRenderer also has publishDate/channel/handle/ucid, but not needed
+ if info.get('title') is None:
+ info['title'] = traverse_obj(
+ (initial_sdcr, initial_data),
+ (0, 'items', Ellipsis, 'videoDescriptionHeaderRenderer', T(dict)),
+ (1, 'playerOverlays', 'playerOverlayRenderer', 'videoDetails',
+ 'playerOverlayVideoDetailsRenderer', T(dict)),
+ expected_type=lambda x: self._get_text(x, 'title'),
+ get_all=False)
+
return merge_dicts(
info, {
'uploader_id': self._extract_uploader_id(owner_profile_url),
@@ -3428,38 +3535,46 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor):
if not content_id:
return
content_type = view_model.get('contentType')
- if content_type not in ('LOCKUP_CONTENT_TYPE_PLAYLIST', 'LOCKUP_CONTENT_TYPE_PODCAST'):
+ if content_type == 'LOCKUP_CONTENT_TYPE_VIDEO':
+ ie = YoutubeIE
+ url = update_url_query(
+ 'https://www.youtube.com/watch', {'v': content_id}),
+ thumb_keys = (None,)
+ elif content_type in ('LOCKUP_CONTENT_TYPE_PLAYLIST', 'LOCKUP_CONTENT_TYPE_PODCAST'):
+ ie = YoutubeTabIE
+ url = update_url_query(
+ 'https://www.youtube.com/playlist', {'list': content_id}),
+ thumb_keys = ('collectionThumbnailViewModel', 'primaryThumbnail')
+ else:
self.report_warning(
- 'Unsupported lockup view model content type "{0}"{1}'.format(content_type, bug_reports_message()), only_once=True)
+ 'Unsupported lockup view model content type "{0}"{1}'.format(content_type, bug_reports_message()),
+ only_once=True)
return
+ thumb_keys = ('contentImage',) + thumb_keys + ('thumbnailViewModel', 'image')
return merge_dicts(self.url_result(
- update_url_query('https://www.youtube.com/playlist', {'list': content_id}),
- ie=YoutubeTabIE.ie_key(), video_id=content_id), {
+ url, ie=ie.ie_key(), video_id=content_id), {
'title': traverse_obj(view_model, (
- 'metadata', 'lockupMetadataViewModel', 'title', 'content', T(compat_str))),
- 'thumbnails': self._extract_thumbnails(view_model, (
- 'contentImage', 'collectionThumbnailViewModel', 'primaryThumbnail',
- 'thumbnailViewModel', 'image'), final_key='sources'),
+ 'metadata', 'lockupMetadataViewModel', 'title',
+ 'content', T(compat_str))),
+ 'thumbnails': self._extract_thumbnails(
+ view_model, thumb_keys, final_key='sources'),
})
def _extract_shorts_lockup_view_model(self, view_model):
content_id = traverse_obj(view_model, (
'onTap', 'innertubeCommand', 'reelWatchEndpoint', 'videoId',
T(lambda v: v if YoutubeIE.suitable(v) else None)))
- if not content_id:
- return
return merge_dicts(self.url_result(
content_id, ie=YoutubeIE.ie_key(), video_id=content_id), {
'title': traverse_obj(view_model, (
'overlayMetadata', 'primaryText', 'content', T(compat_str))),
'thumbnails': self._extract_thumbnails(
view_model, 'thumbnail', final_key='sources'),
- })
+ }) if content_id else None
def _video_entry(self, video_renderer):
video_id = video_renderer.get('videoId')
- if video_id:
- return self._extract_video(video_renderer)
+ return self._extract_video(video_renderer) if video_id else None
def _post_thread_entries(self, post_thread_renderer):
post_renderer = try_get(
@@ -4119,6 +4234,7 @@ class YoutubeFeedsInfoExtractor(YoutubeTabIE):
Subclasses must define the _FEED_NAME property.
"""
+
_LOGIN_REQUIRED = True
@property
diff --git a/youtube-dl/youtube_dl/options.py b/youtube-dl/youtube_dl/options.py
index 61705d1f02..ce3633c418 100644
--- a/youtube-dl/youtube_dl/options.py
+++ b/youtube-dl/youtube_dl/options.py
@@ -404,6 +404,10 @@ def parseOpts(overrideArguments=None):
'-F', '--list-formats',
action='store_true', dest='listformats',
help='List all available formats of requested videos')
+ video_format.add_option(
+ '--no-list-formats',
+ action='store_false', dest='listformats',
+ help='Do not list available formats of requested videos (default)')
video_format.add_option(
'--youtube-include-dash-manifest',
action='store_true', dest='youtube_include_dash_manifest', default=True,
@@ -412,6 +416,17 @@ def parseOpts(overrideArguments=None):
'--youtube-skip-dash-manifest',
action='store_false', dest='youtube_include_dash_manifest',
help='Do not download the DASH manifests and related data on YouTube videos')
+ video_format.add_option(
+ '--youtube-player-js-variant',
+ action='store', dest='youtube_player_js_variant',
+ help='For YouTube, the player javascript variant to use for n/sig deciphering; `actual` to follow the site; default `%default`.',
+ choices=('actual', 'main', 'tcc', 'tce', 'es5', 'es6', 'tv', 'tv_es6', 'phone', 'tablet'),
+ default='main', metavar='VARIANT')
+ video_format.add_option(
+ '--youtube-player-js-version',
+ action='store', dest='youtube_player_js_version',
+ help='For YouTube, the player javascript version to use for n/sig deciphering, specified as `signature_timestamp@hash`, or `actual` to follow the site; default `%default`',
+ default='20348@0004de42', metavar='STS@HASH')
video_format.add_option(
'--merge-output-format',
action='store', dest='merge_output_format', metavar='FORMAT', default=None,
diff --git a/yt-dlp/test/test_pot/test_pot_builtin_utils.py b/yt-dlp/test/test_pot/test_pot_builtin_utils.py
index 7645ba601f..15a25cff2f 100644
--- a/yt-dlp/test/test_pot/test_pot_builtin_utils.py
+++ b/yt-dlp/test/test_pot/test_pot_builtin_utils.py
@@ -45,3 +45,8 @@ class TestGetWebPoContentBinding:
def test_invalid_base64(self, pot_request):
pot_request.visitor_data = 'invalid-base64'
assert get_webpo_content_binding(pot_request, bind_to_visitor_id=True) == (pot_request.visitor_data, ContentBindingType.VISITOR_DATA)
+
+ def test_gvs_video_id_binding_experiment(self, pot_request):
+ pot_request.context = PoTokenContext.GVS
+ pot_request._gvs_bind_to_video_id = True
+ assert get_webpo_content_binding(pot_request) == ('example-video-id', ContentBindingType.VIDEO_ID)
diff --git a/yt-dlp/yt_dlp/extractor/youtube/_video.py b/yt-dlp/yt_dlp/extractor/youtube/_video.py
index 79c183c6a5..9ef7f14dfa 100644
--- a/yt-dlp/yt_dlp/extractor/youtube/_video.py
+++ b/yt-dlp/yt_dlp/extractor/youtube/_video.py
@@ -2955,9 +2955,20 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
# TODO(future): This validation should be moved into pot framework.
# Some sort of middleware or validation provider perhaps?
+ gvs_bind_to_video_id = False
+ experiments = traverse_obj(ytcfg, (
+ 'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'serializedExperimentFlags', {urllib.parse.parse_qs}))
+ if 'true' in traverse_obj(experiments, (..., 'html5_generate_content_po_token', -1)):
+ self.write_debug(
+ f'{video_id}: Detected experiment to bind GVS PO Token to video id.', only_once=True)
+ gvs_bind_to_video_id = True
+
# GVS WebPO Token is bound to visitor_data / Visitor ID when logged out.
# Must have visitor_data for it to function.
- if player_url and context == _PoTokenContext.GVS and not visitor_data and not self.is_authenticated:
+ if (
+ player_url and context == _PoTokenContext.GVS
+ and not visitor_data and not self.is_authenticated and not gvs_bind_to_video_id
+ ):
self.report_warning(
f'Unable to fetch GVS PO Token for {client} client: Missing required Visitor Data. '
f'You may need to pass Visitor Data with --extractor-args "youtube:visitor_data=XXX"', only_once=True)
@@ -2971,7 +2982,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
config_po_token = self._get_config_po_token(client, context)
if config_po_token:
# GVS WebPO token is bound to data_sync_id / account Session ID when logged in.
- if player_url and context == _PoTokenContext.GVS and not data_sync_id and self.is_authenticated:
+ if (
+ player_url and context == _PoTokenContext.GVS
+ and not data_sync_id and self.is_authenticated and not gvs_bind_to_video_id
+ ):
self.report_warning(
f'Got a GVS PO Token for {client} client, but missing Data Sync ID for account. Formats may not work.'
f'You may need to pass a Data Sync ID with --extractor-args "youtube:data_sync_id=XXX"')
@@ -2997,6 +3011,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
video_id=video_id,
video_webpage=webpage,
required=required,
+ _gvs_bind_to_video_id=gvs_bind_to_video_id,
**kwargs,
)
@@ -3040,6 +3055,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
data_sync_id=kwargs.get('data_sync_id'),
video_id=kwargs.get('video_id'),
request_cookiejar=self._downloader.cookiejar,
+ _gvs_bind_to_video_id=kwargs.get('_gvs_bind_to_video_id', False),
# All requests that would need to be proxied should be in the
# context of www.youtube.com or the innertube host
diff --git a/yt-dlp/yt_dlp/extractor/youtube/pot/provider.py b/yt-dlp/yt_dlp/extractor/youtube/pot/provider.py
index 13b3b1f9bb..2511edf015 100644
--- a/yt-dlp/yt_dlp/extractor/youtube/pot/provider.py
+++ b/yt-dlp/yt_dlp/extractor/youtube/pot/provider.py
@@ -58,6 +58,8 @@ class PoTokenRequest:
visitor_data: str | None = None
data_sync_id: str | None = None
video_id: str | None = None
+ # Internal, YouTube experiment on whether to bind GVS PO Token to video_id.
+ _gvs_bind_to_video_id: bool = False
# Networking parameters
request_cookiejar: YoutubeDLCookieJar = dataclasses.field(default_factory=YoutubeDLCookieJar)
diff --git a/yt-dlp/yt_dlp/extractor/youtube/pot/utils.py b/yt-dlp/yt_dlp/extractor/youtube/pot/utils.py
index a27921d4af..7f9ca078d6 100644
--- a/yt-dlp/yt_dlp/extractor/youtube/pot/utils.py
+++ b/yt-dlp/yt_dlp/extractor/youtube/pot/utils.py
@@ -42,6 +42,9 @@ def get_webpo_content_binding(
if not client_name or client_name not in webpo_clients:
return None, None
+ if request.context == PoTokenContext.GVS and request._gvs_bind_to_video_id:
+ return request.video_id, ContentBindingType.VIDEO_ID
+
if request.context == PoTokenContext.GVS or client_name in ('WEB_REMIX', ):
if request.is_authenticated:
return request.data_sync_id, ContentBindingType.DATASYNC_ID