diff --git a/.github/update.log b/.github/update.log index 5a5b07f7ba..847f02b1eb 100644 --- a/.github/update.log +++ b/.github/update.log @@ -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 diff --git a/clash-meta/adapter/outbound/vless.go b/clash-meta/adapter/outbound/vless.go index 7ab61ff624..ab5167bf2b 100644 --- a/clash-meta/adapter/outbound/vless.go +++ b/clash-meta/adapter/outbound/vless.go @@ -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, } diff --git a/clash-meta/component/generater/cmd.go b/clash-meta/component/generater/cmd.go new file mode 100644 index 0000000000..9d2c3d976b --- /dev/null +++ b/clash-meta/component/generater/cmd.go @@ -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()) + } +} diff --git a/clash-meta/component/generater/types.go b/clash-meta/component/generater/types.go new file mode 100644 index 0000000000..06f59e9468 --- /dev/null +++ b/clash-meta/component/generater/types.go @@ -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[:]) +} diff --git a/clash-meta/constant/metadata.go b/clash-meta/constant/metadata.go index 5436298925..e4167845fa 100644 --- a/clash-meta/constant/metadata.go +++ b/clash-meta/constant/metadata.go @@ -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": diff --git a/clash-meta/docs/config.yaml b/clash-meta/docs/config.yaml index b92246ae2e..26e0f7672f 100644 --- a/clash-meta/docs/config.yaml +++ b/clash-meta/docs/config.yaml @@ -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 diff --git a/clash-meta/go.mod b/clash-meta/go.mod index 3585bc797e..585e5c5948 100644 --- a/clash-meta/go.mod +++ b/clash-meta/go.mod @@ -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 diff --git a/clash-meta/go.sum b/clash-meta/go.sum index 33ad535553..ae4f31d8bc 100644 --- a/clash-meta/go.sum +++ b/clash-meta/go.sum @@ -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= diff --git a/clash-meta/listener/config/vless.go b/clash-meta/listener/config/vless.go new file mode 100644 index 0000000000..97456acc0a --- /dev/null +++ b/clash-meta/listener/config/vless.go @@ -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 +} diff --git a/clash-meta/listener/inbound/vless.go b/clash-meta/listener/inbound/vless.go new file mode 100644 index 0000000000..5a6da1337e --- /dev/null +++ b/clash-meta/listener/inbound/vless.go @@ -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) diff --git a/clash-meta/listener/parse.go b/clash-meta/listener/parse.go index 1c8b6463e1..38082e92bd 100644 --- a/clash-meta/listener/parse.go +++ b/clash-meta/listener/parse.go @@ -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) diff --git a/clash-meta/listener/sing_vless/server.go b/clash-meta/listener/sing_vless/server.go new file mode 100644 index 0000000000..f537de2d9a --- /dev/null +++ b/clash-meta/listener/sing_vless/server.go @@ -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 +} diff --git a/clash-meta/main.go b/clash-meta/main.go index 9a2222df15..3bc3d74f73 100644 --- a/clash-meta/main.go +++ b/clash-meta/main.go @@ -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) diff --git a/clash-nyanpasu/backend/Cargo.lock b/clash-nyanpasu/backend/Cargo.lock index 89b696d924..1f2c576251 100644 --- a/clash-nyanpasu/backend/Cargo.lock +++ b/clash-nyanpasu/backend/Cargo.lock @@ -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", diff --git a/clash-nyanpasu/backend/tauri/Cargo.toml b/clash-nyanpasu/backend/tauri/Cargo.toml index 17307c64ae..5aaabb630f 100644 --- a/clash-nyanpasu/backend/tauri/Cargo.toml +++ b/clash-nyanpasu/backend/tauri/Cargo.toml @@ -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] diff --git a/clash-nyanpasu/backend/tauri/src/config/nyanpasu/logging.rs b/clash-nyanpasu/backend/tauri/src/config/nyanpasu/logging.rs index 77538b13f7..642a33ed74 100644 --- a/clash-nyanpasu/backend/tauri/src/config/nyanpasu/logging.rs +++ b/clash-nyanpasu/backend/tauri/src/config/nyanpasu/logging.rs @@ -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, diff --git a/clash-nyanpasu/backend/tauri/src/config/nyanpasu/mod.rs b/clash-nyanpasu/backend/tauri/src/config/nyanpasu/mod.rs index 01238782bb..33d41f496d 100644 --- a/clash-nyanpasu/backend/tauri/src/config/nyanpasu/mod.rs +++ b/clash-nyanpasu/backend/tauri/src/config/nyanpasu/mod.rs @@ -149,10 +149,6 @@ pub struct IVerge { /// `light` or `dark` or `system` pub theme_mode: Option, - /// enable blur mode - /// maybe be able to set the alpha - pub theme_blur: Option, - /// enable traffic graph default is true pub traffic_graph: Option, @@ -189,7 +185,7 @@ pub struct IVerge { pub proxy_guard_interval: Option, /// theme setting - pub theme_setting: Option, + pub theme_color: Option, /// web ui list pub web_ui_list: Option>, @@ -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, - pub secondary_color: Option, - pub primary_text: Option, - pub secondary_text: Option, - - pub info_color: Option, - pub error_color: Option, - pub warning_color: Option, - pub success_color: Option, - - pub font_family: Option, - pub css_injection: Option, - - pub page_transition_duration: Option, -} - impl IVerge { pub fn new() -> Self { match dirs::nyanpasu_config_path().and_then(|path| help::read_yaml::(&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), diff --git a/clash-nyanpasu/backend/tauri/src/core/migration/units/unit_200.rs b/clash-nyanpasu/backend/tauri/src/core/migration/units/unit_200.rs index 2ee657a090..39a2f44917 100644 --- a/clash-nyanpasu/backend/tauri/src/core/migration/units/unit_200.rs +++ b/clash-nyanpasu/backend/tauri/src/core/migration/units/unit_200.rs @@ -13,6 +13,7 @@ pub static UNITS: Lazy> = 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(()) + } +} diff --git a/clash-nyanpasu/backend/tauri/src/core/tray/proxies.rs b/clash-nyanpasu/backend/tauri/src/core/tray/proxies.rs index cbaab81f94..395497e6f8 100644 --- a/clash-nyanpasu/backend/tauri/src/core/tray/proxies.rs +++ b/clash-nyanpasu/backend/tauri/src/core/tray/proxies.rs @@ -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)?, diff --git a/clash-nyanpasu/backend/tauri/src/main.rs b/clash-nyanpasu/backend/tauri/src/main.rs index 70a8f8b25e..2a4b2729ce 100644 --- a/clash-nyanpasu/backend/tauri/src/main.rs +++ b/clash-nyanpasu/backend/tauri/src/main.rs @@ -1,5 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - app_lib::run().unwrap(); + clash_nyanpasu_lib::run().unwrap(); } diff --git a/clash-nyanpasu/backend/tauri/src/utils/init/logging.rs b/clash-nyanpasu/backend/tauri/src/utils/init/logging.rs index d2d0539e5f..240244c008 100644 --- a/clash-nyanpasu/backend/tauri/src/utils/init/logging.rs +++ b/clash-nyanpasu/backend/tauri/src/utils/init/logging.rs @@ -62,9 +62,11 @@ pub fn init() -> Result<()> { let (filter, filter_handle) = reload::Layer::new( EnvFilter::builder() .with_default_directive( - std::convert::Into::::into(log_level).into(), + std::convert::Into::::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::::into(level).into(), + std::convert::Into::::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 } diff --git a/clash-nyanpasu/backend/tauri/tauri.conf.json b/clash-nyanpasu/backend/tauri/tauri.conf.json index c9c69f8667..450c7baf2d 100644 --- a/clash-nyanpasu/backend/tauri/tauri.conf.json +++ b/clash-nyanpasu/backend/tauri/tauri.conf.json @@ -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" } diff --git a/clash-nyanpasu/frontend/interface/package.json b/clash-nyanpasu/frontend/interface/package.json index c9cde79e33..e6b42e2500 100644 --- a/clash-nyanpasu/frontend/interface/package.json +++ b/clash-nyanpasu/frontend/interface/package.json @@ -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" } } diff --git a/clash-nyanpasu/frontend/interface/src/index.ts b/clash-nyanpasu/frontend/interface/src/index.ts index 52461603f4..772ba8a2df 100644 --- a/clash-nyanpasu/frontend/interface/src/index.ts +++ b/clash-nyanpasu/frontend/interface/src/index.ts @@ -2,4 +2,5 @@ export * from './ipc' export * from './openapi' export * from './provider' export * from './service' +export * from './template' export * from './utils' diff --git a/clash-nyanpasu/frontend/interface/src/ipc/bindings.ts b/clash-nyanpasu/frontend/interface/src/ipc/bindings.ts index 7a5b88079f..28715f8101 100644 --- a/clash-nyanpasu/frontend/interface/src/ipc/bindings.ts +++ b/clash-nyanpasu/frontend/interface/src/ipc/bindings.ts @@ -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 diff --git a/clash-nyanpasu/frontend/interface/src/ipc/index.ts b/clash-nyanpasu/frontend/interface/src/ipc/index.ts index 60f379e20a..5a2e073617 100644 --- a/clash-nyanpasu/frontend/interface/src/ipc/index.ts +++ b/clash-nyanpasu/frontend/interface/src/ipc/index.ts @@ -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' diff --git a/clash-nyanpasu/frontend/interface/src/ipc/use-profile-content.ts b/clash-nyanpasu/frontend/interface/src/ipc/use-profile-content.ts index 5ec711e0ef..be31460d5c 100644 --- a/clash-nyanpasu/frontend/interface/src/ipc/use-profile-content.ts +++ b/clash-nyanpasu/frontend/interface/src/ipc/use-profile-content.ts @@ -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, }) /** diff --git a/clash-nyanpasu/frontend/interface/src/ipc/use-profile.ts b/clash-nyanpasu/frontend/interface/src/ipc/use-profile.ts index 4e66b75986..8b43079538 100644 --- a/clash-nyanpasu/frontend/interface/src/ipc/use-profile.ts +++ b/clash-nyanpasu/frontend/interface/src/ipc/use-profile.ts @@ -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 @@ -22,68 +27,98 @@ type CreateParams = } } +type ProfileHelperFn = { + view: () => Promise + update: (profile: ProfileBuilder) => Promise + drop: () => Promise +} + +export type ProfileQueryResult = NonNullable< + ReturnType['query']['data'] +> + +export type ProfileQueryResultItem = Profile & Partial /** - * 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) => { + return unwrapResult( + await commands.patchProfilesConfig(options as ProfilesBuilder), + ) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['profiles'] }) diff --git a/clash-nyanpasu/frontend/interface/src/ipc/use-runtime-profile.ts b/clash-nyanpasu/frontend/interface/src/ipc/use-runtime-profile.ts new file mode 100644 index 0000000000..30e1d18975 --- /dev/null +++ b/clash-nyanpasu/frontend/interface/src/ipc/use-runtime-profile.ts @@ -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, + } +} diff --git a/clash-nyanpasu/frontend/interface/src/ipc/use-settings.ts b/clash-nyanpasu/frontend/interface/src/ipc/use-settings.ts new file mode 100644 index 0000000000..725974ccfe --- /dev/null +++ b/clash-nyanpasu/frontend/interface/src/ipc/use-settings.ts @@ -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) => { + 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 = (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), + } +} diff --git a/clash-nyanpasu/frontend/interface/src/ipc/use-system-proxy.ts b/clash-nyanpasu/frontend/interface/src/ipc/use-system-proxy.ts new file mode 100644 index 0000000000..615c1ec593 --- /dev/null +++ b/clash-nyanpasu/frontend/interface/src/ipc/use-system-proxy.ts @@ -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, + } +} diff --git a/clash-nyanpasu/frontend/interface/src/ipc/useClash.ts b/clash-nyanpasu/frontend/interface/src/ipc/useClash.ts index d99606643d..201869e3ba 100644 --- a/clash-nyanpasu/frontend/interface/src/ipc/useClash.ts +++ b/clash-nyanpasu/frontend/interface/src/ipc/useClash.ts @@ -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) => { + const setProfiles = async (uid: string, profile: Partial) => { await tauri.setProfiles({ uid, profile }) await getProfiles.mutate() @@ -41,7 +46,7 @@ export const useClash = () => { await getRuntimeLogs.mutate() } - const setProfilesConfig = async (profiles: Partial) => { + 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, data?: string) => { + const createProfile = async (item: Partial, 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() diff --git a/clash-nyanpasu/frontend/interface/src/service/core.ts b/clash-nyanpasu/frontend/interface/src/service/core.ts index 44e9d8f011..63acbdd492 100644 --- a/clash-nyanpasu/frontend/interface/src/service/core.ts +++ b/clash-nyanpasu/frontend/interface/src/service/core.ts @@ -1,7 +1,5 @@ +import type { ClashCore } from '../ipc/bindings' import { fetchLatestCoreVersions, getCoreVersion } from './tauri' -import { VergeConfig } from './types' - -export type ClashCore = Required['clash_core'] export interface Core { name: string @@ -56,7 +54,7 @@ export const fetchLatestCore = async () => { return { ...item, - latest: latest, + latest, } }) diff --git a/clash-nyanpasu/frontend/interface/src/service/tauri.ts b/clash-nyanpasu/frontend/interface/src/service/tauri.ts index 4c055605c0..ac8a942c22 100644 --- a/clash-nyanpasu/frontend/interface/src/service/tauri.ts +++ b/clash-nyanpasu/frontend/interface/src/service/tauri.ts @@ -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, + item: Partial, fileData?: string | null, ) => { return await invoke('create_profile', { item, fileData }) } -export const updateProfile = async (uid: string, option?: Profile.Option) => { +export const updateProfile = async ( + uid: string, + option?: RemoteProfileOptionsBuilder, +) => { return await invoke('update_profile', { uid, option }) } @@ -56,17 +64,17 @@ export const viewProfile = async (uid: string) => { } export const getProfiles = async () => { - return await invoke('get_profiles') + return await invoke('get_profiles') } export const setProfiles = async (payload: { uid: string - profile: Partial + profile: Partial }) => { return await invoke('patch_profile', payload) } -export const setProfilesConfig = async (profiles: Partial) => { +export const setProfilesConfig = async (profiles: ProfilesBuilder) => { return await invoke('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('import_profile', { url, diff --git a/clash-nyanpasu/frontend/interface/src/service/types.ts b/clash-nyanpasu/frontend/interface/src/service/types.ts index 037396f6a1..79f387c0fc 100644 --- a/clash-nyanpasu/frontend/interface/src/service/types.ts +++ b/clash-nyanpasu/frontend/interface/src/service/types.ts @@ -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 - groups: Clash.Proxy[] - proxies: Clash.Proxy[] - records: { - [key: string]: Clash.Proxy - } -} - // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Connection { export interface Item { diff --git a/clash-nyanpasu/frontend/interface/src/template/index.ts b/clash-nyanpasu/frontend/interface/src/template/index.ts new file mode 100644 index 0000000000..8f4e1d8dc9 --- /dev/null +++ b/clash-nyanpasu/frontend/interface/src/template/index.ts @@ -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 diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 13f84885fb..8231fadf74 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -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", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/assets/styles/fonts.scss b/clash-nyanpasu/frontend/nyanpasu/src/assets/styles/fonts.scss index 8a40d8bdbf..0b5084aa72 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/assets/styles/fonts.scss +++ b/clash-nyanpasu/frontend/nyanpasu/src/assets/styles/fonts.scss @@ -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 } diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/connections/connections-total.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/connections/connections-total.tsx index b74b03f00c..17d0794790 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/connections/connections-total.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/connections/connections-total.tsx @@ -51,7 +51,10 @@ export default function ConnectionTotal() {
Promise 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) => { diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/language-chip.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/language-chip.tsx index e82cb3c472..ce5f00cbf3 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/language-chip.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/language-chip.tsx @@ -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) diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/side-chain.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/side-chain.tsx index 4151428636..6cb7c263f9 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/side-chain.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/side-chain.tsx @@ -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 + onChainEdit: (item?: ProfileQueryResultItem) => void | Promise } 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 ( diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/side-log.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/side-log.tsx index d0fd8f524d..9e1f616bc8 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/side-log.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/side-log.tsx @@ -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 (
@@ -54,7 +56,7 @@ export const SideLog = ({ className }: SideLogProps) => { - {!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) => { ) }) }) - ) : ( -
- -

{t('No Logs')}

-
- )} + ) : ( */} +
+ +

{t('No Logs')}

+
+ {/* )} */}
) diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/store.ts b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/store.ts index 5b932558cc..3f09a5311f 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/store.ts +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/modules/store.ts @@ -1,6 +1,5 @@ import { atom } from 'jotai' -import type { Profile } from '@nyanpasu/interface' export const atomGlobalChainCurrent = atom(false) -export const atomChainsSelected = atom() +export const atomChainsSelected = atom() diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-dialog.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-dialog.tsx index 9331cf3e6b..55002018fd 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-dialog.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-dialog.tsx @@ -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({ - 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([]) + 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 }) => ( )} @@ -264,7 +307,7 @@ export const ProfileDialog = ({ render={({ 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) diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx index 5403620f73..a172569cad 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx @@ -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} >
- + } @@ -255,9 +257,9 @@ export const ProfileItem = memo(function ProfileItem({ !!item.updated && ( ), - !!item.extra?.expire && ( + !!(item as RemoteProfile).extra?.expire && ( ), diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-side.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-side.tsx index 33ec0aa184..7e3359b9e8 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-side.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-side.tsx @@ -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() + const [item, setItem] = useState() 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) } diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/quick-import.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/quick-import.tsx index e5098343cb..f2d88d30c8 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/quick-import.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/quick-import.tsx @@ -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) diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/runtime-config-diff-dialog.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/runtime-config-diff-dialog.tsx index 6a03436d1b..2fa644f126 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/runtime-config-diff-dialog.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/runtime-config-diff-dialog.tsx @@ -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({ 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 { 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() + const { create, update } = useProfile() + + const contentFn = useProfileContent(profile?.uid ?? '') + + const form = useForm() 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 = ({
{isEdit ? t('Edit Script') : t('New Script')} - +
} 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 } /> )} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/utils.ts b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/utils.ts index 26534005cb..a2b5d249db 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/utils.ts +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/utils.ts @@ -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(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' } } diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/node-card.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/node-card.tsx index d60edf602f..5a259f61c6 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/node-card.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/node-card.tsx @@ -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 - now?: string + node: ProxyItem + now?: string | null disabled?: boolean onClick: () => void onClickDelay: () => Promise diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/node-list.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/node-list.tsx index 6a4530cf19..bb77942ab4 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/node-list.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/node-list.tsx @@ -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 & { 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>>() + const [group, setGroup] = useState() 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)) diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/utils.ts b/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/utils.ts index 9650ee5bce..316fb7a7a9 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/utils.ts +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/proxies/utils.ts @@ -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>, + selectedGroup: ProxyGroupItem, type: SortType, ) => { let sortedList = selectedGroup.all?.slice() diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/clash-web.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/clash-web.tsx index 3848c06927..cb0a53b863 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/clash-web.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/clash-web.tsx @@ -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 = '' diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-clash-field.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-clash-field.tsx index 64323cef44..f0b5fa415d 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-clash-field.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-clash-field.tsx @@ -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 ( diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-nyanpasu-ui.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-nyanpasu-ui.tsx index b062ee84ed..1c296eec27 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-nyanpasu-ui.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/setting-nyanpasu-ui.tsx @@ -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 ( + 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 ( + 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 ( + <> + + + + { + if (!isHexColor(value ?? defaultTheme.primary_color)) { + setValue(value) + } + }} + onChange={(color: string) => setValue(color)} + /> + + + +
+ +
+
+ + ) +} + +export const SettingNyanpasuUI = () => { + const { t } = useTranslation() const [onlyIcon, setOnlyIcon] = useAtom(atomIsDrawerOnlyIcon) return ( - - setNyanpasuConfig({ language: value as string }) - } - /> + - - setNyanpasuConfig({ - theme_mode: value as VergeConfig['theme_mode'], - }) - } - /> + - - - - { - if ( - !isHexColor(themeColorRef.current ?? defaultTheme.primary_color) - ) { - setThemeColor(themeColorRef.current) - return - } - themeColorRef.current = themeColor - }} - onChange={(color: string) => setThemeColor(color)} - /> - - - - - - - + { + 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 ( + + ) +} + +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 ( + + ) +} + +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 ( + + ) +} + +const ProxyGuardInterval = () => { + const { t } = useTranslation() + + const proxyGuardInterval = useSetting('proxy_guard_interval') + + return ( + input <= 0} + checkLabel={t('The interval must be greater than 0 second')} + onApply={(value) => { + proxyGuardInterval.upsert(value) + }} + textFieldProps={{ + inputProps: { + 'aria-autocomplete': 'none', + }, + InputProps: { + endAdornment: s, + }, + }} + /> + ) +} + +const SystemProxyBypass = () => { + const { t } = useTranslation() + + const systemProxyBypass = useSetting('system_proxy_bypass') + + return ( + { + systemProxyBypass.upsert(value) + }} + /> + ) +} + +const CurrentSystemProxy = () => { + const { t } = useTranslation() + + const { data } = useSystemProxy() + + return ( + +
{t('Current System Proxy')}
+ + {Object.entries(data ?? []).map(([key, value], index) => { + return ( +
+
{key}:
+ +
{String(value)}
+
+ ) + })} +
+ ) +} + 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 ( { } > - - handleClick('enable_tun_mode')} - /> + + - handleClick('enable_system_proxy')} - /> + - - setNyanpasuConfig({ - enable_proxy_guard: !nyanpasuConfig?.enable_proxy_guard, - }) - } - /> + - 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: s, - }, - }} - /> + - - setProxyBypass(e.target.value)} - /> - + - - - - - - - - - - {t('Current System Proxy')} - - - {Object.entries(getSystemProxy?.data ?? []).map( - ([key, value], index) => { - return ( - - - {key}: - - - {String(value)} - - ) - }, - )} - - + diff --git a/clash-nyanpasu/frontend/nyanpasu/src/locales/en.json b/clash-nyanpasu/frontend/nyanpasu/src/locales/en.json index 6cc9aabb64..6f7cb009ae 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/locales/en.json +++ b/clash-nyanpasu/frontend/nyanpasu/src/locales/en.json @@ -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", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/locales/ru.json b/clash-nyanpasu/frontend/nyanpasu/src/locales/ru.json index d6873fdf2e..6b58909213 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/locales/ru.json +++ b/clash-nyanpasu/frontend/nyanpasu/src/locales/ru.json @@ -15,7 +15,7 @@ "Active Connections": "Активные соединения", "Timeout": "Тайм-аут", "Click to Refresh Now": "Нажмите для обновления", - "No Proxy": "Без прокси", + "No Proxies": "Без прокси", "Direct Mode": "Прямой режим", "Rules": "Правила", "No Rules": "Нет правил", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/locales/zh-CN.json b/clash-nyanpasu/frontend/nyanpasu/src/locales/zh-CN.json index 529fb74064..7a1ab78a05 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/locales/zh-CN.json +++ b/clash-nyanpasu/frontend/nyanpasu/src/locales/zh-CN.json @@ -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}}", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/locales/zh-TW.json b/clash-nyanpasu/frontend/nyanpasu/src/locales/zh-TW.json index 5bb5709d96..b842b6435c 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/locales/zh-TW.json +++ b/clash-nyanpasu/frontend/nyanpasu/src/locales/zh-TW.json @@ -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": "添加新項目", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/main.tsx b/clash-nyanpasu/frontend/nyanpasu/src/main.tsx index 426bc42aa6..cb1ecbb64e 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/main.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/main.tsx @@ -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, diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx index b2bb60e792..f9df21ef3c 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx @@ -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> = [] + 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 && ( - {profiles.map((item) => ( + {profiles.clash?.map((item) => ( diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/proxies.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/proxies.tsx index 59f9491e42..fc2497efcf 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/proxies.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/proxies.tsx @@ -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 | string>>() + const [group, setGroup] = useState() 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() { ) : ( - + ) ) : ( diff --git a/clash-nyanpasu/frontend/ui/package.json b/clash-nyanpasu/frontend/ui/package.json index 5826a78983..afa711ef70 100644 --- a/clash-nyanpasu/frontend/ui/package.json +++ b/clash-nyanpasu/frontend/ui/package.json @@ -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", diff --git a/clash-nyanpasu/locales/en.json b/clash-nyanpasu/locales/en.json index dbe569b0e7..13de749baa 100644 --- a/clash-nyanpasu/locales/en.json +++ b/clash-nyanpasu/locales/en.json @@ -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", diff --git a/clash-nyanpasu/locales/ru.json b/clash-nyanpasu/locales/ru.json index 8c57a2d922..56c2847f13 100644 --- a/clash-nyanpasu/locales/ru.json +++ b/clash-nyanpasu/locales/ru.json @@ -6,6 +6,7 @@ "ps": "Копировать Env (PS)", "sh": "Копировать Env (sh)" }, + "no_proxies": "Без прокси", "select_proxies": "Выбрать прокси", "dashboard": "Панель управления", "direct_mode": "Прямой режим", diff --git a/clash-nyanpasu/locales/zh-CN.json b/clash-nyanpasu/locales/zh-CN.json index 2cd6b30da1..7b26f2d202 100644 --- a/clash-nyanpasu/locales/zh-CN.json +++ b/clash-nyanpasu/locales/zh-CN.json @@ -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请注意:此操作需要输入密码。" diff --git a/clash-nyanpasu/locales/zh-TW.json b/clash-nyanpasu/locales/zh-TW.json index 095f77d61a..4576128eae 100644 --- a/clash-nyanpasu/locales/zh-TW.json +++ b/clash-nyanpasu/locales/zh-TW.json @@ -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請注意:此操作需要輸入密碼。" diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index 6a05899011..dd335286da 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -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", diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index 323351f847..702a9a286d 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -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 diff --git a/lede/target/linux/generic/backport-6.6/843-v6.13-net-phy-mxl-gpy-add-basic-LED-support.patch b/lede/target/linux/generic/backport-6.6/843-v6.13-net-phy-mxl-gpy-add-basic-LED-support.patch new file mode 100644 index 0000000000..2ceaa0ad3d --- /dev/null +++ b/lede/target/linux/generic/backport-6.6/843-v6.13-net-phy-mxl-gpy-add-basic-LED-support.patch @@ -0,0 +1,332 @@ +From 78997e9a5e4d8a4df561e083a92c91ae23010e07 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Reviewed-by: Andrew Lunn +Link: https://patch.msgid.link/b6ec9050339f8244ff898898a1cecc33b13a48fc.1727741563.git.daniel@makrotopia.org +Signed-off-by: Jakub Kicinski +--- + 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), diff --git a/lede/target/linux/generic/backport-6.6/844-v6.13-net-phy-mxl-gpy-add-missing-support-for-TRIGGER_NETD.patch b/lede/target/linux/generic/backport-6.6/844-v6.13-net-phy-mxl-gpy-add-missing-support-for-TRIGGER_NETD.patch new file mode 100644 index 0000000000..067c62da11 --- /dev/null +++ b/lede/target/linux/generic/backport-6.6/844-v6.13-net-phy-mxl-gpy-add-missing-support-for-TRIGGER_NETD.patch @@ -0,0 +1,28 @@ +From f95b4725e796b12e5f347a0d161e1d3843142aa8 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Reviewed-by: Andrew Lunn +Link: https://patch.msgid.link/cc5da0a989af8b0d49d823656d88053c4de2ab98.1728057367.git.daniel@makrotopia.org +Signed-off-by: Jakub Kicinski +--- + 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) | diff --git a/lede/target/linux/generic/backport-6.6/845-v6.13-net-phy-mxl-gpy-correctly-describe-LED-polarity.patch b/lede/target/linux/generic/backport-6.6/845-v6.13-net-phy-mxl-gpy-correctly-describe-LED-polarity.patch new file mode 100644 index 0000000000..5b88548dd0 --- /dev/null +++ b/lede/target/linux/generic/backport-6.6/845-v6.13-net-phy-mxl-gpy-correctly-describe-LED-polarity.patch @@ -0,0 +1,58 @@ +From eb89c79c1b8f17fc1611540768678e60df89ac42 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Reviewed-by: Andrew Lunn +Link: https://patch.msgid.link/180ccafa837f09908b852a8a874a3808c5ecd2d0.1728558223.git.daniel@makrotopia.org +Signed-off-by: Paolo Abeni +--- + 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[] = { diff --git a/lede/target/linux/generic/backport-6.6/846-v6.13-net-phy-intel-xway-add-support-for-PHY-LEDs.patch b/lede/target/linux/generic/backport-6.6/846-v6.13-net-phy-intel-xway-add-support-for-PHY-LEDs.patch new file mode 100644 index 0000000000..c57b5777ad --- /dev/null +++ b/lede/target/linux/generic/backport-6.6/846-v6.13-net-phy-intel-xway-add-support-for-PHY-LEDs.patch @@ -0,0 +1,379 @@ +From 1758af47b98c17da464cb45f476875150955dd48 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Reviewed-by: Andrew Lunn +Link: https://patch.msgid.link/81f4717ab9acf38f3239727a4540ae96fd01109b.1728558223.git.daniel@makrotopia.org +Signed-off-by: Paolo Abeni +--- + 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); diff --git a/lede/target/linux/generic/hack-6.6/765-mxl-gpy-control-LED-reg-from-DT.patch b/lede/target/linux/generic/hack-6.6/765-mxl-gpy-control-LED-reg-from-DT.patch index fd2a327811..041f05e59b 100644 --- a/lede/target/linux/generic/hack-6.6/765-mxl-gpy-control-LED-reg-from-DT.patch +++ b/lede/target/linux/generic/hack-6.6/765-mxl-gpy-control-LED-reg-from-DT.patch @@ -31,45 +31,21 @@ Signed-off-by: David Bauer #include #include #include -@@ -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 + 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; diff --git a/lede/target/linux/mediatek/patches-6.6/732-net-phy-mxl-gpy-don-t-use-SGMII-AN-if-using-phylink.patch b/lede/target/linux/mediatek/patches-6.6/732-net-phy-mxl-gpy-don-t-use-SGMII-AN-if-using-phylink.patch index 99d0a0dbc2..252263ad11 100644 --- a/lede/target/linux/mediatek/patches-6.6/732-net-phy-mxl-gpy-don-t-use-SGMII-AN-if-using-phylink.patch +++ b/lede/target/linux/mediatek/patches-6.6/732-net-phy-mxl-gpy-don-t-use-SGMII-AN-if-using-phylink.patch @@ -14,7 +14,7 @@ Signed-off-by: Daniel Golle --- 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 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 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 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; diff --git a/mihomo/adapter/outbound/vless.go b/mihomo/adapter/outbound/vless.go index 7ab61ff624..ab5167bf2b 100644 --- a/mihomo/adapter/outbound/vless.go +++ b/mihomo/adapter/outbound/vless.go @@ -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, } diff --git a/mihomo/component/generater/cmd.go b/mihomo/component/generater/cmd.go new file mode 100644 index 0000000000..9d2c3d976b --- /dev/null +++ b/mihomo/component/generater/cmd.go @@ -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()) + } +} diff --git a/mihomo/component/generater/types.go b/mihomo/component/generater/types.go new file mode 100644 index 0000000000..06f59e9468 --- /dev/null +++ b/mihomo/component/generater/types.go @@ -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[:]) +} diff --git a/mihomo/constant/metadata.go b/mihomo/constant/metadata.go index 5436298925..e4167845fa 100644 --- a/mihomo/constant/metadata.go +++ b/mihomo/constant/metadata.go @@ -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": diff --git a/mihomo/docs/config.yaml b/mihomo/docs/config.yaml index b92246ae2e..26e0f7672f 100644 --- a/mihomo/docs/config.yaml +++ b/mihomo/docs/config.yaml @@ -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 diff --git a/mihomo/go.mod b/mihomo/go.mod index 3585bc797e..585e5c5948 100644 --- a/mihomo/go.mod +++ b/mihomo/go.mod @@ -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 diff --git a/mihomo/go.sum b/mihomo/go.sum index 33ad535553..ae4f31d8bc 100644 --- a/mihomo/go.sum +++ b/mihomo/go.sum @@ -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= diff --git a/mihomo/listener/config/vless.go b/mihomo/listener/config/vless.go new file mode 100644 index 0000000000..97456acc0a --- /dev/null +++ b/mihomo/listener/config/vless.go @@ -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 +} diff --git a/mihomo/listener/inbound/vless.go b/mihomo/listener/inbound/vless.go new file mode 100644 index 0000000000..5a6da1337e --- /dev/null +++ b/mihomo/listener/inbound/vless.go @@ -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) diff --git a/mihomo/listener/parse.go b/mihomo/listener/parse.go index 1c8b6463e1..38082e92bd 100644 --- a/mihomo/listener/parse.go +++ b/mihomo/listener/parse.go @@ -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) diff --git a/mihomo/listener/sing_vless/server.go b/mihomo/listener/sing_vless/server.go new file mode 100644 index 0000000000..f537de2d9a --- /dev/null +++ b/mihomo/listener/sing_vless/server.go @@ -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 +} diff --git a/mihomo/main.go b/mihomo/main.go index 9a2222df15..3bc3d74f73 100644 --- a/mihomo/main.go +++ b/mihomo/main.go @@ -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) diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua index bfa24b46ce..6719745480 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -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 diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh index dda7f6dfd0..0818288f05 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/app.sh @@ -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 diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua index 0809f3888f..daabe24b46 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -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('订阅完毕...') diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index f3d132db80..c590ab38f3 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -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]] diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua index d8cbd949d7..943d59f8c5 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -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 diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/app.sh b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/app.sh index ed19cdf023..f07d50ab53 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/app.sh +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -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 diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh index 53a43cc5d5..dfbcbf65c2 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh @@ -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} diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh index b3c174189a..85d81353fb 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh @@ -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 diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js index 340dbb3420..bae18af3b2 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js @@ -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 = '
' + _('Expand/Collapse result') + '' + stdout + '
'; - - 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 = '
' + _('Expand/Collapse result') + '' + stdout + '
'; + + 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() {}; diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index 3e98ec5058..5687101994 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -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") diff --git a/small/luci-app-passwall/root/usr/share/passwall/app.sh b/small/luci-app-passwall/root/usr/share/passwall/app.sh index 1704cc246b..dda7f6dfd0 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/app.sh @@ -919,7 +919,7 @@ run_redir() { _args="${_args} direct_dns_tcp_server=$(config_t_get global direct_dns_tcp 223.5.5.5 | sed 's/:/#/g')" ;; dot) - local tmp_dot_dns=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12") + local tmp_dot_dns=$(config_t_get global direct_dns_dot "tls://1.12.12.12") local tmp_dot_ip=$(echo "$tmp_dot_dns" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p') local tmp_dot_port=$(echo "$tmp_dot_dns" | sed -n 's/.*#\([0-9]\+\).*/\1/p') _args="${_args} direct_dns_dot_server=$tmp_dot_ip#${tmp_dot_port:-853}" @@ -1397,7 +1397,7 @@ start_dns() { ;; dot) if [ "$chinadns_tls" != "nil" ]; then - local DIRECT_DNS=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12") + local DIRECT_DNS=$(config_t_get global direct_dns_dot "tls://1.12.12.12") china_ng_local_dns=${DIRECT_DNS} #当全局(包括访问控制节点)开启chinadns-ng时,不启动新进程。 @@ -1519,7 +1519,7 @@ start_dns() { TCP_PROXY_DNS=1 if [ "$chinadns_tls" != "nil" ]; then local china_ng_listen_port=${NEXT_DNS_LISTEN_PORT} - local china_ng_trust_dns=$(config_t_get global remote_dns_dot "tls://dns.google@8.8.4.4") + local china_ng_trust_dns=$(config_t_get global remote_dns_dot "tls://1.1.1.1") local tmp_dot_ip=$(echo "$china_ng_trust_dns" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p') local tmp_dot_port=$(echo "$china_ng_trust_dns" | sed -n 's/.*#\([0-9]\+\).*/\1/p') REMOTE_DNS="$tmp_dot_ip#${tmp_dot_port:-853}" @@ -1864,7 +1864,7 @@ acl_app() { ;; dot) if [ "$(chinadns-ng -V | grep -i wolfssl)" != "nil" ]; then - _chinadns_local_dns=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12") + _chinadns_local_dns=$(config_t_get global direct_dns_dot "tls://1.12.12.12") fi ;; esac diff --git a/small/mihomo/files/mihomo.init b/small/mihomo/files/mihomo.init index 12f06fe958..80ecc213b7 100644 --- a/small/mihomo/files/mihomo.init +++ b/small/mihomo/files/mihomo.init @@ -617,7 +617,7 @@ mixin_authentications() { } mixin_tun_dns_hijacks() { - dns_hijack="$1" yq -M -i '.tun.dns_hijack += [strenv(dns_hijack)]' "$RUN_PROFILE_PATH" + dns_hijack="$1" yq -M -i '.tun.dns-hijack += [strenv(dns_hijack)]' "$RUN_PROFILE_PATH" } mixin_fake_ip_filters() { diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index 29caa18ecd..1d5bccf2ba 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -30,13 +30,13 @@ define Download/geosite HASH:=ac12d81edc6058b3c66ae96a0a26ca8281616d96ea86d0d77b2ceff34a3e1a9d endef -GEOSITE_IRAN_VER:=202501270034 +GEOSITE_IRAN_VER:=202502030035 GEOSITE_IRAN_FILE:=iran.dat.$(GEOSITE_IRAN_VER) define Download/geosite-ir URL:=https://github.com/bootmortis/iran-hosted-domains/releases/download/$(GEOSITE_IRAN_VER)/ URL_FILE:=iran.dat FILE:=$(GEOSITE_IRAN_FILE) - HASH:=183a9a6f3c3ce09893d51670f97fc0c5f4196e93701ac9351302b4974ac272d7 + HASH:=2e9292d9adfd684df520a9228b641f57e63581eb93f5938284beeb621fde6bf3 endef define Package/v2ray-geodata/template diff --git a/v2rayn/v2rayN/ServiceLib/Enums/ECoreType.cs b/v2rayn/v2rayN/ServiceLib/Enums/ECoreType.cs index cd93e7b989..d2252078f6 100644 --- a/v2rayn/v2rayN/ServiceLib/Enums/ECoreType.cs +++ b/v2rayn/v2rayN/ServiceLib/Enums/ECoreType.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Enums +namespace ServiceLib.Enums { public enum ECoreType { @@ -12,6 +12,8 @@ sing_box = 24, juicity = 25, hysteria2 = 26, + brook = 27, + overtls = 28, v2rayN = 99 } -} \ No newline at end of file +} diff --git a/v2rayn/v2rayN/ServiceLib/Global.cs b/v2rayn/v2rayN/ServiceLib/Global.cs index 9ea4196431..13d6850bda 100644 --- a/v2rayn/v2rayN/ServiceLib/Global.cs +++ b/v2rayn/v2rayN/ServiceLib/Global.cs @@ -1,4 +1,4 @@ -namespace ServiceLib +namespace ServiceLib { public class Global { @@ -7,17 +7,8 @@ public const string AppName = "v2rayN"; public const string GithubUrl = "https://github.com"; public const string GithubApiUrl = "https://api.github.com/repos"; - public const string V2flyCoreUrl = "https://github.com/v2fly/v2ray-core/releases"; - public const string XrayCoreUrl = "https://github.com/XTLS/Xray-core/releases"; - public const string NUrl = @"https://github.com/2dust/v2rayN/releases"; - public const string MihomoCoreUrl = "https://github.com/MetaCubeX/mihomo/releases"; - public const string HysteriaCoreUrl = "https://github.com/apernet/hysteria/releases"; - public const string NaiveproxyCoreUrl = "https://github.com/klzgrad/naiveproxy/releases"; - public const string TuicCoreUrl = "https://github.com/EAimTY/tuic/releases"; - public const string SingboxCoreUrl = "https://github.com/SagerNet/sing-box/releases"; public const string GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/{0}.dat"; public const string SpeedPingTestUrl = @"https://www.google.com/generate_204"; - public const string JuicityCoreUrl = "https://github.com/juicity/juicity/releases"; public const string SingboxRulesetUrl = @"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-{0}/{1}.srs"; public const string IPAPIUrl = "https://api.ip.sb/geoip"; @@ -168,27 +159,27 @@ public static readonly Dictionary ProtocolShares = new() { - {EConfigType.VMess,"vmess://"}, - {EConfigType.Shadowsocks,"ss://"}, - {EConfigType.SOCKS,"socks://"}, - {EConfigType.VLESS,"vless://"}, - {EConfigType.Trojan,"trojan://"}, - {EConfigType.Hysteria2,"hysteria2://"}, - {EConfigType.TUIC,"tuic://"}, - {EConfigType.WireGuard,"wireguard://"} + { EConfigType.VMess, "vmess://" }, + { EConfigType.Shadowsocks, "ss://" }, + { EConfigType.SOCKS, "socks://" }, + { EConfigType.VLESS, "vless://" }, + { EConfigType.Trojan, "trojan://" }, + { EConfigType.Hysteria2, "hysteria2://" }, + { EConfigType.TUIC, "tuic://" }, + { EConfigType.WireGuard, "wireguard://" } }; public static readonly Dictionary ProtocolTypes = new() { - {EConfigType.VMess,"vmess"}, - {EConfigType.Shadowsocks,"shadowsocks"}, - {EConfigType.SOCKS,"socks"}, - {EConfigType.HTTP,"http"}, - {EConfigType.VLESS,"vless"}, - {EConfigType.Trojan,"trojan"}, - {EConfigType.Hysteria2,"hysteria2"}, - {EConfigType.TUIC,"tuic"}, - {EConfigType.WireGuard,"wireguard"} + { EConfigType.VMess, "vmess" }, + { EConfigType.Shadowsocks, "shadowsocks" }, + { EConfigType.SOCKS, "socks" }, + { EConfigType.HTTP, "http" }, + { EConfigType.VLESS, "vless" }, + { EConfigType.Trojan, "trojan" }, + { EConfigType.Hysteria2, "hysteria2" }, + { EConfigType.TUIC, "tuic" }, + { EConfigType.WireGuard, "wireguard" } }; public static readonly List VmessSecurities = @@ -500,6 +491,23 @@ "http" ]; + public static readonly Dictionary CoreUrls = new() + { + { ECoreType.v2fly, "v2fly/v2ray-core" }, + { ECoreType.v2fly_v5, "v2fly/v2ray-core" }, + { ECoreType.Xray, "XTLS/Xray-core" }, + { ECoreType.sing_box, "SagerNet/sing-box" }, + { ECoreType.mihomo, "MetaCubeX/mihomo" }, + { ECoreType.hysteria, "apernet/hysteria" }, + { ECoreType.hysteria2, "apernet/hysteria" }, + { ECoreType.naiveproxy, "klzgrad/naiveproxy" }, + { ECoreType.tuic, "EAimTY/tuic" }, + { ECoreType.juicity, "juicity/juicity" }, + { ECoreType.brook, "txthinking/brook" }, + { ECoreType.overtls, "ShadowsocksR-Live/overtls" }, + { ECoreType.v2rayN, "2dust/v2rayN" }, + }; + #endregion const } -} \ No newline at end of file +} diff --git a/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs index 1270d8caac..6247704e04 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs @@ -225,7 +225,7 @@ namespace ServiceLib.Handler StartInfo = new() { FileName = fileName, - Arguments = string.Format(coreInfo.Arguments, configPath), + Arguments = string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetConfigPath(configPath) : configPath), WorkingDirectory = Utils.GetConfigPath(), UseShellExecute = false, RedirectStandardOutput = displayLog, diff --git a/v2rayn/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs index 1fa073f863..84371e33b2 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/CoreInfoHandler.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Handler +namespace ServiceLib.Handler { public sealed class CoreInfoHandler { @@ -44,7 +44,7 @@ } if (fileName.IsNullOrEmpty()) { - msg = string.Format(ResUI.NotFoundCore, Utils.GetBinPath("", coreInfo.CoreType.ToString()), string.Join(", ", coreInfo.CoreExes.ToArray()), coreInfo.Url); + msg = string.Format(ResUI.NotFoundCore, Utils.GetBinPath("", coreInfo?.CoreType.ToString()), coreInfo?.CoreExes?.LastOrDefault(), coreInfo?.Url); Logging.SaveLog(msg); } return fileName; @@ -52,107 +52,102 @@ private void InitCoreInfo() { + var urlN = GetCoreUrl(ECoreType.v2rayN); + var urlXray = GetCoreUrl(ECoreType.Xray); + var urlMihomo = GetCoreUrl(ECoreType.mihomo); + var urlSingbox = GetCoreUrl(ECoreType.sing_box); + _coreInfo = [ new CoreInfo { CoreType = ECoreType.v2rayN, - Url = Global.NUrl, - ReleaseApiUrl = Global.NUrl.Replace(Global.GithubUrl, Global.GithubApiUrl), - DownloadUrlWin64 = Global.NUrl + "/download/{0}/v2rayN-windows-64.zip", - DownloadUrlWinArm64 = Global.NUrl + "/download/{0}/v2rayN-windows-arm64.zip", - DownloadUrlLinux64 = Global.NUrl + "/download/{0}/v2rayN-linux-64.zip", - DownloadUrlLinuxArm64 = Global.NUrl + "/download/{0}/v2rayN-linux-arm64.zip", - DownloadUrlOSX64 = Global.NUrl + "/download/{0}/v2rayN-macos-64.zip", - DownloadUrlOSXArm64 = Global.NUrl + "/download/{0}/v2rayN-macos-arm64.zip", + Url = GetCoreUrl(ECoreType.v2rayN), + ReleaseApiUrl = urlN.Replace(Global.GithubUrl, Global.GithubApiUrl), + DownloadUrlWin64 = urlN + "/download/{0}/v2rayN-windows-64.zip", + DownloadUrlWinArm64 = urlN + "/download/{0}/v2rayN-windows-arm64.zip", + DownloadUrlLinux64 = urlN + "/download/{0}/v2rayN-linux-64.zip", + DownloadUrlLinuxArm64 = urlN + "/download/{0}/v2rayN-linux-arm64.zip", + DownloadUrlOSX64 = urlN + "/download/{0}/v2rayN-macos-64.zip", + DownloadUrlOSXArm64 = urlN + "/download/{0}/v2rayN-macos-arm64.zip", }, new CoreInfo { CoreType = ECoreType.v2fly, CoreExes = ["wv2ray", "v2ray"], - Arguments = "", - Url = Global.V2flyCoreUrl, - ReleaseApiUrl = Global.V2flyCoreUrl.Replace(Global.GithubUrl, Global.GithubApiUrl), + Arguments = "{0}", + Url = GetCoreUrl(ECoreType.v2fly), Match = "V2Ray", VersionArg = "-version", - RedirectInfo = true, }, new CoreInfo { CoreType = ECoreType.v2fly_v5, CoreExes = ["v2ray"], - Arguments = "run -c config.json -format jsonv5", - Url = Global.V2flyCoreUrl, - ReleaseApiUrl = Global.V2flyCoreUrl.Replace(Global.GithubUrl, Global.GithubApiUrl), + Arguments = "run -c {0} -format jsonv5", + Url = GetCoreUrl(ECoreType.v2fly_v5), Match = "V2Ray", VersionArg = "version", - RedirectInfo = true, }, new CoreInfo { CoreType = ECoreType.Xray, - CoreExes = ["xray", "wxray"], + CoreExes = ["wxray","xray"], Arguments = "run -c {0}", - Url = Global.XrayCoreUrl, - ReleaseApiUrl = Global.XrayCoreUrl.Replace(Global.GithubUrl, Global.GithubApiUrl), - DownloadUrlWin64 = Global.XrayCoreUrl + "/download/{0}/Xray-windows-64.zip", - DownloadUrlWinArm64 = Global.XrayCoreUrl + "/download/{0}/Xray-windows-arm64-v8a.zip", - DownloadUrlLinux64 = Global.XrayCoreUrl + "/download/{0}/Xray-linux-64.zip", - DownloadUrlLinuxArm64 = Global.XrayCoreUrl + "/download/{0}/Xray-linux-arm64-v8a.zip", - DownloadUrlOSX64 = Global.XrayCoreUrl + "/download/{0}/Xray-macos-64.zip", - DownloadUrlOSXArm64 = Global.XrayCoreUrl + "/download/{0}/Xray-macos-arm64-v8a.zip", + Url = GetCoreUrl(ECoreType.Xray), + ReleaseApiUrl = urlXray.Replace(Global.GithubUrl, Global.GithubApiUrl), + DownloadUrlWin64 = urlXray + "/download/{0}/Xray-windows-64.zip", + DownloadUrlWinArm64 = urlXray + "/download/{0}/Xray-windows-arm64-v8a.zip", + DownloadUrlLinux64 = urlXray + "/download/{0}/Xray-linux-64.zip", + DownloadUrlLinuxArm64 = urlXray + "/download/{0}/Xray-linux-arm64-v8a.zip", + DownloadUrlOSX64 = urlXray + "/download/{0}/Xray-macos-64.zip", + DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip", Match = "Xray", VersionArg = "-version", - RedirectInfo = true, }, new CoreInfo { CoreType = ECoreType.mihomo, - CoreExes = ["mihomo-windows-amd64-compatible", "mihomo-windows-amd64", "mihomo-linux-amd64", "mihomo", "clash"], - Arguments = "-f config.json" + PortableMode(), - Url = Global.MihomoCoreUrl, - ReleaseApiUrl = Global.MihomoCoreUrl.Replace(Global.GithubUrl, Global.GithubApiUrl), - DownloadUrlWin64 = Global.MihomoCoreUrl + "/download/{0}/mihomo-windows-amd64-compatible-{0}.zip", - DownloadUrlWinArm64 = Global.MihomoCoreUrl + "/download/{0}/mihomo-windows-arm64-{0}.zip", - DownloadUrlLinux64 = Global.MihomoCoreUrl + "/download/{0}/mihomo-linux-amd64-compatible-{0}.gz", - DownloadUrlLinuxArm64 = Global.MihomoCoreUrl + "/download/{0}/mihomo-linux-arm64-{0}.gz", - DownloadUrlOSX64 = Global.MihomoCoreUrl + "/download/{0}/mihomo-darwin-amd64-compatible-{0}.gz", - DownloadUrlOSXArm64 = Global.MihomoCoreUrl + "/download/{0}/mihomo-darwin-arm64-{0}.gz", + CoreExes = ["mihomo-windows-amd64-compatible", "mihomo-windows-amd64", "mihomo-linux-amd64", "clash", "mihomo"], + Arguments = "-f {0}" + PortableMode(), + Url = GetCoreUrl(ECoreType.mihomo), + ReleaseApiUrl = urlMihomo.Replace(Global.GithubUrl, Global.GithubApiUrl), + DownloadUrlWin64 = urlMihomo + "/download/{0}/mihomo-windows-amd64-compatible-{0}.zip", + DownloadUrlWinArm64 = urlMihomo + "/download/{0}/mihomo-windows-arm64-{0}.zip", + DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-compatible-{0}.gz", + DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz", + DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-compatible-{0}.gz", + DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz", Match = "Mihomo", VersionArg = "-v", - RedirectInfo = true, }, new CoreInfo { CoreType = ECoreType.hysteria, CoreExes = ["hysteria-windows-amd64", "hysteria"], - Arguments = "", - Url = Global.HysteriaCoreUrl, - ReleaseApiUrl = Global.HysteriaCoreUrl.Replace(Global.GithubUrl, Global.GithubApiUrl), - RedirectInfo = true, + Arguments = "-c {0}", + Url = GetCoreUrl(ECoreType.hysteria), }, new CoreInfo { CoreType = ECoreType.naiveproxy, - CoreExes = ["naiveproxy", "naive"], - Arguments = "config.json", - Url = Global.NaiveproxyCoreUrl, - RedirectInfo = false, + CoreExes = [ "naive", "naiveproxy"], + Arguments = "{0}", + Url = GetCoreUrl(ECoreType.naiveproxy), }, new CoreInfo { CoreType = ECoreType.tuic, CoreExes = ["tuic-client", "tuic"], - Arguments = "-c config.json", - Url = Global.TuicCoreUrl, - RedirectInfo = true, + Arguments = "-c {0}", + Url = GetCoreUrl(ECoreType.tuic), }, new CoreInfo @@ -160,15 +155,15 @@ CoreType = ECoreType.sing_box, CoreExes = ["sing-box-client", "sing-box"], Arguments = "run -c {0} --disable-color", - Url = Global.SingboxCoreUrl, - RedirectInfo = true, - ReleaseApiUrl = Global.SingboxCoreUrl.Replace(Global.GithubUrl, Global.GithubApiUrl), - DownloadUrlWin64 = Global.SingboxCoreUrl + "/download/{0}/sing-box-{1}-windows-amd64.zip", - DownloadUrlWinArm64 = Global.SingboxCoreUrl + "/download/{0}/sing-box-{1}-windows-arm64.zip", - DownloadUrlLinux64 = Global.SingboxCoreUrl + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz", - DownloadUrlLinuxArm64 = Global.SingboxCoreUrl + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz", - DownloadUrlOSX64 = Global.SingboxCoreUrl + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz", - DownloadUrlOSXArm64 = Global.SingboxCoreUrl + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz", + Url = GetCoreUrl(ECoreType.sing_box), + + ReleaseApiUrl = urlSingbox.Replace(Global.GithubUrl, Global.GithubApiUrl), + DownloadUrlWin64 = urlSingbox + "/download/{0}/sing-box-{1}-windows-amd64.zip", + DownloadUrlWinArm64 = urlSingbox + "/download/{0}/sing-box-{1}-windows-arm64.zip", + DownloadUrlLinux64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-amd64.tar.gz", + DownloadUrlLinuxArm64 = urlSingbox + "/download/{0}/sing-box-{1}-linux-arm64.tar.gz", + DownloadUrlOSX64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-amd64.tar.gz", + DownloadUrlOSXArm64 = urlSingbox + "/download/{0}/sing-box-{1}-darwin-arm64.tar.gz", Match = "sing-box", VersionArg = "version", }, @@ -177,26 +172,47 @@ { CoreType = ECoreType.juicity, CoreExes = ["juicity-client", "juicity"], - Arguments = "run -c config.json", - Url = Global.JuicityCoreUrl + Arguments = "run -c {0}", + Url = GetCoreUrl(ECoreType.juicity) }, new CoreInfo { CoreType = ECoreType.hysteria2, CoreExes = ["hysteria-windows-amd64", "hysteria-linux-amd64", "hysteria"], - Arguments = "", - Url = Global.HysteriaCoreUrl, - ReleaseApiUrl = Global.HysteriaCoreUrl.Replace(Global.GithubUrl, Global.GithubApiUrl), - RedirectInfo = true, + Arguments = "-c {0}", + Url = GetCoreUrl(ECoreType.hysteria2), + }, + + new CoreInfo + { + CoreType = ECoreType.brook, + CoreExes = ["brook_windows_amd64", "brook_linux_amd64", "brook"], + Arguments = " {0}", + Url = GetCoreUrl(ECoreType.brook), + AbsolutePath = true, + }, + + new CoreInfo + { + CoreType = ECoreType.overtls, + CoreExes = [ "overtls-bin", "overtls"], + Arguments = "-r client -c {0}", + Url = GetCoreUrl(ECoreType.overtls), + AbsolutePath = false, } ]; } - private string PortableMode() + private static string PortableMode() { return $" -d {Utils.GetBinPath("").AppendQuotes()}"; } + + private static string GetCoreUrl(ECoreType eCoreType) + { + return $"{Global.GithubUrl}/{Global.CoreUrls[eCoreType]}/releases"; + } } -} \ No newline at end of file +} diff --git a/v2rayn/v2rayN/ServiceLib/Models/CoreInfo.cs b/v2rayn/v2rayN/ServiceLib/Models/CoreInfo.cs index 1afcd0b661..c6d6113cad 100644 --- a/v2rayn/v2rayN/ServiceLib/Models/CoreInfo.cs +++ b/v2rayn/v2rayN/ServiceLib/Models/CoreInfo.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Models +namespace ServiceLib.Models { [Serializable] public class CoreInfo @@ -16,6 +16,6 @@ public string? DownloadUrlOSXArm64 { get; set; } public string? Match { get; set; } public string? VersionArg { get; set; } - public bool RedirectInfo { get; set; } + public bool AbsolutePath { get; set; } } -} \ No newline at end of file +}