diff --git a/.github/update.log b/.github/update.log
index 9a128ea608..dbbd0c2592 100644
--- a/.github/update.log
+++ b/.github/update.log
@@ -1267,3 +1267,4 @@ Update On Fri Feb 6 20:02:22 CET 2026
Update On Sat Feb 7 19:47:34 CET 2026
Update On Sun Feb 8 19:51:33 CET 2026
Update On Mon Feb 9 20:27:00 CET 2026
+Update On Tue Feb 10 20:16:46 CET 2026
diff --git a/clash-meta-android/build.gradle.kts b/clash-meta-android/build.gradle.kts
index c3f3dd8d33..1b5e1dd67e 100644
--- a/clash-meta-android/build.gradle.kts
+++ b/clash-meta-android/build.gradle.kts
@@ -81,7 +81,7 @@ subprojects {
}
}
- ndkVersion = "27.2.12479018"
+ ndkVersion = "29.0.14206865"
compileSdkVersion(defaultConfig.targetSdk!!)
diff --git a/clash-meta/adapter/outbound/reality.go b/clash-meta/adapter/outbound/reality.go
index 55d3cba9c7..5ba9a91601 100644
--- a/clash-meta/adapter/outbound/reality.go
+++ b/clash-meta/adapter/outbound/reality.go
@@ -14,7 +14,7 @@ type RealityOptions struct {
PublicKey string `proxy:"public-key"`
ShortID string `proxy:"short-id"`
- SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"`
+ SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768,omitempty"`
}
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
diff --git a/clash-meta/adapter/outbound/wireguard.go b/clash-meta/adapter/outbound/wireguard.go
index caedacab43..a26ecea00a 100644
--- a/clash-meta/adapter/outbound/wireguard.go
+++ b/clash-meta/adapter/outbound/wireguard.go
@@ -77,8 +77,8 @@ type WireGuardOption struct {
}
type WireGuardPeerOption struct {
- Server string `proxy:"server"`
- Port int `proxy:"port"`
+ Server string `proxy:"server,omitempty"`
+ Port int `proxy:"port,omitempty"`
PublicKey string `proxy:"public-key,omitempty"`
PreSharedKey string `proxy:"pre-shared-key,omitempty"`
Reserved []uint8 `proxy:"reserved,omitempty"`
diff --git a/clash-meta/adapter/outboundgroup/parser.go b/clash-meta/adapter/outboundgroup/parser.go
index 4640685594..d5ce72888e 100644
--- a/clash-meta/adapter/outboundgroup/parser.go
+++ b/clash-meta/adapter/outboundgroup/parser.go
@@ -42,10 +42,6 @@ type GroupCommonOption struct {
IncludeAllProviders bool `group:"include-all-providers,omitempty"`
Hidden bool `group:"hidden,omitempty"`
Icon string `group:"icon,omitempty"`
-
- // removed configs, only for error logging
- Interface string `group:"interface-name,omitempty"`
- RoutingMark int `group:"routing-mark,omitempty"`
}
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]P.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) {
@@ -62,12 +58,15 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
return nil, errFormat
}
- if groupOption.RoutingMark != 0 {
+ if _, ok := config["routing-mark"]; ok {
log.Errorln("The group [%s] with routing-mark configuration was removed, please set it directly on the proxy instead", groupOption.Name)
}
- if groupOption.Interface != "" {
+ if _, ok := config["interface-name"]; ok {
log.Errorln("The group [%s] with interface-name configuration was removed, please set it directly on the proxy instead", groupOption.Name)
}
+ if _, ok := config["dialer-proxy"]; ok {
+ log.Errorln("The group [%s] with dialer-proxy configuration is not allowed, please set it directly on the proxy instead", groupOption.Name)
+ }
groupName := groupOption.Name
diff --git a/clash-meta/common/structure/structure.go b/clash-meta/common/structure/structure.go
index d43dec033e..1bce151629 100644
--- a/clash-meta/common/structure/structure.go
+++ b/clash-meta/common/structure/structure.go
@@ -7,6 +7,7 @@ import (
"encoding/base64"
"fmt"
"reflect"
+ "sort"
"strconv"
"strings"
)
@@ -38,58 +39,7 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("decode must recive a ptr struct")
}
- t := reflect.TypeOf(dst).Elem()
- v := reflect.ValueOf(dst).Elem()
- for idx := 0; idx < v.NumField(); idx++ {
- field := t.Field(idx)
- if field.Anonymous {
- if err := d.decodeStruct(field.Name, src, v.Field(idx)); err != nil {
- return err
- }
- continue
- }
-
- tag := field.Tag.Get(d.option.TagName)
- key, omitKey, found := strings.Cut(tag, ",")
- omitempty := found && omitKey == "omitempty"
-
- // As a special case, if the field tag is "-", the field is always omitted.
- // Note that a field with name "-" can still be generated using the tag "-,".
- if key == "-" {
- continue
- }
-
- value, ok := src[key]
- if !ok {
- if d.option.KeyReplacer != nil {
- key = d.option.KeyReplacer.Replace(key)
- }
-
- for _strKey := range src {
- strKey := _strKey
- if d.option.KeyReplacer != nil {
- strKey = d.option.KeyReplacer.Replace(strKey)
- }
- if strings.EqualFold(key, strKey) {
- value = src[_strKey]
- ok = true
- break
- }
- }
- }
- if !ok || value == nil {
- if omitempty {
- continue
- }
- return fmt.Errorf("key '%s' missing", key)
- }
-
- err := d.decode(key, value, v.Field(idx))
- if err != nil {
- return err
- }
- }
- return nil
+ return d.decode("", src, reflect.ValueOf(dst).Elem())
}
// isNil returns true if the input is nil or a typed nil pointer.
@@ -456,6 +406,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
}
+ targetValKeysUnused := make(map[any]struct{})
errors := make([]string, 0)
// This slice will keep track of all the structs we'll be decoding.
@@ -470,6 +421,11 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field reflect.StructField
val reflect.Value
}
+
+ // remainField is set to a valid field set with the "remain" tag if
+ // we are keeping track of remaining values.
+ var remainField *field
+
var fields []field
for len(structs) > 0 {
structVal := structs[0]
@@ -479,30 +435,47 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
for i := 0; i < structType.NumField(); i++ {
fieldType := structType.Field(i)
- fieldKind := fieldType.Type.Kind()
+ fieldVal := structVal.Field(i)
+ if fieldVal.Kind() == reflect.Ptr && fieldVal.Elem().Kind() == reflect.Struct {
+ // Handle embedded struct pointers as embedded structs.
+ fieldVal = fieldVal.Elem()
+ }
// If "squash" is specified in the tag, we squash the field down.
- squash := false
+ squash := fieldVal.Kind() == reflect.Struct && fieldType.Anonymous
+ remain := false
+
+ // We always parse the tags cause we're looking for other tags too
tagParts := strings.Split(fieldType.Tag.Get(d.option.TagName), ",")
for _, tag := range tagParts[1:] {
if tag == "squash" {
squash = true
break
}
+
+ if tag == "remain" {
+ remain = true
+ break
+ }
}
if squash {
- if fieldKind != reflect.Struct {
+ if fieldVal.Kind() != reflect.Struct {
errors = append(errors,
- fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldKind).Error())
+ fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldVal.Kind()).Error())
} else {
- structs = append(structs, structVal.FieldByName(fieldType.Name))
+ structs = append(structs, fieldVal)
}
continue
}
- // Normal struct field, store it away
- fields = append(fields, field{fieldType, structVal.Field(i)})
+ // Build our field
+ if remain {
+ remainField = &field{fieldType, fieldVal}
+ } else {
+ // Normal struct field, store it away
+ fields = append(fields, field{fieldType, fieldVal})
+ }
}
}
@@ -511,8 +484,8 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field, fieldValue := f.field, f.val
fieldName := field.Name
- tagValue := field.Tag.Get(d.option.TagName)
- tagValue = strings.SplitN(tagValue, ",", 2)[0]
+ tagParts := strings.Split(field.Tag.Get(d.option.TagName), ",")
+ tagValue := tagParts[0]
if tagValue != "" {
fieldName = tagValue
}
@@ -521,6 +494,13 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
continue
}
+ omitempty := false
+ for _, tag := range tagParts[1:] {
+ if tag == "omitempty" {
+ omitempty = true
+ }
+ }
+
rawMapKey := reflect.ValueOf(fieldName)
rawMapVal := dataVal.MapIndex(rawMapKey)
if !rawMapVal.IsValid() {
@@ -548,7 +528,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
if !rawMapVal.IsValid() {
// There was no matching key in the map for the value in
- // the struct. Just ignore.
+ // the struct. Remember it for potential errors and metadata.
+ if !omitempty {
+ targetValKeysUnused[fieldName] = struct{}{}
+ }
continue
}
}
@@ -570,7 +553,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
// If the name is empty string, then we're at the root, and we
// don't dot-join the fields.
if name != "" {
- fieldName = fmt.Sprintf("%s.%s", name, fieldName)
+ fieldName = name + "." + fieldName
}
if err := d.decode(fieldName, rawMapVal.Interface(), fieldValue); err != nil {
@@ -578,6 +561,36 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
}
}
+ // If we have a "remain"-tagged field and we have unused keys then
+ // we put the unused keys directly into the remain field.
+ if remainField != nil && len(dataValKeysUnused) > 0 {
+ // Build a map of only the unused values
+ remain := map[interface{}]interface{}{}
+ for key := range dataValKeysUnused {
+ remain[key] = dataVal.MapIndex(reflect.ValueOf(key)).Interface()
+ }
+
+ // Decode it as-if we were just decoding this map onto our map.
+ if err := d.decodeMap(name, remain, remainField.val); err != nil {
+ errors = append(errors, err.Error())
+ }
+
+ // Set the map to nil so we have none so that the next check will
+ // not error (ErrorUnused)
+ dataValKeysUnused = nil
+ }
+
+ if len(targetValKeysUnused) > 0 {
+ keys := make([]string, 0, len(targetValKeysUnused))
+ for rawKey := range targetValKeysUnused {
+ keys = append(keys, rawKey.(string))
+ }
+ sort.Strings(keys)
+
+ err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", "))
+ errors = append(errors, err.Error())
+ }
+
if len(errors) > 0 {
return fmt.Errorf(strings.Join(errors, ","))
}
diff --git a/clash-meta/common/structure/structure_test.go b/clash-meta/common/structure/structure_test.go
index c79c2eb46f..1e0cbaa773 100644
--- a/clash-meta/common/structure/structure_test.go
+++ b/clash-meta/common/structure/structure_test.go
@@ -139,6 +139,49 @@ func TestStructure_Nest(t *testing.T) {
assert.Equal(t, s.BazOptional, goal)
}
+func TestStructure_DoubleNest(t *testing.T) {
+ rawMap := map[string]any{
+ "bar": map[string]any{
+ "foo": 1,
+ },
+ }
+
+ goal := BazOptional{
+ Foo: 1,
+ }
+
+ s := &struct {
+ Bar struct {
+ BazOptional
+ } `test:"bar"`
+ }{}
+ err := decoder.Decode(rawMap, s)
+ assert.Nil(t, err)
+ assert.Equal(t, s.Bar.BazOptional, goal)
+}
+
+func TestStructure_Remain(t *testing.T) {
+ rawMap := map[string]any{
+ "foo": 1,
+ "bar": "test",
+ "extra": false,
+ }
+
+ goal := &Baz{
+ Foo: 1,
+ Bar: "test",
+ }
+
+ s := &struct {
+ Baz
+ Remain map[string]any `test:",remain"`
+ }{}
+ err := decoder.Decode(rawMap, s)
+ assert.Nil(t, err)
+ assert.Equal(t, *goal, s.Baz)
+ assert.Equal(t, map[string]any{"extra": false}, s.Remain)
+}
+
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
@@ -228,6 +271,23 @@ func TestStructure_Pointer(t *testing.T) {
assert.Nil(t, s.Bar)
}
+func TestStructure_PointerStruct(t *testing.T) {
+ rawMap := map[string]any{
+ "foo": "foo",
+ }
+
+ s := &struct {
+ Foo *string `test:"foo,omitempty"`
+ Bar *Baz `test:"bar,omitempty"`
+ }{}
+
+ err := decoder.Decode(rawMap, s)
+ assert.Nil(t, err)
+ assert.NotNil(t, s.Foo)
+ assert.Equal(t, "foo", *s.Foo)
+ assert.Nil(t, s.Bar)
+}
+
type num struct {
a int
}
diff --git a/clash-meta/dns/dot.go b/clash-meta/dns/dot.go
index a4ffe4617a..fa37d86aa3 100644
--- a/clash-meta/dns/dot.go
+++ b/clash-meta/dns/dot.go
@@ -23,6 +23,7 @@ type dnsOverTLS struct {
host string
dialer *dnsDialer
skipCertVerify bool
+ disableReuse bool
access sync.Mutex
connections deque.Deque[net.Conn] // LIFO
@@ -57,11 +58,13 @@ func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, err
var conn net.Conn
isOldConn := true
- t.access.Lock()
- if t.connections.Len() > 0 {
- conn = t.connections.PopBack()
+ if !t.disableReuse {
+ t.access.Lock()
+ if t.connections.Len() > 0 {
+ conn = t.connections.PopBack()
+ }
+ t.access.Unlock()
}
- t.access.Unlock()
if conn == nil {
conn, err = t.dialContext(ctx)
@@ -90,13 +93,17 @@ func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, err
return
}
- t.access.Lock()
- if t.connections.Len() >= maxOldDotConns {
- oldConn := t.connections.PopFront()
- go oldConn.Close() // close in a new goroutine, not blocking the current task
+ if !t.disableReuse {
+ t.access.Lock()
+ if t.connections.Len() >= maxOldDotConns {
+ oldConn := t.connections.PopFront()
+ go oldConn.Close() // close in a new goroutine, not blocking the current task
+ }
+ t.connections.PushBack(conn)
+ t.access.Unlock()
+ } else {
+ _ = conn.Close()
}
- t.connections.PushBack(conn)
- t.access.Unlock()
return
}
}()
@@ -134,12 +141,14 @@ func (t *dnsOverTLS) dialContext(ctx context.Context) (net.Conn, error) {
}
func (t *dnsOverTLS) ResetConnection() {
- t.access.Lock()
- for t.connections.Len() > 0 {
- oldConn := t.connections.PopFront()
- go oldConn.Close() // close in a new goroutine, not blocking the current task
+ if !t.disableReuse {
+ t.access.Lock()
+ for t.connections.Len() > 0 {
+ oldConn := t.connections.PopFront()
+ go oldConn.Close() // close in a new goroutine, not blocking the current task
+ }
+ t.access.Unlock()
}
- t.access.Unlock()
}
func (t *dnsOverTLS) Close() error {
@@ -159,6 +168,9 @@ func newDoTClient(addr string, resolver *Resolver, params map[string]string, pro
if params["skip-cert-verify"] == "true" {
c.skipCertVerify = true
}
+ if params["disable-reuse"] == "true" {
+ c.disableReuse = true
+ }
runtime.SetFinalizer(c, (*dnsOverTLS).Close)
return c
}
diff --git a/clash-nyanpasu/backend/Cargo.lock b/clash-nyanpasu/backend/Cargo.lock
index 4c731544c7..e070f21dd0 100644
--- a/clash-nyanpasu/backend/Cargo.lock
+++ b/clash-nyanpasu/backend/Cargo.lock
@@ -216,7 +216,7 @@ dependencies = [
"itertools 0.10.5",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -374,7 +374,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -549,7 +549,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -584,7 +584,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -633,7 +633,7 @@ checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -898,7 +898,7 @@ dependencies = [
"regex",
"rustc-hash 1.1.0",
"shlex",
- "syn 2.0.111",
+ "syn 2.0.114",
"which 4.4.2",
]
@@ -1107,7 +1107,7 @@ dependencies = [
"cow-utils",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"synstructure",
]
@@ -1236,7 +1236,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -1464,9 +1464,9 @@ dependencies = [
[[package]]
name = "chrono"
-version = "0.4.42"
+version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
+checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -1528,7 +1528,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -1762,7 +1762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d"
dependencies = [
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2042,7 +2042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2052,7 +2052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2093,7 +2093,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2104,7 +2104,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2195,7 +2195,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2216,7 +2216,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2226,7 +2226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2239,7 +2239,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version 0.4.1",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2327,7 +2327,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -2388,7 +2388,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2420,7 +2420,7 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2503,7 +2503,7 @@ checksum = "1ec431cd708430d5029356535259c5d645d60edd3d39c54e5eea9782d46caa7d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2738,7 +2738,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2759,7 +2759,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2830,7 +2830,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -2951,7 +2951,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -3025,13 +3025,13 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
-version = "1.1.5"
+version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
- "libz-rs-sys",
"miniz_oxide",
+ "zlib-rs",
]
[[package]]
@@ -3118,7 +3118,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -3253,7 +3253,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -3555,7 +3555,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -3817,7 +3817,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -4124,7 +4124,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.58.0",
+ "windows-core 0.61.2",
]
[[package]]
@@ -4333,7 +4333,7 @@ dependencies = [
"proc-macro2",
"quote",
"sha2 0.10.9",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -4350,7 +4350,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
- "syn 2.0.111",
+ "syn 2.0.114",
"url",
]
@@ -4448,7 +4448,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -4867,15 +4867,6 @@ dependencies = [
"redox_syscall 0.5.17",
]
-[[package]]
-name = "libz-rs-sys"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd"
-dependencies = [
- "zlib-rs",
-]
-
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -5028,7 +5019,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -5540,7 +5531,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -5602,7 +5593,7 @@ dependencies = [
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -5671,7 +5662,7 @@ version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -6333,7 +6324,7 @@ checksum = "d4faecb54d0971f948fbc1918df69b26007e6f279a204793669542e1e8b75eb3"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -6374,7 +6365,7 @@ dependencies = [
"phf 0.13.1",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -6653,7 +6644,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -6801,7 +6792,7 @@ dependencies = [
"phf_shared 0.11.3",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"unicase",
]
@@ -6815,7 +6806,7 @@ dependencies = [
"phf_shared 0.13.1",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -6878,7 +6869,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -7050,7 +7041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -7136,7 +7127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
dependencies = [
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -7534,7 +7525,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -7806,7 +7797,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -7878,7 +7869,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -7891,7 +7882,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -7949,7 +7940,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -8071,7 +8062,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -8243,7 +8234,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -8254,7 +8245,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -8289,7 +8280,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -8351,7 +8342,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -8399,7 +8390,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -8799,7 +8790,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -8911,7 +8902,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -8954,9 +8945,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.111"
+version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
@@ -8980,7 +8971,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -9108,7 +9099,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -9228,7 +9219,7 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
- "syn 2.0.111",
+ "syn 2.0.114",
"tauri-utils",
"thiserror 2.0.18",
"time",
@@ -9246,7 +9237,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"tauri-codegen",
"tauri-utils",
]
@@ -9553,7 +9544,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -9626,7 +9617,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.1.3",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -9722,7 +9713,7 @@ checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -9768,7 +9759,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -9779,7 +9770,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -9926,7 +9917,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -9953,9 +9944,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.17"
+version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
@@ -10149,7 +10140,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -10241,7 +10232,7 @@ version = "0.2.5"
source = "git+https://github.com/Frando/tracing-test.git?rev=e81ec65#e81ec655a5ec5c4351104628b1b1ba694f80a1dc"
dependencies = [
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -10771,7 +10762,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"wasm-bindgen-shared",
]
@@ -11074,7 +11065,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -11491,7 +11482,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -11502,7 +11493,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -11513,7 +11504,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -11524,7 +11515,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -12300,7 +12291,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"synstructure",
]
@@ -12356,7 +12347,7 @@ checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"zbus-lockstep",
"zbus_xml",
"zvariant",
@@ -12371,7 +12362,7 @@ dependencies = [
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"zbus_names",
"zvariant",
"zvariant_utils",
@@ -12419,7 +12410,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -12439,7 +12430,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"synstructure",
]
@@ -12460,7 +12451,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -12493,7 +12484,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
]
[[package]]
@@ -12546,9 +12537,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
-version = "0.5.2"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
+checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c"
[[package]]
name = "zmij"
@@ -12659,7 +12650,7 @@ dependencies = [
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
- "syn 2.0.111",
+ "syn 2.0.114",
"zvariant_utils",
]
@@ -12672,6 +12663,6 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
- "syn 2.0.111",
+ "syn 2.0.114",
"winnow 0.7.13",
]
diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json
index 09f1c7297f..c48b8c229b 100644
--- a/clash-nyanpasu/frontend/nyanpasu/package.json
+++ b/clash-nyanpasu/frontend/nyanpasu/package.json
@@ -73,12 +73,12 @@
"@csstools/normalize.css": "12.1.1",
"@emotion/babel-plugin": "11.13.5",
"@emotion/react": "11.14.0",
- "@iconify/json": "2.2.437",
+ "@iconify/json": "2.2.438",
"@monaco-editor/react": "4.7.0",
"@tanstack/react-query": "5.90.20",
- "@tanstack/react-router": "1.159.4",
- "@tanstack/react-router-devtools": "1.159.4",
- "@tanstack/router-plugin": "1.159.4",
+ "@tanstack/react-router": "1.159.5",
+ "@tanstack/react-router-devtools": "1.159.5",
+ "@tanstack/router-plugin": "1.159.5",
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
"@tauri-apps/plugin-dialog": "2.6.0",
"@tauri-apps/plugin-fs": "2.4.5",
@@ -91,13 +91,13 @@
"@types/react-dom": "19.2.3",
"@types/validator": "13.15.10",
"@vitejs/plugin-legacy": "7.2.1",
- "@vitejs/plugin-react": "5.1.3",
+ "@vitejs/plugin-react": "5.1.4",
"@vitejs/plugin-react-swc": "4.2.3",
"change-case": "5.4.4",
"clsx": "2.1.1",
"core-js": "3.48.0",
"filesize": "11.0.13",
- "meta-json-schema": "1.19.19",
+ "meta-json-schema": "1.19.20",
"monaco-yaml": "5.4.0",
"nanoid": "5.1.6",
"sass-embedded": "1.97.3",
diff --git a/clash-nyanpasu/frontend/ui/package.json b/clash-nyanpasu/frontend/ui/package.json
index 57d773b659..e606b089ef 100644
--- a/clash-nyanpasu/frontend/ui/package.json
+++ b/clash-nyanpasu/frontend/ui/package.json
@@ -20,7 +20,7 @@
"@tauri-apps/api": "2.10.1",
"@types/d3": "7.4.3",
"@types/react": "19.2.13",
- "@vitejs/plugin-react": "5.1.3",
+ "@vitejs/plugin-react": "5.1.4",
"ahooks": "3.9.6",
"d3": "7.9.0",
"framer-motion": "12.33.0",
diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json
index efe4923d78..77a5d73f67 100644
--- a/clash-nyanpasu/manifest/version.json
+++ b/clash-nyanpasu/manifest/version.json
@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.20",
- "mihomo_alpha": "alpha-97f2525",
+ "mihomo_alpha": "alpha-445083b",
"clash_rs": "v0.9.4",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.4-alpha+sha.5675004"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
}
},
- "updated_at": "2026-02-08T22:48:51.230Z"
+ "updated_at": "2026-02-09T22:27:59.419Z"
}
diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml
index fe58c7d982..349c01c3c8 100644
--- a/clash-nyanpasu/pnpm-lock.yaml
+++ b/clash-nyanpasu/pnpm-lock.yaml
@@ -241,7 +241,7 @@ importers:
version: 3.13.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/router-zod-adapter':
specifier: 1.81.5
- version: 1.81.5(@tanstack/react-router@1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(zod@4.3.6)
+ version: 1.81.5(@tanstack/react-router@1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(zod@4.3.6)
'@tauri-apps/api':
specifier: 2.10.1
version: 2.10.1
@@ -346,8 +346,8 @@ importers:
specifier: 11.14.0
version: 11.14.0(@types/react@19.2.13)(react@19.2.4)
'@iconify/json':
- specifier: 2.2.437
- version: 2.2.437
+ specifier: 2.2.438
+ version: 2.2.438
'@monaco-editor/react':
specifier: 4.7.0
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -355,14 +355,14 @@ importers:
specifier: 5.90.20
version: 5.90.20(react@19.2.4)
'@tanstack/react-router':
- specifier: 1.159.4
- version: 1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ specifier: 1.159.5
+ version: 1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/react-router-devtools':
- specifier: 1.159.4
- version: 1.159.4(@tanstack/react-router@1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.159.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ specifier: 1.159.5
+ version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.159.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/router-plugin':
- specifier: 1.159.4
- version: 1.159.4(@tanstack/react-router@1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))
+ specifier: 1.159.5
+ version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))
'@tauri-apps/plugin-clipboard-manager':
specifier: 2.3.2
version: 2.3.2
@@ -400,8 +400,8 @@ importers:
specifier: 7.2.1
version: 7.2.1(terser@5.36.0)(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))
'@vitejs/plugin-react':
- specifier: 5.1.3
- version: 5.1.3(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))
+ specifier: 5.1.4
+ version: 5.1.4(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))
'@vitejs/plugin-react-swc':
specifier: 4.2.3
version: 4.2.3(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))
@@ -418,8 +418,8 @@ importers:
specifier: 11.0.13
version: 11.0.13
meta-json-schema:
- specifier: 1.19.19
- version: 1.19.19
+ specifier: 1.19.20
+ version: 1.19.20
monaco-yaml:
specifier: 5.4.0
version: 5.4.0(monaco-editor@0.55.1)
@@ -490,8 +490,8 @@ importers:
specifier: 19.2.13
version: 19.2.13
'@vitejs/plugin-react':
- specifier: 5.1.3
- version: 5.1.3(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))
+ specifier: 5.1.4
+ version: 5.1.4(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))
ahooks:
specifier: 3.9.6
version: 3.9.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -1669,8 +1669,8 @@ packages:
prettier-plugin-ember-template-tag:
optional: true
- '@iconify/json@2.2.437':
- resolution: {integrity: sha512-N/vAGFj0gSU4KQBCFlUur0Zo41KhqVFU3MlbuYcZVbQQR23aOPHgDNbrulucpsKsCnNFSTO4w/We0okJNbYhTQ==}
+ '@iconify/json@2.2.438':
+ resolution: {integrity: sha512-vDZEuUxqO9b29GBNQrblIH/sE4e1fwNuwZjm3q22w8sKv6bJnqtMw0nCDAf0RVlp+7IPPCRVkFnk6L78cXZ6cg==}
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -2924,6 +2924,9 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.2':
resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==}
+ '@rolldown/pluginutils@1.0.0-rc.3':
+ resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
+
'@rollup/pluginutils@4.2.1':
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
engines: {node: '>= 8.0.0'}
@@ -3369,11 +3372,11 @@ packages:
peerDependencies:
react: ^18 || ^19
- '@tanstack/react-router-devtools@1.159.4':
- resolution: {integrity: sha512-7HXV4b5WZMdWoP6HD+mURh4mq1ssRg0dfcVYx+AzhaLboFzy4LyzdUtMpmNgRFgz3mBXLBoo+gMbKSjKlmsZmw==}
+ '@tanstack/react-router-devtools@1.159.5':
+ resolution: {integrity: sha512-IIyomu+ypWTxyoYT32mxamVmdTs7ZCGcTbdj7HVvtD3xp1lvo/bwRXj9oERENmb+OAPOaWF2doRYC/pmKjK5vg==}
engines: {node: '>=12'}
peerDependencies:
- '@tanstack/react-router': ^1.159.4
+ '@tanstack/react-router': ^1.159.5
'@tanstack/router-core': ^1.159.4
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
@@ -3381,8 +3384,8 @@ packages:
'@tanstack/router-core':
optional: true
- '@tanstack/react-router@1.159.4':
- resolution: {integrity: sha512-z3DhNkRh/joky5b+X4jEYOn9q4Jieie6mVFP62wgwM9pVlNRYh6aIroiU95ZyOwDXDijItVEZtvHuipbLHy4jw==}
+ '@tanstack/react-router@1.159.5':
+ resolution: {integrity: sha512-rVb0MtKzP5c0BkWIoFgWBiRAJHYSU3bhsEHbT0cRdRLmlJiw21Awb6VEjgYq3hJiEhowcKKm6J8AdRD/8oZ5dQ==}
engines: {node: '>=12'}
peerDependencies:
react: '>=18.0.0 || >=19.0.0'
@@ -3431,12 +3434,12 @@ packages:
resolution: {integrity: sha512-O8tICQoSuvK6vs3mvBdI3zVLFmYfj/AYDCX0a5msSADP/2S0GsgDDTB5ah731TqYCtjeNriaWz9iqst38cjF/Q==}
engines: {node: '>=12'}
- '@tanstack/router-plugin@1.159.4':
- resolution: {integrity: sha512-xXLUPwIf1Y+VGrpryHZYoJoG7V5evxTkmP64CYm6JEJGTb3hai/syhZb69iVQYb4f4IR5LqEL7VgagnlekdAWw==}
+ '@tanstack/router-plugin@1.159.5':
+ resolution: {integrity: sha512-i2LR3WRaBOAZ1Uab5QBG9UxZIRJ3V56JVu890NysbuX15rgzRiL5yLAbfenOHdhaHy2+4joX35VICAHuVWy7Og==}
engines: {node: '>=12'}
peerDependencies:
'@rsbuild/core': '>=1.0.2'
- '@tanstack/react-router': ^1.159.4
+ '@tanstack/react-router': ^1.159.5
vite: '>=5.0.0 || >=6.0.0 || >=7.0.0'
vite-plugin-solid: ^2.11.10
webpack: '>=5.92.0'
@@ -3985,8 +3988,8 @@ packages:
peerDependencies:
vite: ^4 || ^5 || ^6 || ^7
- '@vitejs/plugin-react@5.1.3':
- resolution: {integrity: sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==}
+ '@vitejs/plugin-react@5.1.4':
+ resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
@@ -5703,8 +5706,8 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
- meta-json-schema@1.19.19:
- resolution: {integrity: sha512-4lG3uN/h82lFH3pvpyl1FcVGP9UEiyra2y21yDKGzO6ExFggNLZeugCmv5Qo+UP01+Lc7RJ1EAJfXiMeFVS05Q==}
+ meta-json-schema@1.19.20:
+ resolution: {integrity: sha512-9jnByDumPpxyIZL/uB1XPa7HzLSNXYXn9pz88MqWYnfEMVBuoOfhiPkGAUeaa57vdlFjqPGPlukioYWq174BZg==}
engines: {node: '>=18', pnpm: '>=9'}
micromark-core-commonmark@2.0.1:
@@ -6814,11 +6817,6 @@ packages:
peerDependencies:
kysely: '*'
- srvx@0.11.2:
- resolution: {integrity: sha512-u6NbjE84IJwm1XUnJ53WqylLTQ3BdWRw03lcjBNNeMBD+EFjkl0Cnw1RVaGSqRAo38pOHOPXJH30M6cuTINUxw==}
- engines: {node: '>=20.16.0'}
- hasBin: true
-
stack-generator@2.0.10:
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
@@ -8782,7 +8780,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@iconify/json@2.2.437':
+ '@iconify/json@2.2.438':
dependencies:
'@iconify/types': 2.0.0
pathe: 2.0.3
@@ -9992,6 +9990,8 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.2': {}
+ '@rolldown/pluginutils@1.0.0-rc.3': {}
+
'@rollup/pluginutils@4.2.1':
dependencies:
estree-walker: 2.0.2
@@ -10354,9 +10354,9 @@ snapshots:
'@tanstack/query-core': 5.90.20
react: 19.2.4
- '@tanstack/react-router-devtools@1.159.4(@tanstack/react-router@1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.159.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@tanstack/react-router-devtools@1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.159.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
- '@tanstack/react-router': 1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@tanstack/react-router': 1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tanstack/router-devtools-core': 1.159.4(@tanstack/router-core@1.159.4)(csstype@3.2.3)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
@@ -10365,7 +10365,7 @@ snapshots:
transitivePeerDependencies:
- csstype
- '@tanstack/react-router@1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@tanstack/react-router@1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@tanstack/history': 1.154.14
'@tanstack/react-store': 0.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -10373,7 +10373,6 @@ snapshots:
isbot: 5.1.28
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
- srvx: 0.11.2
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
@@ -10434,7 +10433,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@tanstack/router-plugin@1.159.4(@tanstack/react-router@1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))':
+ '@tanstack/router-plugin@1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0)
@@ -10450,7 +10449,7 @@ snapshots:
unplugin: 2.3.11
zod: 3.25.76
optionalDependencies:
- '@tanstack/react-router': 1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@tanstack/react-router': 1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
vite: 7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
@@ -10469,9 +10468,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(zod@4.3.6)':
+ '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(zod@4.3.6)':
dependencies:
- '@tanstack/react-router': 1.159.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@tanstack/react-router': 1.159.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
zod: 4.3.6
'@tanstack/store@0.8.0': {}
@@ -11061,12 +11060,12 @@ snapshots:
transitivePeerDependencies:
- '@swc/helpers'
- '@vitejs/plugin-react@5.1.3(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))':
+ '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
- '@rolldown/pluginutils': 1.0.0-rc.2
+ '@rolldown/pluginutils': 1.0.0-rc.3
'@types/babel__core': 7.20.5
react-refresh: 0.18.0
vite: 7.3.1(@types/node@24.10.12)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.1)
@@ -12783,7 +12782,7 @@ snapshots:
merge2@1.4.1: {}
- meta-json-schema@1.19.19: {}
+ meta-json-schema@1.19.20: {}
micromark-core-commonmark@2.0.1:
dependencies:
@@ -13936,8 +13935,6 @@ snapshots:
'@sqlite.org/sqlite-wasm': 3.48.0-build4
kysely: 0.27.6
- srvx@0.11.2: {}
-
stack-generator@2.0.10:
dependencies:
stackframe: 1.3.4
diff --git a/mihomo/adapter/outbound/reality.go b/mihomo/adapter/outbound/reality.go
index 55d3cba9c7..5ba9a91601 100644
--- a/mihomo/adapter/outbound/reality.go
+++ b/mihomo/adapter/outbound/reality.go
@@ -14,7 +14,7 @@ type RealityOptions struct {
PublicKey string `proxy:"public-key"`
ShortID string `proxy:"short-id"`
- SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"`
+ SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768,omitempty"`
}
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
diff --git a/mihomo/adapter/outbound/wireguard.go b/mihomo/adapter/outbound/wireguard.go
index caedacab43..a26ecea00a 100644
--- a/mihomo/adapter/outbound/wireguard.go
+++ b/mihomo/adapter/outbound/wireguard.go
@@ -77,8 +77,8 @@ type WireGuardOption struct {
}
type WireGuardPeerOption struct {
- Server string `proxy:"server"`
- Port int `proxy:"port"`
+ Server string `proxy:"server,omitempty"`
+ Port int `proxy:"port,omitempty"`
PublicKey string `proxy:"public-key,omitempty"`
PreSharedKey string `proxy:"pre-shared-key,omitempty"`
Reserved []uint8 `proxy:"reserved,omitempty"`
diff --git a/mihomo/adapter/outboundgroup/parser.go b/mihomo/adapter/outboundgroup/parser.go
index 4640685594..d5ce72888e 100644
--- a/mihomo/adapter/outboundgroup/parser.go
+++ b/mihomo/adapter/outboundgroup/parser.go
@@ -42,10 +42,6 @@ type GroupCommonOption struct {
IncludeAllProviders bool `group:"include-all-providers,omitempty"`
Hidden bool `group:"hidden,omitempty"`
Icon string `group:"icon,omitempty"`
-
- // removed configs, only for error logging
- Interface string `group:"interface-name,omitempty"`
- RoutingMark int `group:"routing-mark,omitempty"`
}
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]P.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) {
@@ -62,12 +58,15 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
return nil, errFormat
}
- if groupOption.RoutingMark != 0 {
+ if _, ok := config["routing-mark"]; ok {
log.Errorln("The group [%s] with routing-mark configuration was removed, please set it directly on the proxy instead", groupOption.Name)
}
- if groupOption.Interface != "" {
+ if _, ok := config["interface-name"]; ok {
log.Errorln("The group [%s] with interface-name configuration was removed, please set it directly on the proxy instead", groupOption.Name)
}
+ if _, ok := config["dialer-proxy"]; ok {
+ log.Errorln("The group [%s] with dialer-proxy configuration is not allowed, please set it directly on the proxy instead", groupOption.Name)
+ }
groupName := groupOption.Name
diff --git a/mihomo/common/structure/structure.go b/mihomo/common/structure/structure.go
index d43dec033e..1bce151629 100644
--- a/mihomo/common/structure/structure.go
+++ b/mihomo/common/structure/structure.go
@@ -7,6 +7,7 @@ import (
"encoding/base64"
"fmt"
"reflect"
+ "sort"
"strconv"
"strings"
)
@@ -38,58 +39,7 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("decode must recive a ptr struct")
}
- t := reflect.TypeOf(dst).Elem()
- v := reflect.ValueOf(dst).Elem()
- for idx := 0; idx < v.NumField(); idx++ {
- field := t.Field(idx)
- if field.Anonymous {
- if err := d.decodeStruct(field.Name, src, v.Field(idx)); err != nil {
- return err
- }
- continue
- }
-
- tag := field.Tag.Get(d.option.TagName)
- key, omitKey, found := strings.Cut(tag, ",")
- omitempty := found && omitKey == "omitempty"
-
- // As a special case, if the field tag is "-", the field is always omitted.
- // Note that a field with name "-" can still be generated using the tag "-,".
- if key == "-" {
- continue
- }
-
- value, ok := src[key]
- if !ok {
- if d.option.KeyReplacer != nil {
- key = d.option.KeyReplacer.Replace(key)
- }
-
- for _strKey := range src {
- strKey := _strKey
- if d.option.KeyReplacer != nil {
- strKey = d.option.KeyReplacer.Replace(strKey)
- }
- if strings.EqualFold(key, strKey) {
- value = src[_strKey]
- ok = true
- break
- }
- }
- }
- if !ok || value == nil {
- if omitempty {
- continue
- }
- return fmt.Errorf("key '%s' missing", key)
- }
-
- err := d.decode(key, value, v.Field(idx))
- if err != nil {
- return err
- }
- }
- return nil
+ return d.decode("", src, reflect.ValueOf(dst).Elem())
}
// isNil returns true if the input is nil or a typed nil pointer.
@@ -456,6 +406,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
}
+ targetValKeysUnused := make(map[any]struct{})
errors := make([]string, 0)
// This slice will keep track of all the structs we'll be decoding.
@@ -470,6 +421,11 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field reflect.StructField
val reflect.Value
}
+
+ // remainField is set to a valid field set with the "remain" tag if
+ // we are keeping track of remaining values.
+ var remainField *field
+
var fields []field
for len(structs) > 0 {
structVal := structs[0]
@@ -479,30 +435,47 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
for i := 0; i < structType.NumField(); i++ {
fieldType := structType.Field(i)
- fieldKind := fieldType.Type.Kind()
+ fieldVal := structVal.Field(i)
+ if fieldVal.Kind() == reflect.Ptr && fieldVal.Elem().Kind() == reflect.Struct {
+ // Handle embedded struct pointers as embedded structs.
+ fieldVal = fieldVal.Elem()
+ }
// If "squash" is specified in the tag, we squash the field down.
- squash := false
+ squash := fieldVal.Kind() == reflect.Struct && fieldType.Anonymous
+ remain := false
+
+ // We always parse the tags cause we're looking for other tags too
tagParts := strings.Split(fieldType.Tag.Get(d.option.TagName), ",")
for _, tag := range tagParts[1:] {
if tag == "squash" {
squash = true
break
}
+
+ if tag == "remain" {
+ remain = true
+ break
+ }
}
if squash {
- if fieldKind != reflect.Struct {
+ if fieldVal.Kind() != reflect.Struct {
errors = append(errors,
- fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldKind).Error())
+ fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldVal.Kind()).Error())
} else {
- structs = append(structs, structVal.FieldByName(fieldType.Name))
+ structs = append(structs, fieldVal)
}
continue
}
- // Normal struct field, store it away
- fields = append(fields, field{fieldType, structVal.Field(i)})
+ // Build our field
+ if remain {
+ remainField = &field{fieldType, fieldVal}
+ } else {
+ // Normal struct field, store it away
+ fields = append(fields, field{fieldType, fieldVal})
+ }
}
}
@@ -511,8 +484,8 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field, fieldValue := f.field, f.val
fieldName := field.Name
- tagValue := field.Tag.Get(d.option.TagName)
- tagValue = strings.SplitN(tagValue, ",", 2)[0]
+ tagParts := strings.Split(field.Tag.Get(d.option.TagName), ",")
+ tagValue := tagParts[0]
if tagValue != "" {
fieldName = tagValue
}
@@ -521,6 +494,13 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
continue
}
+ omitempty := false
+ for _, tag := range tagParts[1:] {
+ if tag == "omitempty" {
+ omitempty = true
+ }
+ }
+
rawMapKey := reflect.ValueOf(fieldName)
rawMapVal := dataVal.MapIndex(rawMapKey)
if !rawMapVal.IsValid() {
@@ -548,7 +528,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
if !rawMapVal.IsValid() {
// There was no matching key in the map for the value in
- // the struct. Just ignore.
+ // the struct. Remember it for potential errors and metadata.
+ if !omitempty {
+ targetValKeysUnused[fieldName] = struct{}{}
+ }
continue
}
}
@@ -570,7 +553,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
// If the name is empty string, then we're at the root, and we
// don't dot-join the fields.
if name != "" {
- fieldName = fmt.Sprintf("%s.%s", name, fieldName)
+ fieldName = name + "." + fieldName
}
if err := d.decode(fieldName, rawMapVal.Interface(), fieldValue); err != nil {
@@ -578,6 +561,36 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
}
}
+ // If we have a "remain"-tagged field and we have unused keys then
+ // we put the unused keys directly into the remain field.
+ if remainField != nil && len(dataValKeysUnused) > 0 {
+ // Build a map of only the unused values
+ remain := map[interface{}]interface{}{}
+ for key := range dataValKeysUnused {
+ remain[key] = dataVal.MapIndex(reflect.ValueOf(key)).Interface()
+ }
+
+ // Decode it as-if we were just decoding this map onto our map.
+ if err := d.decodeMap(name, remain, remainField.val); err != nil {
+ errors = append(errors, err.Error())
+ }
+
+ // Set the map to nil so we have none so that the next check will
+ // not error (ErrorUnused)
+ dataValKeysUnused = nil
+ }
+
+ if len(targetValKeysUnused) > 0 {
+ keys := make([]string, 0, len(targetValKeysUnused))
+ for rawKey := range targetValKeysUnused {
+ keys = append(keys, rawKey.(string))
+ }
+ sort.Strings(keys)
+
+ err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", "))
+ errors = append(errors, err.Error())
+ }
+
if len(errors) > 0 {
return fmt.Errorf(strings.Join(errors, ","))
}
diff --git a/mihomo/common/structure/structure_test.go b/mihomo/common/structure/structure_test.go
index c79c2eb46f..1e0cbaa773 100644
--- a/mihomo/common/structure/structure_test.go
+++ b/mihomo/common/structure/structure_test.go
@@ -139,6 +139,49 @@ func TestStructure_Nest(t *testing.T) {
assert.Equal(t, s.BazOptional, goal)
}
+func TestStructure_DoubleNest(t *testing.T) {
+ rawMap := map[string]any{
+ "bar": map[string]any{
+ "foo": 1,
+ },
+ }
+
+ goal := BazOptional{
+ Foo: 1,
+ }
+
+ s := &struct {
+ Bar struct {
+ BazOptional
+ } `test:"bar"`
+ }{}
+ err := decoder.Decode(rawMap, s)
+ assert.Nil(t, err)
+ assert.Equal(t, s.Bar.BazOptional, goal)
+}
+
+func TestStructure_Remain(t *testing.T) {
+ rawMap := map[string]any{
+ "foo": 1,
+ "bar": "test",
+ "extra": false,
+ }
+
+ goal := &Baz{
+ Foo: 1,
+ Bar: "test",
+ }
+
+ s := &struct {
+ Baz
+ Remain map[string]any `test:",remain"`
+ }{}
+ err := decoder.Decode(rawMap, s)
+ assert.Nil(t, err)
+ assert.Equal(t, *goal, s.Baz)
+ assert.Equal(t, map[string]any{"extra": false}, s.Remain)
+}
+
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
@@ -228,6 +271,23 @@ func TestStructure_Pointer(t *testing.T) {
assert.Nil(t, s.Bar)
}
+func TestStructure_PointerStruct(t *testing.T) {
+ rawMap := map[string]any{
+ "foo": "foo",
+ }
+
+ s := &struct {
+ Foo *string `test:"foo,omitempty"`
+ Bar *Baz `test:"bar,omitempty"`
+ }{}
+
+ err := decoder.Decode(rawMap, s)
+ assert.Nil(t, err)
+ assert.NotNil(t, s.Foo)
+ assert.Equal(t, "foo", *s.Foo)
+ assert.Nil(t, s.Bar)
+}
+
type num struct {
a int
}
diff --git a/mihomo/dns/dot.go b/mihomo/dns/dot.go
index a4ffe4617a..fa37d86aa3 100644
--- a/mihomo/dns/dot.go
+++ b/mihomo/dns/dot.go
@@ -23,6 +23,7 @@ type dnsOverTLS struct {
host string
dialer *dnsDialer
skipCertVerify bool
+ disableReuse bool
access sync.Mutex
connections deque.Deque[net.Conn] // LIFO
@@ -57,11 +58,13 @@ func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, err
var conn net.Conn
isOldConn := true
- t.access.Lock()
- if t.connections.Len() > 0 {
- conn = t.connections.PopBack()
+ if !t.disableReuse {
+ t.access.Lock()
+ if t.connections.Len() > 0 {
+ conn = t.connections.PopBack()
+ }
+ t.access.Unlock()
}
- t.access.Unlock()
if conn == nil {
conn, err = t.dialContext(ctx)
@@ -90,13 +93,17 @@ func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, err
return
}
- t.access.Lock()
- if t.connections.Len() >= maxOldDotConns {
- oldConn := t.connections.PopFront()
- go oldConn.Close() // close in a new goroutine, not blocking the current task
+ if !t.disableReuse {
+ t.access.Lock()
+ if t.connections.Len() >= maxOldDotConns {
+ oldConn := t.connections.PopFront()
+ go oldConn.Close() // close in a new goroutine, not blocking the current task
+ }
+ t.connections.PushBack(conn)
+ t.access.Unlock()
+ } else {
+ _ = conn.Close()
}
- t.connections.PushBack(conn)
- t.access.Unlock()
return
}
}()
@@ -134,12 +141,14 @@ func (t *dnsOverTLS) dialContext(ctx context.Context) (net.Conn, error) {
}
func (t *dnsOverTLS) ResetConnection() {
- t.access.Lock()
- for t.connections.Len() > 0 {
- oldConn := t.connections.PopFront()
- go oldConn.Close() // close in a new goroutine, not blocking the current task
+ if !t.disableReuse {
+ t.access.Lock()
+ for t.connections.Len() > 0 {
+ oldConn := t.connections.PopFront()
+ go oldConn.Close() // close in a new goroutine, not blocking the current task
+ }
+ t.access.Unlock()
}
- t.access.Unlock()
}
func (t *dnsOverTLS) Close() error {
@@ -159,6 +168,9 @@ func newDoTClient(addr string, resolver *Resolver, params map[string]string, pro
if params["skip-cert-verify"] == "true" {
c.skipCertVerify = true
}
+ if params["disable-reuse"] == "true" {
+ c.disableReuse = true
+ }
runtime.SetFinalizer(c, (*dnsOverTLS).Close)
return c
}
diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/include/shunt_options.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/include/shunt_options.lua
index 9d571a295f..6101088594 100644
--- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/include/shunt_options.lua
+++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/include/shunt_options.lua
@@ -87,13 +87,6 @@ if data.node.type == "Xray" then
o:value("linear")
end
-o = add_option(Flag, "preproxy_enabled", translate("Preproxy") .. " " .. translate("Main switch"))
-
-main_node = add_option(ListValue, "main_node", string.format('%s', translate("Preproxy Node")), translate("Set the node to be used as a pre-proxy. Each rule (including Default) has a separate switch that controls whether this rule uses the pre-proxy or not."))
-add_depends(main_node, {["preproxy_enabled"] = true})
-main_node.template = appname .. "/cbi/nodes_listvalue"
-main_node.group = {}
-
o = add_option(Flag, "fakedns", 'FakeDNS' .. " " .. translate("Main switch"), translate("Use FakeDNS work in the domain that proxy.") .. "
" ..
translate("Suitable scenarios for let the node servers get the target domain names.") .. "
" ..
translate("Such as: DNS unlocking of streaming media, reducing DNS query latency, etc."))
@@ -160,73 +153,70 @@ o.remove = function(self, section)
return m:del(current_node_id, shunt_rules[section]["_fakedns_option"])
end
-o = s2:option(ListValue, "_proxy_tag", string.format('%s', translate("Preproxy")))
---TODO Choose any node as a pre-proxy. Instead of main node.
-o.template = appname .. "/cbi/nodes_listvalue"
-o.group = {"",""}
-o:value("", translate("Close (Not use)"))
-o:value("main", translate("Use preproxy node"))
-o.cfgvalue = function(self, section)
+proxy_tag_node = s2:option(ListValue, "_proxy_tag", string.format('%s',
+ translate("Set the node to be used as a pre-proxy.") .. "\n" .. translate("Each rule has a separate switch that controls whether this rule uses the pre-proxy or not."),
+ translate("Preproxy")))
+proxy_tag_node.template = appname .. "/cbi/nodes_listvalue"
+proxy_tag_node.group = {""}
+proxy_tag_node:value("", translate("Close (Not use)"))
+proxy_tag_node.cfgvalue = function(self, section)
return m:get(current_node_id, shunt_rules[section]["_proxy_tag_option"])
end
-o.write = function(self, section, value)
+proxy_tag_node.write = function(self, section, value)
return m:set(current_node_id, shunt_rules[section]["_proxy_tag_option"], value)
end
-o.remove = function(self, section)
+proxy_tag_node.remove = function(self, section)
return m:del(current_node_id, shunt_rules[section]["_proxy_tag_option"])
end
if data.socks_list then
for k, v in pairs(data.socks_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
if data.urltest_list then
for k, v in pairs(data.urltest_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
if data.balancing_list then
for k, v in pairs(data.balancing_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
if data.iface_list then
for k, v in pairs(data.iface_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
if data.normal_list then
for k, v in pairs(data.normal_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
- end
-end
-if #main_node.keylist > 0 then
- main_node.default = main_node.keylist[1]
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+ end
end
local footer = Template(appname .. "/include/shunt_options")
footer.api = api
footer.id = current_node_id
+footer.normal_list = api.jsonc.stringify(data.normal_list or {})
m:append(footer)
diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua
index 48614232fb..ed52e5153f 100644
--- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua
+++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua
@@ -750,12 +750,22 @@ o2:depends({ [_n("chain_proxy")] = "2" })
o2.template = appname .. "/cbi/nodes_listvalue"
o2.group = {}
-for k, v in pairs(nodes_list) do
- if v.id ~= arg[1] and (not v.chain_proxy or v.chain_proxy == "") then
- o1:value(v.id, v.remark)
- o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
- o2:value(v.id, v.remark)
- o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+for k, v in pairs(socks_list) do
+ o1:value(v.id, v.remark)
+ o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+end
+
+for k, e in ipairs(api.get_valid_nodes()) do
+ if e[".name"] ~= arg[1] then
+ if e.protocol ~= "_shunt" and e.protocol ~= "_iface" then
+ o1:value(e[".name"], e["remark"])
+ o1.group[#o1.group+1] = (e["group"] and e["group"] ~= "") and e["group"] or translate("default")
+ end
+ if not e.protocol:find("_") then
+ -- Landing Node not support use special node.
+ o2:value(e[".name"], e["remark"])
+ o2.group[#o2.group+1] = (e["group"] and e["group"] ~= "") and e["group"] or translate("default")
+ end
end
end
diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua
index a8aca8030a..9c472c0d72 100644
--- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua
+++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua
@@ -794,12 +794,22 @@ o2:depends({ [_n("chain_proxy")] = "2" })
o2.template = appname .. "/cbi/nodes_listvalue"
o2.group = {}
-for k, v in pairs(nodes_list) do
- if v.id ~= arg[1] and (not v.chain_proxy or v.chain_proxy == "") then
- o1:value(v.id, v.remark)
- o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
- o2:value(v.id, v.remark)
- o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+for k, v in pairs(socks_list) do
+ o1:value(v.id, v.remark)
+ o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+end
+
+for k, e in ipairs(api.get_valid_nodes()) do
+ if e[".name"] ~= arg[1] then
+ if e.protocol ~= "_shunt" and e.protocol ~= "_iface" then
+ o1:value(e[".name"], e["remark"])
+ o1.group[#o1.group+1] = (e["group"] and e["group"] ~= "") and e["group"] or translate("default")
+ end
+ if not e.protocol:find("_") then
+ -- Landing Node not support use special node.
+ o2:value(e[".name"], e["remark"])
+ o2.group[#o2.group+1] = (e["group"] and e["group"] ~= "") and e["group"] or translate("default")
+ end
end
end
diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua
index 8ddb82dfe8..3faacb8076 100644
--- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua
+++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/api.lua
@@ -21,8 +21,6 @@ LOG_FILE = "/tmp/log/" .. appname .. ".log"
TMP_PATH = "/tmp/etc/" .. appname
TMP_IFACE_PATH = TMP_PATH .. "/iface"
-NEW_PORT = nil
-
function log(...)
local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
local f, err = io.open(LOG_FILE, "a")
@@ -98,12 +96,7 @@ end
function get_new_port()
local cmd_format = ". /usr/share/passwall/utils.sh ; echo -n $(get_new_port %s tcp,udp)"
- local set_port = 0
- if NEW_PORT and tonumber(NEW_PORT) then
- set_port = tonumber(NEW_PORT) + 1
- end
- NEW_PORT = tonumber(sys.exec(string.format(cmd_format, set_port == 0 and "auto" or set_port)))
- return NEW_PORT
+ return tonumber(sys.exec(string.format(cmd_format, "auto")))
end
function exec_call(cmd)
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 365e819eff..db0392e059 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
@@ -1087,9 +1087,10 @@ function gen_config(var)
local socks_node = uci:get_all(appname, socks_id) or nil
if socks_node then
if not remarks then
- remarks = "Socks_" .. socks_node.port
+ remarks = socks_node.port
end
result = {
+ [".name"] = "Socksid_" .. socks_id,
remarks = remarks,
type = "sing-box",
protocol = "socks",
@@ -1127,8 +1128,17 @@ function gen_config(var)
end
return nodes
end
+
+ function get_node_by_id(node_id)
+ if not node_id or node_id == "" or node_id == "nil" then return nil end
+ if node_id:find("Socks_") then
+ return gen_socks_config_node(node_id)
+ else
+ return uci:get_all(appname, node_id)
+ end
+ end
- function gen_urltest(_node)
+ function gen_urltest_outbound(_node)
local urltest_id = _node[".name"]
local urltest_tag = "urltest-" .. urltest_id
-- existing urltest
@@ -1158,21 +1168,9 @@ function gen_config(var)
end
end
if is_new_ut_node then
- local ut_node
- if ut_node_id:find("Socks_") then
- ut_node = gen_socks_config_node(ut_node_id)
- else
- ut_node = uci:get_all(appname, ut_node_id)
- end
- if ut_node then
- local outbound = gen_outbound(flag, ut_node, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- if ut_node.remarks then
- outbound.tag = outbound.tag .. ":" .. ut_node.remarks
- end
- table.insert(outbounds, outbound)
- valid_nodes[#valid_nodes + 1] = outbound.tag
- end
+ local outboundTag = gen_outbound_get_tag(flag, ut_node_id, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
+ if outboundTag then
+ valid_nodes[#valid_nodes + 1] = outboundTag
end
end
end
@@ -1187,8 +1185,7 @@ function gen_config(var)
idle_timeout = (api.format_go_time(_node.urltest_idle_timeout) ~= "0s") and api.format_go_time(_node.urltest_idle_timeout) or "30m",
interrupt_exist_connections = (_node.urltest_interrupt_exist_connections == "true" or _node.urltest_interrupt_exist_connections == "1") and true or false
}
- table.insert(outbounds, outbound)
- return urltest_tag
+ return outbound
end
function set_outbound_detour(node, outbound, outbounds_table, shunt_rule_name)
@@ -1222,9 +1219,16 @@ function gen_config(var)
if outbound["_flag_proxy_tag"] then
--Ignore
else
- local preproxy_node = uci:get_all(appname, node.preproxy_node)
+ local preproxy_node = get_node_by_id(node.preproxy_node)
if preproxy_node then
- local preproxy_outbound = gen_outbound(node[".name"], preproxy_node)
+ local preproxy_outbound
+ if preproxy_node.protocol == "_urltest" then
+ if preproxy_node.urltest_node then
+ preproxy_outbound = gen_urltest(preproxy_node)
+ end
+ else
+ preproxy_outbound = gen_outbound(node[".name"], preproxy_node)
+ end
if preproxy_outbound then
preproxy_outbound.tag = preproxy_node[".name"]
if preproxy_node.remarks then
@@ -1239,7 +1243,13 @@ function gen_config(var)
end
end
if node.chain_proxy == "2" and node.to_node then
- local to_node = uci:get_all(appname, node.to_node)
+ local to_node = get_node_by_id(node.to_node)
+ if to_node then
+ -- Landing Node not support use special node.
+ if to_node.protocol:find("_") then
+ to_node = nil
+ end
+ end
if to_node then
local to_outbound
if to_node.type ~= "sing-box" then
@@ -1290,122 +1300,90 @@ function gen_config(var)
return default_outTag, last_insert_outbound
end
+ function gen_outbound_get_tag(flag, node_id, tag, proxy_table)
+ if not node_id or node_id == "nil" then return nil end
+ local node
+ if type(node_id) == "string" then
+ node = get_node_by_id(node_id)
+ elseif type(node_id) == "table" then
+ node = node_id
+ end
+ if node then
+ if node.protocol == "_iface" then
+ if node.iface then
+ local outbound = {
+ tag = tag,
+ type = "direct",
+ bind_interface = node.iface,
+ routing_mark = 255,
+ }
+ table.insert(outbounds, outbound)
+ sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
+ return outbound.tag
+ end
+ return nil
+ end
+ if proxy_table.chain_proxy == "1" or proxy_table.chain_proxy == "2" then
+ node.chain_proxy = proxy_table.chain_proxy
+ node.preproxy_node = proxy_table.chain_proxy == "1" and proxy_table.preproxy_node
+ node.to_node = proxy_table.chain_proxy == "2" and proxy_table.to_node
+ proxy_table.chain_proxy = nil
+ proxy_table.preproxy_node = nil
+ proxy_table.to_node = nil
+ end
+ local outbound
+ if node.protocol == "_urltest" then
+ if node.urltest_node then
+ outbound = gen_urltest_outbound(node)
+ end
+ else
+ outbound = gen_outbound(flag, node, tag, proxy_table)
+ end
+ if outbound then
+ if node.remarks then
+ outbound.tag = outbound.tag .. ":" .. node.remarks
+ end
+ local default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
+ table.insert(outbounds, outbound)
+ if last_insert_outbound then
+ table.insert(outbounds, last_insert_outbound)
+ end
+ return default_outbound_tag
+ end
+ end
+ end
+
rules = {}
if node.protocol == "_shunt" then
- local preproxy_rule_name = node.preproxy_enabled == "1" and "main" or nil
- local preproxy_tag = preproxy_rule_name
- local preproxy_node_id = preproxy_rule_name and node["main_node"] or nil
-
inner_fakedns = node.fakedns or "0"
local function gen_shunt_node(rule_name, _node_id)
if not rule_name then return nil, nil end
if not _node_id then _node_id = node[rule_name] end
- local rule_outboundTag
if _node_id == "_direct" then
- rule_outboundTag = "direct"
+ return "direct"
elseif _node_id == "_blackhole" then
- rule_outboundTag = "block"
+ return "block"
elseif _node_id == "_default" and rule_name ~= "default" then
- rule_outboundTag = "default"
- elseif _node_id and _node_id:find("Socks_") then
- local socks_node = gen_socks_config_node(_node_id)
- local _outbound = gen_outbound(flag, socks_node, rule_name)
- if _outbound then
- table.insert(outbounds, _outbound)
- rule_outboundTag = _outbound.tag
- end
+ return "default"
elseif _node_id then
- local _node = uci:get_all(appname, _node_id)
- if not _node then return nil, nil end
-
- if api.is_normal_node(_node) then
- local use_proxy = preproxy_tag and node[rule_name .. "_proxy_tag"] == preproxy_rule_name and _node_id ~= preproxy_node_id
- local copied_outbound
- for index, value in ipairs(outbounds) do
- if value["_id"] == _node_id and value["_flag_proxy_tag"] == (use_proxy and preproxy_tag or nil) then
- copied_outbound = api.clone(value)
- break
- end
- end
- if copied_outbound then
- copied_outbound.tag = rule_name .. ":" .. _node.remarks
- table.insert(outbounds, copied_outbound)
- rule_outboundTag = copied_outbound.tag
- else
- if use_proxy then
- local pre_proxy = nil
- if _node.type ~= "sing-box" then
- pre_proxy = true
- end
- if pre_proxy then
- local new_port = api.get_new_port()
- table.insert(inbounds, {
- type = "direct",
- tag = "proxy_" .. rule_name,
- listen = "127.0.0.1",
- listen_port = new_port,
- override_address = _node.address,
- override_port = tonumber(_node.port),
- })
- if _node.tls_serverName == nil then
- _node.tls_serverName = _node.address
- end
- _node.address = "127.0.0.1"
- _node.port = new_port
- table.insert(rules, 1, {
- inbound = {"proxy_" .. rule_name},
- outbound = preproxy_tag,
- })
- end
- end
- local proxy_table = {
- tag = use_proxy and preproxy_tag or nil,
- run_socks_instance = not no_run
- }
- if not proxy_table.tag then
- if singbox_settings.fragment == "1" then
- proxy_table.fragment = true
- end
- if singbox_settings.record_fragment == "1" then
- proxy_table.record_fragment = true
- end
- end
- local _outbound = gen_outbound(flag, _node, rule_name, proxy_table)
- if _outbound then
- _outbound.tag = _outbound.tag .. ":" .. _node.remarks
- rule_outboundTag, last_insert_outbound = set_outbound_detour(_node, _outbound, outbounds, rule_name)
- table.insert(outbounds, _outbound)
- if last_insert_outbound then
- table.insert(outbounds, last_insert_outbound)
- end
- end
- end
- elseif _node.protocol == "_urltest" then
- rule_outboundTag = gen_urltest(_node)
- elseif _node.protocol == "_iface" then
- if _node.iface then
- local _outbound = {
- type = "direct",
- tag = rule_name .. ":" .. _node.remarks,
- bind_interface = _node.iface,
- routing_mark = 255,
- }
- table.insert(outbounds, _outbound)
- rule_outboundTag = _outbound.tag
- sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, _node.iface))
- end
+ local proxy_table = {
+ fragment = singbox_settings.fragment == "1",
+ record_fragment = singbox_settings.record_fragment == "1",
+ run_socks_instance = not no_run,
+ }
+ local preproxy_node_id = node[rule_name .. "_proxy_tag"]
+ if preproxy_node_id == _node_id then preproxy_node_id = nil end
+ if preproxy_node_id then
+ proxy_table.chain_proxy = "2"
+ proxy_table.to_node = _node_id
+ return gen_outbound_get_tag(flag, preproxy_node_id, rule_name, proxy_table)
+ else
+ return gen_outbound_get_tag(flag, _node_id, rule_name, proxy_table)
end
end
- return rule_outboundTag
- end
-
- if preproxy_tag and preproxy_node_id then
- local preproxy_outboundTag = gen_shunt_node(preproxy_rule_name, preproxy_node_id)
- if preproxy_outboundTag then
- preproxy_tag = preproxy_outboundTag
- end
+ return nil
end
--default_node
@@ -1611,30 +1589,12 @@ function gen_config(var)
table.insert(rules, rule)
end
end)
- elseif node.protocol == "_urltest" then
- COMMON.default_outbound_tag = gen_urltest(node)
- elseif node.protocol == "_iface" then
- if node.iface then
- local outbound = {
- type = "direct",
- tag = node.remarks or node_id,
- bind_interface = node.iface,
- routing_mark = 255,
- }
- table.insert(outbounds, outbound)
- COMMON.default_outbound_tag = outbound.tag
- sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
- end
else
- local outbound = gen_outbound(flag, node, nil, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- outbound.tag = outbound.tag .. ":" .. node.remarks
- COMMON.default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
- table.insert(outbounds, outbound)
- if last_insert_outbound then
- table.insert(outbounds, last_insert_outbound)
- end
- end
+ COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node, nil, {
+ fragment = singbox_settings.fragment == "1" or nil,
+ record_fragment = singbox_settings.record_fragment == "1" or nil,
+ run_socks_instance = not no_run
+ })
end
for index, value in ipairs(rules) do
@@ -1866,7 +1826,7 @@ function gen_config(var)
if dns_socks_address and dns_socks_port then
else
if node_id and (tcp_redir_port or udp_redir_port) then
- local node = uci:get_all(appname, node_id)
+ local node = get_node_by_id(node_id)
if node.protocol == "_shunt" then
if node.default_node == "_direct" then
default_dns_flag = "direct"
diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua
index 5513f803c1..e6a242eb1b 100644
--- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua
+++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_xray.lua
@@ -57,7 +57,7 @@ function gen_outbound(flag, node, tag, proxy_table)
run_socks_instance = proxy_table.run_socks_instance
end
- if node.type ~= "Xray" then
+ if node.type ~= "Xray" or node.protocol == "_balancing" then
if node.type == "Socks" then
node.protocol = "socks"
node.transport = "tcp"
@@ -774,9 +774,10 @@ function gen_config(var)
local socks_node = uci:get_all(appname, socks_id) or nil
if socks_node then
if not remarks then
- remarks = "Socks_" .. socks_node.port
+ remarks = socks_node.port
end
result = {
+ [".name"] = "Socksid_" .. socks_id,
remarks = remarks,
type = "Xray",
protocol = "socks",
@@ -789,6 +790,15 @@ function gen_config(var)
return result
end
+ function get_node_by_id(node_id)
+ if not node_id or node_id == "" or node_id == "nil" then return nil end
+ if node_id:find("Socks_") then
+ return gen_socks_config_node(node_id)
+ else
+ return uci:get_all(appname, node_id)
+ end
+ end
+
local nodes_list = {}
function get_balancer_batch_nodes(_node)
if #nodes_list == 0 then
@@ -859,21 +869,9 @@ function gen_config(var)
end
end
if is_new_blc_node then
- local blc_node
- if blc_node_id:find("Socks_") then
- blc_node = gen_socks_config_node(blc_node_id)
- else
- blc_node = uci:get_all(appname, blc_node_id)
- end
- if blc_node then
- local outbound = gen_outbound(flag, blc_node, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- if blc_node.remarks then
- outbound.tag = outbound.tag .. ":" .. blc_node.remarks
- end
- table.insert(outbounds, outbound)
- valid_nodes[#valid_nodes + 1] = outbound.tag
- end
+ local outboundTag = gen_outbound_get_tag(flag, blc_node_id, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
+ if outboundTag then
+ valid_nodes[#valid_nodes + 1] = outboundTag
end
end
end
@@ -893,21 +891,12 @@ function gen_config(var)
end
end
if is_new_node then
- local fallback_node
- if fallback_node_id:find("Socks_") then
- fallback_node = gen_socks_config_node(fallback_node_id)
- else
- fallback_node = uci:get_all(appname, fallback_node_id)
- end
+ local fallback_node = get_node_by_id(fallback_node_id)
if fallback_node then
if fallback_node.protocol ~= "_balancing" then
- local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- if fallback_node.remarks then
- outbound.tag = outbound.tag .. ":" .. fallback_node.remarks
- end
- table.insert(outbounds, outbound)
- fallback_node_tag = outbound.tag
+ local outboundTag = gen_outbound_get_tag(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
+ if outboundTag then
+ fallback_node_tag = outboundTag
end
else
if gen_balancer(fallback_node) then
@@ -972,7 +961,7 @@ function gen_config(var)
if outbound["_flag_proxy_tag"] then
--Ignore
else
- local preproxy_node = uci:get_all(appname, node.preproxy_node)
+ local preproxy_node = get_node_by_id(node.preproxy_node)
if preproxy_node then
local preproxy_outbound = gen_outbound(node[".name"], preproxy_node)
if preproxy_outbound then
@@ -992,7 +981,13 @@ function gen_config(var)
end
end
if node.chain_proxy == "2" and node.to_node then
- local to_node = uci:get_all(appname, node.to_node)
+ local to_node = get_node_by_id(node.to_node)
+ if to_node then
+ -- Landing Node not support use special node.
+ if to_node.protocol:find("_") then
+ to_node = nil
+ end
+ end
if to_node then
local to_outbound
if to_node.type ~= "Xray" then
@@ -1045,13 +1040,61 @@ function gen_config(var)
return default_outTag, last_insert_outbound
end
- if node.protocol == "_shunt" then
- local preproxy_rule_name = node.preproxy_enabled == "1" and "main" or nil
- local preproxy_tag = preproxy_rule_name
- local preproxy_node_id = preproxy_rule_name and node["main_node"] or nil
- local preproxy_outbound_tag, preproxy_balancer_tag
- local preproxy_nodes
+ function gen_outbound_get_tag(flag, node_id, tag, proxy_table)
+ if not node_id or node_id == "nil" then return nil end
+ local node
+ if type(node_id) == "string" then
+ node = get_node_by_id(node_id)
+ elseif type(node_id) == "table" then
+ node = node_id
+ end
+ if node then
+ if node.protocol == "_iface" then
+ if node.iface then
+ local outbound = {
+ tag = tag,
+ protocol = "freedom",
+ streamSettings = {
+ sockopt = {
+ mark = 255,
+ interface = node.iface
+ }
+ }
+ }
+ table.insert(outbounds, outbound)
+ sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
+ return outbound.tag
+ end
+ return nil
+ end
+ if proxy_table.chain_proxy == "1" or proxy_table.chain_proxy == "2" then
+ node.chain_proxy = proxy_table.chain_proxy
+ node.preproxy_node = proxy_table.chain_proxy == "1" and proxy_table.preproxy_node
+ node.to_node = proxy_table.chain_proxy == "2" and proxy_table.to_node
+ proxy_table.chain_proxy = nil
+ proxy_table.preproxy_node = nil
+ proxy_table.to_node = nil
+ end
+ local outbound = gen_outbound(flag, node, tag, proxy_table)
+ if outbound then
+ if node.remarks then
+ outbound.tag = outbound.tag .. ":" .. node.remarks
+ end
+ local default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
+ if tag == "default" then
+ table.insert(outbounds, 1, outbound)
+ else
+ table.insert(outbounds, outbound)
+ end
+ if last_insert_outbound then
+ table.insert(outbounds, last_insert_outbound)
+ end
+ return default_outbound_tag
+ end
+ end
+ end
+ if node.protocol == "_shunt" then
inner_fakedns = node.fakedns or "0"
local function gen_shunt_node(rule_name, _node_id)
@@ -1064,149 +1107,25 @@ function gen_config(var)
return "direct", nil
elseif _node_id == "_blackhole" then
return "blackhole", nil
- elseif _node_id == "_default" then
+ elseif _node_id == "_default" and rule_name ~= "default" then
return "default", nil
- elseif _node_id and _node_id:find("Socks_") then
- local socks_tag
- local socks_node = gen_socks_config_node(_node_id)
- local outbound = gen_outbound(flag, socks_node, rule_name)
- if outbound then
- if rule_name == "default" then
- table.insert(outbounds, 1, outbound)
- else
- table.insert(outbounds, outbound)
- end
- socks_tag = outbound.tag
- end
- return socks_tag, nil
- end
-
- local _node = uci:get_all(appname, _node_id)
- if not _node then return nil, nil end
-
- if api.is_normal_node(_node) then
- local use_proxy = preproxy_tag and node[rule_name .. "_proxy_tag"] == preproxy_rule_name and _node_id ~= preproxy_node_id
- if use_proxy and preproxy_balancer_tag and preproxy_nodes[_node_id] then use_proxy = false end
- local copied_outbound
- for index, value in ipairs(outbounds) do
- if value["_id"] == _node_id and value["_flag_proxy_tag"] == (use_proxy and preproxy_tag or nil) then
- copied_outbound = api.clone(value)
- break
- end
- end
- if copied_outbound then
- copied_outbound.tag = rule_name
- table.insert(outbounds, copied_outbound)
- return copied_outbound.tag, nil
- end
- --new outbound
- if use_proxy and _node.type ~= "Xray" then
- local new_port = api.get_new_port()
- table.insert(inbounds, {
- tag = "proxy_" .. rule_name,
- listen = "127.0.0.1",
- port = new_port,
- protocol = "dokodemo-door",
- settings = {network = "tcp,udp", address = _node.address, port = tonumber(_node.port)}
- })
- if _node.tls_serverName == nil then
- _node.tls_serverName = _node.address
- end
- _node.address = "127.0.0.1"
- _node.port = new_port
- table.insert(rules, 1, {
- inboundTag = {"proxy_" .. rule_name},
- outboundTag = not preproxy_balancer_tag and preproxy_tag or nil,
- balancerTag = preproxy_balancer_tag
- })
- end
+ elseif _node_id then
local proxy_table = {
- tag = use_proxy and preproxy_tag or nil,
- run_socks_instance = not no_run
+ fragment = xray_settings.fragment == "1",
+ noise = xray_settings.noise == "1",
+ run_socks_instance = not no_run,
}
- if not proxy_table.tag then
- if xray_settings.fragment == "1" then
- proxy_table.fragment = true
- end
- if xray_settings.noise == "1" then
- proxy_table.noise = true
- end
+ local preproxy_node_id = node[rule_name .. "_proxy_tag"]
+ if preproxy_node_id == _node_id then preproxy_node_id = nil end
+ if preproxy_node_id then
+ proxy_table.chain_proxy = "2"
+ proxy_table.to_node = _node_id
+ return gen_outbound_get_tag(flag, preproxy_node_id, rule_name, proxy_table), nil
+ else
+ return gen_outbound_get_tag(flag, _node_id, rule_name, proxy_table), nil
end
- local outbound = gen_outbound(flag, _node, rule_name, proxy_table)
- local outbound_tag
- if outbound then
- outbound.tag = outbound.tag .. ":" .. _node.remarks
- outbound_tag, last_insert_outbound = set_outbound_detour(_node, outbound, outbounds, rule_name)
- if rule_name == "default" then
- table.insert(outbounds, 1, outbound)
- else
- table.insert(outbounds, outbound)
- end
- if last_insert_outbound then
- table.insert(outbounds, last_insert_outbound)
- end
- end
- return outbound_tag, nil
- elseif _node.protocol == "_balancing" then
- local blc_tag = gen_balancer(_node, rule_name)
- if rule_name == "default" then
- for i, ob in ipairs(outbounds) do
- if ob.protocol == "loopback" and ob.tag == "default" then
- if i > 1 then table.insert(outbounds, 1, table.remove(outbounds, i)) end
- break
- end
- end
- end
- return nil, blc_tag
- elseif _node.protocol == "_iface" then
- local outbound_tag
- if _node.iface then
- local outbound = {
- protocol = "freedom",
- tag = rule_name,
- streamSettings = {
- sockopt = {
- mark = 255,
- interface = _node.iface
- }
- }
- }
- outbound_tag = outbound.tag
- if rule_name == "default" then
- table.insert(outbounds, 1, outbound)
- else
- table.insert(outbounds, outbound)
- end
- sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, _node.iface))
- end
- return outbound_tag, nil
- end
- end
-
- if preproxy_tag and preproxy_node_id then
- preproxy_outbound_tag, preproxy_balancer_tag = gen_shunt_node(preproxy_rule_name, preproxy_node_id)
- if preproxy_balancer_tag then
- local _node_id = preproxy_node_id
- preproxy_nodes = {}
- while _node_id do
- _node = uci:get_all(appname, _node_id)
- if not _node then break end
- if _node.protocol ~= "_balancing" then
- preproxy_nodes[_node_id] = true
- break
- end
- local _blc_nodes
- if _node.node_add_mode and _node.node_add_mode == "batch" then
- _blc_nodes = get_balancer_batch_nodes(_node)
- else
- _blc_nodes = _node.balancing_node
- end
- for i = 1, #_blc_nodes do preproxy_nodes[_blc_nodes[i]] = true end
- _node_id = _node.fallback_node
- end
- else
- preproxy_tag = preproxy_outbound_tag
end
+ return nil, nil
end
--default_node
@@ -1360,42 +1279,24 @@ function gen_config(var)
}
COMMON.default_balancer_tag = balancer_tag
end
- elseif node.protocol == "_iface" then
- if node.iface then
- local outbound = {
- protocol = "freedom",
- tag = node.remarks or node_id,
- streamSettings = {
- sockopt = {
- mark = 255,
- interface = node.iface
- }
- }
- }
- table.insert(outbounds, outbound)
- COMMON.default_outbound_tag = outbound.tag
- sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
- end
else
- local outbound = gen_outbound(flag, node, nil, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- outbound.tag = outbound.tag .. ":" .. node.remarks
- COMMON.default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
- table.insert(outbounds, outbound)
- if last_insert_outbound then
- table.insert(outbounds, last_insert_outbound)
- end
- end
- routing = {
- domainStrategy = "AsIs",
- domainMatcher = "hybrid",
- rules = rules
- }
- table.insert(routing.rules, {
- ruleTag = "default",
- outboundTag = COMMON.default_outbound_tag,
- network = "tcp,udp"
+ COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node, nil, {
+ fragment = xray_settings.fragment == "1" or nil,
+ noise = xray_settings.noise == "1" or nil,
+ run_socks_instance = not no_run
})
+ if COMMON.default_outbound_tag then
+ routing = {
+ domainStrategy = "AsIs",
+ domainMatcher = "hybrid",
+ rules = rules
+ }
+ table.insert(routing.rules, {
+ ruleTag = "default",
+ outboundTag = COMMON.default_outbound_tag,
+ network = "tcp,udp"
+ })
+ end
end
if tcp_redir_port or udp_redir_port then
diff --git a/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/include/shunt_options.htm b/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/include/shunt_options.htm
index ddf8e3611b..96440b59a9 100644
--- a/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/include/shunt_options.htm
+++ b/openwrt-passwall/luci-app-passwall/luasrc/view/passwall/include/shunt_options.htm
@@ -6,6 +6,7 @@
diff --git a/small/luci-app-passwall/root/usr/share/passwall/iptables.sh b/small/luci-app-passwall/root/usr/share/passwall/iptables.sh
index e9cfb50b9e..994e260727 100755
--- a/small/luci-app-passwall/root/usr/share/passwall/iptables.sh
+++ b/small/luci-app-passwall/root/usr/share/passwall/iptables.sh
@@ -2,6 +2,7 @@
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/iptables.sh
+UTILS_PATH=$DIR/utils.sh
IPSET_LOCAL="passwall_local"
IPSET_WAN="passwall_wan"
IPSET_LAN="passwall_lan"
@@ -29,8 +30,6 @@ FORCE_INDEX=2
USE_SHUNT_TCP=0
USE_SHUNT_UDP=0
-. /lib/functions/network.sh
-
ipt=$(command -v iptables-legacy || command -v iptables)
ip6t=$(command -v ip6tables-legacy || command -v ip6tables)
@@ -283,7 +282,6 @@ load_acl() {
local _ipt_source _ipv4
local msg
if [ -n "${interface}" ]; then
- . /lib/functions/network.sh
local gateway device
network_get_gateway gateway "${interface}"
network_get_device device "${interface}"
@@ -1398,6 +1396,7 @@ gen_include() {
local __ipt=""
[ -n "${ipt}" ] && {
__ipt=$(cat <<- EOF
+ . $UTILS_PATH
mangle_output_psw=\$(${ipt}-save -t mangle | grep "PSW" | grep "mangle\-OUTPUT\-PSW" | sed "s#-A OUTPUT ##g")
$ipt-save -c | grep -v "PSW" | $ipt-restore -c
$ipt-restore -n <<-EOT
@@ -1415,7 +1414,7 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW")
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT")
- WAN_IP=\$(${MY_PATH} get_wan_ips ip4)
+ WAN_IP=\$(get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in \$WAN_IP; do
@@ -1429,6 +1428,7 @@ gen_include() {
local __ip6t=""
[ -n "${ip6t}" ] && {
__ip6t=$(cat <<- EOF
+ . $UTILS_PATH
mangle_output_psw=\$(${ip6t}-save -t mangle | grep "PSW" | grep "mangle\-OUTPUT\-PSW" | sed "s#-A OUTPUT ##g")
$ip6t-save -c | grep -v "PSW" | $ip6t-restore -c
$ip6t-restore -n <<-EOT
@@ -1445,7 +1445,7 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW")
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT")
- WAN6_IP=\$(${MY_PATH} get_wan_ips ip6)
+ WAN6_IP=\$(get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in \$WAN6_IP; do
@@ -1510,9 +1510,6 @@ get_ipt_bin)
get_ip6t_bin)
get_ip6t_bin
;;
-get_wan_ips)
- get_wan_ips
- ;;
filter_direct_node_list)
filter_direct_node_list
;;
diff --git a/small/luci-app-passwall/root/usr/share/passwall/nftables.sh b/small/luci-app-passwall/root/usr/share/passwall/nftables.sh
index c78048d2f4..3bef5a8fa3 100755
--- a/small/luci-app-passwall/root/usr/share/passwall/nftables.sh
+++ b/small/luci-app-passwall/root/usr/share/passwall/nftables.sh
@@ -2,6 +2,7 @@
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/nftables.sh
+UTILS_PATH=$DIR/utils.sh
NFTABLE_NAME="inet passwall"
NFTSET_LOCAL="passwall_local"
NFTSET_WAN="passwall_wan"
@@ -30,8 +31,6 @@ FORCE_INDEX=0
USE_SHUNT_TCP=0
USE_SHUNT_UDP=0
-. /lib/functions/network.sh
-
FWI=$(uci -q get firewall.passwall.path 2>/dev/null)
FAKE_IP="198.18.0.0/15"
FAKE_IP_6="fc00::/18"
@@ -177,18 +176,27 @@ insert_nftset() {
*) suffix=" timeout $timeout_argument" ;;
esac
{
- if [ $# -gt 0 ]; then
- echo "add element $NFTABLE_NAME $nftset_name { "
- printf "%s\n" "$@" | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
- echo " }"
- else
- local first_line
- if IFS= read -r first_line; then
- echo "add element $NFTABLE_NAME $nftset_name { "
- { echo "$first_line"; cat; } | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
- echo " }"
- fi
- fi
+ if [ $# -gt 0 ]; then
+ printf "%s\n" "$@"
+ else
+ cat
+ fi | tr -s ' \t' '\n' | awk -v s="$suffix" -v n="$nftset_name" -v t="$NFTABLE_NAME" '
+ {
+ gsub(/^[ \t\r]+|[ \t\r]+$/, "");
+ }
+ $0 != "" {
+ if (first == 0) {
+ printf "add element %s %s { \n", t, n;
+ first = 1;
+ } else {
+ printf ",\n";
+ }
+ printf "%s%s", $0, s;
+ }
+ END {
+ if (first == 1) printf "\n }\n";
+ }
+ '
} | nft -f -
fi
}
@@ -315,7 +323,6 @@ load_acl() {
local _ipt_source _ipv4
local msg
if [ -n "${interface}" ]; then
- . /lib/functions/network.sh
local gateway device
network_get_gateway gateway "${interface}"
network_get_device device "${interface}"
@@ -757,25 +764,25 @@ load_acl() {
filter_haproxy() {
for item in ${haproxy_items}; do
- local ip=$(get_host_ip ipv4 $(echo $item | awk -F ":" '{print $1}') 1)
- insert_nftset $NFTSET_VPS "-1" $ip
- done
+ get_host_ip ipv4 $(echo $item | awk -F ":" '{print $1}') 1
+ done | insert_nftset $NFTSET_VPS "-1"
echolog " - [$?]加入负载均衡的节点到nftset[$NFTSET_VPS]直连完成"
}
filter_vps_addr() {
- for server_host in $@; do
- local vps_ip4=$(get_host_ip "ipv4" ${server_host})
- local vps_ip6=$(get_host_ip "ipv6" ${server_host})
- [ -n "$vps_ip4" ] && insert_nftset $NFTSET_VPS "-1" $vps_ip4
- [ -n "$vps_ip6" ] && insert_nftset $NFTSET_VPS6 "-1" $vps_ip6
- done
+ for server_host in "$@"; do
+ get_host_ip "ipv4" ${server_host}
+ done | insert_nftset $NFTSET_VPS "-1"
+
+ for server_host in "$@"; do
+ get_host_ip "ipv6" ${server_host}
+ done | insert_nftset $NFTSET_VPS6 "-1"
}
filter_vpsip() {
- uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS "-1"
+ uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | insert_nftset $NFTSET_VPS "-1"
echolog " - [$?]加入所有IPv4节点到nftset[$NFTSET_VPS]直连完成"
- uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS6 "-1"
+ uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_VPS6 "-1"
echolog " - [$?]加入所有IPv6节点到nftset[$NFTSET_VPS6]直连完成"
#订阅方式为直连时
get_subscribe_host | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS "-1"
@@ -888,8 +895,8 @@ add_firewall_rule() {
#直连列表
[ "$USE_DIRECT_LIST_ALL" = "1" ] && {
- insert_nftset $NFTSET_WHITE "0" $(cat $RULES_PATH/direct_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
- insert_nftset $NFTSET_WHITE6 "0" $(cat $RULES_PATH/direct_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
+ cat $RULES_PATH/direct_ip | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_WHITE "0"
+ cat $RULES_PATH/direct_ip | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_WHITE6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/direct_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ]; then
@@ -902,8 +909,8 @@ add_firewall_rule() {
#代理列表
[ "$USE_PROXY_LIST_ALL" = "1" ] && {
- insert_nftset $NFTSET_BLACK "0" $(cat $RULES_PATH/proxy_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
- insert_nftset $NFTSET_BLACK6 "0" $(cat $RULES_PATH/proxy_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
+ cat $RULES_PATH/proxy_ip | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLACK "0"
+ cat $RULES_PATH/proxy_ip | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLACK6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/proxy_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ]; then
@@ -916,8 +923,8 @@ add_firewall_rule() {
#屏蔽列表
[ "$USE_BLOCK_LIST_ALL" = "1" ] && {
- insert_nftset $NFTSET_BLOCK "0" $(cat $RULES_PATH/block_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}")
- insert_nftset $NFTSET_BLOCK6 "0" $(cat $RULES_PATH/block_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
+ cat $RULES_PATH/block_ip | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLOCK "0"
+ cat $RULES_PATH/block_ip | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLOCK6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/block_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ]; then
@@ -933,8 +940,8 @@ add_firewall_rule() {
local GEOIP_CODE=""
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
for shunt_id in $shunt_ids; do
- config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT "0"
- config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6 "0"
+ config_n_get $shunt_id ip_list | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT "0"
+ config_n_get $shunt_id ip_list | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local geoip_code=$(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
[ -n "$geoip_code" ] && GEOIP_CODE="${GEOIP_CODE:+$GEOIP_CODE,}$geoip_code"
@@ -947,8 +954,8 @@ add_firewall_rule() {
fi
}
- ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL "-1"
- ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL6 "-1"
+ ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | insert_nftset $NFTSET_LOCAL "-1"
+ ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | insert_nftset $NFTSET_LOCAL6 "-1"
# 忽略特殊IP段
local lan_ifname lan_ip
@@ -959,22 +966,22 @@ add_firewall_rule() {
#echolog "本机IPv4网段互访直连:${lan_ip}"
#echolog "本机IPv6网段互访直连:${lan_ip6}"
- [ -n "$lan_ip" ] && insert_nftset $NFTSET_LAN "-1" $(echo $lan_ip | sed -e "s/ /\n/g")
- [ -n "$lan_ip6" ] && insert_nftset $NFTSET_LAN6 "-1" $(echo $lan_ip6 | sed -e "s/ /\n/g")
+ [ -n "$lan_ip" ] && echo $lan_ip | insert_nftset $NFTSET_LAN "-1"
+ [ -n "$lan_ip6" ] && echo $lan_ip6 | insert_nftset $NFTSET_LAN6 "-1"
}
[ -n "$ISP_DNS" ] && {
#echolog "处理 ISP DNS 例外..."
+ echo "$ISP_DNS" | insert_nftset $NFTSET_WHITE 0
for ispip in $ISP_DNS; do
- insert_nftset $NFTSET_WHITE 0 $ispip
echolog " - [$?]追加ISP IPv4 DNS到白名单:${ispip}"
done
}
[ -n "$ISP_DNS6" ] && {
#echolog "处理 ISP IPv6 DNS 例外..."
+ echo $ISP_DNS6 | insert_nftset $NFTSET_WHITE6 0
for ispip6 in $ISP_DNS6; do
- insert_nftset $NFTSET_WHITE6 0 $ispip6
echolog " - [$?]追加ISP IPv6 DNS到白名单:${ispip6}"
done
}
@@ -1078,7 +1085,7 @@ add_firewall_rule() {
WAN_IP=$(get_wan_ips ip4)
if [ -n "${WAN_IP}" ]; then
nft flush set $NFTABLE_NAME $NFTSET_WAN
- insert_nftset $NFTSET_WAN "-1" $WAN_IP
+ echo $WAN_IP | insert_nftset $NFTSET_WAN "-1"
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
for wan_ip in $WAN_IP; do
@@ -1129,7 +1136,7 @@ add_firewall_rule() {
WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
- insert_nftset $NFTSET_WAN6 "-1" $WAN6_IP
+ echo $WAN6_IP | insert_nftset $NFTSET_WAN6 "-1"
nft "add rule $NFTABLE_NAME PSW_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
for wan6_ip in $WAN6_IP; do
echolog " - [$?]加入WAN IPv6到nftset[$NFTSET_WAN6]:${wan6_ip}"
@@ -1419,22 +1426,23 @@ flush_include() {
gen_include() {
flush_include
local nft_chain_file=$TMP_PATH/PSW_RULE.nft
- echo '#!/usr/sbin/nft -f' > $nft_chain_file
+ echo '#!/bin/sh' > $nft_chain_file
nft list table $NFTABLE_NAME >> $nft_chain_file
local __nft=" "
__nft=$(cat <<- EOF
+ . $UTILS_PATH
[ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW_DIVERT)" ] && nft -f ${nft_chain_file}
- WAN_IP=\$(sh ${MY_PATH} get_wan_ips ip4)
+ WAN_IP=\$(get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
- sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1" \$WAN_IP
+ echo "\${WAN_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1"
}
[ "$PROXY_IPV6" == "1" ] && {
- WAN6_IP=\$(sh ${MY_PATH} get_wan_ips ip6)
+ WAN6_IP=\$(get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
- sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1" \$WAN6_IP
+ echo "\${WAN6_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1"
}
}
EOF
@@ -1474,9 +1482,6 @@ case $arg1 in
insert_nftset)
insert_nftset "$@"
;;
-get_wan_ips)
- get_wan_ips "$@"
- ;;
filter_direct_node_list)
filter_direct_node_list
;;
diff --git a/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua
index 5e717125cb..0634c89113 100755
--- a/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua
+++ b/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua
@@ -620,6 +620,8 @@ local function processData(szType, content, add_mode, group)
result.tls = "0"
end
+ result.tcp_fast_open = info.tfo
+
if result.type == "sing-box" and (result.transport == "mkcp" or result.transport == "xhttp") then
log("跳过节点:" .. result.remarks ..",因Sing-Box不支持" .. szType .. "协议的" .. result.transport .. "传输方式,需更换Xray。")
return nil
@@ -723,13 +725,17 @@ local function processData(szType, content, add_mode, group)
result.method = method
result.password = password
+ result.tcp_fast_open = params.tfo
- if has_xray and (result.type ~= 'Xray' and result.type ~= 'sing-box' and params.type) then
- result.type = 'Xray'
- result.protocol = 'shadowsocks'
- elseif has_singbox and (result.type ~= 'Xray' and result.type ~= 'sing-box' and params.type) then
- result.type = 'sing-box'
- result.protocol = 'shadowsocks'
+ local need_upgrade = (result.type ~= "Xray" and result.type ~= "sing-box")
+ and (params.type and params.type ~= "tcp")
+ and (params.headerType and params.headerType ~= "none")
+ if has_xray and (need_upgrade or params.type == "xhttp") then
+ result.type = "Xray"
+ result.protocol = "shadowsocks"
+ elseif has_singbox and need_upgrade then
+ result.type = "sing-box"
+ result.protocol = "shadowsocks"
end
if result.plugin then
@@ -892,7 +898,8 @@ local function processData(szType, content, add_mode, group)
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
- else
+ result.uot = params.udp
+ elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then
result.error_msg = "请更换Xray或Sing-Box来支持SS更多的传输方式."
end
end
@@ -1091,6 +1098,7 @@ local function processData(szType, content, add_mode, group)
end
result.alpn = params.alpn
+ result.tcp_fast_open = params.tfo
if result.type == "sing-box" and (result.transport == "mkcp" or result.transport == "xhttp") then
log("跳过节点:" .. result.remarks ..",因Sing-Box不支持" .. szType .. "协议的" .. result.transport .. "传输方式,需更换Xray。")
@@ -1281,6 +1289,8 @@ local function processData(szType, content, add_mode, group)
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
+ result.tcp_fast_open = params.tfo
+
if result.type == "sing-box" and (result.transport == "mkcp" or result.transport == "xhttp") then
log("跳过节点:" .. result.remarks ..",因Sing-Box不支持" .. szType .. "协议的" .. result.transport .. "传输方式,需更换Xray。")
return nil
diff --git a/small/luci-app-passwall/root/usr/share/passwall/utils.sh b/small/luci-app-passwall/root/usr/share/passwall/utils.sh
index 8d617e7719..ce93e2851d 100755
--- a/small/luci-app-passwall/root/usr/share/passwall/utils.sh
+++ b/small/luci-app-passwall/root/usr/share/passwall/utils.sh
@@ -15,6 +15,8 @@ TMP_ROUTE_PATH=${TMP_PATH}/route
TMP_SCRIPT_FUNC_PATH=${TMP_PATH}/script_func
RULES_PATH=/usr/share/${CONFIG}/rules
+. /lib/functions/network.sh
+
echolog() {
local d="$(date "+%Y-%m-%d %H:%M:%S")"
echo -e "$d: $*" >>$LOG_FILE
@@ -81,14 +83,15 @@ get_host_ip() {
local count=$3
[ -z "$count" ] && count=3
local isip=""
- local ip=$host
+ local ip=""
if [ "$1" == "ipv6" ]; then
isip=$(echo $host | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
if [ -n "$isip" ]; then
- isip=$(echo $host | cut -d '[' -f2 | cut -d ']' -f1)
+ ip=$(echo "$host" | tr -d '[]')
fi
else
isip=$(echo $host | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}")
+ [ -n "$isip" ] && ip=$isip
fi
[ -z "$isip" ] && {
local t=4
@@ -96,7 +99,7 @@ get_host_ip() {
local vpsrip=$(resolveip -$t -t $count $host | awk 'NR==1{print}')
ip=$vpsrip
}
- echo $ip
+ [ -n "$ip" ] && echo "$ip"
}
get_node_host_ip() {
@@ -354,7 +357,6 @@ add_ip2route() {
local remarks="${1}"
[ "$remarks" != "$ip" ] && remarks="${1}(${ip})"
- . /lib/functions/network.sh
local gateway device
network_get_gateway gateway "$2"
network_get_device device "$2"
diff --git a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/include/shunt_options.lua b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/include/shunt_options.lua
index d2d1f7922e..9df55fa411 100644
--- a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/include/shunt_options.lua
+++ b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/include/shunt_options.lua
@@ -87,13 +87,6 @@ if data.node.type == "Xray" then
o:value("linear")
end
-o = add_option(Flag, "preproxy_enabled", translate("Preproxy") .. " " .. translate("Main switch"))
-
-main_node = add_option(ListValue, "main_node", string.format('%s', translate("Preproxy Node")), translate("Set the node to be used as a pre-proxy. Each rule (including Default) has a separate switch that controls whether this rule uses the pre-proxy or not."))
-add_depends(main_node, {["preproxy_enabled"] = true})
-main_node.template = appname .. "/cbi/nodes_listvalue"
-main_node.group = {}
-
o = add_option(Flag, "fakedns", 'FakeDNS' .. " " .. translate("Main switch"), translate("Use FakeDNS work in the domain that proxy.") .. "
" ..
translate("Suitable scenarios for let the node servers get the target domain names.") .. "
" ..
translate("Such as: DNS unlocking of streaming media, reducing DNS query latency, etc."))
@@ -174,73 +167,70 @@ o.remove = function(self, section)
return m:del(current_node_id, shunt_rules[section]["_fakedns_option"])
end
-o = s2:option(ListValue, "_proxy_tag", string.format('%s', translate("Preproxy")))
---TODO Choose any node as a pre-proxy. Instead of main node.
-o.template = appname .. "/cbi/nodes_listvalue"
-o.group = {"",""}
-o:value("", translate("Close (Not use)"))
-o:value("main", translate("Use preproxy node"))
-o.cfgvalue = function(self, section)
+proxy_tag_node = s2:option(ListValue, "_proxy_tag", string.format('%s',
+ translate("Set the node to be used as a pre-proxy.") .. "\n" .. translate("Each rule has a separate switch that controls whether this rule uses the pre-proxy or not."),
+ translate("Preproxy")))
+proxy_tag_node.template = appname .. "/cbi/nodes_listvalue"
+proxy_tag_node.group = {""}
+proxy_tag_node:value("", translate("Close (Not use)"))
+proxy_tag_node.cfgvalue = function(self, section)
return m:get(current_node_id, shunt_rules[section]["_proxy_tag_option"])
end
-o.write = function(self, section, value)
+proxy_tag_node.write = function(self, section, value)
return m:set(current_node_id, shunt_rules[section]["_proxy_tag_option"], value)
end
-o.remove = function(self, section)
+proxy_tag_node.remove = function(self, section)
return m:del(current_node_id, shunt_rules[section]["_proxy_tag_option"])
end
if data.socks_list then
for k, v in pairs(data.socks_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
if data.urltest_list then
for k, v in pairs(data.urltest_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
if data.balancing_list then
for k, v in pairs(data.balancing_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
if data.iface_list then
for k, v in pairs(data.iface_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
if data.normal_list then
for k, v in pairs(data.normal_list) do
- main_node:value(v.id, v.remark)
- main_node.group[#main_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
-
_node:value(v.id, v.remark)
_node.group[#_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
- end
-end
-if #main_node.keylist > 0 then
- main_node.default = main_node.keylist[1]
+ proxy_tag_node:value(v.id, v.remark)
+ proxy_tag_node.group[#proxy_tag_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+ end
end
local footer = Template(appname .. "/include/shunt_options")
footer.api = api
footer.id = current_node_id
+footer.normal_list = api.jsonc.stringify(data.normal_list or {})
m:append(footer)
diff --git a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua
index a9bbcea7ae..6b9db8abba 100644
--- a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua
+++ b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua
@@ -17,6 +17,15 @@ m:append(header)
m:append(Template(appname .. "/cbi/nodes_multivalue_com"))
m:append(Template(appname .. "/cbi/nodes_listvalue_com"))
+groups = {}
+m.uci:foreach(appname, "nodes", function(s)
+ if s[".name"] ~= arg[1] then
+ if s.group and s.group ~= "" then
+ groups[s.group] = true
+ end
+ end
+end)
+
s = m:section(NamedSection, arg[1], "nodes", "")
s.addremove = false
s.dynamic = false
@@ -33,14 +42,6 @@ o.rmempty = false
o = s:option(Value, "group", translate("Group Name"))
o.default = ""
o:value("", translate("default"))
-local groups = {}
-m.uci:foreach(appname, "nodes", function(s)
- if s[".name"] ~= arg[1] then
- if s.group and s.group ~= "" then
- groups[s.group] = true
- end
- end
-end)
for k, v in pairs(groups) do
o:value(k)
end
diff --git a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua
index 6e953c6ff3..7d9c9151fe 100644
--- a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua
+++ b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua
@@ -38,6 +38,10 @@ local ss_method_list = {
local security_list = { "none", "auto", "aes-128-gcm", "chacha20-poly1305", "zero" }
+local header_type_list = {
+ "none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
+}
+
local xray_version = api.get_app_version("xray")
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
@@ -128,8 +132,14 @@ m.uci:foreach(appname, "socks", function(s)
end)
if load_balancing_options then -- [[ Load balancing Start ]]
- o = s:option(MultiValue, _n("balancing_node"), translate("Load balancing node list"), translate("Load balancing node list, document"))
+ o = s:option(ListValue, _n("node_add_mode"), translate("Node Addition Method"))
o:depends({ [_n("protocol")] = "_balancing" })
+ o.default = "manual"
+ o:value("manual", translate("Manual"))
+ o:value("batch", translate("Batch"))
+
+ o = s:option(MultiValue, _n("balancing_node"), translate("Load balancing node list"), translate("Load balancing node list, document"))
+ o:depends({ [_n("node_add_mode")] = "manual" })
o.widget = "checkbox"
o.template = appname .. "/cbi/nodes_multivalue"
o.group = {}
@@ -166,6 +176,21 @@ if load_balancing_options then -- [[ Load balancing Start ]]
end
end
+ o = s:option(MultiValue, _n("node_group"), translate("Select Group"))
+ o:depends({ [_n("node_add_mode")] = "batch" })
+ o.widget = "checkbox"
+ o:value("default", translate("default"))
+ for k, v in pairs(groups) do
+ o:value(api.UrlEncode(k), k)
+ end
+
+ o = s:option(Value, _n("node_match_rule"), translate("Node Matching Rules"))
+ o:depends({ [_n("node_add_mode")] = "batch" })
+ local descrStr = "Example: ^A && B && !C && D$
"
+ descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
"
+ descrStr = descrStr .. "Conditions are joined by &&, and their order does not affect the result."
+ o.description = translate(descrStr)
+
o = s:option(ListValue, _n("balancingStrategy"), translate("Balancing Strategy"))
o:depends({ [_n("protocol")] = "_balancing" })
o:value("random")
@@ -517,17 +542,11 @@ o:depends({ [_n("tcp_guise")] = "http" })
-- [[ mKCP ]]--
o = s:option(ListValue, _n("mkcp_guise"), translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.'))
-o:value("none", "none")
-o:value("header-srtp", "srtp")
-o:value("header-utp", "utp")
-o:value("header-wechat", "wechat-video")
-o:value("header-dtls", "dtls")
-o:value("header-wireguard", "wireguard")
-o:value("header-dns", "dns")
+for a, t in ipairs(header_type_list) do o:value(t) end
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
-o:depends({ [_n("mkcp_guise")] = "header-dns" })
+o:depends({ [_n("mkcp_guise")] = "dns" })
o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU"))
o.default = "1350"
@@ -692,7 +711,7 @@ o = s:option(Value, _n("xudp_concurrency"), translate("XUDP Mux concurrency"))
o.default = 8
o:depends({ [_n("mux")] = true })
-o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
+o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
o.default = 0
--[[tcpMptcp]]
@@ -724,12 +743,22 @@ o2:depends({ [_n("chain_proxy")] = "2" })
o2.template = appname .. "/cbi/nodes_listvalue"
o2.group = {}
-for k, v in pairs(nodes_list) do
- if v.id ~= arg[1] and (not v.chain_proxy or v.chain_proxy == "") then
- o1:value(v.id, v.remark)
- o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
- o2:value(v.id, v.remark)
- o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+for k, v in pairs(socks_list) do
+ o1:value(v.id, v.remark)
+ o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+end
+
+for k, e in ipairs(api.get_valid_nodes()) do
+ if e[".name"] ~= arg[1] then
+ if e.protocol ~= "_shunt" and e.protocol ~= "_iface" then
+ o1:value(e[".name"], e["remark"])
+ o1.group[#o1.group+1] = (e["group"] and e["group"] ~= "") and e["group"] or translate("default")
+ end
+ if not e.protocol:find("_") then
+ -- Landing Node not support use special node.
+ o2:value(e[".name"], e["remark"])
+ o2.group[#o2.group+1] = (e["group"] and e["group"] ~= "") and e["group"] or translate("default")
+ end
end
end
diff --git a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua
index 811426a78b..4a1131ec15 100644
--- a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua
+++ b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua
@@ -136,8 +136,14 @@ m.uci:foreach(appname, "socks", function(s)
end)
if load_urltest_options then -- [[ URLTest Start ]]
- o = s:option(MultiValue, _n("urltest_node"), translate("URLTest node list"), translate("List of nodes to test, document"))
+ o = s:option(ListValue, _n("node_add_mode"), translate("Node Addition Method"))
o:depends({ [_n("protocol")] = "_urltest" })
+ o.default = "manual"
+ o:value("manual", translate("Manual"))
+ o:value("batch", translate("Batch"))
+
+ o = s:option(MultiValue, _n("urltest_node"), translate("URLTest node list"), translate("List of nodes to test, document"))
+ o:depends({ [_n("node_add_mode")] = "manual" })
o.widget = "checkbox"
o.template = appname .. "/cbi/nodes_multivalue"
o.group = {}
@@ -174,6 +180,21 @@ if load_urltest_options then -- [[ URLTest Start ]]
end
end
+ o = s:option(MultiValue, _n("node_group"), translate("Select Group"))
+ o:depends({ [_n("node_add_mode")] = "batch" })
+ o.widget = "checkbox"
+ o:value("default", translate("default"))
+ for k, v in pairs(groups) do
+ o:value(api.UrlEncode(k), k)
+ end
+
+ o = s:option(Value, _n("node_match_rule"), translate("Node Matching Rules"))
+ o:depends({ [_n("node_add_mode")] = "batch" })
+ local descrStr = "Example: ^A && B && !C && D$
"
+ descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
"
+ descrStr = descrStr .. "Conditions are joined by &&, and their order does not affect the result."
+ o.description = translate(descrStr)
+
o = s:option(Value, _n("urltest_url"), translate("Probe URL"))
o:depends({ [_n("protocol")] = "_urltest" })
o:value("https://cp.cloudflare.com/", "Cloudflare")
@@ -826,12 +847,22 @@ o2:depends({ [_n("chain_proxy")] = "2" })
o2.template = appname .. "/cbi/nodes_listvalue"
o2.group = {}
-for k, v in pairs(nodes_list) do
- if v.id ~= arg[1] and (not v.chain_proxy or v.chain_proxy == "") then
- o1:value(v.id, v.remark)
- o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
- o2:value(v.id, v.remark)
- o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+for k, v in pairs(socks_list) do
+ o1:value(v.id, v.remark)
+ o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
+end
+
+for k, e in ipairs(api.get_valid_nodes()) do
+ if e[".name"] ~= arg[1] then
+ if e.protocol ~= "_shunt" and e.protocol ~= "_iface" then
+ o1:value(e[".name"], e["remark"])
+ o1.group[#o1.group+1] = (e["group"] and e["group"] ~= "") and e["group"] or translate("default")
+ end
+ if not e.protocol:find("_") then
+ -- Landing Node not support use special node.
+ o2:value(e[".name"], e["remark"])
+ o2.group[#o2.group+1] = (e["group"] and e["group"] ~= "") and e["group"] or translate("default")
+ end
end
end
diff --git a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua
index 7df083e8d4..ae18e369f0 100644
--- a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua
+++ b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua
@@ -41,9 +41,8 @@ o = s:option(Value, _n("timeout"), translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
-o = s:option(ListValue, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
-o:value("false")
-o:value("true")
+o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
+o.default = 0
o = s:option(Flag, _n("plugin_enabled"), translate("plugin"))
o.default = 0
diff --git a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss.lua b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss.lua
index e088a459e8..b1abf4a587 100644
--- a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss.lua
+++ b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss.lua
@@ -42,9 +42,8 @@ o = s:option(Value, _n("timeout"), translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
-o = s:option(ListValue, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
-o:value("false")
-o:value("true")
+o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
+o.default = 0
o = s:option(Flag, _n("plugin_enabled"), translate("plugin"))
o.default = 0
diff --git a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua
index 2c0e1498a1..f23e8efa3d 100644
--- a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua
+++ b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua
@@ -64,8 +64,7 @@ o = s:option(Value, _n("timeout"), translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
-o = s:option(ListValue, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
-o:value("false")
-o:value("true")
+o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
+o.default = 0
api.luci_types(arg[1], m, s, type_name, option_prefix)
diff --git a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua
index c7c8939d0d..19b6861630 100644
--- a/small/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua
+++ b/small/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua
@@ -18,6 +18,10 @@ local x_ss_method_list = {
"none", "plain", "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
+local header_type_list = {
+ "none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
+}
+
-- [[ Xray ]]
s.fields["type"]:value(type_name, "Xray")
@@ -318,17 +322,11 @@ o:depends({ [_n("tcp_guise")] = "http" })
-- [[ mKCP ]]--
o = s:option(ListValue, _n("mkcp_guise"), translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.'))
-o:value("none", "none")
-o:value("header-srtp", "srtp")
-o:value("header-utp", "utp")
-o:value("header-wechat", "wechat-video")
-o:value("header-dtls", "dtls")
-o:value("header-wireguard", "wireguard")
-o:value("header-dns", "dns")
+for a, t in ipairs(header_type_list) do o:value(t) end
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
-o:depends({ [_n("mkcp_guise")] = "header-dns" })
+o:depends({ [_n("mkcp_guise")] = "dns" })
o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU"))
o.default = "1350"
@@ -368,6 +366,10 @@ o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"),
o.default = "0"
o:depends({ [_n("custom")] = false })
+o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
+o.default = "0"
+o:depends({ [_n("custom")] = false })
+
-- [[ Fallback ]]--
o = s:option(Flag, _n("fallback"), translate("Fallback"))
o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "raw" })
diff --git a/small/luci-app-passwall2/luasrc/passwall2/api.lua b/small/luci-app-passwall2/luasrc/passwall2/api.lua
index 8a7e4730ee..5b4511c1e6 100644
--- a/small/luci-app-passwall2/luasrc/passwall2/api.lua
+++ b/small/luci-app-passwall2/luasrc/passwall2/api.lua
@@ -20,8 +20,6 @@ CACHE_PATH = "/tmp/etc/passwall2_tmp"
TMP_PATH = "/tmp/etc/" .. appname
TMP_IFACE_PATH = TMP_PATH .. "/iface"
-NEW_PORT = nil
-
local lang = uci:get("luci", "main", "lang") or "auto"
if lang == "auto" then
local auto_lang = uci:get(appname, "@global[0]", "auto_lang")
@@ -120,12 +118,7 @@ end
function get_new_port()
local cmd_format = ". /usr/share/passwall2/utils.sh ; echo -n $(get_new_port %s tcp,udp)"
- local set_port = 0
- if NEW_PORT and tonumber(NEW_PORT) then
- set_port = tonumber(NEW_PORT) + 1
- end
- NEW_PORT = tonumber(sys.exec(string.format(cmd_format, set_port == 0 and "auto" or set_port)))
- return NEW_PORT
+ return tonumber(sys.exec(string.format(cmd_format, "auto")))
end
function exec_call(cmd)
@@ -164,6 +157,18 @@ function base64Encode(text)
return result
end
+function UrlEncode(szText)
+ return szText:gsub("([^%w%-_%.%~])", function(c)
+ return string.format("%%%02X", string.byte(c))
+ end)
+end
+
+function UrlDecode(szText)
+ return szText and szText:gsub("%+", " "):gsub("%%(%x%x)", function(h)
+ return string.char(tonumber(h, 16))
+ end) or nil
+end
+
-- Extract the domain name and port from the URL (no IP address).
function get_domain_port_from_url(url)
local scheme, domain, port = string.match(url, "^(https?)://([%w%.%-]+):?(%d*)")
@@ -1427,3 +1432,50 @@ function apply_redirect(m)
sys.call("/bin/rm -f " .. tmp_uci_file)
end
end
+
+function match_node_rule(name, rule)
+ if not name then return false end
+ if not rule or rule == "" then return true end
+ -- split rule by &&
+ local function split_and(expr)
+ local t = {}
+ for part in expr:gmatch("[^&]+") do
+ part = part:gsub("^%s+", ""):gsub("%s+$", "")
+ if part ~= "" then
+ table.insert(t, part)
+ end
+ end
+ return t
+ end
+ -- match single condition
+ local function match_cond(str, cond)
+ if cond == "" then
+ return true
+ end
+ -- exclude: !xxx
+ if cond:sub(1, 1) == "!" then
+ local k = cond:sub(2)
+ if k == "" then return true end
+ return not str:find(k, 1, true)
+ end
+ -- prefix: ^xxx
+ if cond:sub(1, 1) == "^" then
+ local k = cond:sub(2)
+ return str:sub(1, #k) == k
+ end
+ -- suffix: xxx$
+ if cond:sub(-1) == "$" then
+ local k = cond:sub(1, -2)
+ return str:sub(-#k) == k
+ end
+ -- contains
+ return str:find(cond, 1, true) ~= nil
+ end
+ -- AND logic
+ for _, cond in ipairs(split_and(rule)) do
+ if not match_cond(name, cond) then
+ return false
+ end
+ end
+ return true
+end
diff --git a/small/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua b/small/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua
index 77b57115d6..e543d91510 100644
--- a/small/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua
+++ b/small/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua
@@ -71,7 +71,7 @@ function gen_config(var)
password = node.password,
method = node.method,
timeout = tonumber(node.timeout),
- fast_open = (node.tcp_fast_open and node.tcp_fast_open == "true") and true or false,
+ fast_open = (node.tcp_fast_open and node.tcp_fast_open == "1") and true or false,
reuse_port = true
}
@@ -98,7 +98,7 @@ function gen_config(var)
}
},
locals = {},
- fast_open = (node.tcp_fast_open and node.tcp_fast_open == "true") and true or false
+ fast_open = (node.tcp_fast_open and node.tcp_fast_open == "1") and true or false
}
if local_socks_address and local_socks_port then
table.insert(config.locals, {
diff --git a/small/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/small/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua
index fa0c9b5f09..753215478c 100644
--- a/small/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua
+++ b/small/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua
@@ -1095,9 +1095,10 @@ function gen_config(var)
local socks_node = uci:get_all(appname, socks_id) or nil
if socks_node then
if not remarks then
- remarks = "Socks_" .. socks_node.port
+ remarks = socks_node.port
end
result = {
+ [".name"] = "Socksid_" .. socks_id,
remarks = remarks,
type = "sing-box",
protocol = "socks",
@@ -1109,7 +1110,43 @@ function gen_config(var)
return result
end
- function gen_urltest(_node)
+ local nodes_list = {}
+ function get_urltest_batch_nodes(_node)
+ if #nodes_list == 0 then
+ for k, e in ipairs(api.get_valid_nodes()) do
+ if e.node_type == "normal" and (not e.chain_proxy or e.chain_proxy == "") then
+ nodes_list[#nodes_list + 1] = {
+ id = e[".name"],
+ remarks = e["remarks"],
+ group = e["group"]
+ }
+ end
+ end
+ end
+ if not _node.node_group or _node.node_group == "" then return {} end
+ local nodes = {}
+ for g in _node.node_group:gmatch("%S+") do
+ g = api.UrlDecode(g)
+ for k, v in pairs(nodes_list) do
+ local gn = (v.group and v.group ~= "") and v.group or "default"
+ if gn == g and api.match_node_rule(v.remarks, _node.node_match_rule) then
+ nodes[#nodes + 1] = v.id
+ end
+ end
+ end
+ return nodes
+ end
+
+ function get_node_by_id(node_id)
+ if not node_id or node_id == "" or node_id == "nil" then return nil end
+ if node_id:find("Socks_") then
+ return gen_socks_config_node(node_id)
+ else
+ return uci:get_all(appname, node_id)
+ end
+ end
+
+ function gen_urltest_outbound(_node)
local urltest_id = _node[".name"]
local urltest_tag = "urltest-" .. urltest_id
-- existing urltest
@@ -1119,7 +1156,13 @@ function gen_config(var)
end
end
-- new urltest
- local ut_nodes = _node.urltest_node
+ local ut_nodes
+ if _node.node_add_mode and _node.node_add_mode == "batch" then
+ ut_nodes = get_urltest_batch_nodes(_node)
+ else
+ ut_nodes = _node.urltest_node
+ end
+ if #ut_nodes == 0 then return nil end
local valid_nodes = {}
for i = 1, #ut_nodes do
local ut_node_id = ut_nodes[i]
@@ -1133,21 +1176,9 @@ function gen_config(var)
end
end
if is_new_ut_node then
- local ut_node
- if ut_node_id:find("Socks_") then
- ut_node = gen_socks_config_node(ut_node_id)
- else
- ut_node = uci:get_all(appname, ut_node_id)
- end
- if ut_node then
- local outbound = gen_outbound(flag, ut_node, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- if ut_node.remarks then
- outbound.tag = outbound.tag .. ":" .. ut_node.remarks
- end
- table.insert(outbounds, outbound)
- valid_nodes[#valid_nodes + 1] = outbound.tag
- end
+ local outboundTag = gen_outbound_get_tag(flag, ut_node_id, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
+ if outboundTag then
+ valid_nodes[#valid_nodes + 1] = outboundTag
end
end
end
@@ -1162,8 +1193,7 @@ function gen_config(var)
idle_timeout = (api.format_go_time(_node.urltest_idle_timeout) ~= "0s") and api.format_go_time(_node.urltest_idle_timeout) or "30m",
interrupt_exist_connections = (_node.urltest_interrupt_exist_connections == "true" or _node.urltest_interrupt_exist_connections == "1") and true or false
}
- table.insert(outbounds, outbound)
- return urltest_tag
+ return outbound
end
function set_outbound_detour(node, outbound, outbounds_table, shunt_rule_name)
@@ -1197,9 +1227,16 @@ function gen_config(var)
if outbound["_flag_proxy_tag"] then
--Ignore
else
- local preproxy_node = uci:get_all(appname, node.preproxy_node)
+ local preproxy_node = get_node_by_id(node.preproxy_node)
if preproxy_node then
- local preproxy_outbound = gen_outbound(node[".name"], preproxy_node)
+ local preproxy_outbound
+ if preproxy_node.protocol == "_urltest" then
+ if preproxy_node.urltest_node then
+ preproxy_outbound = gen_urltest_outbound(preproxy_node)
+ end
+ else
+ preproxy_outbound = gen_outbound(node[".name"], preproxy_node)
+ end
if preproxy_outbound then
preproxy_outbound.tag = preproxy_node[".name"]
if preproxy_node.remarks then
@@ -1214,7 +1251,13 @@ function gen_config(var)
end
end
if node.chain_proxy == "2" and node.to_node then
- local to_node = uci:get_all(appname, node.to_node)
+ local to_node = get_node_by_id(node.to_node)
+ if to_node then
+ -- Landing Node not support use special node.
+ if to_node.protocol:find("_") then
+ to_node = nil
+ end
+ end
if to_node then
local to_outbound
if to_node.type ~= "sing-box" then
@@ -1265,122 +1308,90 @@ function gen_config(var)
return default_outTag, last_insert_outbound
end
+ function gen_outbound_get_tag(flag, node_id, tag, proxy_table)
+ if not node_id or node_id == "nil" then return nil end
+ local node
+ if type(node_id) == "string" then
+ node = get_node_by_id(node_id)
+ elseif type(node_id) == "table" then
+ node = node_id
+ end
+ if node then
+ if node.protocol == "_iface" then
+ if node.iface then
+ local outbound = {
+ tag = tag,
+ type = "direct",
+ bind_interface = node.iface,
+ routing_mark = 255,
+ }
+ table.insert(outbounds, outbound)
+ sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
+ return outbound.tag
+ end
+ return nil
+ end
+ if proxy_table.chain_proxy == "1" or proxy_table.chain_proxy == "2" then
+ node.chain_proxy = proxy_table.chain_proxy
+ node.preproxy_node = proxy_table.chain_proxy == "1" and proxy_table.preproxy_node
+ node.to_node = proxy_table.chain_proxy == "2" and proxy_table.to_node
+ proxy_table.chain_proxy = nil
+ proxy_table.preproxy_node = nil
+ proxy_table.to_node = nil
+ end
+ local outbound
+ if node.protocol == "_urltest" then
+ if node.urltest_node then
+ outbound = gen_urltest_outbound(node)
+ end
+ else
+ outbound = gen_outbound(flag, node, tag, proxy_table)
+ end
+ if outbound then
+ if node.remarks then
+ outbound.tag = outbound.tag .. ":" .. node.remarks
+ end
+ local default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
+ table.insert(outbounds, outbound)
+ if last_insert_outbound then
+ table.insert(outbounds, last_insert_outbound)
+ end
+ return default_outbound_tag
+ end
+ end
+ end
+
rules = {}
if node.protocol == "_shunt" then
- local preproxy_rule_name = node.preproxy_enabled == "1" and "main" or nil
- local preproxy_tag = preproxy_rule_name
- local preproxy_node_id = preproxy_rule_name and node["main_node"] or nil
-
inner_fakedns = node.fakedns or "0"
local function gen_shunt_node(rule_name, _node_id)
if not rule_name then return nil end
if not _node_id then _node_id = node[rule_name] end
- local rule_outboundTag
if _node_id == "_direct" then
- rule_outboundTag = "direct"
+ return "direct"
elseif _node_id == "_blackhole" then
- rule_outboundTag = "block"
+ return "block"
elseif _node_id == "_default" and rule_name ~= "default" then
- rule_outboundTag = "default"
- elseif _node_id and _node_id:find("Socks_") then
- local socks_node = gen_socks_config_node(_node_id)
- local _outbound = gen_outbound(flag, socks_node, rule_name)
- if _outbound then
- table.insert(outbounds, _outbound)
- rule_outboundTag = _outbound.tag
- end
+ return "default"
elseif _node_id then
- local _node = uci:get_all(appname, _node_id)
- if not _node then return nil end
-
- if api.is_normal_node(_node) then
- local use_proxy = preproxy_tag and node[rule_name .. "_proxy_tag"] == preproxy_rule_name and _node_id ~= preproxy_node_id
- local copied_outbound
- for index, value in ipairs(outbounds) do
- if value["_id"] == _node_id and value["_flag_proxy_tag"] == (use_proxy and preproxy_tag or nil) then
- copied_outbound = api.clone(value)
- break
- end
- end
- if copied_outbound then
- copied_outbound.tag = rule_name .. ":" .. _node.remarks
- table.insert(outbounds, copied_outbound)
- rule_outboundTag = copied_outbound.tag
- else
- if use_proxy then
- local pre_proxy = nil
- if _node.type ~= "sing-box" then
- pre_proxy = true
- end
- if pre_proxy then
- local new_port = api.get_new_port()
- table.insert(inbounds, {
- type = "direct",
- tag = "proxy_" .. rule_name,
- listen = "127.0.0.1",
- listen_port = new_port,
- override_address = _node.address,
- override_port = tonumber(_node.port),
- })
- if _node.tls_serverName == nil then
- _node.tls_serverName = _node.address
- end
- _node.address = "127.0.0.1"
- _node.port = new_port
- table.insert(rules, 1, {
- inbound = {"proxy_" .. rule_name},
- outbound = preproxy_tag,
- })
- end
- end
- local proxy_table = {
- tag = use_proxy and preproxy_tag or nil,
- run_socks_instance = not no_run
- }
- if not proxy_table.tag then
- if singbox_settings.fragment == "1" then
- proxy_table.fragment = true
- end
- if singbox_settings.record_fragment == "1" then
- proxy_table.record_fragment = true
- end
- end
- local _outbound = gen_outbound(flag, _node, rule_name, proxy_table)
- if _outbound then
- _outbound.tag = _outbound.tag .. ":" .. _node.remarks
- rule_outboundTag, last_insert_outbound = set_outbound_detour(_node, _outbound, outbounds, rule_name)
- table.insert(outbounds, _outbound)
- if last_insert_outbound then
- table.insert(outbounds, last_insert_outbound)
- end
- end
- end
- elseif _node.protocol == "_urltest" then
- rule_outboundTag = gen_urltest(_node)
- elseif _node.protocol == "_iface" then
- if _node.iface then
- local _outbound = {
- type = "direct",
- tag = rule_name .. ":" .. _node.remarks,
- bind_interface = _node.iface,
- routing_mark = 255,
- }
- table.insert(outbounds, _outbound)
- rule_outboundTag = _outbound.tag
- sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, _node.iface))
- end
+ local proxy_table = {
+ fragment = singbox_settings.fragment == "1",
+ record_fragment = singbox_settings.record_fragment == "1",
+ run_socks_instance = not no_run,
+ }
+ local preproxy_node_id = node[rule_name .. "_proxy_tag"]
+ if preproxy_node_id == _node_id then preproxy_node_id = nil end
+ if preproxy_node_id then
+ proxy_table.chain_proxy = "2"
+ proxy_table.to_node = _node_id
+ return gen_outbound_get_tag(flag, preproxy_node_id, rule_name, proxy_table)
+ else
+ return gen_outbound_get_tag(flag, _node_id, rule_name, proxy_table)
end
end
- return rule_outboundTag
- end
-
- if preproxy_tag and preproxy_node_id then
- local preproxy_outboundTag = gen_shunt_node(preproxy_rule_name, preproxy_node_id)
- if preproxy_outboundTag then
- preproxy_tag = preproxy_outboundTag
- end
+ return nil
end
--default_node
@@ -1582,32 +1593,12 @@ function gen_config(var)
table.insert(rules, rule)
end
end)
- elseif node.protocol == "_urltest" then
- if node.urltest_node then
- COMMON.default_outbound_tag = gen_urltest(node)
- end
- elseif node.protocol == "_iface" then
- if node.iface then
- local outbound = {
- type = "direct",
- tag = node.remarks or node_id,
- bind_interface = node.iface,
- routing_mark = 255,
- }
- table.insert(outbounds, outbound)
- COMMON.default_outbound_tag = outbound.tag
- sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
- end
else
- local outbound = gen_outbound(flag, node, nil, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- outbound.tag = outbound.tag .. ":" .. node.remarks
- COMMON.default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
- table.insert(outbounds, outbound)
- if last_insert_outbound then
- table.insert(outbounds, last_insert_outbound)
- end
- end
+ COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node, nil, {
+ fragment = singbox_settings.fragment == "1" or nil,
+ record_fragment = singbox_settings.record_fragment == "1" or nil,
+ run_socks_instance = not no_run
+ })
end
for index, value in ipairs(rules) do
@@ -1730,7 +1721,7 @@ function gen_config(var)
local default_dns_flag = "remote"
if node_id and redir_port then
- local node = uci:get_all(appname, node_id)
+ local node = get_node_by_id(node_id)
if node.protocol == "_shunt" then
if node.default_node == "_direct" then
default_dns_flag = "direct"
diff --git a/small/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/small/luci-app-passwall2/luasrc/passwall2/util_xray.lua
index 80edc8ebb1..ca122836f4 100644
--- a/small/luci-app-passwall2/luasrc/passwall2/util_xray.lua
+++ b/small/luci-app-passwall2/luasrc/passwall2/util_xray.lua
@@ -58,7 +58,7 @@ function gen_outbound(flag, node, tag, proxy_table)
run_socks_instance = proxy_table.run_socks_instance
end
- if node.type ~= "Xray" then
+ if node.type ~= "Xray" or node.protocol == "_balancing" then
local relay_port = node.port
local new_port = api.get_new_port()
local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
@@ -282,9 +282,11 @@ function gen_outbound(flag, node, tag, proxy_table)
finalmask = (node.transport == "mkcp") and {
udp = (function()
local t = {}
+ local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
+ dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
if node.mkcp_guise and node.mkcp_guise ~= "none" then
- local g = { type = node.mkcp_guise }
- if node.mkcp_guise == "header-dns" and node.mkcp_domain and node.mkcp_domain ~= "" then
+ local g = { type = map[node.mkcp_guise] }
+ if node.mkcp_guise == "dns" and node.mkcp_domain and node.mkcp_domain ~= "" then
g.settings = { domain = node.mkcp_domain }
end
t[#t + 1] = g
@@ -592,9 +594,11 @@ function gen_config_server(node)
finalmask = (node.transport == "mkcp") and {
udp = (function()
local t = {}
+ local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
+ dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
if node.mkcp_guise and node.mkcp_guise ~= "none" then
- local g = { type = node.mkcp_guise }
- if node.mkcp_guise == "header-dns" and node.mkcp_domain and node.mkcp_domain ~= "" then
+ local g = { type = map[node.mkcp_guise] }
+ if node.mkcp_guise == "dns" and node.mkcp_domain and node.mkcp_domain ~= "" then
g.settings = { domain = node.mkcp_domain }
end
t[#t + 1] = g
@@ -608,6 +612,7 @@ function gen_config_server(node)
end)()
} or nil,
sockopt = {
+ tcpFastOpen = (node.tcp_fast_open == "1") and true or nil,
acceptProxyProtocol = (node.acceptProxyProtocol and node.acceptProxyProtocol == "1") and true or false
}
}
@@ -757,9 +762,10 @@ function gen_config(var)
local socks_node = uci:get_all(appname, socks_id) or nil
if socks_node then
if not remarks then
- remarks = "Socks_" .. socks_node.port
+ remarks = socks_node.port
end
result = {
+ [".name"] = "Socksid_" .. socks_id,
remarks = remarks,
type = "Xray",
protocol = "socks",
@@ -772,15 +778,51 @@ function gen_config(var)
return result
end
- function gen_loopback(outboundTag, dst_node_id)
- if not outboundTag then return nil end
- local inboundTag = dst_node_id and "loop-in-" .. dst_node_id or outboundTag .. "-lo"
+ function get_node_by_id(node_id)
+ if not node_id or node_id == "" or node_id == "nil" then return nil end
+ if node_id:find("Socks_") then
+ return gen_socks_config_node(node_id)
+ else
+ return uci:get_all(appname, node_id)
+ end
+ end
+
+ local nodes_list = {}
+ function get_balancer_batch_nodes(_node)
+ if #nodes_list == 0 then
+ for k, e in ipairs(api.get_valid_nodes()) do
+ if e.node_type == "normal" and (not e.chain_proxy or e.chain_proxy == "") then
+ nodes_list[#nodes_list + 1] = {
+ id = e[".name"],
+ remarks = e["remarks"],
+ group = e["group"]
+ }
+ end
+ end
+ end
+ if not _node.node_group or _node.node_group == "" then return {} end
+ local nodes = {}
+ for g in _node.node_group:gmatch("%S+") do
+ g = api.UrlDecode(g)
+ for k, v in pairs(nodes_list) do
+ local gn = (v.group and v.group ~= "") and v.group or "default"
+ if gn == g and api.match_node_rule(v.remarks, _node.node_match_rule) then
+ nodes[#nodes + 1] = v.id
+ end
+ end
+ end
+ return nodes
+ end
+
+ function gen_loopback(outbound_tag, loopback_dst)
+ if not outbound_tag or outbound_tag == "" then return nil end
+ local inbound_tag = loopback_dst and "lo-to-" .. loopback_dst or outbound_tag .. "-lo"
table.insert(outbounds, {
protocol = "loopback",
- tag = outboundTag,
- settings = { inboundTag = inboundTag }
+ tag = outbound_tag,
+ settings = { inboundTag = inbound_tag }
})
- return inboundTag
+ return inbound_tag
end
function gen_balancer(_node, loopback_tag)
@@ -796,7 +838,12 @@ function gen_config(var)
end
end
-- new balancer
- local blc_nodes = _node.balancing_node
+ local blc_nodes
+ if _node.node_add_mode and _node.node_add_mode == "batch" then
+ blc_nodes = get_balancer_batch_nodes(_node)
+ else
+ blc_nodes = _node.balancing_node
+ end
local valid_nodes = {}
for i = 1, #blc_nodes do
local blc_node_id = blc_nodes[i]
@@ -810,21 +857,9 @@ function gen_config(var)
end
end
if is_new_blc_node then
- local blc_node
- if blc_node_id:find("Socks_") then
- blc_node = gen_socks_config_node(blc_node_id)
- else
- blc_node = uci:get_all(appname, blc_node_id)
- end
- if blc_node then
- local outbound = gen_outbound(flag, blc_node, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- if blc_node.remarks then
- outbound.tag = outbound.tag .. ":" .. blc_node.remarks
- end
- table.insert(outbounds, outbound)
- valid_nodes[#valid_nodes + 1] = outbound.tag
- end
+ local outboundTag = gen_outbound_get_tag(flag, blc_node_id, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
+ if outboundTag then
+ valid_nodes[#valid_nodes + 1] = outboundTag
end
end
end
@@ -844,21 +879,12 @@ function gen_config(var)
end
end
if is_new_node then
- local fallback_node
- if fallback_node_id:find("Socks_") then
- fallback_node = gen_socks_config_node(fallback_node_id)
- else
- fallback_node = uci:get_all(appname, fallback_node_id)
- end
+ local fallback_node = get_node_by_id(fallback_node_id)
if fallback_node then
if fallback_node.protocol ~= "_balancing" then
- local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- if fallback_node.remarks then
- outbound.tag = outbound.tag .. ":" .. fallback_node.remarks
- end
- table.insert(outbounds, outbound)
- fallback_node_tag = outbound.tag
+ local outboundTag = gen_outbound_get_tag(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
+ if outboundTag then
+ fallback_node_tag = outboundTag
end
else
if gen_balancer(fallback_node) then
@@ -923,7 +949,7 @@ function gen_config(var)
if outbound["_flag_proxy_tag"] then
--Ignore
else
- local preproxy_node = uci:get_all(appname, node.preproxy_node)
+ local preproxy_node = get_node_by_id(node.preproxy_node)
if preproxy_node then
local preproxy_outbound = gen_outbound(node[".name"], preproxy_node)
if preproxy_outbound then
@@ -943,7 +969,13 @@ function gen_config(var)
end
end
if node.chain_proxy == "2" and node.to_node then
- local to_node = uci:get_all(appname, node.to_node)
+ local to_node = get_node_by_id(node.to_node)
+ if to_node then
+ -- Landing Node not support use special node.
+ if to_node.protocol:find("_") then
+ to_node = nil
+ end
+ end
if to_node then
local to_outbound
if to_node.type ~= "Xray" then
@@ -996,18 +1028,66 @@ function gen_config(var)
return default_outTag, last_insert_outbound
end
+ function gen_outbound_get_tag(flag, node_id, tag, proxy_table)
+ if not node_id or node_id == "nil" then return nil end
+ local node
+ if type(node_id) == "string" then
+ node = get_node_by_id(node_id)
+ elseif type(node_id) == "table" then
+ node = node_id
+ end
+ if node then
+ if node.protocol == "_iface" then
+ if node.iface then
+ local outbound = {
+ tag = tag,
+ protocol = "freedom",
+ streamSettings = {
+ sockopt = {
+ mark = 255,
+ interface = node.iface
+ }
+ }
+ }
+ table.insert(outbounds, outbound)
+ sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
+ return outbound.tag
+ end
+ return nil
+ end
+ if proxy_table.chain_proxy == "1" or proxy_table.chain_proxy == "2" then
+ node.chain_proxy = proxy_table.chain_proxy
+ node.preproxy_node = proxy_table.chain_proxy == "1" and proxy_table.preproxy_node
+ node.to_node = proxy_table.chain_proxy == "2" and proxy_table.to_node
+ proxy_table.chain_proxy = nil
+ proxy_table.preproxy_node = nil
+ proxy_table.to_node = nil
+ end
+ local outbound = gen_outbound(flag, node, tag, proxy_table)
+ if outbound then
+ if node.remarks then
+ outbound.tag = outbound.tag .. ":" .. node.remarks
+ end
+ local default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
+ if tag == "default" then
+ table.insert(outbounds, 1, outbound)
+ else
+ table.insert(outbounds, outbound)
+ end
+ if last_insert_outbound then
+ table.insert(outbounds, last_insert_outbound)
+ end
+ return default_outbound_tag
+ end
+ end
+ end
+
if node then
if server_host and server_port then
node.address = server_host
node.port = server_port
end
if node.protocol == "_shunt" then
- local preproxy_rule_name = node.preproxy_enabled == "1" and "main" or nil
- local preproxy_tag = preproxy_rule_name
- local preproxy_node_id = node["main_node"]
- local preproxy_outbound_tag, preproxy_balancer_tag
- local preproxy_nodes
-
inner_fakedns = node.fakedns or "0"
local function gen_shunt_node(rule_name, _node_id)
@@ -1017,144 +1097,25 @@ function gen_config(var)
return "direct", nil
elseif _node_id == "_blackhole" then
return "blackhole", nil
- elseif _node_id == "_default" then
+ elseif _node_id == "_default" and rule_name ~= "default" then
return "default", nil
- elseif _node_id and _node_id:find("Socks_") then
- local socks_tag
- local socks_node = gen_socks_config_node(_node_id)
- local outbound = gen_outbound(flag, socks_node, rule_name)
- if outbound then
- if rule_name == "default" then
- table.insert(outbounds, 1, outbound)
- else
- table.insert(outbounds, outbound)
- end
- socks_tag = outbound.tag
- end
- return socks_tag, nil
elseif _node_id then
- local _node = uci:get_all(appname, _node_id)
- if not _node then return nil, nil end
-
- if api.is_normal_node(_node) then
- local use_proxy = preproxy_tag and node[rule_name .. "_proxy_tag"] == preproxy_rule_name and _node_id ~= preproxy_node_id
- if use_proxy and preproxy_balancer_tag and preproxy_nodes[_node_id] then use_proxy = false end
- local copied_outbound
- for index, value in ipairs(outbounds) do
- if value["_id"] == _node_id and value["_flag_proxy_tag"] == (use_proxy and preproxy_tag or nil) then
- copied_outbound = api.clone(value)
- break
- end
- end
- if copied_outbound then
- copied_outbound.tag = rule_name .. ":" .. _node.remarks
- table.insert(outbounds, copied_outbound)
- return copied_outbound.tag, nil
- else
- if use_proxy and _node.type ~= "Xray" then
- local new_port = api.get_new_port()
- table.insert(inbounds, {
- tag = "proxy_" .. rule_name,
- listen = "127.0.0.1",
- port = new_port,
- protocol = "dokodemo-door",
- settings = {network = "tcp,udp", address = _node.address, port = tonumber(_node.port)}
- })
- if _node.tls_serverName == nil then
- _node.tls_serverName = _node.address
- end
- _node.address = "127.0.0.1"
- _node.port = new_port
- table.insert(rules, 1, {
- inboundTag = {"proxy_" .. rule_name},
- outboundTag = not preproxy_balancer_tag and preproxy_tag or nil,
- balancerTag = preproxy_balancer_tag
- })
- end
- local proxy_table = {
- tag = use_proxy and preproxy_tag or nil,
- run_socks_instance = not no_run
- }
- if not proxy_table.tag then
- if xray_settings.fragment == "1" then
- proxy_table.fragment = true
- end
- if xray_settings.noise == "1" then
- proxy_table.noise = true
- end
- end
- local outbound = gen_outbound(flag, _node, rule_name, proxy_table)
- local outbound_tag
- if outbound then
- outbound.tag = outbound.tag .. ":" .. _node.remarks
- outbound_tag, last_insert_outbound = set_outbound_detour(_node, outbound, outbounds, rule_name)
- if rule_name == "default" then
- table.insert(outbounds, 1, outbound)
- else
- table.insert(outbounds, outbound)
- end
- if last_insert_outbound then
- table.insert(outbounds, last_insert_outbound)
- end
- end
- return outbound_tag, nil
- end
- elseif _node.protocol == "_balancing" then
- local blc_tag = gen_balancer(_node, rule_name)
- if rule_name == "default" then
- for i, ob in ipairs(outbounds) do
- if ob.protocol == "loopback" and ob.tag == "default" then
- if i > 1 then table.insert(outbounds, 1, table.remove(outbounds, i)) end
- break
- end
- end
- end
- return nil, blc_tag
- elseif _node.protocol == "_iface" then
- local outbound_tag
- if _node.iface then
- local outbound = {
- protocol = "freedom",
- tag = rule_name,
- streamSettings = {
- sockopt = {
- mark = 255,
- interface = _node.iface
- }
- }
- }
- outbound_tag = outbound.tag
- if rule_name == "default" then
- table.insert(outbounds, 1, outbound)
- else
- table.insert(outbounds, outbound)
- end
- sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, _node.iface))
- end
- return outbound_tag, nil
+ local proxy_table = {
+ fragment = xray_settings.fragment == "1",
+ noise = xray_settings.noise == "1",
+ run_socks_instance = not no_run,
+ }
+ local preproxy_node_id = node[rule_name .. "_proxy_tag"]
+ if preproxy_node_id == _node_id then preproxy_node_id = nil end
+ if preproxy_node_id then
+ proxy_table.chain_proxy = "2"
+ proxy_table.to_node = _node_id
+ return gen_outbound_get_tag(flag, preproxy_node_id, rule_name, proxy_table), nil
+ else
+ return gen_outbound_get_tag(flag, _node_id, rule_name, proxy_table), nil
end
end
- end
-
- if preproxy_tag and preproxy_node_id then
- preproxy_outbound_tag, preproxy_balancer_tag = gen_shunt_node(preproxy_rule_name, preproxy_node_id)
- if preproxy_balancer_tag then
- local _node_id = preproxy_node_id
- preproxy_nodes = {}
- while _node_id do
- _node = uci:get_all(appname, _node_id)
- if not _node then break end
- if _node.protocol ~= "_balancing" then
- preproxy_nodes[_node_id] = true
- break
- end
- local _blc_nodes = _node.balancing_node
- for i = 1, #_blc_nodes do preproxy_nodes[_blc_nodes[i]] = true end
- _node_id = _node.fallback_node
- end
- elseif preproxy_outbound_tag then
- preproxy_tag = preproxy_outbound_tag
- end
+ return nil, nil
end
--default_node
@@ -1290,7 +1251,13 @@ function gen_config(var)
rules = rules
}
elseif node.protocol == "_balancing" then
- if node.balancing_node then
+ local blc_nodes
+ if node.node_add_mode and node.node_add_mode == "batch" then
+ blc_nodes = get_balancer_batch_nodes(node)
+ else
+ blc_nodes = node.balancing_node
+ end
+ if blc_nodes and #blc_nodes > 0 then
local balancer_tag = gen_balancer(node)
if balancer_tag then
table.insert(rules, { network = "tcp,udp", balancerTag = balancer_tag })
@@ -1301,31 +1268,13 @@ function gen_config(var)
}
COMMON.default_balancer_tag = balancer_tag
end
- elseif node.protocol == "_iface" then
- if node.iface then
- local outbound = {
- protocol = "freedom",
- tag = node.remarks or node_id,
- streamSettings = {
- sockopt = {
- mark = 255,
- interface = node.iface
- }
- }
- }
- table.insert(outbounds, outbound)
- COMMON.default_outbound_tag = outbound.tag
- sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
- end
else
- local outbound = gen_outbound(flag, node, nil, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
- if outbound then
- outbound.tag = outbound.tag .. ":" .. node.remarks
- COMMON.default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
- table.insert(outbounds, outbound)
- if last_insert_outbound then
- table.insert(outbounds, last_insert_outbound)
- end
+ COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node, nil, {
+ fragment = xray_settings.fragment == "1" or nil,
+ noise = xray_settings.noise == "1" or nil,
+ run_socks_instance = not no_run
+ })
+ if COMMON.default_outbound_tag then
routing = {
domainStrategy = "AsIs",
domainMatcher = "hybrid",
diff --git a/small/luci-app-passwall2/luasrc/view/passwall2/include/shunt_options.htm b/small/luci-app-passwall2/luasrc/view/passwall2/include/shunt_options.htm
index d5606ee509..b78b4967e9 100644
--- a/small/luci-app-passwall2/luasrc/view/passwall2/include/shunt_options.htm
+++ b/small/luci-app-passwall2/luasrc/view/passwall2/include/shunt_options.htm
@@ -6,6 +6,7 @@
diff --git a/small/luci-app-passwall2/po/fa/passwall2.po b/small/luci-app-passwall2/po/fa/passwall2.po
index e1c2f2dafe..1cc2b0a82d 100644
--- a/small/luci-app-passwall2/po/fa/passwall2.po
+++ b/small/luci-app-passwall2/po/fa/passwall2.po
@@ -453,14 +453,11 @@ msgstr "پیشپروکسی"
msgid "Preproxy Node"
msgstr "گره پیشپروکسی"
-msgid ""
-"Set the node to be used as a pre-proxy. Each rule (including Default"
-"code>) has a separate switch that controls whether this rule uses the pre-"
-"proxy or not."
-msgstr ""
-"گرهای را که باید به عنوان پیشپروکسی استفاده شود تنظیم کنید. هر قانون (شامل Default"
-"code>) دارای یک سوئیچ جداگانه است که کنترل میکند آیا این قانون از پیش-"
-"پروکسی استفاده کند یا خیر."
+msgid "Set the node to be used as a pre-proxy."
+msgstr "گره را طوری تنظیم کنید که به عنوان پیش پروکسی استفاده شود."
+
+msgid "Each rule has a separate switch that controls whether this rule uses the pre-proxy or not."
+msgstr "هر قانون یک سوئیچ جداگانه دارد که کنترل میکند آیا این قانون از پیش-پروکسی استفاده میکند یا خیر."
msgid "Close (Not use)"
msgstr "بستن (عدم استفاده)"
@@ -474,12 +471,6 @@ msgstr "اتصال مستقیم"
msgid "Blackhole (Block)"
msgstr "سیاهچاله (مسدودسازی)"
-msgid "Use preproxy node"
-msgstr "استفاده از گره پیشپروکسی"
-
-msgid "Default Preproxy"
-msgstr "پیشپروکسی پیشفرض"
-
msgid "There are no available nodes, please add or subscribe nodes first."
msgstr "هیچ گره در دسترس نیست، لطفاً ابتدا گرهها را اضافه یا مشترک شوید."
@@ -813,18 +804,6 @@ msgstr "سوئیچ بازگردانی"
msgid "When detects main node is available, switch back to the main node."
msgstr "زمانی که در دسترس بودن گره اصلی تشخیص داده شود، به گره اصلی بازگردانده میشود."
-msgid "If the main node is shunt"
-msgstr "اگر گره اصلی شنت (Shunt) باشد"
-
-msgid "Switch it"
-msgstr "سوئیچ کردن"
-
-msgid "Applying to the default node"
-msgstr "اعمال بر روی گره پیشفرض"
-
-msgid "Applying to the default preproxy node"
-msgstr "اعمال بر روی گره پیشپروکسی پیشفرض"
-
msgid "Add nodes to the standby node list by keywords"
msgstr "افزودن گرهها به لیست گرههای آمادهبهکار با استفاده از کلمات کلیدی"
diff --git a/small/luci-app-passwall2/po/zh-cn/passwall2.po b/small/luci-app-passwall2/po/zh-cn/passwall2.po
index f1657ff7e2..d015cf5648 100644
--- a/small/luci-app-passwall2/po/zh-cn/passwall2.po
+++ b/small/luci-app-passwall2/po/zh-cn/passwall2.po
@@ -367,6 +367,30 @@ msgstr "分流"
msgid "Balancing"
msgstr "负载均衡"
+msgid "Node Addition Method"
+msgstr "节点添加方式"
+
+msgid "Manual"
+msgstr "手动"
+
+msgid "Batch"
+msgstr "批量"
+
+msgid "Select Group"
+msgstr "选择分组"
+
+msgid "Node Matching Rules"
+msgstr "节点匹配规则"
+
+msgid ""
+"Example: ^A && B && !C && D$
"
+"This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
"
+"Conditions are joined by &&, and their order does not affect the result."
+msgstr ""
+"示例:^A && B && !C && D$
"
+"表示节点备注需同时满足:以 A 开头(^)、包含 B、不包含 C(!)、并以 D 结尾($)。
"
+"多个条件使用 && 连接,条件顺序不影响结果。"
+
msgid "Balancing Strategy"
msgstr "负载均衡策略"
@@ -412,8 +436,11 @@ msgstr "前置代理"
msgid "Preproxy Node"
msgstr "前置代理节点"
-msgid "Set the node to be used as a pre-proxy. Each rule (including Default) has a separate switch that controls whether this rule uses the pre-proxy or not."
-msgstr "设置用作前置代理的节点。每条规则(包括默认)都有独立开关控制本规则是否使用前置代理。"
+msgid "Set the node to be used as a pre-proxy."
+msgstr "设置用作前置代理的节点。"
+
+msgid "Each rule has a separate switch that controls whether this rule uses the pre-proxy or not."
+msgstr "每条规则都有独立开关控制本规则是否使用前置代理。"
msgid "Close (Not use)"
msgstr "关闭 (不使用)"
@@ -427,12 +454,6 @@ msgstr "直连"
msgid "Blackhole (Block)"
msgstr "黑洞(屏蔽)"
-msgid "Use preproxy node"
-msgstr "使用前置代理节点"
-
-msgid "Default Preproxy"
-msgstr "默认前置代理"
-
msgid "There are no available nodes, please add or subscribe nodes first."
msgstr "没有可用节点,请先添加或订阅节点。"
@@ -748,18 +769,6 @@ msgstr "恢复切换"
msgid "When detects main node is available, switch back to the main node."
msgstr "当检测到主节点可用时,切换回主节点。"
-msgid "If the main node is shunt"
-msgstr "如果主节点是分流"
-
-msgid "Switch it"
-msgstr "切掉它"
-
-msgid "Applying to the default node"
-msgstr "应用于默认节点"
-
-msgid "Applying to the default preproxy node"
-msgstr "应用于默认前置节点"
-
msgid "Add nodes to the standby node list by keywords"
msgstr "通过关键字添加节点到备用节点列表"
diff --git a/small/luci-app-passwall2/po/zh-tw/passwall2.po b/small/luci-app-passwall2/po/zh-tw/passwall2.po
index 503f7b00eb..5813f29959 100644
--- a/small/luci-app-passwall2/po/zh-tw/passwall2.po
+++ b/small/luci-app-passwall2/po/zh-tw/passwall2.po
@@ -367,6 +367,30 @@ msgstr "分流"
msgid "Balancing"
msgstr "負載均衡"
+msgid "Node Addition Method"
+msgstr "節點新增方式"
+
+msgid "Manual"
+msgstr "手動"
+
+msgid "Batch"
+msgstr "批量"
+
+msgid "Select Group"
+msgstr "選擇分組"
+
+msgid "Node Matching Rules"
+msgstr "節點匹配規則"
+
+msgid ""
+"Example: ^A && B && !C && D$
"
+"This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
"
+"Conditions are joined by &&, and their order does not affect the result."
+msgstr ""
+"示例:^A && B && !C && D$
"
+"表示節點備註需同時滿足:以 A 開頭(^)、包含 B、不包含 C(!)、並以 D 結尾($)。
"
+"多個條件使用 && 連接,條件順序不影響結果。"
+
msgid "Balancing Strategy"
msgstr "負載均衡策略"
@@ -412,8 +436,11 @@ msgstr "前置代理"
msgid "Preproxy Node"
msgstr "前置代理節點"
-msgid "Set the node to be used as a pre-proxy. Each rule (including Default) has a separate switch that controls whether this rule uses the pre-proxy or not."
-msgstr "設置用作前置代理的節點。每条規則(包括默認)都有独立開關控制本規則是否使用前置代理。"
+msgid "Set the node to be used as a pre-proxy."
+msgstr "設置用作前置代理的節點。"
+
+msgid "Each rule has a separate switch that controls whether this rule uses the pre-proxy or not."
+msgstr "每条規則都有独立開關控制本規則是否使用前置代理。"
msgid "Close (Not use)"
msgstr "關閉 (不使用)"
@@ -427,12 +454,6 @@ msgstr "直連"
msgid "Blackhole (Block)"
msgstr "黑洞(屏蔽)"
-msgid "Use preproxy node"
-msgstr "使用前置代理節點"
-
-msgid "Default Preproxy"
-msgstr "默認前置代理"
-
msgid "There are no available nodes, please add or subscribe nodes first."
msgstr "沒有可用節點,請先添加或訂閱節點。"
@@ -748,18 +769,6 @@ msgstr "恢復切換"
msgid "When detects main node is available, switch back to the main node."
msgstr "當檢測到主節點可用時,切換回主節點。"
-msgid "If the main node is shunt"
-msgstr "如果主節點是分流"
-
-msgid "Switch it"
-msgstr "切掉它"
-
-msgid "Applying to the default node"
-msgstr "應用於默認節點"
-
-msgid "Applying to the default preproxy node"
-msgstr "應用於默認前置節點"
-
msgid "Add nodes to the standby node list by keywords"
msgstr "通過關键字添加節點到備用節點列表"
diff --git a/small/luci-app-passwall2/root/usr/share/passwall2/iptables.sh b/small/luci-app-passwall2/root/usr/share/passwall2/iptables.sh
index 699e191303..1c06903013 100755
--- a/small/luci-app-passwall2/root/usr/share/passwall2/iptables.sh
+++ b/small/luci-app-passwall2/root/usr/share/passwall2/iptables.sh
@@ -2,6 +2,7 @@
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/iptables.sh
+UTILS_PATH=$DIR/utils.sh
IPSET_LOCAL="passwall2_local"
IPSET_WAN="passwall2_wan"
IPSET_LAN="passwall2_lan"
@@ -12,8 +13,6 @@ IPSET_WAN6="passwall2_wan6"
IPSET_LAN6="passwall2_lan6"
IPSET_VPS6="passwall2_vps6"
-. /lib/functions/network.sh
-
ipt=$(command -v iptables-legacy || command -v iptables)
ip6t=$(command -v ip6tables-legacy || command -v ip6tables)
@@ -298,7 +297,6 @@ load_acl() {
local _ipt_source _ipv4
local msg
if [ -n "${interface}" ]; then
- . /lib/functions/network.sh
local gateway device
network_get_gateway gateway "${interface}"
network_get_device device "${interface}"
@@ -972,6 +970,7 @@ gen_include() {
local __ipt=""
[ -n "${ipt}" ] && {
__ipt=$(cat <<- EOF
+ . $UTILS_PATH
$ipt-save -c | grep -v "PSW2" | $ipt-restore -c
$ipt-restore -n <<-EOT
$(extract_rules 4 nat)
@@ -983,7 +982,7 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2")
- WAN_IP=\$(${MY_PATH} get_wan_ips ip4)
+ WAN_IP=\$(get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in \$WAN_IP; do
@@ -996,6 +995,7 @@ gen_include() {
local __ip6t=""
[ -n "${ip6t}" ] && {
__ip6t=$(cat <<- EOF
+ . $UTILS_PATH
$ip6t-save -c | grep -v "PSW2" | $ip6t-restore -c
$ip6t-restore -n <<-EOT
$(extract_rules 6 nat)
@@ -1006,7 +1006,7 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2")
- WAN6_IP=\$(${MY_PATH} get_wan_ips ip6)
+ WAN6_IP=\$(get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in \$WAN6_IP; do
@@ -1071,9 +1071,6 @@ get_ipt_bin)
get_ip6t_bin)
get_ip6t_bin
;;
-get_wan_ips)
- get_wan_ips
- ;;
filter_direct_node_list)
filter_direct_node_list
;;
diff --git a/small/luci-app-passwall2/root/usr/share/passwall2/nftables.sh b/small/luci-app-passwall2/root/usr/share/passwall2/nftables.sh
index 3ed8b30651..1ef5346e59 100755
--- a/small/luci-app-passwall2/root/usr/share/passwall2/nftables.sh
+++ b/small/luci-app-passwall2/root/usr/share/passwall2/nftables.sh
@@ -2,6 +2,7 @@
DIR="$(cd "$(dirname "$0")" && pwd)"
MY_PATH=$DIR/nftables.sh
+UTILS_PATH=$DIR/utils.sh
NFTABLE_NAME="inet passwall2"
NFTSET_LOCAL="passwall2_local"
NFTSET_WAN="passwall2_wan"
@@ -13,8 +14,6 @@ NFTSET_WAN6="passwall2_wan6"
NFTSET_LAN6="passwall2_lan6"
NFTSET_VPS6="passwall2_vps6"
-. /lib/functions/network.sh
-
FWI=$(uci -q get firewall.passwall2.path 2>/dev/null)
FAKE_IP="198.18.0.0/16"
FAKE_IP_6="fc00::/18"
@@ -165,18 +164,27 @@ insert_nftset() {
*) suffix=" timeout $timeout_argument" ;;
esac
{
- if [ $# -gt 0 ]; then
- echo "add element $NFTABLE_NAME $nftset_name { "
- printf "%s\n" "$@" | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
- echo " }"
- else
- local first_line
- if IFS= read -r first_line; then
- echo "add element $NFTABLE_NAME $nftset_name { "
- { echo "$first_line"; cat; } | awk -v s="$suffix" '{if (NR > 1) printf ",\n";printf "%s%s", $0, s}'
- echo " }"
- fi
- fi
+ if [ $# -gt 0 ]; then
+ printf "%s\n" "$@"
+ else
+ cat
+ fi | tr -s ' \t' '\n' | awk -v s="$suffix" -v n="$nftset_name" -v t="$NFTABLE_NAME" '
+ {
+ gsub(/^[ \t\r]+|[ \t\r]+$/, "");
+ }
+ $0 != "" {
+ if (first == 0) {
+ printf "add element %s %s { \n", t, n;
+ first = 1;
+ } else {
+ printf ",\n";
+ }
+ printf "%s%s", $0, s;
+ }
+ END {
+ if (first == 1) printf "\n }\n";
+ }
+ '
} | nft -f -
fi
}
@@ -327,7 +335,6 @@ load_acl() {
local _ipt_source _ipv4
local msg
if [ -n "${interface}" ]; then
- . /lib/functions/network.sh
local gateway device
network_get_gateway gateway "${interface}"
network_get_device device "${interface}"
@@ -571,25 +578,25 @@ load_acl() {
filter_haproxy() {
for item in $(uci show $CONFIG | grep ".lbss=" | cut -d "'" -f 2); do
- local ip=$(get_host_ip ipv4 $(echo $item | awk -F ":" '{print $1}') 1)
- [ -n "$ip" ] && insert_nftset $NFTSET_VPS "-1" $ip
- done
+ get_host_ip ipv4 $(echo $item | awk -F ":" '{print $1}') 1
+ done | insert_nftset $NFTSET_VPS "-1"
log_i18n 1 "Add node to the load balancer is directly connected to %s[%s]." "nftset" "${NFTSET_VPS}"
}
filter_vps_addr() {
- for server_host in $@; do
- local vps_ip4=$(get_host_ip "ipv4" ${server_host})
- local vps_ip6=$(get_host_ip "ipv6" ${server_host})
- [ -n "$vps_ip4" ] && insert_nftset $NFTSET_VPS "-1" $vps_ip4
- [ -n "$vps_ip6" ] && insert_nftset $NFTSET_VPS6 "-1" $vps_ip6
- done
+ for server_host in "$@"; do
+ get_host_ip "ipv4" ${server_host}
+ done | insert_nftset $NFTSET_VPS "-1"
+
+ for server_host in "$@"; do
+ get_host_ip "ipv6" ${server_host}
+ done | insert_nftset $NFTSET_VPS6 "-1"
}
filter_vpsip() {
- uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS "-1"
+ uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | grep -v "^127\.0\.0\.1$" | insert_nftset $NFTSET_VPS "-1"
#log 1 "$(i18n "Add all %s nodes to %s[%s] direct connection complete." "IPv4" "nftset" "${$NFTSET_VPS}")"
- uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d" | insert_nftset $NFTSET_VPS6 "-1"
+ uci show $CONFIG | grep -E "(.address=|.download_address=)" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_VPS6 "-1"
#log 1 "$(i18n "Add all %s nodes to %s[%s] direct connection complete." "IPv6" "nftset" "${$NFTSET_VPS6}")"
}
@@ -648,8 +655,8 @@ add_firewall_rule() {
gen_nftset $NFTSET_LAN6 ipv6_addr 0 "-1" $(gen_lanlist_6)
gen_nftset $NFTSET_VPS6 ipv6_addr 0 "-1"
- ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL "-1"
- ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | sed -e "s/ /\n/g" | insert_nftset $NFTSET_LOCAL6 "-1"
+ ip address show | grep -w "inet" | awk '{print $2}' | awk -F '/' '{print $1}' | insert_nftset $NFTSET_LOCAL "-1"
+ ip address show | grep -w "inet6" | awk '{print $2}' | awk -F '/' '{print $1}' | insert_nftset $NFTSET_LOCAL6 "-1"
# Ignore special IP ranges
local lan_ifname lan_ip
@@ -660,20 +667,20 @@ add_firewall_rule() {
#log_i18n 1 "local network segments (%s) direct connection: %s" "IPv4" "${lan_ip}"
#log_i18n 1 "local network segments (%s) direct connection: %s" "IPv6" "${lan_ip6}"
- [ -n "$lan_ip" ] && insert_nftset $NFTSET_LAN "-1" $(echo $lan_ip | sed -e "s/ /\n/g")
- [ -n "$lan_ip6" ] && insert_nftset $NFTSET_LAN6 "-1" $(echo $lan_ip6 | sed -e "s/ /\n/g")
+ [ -n "$lan_ip" ] && echo $lan_ip | insert_nftset $NFTSET_LAN "-1"
+ [ -n "$lan_ip6" ] && echo $lan_ip6 | insert_nftset $NFTSET_LAN6 "-1"
}
[ -n "$ISP_DNS" ] && {
+ echo "$ISP_DNS" | insert_nftset $NFTSET_LAN 0
for ispip in $ISP_DNS; do
- insert_nftset $NFTSET_LAN "-1" $ispip
log_i18n 1 "$(i18n "Add ISP %s DNS to the whitelist: %s" "IPv4" "${ispip}")"
done
}
[ -n "$ISP_DNS6" ] && {
+ echo $ISP_DNS6 | insert_nftset $NFTSET_LAN6 0
for ispip6 in $ISP_DNS6; do
- insert_nftset $NFTSET_LAN6 "-1" $ispip6
log_i18n 1 "$(i18n "Add ISP %s DNS to the whitelist: %s" "IPv6" "${ispip6}")"
done
}
@@ -780,7 +787,7 @@ add_firewall_rule() {
WAN_IP=$(get_wan_ips ip4)
[ -n "${WAN_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
- insert_nftset $NFTSET_WAN "-1" $WAN_IP
+ echo $WAN_IP | insert_nftset $NFTSET_WAN "-1"
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
}
@@ -811,7 +818,7 @@ add_firewall_rule() {
WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
- insert_nftset $NFTSET_WAN6 "-1" $WAN6_IP
+ echo $WAN6_IP | insert_nftset $NFTSET_WAN6 "-1"
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
}
unset WAN6_IP
@@ -1000,22 +1007,23 @@ flush_include() {
gen_include() {
flush_include
local nft_chain_file=$TMP_PATH/PSW2_RULE.nft
- echo '#!/usr/sbin/nft -f' > $nft_chain_file
+ echo '#!/bin/sh' > $nft_chain_file
nft list table $NFTABLE_NAME >> $nft_chain_file
local __nft=" "
__nft=$(cat <<- EOF
+ . $UTILS_PATH
[ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW2)" ] && nft -f ${nft_chain_file}
- WAN_IP=\$(sh ${MY_PATH} get_wan_ips ip4)
+ WAN_IP=\$(get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
- sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1" \$WAN_IP
+ echo "\${WAN_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1"
}
[ "$PROXY_IPV6" == "1" ] && {
- WAN6_IP=\$(sh ${MY_PATH} get_wan_ips ip6)
+ WAN6_IP=\$(get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
- sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1" \$WAN6_IP
+ echo "\${WAN6_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1"
}
}
EOF
@@ -1055,9 +1063,6 @@ case $arg1 in
insert_nftset)
insert_nftset "$@"
;;
-get_wan_ips)
- get_wan_ips "$@"
- ;;
filter_direct_node_list)
filter_direct_node_list
;;
diff --git a/small/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/small/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua
index 1bdbea69ad..88cfc859f0 100755
--- a/small/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua
+++ b/small/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua
@@ -17,6 +17,8 @@ local ssub, slen, schar, sbyte, sformat, sgsub = string.sub, string.len, string.
local split = api.split
local jsonParse, jsonStringify = luci.jsonc.parse, luci.jsonc.stringify
local base64Decode = api.base64Decode
+local UrlEncode = api.UrlEncode
+local UrlDecode = api.UrlDecode
local uci = api.uci
local fs = api.fs
local log = api.log
@@ -247,10 +249,6 @@ do
[".name"] = "default_node",
remarks = i18n.translatef("Default")
})
- table.insert(rules, {
- [".name"] = "main_node",
- remarks = i18n.translatef("Default Preproxy")
- })
for k, e in pairs(rules) do
local _node_id = node[e[".name"]] or nil
@@ -403,18 +401,6 @@ do
end
end
-local function UrlEncode(szText)
- return szText:gsub("([^%w%-_%.%~])", function(c)
- return string.format("%%%02X", string.byte(c))
- end)
-end
-
-local function UrlDecode(szText)
- return szText and szText:gsub("+", " "):gsub("%%(%x%x)", function(h)
- return string.char(tonumber(h, 16))
- end) or nil
-end
-
-- Retrieve subscribe information (remaining data allowance, expiration time).
local subscribe_info = {}
local function get_subscribe_info(cfgid, value)
@@ -617,6 +603,8 @@ local function processData(szType, content, add_mode, group)
result.tls = "0"
end
+ result.tcp_fast_open = info.tfo
+
if result.type == "sing-box" and (result.transport == "mkcp" or result.transport == "xhttp") then
log(2, i18n.translatef("Skip node: %s. Because Sing-Box does not support the %s protocol's %s transmission method, Xray needs to be used instead.", result.remarks, szType, result.transport))
return nil
@@ -719,13 +707,17 @@ local function processData(szType, content, add_mode, group)
result.method = method
result.password = password
+ result.tcp_fast_open = params.tfo
- if has_xray and (result.type ~= 'Xray' and result.type ~= 'sing-box' and params.type) then
- result.type = 'Xray'
- result.protocol = 'shadowsocks'
- elseif has_singbox and (result.type ~= 'Xray' and result.type ~= 'sing-box' and params.type) then
- result.type = 'sing-box'
- result.protocol = 'shadowsocks'
+ local need_upgrade = (result.type ~= "Xray" and result.type ~= "sing-box")
+ and (params.type and params.type ~= "tcp")
+ and (params.headerType and params.headerType ~= "none")
+ if has_xray and (need_upgrade or params.type == "xhttp") then
+ result.type = "Xray"
+ result.protocol = "shadowsocks"
+ elseif has_singbox and need_upgrade then
+ result.type = "sing-box"
+ result.protocol = "shadowsocks"
end
if result.plugin then
@@ -888,7 +880,8 @@ local function processData(szType, content, add_mode, group)
else
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
- else
+ result.uot = params.udp
+ elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then
result.error_msg = i18n.translatef("Please replace Xray or Sing-Box to support more transmission methods in Shadowsocks.")
end
end
@@ -1090,6 +1083,7 @@ local function processData(szType, content, add_mode, group)
end
result.alpn = params.alpn
+ result.tcp_fast_open = params.tfo
if result.type == "sing-box" and (result.transport == "mkcp" or result.transport == "xhttp") then
log(2, i18n.translatef("Skip node: %s. Because Sing-Box does not support the %s protocol's %s transmission method, Xray needs to be used instead.", result.remarks, szType, result.transport))
@@ -1278,6 +1272,8 @@ local function processData(szType, content, add_mode, group)
result.tls_allowInsecure = allowInsecure_default and "1" or "0"
end
+ result.tcp_fast_open = params.tfo
+
if result.type == "sing-box" and (result.transport == "mkcp" or result.transport == "xhttp") then
log(2, i18n.translatef("Skip node: %s. Because Sing-Box does not support the %s protocol's %s transmission method, Xray needs to be used instead.", result.remarks, szType, result.transport))
return nil
diff --git a/small/luci-app-passwall2/root/usr/share/passwall2/utils.sh b/small/luci-app-passwall2/root/usr/share/passwall2/utils.sh
index 4fa8f23638..ba861d1a49 100755
--- a/small/luci-app-passwall2/root/usr/share/passwall2/utils.sh
+++ b/small/luci-app-passwall2/root/usr/share/passwall2/utils.sh
@@ -14,6 +14,8 @@ TMP_IFACE_PATH=${TMP_PATH}/iface
TMP_ROUTE_PATH=${TMP_PATH}/route
TMP_SCRIPT_FUNC_PATH=${TMP_PATH}/script_func
+. /lib/functions/network.sh
+
config_get_type() {
local ret=$(uci -q get "${CONFIG}.${1}" 2>/dev/null)
echo "${ret:=$2}"
@@ -165,16 +167,15 @@ get_host_ip() {
local count=$3
[ -z "$count" ] && count=3
local isip=""
- local ip=$host
+ local ip=""
if [ "$1" == "ipv6" ]; then
isip=$(echo $host | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
if [ -n "$isip" ]; then
- isip=$(echo $host | cut -d '[' -f2 | cut -d ']' -f1)
- else
- isip=$(echo $host | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}")
+ ip=$(echo "$host" | tr -d '[]')
fi
else
isip=$(echo $host | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}")
+ [ -n "$isip" ] && ip=$isip
fi
[ -z "$isip" ] && {
local t=4
@@ -182,7 +183,7 @@ get_host_ip() {
local vpsrip=$(resolveip -$t -t $count $host | awk 'NR==1{print}')
ip=$vpsrip
}
- echo $ip
+ [ -n "$ip" ] && echo "$ip"
}
get_node_host_ip() {
@@ -287,11 +288,20 @@ check_port_exists() {
}
get_new_port() {
- local default_start_port=2000
+ local default_start_port=2001
local min_port=1025
local max_port=49151
local port=$1
- [ "$port" == "auto" ] && port=$default_start_port
+ local last_get_new_port_auto
+ if [ "$1" == "auto" ]; then
+ last_get_new_port_auto=$(get_cache_var "last_get_new_port_auto")
+ if [ -n "$last_get_new_port_auto" ]; then
+ port=$last_get_new_port_auto
+ port=$(expr $port + 1)
+ else
+ port=$default_start_port
+ fi
+ fi
[ "$port" -lt $min_port -o "$port" -gt $max_port ] && port=$default_start_port
local protocol=$(echo $2 | tr 'A-Z' 'a-z')
local result=$(check_port_exists $port $protocol)
@@ -306,6 +316,7 @@ get_new_port() {
fi
get_new_port $temp $protocol
else
+ [ "$1" == "auto" ] && set_cache_var "last_get_new_port_auto" "$port"
echo $port
fi
}
@@ -328,7 +339,6 @@ add_ip2route() {
local remarks="${1}"
[ "$remarks" != "$ip" ] && remarks="${1}(${ip})"
- . /lib/functions/network.sh
local gateway device
network_get_gateway gateway "$2"
network_get_device device "$2"
diff --git a/small/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua b/small/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua
index 18d3b42257..a4285f639f 100644
--- a/small/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua
+++ b/small/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua
@@ -98,10 +98,14 @@ function act_ping()
end
if not e.ping then
- e.ping = tonumber(luci.sys.exec(string.format(
+ local ping_result = tonumber(luci.sys.exec(string.format(
"nping --udp -c 1 -p %d %s 2>/dev/null | grep -o 'Avg rtt: [0-9.]*ms' | awk '{print $3}' | sed 's/ms//' | head -1",
port, domain
)))
+ local ping_num = tonumber(ping_result)
+ if ping_num then
+ e.ping = math.floor(ping_num)
+ end
end
end
diff --git a/small/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua b/small/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua
index 80ce420b27..9e1a7a114c 100644
--- a/small/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua
+++ b/small/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua
@@ -1034,17 +1034,17 @@ o:value("wireguard", translate("WireGuard"))
o = s:option(ListValue, "kcp_guise", translate("Camouflage Type"))
o:depends("transport", "kcp")
o:value("none", translate("None"))
-o:value("header-srtp", translate("VideoCall (SRTP)"))
-o:value("header-utp", translate("BitTorrent (uTP)"))
-o:value("header-wechat", translate("WechatVideo"))
-o:value("header-dtls", translate("DTLS 1.2"))
-o:value("header-wireguard", translate("WireGuard"))
-o:value("header-dns", translate("DNS"))
+o:value("srtp", translate("VideoCall (SRTP)"))
+o:value("utp", translate("BitTorrent (uTP)"))
+o:value("wechat-video", translate("WechatVideo"))
+o:value("dtls", translate("DTLS 1.2"))
+o:value("wireguard", translate("WireGuard"))
+o:value("dns", translate("DNS"))
o.rmempty = true
o = s:option(Value, "kcp_domain", translate("Camouflage Domain"))
o.description = translate("Use it together with the DNS disguised type. You can fill in any domain.")
-o:depends("kcp_guise", "header-dns")
+o:depends("kcp_guise", "dns")
o = s:option(Value, "mtu", translate("MTU"))
o.datatype = "uinteger"
diff --git a/small/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm b/small/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm
index e9b2a44971..dd534aaa32 100644
--- a/small/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm
+++ b/small/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm
@@ -184,6 +184,11 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.insecure')[0].dispatchEvent(event); // 触发事件
}
+ if (params.get("tfo") === "1") {
+ document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].checked = true; // 设置 fast_open 为 true
+ document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].dispatchEvent(event); // 触发事件
+ }
+
s.innerHTML = "<%:Import configuration information successfully.%>";
return false;
case "ss":
@@ -326,6 +331,11 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = false;
}
+ if (params.tfo === "1") {
+ document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].checked = true; // 设置 fast_open 为 true
+ document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].dispatchEvent(event); // 触发事件
+ }
+
if (param != undefined) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = decodeURIComponent(param);
}
@@ -509,6 +519,10 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.password')[0].value = b64decsafe(ssm[6]);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.obfs_param')[0].value = dictvalue(pdict, 'obfsparam');
document.getElementsByName('cbid.shadowsocksr.' + sid + '.protocol_param')[0].value = dictvalue(pdict, 'protoparam');
+ if (pdict.tfo === "1") {
+ document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].checked = true; // 设置 fast_open 为 true
+ document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].dispatchEvent(event); // 触发事件
+ }
var rem = pdict['remarks'];
if (typeof (rem) != 'undefined' && rem != '' && rem.length > 0) document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = b64decutf8safe(rem);
s.innerHTML = "<%:Import configuration information successfully.%>";
@@ -855,6 +869,10 @@ function import_ssr_url(btn, urlname, sid) {
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra', event); // 触发事件
setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_extra', params.get("extra") || "");
}
+ if (params.get("tfo") === "1") {
+ setElementValue('cbid.shadowsocksr.' + sid + '.fast_open', true); // 设置 fast_open 为 true
+ dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.fast_open', event); // 触发事件
+ }
break;
case "kcp":
setElementValue('cbid.shadowsocksr.' + sid + '.kcp_guise', params.get("headerType") || "none");
diff --git a/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/gen_config.lua b/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/gen_config.lua
index beca79a38d..c76db76800 100755
--- a/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/gen_config.lua
+++ b/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/gen_config.lua
@@ -412,9 +412,11 @@ end
finalmask = (server.transport == "kcp") and {
udp = (function()
local t = {}
+ local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
+ dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
if server.kcp_guise and server.kcp_guise ~= "none" then
- local g = { type = server.kcp_guise }
- if server.kcp_guise == "header-dns" and server.kcp_domain and server.kcp_domain ~= "" then
+ local g = { type = map[server.kcp_guise] }
+ if server.kcp_guise == "dns" and server.kcp_domain and server.kcp_domain ~= "" then
g.settings = { domain = server.kcp_domain }
end
t[#t + 1] = g
diff --git a/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/gfw2ipset.sh b/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/gfw2ipset.sh
index 676732af08..f4ade91b08 100755
--- a/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/gfw2ipset.sh
+++ b/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/gfw2ipset.sh
@@ -51,14 +51,21 @@ else
cp -rf /etc/ssrplus/gfw_base.conf $TMP_DNSMASQ_PATH/
fi
-if [ "$nft_support" = "1" ]; then
- # 移除 ipset
- for conf_file in gfw_base.conf gfw_list.conf; do
- if [ -f "$TMP_DNSMASQ_PATH/$conf_file" ]; then
- sed -i 's|ipset=/\([^/]*\)/\([^[:space:]]*\)|nftset=/\1/inet#ss_spec#\2|g' "$TMP_DNSMASQ_PATH/$conf_file"
+for conf_file in gfw_base.conf gfw_list.conf; do
+ conf="$TMP_DNSMASQ_PATH/$conf_file"
+ [ -f "$conf" ] || continue
+
+ if [ "$run_mode" = "gfw" ]; then
+ if [ "$nft_support" = "1" ]; then
+ # gfw + nft:ipset → nftset
+ sed -i 's|ipset=/\([^/]*\)/\([^[:space:]]*\)|nftset=/\1/inet#ss_spec#\2|g' "$conf"
fi
- done
-fi
+ else
+ # 非 gfw:无条件清理所有分流引用
+ # sed -i '/^[[:space:]]*\(ipset=\|nftset=\)/d' "$conf"
+ sed -i '/^[[:space:]]*ipset=/d' "$conf"
+ fi
+done
if [ "$(uci_get_by_type global netflix_enable 0)" == "1" ]; then
# 只有开启 NetFlix分流 才需要取值
diff --git a/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua b/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua
index d96f705842..008a5c7017 100755
--- a/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua
+++ b/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua
@@ -282,6 +282,10 @@ local function processData(szType, content)
result.insecure = "1"
end
end
+ if params.tfo then
+ -- 处理 fast open 参数
+ result.fast_open = params.tfo
+ end
elseif szType == 'ssr' then
-- 去掉前后空白和#注释
local link = trim(content:gsub("#.*$", ""))
@@ -309,6 +313,11 @@ local function processData(szType, content)
result.obfs_param = base64Decode(params.obfsparam or '')
result.protocol_param = base64Decode(params.protoparam or '')
+ if params.tfo then
+ -- 处理 fast open 参数
+ result.fast_open = params.tfo
+ end
+
local group = base64Decode(params.group or '')
local remarks = base64Decode(params.remarks or '')
@@ -396,7 +405,7 @@ local function processData(szType, content)
end
if info.net == 'kcp' then
result.kcp_guise = info.type or "none"
- if info.type and info.type == "header-dns" then
+ if info.type and info.type == "dns" then
result.kcp_guise = info.host or ""
end
result.mtu = 1350
@@ -560,6 +569,11 @@ local function processData(szType, content)
result.server = server
result.server_port = port
+ if params.tfo then
+ -- 处理 fast open 参数
+ result.fast_open = params.tfo
+ end
+
-- 插件处理
if params.plugin then
local plugin_info = UrlDecode(params.plugin)
@@ -719,7 +733,7 @@ local function processData(szType, content)
result.h2_path = params.path and UrlDecode(params.path) or nil
elseif result.transport == "kcp" then
result.kcp_guise = params.headerType or "none"
- if params.headerType and params.headerType == "header-dns" then
+ if params.headerType and params.headerType == "dns" then
result.kcp_domain = params.host or ""
end
result.seed = params.seed
@@ -914,7 +928,7 @@ local function processData(szType, content)
result.h2_path = params.path and UrlDecode(params.path) or nil
elseif result.transport == "kcp" then
result.kcp_guise = params.headerType or "none"
- if params.headerType and params.headerType == "header-dns" then
+ if params.headerType and params.headerType == "dns" then
result.kcp_domain = params.host or ""
end
result.seed = params.seed
@@ -1035,6 +1049,10 @@ local function processData(szType, content)
result.xhttp_mode = params.mode or "auto"
result.xhttp_host = params.host and UrlDecode(params.host) or nil
result.xhttp_path = params.path and UrlDecode(params.path) or "/"
+ if params.tfo then
+ -- 处理 fast open 参数
+ result.fast_open = params.tfo
+ end
if params.extra and params.extra ~= "" then
result.enable_xhttp_extra = "1"
result.xhttp_extra = params.extra
@@ -1054,7 +1072,7 @@ local function processData(szType, content)
elseif result.transport == "kcp" then
result.kcp_guise = params.headerType or "none"
- if params.headerType and params.headerType == "header-dns" then
+ if params.headerType and params.headerType == "dns" then
result.kcp_domain = params.host or ""
end
result.seed = params.seed
diff --git a/small/nikki/files/ucode/mixin.uc b/small/nikki/files/ucode/mixin.uc
index 58c320d8f9..87946bce7e 100644
--- a/small/nikki/files/ucode/mixin.uc
+++ b/small/nikki/files/ucode/mixin.uc
@@ -81,7 +81,6 @@ config['dns']['fake-ip-filter-mode'] = uci.get('nikki', 'mixin', 'fake_ip_filter
config['dns']['respect-rules'] = uci_bool(uci.get('nikki', 'mixin', 'dns_respect_rules'));
config['dns']['prefer-h3'] = uci_bool(uci.get('nikki', 'mixin', 'dns_doh_prefer_http3'));
-config['dns']['direct-nameserver-follow-policy'] = uci_bool(uci.get('nikki', 'mixin', 'dns_direct_nameserver_follow_policy'));
config['dns']['use-system-hosts'] = uci_bool(uci.get('nikki', 'mixin', 'dns_system_hosts'));
config['dns']['use-hosts'] = uci_bool(uci.get('nikki', 'mixin', 'dns_hosts'));
if (uci_bool(uci.get('nikki', 'mixin', 'hosts'))) {
@@ -106,6 +105,16 @@ if (uci_bool(uci.get('nikki', 'mixin', 'dns_nameserver'))) {
push(config['dns'][section.type], ...uci_array(section.nameserver));
})
}
+if (uci_bool(uci.get('nikki', 'mixin', 'dns_proxy_server_nameserver_policy'))) {
+ config['dns']['proxy-server-nameserver-policy'] = {};
+ uci.foreach('nikki', 'proxy_server_nameserver_policy', (section) => {
+ if (!uci_bool(section.enabled)) {
+ return;
+ }
+ config['dns']['proxy-server-nameserver-policy'][section.matcher] = uci_array(section.nameserver);
+ });
+}
+config['dns']['direct-nameserver-follow-policy'] = uci_bool(uci.get('nikki', 'mixin', 'dns_direct_nameserver_follow_policy'));
if (uci_bool(uci.get('nikki', 'mixin', 'dns_nameserver_policy'))) {
config['dns']['nameserver-policy'] = {};
uci.foreach('nikki', 'nameserver_policy', (section) => {
diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile
index 8840274d90..cc5f53d5f1 100644
--- a/small/v2ray-geodata/Makefile
+++ b/small/v2ray-geodata/Makefile
@@ -21,13 +21,13 @@ define Download/geoip
HASH:=838ab094bc01b9bafc849ce70c6f439dcb158d0c0dd41441ddb3c38d4d9ef563
endef
-GEOSITE_VER:=20260209041321
+GEOSITE_VER:=20260210131317
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
URL_FILE:=dlc.dat
FILE:=$(GEOSITE_FILE)
- HASH:=1ac0d25644031a4703e0609eda447de4e7924967ac949c8bc6ac2cbff3e808ee
+ HASH:=5770782fea1f9c2bda455d6a00e9824207edde1fa75adb24e35e2e9be419fc49
endef
GEOSITE_IRAN_VER:=202602090058
diff --git a/yt-dlp/yt_dlp/extractor/_extractors.py b/yt-dlp/yt_dlp/extractor/_extractors.py
index 348cb0e984..39148d2b0e 100644
--- a/yt-dlp/yt_dlp/extractor/_extractors.py
+++ b/yt-dlp/yt_dlp/extractor/_extractors.py
@@ -2180,11 +2180,15 @@ from .tvc import (
TVCIE,
TVCArticleIE,
)
-from .tver import TVerIE
+from .tver import (
+ TVerIE,
+ TVerOlympicIE,
+)
from .tvigle import TvigleIE
from .tviplayer import TVIPlayerIE
from .tvn24 import TVN24IE
from .tvnoe import TVNoeIE
+from .tvo import TvoIE
from .tvopengr import (
TVOpenGrEmbedIE,
TVOpenGrWatchIE,
diff --git a/yt-dlp/yt_dlp/extractor/steam.py b/yt-dlp/yt_dlp/extractor/steam.py
index 71d94815e7..fa60fb25dd 100644
--- a/yt-dlp/yt_dlp/extractor/steam.py
+++ b/yt-dlp/yt_dlp/extractor/steam.py
@@ -8,15 +8,12 @@ from ..utils import (
extract_attributes,
join_nonempty,
js_to_json,
+ parse_resolution,
str_or_none,
+ url_basename,
url_or_none,
)
-from ..utils.traversal import (
- find_element,
- find_elements,
- traverse_obj,
- trim_str,
-)
+from ..utils.traversal import find_element, traverse_obj
class SteamIE(InfoExtractor):
@@ -27,7 +24,7 @@ class SteamIE(InfoExtractor):
'id': '105600',
'title': 'Terraria',
},
- 'playlist_mincount': 3,
+ 'playlist_mincount': 5,
}, {
'url': 'https://store.steampowered.com/app/271590/Grand_Theft_Auto_V/',
'info_dict': {
@@ -37,6 +34,39 @@ class SteamIE(InfoExtractor):
'playlist_mincount': 26,
}]
+ def _entries(self, app_id, app_name, data_props):
+ for trailer in traverse_obj(data_props, (
+ 'trailers', lambda _, v: str_or_none(v['id']),
+ )):
+ movie_id = str_or_none(trailer['id'])
+
+ thumbnails = []
+ for thumbnail_url in traverse_obj(trailer, (
+ ('poster', 'thumbnail'), {url_or_none},
+ )):
+ thumbnails.append({
+ 'url': thumbnail_url,
+ **parse_resolution(url_basename(thumbnail_url)),
+ })
+
+ formats = []
+ if hls_manifest := traverse_obj(trailer, ('hlsManifest', {url_or_none})):
+ formats.extend(self._extract_m3u8_formats(
+ hls_manifest, app_id, 'mp4', m3u8_id='hls', fatal=False))
+ for dash_manifest in traverse_obj(trailer, ('dashManifests', ..., {url_or_none})):
+ formats.extend(self._extract_mpd_formats(
+ dash_manifest, app_id, mpd_id='dash', fatal=False))
+ self._remove_duplicate_formats(formats)
+
+ yield {
+ 'id': join_nonempty(app_id, movie_id),
+ 'title': join_nonempty(app_name, 'video', movie_id, delim=' '),
+ 'formats': formats,
+ 'series': app_name,
+ 'series_id': app_id,
+ 'thumbnails': thumbnails,
+ }
+
def _real_extract(self, url):
app_id = self._match_id(url)
@@ -45,32 +75,13 @@ class SteamIE(InfoExtractor):
self._set_cookie('store.steampowered.com', 'lastagecheckage', '1-January-2000')
webpage = self._download_webpage(url, app_id)
- app_name = traverse_obj(webpage, ({find_element(cls='apphub_AppName')}, {clean_html}))
+ data_props = traverse_obj(webpage, (
+ {find_element(cls='gamehighlight_desktopcarousel', html=True)},
+ {extract_attributes}, 'data-props', {json.loads}, {dict}))
+ app_name = traverse_obj(data_props, ('appName', {clean_html}))
- entries = []
- for data_prop in traverse_obj(webpage, (
- {find_elements(cls='highlight_player_item highlight_movie', html=True)},
- ..., {extract_attributes}, 'data-props', {json.loads}, {dict},
- )):
- formats = []
- if hls_manifest := traverse_obj(data_prop, ('hlsManifest', {url_or_none})):
- formats.extend(self._extract_m3u8_formats(
- hls_manifest, app_id, 'mp4', m3u8_id='hls', fatal=False))
- for dash_manifest in traverse_obj(data_prop, ('dashManifests', ..., {url_or_none})):
- formats.extend(self._extract_mpd_formats(
- dash_manifest, app_id, mpd_id='dash', fatal=False))
-
- movie_id = traverse_obj(data_prop, ('id', {trim_str(start='highlight_movie_')}))
- entries.append({
- 'id': movie_id,
- 'title': join_nonempty(app_name, 'video', movie_id, delim=' '),
- 'formats': formats,
- 'series': app_name,
- 'series_id': app_id,
- 'thumbnail': traverse_obj(data_prop, ('screenshot', {url_or_none})),
- })
-
- return self.playlist_result(entries, app_id, app_name)
+ return self.playlist_result(
+ self._entries(app_id, app_name, data_props), app_id, app_name)
class SteamCommunityIE(InfoExtractor):
diff --git a/yt-dlp/yt_dlp/extractor/streaks.py b/yt-dlp/yt_dlp/extractor/streaks.py
index 60123d67b7..642e0527e3 100644
--- a/yt-dlp/yt_dlp/extractor/streaks.py
+++ b/yt-dlp/yt_dlp/extractor/streaks.py
@@ -22,7 +22,7 @@ class StreaksBaseIE(InfoExtractor):
_GEO_BYPASS = False
_GEO_COUNTRIES = ['JP']
- def _extract_from_streaks_api(self, project_id, media_id, headers=None, query=None, ssai=False):
+ def _extract_from_streaks_api(self, project_id, media_id, headers=None, query=None, ssai=False, live_from_start=False):
try:
response = self._download_json(
self._API_URL_TEMPLATE.format('playback', project_id, media_id, ''),
@@ -83,6 +83,10 @@ class StreaksBaseIE(InfoExtractor):
fmts, subs = self._extract_m3u8_formats_and_subtitles(
src_url, media_id, 'mp4', m3u8_id='hls', fatal=False, live=is_live, query=query)
+ for fmt in fmts:
+ if live_from_start:
+ fmt.setdefault('downloader_options', {}).update({'ffmpeg_args': ['-live_start_index', '0']})
+ fmt['is_from_start'] = True
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
diff --git a/yt-dlp/yt_dlp/extractor/tver.py b/yt-dlp/yt_dlp/extractor/tver.py
index ffcc6a76b7..97f4d4feb5 100644
--- a/yt-dlp/yt_dlp/extractor/tver.py
+++ b/yt-dlp/yt_dlp/extractor/tver.py
@@ -4,6 +4,7 @@ from .streaks import StreaksBaseIE
from ..utils import (
ExtractorError,
GeoRestrictedError,
+ clean_html,
int_or_none,
join_nonempty,
make_archive_id,
@@ -11,7 +12,9 @@ from ..utils import (
str_or_none,
strip_or_none,
time_seconds,
+ unified_timestamp,
update_url_query,
+ url_or_none,
)
from ..utils.traversal import require, traverse_obj
@@ -257,3 +260,113 @@ class TVerIE(StreaksBaseIE):
'id': video_id,
'_old_archive_ids': [make_archive_id('BrightcoveNew', brightcove_id)] if brightcove_id else None,
}
+
+
+class TVerOlympicIE(StreaksBaseIE):
+ IE_NAME = 'tver:olympic'
+
+ _API_BASE = 'https://olympic-data.tver.jp/api'
+ _VALID_URL = r'https?://(?:www\.)?tver\.jp/olympic/milanocortina2026/(?Plive|video)/play/(?P\w+)'
+ _TESTS = [{
+ 'url': 'https://tver.jp/olympic/milanocortina2026/video/play/3b1d4462150b42558d9cc8aabb5238d0/',
+ 'info_dict': {
+ 'id': '3b1d4462150b42558d9cc8aabb5238d0',
+ 'ext': 'mp4',
+ 'title': '【開会式】ぎゅっと凝縮ハイライト',
+ 'display_id': 'ref:3b1d4462150b42558d9cc8aabb5238d0',
+ 'duration': 712.045,
+ 'live_status': 'not_live',
+ 'modified_date': r're:\d{8}',
+ 'modified_timestamp': int,
+ 'tags': 'count:1',
+ 'thumbnail': r're:https://.+\.(?:jpg|png)',
+ 'timestamp': 1770420187,
+ 'upload_date': '20260206',
+ 'uploader_id': 'tver-olympic',
+ },
+ }, {
+ 'url': 'https://tver.jp/olympic/milanocortina2026/live/play/glts313itwvj/',
+ 'info_dict': {
+ 'id': 'glts313itwvj',
+ 'ext': 'mp4',
+ 'title': '開会式ハイライト',
+ 'channel_id': 'ntv',
+ 'display_id': 'ref:sp_260207_spc_01_dvr',
+ 'duration': 7680,
+ 'live_status': 'was_live',
+ 'modified_date': r're:\d{8}',
+ 'modified_timestamp': int,
+ 'thumbnail': r're:https://.+\.(?:jpg|png)',
+ 'timestamp': 1770420300,
+ 'upload_date': '20260206',
+ 'uploader_id': 'tver-olympic-live',
+ },
+ }]
+
+ def _real_extract(self, url):
+ video_type, video_id = self._match_valid_url(url).group('type', 'id')
+ live_from_start = self.get_param('live_from_start')
+
+ if video_type == 'live':
+ project_id = 'tver-olympic-live'
+ api_key = 'a35ebb1ca7d443758dc7fcc5d99b1f72'
+ olympic_data = traverse_obj(self._download_json(
+ f'{self._API_BASE}/live/{video_id}', video_id), ('contents', 'live', {dict}))
+ media_id = traverse_obj(olympic_data, ('video_id', {str}))
+
+ now = time_seconds()
+ start_timestamp_str = traverse_obj(olympic_data, ('onair_start_date', {str}))
+ start_timestamp = unified_timestamp(start_timestamp_str, tz_offset=9)
+ if not start_timestamp:
+ raise ExtractorError('Unable to extract on-air start time')
+ end_timestamp = traverse_obj(olympic_data, (
+ 'onair_end_date', {unified_timestamp(tz_offset=9)}, {require('on-air end time')}))
+
+ if now < start_timestamp:
+ self.raise_no_formats(
+ f'This program is scheduled to start at {start_timestamp_str} JST', expected=True)
+
+ return {
+ 'id': video_id,
+ 'live_status': 'is_upcoming',
+ 'release_timestamp': start_timestamp,
+ }
+ elif start_timestamp <= now < end_timestamp:
+ live_status = 'is_live'
+ if live_from_start:
+ media_id += '_dvr'
+ elif end_timestamp <= now:
+ dvr_end_timestamp = traverse_obj(olympic_data, (
+ 'dvr_end_date', {unified_timestamp(tz_offset=9)}))
+ if dvr_end_timestamp and now < dvr_end_timestamp:
+ live_status = 'was_live'
+ media_id += '_dvr'
+ else:
+ raise ExtractorError(
+ 'This program is no longer available', expected=True)
+ else:
+ project_id = 'tver-olympic'
+ api_key = '4b55a4db3cce4ad38df6dd8543e3e46a'
+ media_id = video_id
+ live_status = 'not_live'
+ olympic_data = traverse_obj(self._download_json(
+ f'{self._API_BASE}/video/{video_id}', video_id), ('contents', 'video', {dict}))
+
+ return {
+ **self._extract_from_streaks_api(project_id, f'ref:{media_id}', {
+ 'Origin': 'https://tver.jp',
+ 'Referer': 'https://tver.jp/',
+ 'X-Streaks-Api-Key': api_key,
+ }, live_from_start=live_from_start),
+ **traverse_obj(olympic_data, {
+ 'title': ('title', {clean_html}, filter),
+ 'alt_title': ('sub_title', {clean_html}, filter),
+ 'channel': ('channel', {clean_html}, filter),
+ 'channel_id': ('channel_id', {clean_html}, filter),
+ 'description': (('description', 'description_l', 'description_s'), {clean_html}, filter, any),
+ 'timestamp': ('onair_start_date', {unified_timestamp(tz_offset=9)}),
+ 'thumbnail': (('picture_l_url', 'picture_m_url', 'picture_s_url'), {url_or_none}, any),
+ }),
+ 'id': video_id,
+ 'live_status': live_status,
+ }
diff --git a/yt-dlp/yt_dlp/extractor/tvo.py b/yt-dlp/yt_dlp/extractor/tvo.py
new file mode 100644
index 0000000000..c4bce3bed7
--- /dev/null
+++ b/yt-dlp/yt_dlp/extractor/tvo.py
@@ -0,0 +1,152 @@
+import json
+import urllib.parse
+
+from .brightcove import BrightcoveNewIE
+from .common import InfoExtractor
+from ..utils import (
+ clean_html,
+ int_or_none,
+ parse_duration,
+ parse_iso8601,
+ smuggle_url,
+ str_or_none,
+ url_or_none,
+)
+from ..utils.traversal import (
+ require,
+ traverse_obj,
+ trim_str,
+)
+
+
+class TvoIE(InfoExtractor):
+ IE_NAME = 'TVO'
+ _VALID_URL = r'https?://(?:www\.)?tvo\.org/video(?:/documentaries)?/(?P[\w-]+)'
+ _TESTS = [{
+ 'url': 'https://www.tvo.org/video/how-can-ontario-survive-the-trade-war',
+ 'info_dict': {
+ 'id': '6377531034112',
+ 'ext': 'mp4',
+ 'title': 'How Can Ontario Survive the Trade War?',
+ 'description': 'md5:e7455d9cd4b6b1270141922044161457',
+ 'display_id': 'how-can-ontario-survive-the-trade-war',
+ 'duration': 3531,
+ 'episode': 'How Can Ontario Survive the Trade War?',
+ 'episode_id': 'how-can-ontario-survive-the-trade-war',
+ 'episode_number': 1,
+ 'season': 'Season 1',
+ 'season_number': 1,
+ 'series': 'TVO at AMO',
+ 'series_id': 'tvo-at-amo',
+ 'tags': 'count:17',
+ 'thumbnail': r're:https?://.+',
+ 'timestamp': 1756944016,
+ 'upload_date': '20250904',
+ 'uploader_id': '18140038001',
+ },
+ }, {
+ 'url': 'https://www.tvo.org/video/documentaries/the-pitch',
+ 'info_dict': {
+ 'id': '6382500333112',
+ 'ext': 'mp4',
+ 'title': 'The Pitch',
+ 'categories': ['Documentaries'],
+ 'description': 'md5:9d4246b70dce772a3a396c4bd84c8506',
+ 'display_id': 'the-pitch',
+ 'duration': 5923,
+ 'episode': 'The Pitch',
+ 'episode_id': 'the-pitch',
+ 'episode_number': 1,
+ 'season': 'Season 1',
+ 'season_number': 1,
+ 'series': 'The Pitch',
+ 'series_id': 'the-pitch',
+ 'tags': 'count:8',
+ 'thumbnail': r're:https?://.+',
+ 'timestamp': 1762693216,
+ 'upload_date': '20251109',
+ 'uploader_id': '18140038001',
+ },
+ }, {
+ 'url': 'https://www.tvo.org/video/documentaries/valentines-day',
+ 'info_dict': {
+ 'id': '6387298331112',
+ 'ext': 'mp4',
+ 'title': 'Valentine\'s Day',
+ 'categories': ['Documentaries'],
+ 'description': 'md5:b142149beb2d3a855244816c50cd2f14',
+ 'display_id': 'valentines-day',
+ 'duration': 3121,
+ 'episode': 'Valentine\'s Day',
+ 'episode_id': 'valentines-day',
+ 'episode_number': 2,
+ 'season': 'Season 1',
+ 'season_number': 1,
+ 'series': 'How We Celebrate',
+ 'series_id': 'how-we-celebrate',
+ 'tags': 'count:6',
+ 'thumbnail': r're:https?://.+',
+ 'timestamp': 1770386416,
+ 'upload_date': '20260206',
+ 'uploader_id': '18140038001',
+ },
+ }]
+ BRIGHTCOVE_URL_TEMPLATE = 'https://players.brightcove.net/18140038001/default_default/index.html?videoId=%s'
+
+ def _real_extract(self, url):
+ display_id = self._match_id(url)
+ video_data = self._download_json(
+ 'https://hmy0rc1bo2.execute-api.ca-central-1.amazonaws.com/graphql',
+ display_id, headers={'Content-Type': 'application/json'},
+ data=json.dumps({
+ 'operationName': 'getVideo',
+ 'variables': {'slug': urllib.parse.urlparse(url).path.rstrip('/')},
+ 'query': '''query getVideo($slug: String) {
+ getTVOOrgVideo(slug: $slug) {
+ contentCategory
+ description
+ length
+ program {
+ nodeUrl
+ title
+ }
+ programOrder
+ publishedAt
+ season
+ tags
+ thumbnail
+ title
+ videoSource {
+ brightcoveRefId
+ }
+ }
+ }''',
+ }, separators=(',', ':')).encode(),
+ )['data']['getTVOOrgVideo']
+
+ brightcove_id = traverse_obj(video_data, (
+ 'videoSource', 'brightcoveRefId', {str_or_none}, {require('Brightcove ID')}))
+
+ return {
+ '_type': 'url_transparent',
+ 'ie_key': BrightcoveNewIE.ie_key(),
+ 'url': smuggle_url(self.BRIGHTCOVE_URL_TEMPLATE % brightcove_id, {'geo_countries': ['CA']}),
+ 'display_id': display_id,
+ 'episode_id': display_id,
+ **traverse_obj(video_data, {
+ 'title': ('title', {clean_html}, filter),
+ 'categories': ('contentCategory', {clean_html}, filter, all, filter),
+ 'description': ('description', {clean_html}, filter),
+ 'duration': ('length', {parse_duration}),
+ 'episode': ('title', {clean_html}, filter),
+ 'episode_number': ('programOrder', {int_or_none}),
+ 'season_number': ('season', {int_or_none}),
+ 'tags': ('tags', ..., {clean_html}, filter),
+ 'thumbnail': ('thumbnail', {url_or_none}),
+ 'timestamp': ('publishedAt', {parse_iso8601}),
+ }),
+ **traverse_obj(video_data, ('program', {
+ 'series': ('title', {clean_html}, filter),
+ 'series_id': ('nodeUrl', {clean_html}, {trim_str(start='/programs/')}, filter),
+ })),
+ }
diff --git a/yt-dlp/yt_dlp/options.py b/yt-dlp/yt_dlp/options.py
index 9658a9da51..39bd16e57b 100644
--- a/yt-dlp/yt_dlp/options.py
+++ b/yt-dlp/yt_dlp/options.py
@@ -511,7 +511,7 @@ def create_parser():
general.add_option(
'--live-from-start',
action='store_true', dest='live_from_start',
- help='Download livestreams from the start. Currently experimental and only supported for YouTube and Twitch')
+ help='Download livestreams from the start. Currently experimental and only supported for YouTube, Twitch, and TVer')
general.add_option(
'--no-live-from-start',
action='store_false', dest='live_from_start',