Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts: # go.mod # go.sum
This commit is contained in:
commit
2f339c2ef5
16 changed files with 210 additions and 74 deletions
|
@ -19,6 +19,7 @@ import (
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
"github.com/sagernet/sing/common/uot"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -29,6 +30,7 @@ type ShadowSocks struct {
|
||||||
*Base
|
*Base
|
||||||
method shadowsocks.Method
|
method shadowsocks.Method
|
||||||
|
|
||||||
|
option *ShadowSocksOption
|
||||||
// obfs
|
// obfs
|
||||||
obfsMode string
|
obfsMode string
|
||||||
obfsOption *simpleObfsOption
|
obfsOption *simpleObfsOption
|
||||||
|
@ -45,6 +47,7 @@ type ShadowSocksOption struct {
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Plugin string `proxy:"plugin,omitempty"`
|
Plugin string `proxy:"plugin,omitempty"`
|
||||||
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
|
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
|
||||||
|
UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type simpleObfsOption struct {
|
type simpleObfsOption struct {
|
||||||
|
@ -77,6 +80,10 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
|
||||||
|
metadata.Host = uot.UOTMagicAddress
|
||||||
|
metadata.DstPort = "443"
|
||||||
|
}
|
||||||
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +103,13 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, op
|
||||||
|
|
||||||
// ListenPacketContext implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
if ss.option.UDPOverTCP {
|
||||||
|
tcpConn, err := ss.DialContext(ctx, metadata, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newPacketConn(uot.NewClientConn(tcpConn), ss), nil
|
||||||
|
}
|
||||||
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -110,6 +124,19 @@ func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Meta
|
||||||
return newPacketConn(pc, ss), nil
|
return newPacketConn(pc, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||||
|
func (ss *ShadowSocks) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
|
if ss.option.UDPOverTCP {
|
||||||
|
return newPacketConn(uot.NewClientConn(c), ss), nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("no support")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportUOT implements C.ProxyAdapter
|
||||||
|
func (ss *ShadowSocks) SupportUOT() bool {
|
||||||
|
return ss.option.UDPOverTCP
|
||||||
|
}
|
||||||
|
|
||||||
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
method, err := shadowimpl.FetchMethod(option.Cipher, option.Password)
|
method, err := shadowimpl.FetchMethod(option.Cipher, option.Password)
|
||||||
|
@ -167,6 +194,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
},
|
},
|
||||||
method: method,
|
method: method,
|
||||||
|
|
||||||
|
option: &option,
|
||||||
obfsMode: obfsMode,
|
obfsMode: obfsMode,
|
||||||
v2rayOption: v2rayOption,
|
v2rayOption: v2rayOption,
|
||||||
obfsOption: obfsOption,
|
obfsOption: obfsOption,
|
||||||
|
|
|
@ -43,6 +43,7 @@ func (f *fetcher[V]) Initial() (V, error) {
|
||||||
buf []byte
|
buf []byte
|
||||||
err error
|
err error
|
||||||
isLocal bool
|
isLocal bool
|
||||||
|
forceUpdate bool
|
||||||
)
|
)
|
||||||
|
|
||||||
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
||||||
|
@ -51,10 +52,8 @@ func (f *fetcher[V]) Initial() (V, error) {
|
||||||
f.updatedAt = &modTime
|
f.updatedAt = &modTime
|
||||||
isLocal = true
|
isLocal = true
|
||||||
if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) {
|
if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) {
|
||||||
defer func() {
|
log.Infoln("[Provider] %s not updated for a long time, force refresh", f.Name())
|
||||||
log.Infoln("[Provider] %s's proxies not updated for a long time, force refresh", f.Name())
|
forceUpdate = true
|
||||||
go f.Update()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
buf, err = f.vehicle.Read()
|
buf, err = f.vehicle.Read()
|
||||||
|
@ -64,7 +63,21 @@ func (f *fetcher[V]) Initial() (V, error) {
|
||||||
return getZero[V](), err
|
return getZero[V](), err
|
||||||
}
|
}
|
||||||
|
|
||||||
proxies, err := f.parser(buf)
|
var proxies V
|
||||||
|
if forceUpdate {
|
||||||
|
var forceBuf []byte
|
||||||
|
if forceBuf, err = f.vehicle.Read(); err == nil {
|
||||||
|
if proxies, err = f.parser(forceBuf); err == nil {
|
||||||
|
isLocal = false
|
||||||
|
buf = forceBuf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil || !forceUpdate {
|
||||||
|
proxies, err = f.parser(buf)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isLocal {
|
if !isLocal {
|
||||||
return getZero[V](), err
|
return getZero[V](), err
|
||||||
|
|
|
@ -173,7 +173,7 @@ func ResolveAllIPv4WithResolver(host string, r Resolver) ([]netip.Addr, error) {
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(host)
|
ip, err := netip.ParseAddr(host)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if ip.Is4() {
|
if ip.Is4() || ip.Is4In6() {
|
||||||
return []netip.Addr{ip}, nil
|
return []netip.Addr{ip}, nil
|
||||||
}
|
}
|
||||||
return []netip.Addr{}, ErrIPVersion
|
return []netip.Addr{}, ErrIPVersion
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -13,9 +13,9 @@ require (
|
||||||
github.com/lucas-clemente/quic-go v0.27.2
|
github.com/lucas-clemente/quic-go v0.27.2
|
||||||
github.com/miekg/dns v1.1.49
|
github.com/miekg/dns v1.1.49
|
||||||
github.com/oschwald/geoip2-golang v1.7.0
|
github.com/oschwald/geoip2-golang v1.7.0
|
||||||
github.com/sagernet/sing v0.0.0-20220615170357-aa30c4140ca9
|
github.com/sagernet/sing v0.0.0-20220619130320-8793fe5e067d
|
||||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220615081955-91a0901d1c89
|
github.com/sagernet/sing-shadowsocks v0.0.0-20220619134218-830a2f478eb1
|
||||||
github.com/sagernet/sing-vmess v0.0.0-20220614092239-0052a5a383f5
|
github.com/sagernet/sing-vmess v0.0.0-20220616051646-3d3fc5d01eec
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
github.com/stretchr/testify v1.7.2
|
github.com/stretchr/testify v1.7.2
|
||||||
github.com/tobyxdd/hysteria v1.0.4
|
github.com/tobyxdd/hysteria v1.0.4
|
||||||
|
@ -28,7 +28,7 @@ require (
|
||||||
golang.org/x/exp v0.0.0-20220608143224-64259d1afd70
|
golang.org/x/exp v0.0.0-20220608143224-64259d1afd70
|
||||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
|
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
|
||||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
||||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c
|
||||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306
|
golang.org/x/time v0.0.0-20220411224347-583f2d630306
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4
|
golang.zx2c4.com/wireguard v0.0.0-20220601130007-6a08d81f6bc4
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e
|
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220328111914-004c22c5647e
|
||||||
|
|
16
go.sum
16
go.sum
|
@ -305,12 +305,12 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0
|
||||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/sagernet/sing v0.0.0-20220615170357-aa30c4140ca9 h1:jsrgPBWIMRBk71zKtEpVLJ10AqSQO+K2psplU8Wrouo=
|
github.com/sagernet/sing v0.0.0-20220619130320-8793fe5e067d h1:zr8y4wmNIxv6Kkvgqysx8Piy82ATAThEj1jaEf23YQs=
|
||||||
github.com/sagernet/sing v0.0.0-20220615170357-aa30c4140ca9/go.mod h1:Bgwxr10oTxYlQ33MgsXW3GuS2w5St11qqk4DqzJOdU4=
|
github.com/sagernet/sing v0.0.0-20220619130320-8793fe5e067d/go.mod h1:I67R/q5f67xDExL2kL3RLIP7kGJBOPkYXkpRAykgC+E=
|
||||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220615081955-91a0901d1c89 h1:l7TGNYSm4H2nZfvp2iOlCxvaGZO0tSts+o7+YJE8M2I=
|
github.com/sagernet/sing-shadowsocks v0.0.0-20220619134218-830a2f478eb1 h1:3GEdnWbuSX4XwSKnxLUB/1rMXUxSVKeyRhEeT7k7N1Q=
|
||||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220615081955-91a0901d1c89/go.mod h1:r3xDib+IPJH/cT1DBzez3O3me4HpGLbzdsMzqwyGX6U=
|
github.com/sagernet/sing-shadowsocks v0.0.0-20220619134218-830a2f478eb1/go.mod h1:xk8Hh1hQiTeiY6jHCQjaxxN8M6B94JoGaNx5q61naE8=
|
||||||
github.com/sagernet/sing-vmess v0.0.0-20220614092239-0052a5a383f5 h1:mTeGXZ/mNvUgiEPQ0hhMtqCdkPQlOjmgdl40TQNQnLQ=
|
github.com/sagernet/sing-vmess v0.0.0-20220616051646-3d3fc5d01eec h1:jUSfKmyL6K9O2TvIvcVacZ4eNXHYbNSfdph+DRPyVlU=
|
||||||
github.com/sagernet/sing-vmess v0.0.0-20220614092239-0052a5a383f5/go.mod h1:LHcJcRDCJnAeI7tlMmVqeJapURFJr7HQfDrBxkylu/g=
|
github.com/sagernet/sing-vmess v0.0.0-20220616051646-3d3fc5d01eec/go.mod h1:jDZ8fJgOea7Y7MMHWgfqwLBVLnhtW2zuxS5wjtDaB84=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||||
|
@ -566,8 +566,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s=
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU=
|
||||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|
|
@ -46,6 +46,7 @@ type configSchema struct {
|
||||||
IPv6 *bool `json:"ipv6"`
|
IPv6 *bool `json:"ipv6"`
|
||||||
Sniffing *bool `json:"sniffing"`
|
Sniffing *bool `json:"sniffing"`
|
||||||
TcpConcurrent *bool `json:"tcp-concurrent"`
|
TcpConcurrent *bool `json:"tcp-concurrent"`
|
||||||
|
InterfaceName *string `json:"interface-name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigs(w http.ResponseWriter, r *http.Request) {
|
func getConfigs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -85,6 +86,10 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
|
||||||
dialer.SetDial(*general.TcpConcurrent)
|
dialer.SetDial(*general.TcpConcurrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if general.InterfaceName != nil {
|
||||||
|
dialer.DefaultInterface.Store(*general.InterfaceName)
|
||||||
|
}
|
||||||
|
|
||||||
ports := P.GetPorts()
|
ports := P.GetPorts()
|
||||||
|
|
||||||
tcpIn := tunnel.TCPIn()
|
tcpIn := tunnel.TCPIn()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"golang.org/x/net/idna"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
type Domain struct {
|
type Domain struct {
|
||||||
*Base
|
*Base
|
||||||
domain string
|
domain string
|
||||||
|
rawDomain string
|
||||||
adapter string
|
adapter string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,14 +30,16 @@ func (d *Domain) Adapter() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Domain) Payload() string {
|
func (d *Domain) Payload() string {
|
||||||
return d.domain
|
return d.rawDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDomain(domain string, adapter string) *Domain {
|
func NewDomain(domain string, adapter string) *Domain {
|
||||||
|
actualDomain, _ := idna.ToASCII(domain)
|
||||||
return &Domain{
|
return &Domain{
|
||||||
Base: &Base{},
|
Base: &Base{},
|
||||||
domain: strings.ToLower(domain),
|
domain: strings.ToLower(actualDomain),
|
||||||
adapter: adapter,
|
adapter: adapter,
|
||||||
|
rawDomain: domain,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"golang.org/x/net/idna"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
@ -10,6 +11,7 @@ type DomainKeyword struct {
|
||||||
*Base
|
*Base
|
||||||
keyword string
|
keyword string
|
||||||
adapter string
|
adapter string
|
||||||
|
rawKeyword string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dk *DomainKeyword) RuleType() C.RuleType {
|
func (dk *DomainKeyword) RuleType() C.RuleType {
|
||||||
|
@ -29,14 +31,16 @@ func (dk *DomainKeyword) Adapter() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dk *DomainKeyword) Payload() string {
|
func (dk *DomainKeyword) Payload() string {
|
||||||
return dk.keyword
|
return dk.rawKeyword
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDomainKeyword(keyword string, adapter string) *DomainKeyword {
|
func NewDomainKeyword(keyword string, adapter string) *DomainKeyword {
|
||||||
|
actualDomainKeyword, _ := idna.ToASCII(keyword)
|
||||||
return &DomainKeyword{
|
return &DomainKeyword{
|
||||||
Base: &Base{},
|
Base: &Base{},
|
||||||
keyword: strings.ToLower(keyword),
|
keyword: strings.ToLower(actualDomainKeyword),
|
||||||
adapter: adapter,
|
adapter: adapter,
|
||||||
|
rawKeyword: keyword,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"golang.org/x/net/idna"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
@ -10,6 +11,7 @@ type DomainSuffix struct {
|
||||||
*Base
|
*Base
|
||||||
suffix string
|
suffix string
|
||||||
adapter string
|
adapter string
|
||||||
|
rawSuffix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *DomainSuffix) RuleType() C.RuleType {
|
func (ds *DomainSuffix) RuleType() C.RuleType {
|
||||||
|
@ -29,14 +31,16 @@ func (ds *DomainSuffix) Adapter() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *DomainSuffix) Payload() string {
|
func (ds *DomainSuffix) Payload() string {
|
||||||
return ds.suffix
|
return ds.rawSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDomainSuffix(suffix string, adapter string) *DomainSuffix {
|
func NewDomainSuffix(suffix string, adapter string) *DomainSuffix {
|
||||||
|
actualDomainKeyword, _ := idna.ToASCII(suffix)
|
||||||
return &DomainSuffix{
|
return &DomainSuffix{
|
||||||
Base: &Base{},
|
Base: &Base{},
|
||||||
suffix: strings.ToLower(suffix),
|
suffix: strings.ToLower(actualDomainKeyword),
|
||||||
adapter: adapter,
|
adapter: adapter,
|
||||||
|
rawSuffix: suffix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type classicalStrategy struct {
|
type classicalStrategy struct {
|
||||||
|
@ -52,6 +53,19 @@ func (c *classicalStrategy) OnUpdate(rules []string) {
|
||||||
c.count = len(classicalRules)
|
c.count = len(classicalRules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ruleParse(ruleRaw string) (string, string, []string) {
|
||||||
|
item := strings.Split(ruleRaw, ",")
|
||||||
|
if len(item) == 1 {
|
||||||
|
return "", item[0], nil
|
||||||
|
} else if len(item) == 2 {
|
||||||
|
return item[0], item[1], nil
|
||||||
|
} else if len(item) > 2 {
|
||||||
|
return item[0], item[1], item[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewClassicalStrategy(parse func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error)) *classicalStrategy {
|
func NewClassicalStrategy(parse func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error)) *classicalStrategy {
|
||||||
return &classicalStrategy{rules: []C.Rule{}, parse: func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) {
|
return &classicalStrategy{rules: []C.Rule{}, parse: func(tp, payload, target string, params []string) (parsed C.Rule, parseErr error) {
|
||||||
switch tp {
|
switch tp {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"github.com/Dreamacro/clash/component/trie"
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
"strings"
|
"golang.org/x/net/idna"
|
||||||
)
|
)
|
||||||
|
|
||||||
type domainStrategy struct {
|
type domainStrategy struct {
|
||||||
|
@ -28,7 +28,8 @@ func (d *domainStrategy) OnUpdate(rules []string) {
|
||||||
domainTrie := trie.New[bool]()
|
domainTrie := trie.New[bool]()
|
||||||
count := 0
|
count := 0
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
err := domainTrie.Insert(rule, true)
|
actualDomain, _ := idna.ToASCII(rule)
|
||||||
|
err := domainTrie.Insert(actualDomain, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnln("invalid domain:[%s]", rule)
|
log.Warnln("invalid domain:[%s]", rule)
|
||||||
} else {
|
} else {
|
||||||
|
@ -40,19 +41,6 @@ func (d *domainStrategy) OnUpdate(rules []string) {
|
||||||
d.count = count
|
d.count = count
|
||||||
}
|
}
|
||||||
|
|
||||||
func ruleParse(ruleRaw string) (string, string, []string) {
|
|
||||||
item := strings.Split(ruleRaw, ",")
|
|
||||||
if len(item) == 1 {
|
|
||||||
return "", item[0], nil
|
|
||||||
} else if len(item) == 2 {
|
|
||||||
return item[0], item[1], nil
|
|
||||||
} else if len(item) > 2 {
|
|
||||||
return item[0], item[1], item[2:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDomainStrategy() *domainStrategy {
|
func NewDomainStrategy() *domainStrategy {
|
||||||
return &domainStrategy{}
|
return &domainStrategy{}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ func (f *fetcher) Initial() (interface{}, error) {
|
||||||
buf []byte
|
buf []byte
|
||||||
hasLocal bool
|
hasLocal bool
|
||||||
err error
|
err error
|
||||||
|
forceUpdate bool
|
||||||
)
|
)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -57,10 +58,8 @@ func (f *fetcher) Initial() (interface{}, error) {
|
||||||
f.updatedAt = &modTime
|
f.updatedAt = &modTime
|
||||||
hasLocal = true
|
hasLocal = true
|
||||||
if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) {
|
if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) {
|
||||||
defer func() {
|
forceUpdate = true
|
||||||
log.Infoln("[Provider] %s's rules not updated for a long time, force refresh", f.Name())
|
log.Infoln("[Provider] %s not updated for a long time, force refresh", f.Name())
|
||||||
go f.update()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
buf, err = f.vehicle.Read()
|
buf, err = f.vehicle.Read()
|
||||||
|
@ -70,7 +69,21 @@ func (f *fetcher) Initial() (interface{}, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rules, err := f.parser(buf)
|
var rules interface{}
|
||||||
|
if forceUpdate {
|
||||||
|
var forceBuf []byte
|
||||||
|
if forceBuf, err = f.vehicle.Read(); err == nil {
|
||||||
|
if rules, err = f.parser(forceBuf); err == nil {
|
||||||
|
hasLocal = false
|
||||||
|
buf = forceBuf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil || !forceUpdate {
|
||||||
|
rules, err = f.parser(buf)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !hasLocal {
|
if !hasLocal {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
27
test/config/xray-shadowsocks.json
Normal file
27
test/config/xray-shadowsocks.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"port": 10002,
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"protocol": "shadowsocks",
|
||||||
|
"settings": {
|
||||||
|
"network": "tcp,udp",
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"method": "aes-128-gcm",
|
||||||
|
"level": 0,
|
||||||
|
"password": "FzcLbKs2dY9mhL"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"protocol": "freedom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"log": {
|
||||||
|
"loglevel": "debug"
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,9 +61,9 @@ require (
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
github.com/prometheus/common v0.32.1 // indirect
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
github.com/sagernet/sing v0.0.0-20220614034114-7caa1d0c0851 // indirect
|
github.com/sagernet/sing v0.0.0-20220619130320-8793fe5e067d // indirect
|
||||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220610074818-432dcbdb1d7c // indirect
|
github.com/sagernet/sing-shadowsocks v0.0.0-20220619134218-830a2f478eb1 // indirect
|
||||||
github.com/sagernet/sing-vmess v0.0.0-20220614042419-6f7c1431421a // indirect
|
github.com/sagernet/sing-vmess v0.0.0-20220616051646-3d3fc5d01eec // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
github.com/tobyxdd/hysteria v1.0.4 // indirect
|
github.com/tobyxdd/hysteria v1.0.4 // indirect
|
||||||
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
|
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
|
||||||
|
@ -79,7 +79,7 @@ require (
|
||||||
golang.org/x/exp v0.0.0-20220608143224-64259d1afd70 // indirect
|
golang.org/x/exp v0.0.0-20220608143224-64259d1afd70 // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
|
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
|
||||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 // indirect
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
|
||||||
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
|
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
|
||||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
|
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
|
||||||
golang.org/x/tools v0.1.10 // indirect
|
golang.org/x/tools v0.1.10 // indirect
|
||||||
|
|
16
test/go.sum
16
test/go.sum
|
@ -323,12 +323,12 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0
|
||||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/sagernet/sing v0.0.0-20220614034114-7caa1d0c0851 h1:Pp+9IPHtlwxrkiKMSrTOPS/eg+qJLdfZoeM89QX40BI=
|
github.com/sagernet/sing v0.0.0-20220619130320-8793fe5e067d h1:zr8y4wmNIxv6Kkvgqysx8Piy82ATAThEj1jaEf23YQs=
|
||||||
github.com/sagernet/sing v0.0.0-20220614034114-7caa1d0c0851/go.mod h1:ZEo7wZBfJmzm8uwnbCtWEHw9GsIfSThSylZYcR+H/Zw=
|
github.com/sagernet/sing v0.0.0-20220619130320-8793fe5e067d/go.mod h1:I67R/q5f67xDExL2kL3RLIP7kGJBOPkYXkpRAykgC+E=
|
||||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220610074818-432dcbdb1d7c h1:dapkcUcFbOwqnBwut6Dct7L695PVS6GoEqJeSRMWe0k=
|
github.com/sagernet/sing-shadowsocks v0.0.0-20220619134218-830a2f478eb1 h1:3GEdnWbuSX4XwSKnxLUB/1rMXUxSVKeyRhEeT7k7N1Q=
|
||||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220610074818-432dcbdb1d7c/go.mod h1:ty1OoG/SgB6IccsWUS5DZBkEGKUKM8nRSWxLlqYUPx0=
|
github.com/sagernet/sing-shadowsocks v0.0.0-20220619134218-830a2f478eb1/go.mod h1:xk8Hh1hQiTeiY6jHCQjaxxN8M6B94JoGaNx5q61naE8=
|
||||||
github.com/sagernet/sing-vmess v0.0.0-20220614042419-6f7c1431421a h1:vEJtwy2Ysw9ftf+5TALig2ZiRFf1pDnWGxhwRqDs+Cs=
|
github.com/sagernet/sing-vmess v0.0.0-20220616051646-3d3fc5d01eec h1:jUSfKmyL6K9O2TvIvcVacZ4eNXHYbNSfdph+DRPyVlU=
|
||||||
github.com/sagernet/sing-vmess v0.0.0-20220614042419-6f7c1431421a/go.mod h1:KZxqBTXh1v6q7TnVm36y+oCzPQLQKgv0z/TeBuozb2s=
|
github.com/sagernet/sing-vmess v0.0.0-20220616051646-3d3fc5d01eec/go.mod h1:jDZ8fJgOea7Y7MMHWgfqwLBVLnhtW2zuxS5wjtDaB84=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||||
|
@ -586,8 +586,8 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0=
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU=
|
||||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|
|
@ -3,11 +3,13 @@ package main
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -277,3 +279,37 @@ func Benchmark_Shadowsocks(b *testing.B) {
|
||||||
require.True(b, TCPing(net.JoinHostPort(localIP.String(), "10002")))
|
require.True(b, TCPing(net.JoinHostPort(localIP.String(), "10002")))
|
||||||
benchmarkProxy(b, proxy)
|
benchmarkProxy(b, proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClash_ShadowsocksUoT(t *testing.T) {
|
||||||
|
configPath := C.Path.Resolve("xray-shadowsocks.json")
|
||||||
|
|
||||||
|
cfg := &container.Config{
|
||||||
|
Image: ImageVless,
|
||||||
|
ExposedPorts: defaultExposedPorts,
|
||||||
|
}
|
||||||
|
hostCfg := &container.HostConfig{
|
||||||
|
PortBindings: defaultPortBindings,
|
||||||
|
Binds: []string{fmt.Sprintf("%s:/etc/xray/config.json", configPath)},
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := startContainer(cfg, hostCfg, "xray-ss")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cleanContainer(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{
|
||||||
|
Name: "ss",
|
||||||
|
Server: localIP.String(),
|
||||||
|
Port: 10002,
|
||||||
|
Password: "FzcLbKs2dY9mhL",
|
||||||
|
Cipher: "aes-128-gcm",
|
||||||
|
UDP: true,
|
||||||
|
UDPOverTCP: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(waitTime)
|
||||||
|
testSuit(t, proxy)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue