Update On Thu Dec 4 19:44:09 CET 2025

This commit is contained in:
github-action[bot]
2025-12-04 19:44:10 +01:00
parent 2ab53f64a3
commit e0d03d241a
75 changed files with 522 additions and 162 deletions

1
.github/update.log vendored
View File

@@ -1201,3 +1201,4 @@ Update On Sun Nov 30 19:38:16 CET 2025
Update On Mon Dec 1 19:44:38 CET 2025
Update On Tue Dec 2 19:43:40 CET 2025
Update On Wed Dec 3 19:42:45 CET 2025
Update On Thu Dec 4 19:44:01 CET 2025

View File

@@ -153,8 +153,9 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
mapping["mptcp"] = proxyInfo.MPTCP
mapping["smux"] = proxyInfo.SMUX
mapping["interface"] = proxyInfo.Interface
mapping["dialer-proxy"] = proxyInfo.DialerProxy
mapping["routing-mark"] = proxyInfo.RoutingMark
mapping["provider-name"] = proxyInfo.ProviderName
mapping["dialer-proxy"] = proxyInfo.DialerProxy
return json.Marshal(mapping)
}
@@ -177,14 +178,12 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
p.history.Pop()
}
state, ok := p.extra.Load(url)
if !ok {
state = &internalProxyState{
state, _ := p.extra.LoadOrStoreFn(url, func() *internalProxyState {
return &internalProxyState{
history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true),
}
p.extra.Store(url, state)
}
})
if !satisfied {
record.Delay = 0

View File

@@ -6,7 +6,7 @@ import (
"strconv"
"time"
CN "github.com/metacubex/mihomo/common/net"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/anytls"
@@ -63,7 +63,7 @@ func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
// create uot on tcp
destination := M.SocksaddrFromNet(metadata.UDPAddr())
return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
return newPacketConn(N.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
}
// SupportUOT implements C.ProxyAdapter
@@ -90,12 +90,13 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
name: option.Name,
addr: addr,
tp: C.AnyTLS,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
}

View File

@@ -27,16 +27,17 @@ type ProxyAdapter interface {
type Base struct {
name string
addr string
iface string
tp C.AdapterType
pdName string
udp bool
xudp bool
tfo bool
mpTcp bool
iface string
rmark int
id string
prefer C.DNSPrefer
dialer C.Dialer
id string
}
// Name implements C.ProxyAdapter
@@ -85,6 +86,7 @@ func (b *Base) ProxyInfo() (info C.ProxyInfo) {
info.SMUX = false
info.Interface = b.iface
info.RoutingMark = b.rmark
info.ProviderName = b.pdName
return
}
@@ -164,10 +166,14 @@ type BasicOption struct {
MPTCP bool `proxy:"mptcp,omitempty"`
Interface string `proxy:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty"`
IPVersion string `proxy:"ip-version,omitempty"`
IPVersion C.DNSPrefer `proxy:"ip-version,omitempty"`
DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy
//
// The following parameters are used internally, assign value by the structure decoder are disallowed
//
DialerForAPI C.Dialer `proxy:"-"` // the dialer used for API usage has higher priority than all the above configurations.
ProviderName string `proxy:"-"`
}
func (b *BasicOption) NewDialer(opts []dialer.Option) C.Dialer {
@@ -213,6 +219,7 @@ func NewBase(opt BaseOption) *Base {
type conn struct {
N.ExtendedConn
chain C.Chain
pdChain C.Chain
adapterAddr string
}
@@ -234,9 +241,15 @@ func (c *conn) Chains() C.Chain {
return c.chain
}
// ProviderChains implements C.Connection
func (c *conn) ProviderChains() C.Chain {
return c.pdChain
}
// AppendToChains implements C.Connection
func (c *conn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
c.pdChain = append(c.pdChain, a.ProxyInfo().ProviderName)
}
func (c *conn) Upstream() any {
@@ -259,7 +272,7 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn
c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
}
cc := &conn{N.NewExtendedConn(c), nil, a.Addr()}
cc := &conn{N.NewExtendedConn(c), nil, nil, a.Addr()}
cc.AppendToChains(a)
return cc
}
@@ -267,6 +280,7 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
type packetConn struct {
N.EnhancePacketConn
chain C.Chain
pdChain C.Chain
adapterName string
connID string
adapterAddr string
@@ -287,9 +301,15 @@ func (c *packetConn) Chains() C.Chain {
return c.chain
}
// ProviderChains implements C.Connection
func (c *packetConn) ProviderChains() C.Chain {
return c.pdChain
}
// AppendToChains implements C.Connection
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
c.pdChain = append(c.pdChain, a.ProxyInfo().ProviderName)
}
func (c *packetConn) LocalAddr() net.Addr {
@@ -318,7 +338,7 @@ func newPacketConn(pc net.PacketConn, a ProxyAdapter) C.PacketConn {
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
}
cpc := &packetConn{epc, nil, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP}
cpc := &packetConn{epc, nil, nil, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP}
cpc.AppendToChains(a)
return cpc
}

View File

@@ -69,12 +69,13 @@ func NewDirectWithOption(option DirectOption) *Direct {
Base: &Base{
name: option.Name,
tp: C.Direct,
pdName: option.ProviderName,
udp: true,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
loopBack: loopback.NewDetector(),
}

View File

@@ -158,12 +158,13 @@ func NewDnsWithOption(option DnsOption) *Dns {
Base: &Base{
name: option.Name,
tp: C.Dns,
pdName: option.ProviderName,
udp: true,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
}
}

View File

@@ -170,11 +170,12 @@ func NewHttp(option HttpOption) (*Http, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http,
pdName: option.ProviderName,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
user: option.UserName,
pass: option.Password,

View File

@@ -243,11 +243,12 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
name: option.Name,
addr: addr,
tp: C.Hysteria,
pdName: option.ProviderName,
udp: true,
tfo: option.FastOpen,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
client: client,

View File

@@ -9,7 +9,7 @@ import (
"strconv"
"time"
CN "github.com/metacubex/mihomo/common/net"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/proxydialer"
@@ -85,7 +85,7 @@ func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if pc == nil {
return nil, errors.New("packetConn is nil")
}
return newPacketConn(CN.NewThreadSafePacketConn(pc), h), nil
return newPacketConn(N.NewThreadSafePacketConn(pc), h), nil
}
// Close implements C.ProxyAdapter
@@ -110,10 +110,11 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
name: option.Name,
addr: addr,
tp: C.Hysteria2,
pdName: option.ProviderName,
udp: true,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
}
@@ -189,7 +190,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
CWND: option.CWND,
UdpMTU: option.UdpMTU,
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, option.IPVersion)
if err != nil {
return nil, err
}

View File

@@ -8,7 +8,7 @@ import (
"strconv"
"sync"
CN "github.com/metacubex/mihomo/common/net"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
@@ -104,7 +104,7 @@ func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
if err != nil {
return nil, fmt.Errorf("dial to %s failed: %w", metadata.UDPAddr(), err)
}
return newPacketConn(CN.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil
return newPacketConn(N.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil
}
// SupportUOT implements C.ProxyAdapter
@@ -167,12 +167,13 @@ func NewMieru(option MieruOption) (*Mieru, error) {
Base: &Base{
name: option.Name,
addr: addr,
iface: option.Interface,
tp: C.Mieru,
pdName: option.ProviderName,
udp: option.UDP,
xudp: false,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
client: c,

View File

@@ -458,12 +458,13 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
name: option.Name,
addr: addr,
tp: C.Shadowsocks,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
method: method,

View File

@@ -162,12 +162,13 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
cipher: coreCiph,

View File

@@ -3,7 +3,7 @@ package outbound
import (
"context"
CN "github.com/metacubex/mihomo/common/net"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
@@ -59,7 +59,7 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
if pc == nil {
return nil, E.New("packetConn is nil")
}
return newPacketConn(CN.NewThreadSafePacketConn(pc), s), nil
return newPacketConn(N.NewThreadSafePacketConn(pc), s), nil
}
func (s *SingMux) SupportUDP() bool {

View File

@@ -165,12 +165,13 @@ func NewSnell(option SnellOption) (*Snell, error) {
name: option.Name,
addr: addr,
tp: C.Snell,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
psk: psk,

View File

@@ -190,12 +190,13 @@ func NewSocks5(option Socks5Option) (*Socks5, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
user: option.UserName,

View File

@@ -186,10 +186,11 @@ func NewSsh(option SshOption) (*Ssh, error) {
name: option.Name,
addr: addr,
tp: C.Ssh,
pdName: option.ProviderName,
udp: false,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
config: &config,

View File

@@ -238,12 +238,13 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
name: option.Name,
addr: baseConf.ServerAddress,
tp: C.Sudoku,
pdName: option.ProviderName,
udp: true,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
table: table,

View File

@@ -288,12 +288,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
name: option.Name,
addr: addr,
tp: C.Trojan,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
hexPassword: trojan.Key(option.Password),

View File

@@ -252,11 +252,12 @@ func NewTuic(option TuicOption) (*Tuic, error) {
name: option.Name,
addr: addr,
tp: C.Tuic,
pdName: option.ProviderName,
udp: true,
tfo: option.FastOpen,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
tlsConfig: tlsClientConfig,

View File

@@ -417,13 +417,14 @@ func NewVless(option VlessOption) (*Vless, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vless,
pdName: option.ProviderName,
udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
client: client,
option: &option,

View File

@@ -427,13 +427,14 @@ func NewVmess(option VmessOption) (*Vmess, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
pdName: option.ProviderName,
udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
client: client,
option: &option,

View File

@@ -170,10 +170,11 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.WireGuard,
pdName: option.ProviderName,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
}
outbound.dialer = option.NewDialer(outbound.DialOptions())

View File

@@ -18,6 +18,7 @@ func ParseProxy(mapping map[string]any, options ...ProxyOption) (C.Proxy, error)
opt := applyProxyOptions(options...)
basicOption := outbound.BasicOption{
DialerForAPI: opt.DialerForAPI,
ProviderName: opt.ProviderName,
}
var (
@@ -186,6 +187,7 @@ func ParseProxy(mapping map[string]any, options ...ProxyOption) (C.Proxy, error)
type proxyOption struct {
DialerForAPI C.Dialer
ProviderName string
}
func applyProxyOptions(options ...ProxyOption) proxyOption {
@@ -203,3 +205,9 @@ func WithDialerForAPI(dialer C.Dialer) ProxyOption {
opt.DialerForAPI = dialer
}
}
func WithProviderName(name string) ProxyOption {
return func(opt *proxyOption) {
opt.ProviderName = name
}
}

View File

@@ -99,7 +99,7 @@ func ParseProxyProvider(name string, mapping map[string]any) (P.ProxyProvider, e
}
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, uint(schema.HealthCheck.TestTimeout), hcInterval, schema.HealthCheck.Lazy, expectedStatus)
parser, err := NewProxiesParser(schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override)
parser, err := NewProxiesParser(name, schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override)
if err != nil {
return nil, err
}

View File

@@ -156,7 +156,7 @@ func (pp *proxySetProvider) Initial() error {
func (pp *proxySetProvider) closeAllConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
for _, chain := range c.Chains() {
for _, chain := range c.ProviderChains() {
if chain == pp.Name() {
_ = c.Close()
break
@@ -330,7 +330,7 @@ func (cp *CompatibleProvider) Close() error {
return cp.compatibleProvider.Close()
}
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
func NewProxiesParser(pdName string, filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
var excludeTypeArray []string
if excludeType != "" {
excludeTypeArray = strings.Split(excludeType, "|")
@@ -448,7 +448,7 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
}
}
proxy, err := adapter.ParseProxy(mapping)
proxy, err := adapter.ParseProxy(mapping, adapter.WithProviderName(pdName))
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}

View File

@@ -517,6 +517,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
fieldName = tagValue
}
if tagValue == "-" {
continue
}
rawMapKey := reflect.ValueOf(fieldName)
rawMapVal := dataVal.MapIndex(rawMapKey)
if !rawMapVal.IsValid() {

View File

@@ -308,3 +308,27 @@ func TestStructure_Ignore(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, s.MustIgnore, "oldData")
}
func TestStructure_IgnoreInNest(t *testing.T) {
rawMap := map[string]any{
"-": "newData",
}
type TP struct {
MustIgnore string `test:"-"`
}
s := struct {
TP
}{TP{MustIgnore: "oldData"}}
err := decoder.Decode(rawMap, &s)
assert.Nil(t, err)
assert.Equal(t, s.MustIgnore, "oldData")
// test omitempty
delete(rawMap, "-")
err = decoder.Decode(rawMap, &s)
assert.Nil(t, err)
assert.Equal(t, s.MustIgnore, "oldData")
}

View File

@@ -59,6 +59,7 @@ var ErrNotSupport = errors.New("no support")
type Connection interface {
Chains() Chain
ProviderChains() Chain
AppendToChains(adapter ProxyAdapter)
RemoteDestination() string
}
@@ -108,6 +109,7 @@ type ProxyInfo struct {
SMUX bool
Interface string
RoutingMark int
ProviderName string
DialerProxy string
}

View File

@@ -86,12 +86,17 @@ func (d DNSPrefer) String() string {
}
}
func NewDNSPrefer(prefer string) DNSPrefer {
if p, ok := dnsPreferMap[prefer]; ok {
return p
} else {
return DualStack
func (d DNSPrefer) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
func (d *DNSPrefer) UnmarshalText(data []byte) error {
p, exist := dnsPreferMap[strings.ToLower(string(data))]
if !exist {
p = DualStack
}
*d = p
return nil
}
// FilterModeMapping is a mapping for FilterMode enum

View File

@@ -28,6 +28,7 @@ type TrackerInfo struct {
DownloadTotal atomic.Int64 `json:"download"`
Start time.Time `json:"start"`
Chain C.Chain `json:"chains"`
ProviderChain C.Chain `json:"providerChains"`
Rule string `json:"rule"`
RulePayload string `json:"rulePayload"`
}
@@ -126,6 +127,7 @@ func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.R
Start: time.Now(),
Metadata: metadata,
Chain: conn.Chains(),
ProviderChain: conn.ProviderChains(),
Rule: "",
UploadTotal: atomic.NewInt64(uploadTotal),
DownloadTotal: atomic.NewInt64(downloadTotal),
@@ -217,6 +219,7 @@ func NewUDPTracker(conn C.PacketConn, manager *Manager, metadata *C.Metadata, ru
Start: time.Now(),
Metadata: metadata,
Chain: conn.Chains(),
ProviderChain: conn.ProviderChains(),
Rule: "",
UploadTotal: atomic.NewInt64(uploadTotal),
DownloadTotal: atomic.NewInt64(downloadTotal),

View File

@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.17",
"mihomo_alpha": "alpha-fdb7cb1",
"mihomo_alpha": "alpha-32ce513",
"clash_rs": "v0.9.2",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.2-alpha+sha.e1f8fbb"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2025-12-02T22:21:02.066Z"
"updated_at": "2025-12-03T22:21:43.313Z"
}

View File

@@ -299,6 +299,9 @@ ifeq ($(DUMP),1)
ifneq ($(CONFIG_PCIEPORTBUS),)
FEATURES += pcie
endif
ifneq ($(CONFIG_PWM),)
FEATURES += pwm
endif
ifneq ($(CONFIG_USB)$(CONFIG_USB_SUPPORT),)
ifneq ($(CONFIG_USB_ARCH_HAS_HCD)$(CONFIG_USB_EHCI_HCD),)
FEATURES += usb

View File

@@ -153,8 +153,9 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
mapping["mptcp"] = proxyInfo.MPTCP
mapping["smux"] = proxyInfo.SMUX
mapping["interface"] = proxyInfo.Interface
mapping["dialer-proxy"] = proxyInfo.DialerProxy
mapping["routing-mark"] = proxyInfo.RoutingMark
mapping["provider-name"] = proxyInfo.ProviderName
mapping["dialer-proxy"] = proxyInfo.DialerProxy
return json.Marshal(mapping)
}
@@ -177,14 +178,12 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
p.history.Pop()
}
state, ok := p.extra.Load(url)
if !ok {
state = &internalProxyState{
state, _ := p.extra.LoadOrStoreFn(url, func() *internalProxyState {
return &internalProxyState{
history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true),
}
p.extra.Store(url, state)
}
})
if !satisfied {
record.Delay = 0

View File

@@ -6,7 +6,7 @@ import (
"strconv"
"time"
CN "github.com/metacubex/mihomo/common/net"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/anytls"
@@ -63,7 +63,7 @@ func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
// create uot on tcp
destination := M.SocksaddrFromNet(metadata.UDPAddr())
return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
return newPacketConn(N.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
}
// SupportUOT implements C.ProxyAdapter
@@ -90,12 +90,13 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
name: option.Name,
addr: addr,
tp: C.AnyTLS,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
}

View File

@@ -27,16 +27,17 @@ type ProxyAdapter interface {
type Base struct {
name string
addr string
iface string
tp C.AdapterType
pdName string
udp bool
xudp bool
tfo bool
mpTcp bool
iface string
rmark int
id string
prefer C.DNSPrefer
dialer C.Dialer
id string
}
// Name implements C.ProxyAdapter
@@ -85,6 +86,7 @@ func (b *Base) ProxyInfo() (info C.ProxyInfo) {
info.SMUX = false
info.Interface = b.iface
info.RoutingMark = b.rmark
info.ProviderName = b.pdName
return
}
@@ -164,10 +166,14 @@ type BasicOption struct {
MPTCP bool `proxy:"mptcp,omitempty"`
Interface string `proxy:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty"`
IPVersion string `proxy:"ip-version,omitempty"`
IPVersion C.DNSPrefer `proxy:"ip-version,omitempty"`
DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy
//
// The following parameters are used internally, assign value by the structure decoder are disallowed
//
DialerForAPI C.Dialer `proxy:"-"` // the dialer used for API usage has higher priority than all the above configurations.
ProviderName string `proxy:"-"`
}
func (b *BasicOption) NewDialer(opts []dialer.Option) C.Dialer {
@@ -213,6 +219,7 @@ func NewBase(opt BaseOption) *Base {
type conn struct {
N.ExtendedConn
chain C.Chain
pdChain C.Chain
adapterAddr string
}
@@ -234,9 +241,15 @@ func (c *conn) Chains() C.Chain {
return c.chain
}
// ProviderChains implements C.Connection
func (c *conn) ProviderChains() C.Chain {
return c.pdChain
}
// AppendToChains implements C.Connection
func (c *conn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
c.pdChain = append(c.pdChain, a.ProxyInfo().ProviderName)
}
func (c *conn) Upstream() any {
@@ -259,7 +272,7 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn
c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
}
cc := &conn{N.NewExtendedConn(c), nil, a.Addr()}
cc := &conn{N.NewExtendedConn(c), nil, nil, a.Addr()}
cc.AppendToChains(a)
return cc
}
@@ -267,6 +280,7 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
type packetConn struct {
N.EnhancePacketConn
chain C.Chain
pdChain C.Chain
adapterName string
connID string
adapterAddr string
@@ -287,9 +301,15 @@ func (c *packetConn) Chains() C.Chain {
return c.chain
}
// ProviderChains implements C.Connection
func (c *packetConn) ProviderChains() C.Chain {
return c.pdChain
}
// AppendToChains implements C.Connection
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
c.pdChain = append(c.pdChain, a.ProxyInfo().ProviderName)
}
func (c *packetConn) LocalAddr() net.Addr {
@@ -318,7 +338,7 @@ func newPacketConn(pc net.PacketConn, a ProxyAdapter) C.PacketConn {
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
}
cpc := &packetConn{epc, nil, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP}
cpc := &packetConn{epc, nil, nil, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP}
cpc.AppendToChains(a)
return cpc
}

View File

@@ -69,12 +69,13 @@ func NewDirectWithOption(option DirectOption) *Direct {
Base: &Base{
name: option.Name,
tp: C.Direct,
pdName: option.ProviderName,
udp: true,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
loopBack: loopback.NewDetector(),
}

View File

@@ -158,12 +158,13 @@ func NewDnsWithOption(option DnsOption) *Dns {
Base: &Base{
name: option.Name,
tp: C.Dns,
pdName: option.ProviderName,
udp: true,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
}
}

View File

@@ -170,11 +170,12 @@ func NewHttp(option HttpOption) (*Http, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http,
pdName: option.ProviderName,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
user: option.UserName,
pass: option.Password,

View File

@@ -243,11 +243,12 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
name: option.Name,
addr: addr,
tp: C.Hysteria,
pdName: option.ProviderName,
udp: true,
tfo: option.FastOpen,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
client: client,

View File

@@ -9,7 +9,7 @@ import (
"strconv"
"time"
CN "github.com/metacubex/mihomo/common/net"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/proxydialer"
@@ -85,7 +85,7 @@ func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if pc == nil {
return nil, errors.New("packetConn is nil")
}
return newPacketConn(CN.NewThreadSafePacketConn(pc), h), nil
return newPacketConn(N.NewThreadSafePacketConn(pc), h), nil
}
// Close implements C.ProxyAdapter
@@ -110,10 +110,11 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
name: option.Name,
addr: addr,
tp: C.Hysteria2,
pdName: option.ProviderName,
udp: true,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
}
@@ -189,7 +190,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
CWND: option.CWND,
UdpMTU: option.UdpMTU,
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, option.IPVersion)
if err != nil {
return nil, err
}

View File

@@ -8,7 +8,7 @@ import (
"strconv"
"sync"
CN "github.com/metacubex/mihomo/common/net"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
@@ -104,7 +104,7 @@ func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
if err != nil {
return nil, fmt.Errorf("dial to %s failed: %w", metadata.UDPAddr(), err)
}
return newPacketConn(CN.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil
return newPacketConn(N.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil
}
// SupportUOT implements C.ProxyAdapter
@@ -167,12 +167,13 @@ func NewMieru(option MieruOption) (*Mieru, error) {
Base: &Base{
name: option.Name,
addr: addr,
iface: option.Interface,
tp: C.Mieru,
pdName: option.ProviderName,
udp: option.UDP,
xudp: false,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
client: c,

View File

@@ -458,12 +458,13 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
name: option.Name,
addr: addr,
tp: C.Shadowsocks,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
method: method,

View File

@@ -162,12 +162,13 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
cipher: coreCiph,

View File

@@ -3,7 +3,7 @@ package outbound
import (
"context"
CN "github.com/metacubex/mihomo/common/net"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
@@ -59,7 +59,7 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
if pc == nil {
return nil, E.New("packetConn is nil")
}
return newPacketConn(CN.NewThreadSafePacketConn(pc), s), nil
return newPacketConn(N.NewThreadSafePacketConn(pc), s), nil
}
func (s *SingMux) SupportUDP() bool {

View File

@@ -165,12 +165,13 @@ func NewSnell(option SnellOption) (*Snell, error) {
name: option.Name,
addr: addr,
tp: C.Snell,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
psk: psk,

View File

@@ -190,12 +190,13 @@ func NewSocks5(option Socks5Option) (*Socks5, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
user: option.UserName,

View File

@@ -186,10 +186,11 @@ func NewSsh(option SshOption) (*Ssh, error) {
name: option.Name,
addr: addr,
tp: C.Ssh,
pdName: option.ProviderName,
udp: false,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
config: &config,

View File

@@ -238,12 +238,13 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
name: option.Name,
addr: baseConf.ServerAddress,
tp: C.Sudoku,
pdName: option.ProviderName,
udp: true,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
table: table,

View File

@@ -288,12 +288,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
name: option.Name,
addr: addr,
tp: C.Trojan,
pdName: option.ProviderName,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
hexPassword: trojan.Key(option.Password),

View File

@@ -252,11 +252,12 @@ func NewTuic(option TuicOption) (*Tuic, error) {
name: option.Name,
addr: addr,
tp: C.Tuic,
pdName: option.ProviderName,
udp: true,
tfo: option.FastOpen,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
option: &option,
tlsConfig: tlsClientConfig,

View File

@@ -417,13 +417,14 @@ func NewVless(option VlessOption) (*Vless, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vless,
pdName: option.ProviderName,
udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
client: client,
option: &option,

View File

@@ -427,13 +427,14 @@ func NewVmess(option VmessOption) (*Vmess, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
pdName: option.ProviderName,
udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
client: client,
option: &option,

View File

@@ -170,10 +170,11 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.WireGuard,
pdName: option.ProviderName,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
prefer: option.IPVersion,
},
}
outbound.dialer = option.NewDialer(outbound.DialOptions())

View File

@@ -18,6 +18,7 @@ func ParseProxy(mapping map[string]any, options ...ProxyOption) (C.Proxy, error)
opt := applyProxyOptions(options...)
basicOption := outbound.BasicOption{
DialerForAPI: opt.DialerForAPI,
ProviderName: opt.ProviderName,
}
var (
@@ -186,6 +187,7 @@ func ParseProxy(mapping map[string]any, options ...ProxyOption) (C.Proxy, error)
type proxyOption struct {
DialerForAPI C.Dialer
ProviderName string
}
func applyProxyOptions(options ...ProxyOption) proxyOption {
@@ -203,3 +205,9 @@ func WithDialerForAPI(dialer C.Dialer) ProxyOption {
opt.DialerForAPI = dialer
}
}
func WithProviderName(name string) ProxyOption {
return func(opt *proxyOption) {
opt.ProviderName = name
}
}

View File

@@ -99,7 +99,7 @@ func ParseProxyProvider(name string, mapping map[string]any) (P.ProxyProvider, e
}
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, uint(schema.HealthCheck.TestTimeout), hcInterval, schema.HealthCheck.Lazy, expectedStatus)
parser, err := NewProxiesParser(schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override)
parser, err := NewProxiesParser(name, schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override)
if err != nil {
return nil, err
}

View File

@@ -156,7 +156,7 @@ func (pp *proxySetProvider) Initial() error {
func (pp *proxySetProvider) closeAllConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
for _, chain := range c.Chains() {
for _, chain := range c.ProviderChains() {
if chain == pp.Name() {
_ = c.Close()
break
@@ -330,7 +330,7 @@ func (cp *CompatibleProvider) Close() error {
return cp.compatibleProvider.Close()
}
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
func NewProxiesParser(pdName string, filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
var excludeTypeArray []string
if excludeType != "" {
excludeTypeArray = strings.Split(excludeType, "|")
@@ -448,7 +448,7 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
}
}
proxy, err := adapter.ParseProxy(mapping)
proxy, err := adapter.ParseProxy(mapping, adapter.WithProviderName(pdName))
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}

View File

@@ -517,6 +517,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
fieldName = tagValue
}
if tagValue == "-" {
continue
}
rawMapKey := reflect.ValueOf(fieldName)
rawMapVal := dataVal.MapIndex(rawMapKey)
if !rawMapVal.IsValid() {

View File

@@ -308,3 +308,27 @@ func TestStructure_Ignore(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, s.MustIgnore, "oldData")
}
func TestStructure_IgnoreInNest(t *testing.T) {
rawMap := map[string]any{
"-": "newData",
}
type TP struct {
MustIgnore string `test:"-"`
}
s := struct {
TP
}{TP{MustIgnore: "oldData"}}
err := decoder.Decode(rawMap, &s)
assert.Nil(t, err)
assert.Equal(t, s.MustIgnore, "oldData")
// test omitempty
delete(rawMap, "-")
err = decoder.Decode(rawMap, &s)
assert.Nil(t, err)
assert.Equal(t, s.MustIgnore, "oldData")
}

View File

@@ -59,6 +59,7 @@ var ErrNotSupport = errors.New("no support")
type Connection interface {
Chains() Chain
ProviderChains() Chain
AppendToChains(adapter ProxyAdapter)
RemoteDestination() string
}
@@ -108,6 +109,7 @@ type ProxyInfo struct {
SMUX bool
Interface string
RoutingMark int
ProviderName string
DialerProxy string
}

View File

@@ -86,12 +86,17 @@ func (d DNSPrefer) String() string {
}
}
func NewDNSPrefer(prefer string) DNSPrefer {
if p, ok := dnsPreferMap[prefer]; ok {
return p
} else {
return DualStack
func (d DNSPrefer) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
func (d *DNSPrefer) UnmarshalText(data []byte) error {
p, exist := dnsPreferMap[strings.ToLower(string(data))]
if !exist {
p = DualStack
}
*d = p
return nil
}
// FilterModeMapping is a mapping for FilterMode enum

View File

@@ -28,6 +28,7 @@ type TrackerInfo struct {
DownloadTotal atomic.Int64 `json:"download"`
Start time.Time `json:"start"`
Chain C.Chain `json:"chains"`
ProviderChain C.Chain `json:"providerChains"`
Rule string `json:"rule"`
RulePayload string `json:"rulePayload"`
}
@@ -126,6 +127,7 @@ func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.R
Start: time.Now(),
Metadata: metadata,
Chain: conn.Chains(),
ProviderChain: conn.ProviderChains(),
Rule: "",
UploadTotal: atomic.NewInt64(uploadTotal),
DownloadTotal: atomic.NewInt64(downloadTotal),
@@ -217,6 +219,7 @@ func NewUDPTracker(conn C.PacketConn, manager *Manager, metadata *C.Metadata, ru
Start: time.Now(),
Metadata: metadata,
Chain: conn.Chains(),
ProviderChain: conn.ProviderChains(),
Rule: "",
UploadTotal: atomic.NewInt64(uploadTotal),
DownloadTotal: atomic.NewInt64(downloadTotal),

View File

@@ -82,6 +82,7 @@ function index()
entry({"admin", "services", appname, "copy_node"}, call("copy_node")).leaf = true
entry({"admin", "services", appname, "clear_all_nodes"}, call("clear_all_nodes")).leaf = true
entry({"admin", "services", appname, "delete_select_nodes"}, call("delete_select_nodes")).leaf = true
entry({"admin", "services", appname, "reassign_group"}, call("reassign_group")).leaf = true
entry({"admin", "services", appname, "get_node"}, call("get_node")).leaf = true
entry({"admin", "services", appname, "save_node_order"}, call("save_node_order")).leaf = true
entry({"admin", "services", appname, "update_rules"}, call("update_rules")).leaf = true
@@ -640,6 +641,20 @@ function save_node_order()
http_write_json({ status = "ok" })
end
function reassign_group()
local ids = http.formvalue("ids") or ""
local group = http.formvalue("group") or "default"
for id in ids:gmatch("([^,]+)") do
if group ~="" and group ~= "default" then
api.sh_uci_set(appname, id, "group", group)
else
api.sh_uci_del(appname, id, "group")
end
end
api.sh_uci_commit(appname)
http_write_json({ status = "ok" })
end
function update_rules()
local update = http.formvalue("update")
luci.sys.call("lua /usr/share/passwall/rule_update.lua log '" .. update .. "' > /dev/null 2>&1 &")

View File

@@ -41,12 +41,16 @@ local api = require "luci.passwall.api"
}
function open_add_link_div() {
document.getElementById('modal-mask').style.display = 'block';
document.getElementById("add_link_div").style.display = "block";
document.body.classList.add('modal-open');
document.getElementById("nodes_link").focus();
}
function close_add_link_div() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("add_link_div").style.display = "none";
document.body.classList.remove('modal-open');
}
function add_node() {
@@ -81,13 +85,69 @@ local api = require "luci.passwall.api"
}
}
function open_reassign_group_div() {
var ids = [];
var visibleContainer = document.querySelector('#cbi-passwall-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
var doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (var i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
document.getElementById('modal-mask').style.display = 'block';
document.getElementById("reassign_group_div").style.display = "block";
document.body.classList.add('modal-open');
} else {
alert("<%:You no select nodes !%>");
}
}
}
function close_reassign_group_div() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("reassign_group_div").style.display = "none";
document.body.classList.remove('modal-open');
}
function reassign_group() {
var ids = [];
var group = (document.querySelector('#reassign_group_custom input[type="hidden"]')?.value || "default");
var visibleContainer = document.querySelector('#cbi-passwall-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
var doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (var i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
XHR.get('<%=api.url("reassign_group")%>', {
group: group,
ids: ids.join(",")
},
function(x, data) {
if (x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
}
else {
alert("<%:Error%>");
}
});
}
}
}
function add_new_node() {
window.location.href = '<%=api.url("add_node")%>?redirect=1';
}
//自定义分组下拉列表事件
document.addEventListener("DOMContentLoaded", function() {
var dropdown = document.getElementById("addlink_group_custom");
function dropdown_list_fun(div_id) {
var dropdown = document.getElementById(div_id);
if (!dropdown) return;
var display = dropdown.querySelector(".selected-display");
@@ -99,7 +159,7 @@ local api = require "luci.passwall.api"
display.addEventListener("click", function() {
list.style.display = list.style.display === "none" ? "block" : "none";
input.value = "";
input.focus();
//input.focus();
});
function selectItem(li) {
@@ -119,7 +179,8 @@ local api = require "luci.passwall.api"
});
input.addEventListener("keydown", function(e){
if (e.keyCode !== 13) return;
var isEnter = e.key === "Enter" || e.keyCode === 13;
if (!isEnter) return;
e.stopPropagation();
e.preventDefault();
@@ -135,7 +196,7 @@ local api = require "luci.passwall.api"
}
var li = Array.from(list.querySelectorAll(".dropdown-item")).find(function(el){
return el.dataset.value.toLowerCase() === val.toLowerCase();
return el.dataset.value && el.dataset.value.toLowerCase() === val.toLowerCase();
});
if (!li) {
li = document.createElement("li");
@@ -176,11 +237,20 @@ local api = require "luci.passwall.api"
list.style.display = "none";
}
});
}
document.addEventListener("DOMContentLoaded", function() {
dropdown_list_fun("addlink_group_custom");
});
document.addEventListener("DOMContentLoaded", function() {
dropdown_list_fun("reassign_group_custom");
});
//]]>
</script>
<div id="modal-mask"></div>
<div id="add_link_div">
<div id="add_link_modal_container">
<h3><%:Add the node via the link%></h3>
@@ -199,7 +269,7 @@ local api = require "luci.passwall.api"
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="addlink_group" value="">
@@ -213,6 +283,36 @@ local api = require "luci.passwall.api"
</div>
</div>
<div id="reassign_group_div">
<div id="reassign_group_modal_container">
<h3><%:Reassign Node Group%></h3>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="reassign_group_custom"><%:Group Name%></label>
<div id="reassign_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="to_group" value="">
</div>
</div>
</div>
<div id="reassign_group_button_container">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group()" value="<%:Save%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_reassign_group_div()" value="<%:Close%>" />
</div>
</div>
</div>
<div class="pw-toolbar">
<div class="pw-toolbar-field">
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_new_node()" value="<%:Add%>" />
@@ -220,9 +320,10 @@ local api = require "luci.passwall.api"
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clear_all_nodes()" value="<%:Clear all nodes%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="delete_select_nodes()" value="<%:Delete select nodes%>" />
<input class="btn cbi-button cbi-button-edit" type="button" id="select_all_btn" onclick="checked_all_node(this)" value="<%:Select all%>" />
<input class="btn cbi-button cbi-button-apply" type="submit" name="cbi.apply" value="<%:Save & Apply%>" />
<input class="btn cbi-button cbi-button-save" type="submit" name="cbi.save" value="<%:Save%>" />
<input class="btn cbi-button cbi-button-reset" type="button" value="<%:Reset%>" onclick="location.href='<%=REQUEST_URI%>'" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="open_reassign_group_div()" value="<%:Reassign Group%>" />
<!--<input class="btn cbi-button cbi-button-apply" type="submit" name="cbi.apply" value="<%:Save & Apply%>" />-->
<!--<input class="btn cbi-button cbi-button-save" type="submit" name="cbi.save" value="<%:Save%>" />-->
<!--<input class="btn cbi-button cbi-button-reset" type="button" value="<%:Reset%>" onclick="location.href='<%=REQUEST_URI%>'" />-->
</div>
</div>
@@ -239,7 +340,28 @@ local api = require "luci.passwall.api"
padding: 5px 0 5px;
}
#add_link_div {
#modal-mask {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.4);
z-index: 999;
}
body.modal-open {
overflow: hidden;
pointer-events: none;
}
body.modal-open #add_link_div,
body.modal-open #reassign_group_div {
pointer-events: auto;
}
#add_link_div, #reassign_group_div {
display: none;
position: fixed;
left: 50%;
@@ -254,7 +376,7 @@ local api = require "luci.passwall.api"
max-width: 500px;
}
#add_link_modal_container {
#add_link_modal_container, #reassign_group_modal_container {
width: 100%;
text-align: center;
display: flex;
@@ -281,7 +403,7 @@ local api = require "luci.passwall.api"
width: 100%;
}
#add_link_button_container {
#add_link_button_container, #reassign_group_button_container {
display: flex;
justify-content: space-between;
width: 100%;

View File

@@ -2008,5 +2008,11 @@ msgstr "配置的类型同样适用于手动导入节点时所指定的核心程
msgid "Group Name"
msgstr "分组名"
msgid "Reassign Group"
msgstr "调整分组"
msgid "Reassign Node Group"
msgstr "调整节点分组"
msgid "Currently using %s node"
msgstr "当前使用的 %s 节点"

View File

@@ -40,7 +40,7 @@ RUN case "$TARGETARCH" in \
&& RUSTFLAGS="-C linker=$CC" CC=$CC cargo build --target "$RUST_TARGET" --release --features "full" \
&& mv target/$RUST_TARGET/release/ss* target/release/
FROM alpine:3.22 AS sslocal
FROM alpine:3.23 AS sslocal
# NOTE: Please be careful to change the path of these binaries, refer to #1149 for more information.
COPY --from=builder /root/shadowsocks-rust/target/release/sslocal /usr/bin/
@@ -50,7 +50,7 @@ COPY --from=builder /root/shadowsocks-rust/docker/docker-entrypoint.sh /usr/bin/
ENTRYPOINT [ "docker-entrypoint.sh" ]
CMD [ "sslocal", "--log-without-time", "-c", "/etc/shadowsocks-rust/config.json" ]
FROM alpine:3.22 AS ssserver
FROM alpine:3.23 AS ssserver
COPY --from=builder /root/shadowsocks-rust/target/release/ssserver /usr/bin/
COPY --from=builder /root/shadowsocks-rust/examples/config.json /etc/shadowsocks-rust/

View File

@@ -124,8 +124,8 @@ const health_checkurls = [
const inbound_type = [
['http', _('HTTP') + ' - ' + _('TCP')],
['socks', _('SOCKS') + ' - ' + _('TCP')],
['mixed', _('Mixed') + ' - ' + _('TCP')],
['socks', _('SOCKS') + ' - ' + _('TCP/UDP')],
['mixed', _('Mixed') + ' - ' + _('TCP/UDP')],
['shadowsocks', _('Shadowsocks') + ' - ' + _('TCP/UDP')],
['mieru', _('Mieru') + ' - ' + _('TCP/UDP')],
['sudoku', _('Sudoku') + ' - ' + _('TCP')],
@@ -156,7 +156,7 @@ const load_balance_strategy = [
const outbound_type = [
['direct', _('DIRECT') + ' - ' + _('TCP/UDP')],
['http', _('HTTP') + ' - ' + _('TCP')],
['socks5', _('SOCKS5') + ' - ' + _('TCP')],
['socks5', _('SOCKS5') + ' - ' + _('TCP/UDP')],
['ss', _('Shadowsocks') + ' - ' + _('TCP/UDP')],
//['ssr', _('ShadowsocksR')], // Deprecated
['mieru', _('Mieru') + ' - ' + _('TCP/UDP')],

View File

@@ -21,13 +21,13 @@ define Download/geoip
HASH:=2445b44d9ae3ab9a867c9d1e0e244646c4c378622e14b9afaf3658ecf46a40b9
endef
GEOSITE_VER:=20251203115157
GEOSITE_VER:=20251204062603
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
URL_FILE:=dlc.dat
FILE:=$(GEOSITE_FILE)
HASH:=0a14ebf9162fd0e3486b15a40025b9c807d2d38dd4379830edbbb113ee75ea80
HASH:=3355697a91bb72c485aba8fb1748c202573da19b42f4cb050d1a72594411c34d
endef
GEOSITE_IRAN_VER:=202512010051

View File

@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6.0.0
uses: actions/checkout@v6.0.1
with:
submodules: 'recursive'
fetch-depth: '0'
@@ -110,7 +110,7 @@ jobs:
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which
- name: Checkout repo (for scripts)
uses: actions/checkout@v6.0.0
uses: actions/checkout@v6.0.1
with:
submodules: 'recursive'
fetch-depth: '0'

View File

@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6.0.0
uses: actions/checkout@v6.0.1
with:
submodules: 'recursive'
fetch-depth: '0'

View File

@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6.0.0
uses: actions/checkout@v6.0.1
with:
submodules: 'recursive'
fetch-depth: '0'

View File

@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6.0.0
uses: actions/checkout@v6.0.1
- name: Setup
uses: actions/setup-dotnet@v5.0.1

View File

@@ -1871,6 +1871,7 @@ The following extractors use this feature:
* `pot_trace`: Enable debug logging for PO Token fetching. Either `true` or `false` (default)
* `fetch_pot`: Policy to use for fetching a PO Token from providers. One of `always` (always try fetch a PO Token regardless if the client requires one for the given context), `never` (never fetch a PO Token), or `auto` (default; only fetch a PO Token if the client requires one for the given context)
* `jsc_trace`: Enable debug logging for JS Challenge fetching. Either `true` or `false` (default)
* `use_ad_playback_context`: Skip preroll ads to eliminate the mandatory wait period before download. Do NOT use this when passing premium account cookies to yt-dlp, as it will result in a loss of premium formats. Only effective with the `web`, `web_safari`, `web_music` and `mweb` player clients. Either `true` or `false` (default)
#### youtube-ejs
* `jitless`: Run suported Javascript engines in JIT-less mode. Supported runtimes are `deno`, `node` and `bun`. Provides better security at the cost of performance/speed. Do note that `node` and `bun` are still considered unsecure. Either `true` or `false` (default)

View File

@@ -704,6 +704,24 @@ class YoutubeWebArchiveIE(InfoExtractor):
'thumbnail': 'https://web.archive.org/web/20160108040020if_/https://i.ytimg.com/vi/SQCom7wjGDs/maxresdefault.jpg',
'upload_date': '20160107',
},
}, {
# dmuxed formats
'url': 'https://web.archive.org/web/20240922160632/https://www.youtube.com/watch?v=z7hzvTL3k1k',
'info_dict': {
'id': 'z7hzvTL3k1k',
'ext': 'webm',
'title': 'Praise the Lord and Pass the Ammunition (BARRXN REMIX)',
'description': 'md5:45dbf2c71c23b0734c8dfb82dd1e94b6',
'uploader': 'Barrxn',
'uploader_id': 'TheRockstar6086',
'uploader_url': 'https://www.youtube.com/user/TheRockstar6086',
'channel_id': 'UCjJPGUTtvR9uizmawn2ThqA',
'channel_url': 'https://www.youtube.com/channel/UCjJPGUTtvR9uizmawn2ThqA',
'duration': 125,
'thumbnail': r're:https?://.*\.(jpg|webp)',
'upload_date': '20201207',
},
'params': {'format': 'bv'},
}, {
'url': 'https://web.archive.org/web/http://www.youtube.com/watch?v=kH-G_aIBlFw',
'only_matching': True,
@@ -1060,6 +1078,19 @@ class YoutubeWebArchiveIE(InfoExtractor):
capture_dates.extend([self._OLDEST_CAPTURE_DATE, self._NEWEST_CAPTURE_DATE])
return orderedSet(filter(None, capture_dates))
def _parse_fmt(self, fmt, extra_info=None):
format_id = traverse_obj(fmt, ('url', {parse_qs}, 'itag', 0))
return {
'format_id': format_id,
**self._FORMATS.get(format_id, {}),
**traverse_obj(fmt, {
'url': ('url', {lambda x: f'https://web.archive.org/web/2id_/{x}'}),
'ext': ('ext', {str}),
'filesize': ('url', {parse_qs}, 'clen', 0, {int_or_none}),
}),
**(extra_info or {}),
}
def _real_extract(self, url):
video_id, url_date, url_date_2 = self._match_valid_url(url).group('id', 'date', 'date2')
url_date = url_date or url_date_2
@@ -1090,17 +1121,14 @@ class YoutubeWebArchiveIE(InfoExtractor):
info['thumbnails'] = self._extract_thumbnails(video_id)
formats = []
if video_info.get('dmux'):
for vf in traverse_obj(video_info, ('formats', 'video', lambda _, v: url_or_none(v['url']))):
formats.append(self._parse_fmt(vf, {'acodec': 'none'}))
for af in traverse_obj(video_info, ('formats', 'audio', lambda _, v: url_or_none(v['url']))):
formats.append(self._parse_fmt(af, {'vcodec': 'none'}))
else:
for fmt in traverse_obj(video_info, ('formats', lambda _, v: url_or_none(v['url']))):
format_id = traverse_obj(fmt, ('url', {parse_qs}, 'itag', 0))
formats.append({
'format_id': format_id,
**self._FORMATS.get(format_id, {}),
**traverse_obj(fmt, {
'url': ('url', {lambda x: f'https://web.archive.org/web/2id_/{x}'}),
'ext': ('ext', {str}),
'filesize': ('url', {parse_qs}, 'clen', 0, {int_or_none}),
}),
})
formats.append(self._parse_fmt(fmt))
info['formats'] = formats
return info

View File

@@ -104,6 +104,7 @@ INNERTUBE_CLIENTS = {
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
'SUPPORTS_COOKIES': True,
'SUPPORTS_AD_PLAYBACK_CONTEXT': True,
**WEB_PO_TOKEN_POLICIES,
},
# Safari UA returns pre-merged video+audio 144p/240p/360p/720p/1080p HLS formats
@@ -117,6 +118,7 @@ INNERTUBE_CLIENTS = {
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
'SUPPORTS_COOKIES': True,
'SUPPORTS_AD_PLAYBACK_CONTEXT': True,
**WEB_PO_TOKEN_POLICIES,
},
'web_embedded': {
@@ -157,6 +159,7 @@ INNERTUBE_CLIENTS = {
),
},
'SUPPORTS_COOKIES': True,
'SUPPORTS_AD_PLAYBACK_CONTEXT': True,
},
# This client now requires sign-in for every video
'web_creator': {
@@ -313,6 +316,7 @@ INNERTUBE_CLIENTS = {
),
},
'SUPPORTS_COOKIES': True,
'SUPPORTS_AD_PLAYBACK_CONTEXT': True,
},
'tv': {
'INNERTUBE_CONTEXT': {
@@ -412,6 +416,7 @@ def build_innertube_clients():
ytcfg.setdefault('SUBS_PO_TOKEN_POLICY', SubsPoTokenPolicy())
ytcfg.setdefault('REQUIRE_AUTH', False)
ytcfg.setdefault('SUPPORTS_COOKIES', False)
ytcfg.setdefault('SUPPORTS_AD_PLAYBACK_CONTEXT', False)
ytcfg.setdefault('PLAYER_PARAMS', None)
ytcfg.setdefault('AUTHENTICATED_USER_AGENT', None)
ytcfg['INNERTUBE_CONTEXT']['client'].setdefault('hl', 'en')

View File

@@ -2629,16 +2629,23 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
return {'contentCheckOk': True, 'racyCheckOk': True}
@classmethod
def _generate_player_context(cls, sts=None):
def _generate_player_context(cls, sts=None, use_ad_playback_context=False):
context = {
'html5Preference': 'HTML5_PREF_WANTS',
}
if sts is not None:
context['signatureTimestamp'] = sts
return {
'playbackContext': {
playback_context = {
'contentPlaybackContext': context,
},
}
if use_ad_playback_context:
playback_context['adPlaybackContext'] = {
'pyv': True,
}
return {
'playbackContext': playback_context,
**cls._get_checkok_params(),
}
@@ -2866,7 +2873,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
yt_query['serviceIntegrityDimensions'] = {'poToken': po_token}
sts = self._extract_signature_timestamp(video_id, player_url, webpage_ytcfg, fatal=False) if player_url else None
yt_query.update(self._generate_player_context(sts))
use_ad_playback_context = (
self._configuration_arg('use_ad_playback_context', ['false'])[0] != 'false'
and traverse_obj(INNERTUBE_CLIENTS, (client, 'SUPPORTS_AD_PLAYBACK_CONTEXT', {bool})))
yt_query.update(self._generate_player_context(sts, use_ad_playback_context))
return self._extract_response(
item_id=video_id, ep='player', query=yt_query,
ytcfg=player_ytcfg, headers=headers, fatal=True,