Update On Mon Feb 3 19:34:33 CET 2025

This commit is contained in:
github-action[bot]
2025-02-03 19:34:33 +01:00
parent 10c74b79e1
commit 11aee8b173
108 changed files with 3675 additions and 1211 deletions
+1
View File
@@ -902,3 +902,4 @@ Update On Thu Jan 30 19:32:29 CET 2025
Update On Fri Jan 31 19:32:11 CET 2025
Update On Sat Feb 1 19:32:16 CET 2025
Update On Sun Feb 2 19:31:20 CET 2025
Update On Mon Feb 3 19:34:24 CET 2025
-3
View File
@@ -21,7 +21,6 @@ import (
"github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/socks5"
"github.com/metacubex/mihomo/transport/vless"
@@ -513,8 +512,6 @@ func NewVless(option VlessOption) (*Vless, error) {
if option.Flow != vless.XRV {
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
}
log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV)
addons = &vless.Addons{
Flow: option.Flow,
}
+37
View File
@@ -0,0 +1,37 @@
package generater
import (
"encoding/base64"
"fmt"
"github.com/gofrs/uuid/v5"
)
func Main(args []string) {
if len(args) < 1 {
panic("Using: generate uuid/reality-keypair/wg-keypair")
}
switch args[0] {
case "uuid":
newUUID, err := uuid.NewV4()
if err != nil {
panic(err)
}
fmt.Println(newUUID.String())
case "reality-keypair":
privateKey, err := GeneratePrivateKey()
if err != nil {
panic(err)
}
publicKey := privateKey.PublicKey()
fmt.Println("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]))
fmt.Println("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]))
case "wg-keypair":
privateKey, err := GeneratePrivateKey()
if err != nil {
panic(err)
}
fmt.Println("PrivateKey: " + privateKey.String())
fmt.Println("PublicKey: " + privateKey.PublicKey().String())
}
}
+97
View File
@@ -0,0 +1,97 @@
// Copy from https://github.com/WireGuard/wgctrl-go/blob/a9ab2273dd1075ea74b88c76f8757f8b4003fcbf/wgtypes/types.go#L71-L155
package generater
import (
"crypto/rand"
"encoding/base64"
"fmt"
"golang.org/x/crypto/curve25519"
)
// KeyLen is the expected key length for a WireGuard key.
const KeyLen = 32 // wgh.KeyLen
// A Key is a public, private, or pre-shared secret key. The Key constructor
// functions in this package can be used to create Keys suitable for each of
// these applications.
type Key [KeyLen]byte
// GenerateKey generates a Key suitable for use as a pre-shared secret key from
// a cryptographically safe source.
//
// The output Key should not be used as a private key; use GeneratePrivateKey
// instead.
func GenerateKey() (Key, error) {
b := make([]byte, KeyLen)
if _, err := rand.Read(b); err != nil {
return Key{}, fmt.Errorf("wgtypes: failed to read random bytes: %v", err)
}
return NewKey(b)
}
// GeneratePrivateKey generates a Key suitable for use as a private key from a
// cryptographically safe source.
func GeneratePrivateKey() (Key, error) {
key, err := GenerateKey()
if err != nil {
return Key{}, err
}
// Modify random bytes using algorithm described at:
// https://cr.yp.to/ecdh.html.
key[0] &= 248
key[31] &= 127
key[31] |= 64
return key, nil
}
// NewKey creates a Key from an existing byte slice. The byte slice must be
// exactly 32 bytes in length.
func NewKey(b []byte) (Key, error) {
if len(b) != KeyLen {
return Key{}, fmt.Errorf("wgtypes: incorrect key size: %d", len(b))
}
var k Key
copy(k[:], b)
return k, nil
}
// ParseKey parses a Key from a base64-encoded string, as produced by the
// Key.String method.
func ParseKey(s string) (Key, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return Key{}, fmt.Errorf("wgtypes: failed to parse base64-encoded key: %v", err)
}
return NewKey(b)
}
// PublicKey computes a public key from the private key k.
//
// PublicKey should only be called when k is a private key.
func (k Key) PublicKey() Key {
var (
pub [KeyLen]byte
priv = [KeyLen]byte(k)
)
// ScalarBaseMult uses the correct base value per https://cr.yp.to/ecdh.html,
// so no need to specify it.
curve25519.ScalarBaseMult(&pub, &priv)
return Key(pub)
}
// String returns the base64-encoded string representation of a Key.
//
// ParseKey can be used to produce a new Key from this string.
func (k Key) String() string {
return base64.StdEncoding.EncodeToString(k[:])
}
+5
View File
@@ -25,6 +25,7 @@ const (
SOCKS5
SHADOWSOCKS
VMESS
VLESS
REDIR
TPROXY
TUNNEL
@@ -69,6 +70,8 @@ func (t Type) String() string {
return "ShadowSocks"
case VMESS:
return "Vmess"
case VLESS:
return "Vless"
case REDIR:
return "Redir"
case TPROXY:
@@ -103,6 +106,8 @@ func ParseType(t string) (*Type, error) {
res = SHADOWSOCKS
case "VMESS":
res = VMESS
case "VLESS":
res = VLESS
case "REDIR":
res = REDIR
case "TPROXY":
+24
View File
@@ -1176,6 +1176,30 @@ listeners:
network: [tcp, udp]
target: target.com
- name: vless-in-1
type: vless
port: 10817
listen: 0.0.0.0
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
users:
- username: 1
uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68
flow: xtls-rprx-vision
# ws-path: "/" # 如果不为空则开启 websocket 传输层
# 下面两项如果填写则开启 tls(需要同时填写)
# certificate: ./server.crt
# private-key: ./server.key
# 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写)
reality-config:
dest: test.com:443
private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成
short-id:
- 0123456789abcdef
server-names:
- test.com
### 注意,对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 的其中一项 ###
- name: tun-in-1
type: tun
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
+2 -1
View File
@@ -27,7 +27,7 @@ require (
github.com/metacubex/sing-shadowsocks v0.2.8
github.com/metacubex/sing-shadowsocks2 v0.2.2
github.com/metacubex/sing-tun v0.4.5
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
github.com/metacubex/utls v1.6.6
@@ -40,6 +40,7 @@ require (
github.com/sagernet/cors v1.2.1
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-mux v0.2.1
github.com/sagernet/sing-shadowtls v0.1.5
+4 -2
View File
@@ -122,8 +122,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhD
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3 h1:2kq6azIvsTjTnyw66xXDl5zMzIJqF7GTbvLpkroHssg=
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
@@ -170,6 +170,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
+38
View File
@@ -0,0 +1,38 @@
package config
import (
"github.com/metacubex/mihomo/listener/sing"
"encoding/json"
)
type VlessUser struct {
Username string
UUID string
Flow string
}
type VlessServer struct {
Enable bool
Listen string
Users []VlessUser
WsPath string
Certificate string
PrivateKey string
RealityConfig RealityConfig
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
}
func (t VlessServer) String() string {
b, _ := json.Marshal(t)
return string(b)
}
type RealityConfig struct {
Dest string
PrivateKey string
ShortID []string
ServerNames []string
MaxTimeDifference int
Proxy string
}
+125
View File
@@ -0,0 +1,125 @@
package inbound
import (
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing_vless"
"github.com/metacubex/mihomo/log"
)
type VlessOption struct {
BaseOption
Users []VlessUser `inbound:"users"`
WsPath string `inbound:"ws-path,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
}
type VlessUser struct {
Username string `inbound:"username,omitempty"`
UUID string `inbound:"uuid"`
Flow string `inbound:"flow,omitempty"`
}
type RealityConfig struct {
Dest string `inbound:"dest"`
PrivateKey string `inbound:"private-key"`
ShortID []string `inbound:"short-id"`
ServerNames []string `inbound:"server-names"`
MaxTimeDifference int `inbound:"max-time-difference,omitempty"`
Proxy string `inbound:"proxy,omitempty"`
}
func (c RealityConfig) Build() LC.RealityConfig {
return LC.RealityConfig{
Dest: c.Dest,
PrivateKey: c.PrivateKey,
ShortID: c.ShortID,
ServerNames: c.ServerNames,
MaxTimeDifference: c.MaxTimeDifference,
Proxy: c.Proxy,
}
}
func (o VlessOption) Equal(config C.InboundConfig) bool {
return optionToString(o) == optionToString(config)
}
type Vless struct {
*Base
config *VlessOption
l C.MultiAddrListener
vs LC.VlessServer
}
func NewVless(options *VlessOption) (*Vless, error) {
base, err := NewBase(&options.BaseOption)
if err != nil {
return nil, err
}
users := make([]LC.VlessUser, len(options.Users))
for i, v := range options.Users {
users[i] = LC.VlessUser{
Username: v.Username,
UUID: v.UUID,
Flow: v.Flow,
}
}
return &Vless{
Base: base,
config: options,
vs: LC.VlessServer{
Enable: true,
Listen: base.RawAddress(),
Users: users,
WsPath: options.WsPath,
Certificate: options.Certificate,
PrivateKey: options.PrivateKey,
RealityConfig: options.RealityConfig.Build(),
MuxOption: options.MuxOption.Build(),
},
}, nil
}
// Config implements constant.InboundListener
func (v *Vless) Config() C.InboundConfig {
return v.config
}
// Address implements constant.InboundListener
func (v *Vless) Address() string {
if v.l != nil {
for _, addr := range v.l.AddrList() {
return addr.String()
}
}
return ""
}
// Listen implements constant.InboundListener
func (v *Vless) Listen(tunnel C.Tunnel) error {
var err error
users := make([]LC.VlessUser, len(v.config.Users))
for i, v := range v.config.Users {
users[i] = LC.VlessUser{
Username: v.Username,
UUID: v.UUID,
Flow: v.Flow,
}
}
v.l, err = sing_vless.New(v.vs, tunnel, v.Additions()...)
if err != nil {
return err
}
log.Infoln("Vless[%s] proxy listening at: %s", v.Name(), v.Address())
return nil
}
// Close implements constant.InboundListener
func (v *Vless) Close() error {
return v.l.Close()
}
var _ C.InboundListener = (*Vless)(nil)
+7
View File
@@ -86,6 +86,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) {
return nil, err
}
listener, err = IN.NewVmess(vmessOption)
case "vless":
vlessOption := &IN.VlessOption{}
err = decoder.Decode(mapping, vlessOption)
if err != nil {
return nil, err
}
listener, err = IN.NewVless(vlessOption)
case "hysteria2":
hysteria2Option := &IN.Hysteria2Option{}
err = decoder.Decode(mapping, hysteria2Option)
+263
View File
@@ -0,0 +1,263 @@
package sing_vless
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net"
"net/http"
"reflect"
"strings"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter/inbound"
N "github.com/metacubex/mihomo/common/net"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/inner"
"github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/ntp"
mihomoVMess "github.com/metacubex/mihomo/transport/vmess"
"github.com/metacubex/sing-vmess/vless"
utls "github.com/metacubex/utls"
"github.com/sagernet/reality"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/metadata"
)
func init() {
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
tlsConn, loaded := common.Cast[*reality.Conn](conn)
if !loaded {
return
}
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), unsafe.Pointer(tlsConn)
})
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
tlsConn, loaded := common.Cast[*utls.UConn](conn)
if !loaded {
return
}
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn)
})
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
tlsConn, loaded := common.Cast[*tlsC.UConn](conn)
if !loaded {
return
}
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn)
})
}
type Listener struct {
closed bool
config LC.VlessServer
listeners []net.Listener
service *vless.Service[string]
}
func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) {
if len(additions) == 0 {
additions = []inbound.Addition{
inbound.WithInName("DEFAULT-VLESS"),
inbound.WithSpecialRules(""),
}
}
h, err := sing.NewListenerHandler(sing.ListenerConfig{
Tunnel: tunnel,
Type: C.VLESS,
Additions: additions,
MuxOption: config.MuxOption,
})
if err != nil {
return nil, err
}
service := vless.NewService[string](log.SingLogger, h)
service.UpdateUsers(
common.Map(config.Users, func(it LC.VlessUser) string {
return it.Username
}),
common.Map(config.Users, func(it LC.VlessUser) string {
return it.UUID
}),
common.Map(config.Users, func(it LC.VlessUser) string {
return it.Flow
}))
sl = &Listener{false, config, nil, service}
tlsConfig := &tls.Config{}
var realityConfig *reality.Config
var httpMux *http.ServeMux
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
if config.WsPath != "" {
httpMux = http.NewServeMux()
httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) {
conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
sl.HandleConn(conn, tunnel)
})
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1")
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
return nil, errors.New("certificate is unavailable in reality")
}
realityConfig = &reality.Config{}
realityConfig.SessionTicketsDisabled = true
realityConfig.Type = "tcp"
realityConfig.Dest = config.RealityConfig.Dest
realityConfig.Time = ntp.Now
realityConfig.ServerNames = make(map[string]bool)
for _, it := range config.RealityConfig.ServerNames {
realityConfig.ServerNames[it] = true
}
privateKey, err := base64.RawURLEncoding.DecodeString(config.RealityConfig.PrivateKey)
if err != nil {
return nil, fmt.Errorf("decode private key: %w", err)
}
if len(privateKey) != 32 {
return nil, errors.New("invalid private key")
}
realityConfig.PrivateKey = privateKey
realityConfig.MaxTimeDiff = time.Duration(config.RealityConfig.MaxTimeDifference) * time.Microsecond
realityConfig.ShortIds = make(map[[8]byte]bool)
for i, shortIDString := range config.RealityConfig.ShortID {
var shortID [8]byte
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
if err != nil {
return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err)
}
if decodedLen > 8 {
return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString)
}
realityConfig.ShortIds[shortID] = true
}
realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
return inner.HandleTcp(address, config.RealityConfig.Proxy)
}
}
for _, addr := range strings.Split(config.Listen, ",") {
addr := addr
//TCP
l, err := inbound.Listen("tcp", addr)
if err != nil {
return nil, err
}
if realityConfig != nil {
l = reality.NewListener(l, realityConfig)
// Due to low implementation quality, the reality server intercepted half close and caused memory leaks.
// We fixed it by calling Close() directly.
l = realityListenerWrapper{l}
} else if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig)
} else {
return nil, errors.New("disallow using Vless without both certificates/reality config")
}
sl.listeners = append(sl.listeners, l)
go func() {
if httpMux != nil {
_ = http.Serve(l, httpMux)
return
}
for {
c, err := l.Accept()
if err != nil {
if sl.closed {
break
}
continue
}
go sl.HandleConn(c, tunnel)
}
}()
}
return sl, nil
}
func (l *Listener) Close() error {
l.closed = true
var retErr error
for _, lis := range l.listeners {
err := lis.Close()
if err != nil {
retErr = err
}
}
return retErr
}
func (l *Listener) Config() string {
return l.config.String()
}
func (l *Listener) AddrList() (addrList []net.Addr) {
for _, lis := range l.listeners {
addrList = append(addrList, lis.Addr())
}
return
}
func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
ctx := sing.WithAdditions(context.TODO(), additions...)
err := l.service.NewConnection(ctx, conn, metadata.Metadata{
Protocol: "vless",
Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()),
})
if err != nil {
_ = conn.Close()
return
}
}
type realityConnWrapper struct {
*reality.Conn
}
func (c realityConnWrapper) Upstream() any {
return c.Conn
}
func (c realityConnWrapper) CloseWrite() error {
return c.Close()
}
type realityListenerWrapper struct {
net.Listener
}
func (l realityListenerWrapper) Accept() (net.Conn, error) {
c, err := l.Listener.Accept()
if err != nil {
return nil, err
}
return realityConnWrapper{c.(*reality.Conn)}, nil
}
+6
View File
@@ -14,6 +14,7 @@ import (
"strings"
"syscall"
"github.com/metacubex/mihomo/component/generater"
"github.com/metacubex/mihomo/component/geodata"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
@@ -71,6 +72,11 @@ func main() {
return
}
if len(os.Args) > 1 && os.Args[1] == "generate" {
generater.Main(os.Args[2:])
return
}
if version {
fmt.Printf("Mihomo Meta %s %s %s with %s %s\n",
C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime)
+4 -4
View File
@@ -5957,9 +5957,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.69"
version = "0.10.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e"
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
@@ -5998,9 +5998,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.104"
version = "0.9.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
dependencies = [
"cc",
"libc",
+1 -1
View File
@@ -10,7 +10,7 @@ edition = { workspace = true }
build = "build.rs"
[lib]
name = "app_lib"
name = "clash_nyanpasu_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
@@ -1,7 +1,10 @@
use super::IVerge;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use tracing_subscriber::filter;
#[derive(Deserialize, Serialize, Debug, Clone, specta::Type)]
#[derive(Deserialize, Serialize, Debug, Clone, specta::Type, EnumString, Display)]
#[strum(serialize_all = "kebab-case")]
pub enum LoggingLevel {
#[serde(rename = "silent", alias = "off")]
Silent,
@@ -149,10 +149,6 @@ pub struct IVerge {
/// `light` or `dark` or `system`
pub theme_mode: Option<String>,
/// enable blur mode
/// maybe be able to set the alpha
pub theme_blur: Option<bool>,
/// enable traffic graph default is true
pub traffic_graph: Option<bool>,
@@ -189,7 +185,7 @@ pub struct IVerge {
pub proxy_guard_interval: Option<u64>,
/// theme setting
pub theme_setting: Option<IVergeTheme>,
pub theme_color: Option<String>,
/// web ui list
pub web_ui_list: Option<Vec<String>>,
@@ -267,24 +263,6 @@ pub struct WindowState {
pub fullscreen: bool,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
pub struct IVergeTheme {
pub primary_color: Option<String>,
pub secondary_color: Option<String>,
pub primary_text: Option<String>,
pub secondary_text: Option<String>,
pub info_color: Option<String>,
pub error_color: Option<String>,
pub warning_color: Option<String>,
pub success_color: Option<String>,
pub font_family: Option<String>,
pub css_injection: Option<String>,
pub page_transition_duration: Option<f64>,
}
impl IVerge {
pub fn new() -> Self {
match dirs::nyanpasu_config_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
@@ -331,7 +309,6 @@ impl IVerge {
},
app_log_level: Some(logging::LoggingLevel::default()),
theme_mode: Some("system".into()),
theme_blur: Some(false),
traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_auto_launch: Some(false),
@@ -13,6 +13,7 @@ pub static UNITS: Lazy<Vec<DynMigration>> = Lazy::new(|| {
vec![
MigrateProfilesNullValue.into(),
MigrateLanguageOption.into(),
MigrateThemeSetting.into(),
]
});
@@ -147,3 +148,63 @@ impl<'a> Migration<'a> for MigrateLanguageOption {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct MigrateThemeSetting;
impl<'a> Migration<'a> for MigrateThemeSetting {
fn version(&self) -> &'a semver::Version {
&VERSION
}
fn name(&self) -> std::borrow::Cow<'a, str> {
Cow::Borrowed("Migrate Theme Setting")
}
fn migrate(&self) -> std::io::Result<()> {
let config_path = crate::utils::dirs::nyanpasu_config_path().unwrap();
if !config_path.exists() {
return Ok(());
}
let raw_config = std::fs::read_to_string(&config_path)?;
let mut config: Mapping = serde_yaml::from_str(&raw_config)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
if let Some(theme) = config.get("theme_setting") {
if !theme.is_null() {
if let Some(theme_obj) = theme.as_mapping() {
if let Some(color) = theme_obj.get("primary_color") {
println!("color: {:?}", color);
config.insert("theme_color".into(), color.clone());
}
}
}
}
config.remove("theme_setting");
let new_config = serde_yaml::to_string(&config)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
std::fs::write(&config_path, new_config)?;
Ok(())
}
fn discard(&self) -> std::io::Result<()> {
let config_path = crate::utils::dirs::nyanpasu_config_path().unwrap();
if !config_path.exists() {
return Ok(());
}
let raw_config = std::fs::read_to_string(&config_path)?;
let mut config: Mapping = serde_yaml::from_str(&raw_config)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
if let Some(color) = config.get("theme_color") {
let mut theme_obj = Mapping::new();
theme_obj.insert("primary_color".into(), color.clone());
config.insert(
"theme_setting".into(),
serde_yaml::Value::Mapping(theme_obj),
);
config.remove("theme_color");
}
let new_config = serde_yaml::to_string(&config)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
std::fs::write(&config_path, new_config)?;
Ok(())
}
}
@@ -280,7 +280,7 @@ mod platform_impl {
let mut items = Vec::new();
if proxies.is_empty() {
items.push(MenuItemKind::MenuItem(
MenuItemBuilder::new("No Proxies")
MenuItemBuilder::new(t!("tray.no_proxies"))
.id("no_proxies")
.enabled(false)
.build(app_handle)?,
+1 -1
View File
@@ -1,5 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run().unwrap();
clash_nyanpasu_lib::run().unwrap();
}
@@ -62,9 +62,11 @@ pub fn init() -> Result<()> {
let (filter, filter_handle) = reload::Layer::new(
EnvFilter::builder()
.with_default_directive(
std::convert::Into::<filter::LevelFilter>::into(log_level).into(),
std::convert::Into::<filter::LevelFilter>::into(LoggingLevel::Warn).into(),
)
.from_env_lossy(),
.from_env_lossy()
.add_directive(format!("nyanpasu={}", log_level).parse().unwrap())
.add_directive(format!("clash_nyanpasu={}", log_level).parse().unwrap()),
);
// register the logger
@@ -92,9 +94,12 @@ pub fn init() -> Result<()> {
.reload(
EnvFilter::builder()
.with_default_directive(
std::convert::Into::<filter::LevelFilter>::into(level).into(),
std::convert::Into::<filter::LevelFilter>::into(LoggingLevel::Warn)
.into(),
)
.from_env_lossy(),
.from_env_lossy()
.add_directive(format!("nyanpasu={}", level).parse().unwrap())
.add_directive(format!("clash_nyanpasu={}", level).parse().unwrap()),
)
.unwrap(); // panic if error
}
+2 -2
View File
@@ -12,14 +12,14 @@
"type": "embedBootstrapper"
},
"wix": {
"language": ["zh-CN", "en-US", "ru-RU"],
"language": ["en-US", "ru-RU", "zh-CN", "zh-TW"],
"template": "./templates/installer.wxs",
"fragmentPaths": ["./templates/cleanup.wxs"]
},
"nsis": {
"displayLanguageSelector": true,
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English", "Russian"],
"languages": ["English", "Russian", "SimpChinese", "TradChinese"],
"template": "./templates/installer.nsi",
"installMode": "both"
}
@@ -14,11 +14,13 @@
"@tanstack/react-query": "5.66.0",
"@tauri-apps/api": "2.2.0",
"ahooks": "3.8.4",
"lodash-es": "4.17.21",
"ofetch": "1.4.1",
"react": "19.0.0",
"swr": "2.3.0"
},
"devDependencies": {
"@types/lodash-es": "4.17.12",
"@types/react": "19.0.8"
}
}
@@ -2,4 +2,5 @@ export * from './ipc'
export * from './openapi'
export * from './provider'
export * from './service'
export * from './template'
export * from './utils'
@@ -869,11 +869,6 @@ export type IVerge = {
* `light` or `dark` or `system`
*/
theme_mode: string | null
/**
* enable blur mode
* maybe be able to set the alpha
*/
theme_blur: boolean | null
/**
* enable traffic graph default is true
*/
@@ -921,7 +916,7 @@ export type IVerge = {
/**
* theme setting
*/
theme_setting: IVergeTheme | null
theme_color: string | null
/**
* web ui list
*/
@@ -1002,19 +997,6 @@ export type IVerge = {
*/
network_statistic_widget?: NetworkStatisticWidgetConfig | null
}
export type IVergeTheme = {
primary_color: string | null
secondary_color: string | null
primary_text: string | null
secondary_text: string | null
info_color: string | null
error_color: string | null
warning_color: string | null
success_color: string | null
font_family: string | null
css_injection: string | null
page_transition_duration: number | null
}
export type JsonValue =
| null
| boolean
@@ -1,4 +1,11 @@
export * from './use-profile-content'
export * from './use-profile'
export * from './use-runtime-profile'
export * from './use-settings'
export * from './use-system-proxy'
export * from './useNyanpasu'
export * from './useClash'
export * from './useClashCore'
export * from './useClashWS'
export type * from './bindings'
@@ -44,10 +44,11 @@ export const useProfileContent = (uid: string) => {
* ```
*/
const query = useQuery({
queryKey: ['profileContent', uid],
queryKey: ['profile-content', uid],
queryFn: async () => {
return unwrapResult(await commands.readProfileFile(uid))
},
enabled: !!uid,
})
/**
@@ -1,6 +1,11 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { unwrapResult } from '../utils'
import { commands, ProfileBuilder, ProfilesBuilder } from './bindings'
import {
commands,
Profile,
type ProfileBuilder,
type ProfilesBuilder,
} from './bindings'
type URLImportParams = Parameters<typeof commands.importProfile>
@@ -22,68 +27,98 @@ type CreateParams =
}
}
type ProfileHelperFn = {
view: () => Promise<null | undefined>
update: (profile: ProfileBuilder) => Promise<null | undefined>
drop: () => Promise<null | undefined>
}
export type ProfileQueryResult = NonNullable<
ReturnType<typeof useProfile>['query']['data']
>
export type ProfileQueryResultItem = Profile & Partial<ProfileHelperFn>
/**
* A custom hook for managing profile operations using React Query.
* Provides functionality for CRUD operations on profiles including creation,
* updating, reordering, and deletion.
* A custom hook for managing profiles with various operations including creation, updating, sorting, and deletion.
*
* @remarks
* This hook provides comprehensive profile management functionality through React Query:
* - Fetching profiles with optional helper functions
* - Creating/importing profiles from URLs or files
* - Updating existing profiles
* - Reordering profiles
* - Upserting profile configurations
* - Deleting profiles
*
* Each operation automatically handles cache invalidation and refetching when successful.
*
* @param options - Configuration options for the hook
* @param options.without_helper_fn - When true, disables the addition of helper functions to profile items
*
* @returns An object containing:
* - query: {@link UseQueryResult} Hook result for fetching profiles data
* - create: {@link UseMutationResult} Mutation for creating/importing profiles
* - update: {@link UseMutationResult} Mutation for updating existing profiles
* - sort: {@link UseMutationResult} Mutation for reordering profiles
* - upsert: {@link UseMutationResult} Mutation for upserting profile configurations
* - drop: {@link UseMutationResult} Mutation for deleting profiles
* - query: Query result for fetching profiles
* - create: Mutation for creating/importing profiles
* - update: Mutation for updating existing profiles
* - sort: Mutation for reordering profiles
* - upsert: Mutation for upserting profile configurations
* - drop: Mutation for deleting profiles
*
* @example
* ```typescript
* ```tsx
* const { query, create, update, sort, upsert, drop } = useProfile();
*
* // Fetch profiles
* const { data, isLoading } = query;
* const profiles = query.data?.items;
*
* // Create a new profile
* create.mutate({
* type: 'file',
* data: { item: profileData, fileData: 'config' }
* });
* create.mutate({ type: 'file', data: { item: newProfile, fileData: 'config' }});
*
* // Update a profile
* update.mutate({ uid: 'profile-id', profile: updatedProfile });
*
* // Reorder profiles
* sort.mutate(['uid1', 'uid2', 'uid3']);
*
* // Upsert profile config
* upsert.mutate(profilesConfig);
*
* // Delete a profile
* drop.mutate('profile-id');
* ```
*/
export const useProfile = () => {
export const useProfile = (options?: { without_helper_fn?: boolean }) => {
const queryClient = useQueryClient()
function addHelperFn(item: Profile): Profile & ProfileHelperFn {
return {
...item,
view: async () => unwrapResult(await commands.viewProfile(item.uid)),
update: async (profile: ProfileBuilder) =>
await update.mutateAsync({ uid: item.uid, profile }),
drop: async () => await drop.mutateAsync(item.uid),
}
}
/**
* A React Query hook that fetches profiles data.
* data is the full Profile configuration, including current, chain, valid, and items fields
* Uses the `getProfiles` command to retrieve profile information.
* Retrieves and processes a list of profiles.
*
* @returns {UseQueryResult} A query result object containing:
* - data: {
* current: string | null - Currently selected profile UID
* chain: string[] - Global chain of profile UIDs
* valid: boolean - Whether the profile configuration is valid
* items: Profile[] - Array of profile configurations
* }
* - `isLoading`: Boolean indicating if the query is in loading state
* - `error`: Error object if the query failed
* - Other standard React Query result properties
* This query uses the `useQuery` hook to fetch profile data by invoking the `commands.getProfiles()` command.
* The raw result is first unwrapped using `unwrapResult`, and then each profile item is augmented with additional
* helper functions:
*
* - view: Invokes `commands.viewProfile` with the profile's UID.
* - update: Executes the update mutation by passing an object containing the UID and the new profile data.
* - drop: Executes the drop mutation using the profile's UID.
*
* @returns A promise resolving to an object containing the profile list along with the extended helper functions.
*/
const query = useQuery({
queryKey: ['profiles'],
queryFn: async () => {
return unwrapResult(await commands.getProfiles())
const result = unwrapResult(await commands.getProfiles())
// Skip helper functions if without_helper_fn is set
if (options?.without_helper_fn) {
return result
}
return {
...result,
items: result?.items?.map((item) => {
return addHelperFn(item)
}),
}
},
})
@@ -191,8 +226,10 @@ export const useProfile = () => {
* - Automatically invalidates the 'profiles' query cache on successful mutation
*/
const upsert = useMutation({
mutationFn: async (options: ProfilesBuilder) => {
return unwrapResult(await commands.patchProfilesConfig(options))
mutationFn: async (options: Partial<ProfilesBuilder>) => {
return unwrapResult(
await commands.patchProfilesConfig(options as ProfilesBuilder),
)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['profiles'] })
@@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query'
import { unwrapResult } from '../utils'
import { commands } from './bindings'
/**
* Custom hook for retrieving the runtime profile.
*
* This hook leverages the useQuery API to asynchronously retrieve and unwrap the runtime's YAML profile data
* via the commands.getRuntimeYaml call. The resulting query object includes properties such as data, error,
* status, and other metadata necessary to manage the loading state.
*
* @returns An object containing the query state and helper methods related to the runtime profile.
*/
export const useRuntimeProfile = () => {
const query = useQuery({
queryKey: ['runtime-profile'],
queryFn: async () => {
return unwrapResult(await commands.getRuntimeYaml())
},
})
return {
...query,
}
}
@@ -0,0 +1,130 @@
import { merge } from 'lodash-es'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { unwrapResult } from '../utils'
import { commands, type IVerge } from './bindings'
/**
* Custom hook for managing Verge configuration settings using React Query.
* Provides functionality to fetch and update settings with automatic cache invalidation.
*
* @returns An object containing:
* - query: UseQueryResult for fetching settings
* - data: Current Verge configuration
* - status: Query status ('loading', 'error', 'success')
* - error: Error object if query fails
* - upsert: UseMutationResult for updating settings
* - mutate: Function to update configuration
* - status: Mutation status
*
* @example
* ```tsx
* const { query, upsert } = useSettings();
*
* // Get current settings
* const settings = query.data;
*
* // Update settings
* upsert.mutate({ theme: 'dark' });
* ```
*/
export const useSettings = () => {
const queryClient = useQueryClient()
/**
* A query hook that fetches Verge configuration settings.
* Uses React Query to manage the data fetching state.
*
* @returns UseQueryResult containing:
* - data: The unwrapped Verge configuration data
* - status: Current status of the query ('loading', 'error', 'success')
* - error: Error object if the query fails
* - other standard React Query properties
*/
const query = useQuery({
queryKey: ['settings'],
queryFn: async () => {
return unwrapResult(await commands.getVergeConfig())
},
})
/**
* Mutation hook for updating Verge configuration settings
*
* @remarks
* Uses React Query's useMutation to manage state and side effects
*
* @param options - Partial configuration options to update
* @returns Mutation object containing mutate function and mutation state
*
* @example
* ```ts
* const { mutate } = upsert();
* mutate({ theme: 'dark' });
* ```
*/
const upsert = useMutation({
// Partial to allow for partial updates
mutationFn: async (options: Partial<IVerge>) => {
return unwrapResult(await commands.patchVergeConfig(options as IVerge))
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['settings'] })
},
})
return {
query,
upsert,
}
}
/**
* A custom hook that manages a specific setting from the Verge configuration.
*
* @template K - The key type extending keyof IVerge
* @param key - The specific setting key to manage
* @returns An object containing:
* - value: The current value of the specified setting
* - upsert: Function to update the setting value
* - Additional merged hook status properties
*
* @example
* ```typescript
* const { value, upsert } = useSetting('theme');
* // value contains current theme setting
* // upsert can be used to update theme setting
* ```
*/
export const useSetting = <K extends keyof IVerge>(key: K) => {
const {
query: { data, ...query },
upsert: update,
} = useSettings()
/**
* The value retrieved from the data object using the specified key.
* May be undefined if either data is undefined or the key doesn't exist in data.
*/
const value = data?.[key]
/**
* Updates a specific setting value in the Verge configuration
* @param value - The new value to be set for the specified key
* @returns void
* @remarks This function will not execute if the data is not available
*/
const upsert = async (value: IVerge[K]) => {
if (!data) {
return
}
await update.mutateAsync({ [key]: value })
}
return {
value,
upsert,
// merge hook status
...merge(query, update),
}
}
@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query'
import { unwrapResult } from '../utils'
import { commands } from './bindings'
/**
* Custom hook to fetch and manage the system proxy settings.
*
* This hook leverages the `useQuery` hook to perform an asynchronous request
* to obtain system proxy data via `commands.getSysProxy()`. The result of the query
* is processed with `unwrapResult` to extract the proxy information.
*
* @returns An object containing the query results and helper properties/methods
* (e.g., loading status, error, and refetch function) provided by `useQuery`.
*/
export const useSystemProxy = () => {
const query = useQuery({
queryKey: ['system-proxy'],
queryFn: async () => {
return unwrapResult(await commands.getSysProxy())
},
})
return {
...query,
}
}
@@ -1,5 +1,10 @@
import useSWR from 'swr'
import { ClashConfig, Profile } from '@/index'
import {
ClashConfig,
Profile,
ProfilesBuilder,
RemoteProfileOptionsBuilder,
} from '@/index'
import * as tauri from '@/service/tauri'
import { clash } from '../service/clash'
@@ -33,7 +38,7 @@ export const useClash = () => {
const getProfiles = useSWR('getProfiles', tauri.getProfiles)
const setProfiles = async (uid: string, profile: Partial<Profile.Item>) => {
const setProfiles = async (uid: string, profile: Partial<Profile>) => {
await tauri.setProfiles({ uid, profile })
await getProfiles.mutate()
@@ -41,7 +46,7 @@ export const useClash = () => {
await getRuntimeLogs.mutate()
}
const setProfilesConfig = async (profiles: Partial<Profile.Config>) => {
const setProfilesConfig = async (profiles: ProfilesBuilder) => {
await tauri.setProfilesConfig(profiles)
await getProfiles.mutate()
@@ -49,13 +54,16 @@ export const useClash = () => {
await getRuntimeLogs.mutate()
}
const createProfile = async (item: Partial<Profile.Item>, data?: string) => {
const createProfile = async (item: Partial<Profile>, data?: string) => {
await tauri.createProfile(item, data)
await getProfiles.mutate()
}
const updateProfile = async (uid: string, option?: Profile.Option) => {
const updateProfile = async (
uid: string,
option?: RemoteProfileOptionsBuilder,
) => {
await tauri.updateProfile(uid, option)
await getProfiles.mutate()
@@ -81,7 +89,10 @@ export const useClash = () => {
}
}
const importProfile = async (url: string, option?: Profile.Option) => {
const importProfile = async (
url: string,
option: RemoteProfileOptionsBuilder,
) => {
await tauri.importProfile(url, option)
await getProfiles.mutate()
@@ -1,7 +1,5 @@
import type { ClashCore } from '../ipc/bindings'
import { fetchLatestCoreVersions, getCoreVersion } from './tauri'
import { VergeConfig } from './types'
export type ClashCore = Required<VergeConfig>['clash_core']
export interface Core {
name: string
@@ -56,7 +54,7 @@ export const fetchLatestCore = async () => {
return {
...item,
latest: latest,
latest,
}
})
@@ -1,13 +1,18 @@
import { IPSBResponse } from '@/openapi'
import { invoke } from '@tauri-apps/api/core'
import type {
ClashInfo,
Profile,
Profiles,
ProfilesBuilder,
Proxies,
RemoteProfileOptionsBuilder,
} from '../ipc/bindings'
import { ManifestVersion } from './core'
import {
ClashConfig,
ClashInfo,
EnvInfos,
InspectUpdater,
Profile,
Proxies,
SystemProxy,
VergeConfig,
} from './types'
@@ -37,13 +42,16 @@ export const getRuntimeLogs = async () => {
}
export const createProfile = async (
item: Partial<Profile.Item>,
item: Partial<Profile>,
fileData?: string | null,
) => {
return await invoke<void>('create_profile', { item, fileData })
}
export const updateProfile = async (uid: string, option?: Profile.Option) => {
export const updateProfile = async (
uid: string,
option?: RemoteProfileOptionsBuilder,
) => {
return await invoke<void>('update_profile', { uid, option })
}
@@ -56,17 +64,17 @@ export const viewProfile = async (uid: string) => {
}
export const getProfiles = async () => {
return await invoke<Profile.Config>('get_profiles')
return await invoke<Profiles>('get_profiles')
}
export const setProfiles = async (payload: {
uid: string
profile: Partial<Profile.Item>
profile: Partial<Profile>
}) => {
return await invoke<void>('patch_profile', payload)
}
export const setProfilesConfig = async (profiles: Partial<Profile.Config>) => {
export const setProfilesConfig = async (profiles: ProfilesBuilder) => {
return await invoke<void>('patch_profiles_config', { profiles })
}
@@ -80,7 +88,7 @@ export const saveProfileFile = async (uid: string, fileData: string) => {
export const importProfile = async (
url: string,
option: Profile.Option = { with_proxy: true },
option: RemoteProfileOptionsBuilder,
) => {
return await invoke<void>('import_profile', {
url,
@@ -54,11 +54,6 @@ export interface VergeConfig {
always_on_top?: boolean
}
export interface ClashInfo {
port?: number
server?: string
secret?: string
}
export interface ClashConfig {
port: number
mode: string
@@ -74,105 +69,12 @@ export interface ClashConfig {
secret: string
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Profile {
export interface Config {
current: string[]
chain: string[]
valid: string[]
items: Item[]
}
export const Template = {
merge: `# Clash Nyanpasu Merge Template (YAML)
# Documentation on https://nyanpasu.elaina.moe/
# Set the default merge strategy to recursive merge.
# Enable the old mode with the override__ prefix.
# Use the filter__ prefix to filter lists (removing unwanted content).
# All prefixes should support accessing maps or lists with a.b.c syntax.
`,
javascript: `// Clash Nyanpasu JavaScript Template
// Documentation on https://nyanpasu.elaina.moe/
/** @type {config} */
export default function (profile) {
return profile;
}
`,
luascript: `-- Clash Nyanpasu Lua Script Template
-- Documentation on https://nyanpasu.elaina.moe/
return config;
`,
profile: `# Clash Nyanpasu Profile Template
# Documentation on https://nyanpasu.elaina.moe/
proxies:
proxy-groups:
rules:
`,
}
export const Type = {
Local: 'local',
Remote: 'remote',
Merge: 'merge',
JavaScript: {
script: 'javascript',
},
LuaScript: {
script: 'lua',
},
} as const
export interface Item {
uid: string
type?: (typeof Type)[keyof typeof Type]
name?: string
desc?: string
file?: string
url?: string
updated?: number
selected?: {
name?: string
now?: string
}[]
extra?: {
upload: number
download: number
total: number
expire: number
}
option?: Option
chain?: string[]
}
export interface Option {
user_agent?: string
with_proxy?: boolean
self_proxy?: boolean
update_interval?: number
}
}
export interface SystemProxy {
enable: boolean
server: string
bypass: string
}
export interface Proxies {
direct: Clash.Proxy
global: Clash.Proxy<Clash.Proxy>
groups: Clash.Proxy<Clash.Proxy>[]
proxies: Clash.Proxy[]
records: {
[key: string]: Clash.Proxy
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Connection {
export interface Item {
@@ -0,0 +1,43 @@
// nyanpasu merge profile chain template
const merge = `# Clash Nyanpasu Merge Template (YAML)
# Documentation on https://nyanpasu.elaina.moe/
# Set the default merge strategy to recursive merge.
# Enable the old mode with the override__ prefix.
# Use the filter__ prefix to filter lists (removing unwanted content).
# All prefixes should support accessing maps or lists with a.b.c syntax.
`
// nyanpasu javascript profile chain template
const javascript = `// Clash Nyanpasu JavaScript Template
// Documentation on https://nyanpasu.elaina.moe/
/** @type {config} */
export default function (profile) {
return profile;
}
`
// nyanpasu lua profile chain template
const luascript = `-- Clash Nyanpasu Lua Script Template
-- Documentation on https://nyanpasu.elaina.moe/
return config;
`
// clash profile template example
const profile = `# Clash Nyanpasu Profile Template
# Documentation on https://nyanpasu.elaina.moe/
proxies:
proxy-groups:
rules:
`
export const ProfileTemplate = {
merge,
javascript,
luascript,
profile,
} as const
@@ -28,7 +28,7 @@
"allotment": "1.20.2",
"country-code-emoji": "2.3.0",
"dayjs": "1.11.13",
"framer-motion": "12.0.6",
"framer-motion": "12.0.11",
"i18next": "24.2.2",
"jotai": "2.11.3",
"json-schema": "0.4.0",
@@ -58,7 +58,7 @@
"@tanstack/react-query": "5.66.0",
"@tanstack/react-router": "1.99.0",
"@tanstack/router-devtools": "1.99.0",
"@tanstack/router-plugin": "1.99.0",
"@tanstack/router-plugin": "1.99.3",
"@tauri-apps/plugin-clipboard-manager": "2.2.1",
"@tauri-apps/plugin-dialog": "2.2.0",
"@tauri-apps/plugin-fs": "2.2.0",
@@ -6,11 +6,10 @@
unicode-range: U+1F1E6-1F1FF;
}
// use local emoji font for better backward compatibility
@font-face {
font-family: 'Color Emoji';
src: local('Apple Color Emoji'), local('Segoe UI Emoji'),
local('Segoe UI Symbol'), local('Noto Color Emoji'),
url('../fonts/Twemoji.Mozilla.ttf');
// use local emoji font for better backward compatibility
}
@@ -51,7 +51,10 @@ export default function ConnectionTotal() {
<div className="flex gap-2">
<Paper
elevation={0}
className="flex min-h-8 items-center justify-center gap-1 rounded-2xl px-2"
className="flex min-h-8 items-center justify-center gap-1 px-2"
sx={{
borderRadius: '1em',
}}
>
<Download
className="scale-75"
@@ -76,7 +79,10 @@ export default function ConnectionTotal() {
</Paper>
<Paper
elevation={0}
className="flex min-h-8 items-center justify-center gap-1 rounded-2xl px-2"
className="flex min-h-8 items-center justify-center gap-1 px-2"
sx={{
borderRadius: '1em',
}}
>
<Upload
className="scale-75"
@@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'
import { Menu as MenuIcon } from '@mui/icons-material'
import { LoadingButton } from '@mui/lab'
import { alpha, ListItemButton, Menu, MenuItem, useTheme } from '@mui/material'
import { Profile, useClash } from '@nyanpasu/interface'
import { ProfileQueryResultItem } from '@nyanpasu/interface'
import { cleanDeepClickEvent } from '@nyanpasu/ui'
const longPressDelay = 200
@@ -23,7 +23,7 @@ export const ChainItem = memo(function ChainItem({
onClick,
onChainEdit,
}: {
item: Profile.Item
item: ProfileQueryResultItem
selected?: boolean
onClick: () => Promise<void>
onChainEdit: () => void
@@ -32,7 +32,7 @@ export const ChainItem = memo(function ChainItem({
const { palette } = useTheme()
const { deleteProfile, viewProfile } = useClash()
// const { deleteProfile, viewProfile } = useClash()
const [isPending, startTransition] = useTransition()
@@ -45,8 +45,8 @@ export const ChainItem = memo(function ChainItem({
const menuMapping = {
Apply: () => handleClick(),
'Edit Info': () => onChainEdit(),
'Open File': () => viewProfile(item.uid),
Delete: () => deleteProfile(item.uid),
'Open File': () => item.view && item.view(),
Delete: () => item.drop && item.drop(),
}
const handleMenuClick = (func: () => void) => {
@@ -1,8 +1,7 @@
import { alpha, useTheme } from '@mui/material'
import { Profile } from '@nyanpasu/interface'
import { getLanguage } from '../utils'
import { getLanguage, ProfileType } from '../utils'
export const LanguageChip = ({ type }: { type: Profile.Item['type'] }) => {
export const LanguageChip = ({ type }: { type: ProfileType }) => {
const { palette } = useTheme()
const lang = getLanguage(type, true)
@@ -7,13 +7,17 @@ import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { Add } from '@mui/icons-material'
import { alpha, ListItemButton, useTheme } from '@mui/material'
import { Profile, useClash } from '@nyanpasu/interface'
import { filterProfiles } from '../utils'
import {
ProfileQueryResultItem,
useClash,
useProfile,
} from '@nyanpasu/interface'
import { ClashProfile, filterProfiles } from '../utils'
import ChainItem from './chain-item'
import { atomChainsSelected, atomGlobalChainCurrent } from './store'
export interface SideChainProps {
onChainEdit: (item?: Profile.Item) => void | Promise<void>
onChainEdit: (item?: ProfileQueryResultItem) => void | Promise<void>
}
export const SideChain = ({ onChainEdit }: SideChainProps) => {
@@ -25,20 +29,19 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
const currentProfileUid = useAtomValue(atomChainsSelected)
const { getProfiles, setProfilesConfig, setProfiles, reorderProfilesByList } =
useClash()
const { setProfiles, reorderProfilesByList } = useClash()
const { scripts, profiles } = filterProfiles(getProfiles.data?.items)
const { query, upsert } = useProfile()
const { clash, chain } = filterProfiles(query.data?.items)
const currentProfile = useMemo(() => {
return getProfiles.data?.items?.find(
(item) => item.uid === currentProfileUid,
)
}, [getProfiles.data?.items, currentProfileUid])
return clash?.find((item) => item.uid === currentProfileUid) as ClashProfile
}, [clash, currentProfileUid])
const handleChainClick = useLockFn(async (uid: string) => {
const chains = isGlobalChainCurrent
? (getProfiles.data?.chain ?? [])
? (query.data?.chain ?? [])
: (currentProfile?.chain ?? [])
const updatedChains = chains.includes(uid)
@@ -47,7 +50,7 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
try {
if (isGlobalChainCurrent) {
await setProfilesConfig({ chain: updatedChains })
await upsert.mutateAsync({ chain: updatedChains })
} else {
if (!currentProfile?.uid) {
return
@@ -63,8 +66,8 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
})
const reorderValues = useMemo(
() => scripts?.map((item) => item.uid) || [],
[scripts],
() => chain?.map((item) => item.uid) || [],
[chain],
)
return (
@@ -73,15 +76,15 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
axis="y"
values={reorderValues}
onReorder={(values) => {
const profileUids = profiles?.map((item) => item.uid) || []
const profileUids = clash?.map((item) => item.uid) || []
reorderProfilesByList([...profileUids, ...values])
}}
layoutScroll
style={{ overflowY: 'scroll' }}
>
{scripts?.map((item, index) => {
{chain?.map((item, index) => {
const selected = isGlobalChainCurrent
? getProfiles.data?.chain?.includes(item.uid)
? query.data?.chain?.includes(item.uid)
: currentProfile?.chain?.includes(item.uid)
return (
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import { VList } from 'virtua'
import { RamenDining, Terminal } from '@mui/icons-material'
import { Divider } from '@mui/material'
import { useClash } from '@nyanpasu/interface'
import { useClash, useProfile } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { filterProfiles } from '../utils'
@@ -37,9 +37,11 @@ export interface SideLogProps {
export const SideLog = ({ className }: SideLogProps) => {
const { t } = useTranslation()
const { getRuntimeLogs, getProfiles } = useClash()
// const { getRuntimeLogs, getProfiles } = useClash()
const { scripts } = filterProfiles(getProfiles.data?.items)
const { query } = useProfile()
const { chain } = filterProfiles(query.data?.items)
return (
<div className={cn('w-full', className)}>
@@ -54,7 +56,7 @@ export const SideLog = ({ className }: SideLogProps) => {
<Divider />
<VList className="flex flex-col gap-2 overflow-auto p-2 select-text">
{!isEmpty(getRuntimeLogs.data) ? (
{/* {!isEmpty(getRuntimeLogs.data) ? (
Object.entries(getRuntimeLogs.data).map(([uid, content]) => {
return content.map((item, index) => {
const name = scripts?.find((script) => script.uid === uid)?.name
@@ -69,12 +71,12 @@ export const SideLog = ({ className }: SideLogProps) => {
)
})
})
) : (
<div className="flex h-full min-h-48 w-full flex-col items-center justify-center">
<RamenDining className="!size-10" />
<p>{t('No Logs')}</p>
</div>
)}
) : ( */}
<div className="flex h-full min-h-48 w-full flex-col items-center justify-center">
<RamenDining className="!size-10" />
<p>{t('No Logs')}</p>
</div>
{/* )} */}
</VList>
</div>
)
@@ -1,6 +1,5 @@
import { atom } from 'jotai'
import type { Profile } from '@nyanpasu/interface'
export const atomGlobalChainCurrent = atom<boolean>(false)
export const atomChainsSelected = atom<Profile.Item['uid']>()
export const atomChainsSelected = atom<string>()
@@ -22,7 +22,14 @@ import { useLatest } from 'react-use'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { Divider, InputAdornment } from '@mui/material'
import { Profile, useClash } from '@nyanpasu/interface'
import {
LocalProfile,
ProfileQueryResultItem,
ProfileTemplate,
RemoteProfile,
useProfile,
useProfileContent,
} from '@nyanpasu/interface'
import { BaseDialog } from '@nyanpasu/ui'
import { LabelSwitch } from '../setting/modules/clash-field'
import { ReadProfile } from './read-profile'
@@ -30,7 +37,7 @@ import { ReadProfile } from './read-profile'
const ProfileMonacoViewer = lazy(() => import('./profile-monaco-viewer'))
export interface ProfileDialogProps {
profile?: Profile.Item
profile?: ProfileQueryResultItem
open: boolean
onClose: () => void
}
@@ -52,27 +59,29 @@ export const ProfileDialog = ({
}: ProfileDialogProps) => {
const { t } = useTranslation()
const { createProfile, setProfiles, getProfileFile, setProfileFile } =
useClash()
const { create, update } = useProfile()
const contentFn = useProfileContent(profile?.uid ?? '')
const localProfile = useRef('')
const addProfileCtx = use(AddProfileContext)
const [localProfileMessage, setLocalProfileMessage] = useState('')
const { control, watch, handleSubmit, reset, setValue } =
useForm<Profile.Item>({
defaultValues: profile || {
type: 'remote',
name: addProfileCtx?.name || t(`New Profile`),
desc: addProfileCtx?.desc || '',
url: addProfileCtx?.url || '',
option: {
// user_agent: "",
with_proxy: false,
self_proxy: false,
},
const { control, watch, handleSubmit, reset, setValue } = useForm<
RemoteProfile | LocalProfile
>({
defaultValues: profile || {
type: 'remote',
name: addProfileCtx?.name || t(`New Profile`),
desc: addProfileCtx?.desc || '',
url: addProfileCtx?.url || '',
option: {
// user_agent: "",
with_proxy: false,
self_proxy: false,
},
})
},
})
useEffect(() => {
if (addProfileCtx) {
@@ -112,10 +121,12 @@ export const ProfileDialog = ({
const latestEditor = useLatest(editor)
const editorMarks = useRef<editor.IMarker[]>([])
const editorHasError = () =>
editorMarks.current.length > 0 &&
editorMarks.current.some((m) => m.severity === 8)
// eslint-disable-next-line react-compiler/react-compiler
const onSubmit = handleSubmit(async (form) => {
if (editorHasError()) {
message('Please fix the error before saving', {
@@ -123,23 +134,55 @@ export const ProfileDialog = ({
})
return
}
const toCreate = async () => {
if (isRemote) {
await createProfile(form)
const data = form as RemoteProfile
await create.mutateAsync({
type: 'url',
data: {
url: data.url,
// TODO: define backend serde(option) to move null
option: data.option
? {
...data.option,
user_agent: data.option.user_agent ?? null,
with_proxy: data.option.with_proxy ?? null,
self_proxy: data.option.self_proxy ?? null,
}
: null,
},
})
} else {
if (localProfile.current) {
await createProfile(form, localProfile.current)
await create.mutateAsync({
type: 'manual',
data: {
item: form,
fileData: localProfile.current,
},
})
} else {
// setLocalProfileMessage("Not selected profile");
await createProfile(form, 'rules: []')
await create.mutateAsync({
type: 'manual',
data: {
item: form,
fileData: ProfileTemplate.profile,
},
})
}
}
}
const toUpdate = async () => {
const value = latestEditor.current.value
await setProfileFile(form.uid, value)
await setProfiles(form.uid, form)
await contentFn.upsert.mutateAsync(value)
await update.mutateAsync({
uid: form.uid,
profile: form,
})
}
try {
@@ -252,7 +295,7 @@ export const ProfileDialog = ({
render={({ field }) => (
<LabelSwitch
label={t('Use System Proxy')}
checked={field.value}
checked={Boolean(field.value)}
{...field}
/>
)}
@@ -264,7 +307,7 @@ export const ProfileDialog = ({
render={({ field }) => (
<LabelSwitch
label={t('Use Clash Proxy')}
checked={field.value}
checked={Boolean(field.value)}
{...field}
/>
)}
@@ -298,7 +341,7 @@ export const ProfileDialog = ({
if (isEdit) {
try {
const value = await getProfileFile(profile?.uid)
const value = contentFn.query.data ?? ''
setEditor((editor) => ({ ...editor, value }))
} catch (error) {
console.error(error)
@@ -26,19 +26,26 @@ import {
Tooltip,
useTheme,
} from '@mui/material'
import { Profile, useClash } from '@nyanpasu/interface'
import {
Profile,
ProfileQueryResultItem,
RemoteProfile,
RemoteProfileOptions,
useClash,
useProfile,
} from '@nyanpasu/interface'
import { cleanDeepClickEvent, cn } from '@nyanpasu/ui'
import { ProfileDialog } from './profile-dialog'
import { GlobalUpdatePendingContext } from './provider'
export interface ProfileItemProps {
item: Profile.Item
item: ProfileQueryResultItem
selected?: boolean
maxLogLevelTriggered?: {
global: undefined | 'info' | 'error' | 'warn'
current: undefined | 'info' | 'error' | 'warn'
}
onClickChains: (item: Profile.Item) => void
onClickChains: (item: Profile) => void
chainsSelected?: boolean
}
@@ -53,13 +60,9 @@ export const ProfileItem = memo(function ProfileItem({
const { palette } = useTheme()
const {
setProfilesConfig,
deleteConnections,
updateProfile,
deleteProfile,
viewProfile,
} = useClash()
const { deleteConnections } = useClash()
const { upsert } = useProfile()
const globalUpdatePending = use(GlobalUpdatePendingContext)
@@ -73,7 +76,7 @@ export const ProfileItem = memo(function ProfileItem({
let total = 0
let used = 0
if (item.extra) {
if ('extra' in item && item.extra) {
const { download, upload, total: t } = item.extra
total = t
@@ -102,7 +105,7 @@ export const ProfileItem = memo(function ProfileItem({
try {
setLoading({ card: true })
await setProfilesConfig({ current: [item.uid] })
await upsert.mutateAsync({ current: [item.uid] })
await deleteConnections()
} catch (err) {
@@ -124,13 +127,18 @@ export const ProfileItem = memo(function ProfileItem({
})
const handleUpdate = useLockFn(async (proxy?: boolean) => {
const options: Profile.Option = item.option || {
// TODO: define backend serde(option) to move null
const selfOption = 'option' in item ? item.option : undefined
const options: RemoteProfileOptions = {
with_proxy: false,
self_proxy: false,
update_interval: 0,
...selfOption,
}
if (proxy) {
if (item.option?.self_proxy) {
if (selfOption?.self_proxy) {
options.with_proxy = false
options.self_proxy = true
} else {
@@ -142,7 +150,7 @@ export const ProfileItem = memo(function ProfileItem({
try {
setLoading({ update: true })
await updateProfile(item.uid, options)
await item?.update?.(item)
} finally {
setLoading({ update: false })
}
@@ -150,7 +158,8 @@ export const ProfileItem = memo(function ProfileItem({
const handleDelete = useLockFn(async () => {
try {
await deleteProfile(item.uid)
// await deleteProfile(item.uid)
await item?.drop?.()
} catch (err) {
message(`Delete failed: \n ${JSON.stringify(err)}`, {
title: t('Error'),
@@ -164,19 +173,12 @@ export const ProfileItem = memo(function ProfileItem({
Select: () => handleSelect(),
'Edit Info': () => setOpen(true),
'Proxy Chains': () => onClickChains(item),
'Open File': () => viewProfile(item.uid),
'Open File': () => item?.view?.(),
Update: () => handleUpdate(),
'Update(Proxy)': () => handleUpdate(true),
Delete: () => handleDelete(),
}),
[
handleDelete,
handleSelect,
handleUpdate,
item,
onClickChains,
viewProfile,
],
[handleDelete, handleSelect, handleUpdate, item, onClickChains],
)
const MenuComp = useMemo(() => {
@@ -232,7 +234,7 @@ export const ProfileItem = memo(function ProfileItem({
onClick={handleSelect}
>
<div className="flex items-center justify-between gap-2">
<Tooltip title={item.url}>
<Tooltip title={(item as RemoteProfile).url}>
<Chip
className="!pr-2 !pl-2 font-bold"
avatar={<IconComponent className="!size-5" color="primary" />}
@@ -255,9 +257,9 @@ export const ProfileItem = memo(function ProfileItem({
!!item.updated && (
<TimeSpan ts={item.updated!} k="Subscription Updated At" />
),
!!item.extra?.expire && (
!!(item as RemoteProfile).extra?.expire && (
<TimeSpan
ts={item.extra!.expire!}
ts={(item as RemoteProfile).extra!.expire!}
k="Subscription Expires In"
/>
),
@@ -5,7 +5,7 @@ import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Close } from '@mui/icons-material'
import { IconButton } from '@mui/material'
import { Profile, useClash } from '@nyanpasu/interface'
import { Profile, useProfile } from '@nyanpasu/interface'
import { SideChain } from './modules/side-chain'
import { SideLog } from './modules/side-log'
import { atomChainsSelected, atomGlobalChainCurrent } from './modules/store'
@@ -21,21 +21,19 @@ export const ProfileSide = ({ onClose }: ProfileSideProps) => {
const [open, setOpen] = useState(false)
const [item, setItem] = useState<Profile.Item>()
const [item, setItem] = useState<Profile>()
const isGlobalChainCurrent = useAtomValue(atomGlobalChainCurrent)
const currentProfileUid = useAtomValue(atomChainsSelected)
const { getProfiles } = useClash()
const { query } = useProfile()
const currentProfile = useMemo(() => {
return getProfiles.data?.items?.find(
(item) => item.uid === currentProfileUid,
)
}, [getProfiles.data?.items, currentProfileUid])
return query.data?.items?.find((item) => item.uid === currentProfileUid)
}, [query.data?.items, currentProfileUid])
const handleEditChain = async (_item?: Profile.Item) => {
const handleEditChain = async (_item?: Profile) => {
setItem(_item)
setOpen(true)
}
@@ -10,7 +10,7 @@ import {
Tooltip,
useTheme,
} from '@mui/material'
import { useClash } from '@nyanpasu/interface'
import { useProfile } from '@nyanpasu/interface'
import { readText } from '@tauri-apps/plugin-clipboard-manager'
export const QuickImport = () => {
@@ -22,7 +22,7 @@ export const QuickImport = () => {
const [loading, setLoading] = useState(false)
const { importProfile } = useClash()
const { create } = useProfile()
const onCopyLink = async () => {
const text = await readText()
@@ -68,7 +68,13 @@ export const QuickImport = () => {
try {
setLoading(true)
await importProfile(url)
await create.mutateAsync({
type: 'url',
data: {
url,
option: null,
},
})
} finally {
setUrl('')
setLoading(false)
@@ -1,10 +1,14 @@
import { useCreation } from 'ahooks'
import { useAtomValue } from 'jotai'
import { nanoid } from 'nanoid'
import { lazy, Suspense, useMemo } from 'react'
import { lazy, Suspense } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { themeMode } from '@/store'
import { getRuntimeYaml, useClash } from '@nyanpasu/interface'
import {
useProfile,
useProfileContent,
useRuntimeProfile,
} from '@nyanpasu/interface'
import { BaseDialog, cn } from '@nyanpasu/ui'
const MonacoDiffEditor = lazy(() => import('./profile-monaco-diff-viewer'))
@@ -19,30 +23,24 @@ export default function RuntimeConfigDiffDialog({
onClose,
}: RuntimeConfigDiffDialogProps) {
const { t } = useTranslation()
const { getProfiles, getProfileFile } = useClash()
const currentProfileUid = getProfiles.data?.current
const { query } = useProfile()
const currentProfileUid = query.data?.current?.[0]
const contentFn = useProfileContent(currentProfileUid || '')
// need manual refetch
contentFn.query.refetch()
const runtimeProfile = useRuntimeProfile()
const loaded = !contentFn.query.isLoading && !query.isLoading
const mode = useAtomValue(themeMode)
const { data: runtimeConfig, isLoading: isLoadingRuntimeConfig } = useSWR(
open ? '/getRuntimeConfigYaml' : null,
getRuntimeYaml,
{},
)
const { data: profileConfig, isLoading: isLoadingProfileConfig } = useSWR(
open ? `/readProfileFile?uid=${currentProfileUid}` : null,
async (key) => {
const url = new URL(key, window.location.origin)
return await getProfileFile(url.searchParams.get('uid')!)
},
{
revalidateOnFocus: true,
refreshInterval: 0,
},
)
const loaded = !isLoadingRuntimeConfig && !isLoadingProfileConfig
const originalModelPath = useMemo(() => `${nanoid()}.clash.yaml`, [])
const modifiedModelPath = useMemo(() => `${nanoid()}.runtime.yaml`, [])
const originalModelPath = useCreation(() => `${nanoid()}.clash.yaml`, [])
const modifiedModelPath = useCreation(() => `${nanoid()}.runtime.yaml`, [])
if (!currentProfileUid) {
return null
@@ -68,9 +66,9 @@ export default function RuntimeConfigDiffDialog({
<MonacoDiffEditor
language="yaml"
theme={mode === 'light' ? 'vs' : 'vs-dark'}
original={profileConfig}
original={contentFn.query.data}
originalModelPath={originalModelPath}
modified={runtimeConfig}
modified={runtimeProfile.data}
modifiedModelPath={modifiedModelPath}
options={{
minimap: { enabled: false },
@@ -5,10 +5,16 @@ import { SelectElement, TextFieldElement, useForm } from 'react-hook-form-mui'
import { useTranslation } from 'react-i18next'
import { message } from '@/utils/notification'
import { Divider } from '@mui/material'
import { Profile, useClash } from '@nyanpasu/interface'
import {
Profile,
ProfileTemplate,
useClash,
useProfile,
useProfileContent,
} from '@nyanpasu/interface'
import { BaseDialog, BaseDialogProps } from '@nyanpasu/ui'
import LanguageChip from './modules/language-chip'
import { getLanguage } from './utils'
import { getLanguage, ProfileType, ProfileTypes } from './utils'
const ProfileMonacoViewer = lazy(() => import('./profile-monaco-viewer'))
@@ -21,25 +27,25 @@ const formCommonProps = {
const optionTypeMapping = [
{
id: 'js',
value: Profile.Type.JavaScript,
value: ProfileTypes.JavaScript,
language: 'javascript',
label: 'JavaScript',
},
{
id: 'lua',
value: Profile.Type.LuaScript,
value: ProfileTypes.LuaScript,
language: 'lua',
label: 'LuaScript',
},
{
id: 'merge',
value: Profile.Type.Merge,
value: ProfileTypes.Merge,
language: 'yaml',
label: 'Merge',
},
]
const convertTypeMapping = (data: Profile.Item) => {
const convertTypeMapping = (data: Profile) => {
optionTypeMapping.forEach((option) => {
if (option.id === data.type) {
data.type = option.value
@@ -52,7 +58,7 @@ const convertTypeMapping = (data: Profile.Item) => {
export interface ScriptDialogProps extends Omit<BaseDialogProps, 'title'> {
open: boolean
onClose: () => void
profile?: Profile.Item
profile?: Profile
}
export const ScriptDialog = ({
@@ -63,10 +69,14 @@ export const ScriptDialog = ({
}: ScriptDialogProps) => {
const { t } = useTranslation()
const { getProfileFile, setProfileFile, createProfile, setProfiles } =
useClash()
// const { getProfileFile, setProfileFile, createProfile, setProfiles } =
// useClash()
const form = useForm<Profile.Item>()
const { create, update } = useProfile()
const contentFn = useProfileContent(profile?.uid ?? '')
const form = useForm<Profile>()
const isEdit = Boolean(profile)
@@ -81,16 +91,16 @@ export const ScriptDialog = ({
desc: '',
})
}
}, [form, isEdit, profile])
}, [form, isEdit, profile, t])
const [openMonaco, setOpenMonaco] = useState(false)
const editor = useReactive<{
value: string
language: string
rawType: Profile.Item['type']
rawType: ProfileType
}>({
value: Profile.Template.merge,
value: ProfileTemplate.merge,
language: 'yaml',
rawType: 'merge',
})
@@ -118,10 +128,19 @@ export const ScriptDialog = ({
try {
if (isEdit) {
await setProfileFile(data.uid, editorValue)
await setProfiles(data.uid, data)
await contentFn.upsert.mutateAsync(editorValue)
await update.mutateAsync({
uid: data.uid,
profile: data,
})
} else {
await createProfile(data, editorValue)
await create.mutateAsync({
type: 'manual',
data: {
item: data,
fileData: editorValue,
},
})
}
} finally {
onClose()
@@ -130,10 +149,12 @@ export const ScriptDialog = ({
useAsyncEffect(async () => {
if (isEdit) {
editor.value = await getProfileFile(profile?.uid)
editor.language = getLanguage(profile?.type)!
await contentFn.query.refetch()
editor.value = contentFn.query.data ?? ''
editor.language = getLanguage(profile!.type)!
} else {
editor.value = Profile.Template.merge
editor.value = ProfileTemplate.merge
editor.language = 'yaml'
}
@@ -155,17 +176,17 @@ export const ScriptDialog = ({
switch (lang) {
case 'yaml': {
editor.value = Profile.Template.merge
editor.value = ProfileTemplate.merge
break
}
case 'lua': {
editor.value = Profile.Template.luascript
editor.value = ProfileTemplate.luascript
break
}
case 'javascript': {
editor.value = Profile.Template.javascript
editor.value = ProfileTemplate.javascript
break
}
}
@@ -177,7 +198,9 @@ export const ScriptDialog = ({
<div className="flex gap-2">
<span>{isEdit ? t('Edit Script') : t('New Script')}</span>
<LanguageChip type={isEdit ? profile?.type : editor.rawType} />
<LanguageChip
type={isEdit ? (profile?.type ?? editor.rawType) : editor.rawType}
/>
</div>
}
open={open}
@@ -242,7 +265,7 @@ export const ScriptDialog = ({
editorMarks.current = marks
}}
schemaType={
editor.rawType === Profile.Type.Merge ? 'merge' : undefined
editor.rawType === ProfileTypes.Merge ? 'merge' : undefined
}
/>
)}
@@ -1,54 +1,80 @@
import { isEqual } from 'lodash-es'
import { Profile } from '@nyanpasu/interface'
import type {
LocalProfile,
MergeProfile,
Profile,
RemoteProfile,
ScriptProfile,
} from '@nyanpasu/interface'
export const filterProfiles = (items?: Profile.Item[]) => {
const getItems = (types: (string | { script: string })[]) => {
return items?.filter((i) => {
if (!i) return false
/**
* Represents a Clash configuration profile, which can be either locally stored or fetched from a remote source.
*/
export type ClashProfile = LocalProfile | RemoteProfile
if (typeof i.type === 'string') {
return types.includes(i.type)
}
/**
* Represents a Clash configuration profile that is a chain of multiple profiles.
*/
export type ChainProfile = MergeProfile | ScriptProfile
if (typeof i.type === 'object' && i.type !== null) {
return types.some(
(type) =>
typeof type === 'object' &&
(i.type as { script: string }).script === type.script,
)
}
/**
* Filters an array of profiles into two categories: clash and chain profiles.
*
* @param items - Array of Profile objects to be filtered
* @returns An object containing two arrays:
* - clash: Array of profiles where type is 'remote' or 'local'
* - chain: Array of profiles where type is 'merge' or has a script property
*/
export function filterProfiles<T extends Profile>(items?: T[]) {
/**
* Filters the input array to include only items of type 'remote' or 'local'
* @param items - Array of items to filter
* @returns {Array} Filtered array containing only remote and local items
*/
const clash = items?.filter(
(item) => item.type === 'remote' || item.type === 'local',
)
return false
})
}
const profiles = getItems([Profile.Type.Local, Profile.Type.Remote])
const scripts = getItems([
Profile.Type.Merge,
Profile.Type.JavaScript,
Profile.Type.LuaScript,
])
/**
* Filters an array of items to get a chain of either 'merge' type items
* or items with a script property in their type object.
*
* @param {Array<{ type: string | { script: 'javascript' | 'lua' } }>} items - The array of items to filter
* @returns {Array<{ type: string | { script: 'javascript' | 'lua' } }>} A filtered array containing only merge items or items with scripts
*/
const chain = items?.filter(
(item) =>
item.type === 'merge' ||
(typeof item.type === 'object' && item.type.script),
)
return {
profiles,
scripts,
clash,
chain,
}
}
export const getLanguage = (type: Profile.Item['type'], snake?: boolean) => {
export type ProfileType = Profile['type']
export const ProfileTypes = {
JavaScript: { script: 'javascript' },
LuaScript: { script: 'lua' },
Merge: 'merge',
} as const
export const getLanguage = (type: ProfileType, snake?: boolean) => {
switch (true) {
case isEqual(type, Profile.Type.JavaScript):
case isEqual(type, Profile.Type.JavaScript.script): {
case isEqual(type, ProfileTypes.JavaScript):
case isEqual(type, ProfileTypes.JavaScript.script): {
return snake ? 'JavaScript' : 'javascript'
}
case isEqual(type, Profile.Type.LuaScript):
case isEqual(type, Profile.Type.LuaScript.script): {
case isEqual(type, ProfileTypes.LuaScript):
case isEqual(type, ProfileTypes.LuaScript.script): {
return snake ? 'Lua' : 'lua'
}
case isEqual(type, Profile.Type.Merge): {
case isEqual(type, ProfileTypes.Merge): {
return snake ? 'YAML' : 'yaml'
}
}
@@ -1,7 +1,7 @@
import { CSSProperties, memo, useMemo } from 'react'
import { alpha, useTheme } from '@mui/material'
import Box from '@mui/material/Box'
import { Clash } from '@nyanpasu/interface'
import { ProxyItem } from '@nyanpasu/interface'
import { cn } from '@nyanpasu/ui'
import { PaperSwitchButton } from '../setting/modules/system-proxy'
import DelayChip from './delay-chip'
@@ -17,8 +17,8 @@ export const NodeCard = memo(function NodeCard({
onClickDelay,
style,
}: {
node: Clash.Proxy<string>
now?: string
node: ProxyItem
now?: string | null
disabled?: boolean
onClick: () => void
onClickDelay: () => Promise<void>
@@ -13,12 +13,17 @@ import {
import { Virtualizer, VListHandle } from 'virtua'
import { proxyGroupAtom, proxyGroupSortAtom } from '@/store'
import { proxiesFilterAtom } from '@/store/proxies'
import { Clash, useClashCore, useNyanpasu } from '@nyanpasu/interface'
import {
ProxyGroupItem,
ProxyItem,
useClashCore,
useNyanpasu,
} from '@nyanpasu/interface'
import { cn, useBreakpointValue } from '@nyanpasu/ui'
import NodeCard from './node-card'
import { nodeSortingFn } from './utils'
type RenderClashProxy = Clash.Proxy<string> & { renderLayoutKey: string }
type RenderClashProxy = ProxyItem & { renderLayoutKey: string }
export interface NodeListRef {
scrollToCurrent: () => void
@@ -44,12 +49,13 @@ export const NodeList = forwardRef(function NodeList(
const proxyGroupSort = useAtomValue(proxyGroupSortAtom)
const [group, setGroup] = useState<Clash.Proxy<Clash.Proxy<string>>>()
const [group, setGroup] = useState<ProxyGroupItem>()
const sortGroup = useCallback(() => {
if (!getCurrentMode.global) {
if (proxyGroup.selector !== null) {
const selectedGroup = data?.groups[proxyGroup.selector]
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
const selectedGroup = data?.groups[proxyGroup.selector]!
if (selectedGroup) {
setGroup(nodeSortingFn(selectedGroup, proxyGroupSort))
@@ -1,4 +1,4 @@
import type { Clash } from '@nyanpasu/interface'
import type { Clash, ProxyGroupItem } from '@nyanpasu/interface'
export type History = Clash.Proxy['history']
@@ -17,7 +17,7 @@ export enum SortType {
}
export const nodeSortingFn = (
selectedGroup: Clash.Proxy<Clash.Proxy<string>>,
selectedGroup: ProxyGroupItem,
type: SortType,
) => {
let sortedList = selectedGroup.all?.slice()
@@ -26,7 +26,7 @@ import { openThat } from '@nyanpasu/interface'
export const renderChip = (
string: string,
labels: {
[label: string]: string | number | undefined
[label: string]: string | number | undefined | null
},
): (string | ReactElement)[] => {
return string.split(/(%[^&?]+)/).map((part, index) => {
@@ -95,7 +95,7 @@ export const extractServer = (
export const openWebUrl = (
string: string,
labels: {
[label: string]: string | number | undefined
[label: string]: string | number | undefined | null
},
): void => {
let url = ''
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import CLASH_FIELD from '@/assets/json/clash-field.json'
import { Box, Typography } from '@mui/material'
import Grid from '@mui/material/Grid2'
import { useClash, useNyanpasu } from '@nyanpasu/interface'
import { useClash, useNyanpasu, useProfile } from '@nyanpasu/interface'
import { BaseCard, BaseDialog } from '@nyanpasu/ui'
import { ClashFieldItem, LabelSwitch } from './modules/clash-field'
@@ -91,7 +91,7 @@ const ClashFieldSwitch = () => {
export const SettingClashField = () => {
const { t } = useTranslation()
const { getProfiles, setProfilesConfig } = useClash()
const { query, upsert } = useProfile()
const mergeFields = useMemo(
() => [
@@ -99,9 +99,9 @@ export const SettingClashField = () => {
...Object.keys(CLASH_FIELD.default),
...Object.keys(CLASH_FIELD.handle),
],
...(getProfiles.data?.valid ?? []),
...(query.data?.valid ?? []),
],
[getProfiles.data],
[query.data],
)
const filteredField = (fields: { [key: string]: string }): string[] => {
@@ -121,7 +121,7 @@ export const SettingClashField = () => {
const updateFiled = async (key: string) => {
const getFields = (): string[] => {
const valid = getProfiles.data?.valid ?? []
const valid = query.data?.valid ?? []
if (valid.includes(key)) {
return valid.filter((item) => item !== key)
@@ -132,7 +132,7 @@ export const SettingClashField = () => {
}
}
await setProfilesConfig({ valid: getFields() })
await upsert.mutateAsync({ valid: getFields() })
}
return (
@@ -1,20 +1,38 @@
import { useAtom } from 'jotai'
import { MuiColorInput } from 'mui-color-input'
import { useRef, useState } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { isHexColor } from 'validator'
import { defaultTheme } from '@/pages/-theme'
import { atomIsDrawerOnlyIcon } from '@/store'
import { languageOptions } from '@/utils/language'
import Done from '@mui/icons-material/Done'
import { Box, Button, List, ListItem, ListItemText } from '@mui/material'
import { useNyanpasu, VergeConfig } from '@nyanpasu/interface'
import { Button, List, ListItem, ListItemText } from '@mui/material'
import { useSetting } from '@nyanpasu/interface'
import { BaseCard, Expand, MenuItem, SwitchItem } from '@nyanpasu/ui'
export const SettingNyanpasuUI = () => {
const commonSx = {
width: 128,
}
const LanguageSwitch = () => {
const { t } = useTranslation()
const { nyanpasuConfig, setNyanpasuConfig } = useNyanpasu()
const language = useSetting('language')
return (
<MenuItem
label={t('Language')}
selectSx={commonSx}
options={languageOptions}
selected={language.value || 'en'}
onSelected={(value) => language.upsert(value as string)}
/>
)
}
const ThemeSwitch = () => {
const { t } = useTranslation()
const themeOptions = {
dark: t('theme.dark'),
@@ -22,89 +40,80 @@ export const SettingNyanpasuUI = () => {
system: t('theme.system'),
}
const [themeColor, setThemeColor] = useState(
nyanpasuConfig?.theme_setting?.primary_color,
)
const themeColorRef = useRef(themeColor)
const themeMode = useSetting('theme_mode')
const commonSx = {
width: 128,
}
return (
<MenuItem
label={t('Theme Mode')}
selectSx={commonSx}
options={themeOptions}
selected={themeMode.value || 'system'}
onSelected={(value) => themeMode.upsert(value as string)}
/>
)
}
const ThemeColor = () => {
const { t } = useTranslation()
const theme = useSetting('theme_color')
const [value, setValue] = useState(theme.value ?? defaultTheme.primary_color)
useEffect(() => {
setValue(theme.value ?? defaultTheme.primary_color)
}, [theme.value])
return (
<>
<ListItem sx={{ pl: 0, pr: 0 }}>
<ListItemText primary={t('Theme Setting')} />
<MuiColorInput
size="small"
sx={commonSx}
value={theme.value ?? '#1867c0'}
isAlphaHidden
format="hex"
onBlur={() => {
if (!isHexColor(value ?? defaultTheme.primary_color)) {
setValue(value)
}
}}
onChange={(color: string) => setValue(color)}
/>
</ListItem>
<Expand open={theme.value !== value}>
<div className="flex justify-end">
<Button
variant="contained"
startIcon={<Done />}
onClick={() => {
theme.upsert(value)
}}
>
Apply
</Button>
</div>
</Expand>
</>
)
}
export const SettingNyanpasuUI = () => {
const { t } = useTranslation()
const [onlyIcon, setOnlyIcon] = useAtom(atomIsDrawerOnlyIcon)
return (
<BaseCard label={t('User Interface')}>
<List disablePadding>
<MenuItem
label={t('Language')}
selectSx={commonSx}
options={languageOptions}
selected={nyanpasuConfig?.language || 'en'}
onSelected={(value) =>
setNyanpasuConfig({ language: value as string })
}
/>
<LanguageSwitch />
<MenuItem
label={t('Theme Mode')}
selectSx={commonSx}
options={themeOptions}
selected={nyanpasuConfig?.theme_mode || 'light'}
onSelected={(value) =>
setNyanpasuConfig({
theme_mode: value as VergeConfig['theme_mode'],
})
}
/>
<ThemeSwitch />
<ListItem sx={{ pl: 0, pr: 0 }}>
<ListItemText primary={t('Theme Setting')} />
<MuiColorInput
size="small"
sx={commonSx}
value={themeColor ?? defaultTheme.primary_color}
isAlphaHidden
format="hex"
onBlur={() => {
if (
!isHexColor(themeColorRef.current ?? defaultTheme.primary_color)
) {
setThemeColor(themeColorRef.current)
return
}
themeColorRef.current = themeColor
}}
onChange={(color: string) => setThemeColor(color)}
/>
</ListItem>
<Expand
open={nyanpasuConfig?.theme_setting?.primary_color !== themeColor}
>
<Box
sx={{ pb: 1 }}
display="flex"
justifyContent="end"
alignItems="center"
>
<Button
variant="contained"
startIcon={<Done />}
onClick={() => {
setNyanpasuConfig({
theme_setting: {
...nyanpasuConfig?.theme_setting,
primary_color: themeColor,
},
})
}}
>
Apply
</Button>
</Box>
</Expand>
<ThemeColor />
<SwitchItem
label={t('Icon Navigation Bar')}
@@ -1,63 +1,167 @@
import { useLockFn, useReactive } from 'ahooks'
import { useLockFn } from 'ahooks'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { message } from '@/utils/notification'
import { Done } from '@mui/icons-material'
import {
Box,
Button,
InputAdornment,
List,
ListItem,
TextField,
Typography,
} from '@mui/material'
import { InputAdornment, List, ListItem } from '@mui/material'
import Grid from '@mui/material/Grid2'
import { useNyanpasu } from '@nyanpasu/interface'
import { useSetting, useSystemProxy } from '@nyanpasu/interface'
import {
BaseCard,
Expand,
ExpandMore,
NumberItem,
SwitchItem,
TextItem,
} from '@nyanpasu/ui'
import { PaperSwitchButton } from './modules/system-proxy'
const TunModeButton = () => {
const { t } = useTranslation()
const tunMode = useSetting('enable_tun_mode')
const handleTunMode = useLockFn(async () => {
try {
await tunMode.upsert(!tunMode.value)
} catch (error) {
message(`Activation TUN Mode failed!`, {
title: t('Error'),
kind: 'error',
})
}
})
return (
<PaperSwitchButton
label={t('TUN Mode')}
checked={Boolean(tunMode.value)}
onClick={handleTunMode}
/>
)
}
const SystemProxyButton = () => {
const { t } = useTranslation()
const systemProxy = useSetting('enable_system_proxy')
const handleSystemProxy = useLockFn(async () => {
try {
await systemProxy.upsert(!systemProxy.value)
} catch (error) {
message(`Activation System Proxy failed!`, {
title: t('Error'),
kind: 'error',
})
}
})
return (
<PaperSwitchButton
label={t('System Proxy')}
checked={Boolean(systemProxy.value)}
onClick={handleSystemProxy}
/>
)
}
const ProxyGuardSwitch = () => {
const { t } = useTranslation()
const proxyGuard = useSetting('enable_proxy_guard')
const handleProxyGuard = useLockFn(async () => {
try {
await proxyGuard.upsert(!proxyGuard.value)
} catch (error) {
message(`Activation Proxy Guard failed!`, {
title: t('Error'),
kind: 'error',
})
}
})
return (
<SwitchItem
label={t('Proxy Guard')}
checked={Boolean(proxyGuard.value)}
onClick={handleProxyGuard}
/>
)
}
const ProxyGuardInterval = () => {
const { t } = useTranslation()
const proxyGuardInterval = useSetting('proxy_guard_interval')
return (
<NumberItem
label={t('Guard Interval')}
value={proxyGuardInterval.value || 0}
checkEvent={(input) => input <= 0}
checkLabel={t('The interval must be greater than 0 second')}
onApply={(value) => {
proxyGuardInterval.upsert(value)
}}
textFieldProps={{
inputProps: {
'aria-autocomplete': 'none',
},
InputProps: {
endAdornment: <InputAdornment position="end">s</InputAdornment>,
},
}}
/>
)
}
const SystemProxyBypass = () => {
const { t } = useTranslation()
const systemProxyBypass = useSetting('system_proxy_bypass')
return (
<TextItem
label={t('Proxy Bypass')}
value={systemProxyBypass.data || ''}
onApply={(value) => {
systemProxyBypass.upsert(value)
}}
/>
)
}
const CurrentSystemProxy = () => {
const { t } = useTranslation()
const { data } = useSystemProxy()
return (
<ListItem
className="!w-full !flex-col !items-start select-text"
sx={{ pl: 0, pr: 0 }}
>
<div className="text-base leading-10">{t('Current System Proxy')}</div>
{Object.entries(data ?? []).map(([key, value], index) => {
return (
<div key={index} className="flex w-full leading-8">
<div className="w-28 capitalize">{key}:</div>
<div className="text-warp flex-1 break-all">{String(value)}</div>
</div>
)
})}
</ListItem>
)
}
export const SettingSystemProxy = () => {
const { t } = useTranslation()
const { nyanpasuConfig, setNyanpasuConfig, getSystemProxy } = useNyanpasu()
const loading = useReactive({
enable_tun_mode: false,
enable_system_proxy: false,
})
const handleClick = useLockFn(
async (key: 'enable_system_proxy' | 'enable_tun_mode') => {
try {
loading[key] = true
await setNyanpasuConfig({
[key]: !nyanpasuConfig?.[key],
})
} catch (e) {
message(`Activation failed!`, {
title: t('Error'),
kind: 'error',
})
} finally {
loading[key] = false
}
},
)
const [expand, setExpand] = useState(false)
const [proxyBypass, setProxyBypass] = useState(
nyanpasuConfig?.system_proxy_bypass || '',
)
return (
<BaseCard
label={t('System Setting')}
@@ -66,107 +170,24 @@ export const SettingSystemProxy = () => {
}
>
<Grid container spacing={2}>
<Grid
size={{
xs: 6,
}}
>
<PaperSwitchButton
label={t('TUN Mode')}
checked={nyanpasuConfig?.enable_tun_mode || false}
loading={loading.enable_tun_mode}
onClick={() => handleClick('enable_tun_mode')}
/>
<Grid size={{ xs: 6 }}>
<TunModeButton />
</Grid>
<Grid size={{ xs: 6 }}>
<PaperSwitchButton
label={t('System Proxy')}
checked={nyanpasuConfig?.enable_system_proxy || false}
loading={loading.enable_system_proxy}
onClick={() => handleClick('enable_system_proxy')}
/>
<SystemProxyButton />
</Grid>
</Grid>
<Expand open={expand}>
<List disablePadding sx={{ pt: 1 }}>
<SwitchItem
label={t('Proxy Guard')}
checked={nyanpasuConfig?.enable_proxy_guard || false}
onChange={() =>
setNyanpasuConfig({
enable_proxy_guard: !nyanpasuConfig?.enable_proxy_guard,
})
}
/>
<ProxyGuardSwitch />
<NumberItem
label={t('Guard Interval')}
value={nyanpasuConfig?.proxy_guard_interval || 0}
checkEvent={(input) => input <= 0}
checkLabel={t('The interval must be greater than 0 second')}
onApply={(value) => {
setNyanpasuConfig({ proxy_guard_interval: value })
}}
textFieldProps={{
inputProps: {
'aria-autocomplete': 'none',
},
InputProps: {
endAdornment: <InputAdornment position="end">s</InputAdornment>,
},
}}
/>
<ProxyGuardInterval />
<ListItem sx={{ pl: 0, pr: 0 }}>
<TextField
value={proxyBypass}
label={t('Proxy Bypass')}
variant="outlined"
sx={{ width: '100%' }}
multiline
onChange={(e) => setProxyBypass(e.target.value)}
/>
</ListItem>
<SystemProxyBypass />
<Expand open={proxyBypass !== nyanpasuConfig?.system_proxy_bypass}>
<Box sx={{ pb: 1 }} display="flex" justifyContent="end">
<Button
variant="contained"
startIcon={<Done />}
onClick={() => {
setNyanpasuConfig({ system_proxy_bypass: proxyBypass })
}}
>
{t('Apply')}
</Button>
</Box>
</Expand>
<ListItem sx={{ pl: 0, pr: 0 }}>
<Box>
<Typography variant="body1" sx={{ fontSize: '18px', mb: 1 }}>
{t('Current System Proxy')}
</Typography>
{Object.entries(getSystemProxy?.data ?? []).map(
([key, value], index) => {
return (
<Box key={index} display="flex" sx={{ pt: 1 }}>
<Typography
sx={{ width: 80, textTransform: 'capitalize' }}
>
{key}:
</Typography>
<Typography>{String(value)}</Typography>
</Box>
)
},
)}
</Box>
</ListItem>
<CurrentSystemProxy />
</List>
</Expand>
</BaseCard>
@@ -15,7 +15,7 @@
"Active Connections": "Active Connections",
"Timeout": "Timeout",
"Click to Refresh Now": "Click to Refresh Now",
"No Proxy": "No Proxy",
"No Proxies": "No Proxies",
"Direct Mode": "Direct Mode",
"Rules": "Rules",
"No Rules": "No Rules",
@@ -15,7 +15,7 @@
"Active Connections": "Активные соединения",
"Timeout": "Тайм-аут",
"Click to Refresh Now": "Нажмите для обновления",
"No Proxy": "Без прокси",
"No Proxies": "Без прокси",
"Direct Mode": "Прямой режим",
"Rules": "Правила",
"No Rules": "Нет правил",
@@ -15,7 +15,7 @@
"Active Connections": "活动连接",
"Timeout": "超时",
"Click to Refresh Now": "点击立即刷新",
"No Proxy": "无代理",
"No Proxies": "无代理",
"Direct Mode": "直连模式",
"Rules": "规则",
"No Rules": "无规则",
@@ -54,7 +54,7 @@
"Open": "打开",
"Open File": "打开文件",
"Update": "更新",
"Update(Proxy)": "更新(代理)",
"Update(Proxy)": "更新(使用代理",
"Delete": "删除",
"Enable": "启用",
"Disable": "禁用",
@@ -62,9 +62,9 @@
"To Top": "移到最前",
"To End": "移到末尾",
"Update All Profiles": "更新所有配置",
"View Runtime Config": "查看运行配置",
"View Runtime Config": "查看运行配置",
"Reactivate Profiles": "重新激活配置",
"Location": "当前节点",
"Location": "当前使用节点",
"Delay check": "延迟测试",
"Sort by default": "默认排序",
"Sort by delay": "按延迟排序",
@@ -171,7 +171,7 @@
"Nyanpasu Version": "Nyanpasu 版本",
"theme.light": "浅色",
"theme.dark": "深色",
"theme.system": "系统",
"theme.system": "跟随系统",
"Clash Field": "Clash 字段",
"Original Config": "原始配置",
"Runtime Config": "运行配置",
@@ -239,7 +239,7 @@
"Proxy Takeover Status": "代理接管状态",
"Subscription Expires In": "{{time}}到期",
"Subscription Updated At": "{{time}}更新",
"Choose file to import or leave it blank to create new one": "选择文件导入或留空新建。",
"Choose file to import or leave it blank to create new one": "选择文件导入或留空新建配置。",
"updater": {
"title": "发现新版本",
"close": "忽略",
@@ -268,7 +268,7 @@
},
"service": "服务",
"UI": "用户界面",
"Service Manual Tips": "服务提示手册",
"Service Manual Tips": "有关服务提示",
"Unable to operation the service automatically": "无法自动{{operation}}服务。请导航到内核所在目录,在 Windows 上以管理员身份打开 PowerShell或在 macOS/Linux 上打开终端仿真器,然后执行以下命令:",
"Successfully switched to the clash core": "成功切换至 {{core}} 内核。",
"Failed to switch. You could see the details in the log": "切换失败,可以在日志中查看详细信息。\n错误:{{error}}",
@@ -15,7 +15,7 @@
"Active Connections": "活動連線",
"Timeout": "逾時",
"Click to Refresh Now": "點擊立即重新整理",
"No Proxy": "無代理",
"No Proxies": "無代理",
"Direct Mode": "直連模式",
"Rules": "規則",
"No Rules": "無規則",
@@ -43,7 +43,7 @@
"Close All": "關閉全部",
"Menu": "選單",
"Select": "使用",
"Applying Profile": "正在用設定檔……",
"Applying Profile": "正在用設定檔……",
"Edit Info": "編輯資訊",
"Proxy Chains": "代理鏈",
"Global Proxy Chains": "全域鏈",
@@ -89,7 +89,7 @@
"Destination IP": "目標位址",
"Destination ASN": "目標 ASN",
"Type": "類型",
"Connection Detail": "連詳情",
"Connection Detail": "連詳情",
"Metadata": "原始資訊",
"Remote": "遠端",
"Local": "本機",
@@ -113,7 +113,7 @@
"Clash Setting": "Clash 設定",
"System Setting": "系統設定",
"Nyanpasu Setting": "Nyanpasu 設定",
"Allow LAN": "區域網路連",
"Allow LAN": "區域網路連",
"IPv6": "IPv6",
"TUN Stack": "TUN 堆疊",
"Log Level": "日誌等級",
@@ -122,7 +122,7 @@
"Random Port": "隨機埠",
"After restart to take effect": "重啟後生效。",
"Clash External Controll": "Clash 外部控制",
"External Controller": "外部控制器監聽址",
"External Controller": "外部控制器監聽址",
"Port Strategy": "埠策略",
"Allow Fallback": "允許 fallback",
"Fixed": "固定",
@@ -133,7 +133,7 @@
"System Service": "系統服務",
"Service Mode": "服務模式",
"Initiating Behavior": "啟動行為",
"Auto Start": "開機啟",
"Auto Start": "開機啟",
"Silent Start": "靜默啟動",
"System Proxy": "系統代理",
"Open UWP Tool": "UWP 工具",
@@ -142,7 +142,7 @@
"Guard Interval": "代理守衛間隔",
"The interval must be greater than 0 second": "間隔時間必須大於 0 秒。",
"Proxy Bypass": "代理繞過",
"Apply": "用",
"Apply": "用",
"Current System Proxy": "目前系統代理",
"User Interface": "使用者介面",
"Theme Mode": "主題模式",
@@ -161,7 +161,7 @@
"Page Transition Animation None": "無",
"Language": "語言設定",
"Path Config": "目錄配置",
"Migrate App Path": "遷移應用程式路徑",
"Migrate App Path": "遷移 App 路徑",
"Open Config Dir": "配置目錄",
"Open Data Dir": "資料目錄",
"Open Core Dir": "核心目錄",
@@ -206,7 +206,7 @@
"toggle_tun_mode": "切換 TUN 模式",
"enable_tun_mode": "開啟 TUN 模式",
"disable_tun_mode": "關閉 TUN 模式",
"App Log Level": "應用程式日誌等級",
"App Log Level": "App 日誌等級",
"Auto Close Connections": "自動結束連線",
"Enable Clash Fields Filter": "開啟 Clash 欄位過濾",
"Enable Builtin Enhanced": "開啟內建增強功能",
@@ -232,14 +232,14 @@
"Update All Proxies Providers": "全部更新",
"Lighten Up Animation Effects": "減輕動畫效果",
"Subscription": "訂閱",
"FetchError": "由於網路問題,無法獲取{{content}}內容。請檢查網路連或稍後再試。",
"FetchError": "由於網路問題,無法獲取{{content}}內容。請檢查網路連或稍後再試。",
"tun": "TUN 模式",
"normal": "預設",
"system_proxy": "系統代理",
"Proxy Takeover Status": "代理接管狀態",
"Subscription Expires In": "{{time}}過期",
"Subscription Updated At": "{{time}}更新",
"Choose file to import or leave it blank to create new one": "選取文件匯入,或留空以建立新檔。",
"Choose file to import or leave it blank to create new one": "選取檔案匯入,或留空以建立新檔。",
"updater": {
"title": "發現新版本",
"close": "忽略",
@@ -274,11 +274,11 @@
"Failed to switch. You could see the details in the log": "切換失敗,可以在日誌中查看詳細資訊。\n錯誤:{{error}}",
"Successfully restarted the core": "成功重啟核心。",
"Failed to restart. You could see the details in the log": "重啟失敗,詳細資訊請檢查日誌。\n\n錯誤:",
"Failed to fetch. Please check your network connection": "獲取更新失敗,請檢查你的網路連。",
"Failed to fetch. Please check your network connection": "獲取更新失敗,請檢查你的網路連。",
"Successfully updated the core": "成功更新「{{core}}」核心。",
"Failed to update": "更新失敗。{{error}}",
"Multiple directories are not supported": "不支援多個目錄。",
"Successfully changed the app directory": "應用程式目錄更改成功。",
"Successfully changed the app directory": "App 目錄更改成功。",
"Failed to migrate": "遷移失敗。{{error}}",
"Web UI": "Web UI",
"New Item": "添加新項目",
@@ -16,6 +16,10 @@ if (!window.ResizeObserver) {
window.ResizeObserver = ResizeObserver
}
window.addEventListener('error', (event) => {
console.error(event)
})
// Set up a Router instance
const router = createRouter({
routeTree,
@@ -20,13 +20,18 @@ import ProfileSide from '@/components/profiles/profile-side'
import { GlobalUpdatePendingContext } from '@/components/profiles/provider'
import { QuickImport } from '@/components/profiles/quick-import'
import RuntimeConfigDiffDialog from '@/components/profiles/runtime-config-diff-dialog'
import { filterProfiles } from '@/components/profiles/utils'
import { ClashProfile, filterProfiles } from '@/components/profiles/utils'
import { formatError } from '@/utils'
import { message } from '@/utils/notification'
import { Public, Update } from '@mui/icons-material'
import { Badge, Button, CircularProgress, IconButton } from '@mui/material'
import Grid from '@mui/material/Grid2'
import { Profile, updateProfile, useClash } from '@nyanpasu/interface'
import {
RemoteProfileOptionsBuilder,
useClash,
useProfile,
type RemoteProfile,
} from '@nyanpasu/interface'
import { FloatingButton, SidePage } from '@nyanpasu/ui'
import { createFileRoute, useLocation } from '@tanstack/react-router'
import { zodSearchValidator } from '@tanstack/router-zod-adapter'
@@ -44,12 +49,23 @@ export const Route = createFileRoute('/profiles')({
function ProfilePage() {
const { t } = useTranslation()
const { getProfiles, getRuntimeLogs } = useClash()
const { getRuntimeLogs } = useClash()
const { query, update } = useProfile()
const profiles = useMemo(() => {
return filterProfiles(query.data?.items)
}, [query.data?.items])
const maxLogLevelTriggered = useMemo(() => {
const currentProfileChains =
getProfiles.data?.items?.find(
// TODO: 支持多 Profile
(item) => getProfiles.data?.current[0] === item.uid,
(
query.data?.items?.find(
// TODO: 支持多 Profile
(item) => query.data?.current?.[0] === item.uid,
// TODO: fix any type
) as any
)?.chain || []
return Object.entries(getRuntimeLogs.data || {}).reduce(
(acc, [key, value]) => {
@@ -78,8 +94,7 @@ function ProfilePage() {
current: undefined | 'info' | 'error' | 'warn'
},
)
}, [getRuntimeLogs.data, getProfiles.data])
const { profiles } = filterProfiles(getProfiles.data?.items)
}, [query.data, getRuntimeLogs.data])
const [globalChain, setGlobalChain] = useAtom(atomGlobalChainCurrent)
@@ -90,7 +105,7 @@ function ProfilePage() {
setGlobalChain(!globalChain)
}
const onClickChains = (profile: Profile.Item) => {
const onClickChains = (profile: ClashProfile) => {
setGlobalChain(false)
if (chainsSelected === profile.uid) {
@@ -124,17 +139,31 @@ function ProfilePage() {
const [globalUpdatePending, startGlobalUpdate] = useTransition()
const handleGlobalProfileUpdate = useLockFn(async () => {
await startGlobalUpdate(async () => {
startGlobalUpdate(async () => {
const remoteProfiles =
profiles?.filter((item) => item.type === 'remote') || []
(profiles.clash?.filter(
(item) => item.type === 'remote',
) as RemoteProfile[]) || []
const updates: Array<Promise<void>> = []
for (const profile of remoteProfiles) {
const options: Profile.Option = profile.option || {
const option = {
with_proxy: false,
self_proxy: false,
}
update_interval: 0,
user_agent: profile.option?.user_agent ?? null,
...profile.option,
} satisfies RemoteProfileOptionsBuilder
updates.push(updateProfile(profile.uid, options))
const result = await update.mutateAsync({
uid: profile.uid,
profile: {
...profile,
option,
},
})
updates.push(Promise.resolve(result || undefined))
}
try {
await Promise.all(updates)
@@ -201,7 +230,7 @@ function ProfilePage() {
{profiles && (
<Grid container spacing={2}>
{profiles.map((item) => (
{profiles.clash?.map((item) => (
<Grid
key={item.uid}
size={{
@@ -221,7 +250,7 @@ function ProfilePage() {
<ProfileItem
item={item}
onClickChains={onClickChains}
selected={getProfiles.data?.current.includes(item.uid)}
selected={query.data?.current?.includes(item.uid)}
maxLogLevelTriggered={maxLogLevelTriggered}
chainsSelected={chainsSelected === item.uid}
/>
@@ -22,7 +22,12 @@ import {
TextField,
useTheme,
} from '@mui/material'
import { Clash, useClashCore, useNyanpasu } from '@nyanpasu/interface'
import {
Clash,
ProxyGroupItem,
useClashCore,
useNyanpasu,
} from '@nyanpasu/interface'
import { cn, SidePage } from '@nyanpasu/ui'
import { createFileRoute } from '@tanstack/react-router'
@@ -71,14 +76,13 @@ function ProxyPage() {
const [proxyGroup] = useAtom(proxyGroupAtom)
const [group, setGroup] =
useState<Clash.Proxy<Clash.Proxy<string> | string>>()
const [group, setGroup] = useState<ProxyGroupItem>()
useEffect(() => {
if (getCurrentMode.global) {
setGroup(data?.global)
} else if (getCurrentMode.direct) {
setGroup(data?.direct)
setGroup(data?.direct ? { ...data.direct, all: [] } : undefined)
} else {
if (proxyGroup.selector !== null) {
setGroup(data?.groups[proxyGroup.selector])
@@ -178,7 +182,7 @@ function ProxyPage() {
<DelayButton onClick={handleDelayClick} />
</>
) : (
<ContentDisplay className="absolute" message={t('No Proxy')} />
<ContentDisplay className="absolute" message={t('No Proxies')} />
)
) : (
<ContentDisplay className="absolute" message={t('Direct Mode')} />
+1 -1
View File
@@ -28,7 +28,7 @@
"@vitejs/plugin-react": "4.3.4",
"ahooks": "3.8.4",
"d3": "7.9.0",
"framer-motion": "12.0.6",
"framer-motion": "12.0.11",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-error-boundary": "5.0.0",
+1
View File
@@ -6,6 +6,7 @@
"ps": "Copy Env (PS)",
"sh": "Copy Env (sh)"
},
"no_proxies": "No Proxies",
"select_proxies": "Select Proxies",
"dashboard": "Dashboard",
"direct_mode": "Direct Mode",
+1
View File
@@ -6,6 +6,7 @@
"ps": "Копировать Env (PS)",
"sh": "Копировать Env (sh)"
},
"no_proxies": "Без прокси",
"select_proxies": "Выбрать прокси",
"dashboard": "Панель управления",
"direct_mode": "Прямой режим",
+8 -7
View File
@@ -2,17 +2,18 @@
"_version": 1,
"tray": {
"copy_env": {
"cmd": "复制环境变量(CMD)",
"ps": "复制环境变量(PS)",
"sh": "复制环境变量(sh)"
"cmd": "复制环境变量 (CMD)",
"ps": "复制环境变量 (PS)",
"sh": "复制环境变量 (SH)"
},
"no_proxies": "无代理",
"select_proxies": "选择代理",
"dashboard": "打开面板",
"direct_mode": "直连模式",
"global_mode": "全局模式",
"more": {
"menu": "更多",
"restart_app": "重启应用",
"restart_app": "重启应用程序",
"restart_clash": "重启 Clash"
},
"open_dir": {
@@ -33,11 +34,11 @@
"tun_mode": "TUN 模式"
},
"dialog": {
"panic": "请将此问题汇报到 GitHub 问题追踪器",
"migrate": "检测到旧版本配置文件\n是否迁移到新版本?\n警告:此操作会覆盖掉现有配置文件",
"panic": "请将此问题汇报到 GitHub Issues。",
"migrate": "检测到旧版本配置文件是否迁移到新版本?\n警告:此操作会覆盖掉现有配置文件",
"custom_app_dir_migrate": "你将要更改应用目录至 %{path}。\n需要将现有数据迁移到新目录吗?",
"warning": {
"enable_tun_with_no_permission": "TUN 模式需要管理员权限或服务模式,当前都未开启,因此 TUN 模式将无法正常工作"
"enable_tun_with_no_permission": "TUN 模式需要授予管理员权限或启用服务模式,当前都未开启,因此 TUN 模式将无法正常工作"
},
"info": {
"grant_core_permission": "Clash 内核需要管理员权限才能使得 TUN 模式正常工作,是否授予?\n\n请注意:此操作需要输入密码。"
+4 -3
View File
@@ -6,7 +6,8 @@
"ps": "複製環境變數 (PS)",
"sh": "複製環境變數 (SH)"
},
"select_proxies": "選擇代理",
"no_proxies": "代理",
"select_proxies": "選取代理",
"dashboard": "開啟儀表盤",
"direct_mode": "直連模式",
"global_mode": "全域模式",
@@ -35,9 +36,9 @@
"dialog": {
"panic": "請將此問題回報至 GitHub Issues。",
"migrate": "檢測到舊版本設定檔,是否遷移到新版本?\n警告:此操作會覆蓋掉現有設定檔。",
"custom_app_dir_migrate": "你將要更改應用目錄至 %{path}。\n需要將現有資料遷移到新目錄嗎?",
"custom_app_dir_migrate": "你將要更改 App 目錄至 %{path}。\n需要將現有資料遷移到新目錄嗎?",
"warning": {
"enable_tun_with_no_permission": "開啟 TUN 模式需要系統管理員權限或服務模式,目前都未啟,因此 TUN 模式將無法正常工作。"
"enable_tun_with_no_permission": "開啟 TUN 模式需要系統管理員權限或服務模式,目前都未啟,因此 TUN 模式將無法正常工作。"
},
"info": {
"grant_core_permission": "Clash 核心需要系統管理員權限才能使 TUN 模式正常工作,是否授予?\n請注意:此操作需要輸入密碼。"
+2 -2
View File
@@ -57,8 +57,8 @@
"lodash-es": "4.17.21"
},
"devDependencies": {
"@commitlint/cli": "19.6.1",
"@commitlint/config-conventional": "19.6.0",
"@commitlint/cli": "19.7.1",
"@commitlint/config-conventional": "19.7.1",
"@eslint/compat": "1.2.6",
"@eslint/eslintrc": "3.2.0",
"@ianvs/prettier-plugin-sort-imports": "4.4.1",
+112 -106
View File
@@ -19,11 +19,11 @@ importers:
version: 4.17.21
devDependencies:
'@commitlint/cli':
specifier: 19.6.1
version: 19.6.1(@types/node@22.13.0)(typescript@5.7.3)
specifier: 19.7.1
version: 19.7.1(@types/node@22.13.0)(typescript@5.7.3)
'@commitlint/config-conventional':
specifier: 19.6.0
version: 19.6.0
specifier: 19.7.1
version: 19.7.1
'@eslint/compat':
specifier: 1.2.6
version: 1.2.6(eslint@9.19.0(jiti@2.4.2))
@@ -183,6 +183,9 @@ importers:
ahooks:
specifier: 3.8.4
version: 3.8.4(react@19.0.0)
lodash-es:
specifier: 4.17.21
version: 4.17.21
ofetch:
specifier: 1.4.1
version: 1.4.1
@@ -193,6 +196,9 @@ importers:
specifier: 2.3.0
version: 2.3.0(react@19.0.0)
devDependencies:
'@types/lodash-es':
specifier: 4.17.12
version: 4.17.12
'@types/react':
specifier: 19.0.8
version: 19.0.8
@@ -254,8 +260,8 @@ importers:
specifier: 1.11.13
version: 1.11.13
framer-motion:
specifier: 12.0.6
version: 12.0.6(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
specifier: 12.0.11
version: 12.0.11(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
i18next:
specifier: 24.2.2
version: 24.2.2(typescript@5.7.3)
@@ -339,8 +345,8 @@ importers:
specifier: 1.99.0
version: 1.99.0(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@tanstack/router-plugin':
specifier: 1.99.0
version: 1.99.0(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.0.11(@types/node@22.13.0)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.1)(sass-embedded@1.83.4)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.7.0))
specifier: 1.99.3
version: 1.99.3(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.0.11(@types/node@22.13.0)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.1)(sass-embedded@1.83.4)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.7.0))
'@tauri-apps/plugin-clipboard-manager':
specifier: 2.2.1
version: 2.2.1
@@ -480,8 +486,8 @@ importers:
specifier: 7.9.0
version: 7.9.0
framer-motion:
specifier: 12.0.6
version: 12.0.6(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
specifier: 12.0.11
version: 12.0.11(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react:
specifier: 19.0.0
version: 19.0.0
@@ -1211,13 +1217,13 @@ packages:
'@bufbuild/protobuf@2.2.3':
resolution: {integrity: sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==}
'@commitlint/cli@19.6.1':
resolution: {integrity: sha512-8hcyA6ZoHwWXC76BoC8qVOSr8xHy00LZhZpauiD0iO0VYbVhMnED0da85lTfIULxl7Lj4c6vZgF0Wu/ed1+jlQ==}
'@commitlint/cli@19.7.1':
resolution: {integrity: sha512-iObGjR1tE/PfDtDTEfd+tnRkB3/HJzpQqRTyofS2MPPkDn1mp3DBC8SoPDayokfAy+xKhF8+bwRCJO25Nea0YQ==}
engines: {node: '>=v18'}
hasBin: true
'@commitlint/config-conventional@19.6.0':
resolution: {integrity: sha512-DJT40iMnTYtBtUfw9ApbsLZFke1zKh6llITVJ+x9mtpHD08gsNXaIRqHTmwTZL3dNX5+WoyK7pCN/5zswvkBCQ==}
'@commitlint/config-conventional@19.7.1':
resolution: {integrity: sha512-fsEIF8zgiI/FIWSnykdQNj/0JE4av08MudLTyYHm4FlLWemKoQvPNUYU2M/3tktWcCEyq7aOkDDgtjrmgWFbvg==}
engines: {node: '>=v18'}
'@commitlint/config-validator@19.5.0':
@@ -1236,12 +1242,12 @@ packages:
resolution: {integrity: sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==}
engines: {node: '>=v18'}
'@commitlint/is-ignored@19.6.0':
resolution: {integrity: sha512-Ov6iBgxJQFR9koOupDPHvcHU9keFupDgtB3lObdEZDroiG4jj1rzky60fbQozFKVYRTUdrBGICHG0YVmRuAJmw==}
'@commitlint/is-ignored@19.7.1':
resolution: {integrity: sha512-3IaOc6HVg2hAoGleRK3r9vL9zZ3XY0rf1RsUf6jdQLuaD46ZHnXBiOPTyQ004C4IvYjSWqJwlh0/u2P73aIE3g==}
engines: {node: '>=v18'}
'@commitlint/lint@19.6.0':
resolution: {integrity: sha512-LRo7zDkXtcIrpco9RnfhOKeg8PAnE3oDDoalnrVU/EVaKHYBWYL1DlRR7+3AWn0JiBqD8yKOfetVxJGdEtZ0tg==}
'@commitlint/lint@19.7.1':
resolution: {integrity: sha512-LhcPfVjcOcOZA7LEuBBeO00o3MeZa+tWrX9Xyl1r9PMd5FWsEoZI9IgnGqTKZ0lZt5pO3ZlstgnRyY1CJJc9Xg==}
engines: {node: '>=v18'}
'@commitlint/load@19.6.1':
@@ -2905,8 +2911,8 @@ packages:
'@tanstack/react-router':
optional: true
'@tanstack/router-plugin@1.99.0':
resolution: {integrity: sha512-Ue96luAqdwL4QtT4CqNQvegScqECoztt6MfyRsz/agG9JtU/7Mpd6h/vKmdXZpg6MR6iC2R1co164NjzAMod7A==}
'@tanstack/router-plugin@1.99.3':
resolution: {integrity: sha512-bE4S8MBXRje5VaslZhv+xaj/0rOpE2QaybwJ53ms5t6JrTkQ42UPKF3paHQuZekzb8ZHbduIk7BubfNLiDBxUw==}
engines: {node: '>=12'}
peerDependencies:
'@rsbuild/core': '>=1.0.2'
@@ -2920,8 +2926,8 @@ packages:
webpack:
optional: true
'@tanstack/router-utils@1.99.0':
resolution: {integrity: sha512-TAWImltqT8fS83E4L5tLNVI4Q1ZlefWYoIFh+ATo/+tLlJDa1E0d7p1/VjlPo+S1hlEoQPCe/ppZLcU6SG+8rg==}
'@tanstack/router-utils@1.99.3':
resolution: {integrity: sha512-aVyDLjuUJ4Uf8Qw+ihuU3kG+gd2f/P78Z81AQAryuF8Qm8bcSJukguCYVI4mL9zAkGAifZ9rVk0lR5BKcnI4qA==}
engines: {node: '>=12'}
'@tanstack/router-zod-adapter@1.81.5':
@@ -4900,8 +4906,8 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
framer-motion@12.0.6:
resolution: {integrity: sha512-LmrXbXF6Vv5WCNmb+O/zn891VPZrH7XbsZgRLBROw6kFiP+iTK49gxTv2Ur3F0Tbw6+sy9BVtSqnWfMUpH+6nA==}
framer-motion@12.0.11:
resolution: {integrity: sha512-1F+YNXr3bSHxt5sCzeCVL56sc4MngbOhdU5ptv02vaepdFYcQd0fZtuAHvFJgMbn5V7SOsaX/3hVqr21ZaCKhA==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -8415,8 +8421,8 @@ snapshots:
'@babel/helpers': 7.26.0
'@babel/parser': 7.26.3
'@babel/template': 7.25.9
'@babel/traverse': 7.26.4
'@babel/types': 7.26.3
'@babel/traverse': 7.26.7
'@babel/types': 7.26.7
convert-source-map: 2.0.0
debug: 4.4.0
gensync: 1.0.0-beta.2
@@ -8455,7 +8461,7 @@ snapshots:
'@babel/generator@7.26.3':
dependencies:
'@babel/parser': 7.26.3
'@babel/types': 7.26.3
'@babel/types': 7.26.7
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
jsesc: 3.0.2
@@ -8466,16 +8472,16 @@ snapshots:
'@babel/types': 7.26.7
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
jsesc: 3.0.2
jsesc: 3.1.0
'@babel/helper-annotate-as-pure@7.25.9':
dependencies:
'@babel/types': 7.26.3
'@babel/types': 7.26.7
'@babel/helper-builder-binary-assignment-operator-visitor@7.25.9':
dependencies:
'@babel/traverse': 7.26.4
'@babel/types': 7.26.3
'@babel/traverse': 7.26.7
'@babel/types': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8503,7 +8509,7 @@ snapshots:
'@babel/helper-optimise-call-expression': 7.25.9
'@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)
'@babel/helper-skip-transparent-expression-wrappers': 7.25.9
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
semver: 6.3.1
transitivePeerDependencies:
- supports-color
@@ -8518,7 +8524,7 @@ snapshots:
'@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-compilation-targets': 7.25.9
'@babel/helper-compilation-targets': 7.26.5
'@babel/helper-plugin-utils': 7.25.9
debug: 4.4.0
lodash.debounce: 4.0.8
@@ -8544,8 +8550,8 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.25.9':
dependencies:
'@babel/traverse': 7.26.4
'@babel/types': 7.26.3
'@babel/traverse': 7.26.7
'@babel/types': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8561,7 +8567,7 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-module-imports': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8570,13 +8576,13 @@ snapshots:
'@babel/core': 7.26.7
'@babel/helper-module-imports': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
transitivePeerDependencies:
- supports-color
'@babel/helper-optimise-call-expression@7.25.9':
dependencies:
'@babel/types': 7.26.3
'@babel/types': 7.26.7
'@babel/helper-plugin-utils@7.25.9': {}
@@ -8585,7 +8591,7 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-annotate-as-pure': 7.25.9
'@babel/helper-wrap-function': 7.25.9
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8594,21 +8600,21 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-member-expression-to-functions': 7.25.9
'@babel/helper-optimise-call-expression': 7.25.9
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
transitivePeerDependencies:
- supports-color
'@babel/helper-simple-access@7.25.9':
dependencies:
'@babel/traverse': 7.26.4
'@babel/types': 7.26.3
'@babel/traverse': 7.26.7
'@babel/types': 7.26.7
transitivePeerDependencies:
- supports-color
'@babel/helper-skip-transparent-expression-wrappers@7.25.9':
dependencies:
'@babel/traverse': 7.26.4
'@babel/types': 7.26.3
'@babel/traverse': 7.26.7
'@babel/types': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8626,15 +8632,15 @@ snapshots:
'@babel/helper-wrap-function@7.25.9':
dependencies:
'@babel/template': 7.25.9
'@babel/traverse': 7.26.4
'@babel/types': 7.26.3
'@babel/traverse': 7.26.7
'@babel/types': 7.26.7
transitivePeerDependencies:
- supports-color
'@babel/helpers@7.26.0':
dependencies:
'@babel/template': 7.25.9
'@babel/types': 7.26.3
'@babel/types': 7.26.7
'@babel/helpers@7.26.7':
dependencies:
@@ -8643,7 +8649,7 @@ snapshots:
'@babel/parser@7.26.3':
dependencies:
'@babel/types': 7.26.3
'@babel/types': 7.26.7
'@babel/parser@7.26.7':
dependencies:
@@ -8653,7 +8659,7 @@ snapshots:
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8680,7 +8686,7 @@ snapshots:
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8732,7 +8738,7 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9
'@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0)
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8775,10 +8781,10 @@ snapshots:
dependencies:
'@babel/core': 7.26.0
'@babel/helper-annotate-as-pure': 7.25.9
'@babel/helper-compilation-targets': 7.25.9
'@babel/helper-compilation-targets': 7.26.5
'@babel/helper-plugin-utils': 7.25.9
'@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -8840,9 +8846,9 @@ snapshots:
'@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-compilation-targets': 7.25.9
'@babel/helper-compilation-targets': 7.26.5
'@babel/helper-plugin-utils': 7.25.9
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8889,7 +8895,7 @@ snapshots:
'@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)
'@babel/helper-plugin-utils': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/traverse': 7.26.4
'@babel/traverse': 7.26.7
transitivePeerDependencies:
- supports-color
@@ -8925,7 +8931,7 @@ snapshots:
'@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-compilation-targets': 7.25.9
'@babel/helper-compilation-targets': 7.26.5
'@babel/helper-plugin-utils': 7.25.9
'@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0)
@@ -9134,7 +9140,7 @@ snapshots:
dependencies:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9
'@babel/types': 7.26.3
'@babel/types': 7.26.7
esutils: 2.0.3
'@babel/runtime@7.26.0':
@@ -9144,7 +9150,7 @@ snapshots:
'@babel/template@7.25.9':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/parser': 7.26.3
'@babel/parser': 7.26.7
'@babel/types': 7.26.7
'@babel/traverse@7.23.2':
@@ -9169,7 +9175,7 @@ snapshots:
'@babel/generator': 7.26.3
'@babel/parser': 7.26.3
'@babel/template': 7.25.9
'@babel/types': 7.26.3
'@babel/types': 7.26.7
debug: 4.4.0
globals: 11.12.0
transitivePeerDependencies:
@@ -9181,7 +9187,7 @@ snapshots:
'@babel/generator': 7.26.3
'@babel/parser': 7.26.3
'@babel/template': 7.25.9
'@babel/types': 7.26.3
'@babel/types': 7.26.7
debug: 4.4.0
globals: 11.12.0
transitivePeerDependencies:
@@ -9222,10 +9228,10 @@ snapshots:
'@bufbuild/protobuf@2.2.3': {}
'@commitlint/cli@19.6.1(@types/node@22.13.0)(typescript@5.7.3)':
'@commitlint/cli@19.7.1(@types/node@22.13.0)(typescript@5.7.3)':
dependencies:
'@commitlint/format': 19.5.0
'@commitlint/lint': 19.6.0
'@commitlint/lint': 19.7.1
'@commitlint/load': 19.6.1(@types/node@22.13.0)(typescript@5.7.3)
'@commitlint/read': 19.5.0
'@commitlint/types': 19.5.0
@@ -9235,7 +9241,7 @@ snapshots:
- '@types/node'
- typescript
'@commitlint/config-conventional@19.6.0':
'@commitlint/config-conventional@19.7.1':
dependencies:
'@commitlint/types': 19.5.0
conventional-changelog-conventionalcommits: 7.0.2
@@ -9261,14 +9267,14 @@ snapshots:
'@commitlint/types': 19.5.0
chalk: 5.4.1
'@commitlint/is-ignored@19.6.0':
'@commitlint/is-ignored@19.7.1':
dependencies:
'@commitlint/types': 19.5.0
semver: 7.6.3
'@commitlint/lint@19.6.0':
'@commitlint/lint@19.7.1':
dependencies:
'@commitlint/is-ignored': 19.6.0
'@commitlint/is-ignored': 19.7.1
'@commitlint/parse': 19.5.0
'@commitlint/rules': 19.6.0
'@commitlint/types': 19.5.0
@@ -10556,54 +10562,54 @@ snapshots:
- supports-color
- typescript
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.0)':
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.0
'@babel/core': 7.26.7
'@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.26.0)':
'@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.0
'@babel/core': 7.26.7
'@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.26.0)':
'@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.0
'@babel/core': 7.26.7
'@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.26.0)':
'@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.0
'@babel/core': 7.26.7
'@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.26.0)':
'@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.0
'@babel/core': 7.26.7
'@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.26.0)':
'@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.0
'@babel/core': 7.26.7
'@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.26.0)':
'@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.0
'@babel/core': 7.26.7
'@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.26.0)':
'@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.0
'@babel/core': 7.26.7
'@svgr/babel-preset@8.1.0(@babel/core@7.26.0)':
'@svgr/babel-preset@8.1.0(@babel/core@7.26.7)':
dependencies:
'@babel/core': 7.26.0
'@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.26.0)
'@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.26.0)
'@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.26.0)
'@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.26.0)
'@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.26.0)
'@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.26.0)
'@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.26.0)
'@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.26.0)
'@babel/core': 7.26.7
'@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.26.7)
'@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.26.7)
'@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.26.7)
'@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.26.7)
'@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.26.7)
'@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.26.7)
'@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.26.7)
'@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.26.7)
'@svgr/core@8.1.0(typescript@5.7.3)':
dependencies:
'@babel/core': 7.26.0
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.0)
'@babel/core': 7.26.7
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.7)
camelcase: 6.3.0
cosmiconfig: 8.3.6(typescript@5.7.3)
snake-case: 3.0.4
@@ -10613,13 +10619,13 @@ snapshots:
'@svgr/hast-util-to-babel-ast@8.0.0':
dependencies:
'@babel/types': 7.26.3
'@babel/types': 7.26.7
entities: 4.5.0
'@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.7.3))':
dependencies:
'@babel/core': 7.26.0
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.0)
'@babel/core': 7.26.7
'@svgr/babel-preset': 8.1.0(@babel/core@7.26.7)
'@svgr/core': 8.1.0(typescript@5.7.3)
'@svgr/hast-util-to-babel-ast': 8.0.0
svg-parser: 2.0.4
@@ -10810,7 +10816,7 @@ snapshots:
optionalDependencies:
'@tanstack/react-router': 1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@tanstack/router-plugin@1.99.0(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.0.11(@types/node@22.13.0)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.1)(sass-embedded@1.83.4)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.7.0))':
'@tanstack/router-plugin@1.99.3(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.0.11(@types/node@22.13.0)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.1)(sass-embedded@1.83.4)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.2)(yaml@2.7.0))':
dependencies:
'@babel/core': 7.26.7
'@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.7)
@@ -10819,7 +10825,7 @@ snapshots:
'@babel/traverse': 7.26.7
'@babel/types': 7.26.7
'@tanstack/router-generator': 1.99.0(@tanstack/react-router@1.99.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
'@tanstack/router-utils': 1.99.0
'@tanstack/router-utils': 1.99.3
'@tanstack/virtual-file-routes': 1.99.0
'@types/babel__core': 7.20.5
'@types/babel__template': 7.4.4
@@ -10834,7 +10840,7 @@ snapshots:
- '@tanstack/react-router'
- supports-color
'@tanstack/router-utils@1.99.0':
'@tanstack/router-utils@1.99.3':
dependencies:
'@babel/generator': 7.26.5
'@babel/parser': 7.26.7
@@ -10960,7 +10966,7 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.26.3
'@babel/parser': 7.26.7
'@babel/types': 7.26.7
'@types/babel__generator': 7.6.8
'@types/babel__template': 7.4.4
@@ -10972,7 +10978,7 @@ snapshots:
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.26.3
'@babel/parser': 7.26.7
'@babel/types': 7.26.7
'@types/babel__traverse@7.20.6':
@@ -11374,7 +11380,7 @@ snapshots:
'@vue/compiler-core@3.5.13':
dependencies:
'@babel/parser': 7.26.3
'@babel/parser': 7.26.7
'@vue/shared': 3.5.13
entities: 4.5.0
estree-walker: 2.0.2
@@ -11666,7 +11672,7 @@ snapshots:
babel-dead-code-elimination@1.0.8:
dependencies:
'@babel/core': 7.26.7
'@babel/parser': 7.26.3
'@babel/parser': 7.26.7
'@babel/traverse': 7.26.7
'@babel/types': 7.26.7
transitivePeerDependencies:
@@ -13230,7 +13236,7 @@ snapshots:
fraction.js@4.3.7: {}
framer-motion@12.0.6(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
framer-motion@12.0.11(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
motion-dom: 12.0.0
motion-utils: 12.0.0
@@ -0,0 +1,332 @@
From 78997e9a5e4d8a4df561e083a92c91ae23010e07 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 1 Oct 2024 01:17:18 +0100
Subject: [PATCH] net: phy: mxl-gpy: add basic LED support
Add basic support for LEDs connected to MaxLinear GPY2xx and GPY115 PHYs.
The PHYs allow up to 4 LEDs to be connected.
Implement controlling LEDs in software as well as netdev trigger offloading
and LED polarity setup.
The hardware claims to support 16 PWM brightness levels but there is no
documentation on how to use that feature, hence this is not supported.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Link: https://patch.msgid.link/b6ec9050339f8244ff898898a1cecc33b13a48fc.1727741563.git.daniel@makrotopia.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/phy/mxl-gpy.c | 218 ++++++++++++++++++++++++++++++++++++++
1 file changed, 218 insertions(+)
--- a/drivers/net/phy/mxl-gpy.c
+++ b/drivers/net/phy/mxl-gpy.c
@@ -38,6 +38,7 @@
#define PHY_MIISTAT 0x18 /* MII state */
#define PHY_IMASK 0x19 /* interrupt mask */
#define PHY_ISTAT 0x1A /* interrupt status */
+#define PHY_LED 0x1B /* LEDs */
#define PHY_FWV 0x1E /* firmware version */
#define PHY_MIISTAT_SPD_MASK GENMASK(2, 0)
@@ -61,6 +62,11 @@
PHY_IMASK_ADSC | \
PHY_IMASK_ANC)
+#define GPY_MAX_LEDS 4
+#define PHY_LED_POLARITY(idx) BIT(12 + (idx))
+#define PHY_LED_HWCONTROL(idx) BIT(8 + (idx))
+#define PHY_LED_ON(idx) BIT(idx)
+
#define PHY_FWV_REL_MASK BIT(15)
#define PHY_FWV_MAJOR_MASK GENMASK(11, 8)
#define PHY_FWV_MINOR_MASK GENMASK(7, 0)
@@ -72,6 +78,23 @@
#define PHY_MDI_MDI_X_CD 0x1
#define PHY_MDI_MDI_X_CROSS 0x0
+/* LED */
+#define VSPEC1_LED(idx) (1 + (idx))
+#define VSPEC1_LED_BLINKS GENMASK(15, 12)
+#define VSPEC1_LED_PULSE GENMASK(11, 8)
+#define VSPEC1_LED_CON GENMASK(7, 4)
+#define VSPEC1_LED_BLINKF GENMASK(3, 0)
+
+#define VSPEC1_LED_LINK10 BIT(0)
+#define VSPEC1_LED_LINK100 BIT(1)
+#define VSPEC1_LED_LINK1000 BIT(2)
+#define VSPEC1_LED_LINK2500 BIT(3)
+
+#define VSPEC1_LED_TXACT BIT(0)
+#define VSPEC1_LED_RXACT BIT(1)
+#define VSPEC1_LED_COL BIT(2)
+#define VSPEC1_LED_NO_CON BIT(3)
+
/* SGMII */
#define VSPEC1_SGMII_CTRL 0x08
#define VSPEC1_SGMII_CTRL_ANEN BIT(12) /* Aneg enable */
@@ -827,6 +850,156 @@ static int gpy115_loopback(struct phy_de
return genphy_soft_reset(phydev);
}
+static int gpy_led_brightness_set(struct phy_device *phydev,
+ u8 index, enum led_brightness value)
+{
+ int ret;
+
+ if (index >= GPY_MAX_LEDS)
+ return -EINVAL;
+
+ /* clear HWCONTROL and set manual LED state */
+ ret = phy_modify(phydev, PHY_LED,
+ ((value == LED_OFF) ? PHY_LED_HWCONTROL(index) : 0) |
+ PHY_LED_ON(index),
+ (value == LED_OFF) ? 0 : PHY_LED_ON(index));
+ if (ret)
+ return ret;
+
+ /* ToDo: set PWM brightness */
+
+ /* clear HW LED setup */
+ if (value == LED_OFF)
+ return phy_write_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_LED(index), 0);
+ else
+ return 0;
+}
+
+static const unsigned long supported_triggers = (BIT(TRIGGER_NETDEV_LINK) |
+ BIT(TRIGGER_NETDEV_LINK_100) |
+ BIT(TRIGGER_NETDEV_LINK_1000) |
+ BIT(TRIGGER_NETDEV_LINK_2500) |
+ BIT(TRIGGER_NETDEV_RX) |
+ BIT(TRIGGER_NETDEV_TX));
+
+static int gpy_led_hw_is_supported(struct phy_device *phydev, u8 index,
+ unsigned long rules)
+{
+ if (index >= GPY_MAX_LEDS)
+ return -EINVAL;
+
+ /* All combinations of the supported triggers are allowed */
+ if (rules & ~supported_triggers)
+ return -EOPNOTSUPP;
+
+ return 0;
+}
+
+static int gpy_led_hw_control_get(struct phy_device *phydev, u8 index,
+ unsigned long *rules)
+{
+ int val;
+
+ if (index >= GPY_MAX_LEDS)
+ return -EINVAL;
+
+ val = phy_read_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_LED(index));
+ if (val < 0)
+ return val;
+
+ if (FIELD_GET(VSPEC1_LED_CON, val) & VSPEC1_LED_LINK10)
+ *rules |= BIT(TRIGGER_NETDEV_LINK_10);
+
+ if (FIELD_GET(VSPEC1_LED_CON, val) & VSPEC1_LED_LINK100)
+ *rules |= BIT(TRIGGER_NETDEV_LINK_100);
+
+ if (FIELD_GET(VSPEC1_LED_CON, val) & VSPEC1_LED_LINK1000)
+ *rules |= BIT(TRIGGER_NETDEV_LINK_1000);
+
+ if (FIELD_GET(VSPEC1_LED_CON, val) & VSPEC1_LED_LINK2500)
+ *rules |= BIT(TRIGGER_NETDEV_LINK_2500);
+
+ if (FIELD_GET(VSPEC1_LED_CON, val) == (VSPEC1_LED_LINK10 |
+ VSPEC1_LED_LINK100 |
+ VSPEC1_LED_LINK1000 |
+ VSPEC1_LED_LINK2500))
+ *rules |= BIT(TRIGGER_NETDEV_LINK);
+
+ if (FIELD_GET(VSPEC1_LED_PULSE, val) & VSPEC1_LED_TXACT)
+ *rules |= BIT(TRIGGER_NETDEV_TX);
+
+ if (FIELD_GET(VSPEC1_LED_PULSE, val) & VSPEC1_LED_RXACT)
+ *rules |= BIT(TRIGGER_NETDEV_RX);
+
+ return 0;
+}
+
+static int gpy_led_hw_control_set(struct phy_device *phydev, u8 index,
+ unsigned long rules)
+{
+ u16 val = 0;
+ int ret;
+
+ if (index >= GPY_MAX_LEDS)
+ return -EINVAL;
+
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
+ rules & BIT(TRIGGER_NETDEV_LINK_10))
+ val |= FIELD_PREP(VSPEC1_LED_CON, VSPEC1_LED_LINK10);
+
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
+ rules & BIT(TRIGGER_NETDEV_LINK_100))
+ val |= FIELD_PREP(VSPEC1_LED_CON, VSPEC1_LED_LINK100);
+
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
+ rules & BIT(TRIGGER_NETDEV_LINK_1000))
+ val |= FIELD_PREP(VSPEC1_LED_CON, VSPEC1_LED_LINK1000);
+
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
+ rules & BIT(TRIGGER_NETDEV_LINK_2500))
+ val |= FIELD_PREP(VSPEC1_LED_CON, VSPEC1_LED_LINK2500);
+
+ if (rules & BIT(TRIGGER_NETDEV_TX))
+ val |= FIELD_PREP(VSPEC1_LED_PULSE, VSPEC1_LED_TXACT);
+
+ if (rules & BIT(TRIGGER_NETDEV_RX))
+ val |= FIELD_PREP(VSPEC1_LED_PULSE, VSPEC1_LED_RXACT);
+
+ /* allow RX/TX pulse without link indication */
+ if ((rules & BIT(TRIGGER_NETDEV_TX) || rules & BIT(TRIGGER_NETDEV_RX)) &&
+ !(val & VSPEC1_LED_CON))
+ val |= FIELD_PREP(VSPEC1_LED_PULSE, VSPEC1_LED_NO_CON) | VSPEC1_LED_CON;
+
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_LED(index), val);
+ if (ret)
+ return ret;
+
+ return phy_set_bits(phydev, PHY_LED, PHY_LED_HWCONTROL(index));
+}
+
+static int gpy_led_polarity_set(struct phy_device *phydev, int index,
+ unsigned long modes)
+{
+ bool active_low = false;
+ u32 mode;
+
+ if (index >= GPY_MAX_LEDS)
+ return -EINVAL;
+
+ for_each_set_bit(mode, &modes, __PHY_LED_MODES_NUM) {
+ switch (mode) {
+ case PHY_LED_ACTIVE_LOW:
+ active_low = true;
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+
+ return phy_modify(phydev, PHY_LED, PHY_LED_POLARITY(index),
+ active_low ? 0 : PHY_LED_POLARITY(index));
+}
+
static struct phy_driver gpy_drivers[] = {
{
PHY_ID_MATCH_MODEL(PHY_ID_GPY2xx),
@@ -844,6 +1017,11 @@ static struct phy_driver gpy_drivers[] =
.set_wol = gpy_set_wol,
.get_wol = gpy_get_wol,
.set_loopback = gpy_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
},
{
.phy_id = PHY_ID_GPY115B,
@@ -862,6 +1040,11 @@ static struct phy_driver gpy_drivers[] =
.set_wol = gpy_set_wol,
.get_wol = gpy_get_wol,
.set_loopback = gpy115_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
},
{
PHY_ID_MATCH_MODEL(PHY_ID_GPY115C),
@@ -879,6 +1062,11 @@ static struct phy_driver gpy_drivers[] =
.set_wol = gpy_set_wol,
.get_wol = gpy_get_wol,
.set_loopback = gpy115_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
},
{
.phy_id = PHY_ID_GPY211B,
@@ -897,6 +1085,11 @@ static struct phy_driver gpy_drivers[] =
.set_wol = gpy_set_wol,
.get_wol = gpy_get_wol,
.set_loopback = gpy_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
},
{
PHY_ID_MATCH_MODEL(PHY_ID_GPY211C),
@@ -914,6 +1107,11 @@ static struct phy_driver gpy_drivers[] =
.set_wol = gpy_set_wol,
.get_wol = gpy_get_wol,
.set_loopback = gpy_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
},
{
.phy_id = PHY_ID_GPY212B,
@@ -932,6 +1130,11 @@ static struct phy_driver gpy_drivers[] =
.set_wol = gpy_set_wol,
.get_wol = gpy_get_wol,
.set_loopback = gpy_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
},
{
PHY_ID_MATCH_MODEL(PHY_ID_GPY212C),
@@ -949,6 +1152,11 @@ static struct phy_driver gpy_drivers[] =
.set_wol = gpy_set_wol,
.get_wol = gpy_get_wol,
.set_loopback = gpy_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
},
{
.phy_id = PHY_ID_GPY215B,
@@ -967,6 +1175,11 @@ static struct phy_driver gpy_drivers[] =
.set_wol = gpy_set_wol,
.get_wol = gpy_get_wol,
.set_loopback = gpy_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
},
{
PHY_ID_MATCH_MODEL(PHY_ID_GPY215C),
@@ -984,6 +1197,11 @@ static struct phy_driver gpy_drivers[] =
.set_wol = gpy_set_wol,
.get_wol = gpy_get_wol,
.set_loopback = gpy_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
},
{
PHY_ID_MATCH_MODEL(PHY_ID_GPY241B),
@@ -0,0 +1,28 @@
From f95b4725e796b12e5f347a0d161e1d3843142aa8 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Fri, 4 Oct 2024 16:56:35 +0100
Subject: [PATCH] net: phy: mxl-gpy: add missing support for
TRIGGER_NETDEV_LINK_10
The PHY also support 10MBit/s links as well as the corresponding link
indication trigger to be offloaded. Add TRIGGER_NETDEV_LINK_10 to the
supported triggers.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Link: https://patch.msgid.link/cc5da0a989af8b0d49d823656d88053c4de2ab98.1728057367.git.daniel@makrotopia.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/phy/mxl-gpy.c | 1 +
1 file changed, 1 insertion(+)
--- a/drivers/net/phy/mxl-gpy.c
+++ b/drivers/net/phy/mxl-gpy.c
@@ -876,6 +876,7 @@ static int gpy_led_brightness_set(struct
}
static const unsigned long supported_triggers = (BIT(TRIGGER_NETDEV_LINK) |
+ BIT(TRIGGER_NETDEV_LINK_10) |
BIT(TRIGGER_NETDEV_LINK_100) |
BIT(TRIGGER_NETDEV_LINK_1000) |
BIT(TRIGGER_NETDEV_LINK_2500) |
@@ -0,0 +1,58 @@
From eb89c79c1b8f17fc1611540768678e60df89ac42 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Thu, 10 Oct 2024 13:55:17 +0100
Subject: [PATCH 3/4] net: phy: mxl-gpy: correctly describe LED polarity
According the datasheet covering the LED (0x1b) register:
0B Active High LEDx pin driven high when activated
1B Active Low LEDx pin driven low when activated
Make use of the now available 'active-high' property and correctly
reflect the polarity setting which was previously inverted.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Link: https://patch.msgid.link/180ccafa837f09908b852a8a874a3808c5ecd2d0.1728558223.git.daniel@makrotopia.org
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
---
drivers/net/phy/mxl-gpy.c | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
--- a/drivers/net/phy/mxl-gpy.c
+++ b/drivers/net/phy/mxl-gpy.c
@@ -981,7 +981,7 @@ static int gpy_led_hw_control_set(struct
static int gpy_led_polarity_set(struct phy_device *phydev, int index,
unsigned long modes)
{
- bool active_low = false;
+ bool force_active_low = false, force_active_high = false;
u32 mode;
if (index >= GPY_MAX_LEDS)
@@ -990,15 +990,23 @@ static int gpy_led_polarity_set(struct p
for_each_set_bit(mode, &modes, __PHY_LED_MODES_NUM) {
switch (mode) {
case PHY_LED_ACTIVE_LOW:
- active_low = true;
+ force_active_low = true;
+ break;
+ case PHY_LED_ACTIVE_HIGH:
+ force_active_high = true;
break;
default:
return -EINVAL;
}
}
- return phy_modify(phydev, PHY_LED, PHY_LED_POLARITY(index),
- active_low ? 0 : PHY_LED_POLARITY(index));
+ if (force_active_low)
+ return phy_set_bits(phydev, PHY_LED, PHY_LED_POLARITY(index));
+
+ if (force_active_high)
+ return phy_clear_bits(phydev, PHY_LED, PHY_LED_POLARITY(index));
+
+ unreachable();
}
static struct phy_driver gpy_drivers[] = {
@@ -0,0 +1,379 @@
From 1758af47b98c17da464cb45f476875150955dd48 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Thu, 10 Oct 2024 13:55:29 +0100
Subject: [PATCH 4/4] net: phy: intel-xway: add support for PHY LEDs
The intel-xway PHY driver predates the PHY LED framework and currently
initializes all LED pins to equal default values.
Add PHY LED functions to the drivers and don't set default values if
LEDs are defined in device tree.
According the datasheets 3 LEDs are supported on all Intel XWAY PHYs.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Link: https://patch.msgid.link/81f4717ab9acf38f3239727a4540ae96fd01109b.1728558223.git.daniel@makrotopia.org
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
---
drivers/net/phy/intel-xway.c | 253 +++++++++++++++++++++++++++++++++--
1 file changed, 244 insertions(+), 9 deletions(-)
--- a/drivers/net/phy/intel-xway.c
+++ b/drivers/net/phy/intel-xway.c
@@ -151,6 +151,13 @@
#define XWAY_MMD_LED3H 0x01E8
#define XWAY_MMD_LED3L 0x01E9
+#define XWAY_GPHY_MAX_LEDS 3
+#define XWAY_GPHY_LED_INV(idx) BIT(12 + (idx))
+#define XWAY_GPHY_LED_EN(idx) BIT(8 + (idx))
+#define XWAY_GPHY_LED_DA(idx) BIT(idx)
+#define XWAY_MMD_LEDxH(idx) (XWAY_MMD_LED0H + 2 * (idx))
+#define XWAY_MMD_LEDxL(idx) (XWAY_MMD_LED0L + 2 * (idx))
+
#define PHY_ID_PHY11G_1_3 0x030260D1
#define PHY_ID_PHY22F_1_3 0x030260E1
#define PHY_ID_PHY11G_1_4 0xD565A400
@@ -229,20 +236,12 @@ static int xway_gphy_rgmii_init(struct p
XWAY_MDIO_MIICTRL_TXSKEW_MASK, val);
}
-static int xway_gphy_config_init(struct phy_device *phydev)
+static int xway_gphy_init_leds(struct phy_device *phydev)
{
int err;
u32 ledxh;
u32 ledxl;
- /* Mask all interrupts */
- err = phy_write(phydev, XWAY_MDIO_IMASK, 0);
- if (err)
- return err;
-
- /* Clear all pending interrupts */
- phy_read(phydev, XWAY_MDIO_ISTAT);
-
/* Ensure that integrated led function is enabled for all leds */
err = phy_write(phydev, XWAY_MDIO_LED,
XWAY_MDIO_LED_LED0_EN |
@@ -276,6 +275,26 @@ static int xway_gphy_config_init(struct
phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LED2H, ledxh);
phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LED2L, ledxl);
+ return 0;
+}
+
+static int xway_gphy_config_init(struct phy_device *phydev)
+{
+ struct device_node *np = phydev->mdio.dev.of_node;
+ int err;
+
+ /* Mask all interrupts */
+ err = phy_write(phydev, XWAY_MDIO_IMASK, 0);
+ if (err)
+ return err;
+
+ /* Use default LED configuration if 'leds' node isn't defined */
+ if (!of_get_child_by_name(np, "leds"))
+ xway_gphy_init_leds(phydev);
+
+ /* Clear all pending interrupts */
+ phy_read(phydev, XWAY_MDIO_ISTAT);
+
err = xway_gphy_rgmii_init(phydev);
if (err)
return err;
@@ -347,6 +366,172 @@ static irqreturn_t xway_gphy_handle_inte
return IRQ_HANDLED;
}
+static int xway_gphy_led_brightness_set(struct phy_device *phydev,
+ u8 index, enum led_brightness value)
+{
+ int ret;
+
+ if (index >= XWAY_GPHY_MAX_LEDS)
+ return -EINVAL;
+
+ /* clear EN and set manual LED state */
+ ret = phy_modify(phydev, XWAY_MDIO_LED,
+ ((value == LED_OFF) ? XWAY_GPHY_LED_EN(index) : 0) |
+ XWAY_GPHY_LED_DA(index),
+ (value == LED_OFF) ? 0 : XWAY_GPHY_LED_DA(index));
+ if (ret)
+ return ret;
+
+ /* clear HW LED setup */
+ if (value == LED_OFF) {
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxH(index), 0);
+ if (ret)
+ return ret;
+
+ return phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxL(index), 0);
+ } else {
+ return 0;
+ }
+}
+
+static const unsigned long supported_triggers = (BIT(TRIGGER_NETDEV_LINK) |
+ BIT(TRIGGER_NETDEV_LINK_10) |
+ BIT(TRIGGER_NETDEV_LINK_100) |
+ BIT(TRIGGER_NETDEV_LINK_1000) |
+ BIT(TRIGGER_NETDEV_RX) |
+ BIT(TRIGGER_NETDEV_TX));
+
+static int xway_gphy_led_hw_is_supported(struct phy_device *phydev, u8 index,
+ unsigned long rules)
+{
+ if (index >= XWAY_GPHY_MAX_LEDS)
+ return -EINVAL;
+
+ /* activity triggers are not possible without combination with a link
+ * trigger.
+ */
+ if (rules & (BIT(TRIGGER_NETDEV_RX) | BIT(TRIGGER_NETDEV_TX)) &&
+ !(rules & (BIT(TRIGGER_NETDEV_LINK) |
+ BIT(TRIGGER_NETDEV_LINK_10) |
+ BIT(TRIGGER_NETDEV_LINK_100) |
+ BIT(TRIGGER_NETDEV_LINK_1000))))
+ return -EOPNOTSUPP;
+
+ /* All other combinations of the supported triggers are allowed */
+ if (rules & ~supported_triggers)
+ return -EOPNOTSUPP;
+
+ return 0;
+}
+
+static int xway_gphy_led_hw_control_get(struct phy_device *phydev, u8 index,
+ unsigned long *rules)
+{
+ int lval, hval;
+
+ if (index >= XWAY_GPHY_MAX_LEDS)
+ return -EINVAL;
+
+ hval = phy_read_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxH(index));
+ if (hval < 0)
+ return hval;
+
+ lval = phy_read_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxL(index));
+ if (lval < 0)
+ return lval;
+
+ if (hval & XWAY_MMD_LEDxH_CON_LINK10)
+ *rules |= BIT(TRIGGER_NETDEV_LINK_10);
+
+ if (hval & XWAY_MMD_LEDxH_CON_LINK100)
+ *rules |= BIT(TRIGGER_NETDEV_LINK_100);
+
+ if (hval & XWAY_MMD_LEDxH_CON_LINK1000)
+ *rules |= BIT(TRIGGER_NETDEV_LINK_1000);
+
+ if ((hval & XWAY_MMD_LEDxH_CON_LINK10) &&
+ (hval & XWAY_MMD_LEDxH_CON_LINK100) &&
+ (hval & XWAY_MMD_LEDxH_CON_LINK1000))
+ *rules |= BIT(TRIGGER_NETDEV_LINK);
+
+ if (lval & XWAY_MMD_LEDxL_PULSE_TXACT)
+ *rules |= BIT(TRIGGER_NETDEV_TX);
+
+ if (lval & XWAY_MMD_LEDxL_PULSE_RXACT)
+ *rules |= BIT(TRIGGER_NETDEV_RX);
+
+ return 0;
+}
+
+static int xway_gphy_led_hw_control_set(struct phy_device *phydev, u8 index,
+ unsigned long rules)
+{
+ u16 hval = 0, lval = 0;
+ int ret;
+
+ if (index >= XWAY_GPHY_MAX_LEDS)
+ return -EINVAL;
+
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
+ rules & BIT(TRIGGER_NETDEV_LINK_10))
+ hval |= XWAY_MMD_LEDxH_CON_LINK10;
+
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
+ rules & BIT(TRIGGER_NETDEV_LINK_100))
+ hval |= XWAY_MMD_LEDxH_CON_LINK100;
+
+ if (rules & BIT(TRIGGER_NETDEV_LINK) ||
+ rules & BIT(TRIGGER_NETDEV_LINK_1000))
+ hval |= XWAY_MMD_LEDxH_CON_LINK1000;
+
+ if (rules & BIT(TRIGGER_NETDEV_TX))
+ lval |= XWAY_MMD_LEDxL_PULSE_TXACT;
+
+ if (rules & BIT(TRIGGER_NETDEV_RX))
+ lval |= XWAY_MMD_LEDxL_PULSE_RXACT;
+
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxH(index), hval);
+ if (ret)
+ return ret;
+
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND2, XWAY_MMD_LEDxL(index), lval);
+ if (ret)
+ return ret;
+
+ return phy_set_bits(phydev, XWAY_MDIO_LED, XWAY_GPHY_LED_EN(index));
+}
+
+static int xway_gphy_led_polarity_set(struct phy_device *phydev, int index,
+ unsigned long modes)
+{
+ bool force_active_low = false, force_active_high = false;
+ u32 mode;
+
+ if (index >= XWAY_GPHY_MAX_LEDS)
+ return -EINVAL;
+
+ for_each_set_bit(mode, &modes, __PHY_LED_MODES_NUM) {
+ switch (mode) {
+ case PHY_LED_ACTIVE_LOW:
+ force_active_low = true;
+ break;
+ case PHY_LED_ACTIVE_HIGH:
+ force_active_high = true;
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+
+ if (force_active_low)
+ return phy_set_bits(phydev, XWAY_MDIO_LED, XWAY_GPHY_LED_INV(index));
+
+ if (force_active_high)
+ return phy_clear_bits(phydev, XWAY_MDIO_LED, XWAY_GPHY_LED_INV(index));
+
+ unreachable();
+}
+
static struct phy_driver xway_gphy[] = {
{
.phy_id = PHY_ID_PHY11G_1_3,
@@ -359,6 +544,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
}, {
.phy_id = PHY_ID_PHY22F_1_3,
.phy_id_mask = 0xffffffff,
@@ -370,6 +560,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
}, {
.phy_id = PHY_ID_PHY11G_1_4,
.phy_id_mask = 0xffffffff,
@@ -381,6 +576,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
}, {
.phy_id = PHY_ID_PHY22F_1_4,
.phy_id_mask = 0xffffffff,
@@ -392,6 +592,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
}, {
.phy_id = PHY_ID_PHY11G_1_5,
.phy_id_mask = 0xffffffff,
@@ -402,6 +607,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
}, {
.phy_id = PHY_ID_PHY22F_1_5,
.phy_id_mask = 0xffffffff,
@@ -412,6 +622,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
}, {
.phy_id = PHY_ID_PHY11G_VR9_1_1,
.phy_id_mask = 0xffffffff,
@@ -422,6 +637,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
}, {
.phy_id = PHY_ID_PHY22F_VR9_1_1,
.phy_id_mask = 0xffffffff,
@@ -432,6 +652,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
}, {
.phy_id = PHY_ID_PHY11G_VR9_1_2,
.phy_id_mask = 0xffffffff,
@@ -442,6 +667,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
}, {
.phy_id = PHY_ID_PHY22F_VR9_1_2,
.phy_id_mask = 0xffffffff,
@@ -452,6 +682,11 @@ static struct phy_driver xway_gphy[] = {
.config_intr = xway_gphy_config_intr,
.suspend = genphy_suspend,
.resume = genphy_resume,
+ .led_brightness_set = xway_gphy_led_brightness_set,
+ .led_hw_is_supported = xway_gphy_led_hw_is_supported,
+ .led_hw_control_get = xway_gphy_led_hw_control_get,
+ .led_hw_control_set = xway_gphy_led_hw_control_set,
+ .led_polarity_set = xway_gphy_led_polarity_set,
},
};
module_phy_driver(xway_gphy);
@@ -31,45 +31,21 @@ Signed-off-by: David Bauer <mail@david-bauer.net>
#include <linux/phy.h>
#include <linux/polynomial.h>
#include <linux/property.h>
@@ -38,6 +39,7 @@
#define PHY_MIISTAT 0x18 /* MII state */
#define PHY_IMASK 0x19 /* interrupt mask */
#define PHY_ISTAT 0x1A /* interrupt status */
+#define PHY_LED 0x1B /* LED control */
#define PHY_FWV 0x1E /* firmware version */
#define PHY_MIISTAT_SPD_MASK GENMASK(2, 0)
@@ -61,10 +63,15 @@
PHY_IMASK_ADSC | \
PHY_IMASK_ANC)
+#define PHY_LED_NUM_LEDS 4
+
#define PHY_FWV_REL_MASK BIT(15)
#define PHY_FWV_MAJOR_MASK GENMASK(11, 8)
#define PHY_FWV_MINOR_MASK GENMASK(7, 0)
+/* LED */
+#define VSPEC1_LED(x) (0x1 + x)
+
#define PHY_PMA_MGBT_POLARITY 0x82
#define PHY_MDI_MDI_X_MASK GENMASK(1, 0)
#define PHY_MDI_MDI_X_NORMAL 0x3
@@ -270,10 +277,39 @@ out:
@@ -293,10 +294,39 @@ out:
return ret;
}
+static int gpy_led_write(struct phy_device *phydev)
+{
+ struct device_node *node = phydev->mdio.dev.of_node;
+ u32 led_regs[PHY_LED_NUM_LEDS];
+ u32 led_regs[GPY_MAX_LEDS];
+ int i, ret;
+ u16 val = 0xff00;
+
+ if (!IS_ENABLED(CONFIG_OF_MDIO))
+ return 0;
+
+ if (of_property_read_u32_array(node, "mxl,led-config", led_regs, PHY_LED_NUM_LEDS))
+ if (of_property_read_u32_array(node, "mxl,led-config", led_regs, GPY_MAX_LEDS))
+ return 0;
+
+ if (of_property_read_bool(node, "mxl,led-drive-vdd"))
@@ -79,7 +55,7 @@ Signed-off-by: David Bauer <mail@david-bauer.net>
+ phy_write(phydev, PHY_LED, val);
+
+ /* Write LED register values */
+ for (i = 0; i < PHY_LED_NUM_LEDS; i++) {
+ for (i = 0; i < GPY_MAX_LEDS; i++) {
+ ret = phy_write_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_LED(i), (u16)led_regs[i]);
+ if (ret < 0)
+ return ret;
@@ -14,7 +14,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
--- a/drivers/net/phy/mxl-gpy.c
+++ b/drivers/net/phy/mxl-gpy.c
@@ -385,8 +385,11 @@ static bool gpy_2500basex_chk(struct phy
@@ -402,8 +402,11 @@ static bool gpy_2500basex_chk(struct phy
phydev->speed = SPEED_2500;
phydev->interface = PHY_INTERFACE_MODE_2500BASEX;
@@ -28,7 +28,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
return true;
}
@@ -437,6 +440,14 @@ static int gpy_config_aneg(struct phy_de
@@ -454,6 +457,14 @@ static int gpy_config_aneg(struct phy_de
u32 adv;
int ret;
@@ -43,7 +43,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
if (phydev->autoneg == AUTONEG_DISABLE) {
/* Configure half duplex with genphy_setup_forced,
* because genphy_c45_pma_setup_forced does not support.
@@ -559,6 +570,8 @@ static int gpy_update_interface(struct p
@@ -576,6 +587,8 @@ static int gpy_update_interface(struct p
switch (phydev->speed) {
case SPEED_2500:
phydev->interface = PHY_INTERFACE_MODE_2500BASEX;
@@ -52,7 +52,7 @@ Signed-off-by: Daniel Golle <daniel@makrotopia.org>
ret = phy_modify_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_SGMII_CTRL,
VSPEC1_SGMII_CTRL_ANEN, 0);
if (ret < 0) {
@@ -572,7 +585,7 @@ static int gpy_update_interface(struct p
@@ -589,7 +602,7 @@ static int gpy_update_interface(struct p
case SPEED_100:
case SPEED_10:
phydev->interface = PHY_INTERFACE_MODE_SGMII;
-3
View File
@@ -21,7 +21,6 @@ import (
"github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/socks5"
"github.com/metacubex/mihomo/transport/vless"
@@ -513,8 +512,6 @@ func NewVless(option VlessOption) (*Vless, error) {
if option.Flow != vless.XRV {
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
}
log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV)
addons = &vless.Addons{
Flow: option.Flow,
}
+37
View File
@@ -0,0 +1,37 @@
package generater
import (
"encoding/base64"
"fmt"
"github.com/gofrs/uuid/v5"
)
func Main(args []string) {
if len(args) < 1 {
panic("Using: generate uuid/reality-keypair/wg-keypair")
}
switch args[0] {
case "uuid":
newUUID, err := uuid.NewV4()
if err != nil {
panic(err)
}
fmt.Println(newUUID.String())
case "reality-keypair":
privateKey, err := GeneratePrivateKey()
if err != nil {
panic(err)
}
publicKey := privateKey.PublicKey()
fmt.Println("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]))
fmt.Println("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]))
case "wg-keypair":
privateKey, err := GeneratePrivateKey()
if err != nil {
panic(err)
}
fmt.Println("PrivateKey: " + privateKey.String())
fmt.Println("PublicKey: " + privateKey.PublicKey().String())
}
}
+97
View File
@@ -0,0 +1,97 @@
// Copy from https://github.com/WireGuard/wgctrl-go/blob/a9ab2273dd1075ea74b88c76f8757f8b4003fcbf/wgtypes/types.go#L71-L155
package generater
import (
"crypto/rand"
"encoding/base64"
"fmt"
"golang.org/x/crypto/curve25519"
)
// KeyLen is the expected key length for a WireGuard key.
const KeyLen = 32 // wgh.KeyLen
// A Key is a public, private, or pre-shared secret key. The Key constructor
// functions in this package can be used to create Keys suitable for each of
// these applications.
type Key [KeyLen]byte
// GenerateKey generates a Key suitable for use as a pre-shared secret key from
// a cryptographically safe source.
//
// The output Key should not be used as a private key; use GeneratePrivateKey
// instead.
func GenerateKey() (Key, error) {
b := make([]byte, KeyLen)
if _, err := rand.Read(b); err != nil {
return Key{}, fmt.Errorf("wgtypes: failed to read random bytes: %v", err)
}
return NewKey(b)
}
// GeneratePrivateKey generates a Key suitable for use as a private key from a
// cryptographically safe source.
func GeneratePrivateKey() (Key, error) {
key, err := GenerateKey()
if err != nil {
return Key{}, err
}
// Modify random bytes using algorithm described at:
// https://cr.yp.to/ecdh.html.
key[0] &= 248
key[31] &= 127
key[31] |= 64
return key, nil
}
// NewKey creates a Key from an existing byte slice. The byte slice must be
// exactly 32 bytes in length.
func NewKey(b []byte) (Key, error) {
if len(b) != KeyLen {
return Key{}, fmt.Errorf("wgtypes: incorrect key size: %d", len(b))
}
var k Key
copy(k[:], b)
return k, nil
}
// ParseKey parses a Key from a base64-encoded string, as produced by the
// Key.String method.
func ParseKey(s string) (Key, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return Key{}, fmt.Errorf("wgtypes: failed to parse base64-encoded key: %v", err)
}
return NewKey(b)
}
// PublicKey computes a public key from the private key k.
//
// PublicKey should only be called when k is a private key.
func (k Key) PublicKey() Key {
var (
pub [KeyLen]byte
priv = [KeyLen]byte(k)
)
// ScalarBaseMult uses the correct base value per https://cr.yp.to/ecdh.html,
// so no need to specify it.
curve25519.ScalarBaseMult(&pub, &priv)
return Key(pub)
}
// String returns the base64-encoded string representation of a Key.
//
// ParseKey can be used to produce a new Key from this string.
func (k Key) String() string {
return base64.StdEncoding.EncodeToString(k[:])
}
+5
View File
@@ -25,6 +25,7 @@ const (
SOCKS5
SHADOWSOCKS
VMESS
VLESS
REDIR
TPROXY
TUNNEL
@@ -69,6 +70,8 @@ func (t Type) String() string {
return "ShadowSocks"
case VMESS:
return "Vmess"
case VLESS:
return "Vless"
case REDIR:
return "Redir"
case TPROXY:
@@ -103,6 +106,8 @@ func ParseType(t string) (*Type, error) {
res = SHADOWSOCKS
case "VMESS":
res = VMESS
case "VLESS":
res = VLESS
case "REDIR":
res = REDIR
case "TPROXY":
+24
View File
@@ -1176,6 +1176,30 @@ listeners:
network: [tcp, udp]
target: target.com
- name: vless-in-1
type: vless
port: 10817
listen: 0.0.0.0
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
users:
- username: 1
uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68
flow: xtls-rprx-vision
# ws-path: "/" # 如果不为空则开启 websocket 传输层
# 下面两项如果填写则开启 tls(需要同时填写)
# certificate: ./server.crt
# private-key: ./server.key
# 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写)
reality-config:
dest: test.com:443
private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成
short-id:
- 0123456789abcdef
server-names:
- test.com
### 注意,对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 的其中一项 ###
- name: tun-in-1
type: tun
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
+2 -1
View File
@@ -27,7 +27,7 @@ require (
github.com/metacubex/sing-shadowsocks v0.2.8
github.com/metacubex/sing-shadowsocks2 v0.2.2
github.com/metacubex/sing-tun v0.4.5
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
github.com/metacubex/utls v1.6.6
@@ -40,6 +40,7 @@ require (
github.com/sagernet/cors v1.2.1
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-mux v0.2.1
github.com/sagernet/sing-shadowtls v0.1.5
+4 -2
View File
@@ -122,8 +122,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhD
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3 h1:2kq6azIvsTjTnyw66xXDl5zMzIJqF7GTbvLpkroHssg=
github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
@@ -170,6 +170,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
+38
View File
@@ -0,0 +1,38 @@
package config
import (
"github.com/metacubex/mihomo/listener/sing"
"encoding/json"
)
type VlessUser struct {
Username string
UUID string
Flow string
}
type VlessServer struct {
Enable bool
Listen string
Users []VlessUser
WsPath string
Certificate string
PrivateKey string
RealityConfig RealityConfig
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
}
func (t VlessServer) String() string {
b, _ := json.Marshal(t)
return string(b)
}
type RealityConfig struct {
Dest string
PrivateKey string
ShortID []string
ServerNames []string
MaxTimeDifference int
Proxy string
}
+125
View File
@@ -0,0 +1,125 @@
package inbound
import (
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing_vless"
"github.com/metacubex/mihomo/log"
)
type VlessOption struct {
BaseOption
Users []VlessUser `inbound:"users"`
WsPath string `inbound:"ws-path,omitempty"`
Certificate string `inbound:"certificate,omitempty"`
PrivateKey string `inbound:"private-key,omitempty"`
RealityConfig RealityConfig `inbound:"reality-config,omitempty"`
MuxOption MuxOption `inbound:"mux-option,omitempty"`
}
type VlessUser struct {
Username string `inbound:"username,omitempty"`
UUID string `inbound:"uuid"`
Flow string `inbound:"flow,omitempty"`
}
type RealityConfig struct {
Dest string `inbound:"dest"`
PrivateKey string `inbound:"private-key"`
ShortID []string `inbound:"short-id"`
ServerNames []string `inbound:"server-names"`
MaxTimeDifference int `inbound:"max-time-difference,omitempty"`
Proxy string `inbound:"proxy,omitempty"`
}
func (c RealityConfig) Build() LC.RealityConfig {
return LC.RealityConfig{
Dest: c.Dest,
PrivateKey: c.PrivateKey,
ShortID: c.ShortID,
ServerNames: c.ServerNames,
MaxTimeDifference: c.MaxTimeDifference,
Proxy: c.Proxy,
}
}
func (o VlessOption) Equal(config C.InboundConfig) bool {
return optionToString(o) == optionToString(config)
}
type Vless struct {
*Base
config *VlessOption
l C.MultiAddrListener
vs LC.VlessServer
}
func NewVless(options *VlessOption) (*Vless, error) {
base, err := NewBase(&options.BaseOption)
if err != nil {
return nil, err
}
users := make([]LC.VlessUser, len(options.Users))
for i, v := range options.Users {
users[i] = LC.VlessUser{
Username: v.Username,
UUID: v.UUID,
Flow: v.Flow,
}
}
return &Vless{
Base: base,
config: options,
vs: LC.VlessServer{
Enable: true,
Listen: base.RawAddress(),
Users: users,
WsPath: options.WsPath,
Certificate: options.Certificate,
PrivateKey: options.PrivateKey,
RealityConfig: options.RealityConfig.Build(),
MuxOption: options.MuxOption.Build(),
},
}, nil
}
// Config implements constant.InboundListener
func (v *Vless) Config() C.InboundConfig {
return v.config
}
// Address implements constant.InboundListener
func (v *Vless) Address() string {
if v.l != nil {
for _, addr := range v.l.AddrList() {
return addr.String()
}
}
return ""
}
// Listen implements constant.InboundListener
func (v *Vless) Listen(tunnel C.Tunnel) error {
var err error
users := make([]LC.VlessUser, len(v.config.Users))
for i, v := range v.config.Users {
users[i] = LC.VlessUser{
Username: v.Username,
UUID: v.UUID,
Flow: v.Flow,
}
}
v.l, err = sing_vless.New(v.vs, tunnel, v.Additions()...)
if err != nil {
return err
}
log.Infoln("Vless[%s] proxy listening at: %s", v.Name(), v.Address())
return nil
}
// Close implements constant.InboundListener
func (v *Vless) Close() error {
return v.l.Close()
}
var _ C.InboundListener = (*Vless)(nil)
+7
View File
@@ -86,6 +86,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) {
return nil, err
}
listener, err = IN.NewVmess(vmessOption)
case "vless":
vlessOption := &IN.VlessOption{}
err = decoder.Decode(mapping, vlessOption)
if err != nil {
return nil, err
}
listener, err = IN.NewVless(vlessOption)
case "hysteria2":
hysteria2Option := &IN.Hysteria2Option{}
err = decoder.Decode(mapping, hysteria2Option)
+263
View File
@@ -0,0 +1,263 @@
package sing_vless
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net"
"net/http"
"reflect"
"strings"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter/inbound"
N "github.com/metacubex/mihomo/common/net"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/inner"
"github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/ntp"
mihomoVMess "github.com/metacubex/mihomo/transport/vmess"
"github.com/metacubex/sing-vmess/vless"
utls "github.com/metacubex/utls"
"github.com/sagernet/reality"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/metadata"
)
func init() {
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
tlsConn, loaded := common.Cast[*reality.Conn](conn)
if !loaded {
return
}
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), unsafe.Pointer(tlsConn)
})
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
tlsConn, loaded := common.Cast[*utls.UConn](conn)
if !loaded {
return
}
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn)
})
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
tlsConn, loaded := common.Cast[*tlsC.UConn](conn)
if !loaded {
return
}
return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn)
})
}
type Listener struct {
closed bool
config LC.VlessServer
listeners []net.Listener
service *vless.Service[string]
}
func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) {
if len(additions) == 0 {
additions = []inbound.Addition{
inbound.WithInName("DEFAULT-VLESS"),
inbound.WithSpecialRules(""),
}
}
h, err := sing.NewListenerHandler(sing.ListenerConfig{
Tunnel: tunnel,
Type: C.VLESS,
Additions: additions,
MuxOption: config.MuxOption,
})
if err != nil {
return nil, err
}
service := vless.NewService[string](log.SingLogger, h)
service.UpdateUsers(
common.Map(config.Users, func(it LC.VlessUser) string {
return it.Username
}),
common.Map(config.Users, func(it LC.VlessUser) string {
return it.UUID
}),
common.Map(config.Users, func(it LC.VlessUser) string {
return it.Flow
}))
sl = &Listener{false, config, nil, service}
tlsConfig := &tls.Config{}
var realityConfig *reality.Config
var httpMux *http.ServeMux
if config.Certificate != "" && config.PrivateKey != "" {
cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
if config.WsPath != "" {
httpMux = http.NewServeMux()
httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) {
conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
sl.HandleConn(conn, tunnel)
})
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1")
}
if config.RealityConfig.PrivateKey != "" {
if tlsConfig.Certificates != nil {
return nil, errors.New("certificate is unavailable in reality")
}
realityConfig = &reality.Config{}
realityConfig.SessionTicketsDisabled = true
realityConfig.Type = "tcp"
realityConfig.Dest = config.RealityConfig.Dest
realityConfig.Time = ntp.Now
realityConfig.ServerNames = make(map[string]bool)
for _, it := range config.RealityConfig.ServerNames {
realityConfig.ServerNames[it] = true
}
privateKey, err := base64.RawURLEncoding.DecodeString(config.RealityConfig.PrivateKey)
if err != nil {
return nil, fmt.Errorf("decode private key: %w", err)
}
if len(privateKey) != 32 {
return nil, errors.New("invalid private key")
}
realityConfig.PrivateKey = privateKey
realityConfig.MaxTimeDiff = time.Duration(config.RealityConfig.MaxTimeDifference) * time.Microsecond
realityConfig.ShortIds = make(map[[8]byte]bool)
for i, shortIDString := range config.RealityConfig.ShortID {
var shortID [8]byte
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
if err != nil {
return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err)
}
if decodedLen > 8 {
return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString)
}
realityConfig.ShortIds[shortID] = true
}
realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
return inner.HandleTcp(address, config.RealityConfig.Proxy)
}
}
for _, addr := range strings.Split(config.Listen, ",") {
addr := addr
//TCP
l, err := inbound.Listen("tcp", addr)
if err != nil {
return nil, err
}
if realityConfig != nil {
l = reality.NewListener(l, realityConfig)
// Due to low implementation quality, the reality server intercepted half close and caused memory leaks.
// We fixed it by calling Close() directly.
l = realityListenerWrapper{l}
} else if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig)
} else {
return nil, errors.New("disallow using Vless without both certificates/reality config")
}
sl.listeners = append(sl.listeners, l)
go func() {
if httpMux != nil {
_ = http.Serve(l, httpMux)
return
}
for {
c, err := l.Accept()
if err != nil {
if sl.closed {
break
}
continue
}
go sl.HandleConn(c, tunnel)
}
}()
}
return sl, nil
}
func (l *Listener) Close() error {
l.closed = true
var retErr error
for _, lis := range l.listeners {
err := lis.Close()
if err != nil {
retErr = err
}
}
return retErr
}
func (l *Listener) Config() string {
return l.config.String()
}
func (l *Listener) AddrList() (addrList []net.Addr) {
for _, lis := range l.listeners {
addrList = append(addrList, lis.Addr())
}
return
}
func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
ctx := sing.WithAdditions(context.TODO(), additions...)
err := l.service.NewConnection(ctx, conn, metadata.Metadata{
Protocol: "vless",
Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()),
})
if err != nil {
_ = conn.Close()
return
}
}
type realityConnWrapper struct {
*reality.Conn
}
func (c realityConnWrapper) Upstream() any {
return c.Conn
}
func (c realityConnWrapper) CloseWrite() error {
return c.Close()
}
type realityListenerWrapper struct {
net.Listener
}
func (l realityListenerWrapper) Accept() (net.Conn, error) {
c, err := l.Listener.Accept()
if err != nil {
return nil, err
}
return realityConnWrapper{c.(*reality.Conn)}, nil
}
+6
View File
@@ -14,6 +14,7 @@ import (
"strings"
"syscall"
"github.com/metacubex/mihomo/component/generater"
"github.com/metacubex/mihomo/component/geodata"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
@@ -71,6 +72,11 @@ func main() {
return
}
if len(os.Args) > 1 && os.Args[1] == "generate" {
generater.Main(os.Args[2:])
return
}
if version {
fmt.Printf("Mihomo Meta %s %s %s with %s %s\n",
C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime)
@@ -7,6 +7,9 @@ local appname = "passwall"
local fs = api.fs
local split = api.split
local local_version = api.get_app_version("singbox")
local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0")
local new_port
local function get_new_port()
@@ -729,6 +732,26 @@ function gen_config_server(node)
end
end
if version_ge_1_11_0 then
-- Migrate logics
-- https://sing-box.sagernet.org/migration/
for i = #config.outbounds, 1, -1 do
local value = config.outbounds[i]
if value.type == "block" then
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
table.remove(config.outbounds, i)
end
end
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
for i = #config.route.rules, 1, -1 do
local value = config.route.rules[i]
if value.outbound == "block" then
value.action = "reject"
value.outbound = nil
end
end
end
return config
end
@@ -1098,7 +1121,6 @@ function gen_config(var)
local rule = {
inbound = inboundTag,
outbound = outboundTag,
invert = false, --匹配反选
protocol = protocols
}
@@ -1487,6 +1509,90 @@ function gen_config(var)
end
end
end
if version_ge_1_11_0 then
-- Migrate logics
-- https://sing-box.sagernet.org/migration/
local endpoints = {}
for i = #config.outbounds, 1, -1 do
local value = config.outbounds[i]
if value.type == "wireguard" then
-- https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint
local endpoint = {
type = "wireguard",
tag = value.tag,
system = value.system_interface,
name = value.interface_name,
mtu = value.mtu,
address = value.local_address,
private_key = value.private_key,
peers = {
{
address = value.server,
port = value.server_port,
public_key = value.peer_public_key,
pre_shared_key = value.pre_shared_key,
allowed_ips = {"0.0.0.0/0"},
reserved = value.reserved
}
},
domain_strategy = value.domain_strategy,
detour = value.detour
}
endpoints[#endpoints + 1] = endpoint
table.remove(config.outbounds, i)
end
if value.type == "block" or value.type == "dns" then
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
table.remove(config.outbounds, i)
end
end
if #endpoints > 0 then
config.endpoints = endpoints
end
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
for i = #config.route.rules, 1, -1 do
local value = config.route.rules[i]
if value.outbound == "block" then
value.action = "reject"
value.outbound = nil
elseif value.outbound == "dns-out" then
value.action = "hijack-dns"
value.outbound = nil
else
value.action = "route"
end
end
-- https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions
for i = #config.inbounds, 1, -1 do
local value = config.inbounds[i]
if value.sniff == true then
table.insert(config.route.rules, 1, {
inbound = value.tag,
action = "sniff"
})
value.sniff = nil
value.sniff_override_destination = nil
end
if value.domain_strategy then
table.insert(config.route.rules, 1, {
inbound = value.tag,
action = "resolve",
strategy = value.domain_strategy,
--server = ""
})
value.domain_strategy = nil
end
end
if config.route.final == "block" then
config.route.final = nil
table.insert(config.route.rules, {
action = "reject"
})
end
end
return jsonc.stringify(config, 1)
end
end
@@ -2026,6 +2026,8 @@ start() {
get_config
export V2RAY_LOCATION_ASSET=$(config_t_get global_rules v2ray_location_asset "/usr/share/v2ray/")
export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET
export ENABLE_DEPRECATED_GEOSITE=true
export ENABLE_DEPRECATED_GEOIP=true
ulimit -n 65535
start_haproxy
start_socks
@@ -85,10 +85,10 @@ local function is_filter_keyword(value)
end
local nodeResult = {} -- update result
local debug = false
local isDebug = false
local log = function(...)
if debug == true then
if isDebug == true then
local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
print(result)
else
@@ -1728,7 +1728,9 @@ if arg[1] then
log('开始订阅...')
xpcall(execute, function(e)
log(e)
log(debug.traceback())
if type(debug) == "table" and type(debug.traceback) == "function" then
log(debug.traceback())
end
log('发生错误, 正在恢复服务')
end)
log('订阅完毕...')
@@ -8,6 +8,9 @@ local fs = api.fs
local CACHE_PATH = api.CACHE_PATH
local split = api.split
local local_version = api.get_app_version("singbox")
local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0")
local new_port
local function get_new_port()
@@ -726,6 +729,26 @@ function gen_config_server(node)
end
end
if version_ge_1_11_0 then
-- Migrate logics
-- https://sing-box.sagernet.org/migration/
for i = #config.outbounds, 1, -1 do
local value = config.outbounds[i]
if value.type == "block" then
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
table.remove(config.outbounds, i)
end
end
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
for i = #config.route.rules, 1, -1 do
local value = config.route.rules[i]
if value.outbound == "block" then
value.action = "reject"
value.outbound = nil
end
end
end
return config
end
@@ -1087,7 +1110,6 @@ function gen_config(var)
local rule = {
inbound = inboundTag,
outbound = outboundTag,
invert = false, --匹配反选
protocol = protocols
}
@@ -1480,6 +1502,90 @@ function gen_config(var)
end
end
end
if version_ge_1_11_0 then
-- Migrate logics
-- https://sing-box.sagernet.org/migration/
local endpoints = {}
for i = #config.outbounds, 1, -1 do
local value = config.outbounds[i]
if value.type == "wireguard" then
-- https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint
local endpoint = {
type = "wireguard",
tag = value.tag,
system = value.system_interface,
name = value.interface_name,
mtu = value.mtu,
address = value.local_address,
private_key = value.private_key,
peers = {
{
address = value.server,
port = value.server_port,
public_key = value.peer_public_key,
pre_shared_key = value.pre_shared_key,
allowed_ips = {"0.0.0.0/0"},
reserved = value.reserved
}
},
domain_strategy = value.domain_strategy,
detour = value.detour
}
endpoints[#endpoints + 1] = endpoint
table.remove(config.outbounds, i)
end
if value.type == "block" or value.type == "dns" then
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
table.remove(config.outbounds, i)
end
end
if #endpoints > 0 then
config.endpoints = endpoints
end
-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
for i = #config.route.rules, 1, -1 do
local value = config.route.rules[i]
if value.outbound == "block" then
value.action = "reject"
value.outbound = nil
elseif value.outbound == "dns-out" then
value.action = "hijack-dns"
value.outbound = nil
else
value.action = "route"
end
end
-- https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions
for i = #config.inbounds, 1, -1 do
local value = config.inbounds[i]
if value.sniff == true then
table.insert(config.route.rules, 1, {
inbound = value.tag,
action = "sniff"
})
value.sniff = nil
value.sniff_override_destination = nil
end
if value.domain_strategy then
table.insert(config.route.rules, 1, {
inbound = value.tag,
action = "resolve",
strategy = value.domain_strategy,
--server = ""
})
value.domain_strategy = nil
end
end
if config.route.final == "block" then
config.route.final = nil
table.insert(config.route.rules, {
action = "reject"
})
end
end
return jsonc.stringify(config, 1)
end
end
@@ -1563,183 +1669,8 @@ function gen_proto_config(var)
return jsonc.stringify(config, 1)
end
function gen_dns_config(var)
local dns_listen_port = var["-dns_listen_port"]
local dns_query_strategy = var["-dns_query_strategy"]
local dns_out_tag = var["-dns_out_tag"]
local direct_dns_udp_server = var["-direct_dns_udp_server"]
local direct_dns_udp_port = var["-direct_dns_udp_port"]
local direct_dns_tcp_server = var["-direct_dns_tcp_server"]
local direct_dns_tcp_port = var["-direct_dns_tcp_port"]
local direct_dns_doh_url = var["-direct_dns_doh_url"]
local direct_dns_doh_host = var["-direct_dns_doh_host"]
local direct_dns_doh_ip = var["-direct_dns_doh_ip"]
local direct_dns_doh_port = var["-direct_dns_doh_port"]
local remote_dns_udp_server = var["-remote_dns_udp_server"]
local remote_dns_udp_port = var["-remote_dns_udp_port"]
local remote_dns_tcp_server = var["-remote_dns_tcp_server"]
local remote_dns_tcp_port = var["-remote_dns_tcp_port"]
local remote_dns_doh_url = var["-remote_dns_doh_url"]
local remote_dns_doh_host = var["-remote_dns_doh_host"]
local remote_dns_doh_ip = var["-remote_dns_doh_ip"]
local remote_dns_doh_port = var["-remote_dns_doh_port"]
local remote_dns_detour = var["-remote_dns_detour"]
local remote_dns_client_ip = var["-remote_dns_client_ip"]
local remote_dns_outbound_socks_address = var["-remote_dns_outbound_socks_address"]
local remote_dns_outbound_socks_port = var["-remote_dns_outbound_socks_port"]
local dns_cache = var["-dns_cache"]
local log = var["-log"] or "0"
local loglevel = var["-loglevel"] or "warn"
local logfile = var["-logfile"] or "/dev/null"
local inbounds = {}
local outbounds = {}
local dns = nil
local route = nil
if dns_listen_port then
route = {
rules = {}
}
dns = {
servers = {},
rules = {},
disable_cache = (dns_cache and dns_cache == "0") and true or false,
disable_expire = false, --禁用 DNS 缓存过期。
independent_cache = false, --使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
reverse_mapping = true, --在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
}
if dns_out_tag == "remote" then
local out_tag = nil
if remote_dns_detour == "direct" then
out_tag = "direct-out"
table.insert(outbounds, 1, {
type = "direct",
tag = out_tag,
routing_mark = 255,
domain_strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6",
})
else
if remote_dns_outbound_socks_address and remote_dns_outbound_socks_port then
out_tag = "remote-out"
table.insert(outbounds, 1, {
type = "socks",
tag = out_tag,
server = remote_dns_outbound_socks_address,
server_port = tonumber(remote_dns_outbound_socks_port),
})
end
end
local server = {
tag = dns_out_tag,
address_strategy = "prefer_ipv4",
strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6",
detour = out_tag,
}
if remote_dns_udp_server then
local server_port = tonumber(remote_dns_udp_port) or 53
server.address = "udp://" .. remote_dns_udp_server .. ":" .. server_port
end
if remote_dns_tcp_server then
local server_port = tonumber(remote_dns_tcp_port) or 53
server.address = "tcp://" .. remote_dns_tcp_server .. ":" .. server_port
end
if remote_dns_doh_url then
server.address = remote_dns_doh_url
end
table.insert(dns.servers, server)
route.final = out_tag
elseif dns_out_tag == "direct" then
local out_tag = "direct-out"
table.insert(outbounds, 1, {
type = "direct",
tag = out_tag,
routing_mark = 255,
domain_strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6",
})
local server = {
tag = dns_out_tag,
address_strategy = "prefer_ipv6",
strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6",
detour = out_tag,
client_subnet = (remote_dns_client_ip and remote_dns_client_ip ~= "") and remote_dns_client_ip or nil,
}
if direct_dns_udp_server then
local server_port = tonumber(direct_dns_udp_port) or 53
server.address = "udp://" .. direct_dns_udp_server .. ":" .. server_port
end
if direct_dns_tcp_server then
local server_port = tonumber(direct_dns_tcp_port) or 53
server.address = "tcp://" .. direct_dns_tcp_server .. ":" .. server_port
end
if direct_dns_doh_url then
server.address = direct_dns_doh_url
end
table.insert(dns.servers, server)
route.final = out_tag
end
table.insert(inbounds, {
type = "direct",
tag = "dns-in",
listen = "127.0.0.1",
listen_port = tonumber(dns_listen_port),
sniff = true,
})
table.insert(outbounds, {
type = "dns",
tag = "dns-out",
})
table.insert(route.rules, 1, {
protocol = "dns",
inbound = {
"dns-in"
},
outbound = "dns-out"
})
end
if inbounds or outbounds then
local config = {
log = {
disabled = log == "0" and true or false,
level = loglevel,
timestamp = true,
output = logfile,
},
-- DNS
dns = dns,
-- 传入连接
inbounds = inbounds,
-- 传出连接
outbounds = outbounds,
-- 路由
route = route
}
return jsonc.stringify(config, 1)
end
end
_G.gen_config = gen_config
_G.gen_proto_config = gen_proto_config
_G.gen_dns_config = gen_dns_config
if arg[1] then
local func =_G[arg[1]]
@@ -715,7 +715,7 @@ function gen_config(var)
local blc_node_tag = "blc-" .. blc_node_id
local is_new_blc_node = true
for _, outbound in ipairs(outbounds) do
if outbound.tag:find("^" .. blc_node_tag) == 1 then
if string.sub(outbound.tag, 1, #blc_node_tag) == blc_node_tag then
is_new_blc_node = false
valid_nodes[#valid_nodes + 1] = outbound.tag
break
@@ -740,7 +740,7 @@ function gen_config(var)
if fallback_node_id then
local is_new_node = true
for _, outbound in ipairs(outbounds) do
if outbound.tag:find("^" .. fallback_node_id) == 1 then
if string.sub(outbound.tag, 1, #fallback_node_id) == fallback_node_id then
is_new_node = false
fallback_node_tag = outbound.tag
break
@@ -1238,6 +1238,8 @@ start() {
get_config
export V2RAY_LOCATION_ASSET=$(config_t_get global_rules v2ray_location_asset "/usr/share/v2ray/")
export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET
export ENABLE_DEPRECATED_GEOSITE=true
export ENABLE_DEPRECATED_GEOIP=true
ulimit -n 65535
start_haproxy
start_socks
@@ -9,20 +9,21 @@ probe_file="/tmp/etc/passwall2/haproxy/Probe_URL"
probeUrl="https://www.google.com/generate_204"
if [ -f "$probe_file" ]; then
firstLine=$(head -n 1 "$probe_file" | tr -d ' \t')
if [ -n "$firstLine" ]; then
probeUrl="$firstLine"
fi
[ -n "$firstLine" ] && probeUrl="$firstLine"
fi
status=$(/usr/bin/curl -I -o /dev/null -skL -x socks5h://${server_address}:${server_port} --connect-timeout 3 --retry 3 -w %{http_code} "${probeUrl}")
extra_params="-x socks5h://${server_address}:${server_port}"
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
extra_params="${extra_params} --retry-all-errors"
fi
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 -w "%{http_code}" "${probeUrl}")
case "$status" in
204|\
200)
status=200
200|204)
exit 0
;;
*)
exit 1
;;
esac
return_code=1
if [ "$status" = "200" ]; then
return_code=0
fi
exit ${return_code}
@@ -24,9 +24,10 @@ test_url() {
local timeout=2
[ -n "$3" ] && timeout=$3
local extra_params=$4
curl --help all | grep "\-\-retry-all-errors" > /dev/null
[ $? == 0 ] && extra_params="--retry-all-errors ${extra_params}"
status=$(/usr/bin/curl -I -o /dev/null -skL --user-agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url")
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
extra_params="--retry-all-errors ${extra_params}"
fi
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url")
case "$status" in
204)
status=200
@@ -106,43 +106,6 @@ function updateResVersion(El, version) {
return El;
}
function renderNATBehaviorTest(El) {
let resEl = E('div', { 'class': 'control-group' }, [
E('select', {
'id': '_status_nattest_l4proto',
'class': 'cbi-input-select',
'style': 'width: 5em'
}, [
E('option', { 'value': 'udp' }, 'UDP'),
E('option', { 'value': 'tcp' }, 'TCP')
]),
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': ui.createHandlerFn(this, function() {
const stun = this.formvalue(this.section.section);
const l4proto = document.getElementById('_status_nattest_l4proto').value;
const l4proto_idx = document.getElementById('_status_nattest_l4proto').selectedIndex;
return fs.exec_direct('/etc/fchomo/scripts/natcheck.sh', [stun, l4proto, getRandom(32768, 61000)]).then((stdout) => {
this.description = '<details><summary>' + _('Expand/Collapse result') + '</summary>' + stdout + '</details>';
return this.map.reset().then((res) => {
document.getElementById('_status_nattest_l4proto').selectedIndex = l4proto_idx;
});
});
})
}, [ _('Check') ])
]);
let newEl = E('div', { style: 'font-weight: bold; align-items: center; display: flex' }, []);
if (El) {
newEl.appendChild(E([El, resEl]));
} else
newEl.appendChild(resEl);
return newEl;
}
return view.extend({
load() {
return Promise.all([
@@ -247,7 +210,7 @@ return view.extend({
}
so = ss.option(form.Value, '_nattest', _('Check routerself NAT Behavior'));
so.default = hm.stunserver[0][0];
so.default = `udp://${hm.stunserver[0][0]}`;
hm.stunserver.forEach((res) => {
so.value.apply(so, res);
})
@@ -257,14 +220,60 @@ return view.extend({
.format('https://github.com/muink/openwrt-stuntman');
so.readonly = true;
} else {
so.renderWidget = function(/* ... */) {
let El = form.Value.prototype.renderWidget.apply(this, arguments);
so.renderWidget = function(section_id, option_index, cfgvalue) {
const cval = new URL(cfgvalue || this.default);
//console.info(cval.toString());
let El = form.Value.prototype.renderWidget.call(this, section_id, option_index, cval.host);
return renderNATBehaviorTest.call(this, El);
let resEl = E('div', { 'class': 'control-group' }, [
E('select', {
'id': '_status_nattest_l4proto',
'class': 'cbi-input-select',
'style': 'width: 5em'
}, [
...[
['udp', 'UDP'], // default
['tcp', 'TCP']
].map(res => E('option', {
value: res[0],
selected: (cval.protocol === `${res[0]}:`) ? "" : null
}, res[1]))
]),
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': ui.createHandlerFn(this, function() {
const stun = this.formvalue(this.section.section);
const l4proto = document.getElementById('_status_nattest_l4proto').value;
return fs.exec_direct('/etc/fchomo/scripts/natcheck.sh', [stun, l4proto, getRandom(32768, 61000)]).then((stdout) => {
this.description = '<details><summary>' + _('Expand/Collapse result') + '</summary>' + stdout + '</details>';
return this.map.reset().then((res) => {
});
});
})
}, [ _('Check') ])
]);
ui.addValidator(resEl.querySelector('#_status_nattest_l4proto'), 'string', false, (v) => {
const section_id = this.section.section;
const stun = this.formvalue(section_id);
this.onchange.call(this, {}, section_id, stun);
return true;
}, 'change');
let newEl = E('div', { style: 'font-weight: bold; align-items: center; display: flex' }, []);
if (El) {
newEl.appendChild(E([El, resEl]));
} else
newEl.appendChild(resEl);
return newEl;
}
}
so.onchange = function(ev, section_id, value) {
this.default = value;
const l4proto = document.getElementById('_status_nattest_l4proto').value;
this.default = `${l4proto}://${value}`;
}
so.write = function() {};
so.remove = function() {};
@@ -352,15 +352,15 @@ o:value("119.28.28.28")
o:depends("direct_dns_mode", "tcp")
o = s:taboption("DNS", Value, "direct_dns_dot", translate("Direct DNS DoT"))
o.default = "tls://dot.pub@1.12.12.12"
o:value("tls://dot.pub@1.12.12.12")
o:value("tls://dot.pub@120.53.53.53")
o:value("tls://dot.360.cn@36.99.170.86")
o:value("tls://dot.360.cn@101.198.191.4")
o:value("tls://dns.alidns.com@223.5.5.5")
o:value("tls://dns.alidns.com@223.6.6.6")
o:value("tls://dns.alidns.com@2400:3200::1")
o:value("tls://dns.alidns.com@2400:3200:baba::1")
o.default = "tls://1.12.12.12"
o:value("tls://1.12.12.12")
o:value("tls://120.53.53.53")
o:value("tls://36.99.170.86")
o:value("tls://101.198.191.4")
o:value("tls://223.5.5.5")
o:value("tls://223.6.6.6")
o:value("tls://2400:3200::1")
o:value("tls://2400:3200:baba::1")
o.validate = chinadns_dot_validate
o:depends("direct_dns_mode", "dot")
@@ -502,17 +502,17 @@ o:depends({singbox_dns_mode = "tcp"})
---- DoT
o = s:taboption("DNS", Value, "remote_dns_dot", translate("Remote DNS DoT"))
o.default = "tls://dns.google@8.8.4.4"
o:value("tls://1dot1dot1dot1.cloudflare-dns.com@1.0.0.1", "1.0.0.1 (CloudFlare)")
o:value("tls://1dot1dot1dot1.cloudflare-dns.com@1.1.1.1", "1.1.1.1 (CloudFlare)")
o:value("tls://dns.google@8.8.4.4", "8.8.4.4 (Google)")
o:value("tls://dns.google@8.8.8.8", "8.8.8.8 (Google)")
o:value("tls://dns.quad9.net@9.9.9.9", "9.9.9.9 (Quad9)")
o:value("tls://dns.quad9.net@149.112.112.112", "149.112.112.112 (Quad9)")
o:value("tls://dns.adguard.com@94.140.14.14", "94.140.14.14 (AdGuard)")
o:value("tls://dns.adguard.com@94.140.15.15", "94.140.15.15 (AdGuard)")
o:value("tls://dns.opendns.com@208.67.222.222", "208.67.222.222 (OpenDNS)")
o:value("tls://dns.opendns.com@208.67.220.220", "208.67.220.220 (OpenDNS)")
o.default = "tls://1.1.1.1"
o:value("tls://1.0.0.1", "1.0.0.1 (CloudFlare)")
o:value("tls://1.1.1.1", "1.1.1.1 (CloudFlare)")
o:value("tls://8.8.4.4", "8.8.4.4 (Google)")
o:value("tls://8.8.8.8", "8.8.8.8 (Google)")
o:value("tls://9.9.9.9", "9.9.9.9 (Quad9)")
o:value("tls://149.112.112.112", "149.112.112.112 (Quad9)")
o:value("tls://94.140.14.14", "94.140.14.14 (AdGuard)")
o:value("tls://94.140.15.15", "94.140.15.15 (AdGuard)")
o:value("tls://208.67.222.222", "208.67.222.222 (OpenDNS)")
o:value("tls://208.67.220.220", "208.67.220.220 (OpenDNS)")
o.validate = chinadns_dot_validate
o:depends("dns_mode", "dot")

Some files were not shown because too many files have changed in this diff Show More