diff --git a/.github/update.log b/.github/update.log index 3145bb0016..9af5ca6df2 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1334,3 +1334,4 @@ Update On Wed Apr 15 21:30:45 CEST 2026 Update On Thu Apr 16 21:28:40 CEST 2026 Update On Fri Apr 17 21:15:01 CEST 2026 Update On Sat Apr 18 21:05:03 CEST 2026 +Update On Sun Apr 19 21:03:12 CEST 2026 diff --git a/clash-meta/common/cmd/cmd.go b/clash-meta/common/cmd/cmd.go index e015dd2e7e..17f4b2efb0 100644 --- a/clash-meta/common/cmd/cmd.go +++ b/clash-meta/common/cmd/cmd.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os/exec" + "runtime" "strings" ) @@ -34,3 +35,19 @@ func splitArgs(cmd string) []string { } return args } + +func ExecShell(shellStr string) (string, error) { + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd.exe", "/C", shellStr) + } else { + cmd = exec.Command("sh", "-c", shellStr) + } + + prepareBackgroundCommand(cmd) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%v, %s", err, string(out)) + } + return string(out), nil +} diff --git a/clash-meta/component/profile/cachefile/cache.go b/clash-meta/component/profile/cachefile/cache.go index 3c46d7338b..f0d3608fc0 100644 --- a/clash-meta/component/profile/cachefile/cache.go +++ b/clash-meta/component/profile/cachefile/cache.go @@ -22,6 +22,7 @@ var ( bucketFakeip6 = []byte("fakeip6") bucketETag = []byte("etag") bucketSubscriptionInfo = []byte("subscriptioninfo") + bucketStorage = []byte("storage") ) // CacheFile store and update the cache file diff --git a/clash-meta/component/profile/cachefile/storage.go b/clash-meta/component/profile/cachefile/storage.go new file mode 100644 index 0000000000..91f0dce111 --- /dev/null +++ b/clash-meta/component/profile/cachefile/storage.go @@ -0,0 +1,165 @@ +package cachefile + +import ( + "sort" + "time" + + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/bbolt" + "github.com/vmihailenco/msgpack/v5" +) + +const storageSizeLimit = 1024 * 1024 +const storageKeySizeLimit = 64 +const maxStorageEntries = storageSizeLimit / storageKeySizeLimit + +type StorageData struct { + Data []byte + Time time.Time +} + +func decodeStorageData(v []byte) (StorageData, error) { + var storage StorageData + if err := msgpack.Unmarshal(v, &storage); err != nil { + return StorageData{}, err + } + return storage, nil +} + +func (c *CacheFile) GetStorage(key string) []byte { + if c.DB == nil { + return nil + } + var data []byte + decodeFailed := false + err := c.DB.View(func(t *bbolt.Tx) error { + if bucket := t.Bucket(bucketStorage); bucket != nil { + if v := bucket.Get([]byte(key)); v != nil { + storage, err := decodeStorageData(v) + if err != nil { + decodeFailed = true + return err + } + data = storage.Data + } + } + return nil + }) + if err != nil { + log.Warnln("[CacheFile] read cache for key %s failed: %s", key, err.Error()) + if decodeFailed { + c.DeleteStorage(key) + } + return nil + } + return data +} + +func (c *CacheFile) SetStorage(key string, data []byte) { + if c.DB == nil { + return + } + if len(key) > storageKeySizeLimit { + log.Warnln("[CacheFile] skip storage for key %s: key exceeds %d bytes", key, storageKeySizeLimit) + return + } + if len(data) > storageSizeLimit { + log.Warnln("[CacheFile] skip storage for key %s: payload exceeds %d bytes", key, storageSizeLimit) + return + } + keyBytes := []byte(key) + payload, err := msgpack.Marshal(StorageData{ + Data: data, + Time: time.Now(), + }) + if err != nil { + return + } + err = c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketStorage) + if err != nil { + return err + } + type storageEntry struct { + Key string + Data StorageData + } + + entries := make(map[string]StorageData) + usedSize := 0 + entryCount := 0 + corruptedKeys := make([][]byte, 0) + c := bucket.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + storage, err := decodeStorageData(v) + if err != nil { + log.Warnln("[CacheFile] drop corrupted storage entry %s: %s", string(k), err.Error()) + corruptedKeys = append(corruptedKeys, append([]byte(nil), k...)) + continue + } + entryKey := string(k) + entries[entryKey] = storage + if entryKey != key { + usedSize += len(storage.Data) + entryCount++ + } + } + for _, k := range corruptedKeys { + if err := bucket.Delete(k); err != nil { + return err + } + } + + evictionQueue := make([]storageEntry, 0, len(entries)) + for entryKey, storage := range entries { + if entryKey == key { + continue + } + evictionQueue = append(evictionQueue, storageEntry{ + Key: entryKey, + Data: storage, + }) + } + sort.Slice(evictionQueue, func(i, j int) bool { + left := evictionQueue[i] + right := evictionQueue[j] + if left.Data.Time.Equal(right.Data.Time) { + return left.Key < right.Key + } + return left.Data.Time.Before(right.Data.Time) + }) + + for _, entry := range evictionQueue { + if usedSize+len(data) <= storageSizeLimit && entryCount < maxStorageEntries { + break + } + if err := bucket.Delete([]byte(entry.Key)); err != nil { + return err + } + log.Infoln("[CacheFile] evict storage entry %s to make room for %s", entry.Key, key) + usedSize -= len(entry.Data.Data) + entryCount-- + } + return bucket.Put(keyBytes, payload) + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + } +} + +func (c *CacheFile) DeleteStorage(key string) { + if c.DB == nil { + return + } + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket := t.Bucket(bucketStorage) + if bucket == nil { + return nil + } + return bucket.Delete([]byte(key)) + }) + if err != nil { + log.Warnln("[CacheFile] delete cache from %s failed: %s", c.DB.Path(), err.Error()) + } +} diff --git a/clash-meta/go.mod b/clash-meta/go.mod index 0e4202bb0e..f0d1c2fc05 100644 --- a/clash-meta/go.mod +++ b/clash-meta/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 - github.com/dlclark/regexp2 v1.11.5 + github.com/dlclark/regexp2 v1.12.0 github.com/enfein/mieru/v3 v3.31.0 github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.4.0 @@ -21,7 +21,7 @@ require ( github.com/metacubex/edwards25519 v1.2.0 github.com/metacubex/fswatch v0.1.1 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 - github.com/metacubex/http v0.1.1 + github.com/metacubex/http v0.1.2 github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 github.com/metacubex/mhurl v0.1.0 github.com/metacubex/mlkem v0.1.0 diff --git a/clash-meta/go.sum b/clash-meta/go.sum index 9d301a099a..ceac79cbfb 100644 --- a/clash-meta/go.sum +++ b/clash-meta/go.sum @@ -16,8 +16,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/enfein/mieru/v3 v3.31.0 h1:Fl2ocRCRXJzMygzdRjBHgqI996ZuIDHUmyQyovSf9sA= @@ -103,8 +103,8 @@ github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA= github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4= github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ= github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U= -github.com/metacubex/http v0.1.1 h1:zVea0zOoaZvxe51EvJMRWGNQv6MvWqJhkZSuoAjOjVw= -github.com/metacubex/http v0.1.1/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= +github.com/metacubex/http v0.1.2 h1:XxW+pMaGAvVveRqREbyWJJDDl9dRZQGJoyKBiPPPKs8= +github.com/metacubex/http v0.1.2/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc= github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE= diff --git a/clash-meta/hub/route/server.go b/clash-meta/hub/route/server.go index 4e0d0c93f5..7b8562ca75 100644 --- a/clash-meta/hub/route/server.go +++ b/clash-meta/hub/route/server.go @@ -132,6 +132,7 @@ func router(isDebug bool, secret string, dohServer string, cors Cors) *chi.Mux { r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/cache", cacheRouter()) r.Mount("/dns", dnsRouter()) + r.Mount("/storage", storageRouter()) if !embedMode { // disallow restart in embed mode r.Mount("/restart", restartRouter()) } diff --git a/clash-meta/hub/route/storage.go b/clash-meta/hub/route/storage.go new file mode 100644 index 0000000000..24e809ef4a --- /dev/null +++ b/clash-meta/hub/route/storage.go @@ -0,0 +1,59 @@ +package route + +import ( + "encoding/json" + "io" + + "github.com/metacubex/mihomo/component/profile/cachefile" + + "github.com/metacubex/chi" + "github.com/metacubex/chi/render" + "github.com/metacubex/http" +) + +func storageRouter() http.Handler { + r := chi.NewRouter() + r.Get("/{key}", getStorage) + r.Put("/{key}", setStorage) + r.Delete("/{key}", deleteStorage) + return r +} + +func getStorage(w http.ResponseWriter, r *http.Request) { + key := getEscapeParam(r, "key") + data := cachefile.Cache().GetStorage(key) + w.Header().Set("Content-Type", "application/json") + if len(data) == 0 { + w.Write([]byte("null")) + return + } + w.Write(data) +} + +func setStorage(w http.ResponseWriter, r *http.Request) { + key := getEscapeParam(r, "key") + data, err := io.ReadAll(r.Body) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(err.Error())) + return + } + if !json.Valid(data) { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + if len(data) > 1024*1024 { + render.Status(r, http.StatusRequestEntityTooLarge) + render.JSON(w, r, newError("payload exceeds 1MB limit")) + return + } + cachefile.Cache().SetStorage(key, data) + render.NoContent(w, r) +} + +func deleteStorage(w http.ResponseWriter, r *http.Request) { + key := getEscapeParam(r, "key") + cachefile.Cache().DeleteStorage(key) + render.NoContent(w, r) +} diff --git a/clash-meta/listener/inbound/vless.go b/clash-meta/listener/inbound/vless.go index 02e3f74780..2b91553c11 100644 --- a/clash-meta/listener/inbound/vless.go +++ b/clash-meta/listener/inbound/vless.go @@ -35,12 +35,12 @@ type XHTTPConfig struct { Path string `inbound:"path,omitempty"` Host string `inbound:"host,omitempty"` Mode string `inbound:"mode,omitempty"` - XPaddingBytes string `inbound:"xpadding-bytes,omitempty"` - XPaddingObfsMode bool `inbound:"xpadding-obfs-mode,omitempty"` - XPaddingKey string `inbound:"xpadding-key,omitempty"` - XPaddingHeader string `inbound:"xpadding-header,omitempty"` - XPaddingPlacement string `inbound:"xpadding-placement,omitempty"` - XPaddingMethod string `inbound:"xpadding-method,omitempty"` + XPaddingBytes string `inbound:"x-padding-bytes,omitempty"` + XPaddingObfsMode bool `inbound:"x-padding-obfs-mode,omitempty"` + XPaddingKey string `inbound:"x-padding-key,omitempty"` + XPaddingHeader string `inbound:"x-padding-header,omitempty"` + XPaddingPlacement string `inbound:"x-padding-placement,omitempty"` + XPaddingMethod string `inbound:"x-padding-method,omitempty"` UplinkHTTPMethod string `inbound:"uplink-http-method,omitempty"` SessionPlacement string `inbound:"session-placement,omitempty"` SessionKey string `inbound:"session-key,omitempty"` diff --git a/clash-meta/listener/inbound/vless_test.go b/clash-meta/listener/inbound/vless_test.go index a483c34113..834b1d2881 100644 --- a/clash-meta/listener/inbound/vless_test.go +++ b/clash-meta/listener/inbound/vless_test.go @@ -378,40 +378,43 @@ func TestInboundVless_XHTTP(t *testing.T) { } return inboundOptions, outboundOptions } - - t.Run("nosplit", func(t *testing.T) { - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVlessTLS(t, inboundOptions, outboundOptions, false) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) - }) - }) - - t.Run("split", func(t *testing.T) { - if testCase.mode == "stream-one" { // stream-one not supported download settings - return - } - - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVlessTLS(t, inboundOptions, outboundOptions, false) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) - }) - }) + testInboundVless_XHTTP(t, getConfig, testCase.mode) }) } } +func testInboundVless_XHTTP(t *testing.T, getConfig func() (inbound.VlessOption, outbound.VlessOption), mode string) { + t.Run("nosplit", func(t *testing.T) { + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) + }) + }) + + t.Run("split", func(t *testing.T) { + if mode == "stream-one" { // stream-one not supported download settings + return + } + + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) + }) + }) +} + func TestInboundVless_XHTTP_Reality(t *testing.T) { testCases := []struct { mode string @@ -455,40 +458,43 @@ func TestInboundVless_XHTTP_Reality(t *testing.T) { } return inboundOptions, outboundOptions } - - t.Run("nosplit", func(t *testing.T) { - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, outboundOptions) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) - }) - }) - - t.Run("split", func(t *testing.T) { - if testCase.mode == "stream-one" { // stream-one not supported download settings - return - } - - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVless(t, inboundOptions, outboundOptions) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) - }) - }) + testInboundVless_XHTTP_Reality(t, getConfig, testCase.mode) }) } } +func testInboundVless_XHTTP_Reality(t *testing.T, getConfig func() (inbound.VlessOption, outbound.VlessOption), mode string) { + t.Run("nosplit", func(t *testing.T) { + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, outboundOptions) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) + }) + }) + + t.Run("split", func(t *testing.T) { + if mode == "stream-one" { // stream-one not supported download settings + return + } + + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVless(t, inboundOptions, outboundOptions) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) + }) + }) +} + func TestInboundVless_XHTTP_Encryption(t *testing.T) { privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") if err != nil { @@ -526,123 +532,124 @@ func TestInboundVless_XHTTP_Encryption(t *testing.T) { } return inboundOptions, outboundOptions } - - t.Run("nosplit", func(t *testing.T) { - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, outboundOptions) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) - }) - }) - - t.Run("split", func(t *testing.T) { - if testCase.mode == "stream-one" { // stream-one not supported download settings - return - } - - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVless(t, inboundOptions, outboundOptions) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) - }) - }) + testInboundVless_XHTTP_Encryption(t, getConfig, testCase.mode) }) } } -func TestInboundVless_XHTTP_PacketUp_H1(t *testing.T) { - getConfig := func() (inbound.VlessOption, outbound.VlessOption) { - inboundOptions := inbound.VlessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - XHTTPConfig: inbound.XHTTPConfig{ - Path: "/vless-xhttp", - Host: "example.com", - Mode: "packet-up", - }, - } - outboundOptions := outbound.VlessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "xhttp", - ALPN: []string{"http/1.1"}, - XHTTPOpts: outbound.XHTTPOptions{ - Path: "/vless-xhttp", - Host: "example.com", - Mode: "packet-up", - }, - } - return inboundOptions, outboundOptions - } +func testInboundVless_XHTTP_Encryption(t *testing.T, getConfig func() (inbound.VlessOption, outbound.VlessOption), mode string) { + t.Run("nosplit", func(t *testing.T) { + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, outboundOptions) + }) - t.Run("default", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVlessTLS(t, inboundOptions, outboundOptions, false) + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) + }) }) - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) + t.Run("split", func(t *testing.T) { + if mode == "stream-one" { // stream-one not supported download settings + return + } + + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVless(t, inboundOptions, outboundOptions) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) + }) }) } -func TestInboundVless_XHTTP_PacketUp_H1_Encryption(t *testing.T) { +func TestInboundVless_XHTTP_H1(t *testing.T) { + testCases := []struct { + mode string + }{ + {mode: "auto"}, + {mode: "stream-one"}, + {mode: "stream-up"}, + {mode: "packet-up"}, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.mode, func(t *testing.T) { + getConfig := func() (inbound.VlessOption, outbound.VlessOption) { + inboundOptions := inbound.VlessOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + XHTTPConfig: inbound.XHTTPConfig{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + outboundOptions := outbound.VlessOption{ + TLS: true, + Fingerprint: tlsFingerprint, + Network: "xhttp", + ALPN: []string{"http/1.1"}, + XHTTPOpts: outbound.XHTTPOptions{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + return inboundOptions, outboundOptions + } + testInboundVless_XHTTP(t, getConfig, testCase.mode) + }) + } +} + +func TestInboundVless_XHTTP_H1_Encryption(t *testing.T) { privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") if err != nil { t.Fatal(err) return } - getConfig := func() (inbound.VlessOption, outbound.VlessOption) { - inboundOptions := inbound.VlessOption{ - Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, - XHTTPConfig: inbound.XHTTPConfig{ - Path: "/vless-xhttp", - Host: "example.com", - Mode: "packet-up", - }, - } - outboundOptions := outbound.VlessOption{ - Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, - Network: "xhttp", - ALPN: []string{"http/1.1"}, - XHTTPOpts: outbound.XHTTPOptions{ - Path: "/vless-xhttp", - Host: "example.com", - Mode: "packet-up", - }, - } - return inboundOptions, outboundOptions + testCases := []struct { + mode string + }{ + {mode: "auto"}, + {mode: "stream-one"}, + {mode: "stream-up"}, + {mode: "packet-up"}, } - - t.Run("default", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.mode, func(t *testing.T) { + getConfig := func() (inbound.VlessOption, outbound.VlessOption) { + inboundOptions := inbound.VlessOption{ + Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, + XHTTPConfig: inbound.XHTTPConfig{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + outboundOptions := outbound.VlessOption{ + Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, + Network: "xhttp", + ALPN: []string{"http/1.1"}, + XHTTPOpts: outbound.XHTTPOptions{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + return inboundOptions, outboundOptions + } + testInboundVless_XHTTP_Encryption(t, getConfig, testCase.mode) }) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) - }) + } } func withXHTTPReuse(out outbound.VlessOption) outbound.VlessOption { diff --git a/clash-meta/main.go b/clash-meta/main.go index 6f21e8110a..a9ada2b97f 100644 --- a/clash-meta/main.go +++ b/clash-meta/main.go @@ -14,6 +14,7 @@ import ( "strings" "syscall" + "github.com/metacubex/mihomo/common/cmd" "github.com/metacubex/mihomo/component/generator" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/updater" @@ -41,6 +42,8 @@ var ( externalControllerUnix string externalControllerPipe string secret string + postUp string + postDown string ) func init() { @@ -52,6 +55,8 @@ func init() { flag.StringVar(&externalControllerUnix, "ext-ctl-unix", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_UNIX"), "override external controller unix address") flag.StringVar(&externalControllerPipe, "ext-ctl-pipe", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_PIPE"), "override external controller pipe address") flag.StringVar(&secret, "secret", os.Getenv("CLASH_OVERRIDE_SECRET"), "override secret for RESTful API") + flag.StringVar(&postUp, "post-up", os.Getenv("CLASH_POST_UP"), "set post-up script") + flag.StringVar(&postDown, "post-down", os.Getenv("CLASH_POST_DOWN"), "set post-down script") flag.BoolVar(&geodataMode, "m", false, "set geodata mode") flag.BoolVar(&version, "v", false, "show current version of mihomo") flag.BoolVar(&testConfig, "t", false, "test configuration and exit") @@ -184,6 +189,19 @@ func main() { updater.RegisterGeoUpdater() } + if postDown != "" { + defer func() { + if _, err := cmd.ExecShell(postDown); err != nil { + log.Errorln("post-down script error: %s", err.Error()) + } + }() + } + if postUp != "" { + if _, err := cmd.ExecShell(postUp); err != nil { + log.Fatalln("post-up script error: %s", err.Error()) + } + } + defer executor.Shutdown() termSign := make(chan os.Signal, 1) diff --git a/clash-meta/transport/xhttp/server.go b/clash-meta/transport/xhttp/server.go index 4f41019ce1..6351d1cf06 100644 --- a/clash-meta/transport/xhttp/server.go +++ b/clash-meta/transport/xhttp/server.go @@ -28,13 +28,13 @@ type httpServerConn struct { mu sync.Mutex w http.ResponseWriter flusher http.Flusher - reader io.Reader + reader io.ReadCloser closed bool done chan struct{} once sync.Once } -func newHTTPServerConn(w http.ResponseWriter, r io.Reader) *httpServerConn { +func newHTTPServerConn(w http.ResponseWriter, r io.ReadCloser) *httpServerConn { flusher, _ := w.(http.Flusher) return &httpServerConn{ w: w, @@ -70,7 +70,7 @@ func (c *httpServerConn) Close() error { c.mu.Unlock() close(c.done) }) - return nil + return c.reader.Close() } func (c *httpServerConn) Wait() <-chan struct{} { @@ -305,6 +305,11 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") } w.WriteHeader(http.StatusOK) + + rc := http.NewResponseController(w) + _ = rc.EnableFullDuplex() // http1 need to enable full duplex manually + _ = rc.Flush() // force flush the response header + referrer := r.Header.Get("Referer") if referrer != "" && h.scStreamUpServerSecs.Max > 0 { go func() { @@ -440,9 +445,10 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") } w.WriteHeader(http.StatusOK) - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } + + rc := http.NewResponseController(w) + _ = rc.EnableFullDuplex() // http1 need to enable full duplex manually + _ = rc.Flush() // force flush the response header httpSC := newHTTPServerConn(w, r.Body) conn := &Conn{ @@ -470,9 +476,10 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Accel-Buffering", "no") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } + + rc := http.NewResponseController(w) + _ = rc.EnableFullDuplex() // http1 need to enable full duplex manually + _ = rc.Flush() // force flush the response header httpSC := newHTTPServerConn(w, r.Body) conn := &Conn{ diff --git a/clash-nyanpasu/backend/Cargo.lock b/clash-nyanpasu/backend/Cargo.lock index 44bc297fb6..5a07b91dd6 100644 --- a/clash-nyanpasu/backend/Cargo.lock +++ b/clash-nyanpasu/backend/Cargo.lock @@ -6225,9 +6225,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.3.3" +version = "5.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" dependencies = [ "dunce", "is-wsl", @@ -9784,9 +9784,9 @@ dependencies = [ [[package]] name = "test-log" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4" +checksum = "2f46bf474f0a4afebf92f076d54fd5e63423d9438b8c278a3d2ccb0f47f7cdb3" dependencies = [ "env_logger", "test-log-macros", @@ -9794,16 +9794,26 @@ dependencies = [ ] [[package]] -name = "test-log-macros" -version = "0.2.19" +name = "test-log-core" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" +checksum = "37d4d41320b48bc4a211a9021678fcc0c99569b594ea31c93735b8e517102b4c" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "test-log-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9beb9249a81e430dffd42400a49019bcf548444f1968ff23080a625de0d4d320" +dependencies = [ + "syn 2.0.117", + "test-log-core", +] + [[package]] name = "textwrap" version = "0.16.2" diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 2830c9d71d..d61b4a8f52 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -70,10 +70,10 @@ "@csstools/normalize.css": "12.1.1", "@emotion/babel-plugin": "11.13.5", "@emotion/react": "11.14.0", - "@iconify/json": "2.2.463", + "@iconify/json": "2.2.464", "@monaco-editor/react": "4.7.0", "@tanstack/react-query": "5.97.0", - "@tanstack/react-router": "1.168.22", + "@tanstack/react-router": "1.168.23", "@tanstack/react-router-devtools": "1.166.13", "@tanstack/router-plugin": "1.167.22", "@tauri-apps/plugin-clipboard-manager": "2.3.2", diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index e26634e0da..d84aea82a0 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -2,7 +2,7 @@ "manifest_version": 1, "latest": { "mihomo": "v1.19.23", - "mihomo_alpha": "alpha-0e0265f", + "mihomo_alpha": "alpha-3f2b0ba", "clash_rs": "v0.9.7", "clash_premium": "2023-09-05-gdcc8d87", "clash_rs_alpha": "0.9.7-alpha+sha.dd693bf" @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2026-04-17T22:24:36.473Z" + "updated_at": "2026-04-18T22:23:41.956Z" } diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index 3e246e4f85..86e6fc3045 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -67,7 +67,7 @@ "conventional-changelog-conventionalcommits": "9.3.1", "cross-env": "10.1.0", "dedent": "1.7.2", - "globals": "17.4.0", + "globals": "17.5.0", "knip": "6.3.1", "lint-staged": "16.4.0", "npm-run-all2": "8.0.4", diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index 659913a402..bd4d38f4e8 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -56,8 +56,8 @@ importers: specifier: 1.7.2 version: 1.7.2(babel-plugin-macros@3.1.0) globals: - specifier: 17.4.0 - version: 17.4.0 + specifier: 17.5.0 + version: 17.5.0 knip: specifier: 6.3.1 version: 6.3.1 @@ -226,7 +226,7 @@ importers: version: 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-zod-adapter': specifier: 1.81.5 - version: 1.81.5(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(zod@4.3.6) + version: 1.81.5(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(zod@4.3.6) '@tauri-apps/api': specifier: 2.10.1 version: 2.10.1 @@ -337,8 +337,8 @@ importers: specifier: 11.14.0 version: 11.14.0(@types/react@19.2.14)(react@19.2.5) '@iconify/json': - specifier: 2.2.463 - version: 2.2.463 + specifier: 2.2.464 + version: 2.2.464 '@monaco-editor/react': specifier: 4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -346,14 +346,14 @@ importers: specifier: 5.97.0 version: 5.97.0(react@19.2.5) '@tanstack/react-router': - specifier: 1.168.22 - version: 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: 1.168.23 + version: 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-router-devtools': specifier: 1.166.13 - version: 1.166.13(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 1.166.13(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-plugin': specifier: 1.167.22 - version: 1.167.22(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.167.22(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)) '@tauri-apps/plugin-clipboard-manager': specifier: 2.3.2 version: 2.3.2 @@ -1720,8 +1720,8 @@ packages: prettier-plugin-ember-template-tag: optional: true - '@iconify/json@2.2.463': - resolution: {integrity: sha512-VZ0n+99OWe9677b04KPF0NajDbFEyWNxMalXZA/4j8HrqyVvY+N1XN/EIER4ceQlKQJ501w9UxLJZjZ5mga0xA==} + '@iconify/json@2.2.464': + resolution: {integrity: sha512-VSU7hPHuqqEwQk8Hmy+88iGmsL6KE2JAv1ZwxE0z50+JpymIzhZMHcuSrL2MOFtTDZQ4eJ7Ao9Hdm+TCctDwCQ==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -4052,8 +4052,8 @@ packages: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.168.22': - resolution: {integrity: sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q==} + '@tanstack/react-router@1.168.23': + resolution: {integrity: sha512-+GblieDnutG6oipJJPNtRJjrWF8QTZEG/l0532+BngFkVK48oHNOcvIkSoAFYftK1egAwM7KBxXsb0Ou+X6/MQ==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -5761,8 +5761,8 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - globals@17.4.0: - resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} engines: {node: '>=18'} globby@16.2.0: @@ -9538,7 +9538,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@iconify/json@2.2.463': + '@iconify/json@2.2.464': dependencies: '@iconify/types': 2.0.0 pathe: 2.0.3 @@ -11650,9 +11650,9 @@ snapshots: '@tanstack/query-core': 5.97.0 react: 19.2.5 - '@tanstack/react-router-devtools@1.166.13(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-router-devtools@1.166.13(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@tanstack/react-router': 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-router': 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-devtools-core': 1.167.3(@tanstack/router-core@1.168.15)(csstype@3.2.3) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -11661,7 +11661,7 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tanstack/history': 1.161.6 '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -11723,7 +11723,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.167.22(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/router-plugin@1.167.22(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) @@ -11739,7 +11739,7 @@ snapshots: unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-router': 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) vite: 7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -11758,9 +11758,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(zod@4.3.6)': + '@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(zod@4.3.6)': dependencies: - '@tanstack/react-router': 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-router': 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) zod: 4.3.6 '@tanstack/store@0.9.3': {} @@ -13469,7 +13469,7 @@ snapshots: globals@11.12.0: optional: true - globals@17.4.0: {} + globals@17.5.0: {} globby@16.2.0: dependencies: diff --git a/mieru/test/deploy/mihomo/mihomo-server.yaml b/mieru/test/deploy/mihomo/mihomo-server.yaml index d7d84c6568..44a77555ac 100644 --- a/mieru/test/deploy/mihomo/mihomo-server.yaml +++ b/mieru/test/deploy/mihomo/mihomo-server.yaml @@ -7,6 +7,7 @@ listeners: transport: TCP users: baozi: manlianpenfen + user-hint-is-mandatory: true - name: mieru-udp type: mieru port: 8966 @@ -14,3 +15,4 @@ listeners: transport: UDP users: baozi: manlianpenfen + user-hint-is-mandatory: true diff --git a/mihomo/common/cmd/cmd.go b/mihomo/common/cmd/cmd.go index e015dd2e7e..17f4b2efb0 100644 --- a/mihomo/common/cmd/cmd.go +++ b/mihomo/common/cmd/cmd.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os/exec" + "runtime" "strings" ) @@ -34,3 +35,19 @@ func splitArgs(cmd string) []string { } return args } + +func ExecShell(shellStr string) (string, error) { + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd.exe", "/C", shellStr) + } else { + cmd = exec.Command("sh", "-c", shellStr) + } + + prepareBackgroundCommand(cmd) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%v, %s", err, string(out)) + } + return string(out), nil +} diff --git a/mihomo/component/profile/cachefile/cache.go b/mihomo/component/profile/cachefile/cache.go index 3c46d7338b..f0d3608fc0 100644 --- a/mihomo/component/profile/cachefile/cache.go +++ b/mihomo/component/profile/cachefile/cache.go @@ -22,6 +22,7 @@ var ( bucketFakeip6 = []byte("fakeip6") bucketETag = []byte("etag") bucketSubscriptionInfo = []byte("subscriptioninfo") + bucketStorage = []byte("storage") ) // CacheFile store and update the cache file diff --git a/mihomo/component/profile/cachefile/storage.go b/mihomo/component/profile/cachefile/storage.go new file mode 100644 index 0000000000..91f0dce111 --- /dev/null +++ b/mihomo/component/profile/cachefile/storage.go @@ -0,0 +1,165 @@ +package cachefile + +import ( + "sort" + "time" + + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/bbolt" + "github.com/vmihailenco/msgpack/v5" +) + +const storageSizeLimit = 1024 * 1024 +const storageKeySizeLimit = 64 +const maxStorageEntries = storageSizeLimit / storageKeySizeLimit + +type StorageData struct { + Data []byte + Time time.Time +} + +func decodeStorageData(v []byte) (StorageData, error) { + var storage StorageData + if err := msgpack.Unmarshal(v, &storage); err != nil { + return StorageData{}, err + } + return storage, nil +} + +func (c *CacheFile) GetStorage(key string) []byte { + if c.DB == nil { + return nil + } + var data []byte + decodeFailed := false + err := c.DB.View(func(t *bbolt.Tx) error { + if bucket := t.Bucket(bucketStorage); bucket != nil { + if v := bucket.Get([]byte(key)); v != nil { + storage, err := decodeStorageData(v) + if err != nil { + decodeFailed = true + return err + } + data = storage.Data + } + } + return nil + }) + if err != nil { + log.Warnln("[CacheFile] read cache for key %s failed: %s", key, err.Error()) + if decodeFailed { + c.DeleteStorage(key) + } + return nil + } + return data +} + +func (c *CacheFile) SetStorage(key string, data []byte) { + if c.DB == nil { + return + } + if len(key) > storageKeySizeLimit { + log.Warnln("[CacheFile] skip storage for key %s: key exceeds %d bytes", key, storageKeySizeLimit) + return + } + if len(data) > storageSizeLimit { + log.Warnln("[CacheFile] skip storage for key %s: payload exceeds %d bytes", key, storageSizeLimit) + return + } + keyBytes := []byte(key) + payload, err := msgpack.Marshal(StorageData{ + Data: data, + Time: time.Now(), + }) + if err != nil { + return + } + err = c.DB.Batch(func(t *bbolt.Tx) error { + bucket, err := t.CreateBucketIfNotExists(bucketStorage) + if err != nil { + return err + } + type storageEntry struct { + Key string + Data StorageData + } + + entries := make(map[string]StorageData) + usedSize := 0 + entryCount := 0 + corruptedKeys := make([][]byte, 0) + c := bucket.Cursor() + for k, v := c.First(); k != nil; k, v = c.Next() { + storage, err := decodeStorageData(v) + if err != nil { + log.Warnln("[CacheFile] drop corrupted storage entry %s: %s", string(k), err.Error()) + corruptedKeys = append(corruptedKeys, append([]byte(nil), k...)) + continue + } + entryKey := string(k) + entries[entryKey] = storage + if entryKey != key { + usedSize += len(storage.Data) + entryCount++ + } + } + for _, k := range corruptedKeys { + if err := bucket.Delete(k); err != nil { + return err + } + } + + evictionQueue := make([]storageEntry, 0, len(entries)) + for entryKey, storage := range entries { + if entryKey == key { + continue + } + evictionQueue = append(evictionQueue, storageEntry{ + Key: entryKey, + Data: storage, + }) + } + sort.Slice(evictionQueue, func(i, j int) bool { + left := evictionQueue[i] + right := evictionQueue[j] + if left.Data.Time.Equal(right.Data.Time) { + return left.Key < right.Key + } + return left.Data.Time.Before(right.Data.Time) + }) + + for _, entry := range evictionQueue { + if usedSize+len(data) <= storageSizeLimit && entryCount < maxStorageEntries { + break + } + if err := bucket.Delete([]byte(entry.Key)); err != nil { + return err + } + log.Infoln("[CacheFile] evict storage entry %s to make room for %s", entry.Key, key) + usedSize -= len(entry.Data.Data) + entryCount-- + } + return bucket.Put(keyBytes, payload) + }) + if err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error()) + } +} + +func (c *CacheFile) DeleteStorage(key string) { + if c.DB == nil { + return + } + err := c.DB.Batch(func(t *bbolt.Tx) error { + bucket := t.Bucket(bucketStorage) + if bucket == nil { + return nil + } + return bucket.Delete([]byte(key)) + }) + if err != nil { + log.Warnln("[CacheFile] delete cache from %s failed: %s", c.DB.Path(), err.Error()) + } +} diff --git a/mihomo/go.mod b/mihomo/go.mod index 0e4202bb0e..f0d1c2fc05 100644 --- a/mihomo/go.mod +++ b/mihomo/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 - github.com/dlclark/regexp2 v1.11.5 + github.com/dlclark/regexp2 v1.12.0 github.com/enfein/mieru/v3 v3.31.0 github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.4.0 @@ -21,7 +21,7 @@ require ( github.com/metacubex/edwards25519 v1.2.0 github.com/metacubex/fswatch v0.1.1 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 - github.com/metacubex/http v0.1.1 + github.com/metacubex/http v0.1.2 github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 github.com/metacubex/mhurl v0.1.0 github.com/metacubex/mlkem v0.1.0 diff --git a/mihomo/go.sum b/mihomo/go.sum index 9d301a099a..ceac79cbfb 100644 --- a/mihomo/go.sum +++ b/mihomo/go.sum @@ -16,8 +16,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/enfein/mieru/v3 v3.31.0 h1:Fl2ocRCRXJzMygzdRjBHgqI996ZuIDHUmyQyovSf9sA= @@ -103,8 +103,8 @@ github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA= github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4= github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ= github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U= -github.com/metacubex/http v0.1.1 h1:zVea0zOoaZvxe51EvJMRWGNQv6MvWqJhkZSuoAjOjVw= -github.com/metacubex/http v0.1.1/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= +github.com/metacubex/http v0.1.2 h1:XxW+pMaGAvVveRqREbyWJJDDl9dRZQGJoyKBiPPPKs8= +github.com/metacubex/http v0.1.2/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc= github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE= diff --git a/mihomo/hub/route/server.go b/mihomo/hub/route/server.go index 4e0d0c93f5..7b8562ca75 100644 --- a/mihomo/hub/route/server.go +++ b/mihomo/hub/route/server.go @@ -132,6 +132,7 @@ func router(isDebug bool, secret string, dohServer string, cors Cors) *chi.Mux { r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/cache", cacheRouter()) r.Mount("/dns", dnsRouter()) + r.Mount("/storage", storageRouter()) if !embedMode { // disallow restart in embed mode r.Mount("/restart", restartRouter()) } diff --git a/mihomo/hub/route/storage.go b/mihomo/hub/route/storage.go new file mode 100644 index 0000000000..24e809ef4a --- /dev/null +++ b/mihomo/hub/route/storage.go @@ -0,0 +1,59 @@ +package route + +import ( + "encoding/json" + "io" + + "github.com/metacubex/mihomo/component/profile/cachefile" + + "github.com/metacubex/chi" + "github.com/metacubex/chi/render" + "github.com/metacubex/http" +) + +func storageRouter() http.Handler { + r := chi.NewRouter() + r.Get("/{key}", getStorage) + r.Put("/{key}", setStorage) + r.Delete("/{key}", deleteStorage) + return r +} + +func getStorage(w http.ResponseWriter, r *http.Request) { + key := getEscapeParam(r, "key") + data := cachefile.Cache().GetStorage(key) + w.Header().Set("Content-Type", "application/json") + if len(data) == 0 { + w.Write([]byte("null")) + return + } + w.Write(data) +} + +func setStorage(w http.ResponseWriter, r *http.Request) { + key := getEscapeParam(r, "key") + data, err := io.ReadAll(r.Body) + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(err.Error())) + return + } + if !json.Valid(data) { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, ErrBadRequest) + return + } + if len(data) > 1024*1024 { + render.Status(r, http.StatusRequestEntityTooLarge) + render.JSON(w, r, newError("payload exceeds 1MB limit")) + return + } + cachefile.Cache().SetStorage(key, data) + render.NoContent(w, r) +} + +func deleteStorage(w http.ResponseWriter, r *http.Request) { + key := getEscapeParam(r, "key") + cachefile.Cache().DeleteStorage(key) + render.NoContent(w, r) +} diff --git a/mihomo/listener/inbound/vless.go b/mihomo/listener/inbound/vless.go index 02e3f74780..2b91553c11 100644 --- a/mihomo/listener/inbound/vless.go +++ b/mihomo/listener/inbound/vless.go @@ -35,12 +35,12 @@ type XHTTPConfig struct { Path string `inbound:"path,omitempty"` Host string `inbound:"host,omitempty"` Mode string `inbound:"mode,omitempty"` - XPaddingBytes string `inbound:"xpadding-bytes,omitempty"` - XPaddingObfsMode bool `inbound:"xpadding-obfs-mode,omitempty"` - XPaddingKey string `inbound:"xpadding-key,omitempty"` - XPaddingHeader string `inbound:"xpadding-header,omitempty"` - XPaddingPlacement string `inbound:"xpadding-placement,omitempty"` - XPaddingMethod string `inbound:"xpadding-method,omitempty"` + XPaddingBytes string `inbound:"x-padding-bytes,omitempty"` + XPaddingObfsMode bool `inbound:"x-padding-obfs-mode,omitempty"` + XPaddingKey string `inbound:"x-padding-key,omitempty"` + XPaddingHeader string `inbound:"x-padding-header,omitempty"` + XPaddingPlacement string `inbound:"x-padding-placement,omitempty"` + XPaddingMethod string `inbound:"x-padding-method,omitempty"` UplinkHTTPMethod string `inbound:"uplink-http-method,omitempty"` SessionPlacement string `inbound:"session-placement,omitempty"` SessionKey string `inbound:"session-key,omitempty"` diff --git a/mihomo/listener/inbound/vless_test.go b/mihomo/listener/inbound/vless_test.go index a483c34113..834b1d2881 100644 --- a/mihomo/listener/inbound/vless_test.go +++ b/mihomo/listener/inbound/vless_test.go @@ -378,40 +378,43 @@ func TestInboundVless_XHTTP(t *testing.T) { } return inboundOptions, outboundOptions } - - t.Run("nosplit", func(t *testing.T) { - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVlessTLS(t, inboundOptions, outboundOptions, false) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) - }) - }) - - t.Run("split", func(t *testing.T) { - if testCase.mode == "stream-one" { // stream-one not supported download settings - return - } - - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVlessTLS(t, inboundOptions, outboundOptions, false) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) - }) - }) + testInboundVless_XHTTP(t, getConfig, testCase.mode) }) } } +func testInboundVless_XHTTP(t *testing.T, getConfig func() (inbound.VlessOption, outbound.VlessOption), mode string) { + t.Run("nosplit", func(t *testing.T) { + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) + }) + }) + + t.Run("split", func(t *testing.T) { + if mode == "stream-one" { // stream-one not supported download settings + return + } + + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) + }) + }) +} + func TestInboundVless_XHTTP_Reality(t *testing.T) { testCases := []struct { mode string @@ -455,40 +458,43 @@ func TestInboundVless_XHTTP_Reality(t *testing.T) { } return inboundOptions, outboundOptions } - - t.Run("nosplit", func(t *testing.T) { - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, outboundOptions) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) - }) - }) - - t.Run("split", func(t *testing.T) { - if testCase.mode == "stream-one" { // stream-one not supported download settings - return - } - - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVless(t, inboundOptions, outboundOptions) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) - }) - }) + testInboundVless_XHTTP_Reality(t, getConfig, testCase.mode) }) } } +func testInboundVless_XHTTP_Reality(t *testing.T, getConfig func() (inbound.VlessOption, outbound.VlessOption), mode string) { + t.Run("nosplit", func(t *testing.T) { + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, outboundOptions) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) + }) + }) + + t.Run("split", func(t *testing.T) { + if mode == "stream-one" { // stream-one not supported download settings + return + } + + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVless(t, inboundOptions, outboundOptions) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) + }) + }) +} + func TestInboundVless_XHTTP_Encryption(t *testing.T) { privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") if err != nil { @@ -526,123 +532,124 @@ func TestInboundVless_XHTTP_Encryption(t *testing.T) { } return inboundOptions, outboundOptions } - - t.Run("nosplit", func(t *testing.T) { - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, outboundOptions) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) - }) - }) - - t.Run("split", func(t *testing.T) { - if testCase.mode == "stream-one" { // stream-one not supported download settings - return - } - - t.Run("single", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVless(t, inboundOptions, outboundOptions) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} - testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) - }) - }) + testInboundVless_XHTTP_Encryption(t, getConfig, testCase.mode) }) } } -func TestInboundVless_XHTTP_PacketUp_H1(t *testing.T) { - getConfig := func() (inbound.VlessOption, outbound.VlessOption) { - inboundOptions := inbound.VlessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - XHTTPConfig: inbound.XHTTPConfig{ - Path: "/vless-xhttp", - Host: "example.com", - Mode: "packet-up", - }, - } - outboundOptions := outbound.VlessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - Network: "xhttp", - ALPN: []string{"http/1.1"}, - XHTTPOpts: outbound.XHTTPOptions{ - Path: "/vless-xhttp", - Host: "example.com", - Mode: "packet-up", - }, - } - return inboundOptions, outboundOptions - } +func testInboundVless_XHTTP_Encryption(t *testing.T, getConfig func() (inbound.VlessOption, outbound.VlessOption), mode string) { + t.Run("nosplit", func(t *testing.T) { + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, outboundOptions) + }) - t.Run("default", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVlessTLS(t, inboundOptions, outboundOptions, false) + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) + }) }) - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVlessTLS(t, inboundOptions, withXHTTPReuse(outboundOptions), false) + t.Run("split", func(t *testing.T) { + if mode == "stream-one" { // stream-one not supported download settings + return + } + + t.Run("single", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVless(t, inboundOptions, outboundOptions) + }) + + t.Run("reuse", func(t *testing.T) { + inboundOptions, outboundOptions := getConfig() + outboundOptions.XHTTPOpts.DownloadSettings = &outbound.XHTTPDownloadSettings{} + testInboundVless(t, inboundOptions, withXHTTPReuse(outboundOptions)) + }) }) } -func TestInboundVless_XHTTP_PacketUp_H1_Encryption(t *testing.T) { +func TestInboundVless_XHTTP_H1(t *testing.T) { + testCases := []struct { + mode string + }{ + {mode: "auto"}, + {mode: "stream-one"}, + {mode: "stream-up"}, + {mode: "packet-up"}, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.mode, func(t *testing.T) { + getConfig := func() (inbound.VlessOption, outbound.VlessOption) { + inboundOptions := inbound.VlessOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + XHTTPConfig: inbound.XHTTPConfig{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + outboundOptions := outbound.VlessOption{ + TLS: true, + Fingerprint: tlsFingerprint, + Network: "xhttp", + ALPN: []string{"http/1.1"}, + XHTTPOpts: outbound.XHTTPOptions{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + return inboundOptions, outboundOptions + } + testInboundVless_XHTTP(t, getConfig, testCase.mode) + }) + } +} + +func TestInboundVless_XHTTP_H1_Encryption(t *testing.T) { privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("") if err != nil { t.Fatal(err) return } - getConfig := func() (inbound.VlessOption, outbound.VlessOption) { - inboundOptions := inbound.VlessOption{ - Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, - XHTTPConfig: inbound.XHTTPConfig{ - Path: "/vless-xhttp", - Host: "example.com", - Mode: "packet-up", - }, - } - outboundOptions := outbound.VlessOption{ - Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, - Network: "xhttp", - ALPN: []string{"http/1.1"}, - XHTTPOpts: outbound.XHTTPOptions{ - Path: "/vless-xhttp", - Host: "example.com", - Mode: "packet-up", - }, - } - return inboundOptions, outboundOptions + testCases := []struct { + mode string + }{ + {mode: "auto"}, + {mode: "stream-one"}, + {mode: "stream-up"}, + {mode: "packet-up"}, } - - t.Run("default", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.mode, func(t *testing.T) { + getConfig := func() (inbound.VlessOption, outbound.VlessOption) { + inboundOptions := inbound.VlessOption{ + Decryption: "mlkem768x25519plus.native.600s." + privateKeyBase64, + XHTTPConfig: inbound.XHTTPConfig{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + outboundOptions := outbound.VlessOption{ + Encryption: "mlkem768x25519plus.native.0rtt." + passwordBase64, + Network: "xhttp", + ALPN: []string{"http/1.1"}, + XHTTPOpts: outbound.XHTTPOptions{ + Path: "/vless-xhttp", + Host: "example.com", + Mode: testCase.mode, + }, + } + return inboundOptions, outboundOptions + } + testInboundVless_XHTTP_Encryption(t, getConfig, testCase.mode) }) - }) - - t.Run("reuse", func(t *testing.T) { - inboundOptions, outboundOptions := getConfig() - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) - }) + } } func withXHTTPReuse(out outbound.VlessOption) outbound.VlessOption { diff --git a/mihomo/main.go b/mihomo/main.go index 6f21e8110a..a9ada2b97f 100644 --- a/mihomo/main.go +++ b/mihomo/main.go @@ -14,6 +14,7 @@ import ( "strings" "syscall" + "github.com/metacubex/mihomo/common/cmd" "github.com/metacubex/mihomo/component/generator" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/updater" @@ -41,6 +42,8 @@ var ( externalControllerUnix string externalControllerPipe string secret string + postUp string + postDown string ) func init() { @@ -52,6 +55,8 @@ func init() { flag.StringVar(&externalControllerUnix, "ext-ctl-unix", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_UNIX"), "override external controller unix address") flag.StringVar(&externalControllerPipe, "ext-ctl-pipe", os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER_PIPE"), "override external controller pipe address") flag.StringVar(&secret, "secret", os.Getenv("CLASH_OVERRIDE_SECRET"), "override secret for RESTful API") + flag.StringVar(&postUp, "post-up", os.Getenv("CLASH_POST_UP"), "set post-up script") + flag.StringVar(&postDown, "post-down", os.Getenv("CLASH_POST_DOWN"), "set post-down script") flag.BoolVar(&geodataMode, "m", false, "set geodata mode") flag.BoolVar(&version, "v", false, "show current version of mihomo") flag.BoolVar(&testConfig, "t", false, "test configuration and exit") @@ -184,6 +189,19 @@ func main() { updater.RegisterGeoUpdater() } + if postDown != "" { + defer func() { + if _, err := cmd.ExecShell(postDown); err != nil { + log.Errorln("post-down script error: %s", err.Error()) + } + }() + } + if postUp != "" { + if _, err := cmd.ExecShell(postUp); err != nil { + log.Fatalln("post-up script error: %s", err.Error()) + } + } + defer executor.Shutdown() termSign := make(chan os.Signal, 1) diff --git a/mihomo/transport/xhttp/server.go b/mihomo/transport/xhttp/server.go index 4f41019ce1..6351d1cf06 100644 --- a/mihomo/transport/xhttp/server.go +++ b/mihomo/transport/xhttp/server.go @@ -28,13 +28,13 @@ type httpServerConn struct { mu sync.Mutex w http.ResponseWriter flusher http.Flusher - reader io.Reader + reader io.ReadCloser closed bool done chan struct{} once sync.Once } -func newHTTPServerConn(w http.ResponseWriter, r io.Reader) *httpServerConn { +func newHTTPServerConn(w http.ResponseWriter, r io.ReadCloser) *httpServerConn { flusher, _ := w.(http.Flusher) return &httpServerConn{ w: w, @@ -70,7 +70,7 @@ func (c *httpServerConn) Close() error { c.mu.Unlock() close(c.done) }) - return nil + return c.reader.Close() } func (c *httpServerConn) Wait() <-chan struct{} { @@ -305,6 +305,11 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") } w.WriteHeader(http.StatusOK) + + rc := http.NewResponseController(w) + _ = rc.EnableFullDuplex() // http1 need to enable full duplex manually + _ = rc.Flush() // force flush the response header + referrer := r.Header.Get("Referer") if referrer != "" && h.scStreamUpServerSecs.Max > 0 { go func() { @@ -440,9 +445,10 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") } w.WriteHeader(http.StatusOK) - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } + + rc := http.NewResponseController(w) + _ = rc.EnableFullDuplex() // http1 need to enable full duplex manually + _ = rc.Flush() // force flush the response header httpSC := newHTTPServerConn(w, r.Body) conn := &Conn{ @@ -470,9 +476,10 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Accel-Buffering", "no") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } + + rc := http.NewResponseController(w) + _ = rc.EnableFullDuplex() // http1 need to enable full duplex manually + _ = rc.Flush() // force flush the response header httpSC := newHTTPServerConn(w, r.Body) conn := &Conn{ diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua index 557fcf635d..397944eb42 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua @@ -48,15 +48,23 @@ o.default = "" o:depends("console_auth", true) ---- Console Port -o = s:option(Value, "console_port", translate("Console Port"), translate( - "In the browser input routing IP plus port access, such as:192.168.1.1:1188")) -o.default = "1188" +o = s:option(Value, "console_port", translate("Console Port"), translate("In the browser input routing IP plus port access, such as:192.168.1.1:1188")) +o.datatype = "port" +o.default = 1188 o:depends("balancing_enable", true) o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) o.default = "0" o:depends("balancing_enable", true) +o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy")) +o:value("roundrobin", "roundRobin") +o:value("random", "random") +o:value("leastconn", "leastConn") +o:value("source", "source") +o.default = "roundrobin" +o:depends("balancing_enable", true) + ---- Health Check Type o = s:option(ListValue, "health_check_type", translate("Health Check Type")) o.default = "passwall_logic" @@ -77,9 +85,12 @@ o.description = translate("The URL used to detect the connection status.") o:depends("health_check_type", "passwall_logic") ---- Health Check Inter -o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds")) -o.default = "60" +o = s:option(Value, "health_check_inter", translate("Health Check Inter")) +o.datatype = "min(20)" +o.default = 60 o:depends("balancing_enable", true) +o.description = translate("Units:seconds") .. " ( ≥ 20s )
" .. + translate("Note: Frequent health checks increase CPU usage; an interval ≥ 60 seconds is recommended.") o = s:option(DummyValue, "health_check_tips", " ") o.rawhtml = true @@ -89,11 +100,10 @@ end o:depends("health_check_type", "passwall_logic") -- [[ Balancing Settings ]]-- -s = m:section(TypedSection, "haproxy_config", translate("Node List"), - "" .. - translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. - "\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. - "") +s = m:section(TypedSection, "haproxy_config", translate("Node List")) +s.description = "" .. + translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "
" .. + translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "
" s.template = "cbi/tblsection" s.sortable = true s.anonymous = true @@ -148,7 +158,7 @@ o.rmempty = false ---- Node Weight o = s:option(Value, "lbweight", translate("Node Weight")) -o.datatype = "uinteger" +o.datatype = "min(1)" o.default = 5 o.rmempty = false diff --git a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po index 5741f02a83..011fcf92c6 100644 --- a/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po +++ b/openwrt-passwall/luci-app-passwall/po/zh-cn/passwall.po @@ -928,6 +928,9 @@ msgstr "内置实现" msgid "Health Check Inter" msgstr "健康检查节点间隔时间" +msgid "Note: Frequent health checks increase CPU usage; an interval ≥ 60 seconds is recommended." +msgstr "注意:检测过于频繁会增加 CPU 负载,建议间隔 ≥60 秒。" + msgid "When the URL test is used, the load balancing node will be converted into a Socks node. when node list set customizing, must be a Socks node, otherwise the health check will be invalid." msgstr "当使用 URL 测试时,负载均衡节点将转换成 Socks 节点。下面的节点列表自定义时必须为 Socks 节点,否则健康检查将无效。" diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/haproxy.lua b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/haproxy.lua index fcff734469..ba5e171fb2 100644 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/haproxy.lua +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/haproxy.lua @@ -1,7 +1,7 @@ #!/usr/bin/lua local api = require ("luci.passwall.api") -local appname = "passwall" +local appname = api.appname local fs = api.fs local jsonc = api.jsonc local uci = api.uci @@ -24,7 +24,8 @@ local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53" local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1" local health_check_type = uci:get(appname, "@global_haproxy[0]", "health_check_type") or "tcp" -local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "10" +local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "20" +local balancingStrategy = uci:get(appname, "@global_haproxy[0]", "balancingStrategy") or "roundrobin" local console_port = uci:get(appname, "@global_haproxy[0]", "console_port") local bind_local = uci:get(appname, "@global_haproxy[0]", "bind_local") or "0" local bind_address = "0.0.0.0" @@ -40,35 +41,37 @@ local f_out = io.open(haproxy_file, "a") local haproxy_config = [[ global daemon - log 127.0.0.1 local2 - maxconn 60000 - stats socket {{path}}/haproxy.sock + log 127.0.0.1 local2 + maxconn 60000 + stats socket {{path}}/haproxy.sock nbthread {{nbthread}} external-check insecure-fork-wanted + spread-checks 50 + tune.max-checks-per-thread 5 defaults - mode tcp - log global - option tcplog - option dontlognull + mode tcp + log global + option tcplog + option dontlognull option http-server-close - #option forwardfor except 127.0.0.0/8 - option redispatch - retries 2 - timeout http-request 10s - timeout queue 1m - timeout connect 10s - timeout client 1m - timeout server 1m + #option forwardfor except 127.0.0.0/8 + option redispatch + retries 2 + timeout http-request 10s + timeout queue 1m + timeout connect 10s + timeout client 1m + timeout server 1m timeout http-keep-alive 10s - timeout check 10s - maxconn 3000 - + timeout check 10s + maxconn 3000 + resolvers mydns - resolve_retries 1 - timeout resolve 5s - hold valid 600s + resolve_retries 1 + timeout resolve 5s + hold valid 600s {{dns}} ]] @@ -99,10 +102,12 @@ uci:foreach(appname, "haproxy_config", function(t) local lbss = t.lbss local listen_port = tonumber(t.haproxy_port) or 0 local server_node = uci:get_all(appname, lbss) - if server_node and server_node.address and server_node.port then - server_remark = server_node.address .. ":" .. server_node.port + local hop = (health_check_type == "passwall_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil + hop = hop and hop:gsub(":", "-") or nil + if server_node and server_node.address and (server_node.port or hop) then + server_remark = server_node.address .. ":" .. (server_node.port or hop) server_address = server_node.address - server_port = server_node.port + server_port = server_node.port or hop t.origin_address = server_address t.origin_port = server_port if health_check_type == "passwall_logic" then @@ -145,6 +150,10 @@ uci:foreach(appname, "haproxy_config", function(t) end end) +local inter = tonumber((health_check_inter or "20"):match("^%d+")) +inter = (inter and inter >= 20) and inter or 20 +local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s" + local sortTable = {} for i in pairs(listens) do if i ~= nil then @@ -160,14 +169,14 @@ for i, port in pairs(sortTable) do listen %s bind %s:%s mode tcp - balance roundrobin -]], port, bind_address, port)) + balance %s +]], port, bind_address, port, balancingStrategy)) if health_check_type == "passwall_logic" then f_out:write(string.format([[ option external-check - external-check command "/usr/share/passwall/haproxy_check.sh" -]], port, port)) + external-check command "/usr/share/%s/haproxy_check.sh" +]], appname)) end local count_M, count_B = 1, 1 @@ -182,7 +191,7 @@ listen %s count_B = count_B + 1 end local server = o.server_address .. ":" .. o.server_port - local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check inter {{inter}} rise 1 fall 3 {{backup}}" + local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}" server_conf = server_conf:gsub("{{remark}}", remark) server_conf = server_conf:gsub("{{server}}", server) server_conf = server_conf:gsub("{{weight}}", o.lbweight) @@ -191,13 +200,13 @@ listen %s resolvers = "" end server_conf = server_conf:gsub("{{resolvers}}", resolvers) - server_conf = server_conf:gsub("{{inter}}", tonumber(health_check_inter) .. "s") + server_conf = server_conf:gsub("{{inter_arg}}", inter_arg) server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "") - f_out:write(" " .. server_conf .. "\n") + f_out:write(" " .. api.trim(server_conf) .. "\n") if o.export ~= "0" then - sys.call(string.format(". /usr/share/passwall/utils.sh ; add_ip2route %s %s", o.origin_address, o.export)) + sys.call(string.format(". /usr/share/%s/utils.sh ; add_ip2route %s %s", appname, o.origin_address, o.export)) end log(string.format(" | - 出口节点:%s:%s,权重:%s", o.origin_address, o.origin_port, o.lbweight)) diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh index 91c1531b14..cea5d6294e 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh @@ -25,7 +25,7 @@ if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then extra_params="${extra_params} --retry-all-errors" fi -status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 2 --max-time 10 -w "%{http_code}" "${probeUrl}") +status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}") case "$status" in 200|204) diff --git a/openwrt-passwall2/luci-app-passwall2/Makefile b/openwrt-passwall2/luci-app-passwall2/Makefile index 7afd2b027c..00c34b795d 100644 --- a/openwrt-passwall2/luci-app-passwall2/Makefile +++ b/openwrt-passwall2/luci-app-passwall2/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 -PKG_VERSION:=26.4.10 +PKG_VERSION:=26.4.20 PKG_RELEASE:=1 PKG_PO_VERSION:=$(PKG_VERSION) diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua index 6096845db6..103af7787b 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua @@ -268,8 +268,8 @@ o = s:taboption("DNS", Flag, "dns_redirect", translate("DNS Redirect"), translat o.default = "1" o.rmempty = false -local use_nft = m:get("@global_forwarding[0]", "use_nft") == "1" -local set_title = api.i18n.translate(use_nft and "Clear NFTSET" or "Clear IPSET") +local prefer_nft = m:get("@global_forwarding[0]", "prefer_nft") == "1" +local set_title = api.i18n.translate(prefer_nft and "Clear NFTSET" or "Clear IPSET") o = s:taboption("DNS", DummyValue, "clear_ipset", set_title, translate("Try this feature if the rule modification does not take effect.")) o.rawhtml = true function o.cfgvalue(self, section) diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua index 2304e7f134..0f2ea0fa4e 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua @@ -48,15 +48,23 @@ o.default = "" o:depends("console_auth", true) ---- Console Port -o = s:option(Value, "console_port", translate("Console Port"), translate( - "In the browser input routing IP plus port access, such as:192.168.1.1:1188")) -o.default = "1188" +o = s:option(Value, "console_port", translate("Console Port"), translate("In the browser input routing IP plus port access, such as:192.168.1.1:1188")) +o.datatype = "port" +o.default = 1188 o:depends("balancing_enable", true) o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) o.default = "0" o:depends("balancing_enable", true) +o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy")) +o:value("roundrobin", "roundRobin") +o:value("random", "random") +o:value("leastconn", "leastConn") +o:value("source", "source") +o.default = "roundrobin" +o:depends("balancing_enable", true) + ---- Health Check Type o = s:option(ListValue, "health_check_type", translate("Health Check Type")) o.default = "passwall_logic" @@ -77,9 +85,11 @@ o.description = translate("The URL used to detect the connection status.") o:depends("health_check_type", "passwall_logic") ---- Health Check Inter -o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds")) -o.default = "60" +o = s:option(Value, "health_check_inter", translate("Health Check Inter")) +o.datatype = "min(20)" +o.default = 60 o:depends("balancing_enable", true) +o.description = translate("Units:seconds") .. " ( ≥ 20s )" o = s:option(DummyValue, "health_check_tips", " ") o.rawhtml = true @@ -89,11 +99,10 @@ end o:depends("health_check_type", "passwall_logic") -- [[ Balancing Settings ]]-- -s = m:section(TypedSection, "haproxy_config", translate("Node List"), - "" .. - translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. - "\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. - "") +s = m:section(TypedSection, "haproxy_config", translate("Node List")) +s.description = "" .. + translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "
" .. + translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "
" s.template = "cbi/tblsection" s.sortable = true s.anonymous = true @@ -148,7 +157,7 @@ o.rmempty = false ---- Node Weight o = s:option(Value, "lbweight", translate("Node Weight")) -o.datatype = "uinteger" +o.datatype = "min(1)" o.default = 5 o.rmempty = false diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua index 68f507ad01..f3930ed4fa 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua @@ -8,7 +8,7 @@ if not arg[1] or not m:get(arg[1]) then luci.http.redirect(api.url()) end -m:append(Template(appname .. "/cbi/nodes_multivalue_com")) +m:append(Template(appname .. "/cbi/nodes_dynamiclist_com")) m:append(Template(appname .. "/cbi/nodes_listvalue_com")) local has_singbox = api.finded_com("sing-box") @@ -95,41 +95,28 @@ o.datatype = "min(1)" o.default = 1 o:depends("enable_autoswitch", true) -o = s:option(MultiValue, "autoswitch_backup_node", translate("List of backup nodes")) +o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes")) o:depends("enable_autoswitch", true) -o.widget = "checkbox" -o.template = appname .. "/cbi/nodes_multivalue" +o.template = appname .. "/cbi/nodes_dynamiclist" o.group = {} +o.write = function(self, section, value) + local n = s.fields["node"]:formvalue(section) + local v = type(value) == "table" and value or { value } + local t = {} + for _, x in ipairs(v) do + if x and x ~= n then t[#t+1] = x end + end + return DynamicList.write(self, section, t) +end for i, v in pairs(nodes_table) do - o:value(v.id, v.remark) - o.group[#o.group+1] = v.group or "" + if v.protocol ~= "_shunt" then + o:value(v.id, v.remark) + o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + s.fields["enable_autoswitch"]:depends({ node = v.id }) + end socks_node:value(v.id, v["remark"]) socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default") end --- Reading the old DynamicList -function o.cfgvalue(self, section) - return m.uci:get_list(appname, section, "autoswitch_backup_node") or {} -end --- Write-and-hold DynamicList -function o.write(self, section, value) - local old = m.uci:get_list(appname, section, "autoswitch_backup_node") or {} - local new, set = {}, {} - for v in value:gmatch("%S+") do - new[#new + 1] = v - set[v] = 1 - end - for _, v in ipairs(old) do - if not set[v] then - m.uci:set_list(appname, section, "autoswitch_backup_node", new) - return - end - set[v] = nil - end - for _ in pairs(set) do - m.uci:set_list(appname, section, "autoswitch_backup_node", new) - return - end -end o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node.")) o:depends("enable_autoswitch", true) diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua index ac2a7b3490..27b9f40a35 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua @@ -506,33 +506,6 @@ 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")] = "dns" }) -o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU")) -o.default = "1350" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_tti"), translate("KCP TTI")) -o.default = "20" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity")) -o.default = "5" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity")) -o.default = "20" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Flag, _n("mkcp_congestion"), translate("KCP Congestion")) -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_readBufferSize"), translate("KCP readBufferSize")) -o.default = "1" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_writeBufferSize"), translate("KCP writeBufferSize")) -o.default = "1" -o:depends({ [_n("transport")] = "mkcp" }) - o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed")) o:depends({ [_n("transport")] = "mkcp" }) @@ -645,6 +618,14 @@ end -- [[ User-Agent ]]-- o = s:option(Value, _n("user_agent"), translate("User-Agent")) +o.default = "" +o:value("", translate("default")) +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox") +o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") +o:value("Go-http-client/1.1", "golang") +o:value("curl/7.68.0", "curl") o:depends({ [_n("tcp_guise")] = "http" }) o:depends({ [_n("transport")] = "ws" }) o:depends({ [_n("transport")] = "httpupgrade" }) diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua index 42d39e8db4..f45ee300cd 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua @@ -416,8 +416,14 @@ if singbox_tags:find("with_quic") then end -- [[ SSH config start ]] -- -o = s:option(Value, _n("ssh_priv_key"), translate("Private Key")) +o = s:option(TextValue, _n("ssh_priv_key"), translate("Private Key")) +o.rows = 5 +o.wrap = "off" o:depends({ [_n("protocol")] = "ssh" }) +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") + return value +end o = s:option(Value, _n("ssh_priv_key_pp"), translate("Private Key Passphrase")) o.password = true @@ -509,11 +515,7 @@ o.rows = 5 o.wrap = "off" o:depends({ [_n("ech")] = true }) o.validate = function(self, value) - value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n") - value = value:gsub("^%s*\n", "") - if value:sub(-1) == "\n" then - value = value:sub(1, -2) - end + value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") return value end @@ -681,6 +683,14 @@ o:depends({ [_n("grpc_health_check")] = true }) -- [[ User-Agent ]]-- o = s:option(Value, _n("user_agent"), translate("User-Agent")) +o.default = "" +o:value("", translate("default")) +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox") +o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") +o:value("Go-http-client/1.1", "golang") +o:value("curl/7.68.0", "curl") o:depends({ [_n("tcp_guise")] = "http" }) o:depends({ [_n("transport")] = "http" }) o:depends({ [_n("transport")] = "ws" }) diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua index 17a441e31b..cb2aba71c7 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua @@ -356,33 +356,6 @@ 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")] = "dns" }) -o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU")) -o.default = "1350" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_tti"), translate("KCP TTI")) -o.default = "20" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity")) -o.default = "5" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity")) -o.default = "20" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Flag, _n("mkcp_congestion"), translate("KCP Congestion")) -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_readBufferSize"), translate("KCP readBufferSize")) -o.default = "1" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_writeBufferSize"), translate("KCP writeBufferSize")) -o.default = "1" -o:depends({ [_n("transport")] = "mkcp" }) - o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed")) o:depends({ [_n("transport")] = "mkcp" }) diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 7cc3894856..5ccfba8841 100644 --- a/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -217,13 +217,12 @@ function gen_outbound(flag, node, tag, proxy_table) } } or nil, kcpSettings = (node.transport == "mkcp") and { - mtu = tonumber(node.mkcp_mtu), - tti = tonumber(node.mkcp_tti), - uplinkCapacity = tonumber(node.mkcp_uplinkCapacity), - downlinkCapacity = tonumber(node.mkcp_downlinkCapacity), - congestion = (node.mkcp_congestion == "1") and true or false, - readBufferSize = tonumber(node.mkcp_readBufferSize), - writeBufferSize = tonumber(node.mkcp_writeBufferSize) + mtu = 1350, + tti = 50, + uplinkCapacity = 12, + downlinkCapacity = 100, + CwndMultiplier = 1, + MaxSendingWindow = 2 * 1024 * 1024 } or nil, wsSettings = (node.transport == "ws") and { path = node.ws_path or "/", @@ -675,13 +674,12 @@ function gen_config_server(node) } } or nil, kcpSettings = (node.transport == "mkcp") and { - mtu = tonumber(node.mkcp_mtu), - tti = tonumber(node.mkcp_tti), - uplinkCapacity = tonumber(node.mkcp_uplinkCapacity), - downlinkCapacity = tonumber(node.mkcp_downlinkCapacity), - congestion = (node.mkcp_congestion == "1") and true or false, - readBufferSize = tonumber(node.mkcp_readBufferSize), - writeBufferSize = tonumber(node.mkcp_writeBufferSize) + mtu = 1350, + tti = 50, + uplinkCapacity = 12, + downlinkCapacity = 100, + CwndMultiplier = 1, + MaxSendingWindow = 2 * 1024 * 1024 } or nil, wsSettings = (node.transport == "ws") and { host = node.ws_host or nil, diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist.htm b/openwrt-passwall2/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist.htm new file mode 100644 index 0000000000..f516acebe1 --- /dev/null +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist.htm @@ -0,0 +1,149 @@ +<%+cbi/valueheader%> +<% +-- Template Developers: +-- - lwb1978 +-- - snowie2000 +-- Copyright: copyright(c)2025–2027 +-- Description: Passwall(2) UI template + +local json = require "luci.jsonc" + +-- 读取值(keylist/vallist/group) +local values = {} +for i, key in pairs(self.keylist) do + values[#values + 1] = { + key = key, + label = self.vallist[i] or key, + group = self.group and self.group[i] or nil + } +end + +-- 获取当前配置值 +local current_key = {} +local cval = self:cfgvalue(section) +if type(cval) == "table" then + for _, v in ipairs(cval) do + for item in string.gmatch(v, "[^,]+") do + current_key[#current_key+1] = item + end + end +elseif type(cval) == "string" then + for item in string.gmatch(cval, "[^,]+") do + current_key[#current_key+1] = item + end +end + +-- 分离无组节点(ungrouped)与有组节点(grouped),并保持原有顺序 +local ungrouped = {} +local groups = {} +local group_order = {} +for _, item in ipairs(values) do + if not item.group or item.group == "" then + table.insert(ungrouped, item) + else + local g = item.group + if not groups[g] then + groups[g] = {} + table.insert(group_order, g) + end + table.insert(groups[g], item) + end +end + +-- 打包数据给 JS +local dropdown_data = { + current_key = current_key, + ungrouped = ungrouped, + groups = groups, + group_order = group_order, + cbid = cbid, +} +%> + + +
+ + + +
+ + +
+ + <%=translate("-- Please choose --")%> + + +
+ + +
+ + + +<%+cbi/valuefooter%> diff --git a/openwrt-passwall2/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist_com.htm b/openwrt-passwall2/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist_com.htm new file mode 100644 index 0000000000..4f932a2151 --- /dev/null +++ b/openwrt-passwall2/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist_com.htm @@ -0,0 +1,654 @@ +<% +-- Template Developers: +-- - lwb1978 +-- - snowie2000 +-- Copyright: copyright(c)2025–2027 +-- Description: Passwall(2) UI template +-- It is the common part of the template and cannot be used independently +%> + + + + diff --git a/openwrt-passwall2/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 b/openwrt-passwall2/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 index b47c6adb8f..069212254b 100644 --- a/openwrt-passwall2/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 +++ b/openwrt-passwall2/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 @@ -1,11 +1,23 @@ #!/bin/sh -[[ "$ACTION" == "ifup" && $(uci get "passwall2.@global[0].enabled") == "1" ]] && [ -f /var/lock/passwall2_ready.lock ] && { - default_device=$(ip route | grep default | awk -F 'dev ' '{print $2}' | awk '{print $1}') - [ "$default_device" == "$DEVICE" ] && { - LOCK_FILE_DIR=/var/lock - [ ! -d ${LOCK_FILE_DIR} ] && mkdir -p ${LOCK_FILE_DIR} - LOCK_FILE="${LOCK_FILE_DIR}/passwall2_ifup.lock" +[ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ] || exit 0 + +. /usr/share/passwall2/utils.sh + +([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && { + + default_device=$(ip route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1) + default6_device=$(ip -6 route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1) + ( [ "$default_device" = "$DEVICE" ] || [ "$default6_device" = "$DEVICE" ] ) && { + + if [ "$ACTION" = "ifupdate" ]; then + USE_TABLES=$(get_cache_var "USE_TABLES") + [ -n "$USE_TABLES" ] && ${APP_PATH}/${USE_TABLES}.sh update_wan_sets >/dev/null 2>&1 & + exit 0 + fi + + [ ! -d ${LOCK_PATH} ] && mkdir -p ${LOCK_PATH} + LOCK_FILE="${LOCK_PATH}/${CONFIG}_ifup.lock" if [ -s ${LOCK_FILE} ]; then SPID=$(cat ${LOCK_FILE}) if [ -e /proc/${SPID}/status ]; then @@ -15,8 +27,8 @@ fi echo $$ > ${LOCK_FILE} - /etc/init.d/passwall2 restart >/dev/null 2>&1 & - logger -p notice -t network -s "passwall2: restart when $INTERFACE ifup" + /etc/init.d/${CONFIG} restart >/dev/null 2>&1 & + logger -p notice -t network -s "${CONFIG}: restart when $INTERFACE ifup" rm -rf ${LOCK_FILE} } diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/app.sh b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/app.sh index bd1110eb8c..dbe5234acf 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/app.sh +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -933,8 +933,8 @@ start_haproxy() { [ "$(config_t_get global_haproxy balancing_enable 0)" != "1" ] && return haproxy_path=$TMP_PATH/haproxy haproxy_conf="config.cfg" - lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf} -dns ${LOCAL_DNS:-${AUTO_DNS}} - ln_run ${QUEUE_RUN} "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}" + lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf} + ln_run 0 "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}" } run_copy_dnsmasq() { diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua index 8c7ee3c486..1021568fba 100644 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua @@ -1,7 +1,7 @@ #!/usr/bin/lua local api = require ("luci.passwall2.api") -local appname = "passwall2" +local appname = api.appname local fs = api.fs local jsonc = api.jsonc local uci = api.uci @@ -18,11 +18,12 @@ end local var = api.get_args(arg) local haproxy_path = var["-path"] local haproxy_conf = var["-conf"] -local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53" +local haproxy_dns = "127.0.0.1" local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1" local health_check_type = uci:get(appname, "@global_haproxy[0]", "health_check_type") or "tcp" -local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "10" +local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "20" +local balancingStrategy = uci:get(appname, "@global_haproxy[0]", "balancingStrategy") or "roundrobin" local console_port = uci:get(appname, "@global_haproxy[0]", "console_port") local bind_local = uci:get(appname, "@global_haproxy[0]", "bind_local") or "0" local bind_address = "0.0.0.0" @@ -38,35 +39,37 @@ local f_out = io.open(haproxy_file, "a") local haproxy_config = [[ global daemon - log 127.0.0.1 local2 - maxconn 60000 - stats socket {{path}}/haproxy.sock + log 127.0.0.1 local2 + maxconn 60000 + stats socket {{path}}/haproxy.sock nbthread {{nbthread}} external-check insecure-fork-wanted + spread-checks 50 + tune.max-checks-per-thread 5 defaults - mode tcp - log global - option tcplog - option dontlognull + mode tcp + log global + option tcplog + option dontlognull option http-server-close - #option forwardfor except 127.0.0.0/8 - option redispatch - retries 2 - timeout http-request 10s - timeout queue 1m - timeout connect 10s - timeout client 1m - timeout server 1m + #option forwardfor except 127.0.0.0/8 + option redispatch + retries 2 + timeout http-request 10s + timeout queue 1m + timeout connect 10s + timeout client 1m + timeout server 1m timeout http-keep-alive 10s - timeout check 10s - maxconn 3000 - + timeout check 10s + maxconn 3000 + resolvers mydns - resolve_retries 1 - timeout resolve 5s - hold valid 600s + resolve_retries 1 + timeout resolve 5s + hold valid 600s {{dns}} ]] @@ -97,10 +100,12 @@ uci:foreach(appname, "haproxy_config", function(t) local lbss = t.lbss local listen_port = tonumber(t.haproxy_port) or 0 local server_node = uci:get_all(appname, lbss) - if server_node and server_node.address and server_node.port then - server_remark = server_node.address .. ":" .. server_node.port + local hop = (health_check_type == "passwall_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil + hop = hop and hop:gsub(":", "-") or nil + if server_node and server_node.address and (server_node.port or hop) then + server_remark = server_node.address .. ":" .. (server_node.port or hop) server_address = server_node.address - server_port = server_node.port + server_port = server_node.port or hop t.origin_address = server_address t.origin_port = server_port if health_check_type == "passwall_logic" then @@ -143,6 +148,10 @@ uci:foreach(appname, "haproxy_config", function(t) end end) +local inter = tonumber((health_check_inter or "20"):match("^%d+")) +inter = (inter and inter >= 20) and inter or 20 +local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s" + local sortTable = {} for i in pairs(listens) do if i ~= nil then @@ -158,14 +167,14 @@ for i, port in pairs(sortTable) do listen %s bind %s:%s mode tcp - balance roundrobin -]], port, bind_address, port)) + balance %s +]], port, bind_address, port, balancingStrategy)) if health_check_type == "passwall_logic" then f_out:write(string.format([[ option external-check - external-check command "/usr/share/passwall2/haproxy_check.sh" -]], port, port)) + external-check command "/usr/share/%s/haproxy_check.sh" +]], appname)) end local count_M, count_B = 1, 1 @@ -180,7 +189,7 @@ listen %s count_B = count_B + 1 end local server = o.server_address .. ":" .. o.server_port - local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check inter {{inter}} rise 1 fall 3 {{backup}}" + local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}" server_conf = server_conf:gsub("{{remark}}", remark) server_conf = server_conf:gsub("{{server}}", server) server_conf = server_conf:gsub("{{weight}}", o.lbweight) @@ -189,13 +198,13 @@ listen %s resolvers = "" end server_conf = server_conf:gsub("{{resolvers}}", resolvers) - server_conf = server_conf:gsub("{{inter}}", tonumber(health_check_inter) .. "s") + server_conf = server_conf:gsub("{{inter_arg}}", inter_arg) server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "") - f_out:write(" " .. server_conf .. "\n") + f_out:write(" " .. api.trim(server_conf) .. "\n") if o.export ~= "0" then - sys.call(string.format(". /usr/share/passwall2/utils.sh ; add_ip2route %s %s", o.origin_address, o.export)) + sys.call(string.format(". /usr/share/%s/utils.sh ; add_ip2route %s %s", appname, o.origin_address, o.export)) end log(2, string.format(api.i18n.translatef("Node: %s:%s, Weight: %s", o.origin_address, o.origin_port, o.lbweight))) diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh index 0bf0013eb8..028484f906 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh @@ -24,7 +24,7 @@ if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then extra_params="${extra_params} --retry-all-errors" fi -status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 2 --max-time 10 -w "%{http_code}" "${probeUrl}") +status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}") case "$status" in 200|204) diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/iptables.sh b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/iptables.sh index cdc390c29f..509eff2185 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/iptables.sh +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/iptables.sh @@ -617,6 +617,24 @@ filter_direct_node_list() { done } +update_wan_sets() { + local WAN_IP=$(get_wan_ips ip4) + [ -n "$WAN_IP" ] && { + ipset -F "$IPSET_WAN" + for wan_ip in $WAN_IP; do + ipset -! add "$IPSET_WAN" "$wan_ip" + done + } + + local WAN6_IP=$(get_wan_ips ip6) + [ -n "$WAN6_IP" ] && { + ipset -F "$IPSET_WAN6" + for wan6_ip in $WAN6_IP; do + ipset -! add "$IPSET_WAN6" "$wan6_ip" + done + } +} + add_firewall_rule() { log_i18n 0 "Starting to load %s firewall rules..." "iptables" @@ -665,6 +683,8 @@ add_firewall_rule() { EOF } + update_wan_sets + [ -n "$ISP_DNS" ] && { for ispip in $ISP_DNS; do ipset -! add $IPSET_LAN $ispip @@ -716,6 +736,7 @@ add_firewall_rule() { $ipt_n -N PSW2 $ipt_n -A PSW2 $(dst $IPSET_LAN) -j RETURN $ipt_n -A PSW2 $(dst $IPSET_VPS) -j RETURN + $ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN [ "$accept_icmp" = "1" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW2" [ -z "${is_tproxy}" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW2" @@ -747,19 +768,9 @@ add_firewall_rule() { $ipt_m -N PSW2 $ipt_m -A PSW2 $(dst $IPSET_LAN) -j RETURN $ipt_m -A PSW2 $(dst $IPSET_VPS) -j RETURN + $ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN $ipt_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN - WAN_IP=$(get_wan_ips ip4) - [ -n "${WAN_IP}" ] && { - ipset -F $IPSET_WAN - for wan_ip in $WAN_IP; do - ipset -! add $IPSET_WAN ${wan_ip} - done - $ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN - $ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN - } - unset WAN_IP wan_ip - insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2" # Only TCP, UDP Invalid. insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT" @@ -807,17 +818,8 @@ add_firewall_rule() { $ip6t_m -N PSW2 $ip6t_m -A PSW2 $(dst $IPSET_LAN6) -j RETURN $ip6t_m -A PSW2 $(dst $IPSET_VPS6) -j RETURN + $ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN $ip6t_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN - - WAN6_IP=$(get_wan_ips ip6) - [ -n "${WAN6_IP}" ] && { - ipset -F $IPSET_WAN6 - for wan6_ip in $WAN6_IP; do - ipset -! add $IPSET_WAN6 ${wan6_ip} - done - $ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN - } - unset WAN6_IP wan6_ip insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2" # Only TCP, UDP Invalid. @@ -835,7 +837,7 @@ add_firewall_rule() { local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}') if [[ "$dns_address" == *::* ]]; then $ip6t_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN - log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "ip6tables" "${dns_address}:${dns_port:-53}")" + log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "ip6tables" "[${dns_address}]:${dns_port:-53}")" else $ipt_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "iptables" "${dns_address}:${dns_port:-53}")" @@ -1050,7 +1052,7 @@ gen_include() { local __ipt="" [ -n "${ipt}" ] && { __ipt=$(cat <<- EOF - . $UTILS_PATH + ${MY_PATH} update_wan_sets $ipt-save -c | grep -v "PSW2" | $ipt-restore -c $ipt-restore -n <<-EOT $(extract_rules 4 nat) @@ -1062,21 +1064,13 @@ gen_include() { \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2") \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT") - - WAN_IP=\$(get_wan_ips ip4) - [ ! -z "\${WAN_IP}" ] && { - ipset -F $IPSET_WAN - for wan_ip in \$WAN_IP; do - ipset -! add $IPSET_WAN \${wan_ip} - done - } EOF ) } local __ip6t="" [ -n "${ip6t}" ] && { __ip6t=$(cat <<- EOF - . $UTILS_PATH + ${MY_PATH} update_wan_sets $ip6t-save -c | grep -v "PSW2" | $ip6t-restore -c $ip6t-restore -n <<-EOT $(extract_rules 6 nat) @@ -1087,20 +1081,12 @@ gen_include() { \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2") \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT") - - WAN6_IP=\$(get_wan_ips ip6) - [ ! -z "\${WAN6_IP}" ] && { - ipset -F $IPSET_WAN6 - for wan6_ip in \$WAN6_IP; do - ipset -! add $IPSET_WAN6 \${wan6_ip} - done - } EOF ) } cat <<-EOF >> $FWI ${__ipt} - + ${__ip6t} return 0 @@ -1158,6 +1144,9 @@ get_ip6t_bin) filter_direct_node_list) filter_direct_node_list ;; +update_wan_sets) + update_wan_sets "$@" + ;; stop) stop ;; diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/nftables.sh b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/nftables.sh index d4fd27be24..622dbeb831 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/nftables.sh +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/nftables.sh @@ -680,6 +680,20 @@ mwan3_start() { nft list chain ip mangle mwan3_hook >/dev/null 2>&1 && nft insert rule ip mangle mwan3_hook ct mark ${FWMARK} counter return >/dev/null 2>&1 } +update_wan_sets() { + local WAN_IP=$(get_wan_ips ip4) + [ -n "$WAN_IP" ] && { + nft flush set $NFTABLE_NAME $NFTSET_WAN + echo "$WAN_IP" | insert_nftset $NFTSET_WAN "-1" + } + + local WAN6_IP=$(get_wan_ips ip6) + [ -n "${WAN6_IP}" ] && { + nft flush set $NFTABLE_NAME $NFTSET_WAN6 + echo "$WAN6_IP" | insert_nftset $NFTSET_WAN6 "-1" + } +} + add_firewall_rule() { log_i18n 0 "Starting to load %s firewall rules..." "nftables" gen_nft_tables @@ -714,6 +728,8 @@ add_firewall_rule() { [ -n "$lan_ip6" ] && echo $lan_ip6 | insert_nftset $NFTSET_LAN6 "-1" } + update_wan_sets + [ -n "$ISP_DNS" ] && { echo "$ISP_DNS" | insert_nftset $NFTSET_LAN 0 for ispip in $ISP_DNS; do @@ -835,14 +851,9 @@ add_firewall_rule() { nft "add rule $NFTABLE_NAME nat_output meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT" fi - WAN_IP=$(get_wan_ips ip4) - [ -n "${WAN_IP}" ] && { - nft flush set $NFTABLE_NAME $NFTSET_WAN - 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\"" - } - unset WAN_IP + #ipv4 wan_ip + [ -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\"" ip rule add fwmark ${FWMARK} table 999 priority 999 ip route add local 0.0.0.0/0 dev lo table 999 @@ -867,10 +878,11 @@ add_firewall_rule() { local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}') if [[ "$dns_address" == *::* ]]; then nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return" + log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "[${dns_address}]:${dns_port:-53}")" else nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return" + log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")" fi - log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")" done } @@ -879,13 +891,7 @@ add_firewall_rule() { nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto {ipv6} counter jump PSW2_MANGLE_V6" nft "add rule $NFTABLE_NAME mangle_output meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\"" - WAN6_IP=$(get_wan_ips ip6) - [ -n "${WAN6_IP}" ] && { - nft flush set $NFTABLE_NAME $NFTSET_WAN6 - 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 + nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\"" ip -6 rule add fwmark ${FWMARK} table 999 priority 999 ip -6 route add local ::/0 dev lo table 999 @@ -894,7 +900,7 @@ add_firewall_rule() { [ "$ENABLED_DEFAULT_ACL" == 1 ] && { TCP_LOCALHOST_PROXY=$LOCALHOST_PROXY UDP_LOCALHOST_PROXY=$LOCALHOST_PROXY - + msg="$(i18n "[Local],")" [ "$TCP_NO_REDIR_PORTS" != "disable" ] && { nft "add rule $NFTABLE_NAME $nft_output_chain ip protocol tcp $(factor $TCP_NO_REDIR_PORTS "tcp dport") counter return" @@ -906,7 +912,7 @@ add_firewall_rule() { log 1 "${msg}$(i18n "not proxy all %s" "TCP")" fi } - + [ "$UDP_NO_REDIR_PORTS" != "disable" ] && { nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return" @@ -1088,20 +1094,9 @@ gen_include() { local __nft=" " __nft=$(cat <<- EOF - . $UTILS_PATH [ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW2)" ] && nft -f ${nft_chain_file} - WAN_IP=\$(get_wan_ips ip4) - [ ! -z "\${WAN_IP}" ] && { - nft flush set $NFTABLE_NAME $NFTSET_WAN - echo "\${WAN_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1" - } - [ "$PROXY_IPV6" == "1" ] && { - WAN6_IP=\$(get_wan_ips ip6) - [ ! -z "\${WAN6_IP}" ] && { - nft flush set $NFTABLE_NAME $NFTSET_WAN6 - echo "\${WAN6_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1" - } - } + + ${MY_PATH} update_wan_sets EOF ) @@ -1150,6 +1145,9 @@ mwan3_start) mwan3_stop) mwan3_stop ;; +update_wan_sets) + update_wan_sets "$@" + ;; stop) stop ;; diff --git a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua index c1ada5881f..90d15f0c47 100755 --- a/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua +++ b/openwrt-passwall2/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -31,59 +31,60 @@ local has_ssr = api.is_finded("ssr-local") and api.is_finded("ssr-redir") local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") -local allowInsecure_default = true +local DEFAULT_ALLOWINSECURE = true +local DEFAULT_FILTER_KEYWORD_MODE = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" +local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} +local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} -- Nodes should be retrieved using the core type (if not set on the node subscription page, the default type will be used automatically). -local ss_type_default = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}}) -local trojan_type_default = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) -local vmess_type_default = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) -local vless_type_default = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) -local hysteria2_type_default = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) +local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) +local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}}) local core_has = { - ["xray"] = has_xray, ["sing-box"] = has_singbox, ["shadowsocks-libev"] = has_ss,["shadowsocks-rust"] = has_ss_rust, + ["xray"] = has_xray, + ["sing-box"] = has_singbox, + ["shadowsocks-libev"] = has_ss, + ["shadowsocks-rust"] = has_ss_rust, ["hysteria2"] = has_hysteria2 } -local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy -local preproxy_node_group, to_node_group, chain_node_type = "", "", "" -- Determine whether to filter node keywords -local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" -local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} -local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} -local function is_filter_keyword(value) - if filter_keyword_mode_default == "1" then - for k,v in ipairs(filter_keyword_discard_list_default) do +local function is_filter_keyword(mode, discard_list, keep_list, value) + if mode == "1" then + for k,v in ipairs(discard_list) do if value:find(v, 1, true) then return true end end - elseif filter_keyword_mode_default == "2" then + elseif mode == "2" then local result = true - for k,v in ipairs(filter_keyword_keep_list_default) do + for k,v in ipairs(keep_list) do if value:find(v, 1, true) then result = false end end return result - elseif filter_keyword_mode_default == "3" then + elseif mode == "3" then local result = false - for k,v in ipairs(filter_keyword_discard_list_default) do + for k,v in ipairs(discard_list) do if value:find(v, 1, true) then result = true end end - for k,v in ipairs(filter_keyword_keep_list_default) do + for k,v in ipairs(keep_list) do if value:find(v, 1, true) then result = false end end return result - elseif filter_keyword_mode_default == "4" then + elseif mode == "4" then local result = true - for k,v in ipairs(filter_keyword_keep_list_default) do + for k,v in ipairs(keep_list) do if value:find(v, 1, true) then result = false end end - for k,v in ipairs(filter_keyword_discard_list_default) do + for k,v in ipairs(discard_list) do if value:find(v, 1, true) then result = true end @@ -425,16 +426,16 @@ local function get_subscribe_info(cfgid, value) end -- Configure the SS protocol implementation type -local function set_ss_implementation(result) - if ss_type_default == "shadowsocks-libev" and has_ss then +local function set_ss_implementation(ss_type, result) + if ss_type == "shadowsocks-libev" and has_ss then result.type = "SS" - elseif ss_type_default == "shadowsocks-rust" and has_ss_rust then + elseif ss_type == "shadowsocks-rust" and has_ss_rust then result.type = 'SS-Rust' - elseif ss_type_default == "xray" and has_xray then + elseif ss_type == "xray" and has_xray then result.type = 'Xray' result.protocol = 'shadowsocks' result.transport = 'raw' - elseif ss_type_default == "sing-box" and has_singbox then + elseif ss_type == "sing-box" and has_singbox then result.type = 'sing-box' result.protocol = 'shadowsocks' else @@ -445,8 +446,60 @@ local function set_ss_implementation(result) end -- Processing data -local function processData(szType, content, add_mode, group) +local function processData(szType, content, add_mode, group, sub_cfg) --log(2, content, add_mode, group) + local default_allowinsecure = DEFAULT_ALLOWINSECURE + local default_filter_keyword_mode = DEFAULT_FILTER_KEYWORD_MODE + local default_filter_keyword_discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST + local default_filter_keyword_keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST + local default_ss_type = DEFAULT_SS_TYPE + local default_trojan_type = DEFAULT_TROJAN_TYPE + local default_vmess_type = DEFAULT_VMESS_TYPE + local default_vless_type = DEFAULT_VLESS_TYPE + local default_hysteria2_type = DEFAULT_HYSTERIA2_TYPE + if sub_cfg then + if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then + default_allowinsecure = nil + end + local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global + if filter_keyword_mode == "0" then + default_filter_keyword_mode = "0" + elseif filter_keyword_mode == "1" then + default_filter_keyword_mode = "1" + default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {} + elseif filter_keyword_mode == "2" then + default_filter_keyword_mode = "2" + default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {} + elseif filter_keyword_mode == "3" then + default_filter_keyword_mode = "3" + default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {} + default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {} + elseif filter_keyword_mode == "4" then + default_filter_keyword_mode = "4" + default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {} + default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {} + end + local ss_type = sub_cfg.ss_type or "global" + if ss_type ~= "global" and core_has[ss_type] then + default_ss_type = ss_type + end + local trojan_type = sub_cfg.trojan_type or "global" + if trojan_type ~= "global" and core_has[trojan_type] then + default_trojan_type = trojan_type + end + local vmess_type = sub_cfg.vmess_type or "global" + if vmess_type ~= "global" and core_has[vmess_type] then + default_vmess_type = vmess_type + end + local vless_type = sub_cfg.vless_type or "global" + if vless_type ~= "global" and core_has[vless_type] then + default_vless_type = vless_type + end + local hysteria2_type = sub_cfg.hysteria2_type or "global" + if hysteria2_type ~= "global" and core_has[hysteria2_type] then + default_hysteria2_type = hysteria2_type + end + end local result = { timeout = 60, add_mode = add_mode, -- `0` for manual configuration, `1` for import, `2` for subscription @@ -486,9 +539,9 @@ local function processData(szType, content, add_mode, group) result.remarks = base64Decode(params.remarks) elseif szType == 'vmess' then local info = jsonParse(content) - if vmess_type_default == "sing-box" and has_singbox then + if default_vmess_type == "sing-box" and has_singbox then result.type = 'sing-box' - elseif vmess_type_default == "xray" and has_xray then + elseif default_vmess_type == "xray" and has_xray then result.type = "Xray" else log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VMess", "VMess")) @@ -559,12 +612,7 @@ local function processData(szType, content, add_mode, group) if info.net == 'kcp' or info.net == 'mkcp' then info.net = "mkcp" result.mkcp_guise = info.type - result.mkcp_mtu = 1350 - result.mkcp_tti = 50 - result.mkcp_uplinkCapacity = 5 - result.mkcp_downlinkCapacity = 20 - result.mkcp_readBufferSize = 2 - result.mkcp_writeBufferSize = 2 + result.mkcp_seed = info.seed end if info.net == 'quic' then result.quic_guise = info.type @@ -599,7 +647,7 @@ local function processData(szType, content, add_mode, group) result.tls_CertSha = info.pcs result.tls_CertByName = info.vcn local insecure = info.allowinsecure or info.allowInsecure or info.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") else result.tls = "0" end @@ -615,7 +663,7 @@ local function processData(szType, content, add_mode, group) return nil end elseif szType == "ss" then - result = set_ss_implementation(result) + result = set_ss_implementation(default_ss_type, result) if not result then return nil end --SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ] @@ -825,12 +873,6 @@ local function processData(szType, content, add_mode, group) if params.type == 'kcp' or params.type == 'mkcp' then result.transport = "mkcp" result.mkcp_guise = params.headerType or "none" - result.mkcp_mtu = 1350 - result.mkcp_tti = 50 - result.mkcp_uplinkCapacity = 5 - result.mkcp_downlinkCapacity = 20 - result.mkcp_readBufferSize = 2 - result.mkcp_writeBufferSize = 2 result.mkcp_seed = params.seed end if params.type == 'quic' then @@ -884,7 +926,7 @@ local function processData(szType, content, add_mode, group) end end local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") 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.") @@ -893,7 +935,7 @@ local function processData(szType, content, add_mode, group) if params["shadow-tls"] then if result.type ~= "sing-box" and result.type ~= "SS-Rust" then - result.error_msg = ss_type_default .. " " .. i18n.translatef("unsupport %s plugin.", "shadow-tls") + result.error_msg = default_ss_type .. " " .. i18n.translatef("unsupport %s plugin.", "shadow-tls") else -- Parsing SS Shadow-TLS plugin parameters local function parseShadowTLSParams(b64str, out) @@ -935,10 +977,10 @@ local function processData(szType, content, add_mode, group) end end elseif szType == "trojan" then - if trojan_type_default == "sing-box" and has_singbox then + if default_trojan_type == "sing-box" and has_singbox then result.type = 'sing-box' result.protocol = 'trojan' - elseif trojan_type_default == "xray" and has_xray then + elseif default_trojan_type == "xray" and has_xray then result.type = 'Xray' result.protocol = 'trojan' else @@ -988,7 +1030,7 @@ local function processData(szType, content, add_mode, group) result.tls_CertSha = params.pcs result.tls_CertByName = params.vcn local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") if not params.type then params.type = "tcp" end params.type = string.lower(params.type) @@ -1042,12 +1084,6 @@ local function processData(szType, content, add_mode, group) if params.type == 'kcp' or params.type == 'mkcp' then result.transport = "mkcp" result.mkcp_guise = params.headerType or "none" - result.mkcp_mtu = 1350 - result.mkcp_tti = 50 - result.mkcp_uplinkCapacity = 5 - result.mkcp_downlinkCapacity = 20 - result.mkcp_readBufferSize = 2 - result.mkcp_writeBufferSize = 2 result.mkcp_seed = params.seed end if params.type == 'quic' then @@ -1079,7 +1115,7 @@ local function processData(szType, content, add_mode, group) end end elseif szType == "ssd" then - result = set_ss_implementation(result) + result = set_ss_implementation(default_ss_type, result) if not result then return nil end result.address = content.server result.port = content.port @@ -1090,9 +1126,9 @@ local function processData(szType, content, add_mode, group) result.group = content.airport result.remarks = content.remarks elseif szType == "vless" then - if vless_type_default == "sing-box" and has_singbox then + if default_vless_type == "sing-box" and has_singbox then result.type = 'sing-box' - elseif vless_type_default == "xray" and has_xray then + elseif default_vless_type == "xray" and has_xray then result.type = "Xray" else log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VLESS", "VLESS")) @@ -1189,12 +1225,7 @@ local function processData(szType, content, add_mode, group) if params.type == 'kcp' or params.type == 'mkcp' then result.transport = "mkcp" result.mkcp_guise = params.headerType or "none" - result.mkcp_mtu = 1350 - result.mkcp_tti = 50 - result.mkcp_uplinkCapacity = 5 - result.mkcp_downlinkCapacity = 20 - result.mkcp_readBufferSize = 2 - result.mkcp_writeBufferSize = 2 + result.mkcp_seed = params.seed end if params.type == 'quic' then result.quic_guise = params.headerType or "none" @@ -1250,7 +1281,7 @@ local function processData(szType, content, add_mode, group) result.reality_mldsa65Verify = params.pqv or nil end local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") end result.port = port @@ -1307,7 +1338,7 @@ local function processData(szType, content, add_mode, group) result.hysteria_auth_password = params.auth result.tls_serverName = params.peer or params.sni or "" local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") result.alpn = params.alpn result.hysteria_up_mbps = params.upmbps result.hysteria_down_mbps = params.downmbps @@ -1353,12 +1384,12 @@ local function processData(szType, content, add_mode, group) result.tls_CertSha = params.pcs result.tls_CertByName = params.vcn local insecure = params.allowinsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") result.hysteria2_tls_pinSHA256 = params.pinSHA256 result.hysteria2_hop = params.mport - if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then - local is_singbox = hysteria2_type_default == "sing-box" and has_singbox + if (default_hysteria2_type == "sing-box" and has_singbox) or (default_hysteria2_type == "xray" and has_xray) then + local is_singbox = default_hysteria2_type == "sing-box" and has_singbox result.type = is_singbox and 'sing-box' or 'Xray' result.protocol = "hysteria2" if params["obfs-password"] or params["obfs_password"] then @@ -1433,7 +1464,7 @@ local function processData(szType, content, add_mode, group) result.tuic_congestion_control = params.congestion_control or "cubic" result.tuic_udp_relay_mode = params.udp_relay_mode or "native" local insecure = params.allowinsecure or params.insecure or params.allow_insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") elseif szType == "anytls" then if has_singbox then result.type = 'sing-box' @@ -1497,7 +1528,7 @@ local function processData(szType, content, add_mode, group) end result.port = port local insecure = params.allowinsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") end elseif szType == 'naive+https' or szType == 'naive+quic' then if has_singbox then @@ -1840,6 +1871,29 @@ local function update_node(manual) for _, v in ipairs(nodeResult) do local remark = v["remark"] local list = v["list"] + local sub_cfg = v["sub_cfg"] + local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy + local preproxy_node_group, to_node_group, chain_node_type = "", "", "" + -- Subscription Group Chain Agent + local function valid_chain_node(node) + if not node then return "" end + local cp = uci:get(appname, node, "chain_proxy") or "" + local am = uci:get(appname, node, "add_mode") or "0" + chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or "" + if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then + chain_node_type = "" + return "" + end + return node + end + if sub_cfg then + domain_resolver = sub_cfg.domain_resolver + domain_resolver_dns = sub_cfg.domain_resolver_dns + domain_resolver_dns_https = sub_cfg.domain_resolver_dns_https + domain_strategy = (sub_cfg.domain_strategy == "UseIPv4" or sub_cfg.domain_strategy == "UseIPv6") and sub_cfg.domain_strategy or nil + preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or "" + to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or "" + end for _, vv in ipairs(list) do local cfgid = uci:section(appname, "nodes", api.gen_short_uuid()) for kkk, vvv in pairs(vv) do @@ -1929,8 +1983,12 @@ local function update_node(manual) end end -local function parse_link(raw, add_mode, group, cfgid) +local function parse_link(raw, add_mode, group, sub_cfg) if raw and #raw > 0 then + local cfgid + if sub_cfg then + cfgid = sub_cfg[".name"] + end local nodes, szType local node_list = {} -- ssd appear to be in this format, starting with ssd://. @@ -1965,17 +2023,17 @@ local function parse_link(raw, add_mode, group, cfgid) xpcall(function () local result if szType == 'ssd' then - result = processData(szType, v, add_mode, group) + result = processData(szType, v, add_mode, group, sub_cfg) elseif not szType then local node = api.trim(v) local dat = split(node, "://") if dat and dat[1] and dat[2] then if dat[1] == 'vmess' or dat[1] == 'ssr' then local link = api.trim(dat[2]:gsub("#.*$", "")) - result = processData(dat[1], base64Decode(link), add_mode, group) + result = processData(dat[1], base64Decode(link), add_mode, group, sub_cfg) else local link = dat[2]:gsub("&", "&"):gsub("%s*#%s*", "#") -- Some odd links use "&" as "&", and include spaces before and after "#". - result = processData(dat[1], link, add_mode, group) + result = processData(dat[1], link, add_mode, group, sub_cfg) end end else @@ -1987,7 +2045,7 @@ local function parse_link(raw, add_mode, group, cfgid) log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. result.error_msg) elseif not result.type then log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. i18n.translatef("No usable binary was found.")) - elseif (add_mode == "2" and is_filter_keyword(result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or + elseif (add_mode == "2" and is_filter_keyword(default_filter_keyword_mode, default_filter_keyword_discard_list, default_filter_keyword_keep_list, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or (not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then log(2, i18n.translatef("Discard filter nodes: %s type node %s", result.type, result.remarks)) else @@ -2007,7 +2065,8 @@ local function parse_link(raw, add_mode, group, cfgid) if #node_list > 0 then nodeResult[#nodeResult + 1] = { remark = group, - list = node_list + list = node_list, + sub_cfg = sub_cfg } end log(2, i18n.translatef("Successfully resolved the [%s] node, number: %s", group, #node_list)) @@ -2038,77 +2097,26 @@ local execute = function() local cfgid = value[".name"] local remark = value.remark or "" local url = value.url or "" - if value.allowInsecure and value.allowInsecure ~= "1" then - allowInsecure_default = nil - end - local filter_keyword_mode = value.filter_keyword_mode or "5" - if filter_keyword_mode == "0" then - filter_keyword_mode_default = "0" - elseif filter_keyword_mode == "1" then - filter_keyword_mode_default = "1" - filter_keyword_discard_list_default = value.filter_discard_list or {} - elseif filter_keyword_mode == "2" then - filter_keyword_mode_default = "2" - filter_keyword_keep_list_default = value.filter_keep_list or {} - elseif filter_keyword_mode == "3" then - filter_keyword_mode_default = "3" - filter_keyword_keep_list_default = value.filter_keep_list or {} - filter_keyword_discard_list_default = value.filter_discard_list or {} - elseif filter_keyword_mode == "4" then - filter_keyword_mode_default = "4" - filter_keyword_keep_list_default = value.filter_keep_list or {} - filter_keyword_discard_list_default = value.filter_discard_list or {} - end - local ss_type = value.ss_type or "global" - if ss_type ~= "global" and core_has[ss_type] then - ss_type_default = ss_type - end - local trojan_type = value.trojan_type or "global" - if trojan_type ~= "global" and core_has[trojan_type] then - trojan_type_default = trojan_type - end - local vmess_type = value.vmess_type or "global" - if vmess_type ~= "global" and core_has[vmess_type] then - vmess_type_default = vmess_type - end - local vless_type = value.vless_type or "global" - if vless_type ~= "global" and core_has[vless_type] then - vless_type_default = vless_type - end - local hysteria2_type = value.hysteria2_type or "global" - if hysteria2_type ~= "global" and core_has[hysteria2_type] then - hysteria2_type_default = hysteria2_type - end - domain_resolver = value.domain_resolver - domain_resolver_dns = value.domain_resolver_dns - domain_resolver_dns_https = value.domain_resolver_dns_https - domain_strategy = (value.domain_strategy == "UseIPv4" or value.domain_strategy == "UseIPv6") and value.domain_strategy or nil - - -- Subscription Group Chain Agent - local function valid_chain_node(node) - if not node then return "" end - local cp = uci:get(appname, node, "chain_proxy") or "" - local am = uci:get(appname, node, "add_mode") or "0" - chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or "" - if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then - chain_node_type = "" - return "" - end - return node - end - preproxy_node_group = (value.chain_proxy == "1") and valid_chain_node(value.preproxy_node) or "" - to_node_group = (value.chain_proxy == "2") and valid_chain_node(value.to_node) or "" - - local ua = value.user_agent - local access_mode = value.access_mode - local result = (not access_mode) and i18n.translatef("Auto") or (access_mode == "direct" and i18n.translatef("Direct") or (access_mode == "proxy" and i18n.translatef("Proxy") or i18n.translatef("Auto"))) - log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url .. ' [' .. result .. ']')) - local tmp_file = "/tmp/" .. cfgid - value.http_code = curl(url, tmp_file, ua, access_mode) - if value.http_code ~= 200 then - fail_list[#fail_list + 1] = value + local url_is_local + if fs.access(url) then + -- debug, reads local files. + log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url)) + url_is_local = true + tmp_file = url else + local ua = value.user_agent + local access_mode = value.access_mode + local result = (not access_mode) and i18n.translatef("Auto") or (access_mode == "direct" and i18n.translatef("Direct") or (access_mode == "proxy" and i18n.translatef("Proxy") or i18n.translatef("Auto"))) + log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url .. ' [' .. result .. ']')) + tmp_file = "/tmp/" .. cfgid + value.http_code = curl(url, tmp_file, ua, access_mode) + if value.http_code ~= 200 then + fail_list[#fail_list + 1] = value + luci.sys.call("rm -f " .. tmp_file) + end + end + if fs.access(tmp_file) then if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then local f = io.open(tmp_file, "r") local stdout = f:read("*all") @@ -2119,33 +2127,18 @@ local execute = function() if not manual_sub and old_md5 == new_md5 then log(1, i18n.translatef("Subscription: [%s] No changes, no update required.", remark)) else - parse_link(raw_data, "2", remark, cfgid) + parse_link(raw_data, "2", remark, value) uci:set(appname, cfgid, "md5", new_md5) end else fail_list[#fail_list + 1] = value end + if url_is_local then + value.http_code = 0 + else + luci.sys.call("rm -f " .. tmp_file) + end end - allowInsecure_default = true - luci.sys.call("rm -f " .. tmp_file) - filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" - filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} - filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} - - ss_type = uci:get(appname, "@global_subscribe[0]", "ss_type") or "" - ss_type_default = core_has[ss_type] and ss_type or ss_type_default - - trojan_type = uci:get(appname, "@global_subscribe[0]", "trojan_type") or "" - trojan_type_default = core_has[trojan_type] and trojan_type or trojan_type_default - - vmess_type = uci:get(appname, "@global_subscribe[0]", "vmess_type") or "" - vmess_type_default = core_has[vmess_type] and vmess_type or vmess_type_default - - vless_type = uci:get(appname, "@global_subscribe[0]", "vless_type") or "" - vless_type_default = core_has[vless_type] and vless_type or vless_type_default - - hysteria2_type = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or "" - hysteria2_type_default = core_has[hysteria2_type] and hysteria2_type or hysteria2_type_default end if #fail_list > 0 then diff --git a/shadowsocks-rust/Cargo.lock b/shadowsocks-rust/Cargo.lock index 73947b18f8..5825cc0826 100644 --- a/shadowsocks-rust/Cargo.lock +++ b/shadowsocks-rust/Cargo.lock @@ -378,7 +378,7 @@ dependencies = [ "hex", "indexmap", "js-sys", - "rand 0.9.2", + "rand 0.9.4", "serde", "serde_bytes", "simdutf8", @@ -585,9 +585,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] @@ -883,7 +883,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -996,7 +996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1389,7 +1389,7 @@ dependencies = [ "once_cell", "pin-project-lite", "quinn", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustls", "serde", @@ -1416,7 +1416,7 @@ dependencies = [ "once_cell", "parking_lot", "quinn", - "rand 0.9.2", + "rand 0.9.4", "resolv-conf", "rustls", "serde", @@ -2045,7 +2045,7 @@ dependencies = [ "log-mdc", "mock_instant", "parking_lot", - "rand 0.9.2", + "rand 0.9.4", "serde", "serde-value", "serde_json", @@ -2245,7 +2245,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2597,7 +2597,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -2620,7 +2620,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -2646,9 +2646,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core 0.9.3", @@ -2897,7 +2897,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2956,7 +2956,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2967,9 +2967,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "aws-lc-rs", "ring", @@ -3235,7 +3235,7 @@ dependencies = [ "ghash", "hkdf", "md-5", - "rand 0.9.2", + "rand 0.9.4", "ring-compat", "sha1", "sm4", @@ -3603,7 +3603,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/shadowsocks-rust/crates/shadowsocks-service/src/local/net/tcp/outbound_proxy.rs b/shadowsocks-rust/crates/shadowsocks-service/src/local/net/tcp/outbound_proxy.rs index 9cad8f73bd..c881e0f944 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/src/local/net/tcp/outbound_proxy.rs +++ b/shadowsocks-rust/crates/shadowsocks-service/src/local/net/tcp/outbound_proxy.rs @@ -11,20 +11,13 @@ use std::{ task::{self, Poll}, }; -use shadowsocks::{ - net::ConnectOpts, - relay::socks5::{Address, Command}, -}; +use shadowsocks::{net::ConnectOpts, relay::socks5::Address}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use crate::{ config::{OutboundProxy, OutboundProxyProtocol}, local::context::ServiceContext, - net::{ - http_stream::ProxyHttpStream, - outbound_proxy::connect_via_proxy, - socks5_client::{socks5_command, socks5_handshake}, - }, + net::{http_stream::ProxyHttpStream, outbound_proxy::connect_via_proxy}, }; use super::auto_proxy_stream::AutoProxyClientStream; diff --git a/sing-box/daemon/instance.go b/sing-box/daemon/instance.go index 9f950c6432..f16e594e2c 100644 --- a/sing-box/daemon/instance.go +++ b/sing-box/daemon/instance.go @@ -69,7 +69,7 @@ type OverrideOptions struct { } func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) { - ctx := s.ctx + ctx := service.ExtendContext(s.ctx) service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) ctx, cancel := context.WithCancel(include.Context(ctx)) options, err := parseConfig(ctx, profileContent) diff --git a/sing-box/go.mod b/sing-box/go.mod index 6fda1a8eef..bed0b9b2ed 100644 --- a/sing-box/go.mod +++ b/sing-box/go.mod @@ -33,12 +33,12 @@ require ( github.com/sagernet/cors v1.2.1 github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa - github.com/sagernet/fswatch v0.1.1 + github.com/sagernet/fswatch v0.1.2 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.7-0.20260417135631-0d22698ed386 - github.com/sagernet/sing-cloudflared v0.0.0-20260416083718-efa6ab16dba9 + github.com/sagernet/sing v0.8.8-0.20260419124655-fbfc081294d5 + github.com/sagernet/sing-cloudflared v0.1.0 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 github.com/sagernet/sing-shadowsocks v0.2.8 @@ -81,7 +81,7 @@ require ( github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect github.com/ebitengine/purego v0.9.1 // indirect github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gaissmai/bart v0.18.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect diff --git a/sing-box/go.sum b/sing-box/go.sum index 77a3ad21ab..b9efcbffb2 100644 --- a/sing-box/go.sum +++ b/sing-box/go.sum @@ -50,8 +50,8 @@ github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE= github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= @@ -230,8 +230,8 @@ github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e27 github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:hy/3lPV11pKAAojDFnb95l9NpwOym6kME7FxS9p8sXs= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= -github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= -github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= +github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk= +github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= github.com/sagernet/gomobile v0.1.12/go.mod h1:A8l3FlHi2D/+mfcd4HHvk5DGFPW/ShFb9jHP5VmSiDY= github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/GejpNh+z30d5H1hwaB0nD9eY3o= @@ -242,10 +242,10 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.7-0.20260417135631-0d22698ed386 h1:gNonzIaoKi4MiEmK/5iJI+RoGof1PtYqzSaNE/lmWLw= -github.com/sagernet/sing v0.8.7-0.20260417135631-0d22698ed386/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= -github.com/sagernet/sing-cloudflared v0.0.0-20260416083718-efa6ab16dba9 h1:wWJexq4V7ope1c4XYSqT/hydqd9omdtbDRvSZIOhjfY= -github.com/sagernet/sing-cloudflared v0.0.0-20260416083718-efa6ab16dba9/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= +github.com/sagernet/sing v0.8.8-0.20260419124655-fbfc081294d5 h1:QdM7rhEdscgjXvy8qghxOk6zrf5swVaHZpRk/pTL68c= +github.com/sagernet/sing v0.8.8-0.20260419124655-fbfc081294d5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-cloudflared v0.1.0 h1:to+2fcCx8zu4X/DirRw9Ihc+FrEZ7oEyIqeCoJiwIpw= +github.com/sagernet/sing-cloudflared v0.1.0/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.2-0.20260412143638-8f65b6be7cd6 h1:j3ISQRDyY5rs27NzUS/le+DHR0iOO0K0x+mWDLzu4Ok= diff --git a/small/gn/Makefile b/small/gn/Makefile index 8b714a478d..809527ac01 100644 --- a/small/gn/Makefile +++ b/small/gn/Makefile @@ -9,9 +9,9 @@ PKG_RELEASE:=1 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://gn.googlesource.com/gn.git -PKG_SOURCE_DATE:=2026-04-09 -PKG_SOURCE_VERSION:=ec56d4d935a0e2ab9d52b88dd00c93ec51233055 -PKG_MIRROR_HASH:=08b2758166420f717ade756fc0f8fb1418055352c13a686c4115f21c7d792934 +PKG_SOURCE_DATE:=2026-04-17 +PKG_SOURCE_VERSION:=7f7c0c9a3b63b6b052714bbd1848b1b0723f6389 +PKG_MIRROR_HASH:=56526654380a74e98a7f4fc5644231e3bac9e4696369c5cff1b4228e4256cc48 PKG_LICENSE:=BSD 3-Clause PKG_LICENSE_FILES:=LICENSE diff --git a/small/gn/src/out/last_commit_position.h b/small/gn/src/out/last_commit_position.h index 377aacd25a..08b0a3d358 100644 --- a/small/gn/src/out/last_commit_position.h +++ b/small/gn/src/out/last_commit_position.h @@ -3,7 +3,7 @@ #ifndef OUT_LAST_COMMIT_POSITION_H_ #define OUT_LAST_COMMIT_POSITION_H_ -#define LAST_COMMIT_POSITION_NUM 2360 -#define LAST_COMMIT_POSITION "2360 (ec56d4d935a0)" +#define LAST_COMMIT_POSITION_NUM 2365 +#define LAST_COMMIT_POSITION "2365 (7f7c0c9a3b63)" #endif // OUT_LAST_COMMIT_POSITION_H_ diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua index 557fcf635d..397944eb42 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/haproxy.lua @@ -48,15 +48,23 @@ o.default = "" o:depends("console_auth", true) ---- Console Port -o = s:option(Value, "console_port", translate("Console Port"), translate( - "In the browser input routing IP plus port access, such as:192.168.1.1:1188")) -o.default = "1188" +o = s:option(Value, "console_port", translate("Console Port"), translate("In the browser input routing IP plus port access, such as:192.168.1.1:1188")) +o.datatype = "port" +o.default = 1188 o:depends("balancing_enable", true) o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) o.default = "0" o:depends("balancing_enable", true) +o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy")) +o:value("roundrobin", "roundRobin") +o:value("random", "random") +o:value("leastconn", "leastConn") +o:value("source", "source") +o.default = "roundrobin" +o:depends("balancing_enable", true) + ---- Health Check Type o = s:option(ListValue, "health_check_type", translate("Health Check Type")) o.default = "passwall_logic" @@ -77,9 +85,12 @@ o.description = translate("The URL used to detect the connection status.") o:depends("health_check_type", "passwall_logic") ---- Health Check Inter -o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds")) -o.default = "60" +o = s:option(Value, "health_check_inter", translate("Health Check Inter")) +o.datatype = "min(20)" +o.default = 60 o:depends("balancing_enable", true) +o.description = translate("Units:seconds") .. " ( ≥ 20s )
" .. + translate("Note: Frequent health checks increase CPU usage; an interval ≥ 60 seconds is recommended.") o = s:option(DummyValue, "health_check_tips", " ") o.rawhtml = true @@ -89,11 +100,10 @@ end o:depends("health_check_type", "passwall_logic") -- [[ Balancing Settings ]]-- -s = m:section(TypedSection, "haproxy_config", translate("Node List"), - "" .. - translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. - "\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. - "") +s = m:section(TypedSection, "haproxy_config", translate("Node List")) +s.description = "" .. + translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "
" .. + translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "
" s.template = "cbi/tblsection" s.sortable = true s.anonymous = true @@ -148,7 +158,7 @@ o.rmempty = false ---- Node Weight o = s:option(Value, "lbweight", translate("Node Weight")) -o.datatype = "uinteger" +o.datatype = "min(1)" o.default = 5 o.rmempty = false diff --git a/small/luci-app-passwall/po/zh-cn/passwall.po b/small/luci-app-passwall/po/zh-cn/passwall.po index 5741f02a83..011fcf92c6 100644 --- a/small/luci-app-passwall/po/zh-cn/passwall.po +++ b/small/luci-app-passwall/po/zh-cn/passwall.po @@ -928,6 +928,9 @@ msgstr "内置实现" msgid "Health Check Inter" msgstr "健康检查节点间隔时间" +msgid "Note: Frequent health checks increase CPU usage; an interval ≥ 60 seconds is recommended." +msgstr "注意:检测过于频繁会增加 CPU 负载,建议间隔 ≥60 秒。" + msgid "When the URL test is used, the load balancing node will be converted into a Socks node. when node list set customizing, must be a Socks node, otherwise the health check will be invalid." msgstr "当使用 URL 测试时,负载均衡节点将转换成 Socks 节点。下面的节点列表自定义时必须为 Socks 节点,否则健康检查将无效。" diff --git a/small/luci-app-passwall/root/usr/share/passwall/haproxy.lua b/small/luci-app-passwall/root/usr/share/passwall/haproxy.lua index fcff734469..ba5e171fb2 100644 --- a/small/luci-app-passwall/root/usr/share/passwall/haproxy.lua +++ b/small/luci-app-passwall/root/usr/share/passwall/haproxy.lua @@ -1,7 +1,7 @@ #!/usr/bin/lua local api = require ("luci.passwall.api") -local appname = "passwall" +local appname = api.appname local fs = api.fs local jsonc = api.jsonc local uci = api.uci @@ -24,7 +24,8 @@ local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53" local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1" local health_check_type = uci:get(appname, "@global_haproxy[0]", "health_check_type") or "tcp" -local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "10" +local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "20" +local balancingStrategy = uci:get(appname, "@global_haproxy[0]", "balancingStrategy") or "roundrobin" local console_port = uci:get(appname, "@global_haproxy[0]", "console_port") local bind_local = uci:get(appname, "@global_haproxy[0]", "bind_local") or "0" local bind_address = "0.0.0.0" @@ -40,35 +41,37 @@ local f_out = io.open(haproxy_file, "a") local haproxy_config = [[ global daemon - log 127.0.0.1 local2 - maxconn 60000 - stats socket {{path}}/haproxy.sock + log 127.0.0.1 local2 + maxconn 60000 + stats socket {{path}}/haproxy.sock nbthread {{nbthread}} external-check insecure-fork-wanted + spread-checks 50 + tune.max-checks-per-thread 5 defaults - mode tcp - log global - option tcplog - option dontlognull + mode tcp + log global + option tcplog + option dontlognull option http-server-close - #option forwardfor except 127.0.0.0/8 - option redispatch - retries 2 - timeout http-request 10s - timeout queue 1m - timeout connect 10s - timeout client 1m - timeout server 1m + #option forwardfor except 127.0.0.0/8 + option redispatch + retries 2 + timeout http-request 10s + timeout queue 1m + timeout connect 10s + timeout client 1m + timeout server 1m timeout http-keep-alive 10s - timeout check 10s - maxconn 3000 - + timeout check 10s + maxconn 3000 + resolvers mydns - resolve_retries 1 - timeout resolve 5s - hold valid 600s + resolve_retries 1 + timeout resolve 5s + hold valid 600s {{dns}} ]] @@ -99,10 +102,12 @@ uci:foreach(appname, "haproxy_config", function(t) local lbss = t.lbss local listen_port = tonumber(t.haproxy_port) or 0 local server_node = uci:get_all(appname, lbss) - if server_node and server_node.address and server_node.port then - server_remark = server_node.address .. ":" .. server_node.port + local hop = (health_check_type == "passwall_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil + hop = hop and hop:gsub(":", "-") or nil + if server_node and server_node.address and (server_node.port or hop) then + server_remark = server_node.address .. ":" .. (server_node.port or hop) server_address = server_node.address - server_port = server_node.port + server_port = server_node.port or hop t.origin_address = server_address t.origin_port = server_port if health_check_type == "passwall_logic" then @@ -145,6 +150,10 @@ uci:foreach(appname, "haproxy_config", function(t) end end) +local inter = tonumber((health_check_inter or "20"):match("^%d+")) +inter = (inter and inter >= 20) and inter or 20 +local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s" + local sortTable = {} for i in pairs(listens) do if i ~= nil then @@ -160,14 +169,14 @@ for i, port in pairs(sortTable) do listen %s bind %s:%s mode tcp - balance roundrobin -]], port, bind_address, port)) + balance %s +]], port, bind_address, port, balancingStrategy)) if health_check_type == "passwall_logic" then f_out:write(string.format([[ option external-check - external-check command "/usr/share/passwall/haproxy_check.sh" -]], port, port)) + external-check command "/usr/share/%s/haproxy_check.sh" +]], appname)) end local count_M, count_B = 1, 1 @@ -182,7 +191,7 @@ listen %s count_B = count_B + 1 end local server = o.server_address .. ":" .. o.server_port - local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check inter {{inter}} rise 1 fall 3 {{backup}}" + local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}" server_conf = server_conf:gsub("{{remark}}", remark) server_conf = server_conf:gsub("{{server}}", server) server_conf = server_conf:gsub("{{weight}}", o.lbweight) @@ -191,13 +200,13 @@ listen %s resolvers = "" end server_conf = server_conf:gsub("{{resolvers}}", resolvers) - server_conf = server_conf:gsub("{{inter}}", tonumber(health_check_inter) .. "s") + server_conf = server_conf:gsub("{{inter_arg}}", inter_arg) server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "") - f_out:write(" " .. server_conf .. "\n") + f_out:write(" " .. api.trim(server_conf) .. "\n") if o.export ~= "0" then - sys.call(string.format(". /usr/share/passwall/utils.sh ; add_ip2route %s %s", o.origin_address, o.export)) + sys.call(string.format(". /usr/share/%s/utils.sh ; add_ip2route %s %s", appname, o.origin_address, o.export)) end log(string.format(" | - 出口节点:%s:%s,权重:%s", o.origin_address, o.origin_port, o.lbweight)) diff --git a/small/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh b/small/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh index 91c1531b14..cea5d6294e 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh +++ b/small/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh @@ -25,7 +25,7 @@ if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then extra_params="${extra_params} --retry-all-errors" fi -status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 2 --max-time 10 -w "%{http_code}" "${probeUrl}") +status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}") case "$status" in 200|204) diff --git a/v2raya/.github/workflows/_build_binaries.yml b/v2raya/.github/workflows/_build_binaries.yml new file mode 100644 index 0000000000..6b2b78737b --- /dev/null +++ b/v2raya/.github/workflows/_build_binaries.yml @@ -0,0 +1,85 @@ +name: Build v2rayA Binaries (Reusable) + +on: + workflow_call: + inputs: + version: + description: 'Version string for the build' + type: string + required: true + +jobs: + Build_v2rayA_Binaries: + runs-on: ubuntu-latest + strategy: + matrix: + include: + # Linux amd64/arm64: TinyTun supported + - { goos: linux, goarch: amd64, tinytun: true } + - { goos: linux, goarch: arm64, tinytun: true } + # Linux other architectures: no TinyTun + - { goos: linux, goarch: '386', tinytun: false } + - { goos: linux, goarch: riscv64, tinytun: false } + - { goos: linux, goarch: mips64, tinytun: false } + - { goos: linux, goarch: mips64le, tinytun: false } + - { goos: linux, goarch: mipsle, tinytun: false } + - { goos: linux, goarch: mips, tinytun: false } + - { goos: linux, goarch: loong64, tinytun: false } + # Linux armv7: no TinyTun + - { goos: linux, goarch: arm, goarm: '7', tinytun: false } + # Windows amd64/arm64: TinyTun supported + - { goos: windows, goarch: amd64, tinytun: true } + - { goos: windows, goarch: arm64, tinytun: true } + # macOS amd64/arm64: TinyTun supported + - { goos: darwin, goarch: amd64, tinytun: true } + - { goos: darwin, goarch: arm64, tinytun: true } + # FreeBSD: no TinyTun + - { goos: freebsd, goarch: amd64, tinytun: false } + - { goos: freebsd, goarch: arm64, tinytun: false } + # OpenBSD: no TinyTun + - { goos: openbsd, goarch: amd64, tinytun: false } + - { goos: openbsd, goarch: arm64, tinytun: false } + env: + CGO_ENABLED: 0 + VERSION: ${{ inputs.version }} + NAME: v2raya + DESC: "A web GUI client of Project V which supports VMess, VLESS, SS, SSR, Trojan, Tuic and Juicity protocols" + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Download Web Artifact + uses: actions/download-artifact@v8 + with: + name: web + path: service/server/router/web + - name: Set up Go + uses: actions/setup-go@v6 + with: + cache-dependency-path: | + service/go.mod + service/go.sum + go-version: '1.24' + - name: Build v2rayA Binary + id: build_binary + shell: bash + run: | + mkdir -p v2raya_binaries + key="${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}" + filename=$(jq -r --arg key "$key" '.[$key].friendlyName' ./install/friendly-filenames.json) + ext="" + [ "${{ matrix.goos }}" = "windows" ] && ext=".exe" + build_tags=() + [ "${{ matrix.tinytun }}" = "true" ] && build_tags=(-tags tinytun) + cd service + GOOS="${{ matrix.goos }}" GOARCH="${{ matrix.goarch }}" GOARM="${{ matrix.goarm }}" \ + go build "${build_tags[@]}" \ + -o ../v2raya_binaries/v2raya_${filename}_${{ inputs.version }}${ext} \ + -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${{ inputs.version }} -s -w" \ + -trimpath + echo "filename=v2raya_${filename}_${{ inputs.version }}${ext}" >> ${GITHUB_OUTPUT} + - name: Upload Binary + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.build_binary.outputs.filename }} + path: v2raya_binaries/${{ steps.build_binary.outputs.filename }} \ No newline at end of file diff --git a/v2raya/.github/workflows/_build_linux_packages.yml b/v2raya/.github/workflows/_build_linux_packages.yml new file mode 100644 index 0000000000..36e77053fd --- /dev/null +++ b/v2raya/.github/workflows/_build_linux_packages.yml @@ -0,0 +1,139 @@ +name: Build Linux Packages (Reusable) + +on: + workflow_call: + inputs: + version: + description: 'Version string for the packages' + type: string + required: true + +jobs: + Build_Linux_Packages: + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux] + goarch: [amd64, arm64, 386, riscv64, mips64, mips64le, mipsle, mips, loong64] + include: + - goos: linux + goarch: arm + goarm: 7 + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOARM: ${{ matrix.goarm }} + CGO_ENABLED: 0 + VERSION: ${{ inputs.version }} + PACKAGE_VERSION: ${{ inputs.version }} + NAME: v2raya + DESC: "A web GUI client of Project V which supports VMess, VLESS, SS, SSR, Trojan, Tuic and Juicity protocols" + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Get Friendly File Name + shell: pwsh + id: get_filename + run: | + $build_name = $(((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}").friendlyName) + $friendly_arch = $((((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}").friendlyName).Split('_')[1]) + Write-Output "BUILD_NAME=$build_name" >> ${env:GITHUB_OUTPUT} + Write-Output "BUILD_NAME=$build_name" >> ${env:GITHUB_ENV} + Write-Output "FRIENDLY_ARCH=$friendly_arch" >> ${env:GITHUB_OUTPUT} + Write-Output "FRIENDLY_ARCH=$friendly_arch" >> ${env:GITHUB_ENV} + - name: Download Artifact + uses: actions/download-artifact@v8 + with: + name: v2raya_${{ steps.get_filename.outputs.BUILD_NAME }}_${{ inputs.version }} + path: build/ + - name: Download x64 Linux Build + uses: actions/download-artifact@v8 + with: + name: v2raya_linux_x64_${{ inputs.version }} + path: test/ + - name: Show Artifacts + run: | + ls -l build/ + - name: Build Packages + run: | + sudo gem install fpm -v 1.15.1 + sudo apt-get install -y libarchive-tools rpm tar unzip + sudo chmod 755 ./test/v2raya_linux_x64_$VERSION + ./test/v2raya_linux_x64_$VERSION --report config | sed '1,6d' | fold -s -w 78 | sed -E 's/^([^#].+)/# \1/' >> install/universal/v2raya.default + if [ -n "$(echo $GOARCH | grep mips)" ]; then + packages="deb rpm" + else + packages="deb rpm pacman" + fi + sudo chmod 755 ./build/v2raya_${{ steps.get_filename.outputs.BUILD_NAME }}_${{ inputs.version }} + for package_manager in $packages; do + if [ "$package_manager" == 'pacman' ];then + if [ "$GOARCH" == 'arm' ] && [ "$GOARM" == '7' ];then + package_arch='arm7hf' + elif [ "$GOARCH" == 'arm64' ];then + package_arch='aarch64' + else + package_arch="$GOARCH" + fi + elif [ "$package_manager" == 'rpm' ];then + if [ "$GOARCH" == 'arm' ] && [ "$GOARM" == '7' ];then + package_arch='armhfp' + elif [ "$GOARCH" == 'arm64' ];then + package_arch='aarch64' + elif [ "$GOARCH" == 'loong64' ];then + package_arch='loongson64' + else + package_arch="$GOARCH" + fi + elif [ "$package_manager" == 'deb' ];then + if [ "$GOARCH" == 'arm' ] && [ "$GOARM" == '7' ];then + package_arch='armhf' + elif [ "$GOARCH" == '386' ];then + package_arch='i386' + elif [ "$GOARCH" == 'mipsle' ];then + package_arch='mips32le' + else + package_arch="$GOARCH" + fi + fi + fpm -s dir -t "$package_manager" -a $package_arch --version "${{ inputs.version }}" \ + --url 'https://github.com/v2rayA/v2rayA' --description "$DESC" \ + --maintainer "v2raya@v2raya.org" --name v2raya --license 'AGPL' \ + --package installer_linux_$GOARCH$GOAMD64$GOARM_${{ inputs.version }}.$package_manager \ + --after-install ./install/universal/after_install.sh \ + --after-upgrade ./install/universal/after_upgrade.sh \ + ./build/v2raya_${{ steps.get_filename.outputs.BUILD_NAME }}_${{ inputs.version }}=/usr/bin/v2raya \ + ./install/universal/v2raya.service=/usr/lib/systemd/system/v2raya.service \ + ./install/universal/v2raya-lite.service=/usr/lib/systemd/user/v2raya-lite.service \ + ./install/universal/v2raya.png=/usr/share/icons/hicolor/512x512/apps/v2raya.png \ + ./install/universal/v2raya.desktop=/usr/share/applications/v2raya.desktop \ + ./install/universal/v2raya.default=/etc/default/v2raya + done + mkdir fpm_packages + [ -f installer_linux_$GOARCH$GOAMD64$GOARM_${{ inputs.version }}.pacman ] && \ + mv installer_linux_$GOARCH$GOAMD64$GOARM_${{ inputs.version }}.pacman \ + fpm_packages/installer_archlinux_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ inputs.version }}.pkg.tar.zst + mv installer_linux_$GOARCH$GOAMD64$GOARM_${{ inputs.version }}.rpm \ + fpm_packages/installer_redhat_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ inputs.version }}.rpm + mv installer_linux_$GOARCH$GOAMD64$GOARM_${{ inputs.version }}.deb \ + fpm_packages/installer_debian_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ inputs.version }}.deb + - name: Upload Arch Linux Package + uses: actions/upload-artifact@v7 + with: + if-no-files-found: ignore + name: installer_archlinux_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ inputs.version }}.pkg.tar.zst + path: | + fpm_packages/installer_archlinux_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ inputs.version }}.pkg.tar.zst + - name: Upload Red Hat Package + uses: actions/upload-artifact@v7 + with: + name: installer_redhat_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ inputs.version }}.rpm + path: | + fpm_packages/installer_redhat_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ inputs.version }}.rpm + - name: Upload Debian Package + uses: actions/upload-artifact@v7 + with: + name: installer_debian_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ inputs.version }}.deb + path: | + fpm_packages/installer_debian_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ inputs.version }}.deb diff --git a/v2raya/.github/workflows/_build_web.yml b/v2raya/.github/workflows/_build_web.yml new file mode 100644 index 0000000000..3907fca819 --- /dev/null +++ b/v2raya/.github/workflows/_build_web.yml @@ -0,0 +1,39 @@ +name: Build v2rayA Web (Reusable) + +on: + workflow_call: + inputs: + upload_archive: + description: 'Whether to also upload the web.tar.gz archive as a release asset' + type: boolean + default: false + +jobs: + Build_v2rayA_Web: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + - name: Build GUI + run: | + yarn --cwd gui --check-files + yarn --cwd gui build + echo "Use tar to generate web.tar.gz..." + tar -zcvf web.tar.gz web/ + - name: Upload Web Files to Artifacts + uses: actions/upload-artifact@v7 + with: + path: web/* + name: web + - name: Upload the tar archive to Artifacts + if: inputs.upload_archive + uses: actions/upload-artifact@v7 + with: + name: web.tar.gz + path: | + web.tar.gz diff --git a/v2raya/.github/workflows/_build_windows_installer.yml b/v2raya/.github/workflows/_build_windows_installer.yml new file mode 100644 index 0000000000..a8756ff49f --- /dev/null +++ b/v2raya/.github/workflows/_build_windows_installer.yml @@ -0,0 +1,115 @@ +name: Build Windows Installer (Reusable) + +on: + workflow_call: + inputs: + version: + description: 'Version string for the installer' + type: string + required: true + +jobs: + Build_Windows_Installer: + runs-on: windows-latest + env: + VERSION: ${{ inputs.version }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Download Artifact Windows x64 + uses: actions/download-artifact@v8 + with: + name: v2raya_windows_x64_${{ inputs.version }}.exe + path: D:\Downloads + - name: Download TinyTun x64 + shell: pwsh + run: | + $Url_TinyTun_x64 = "https://github.com/v2rayA/TinyTun/releases/download/v0.0.2-alpha.3/tinytun-v0.0.2-alpha.3-x86_64-pc-windows-msvc.zip" + $sha256_TinyTun_x64 = "a5e8c92999dc8d64862d14552ec833ea1ee2044cc8ac395eda03b041e1932ed4" + Invoke-WebRequest $Url_TinyTun_x64 -OutFile "D:\tinytun-x64.zip" + $hash_TinyTun_x64 = Get-FileHash -Path "D:\tinytun-x64.zip" -Algorithm SHA256 + if ($hash_TinyTun_x64.Hash -ne $sha256_TinyTun_x64) { + throw "SHA256 hash mismatch for TinyTun x64. Expected: $sha256_TinyTun_x64, Actual: $($hash_TinyTun_x64.Hash)" + exit 1 + } else { + Write-Host "SHA256 hash verified for TinyTun x64." + } + - name: Download Artifact Windows arm64 + uses: actions/download-artifact@v8 + with: + name: v2raya_windows_arm64_${{ inputs.version }}.exe + path: D:\Downloads + - name: Download TinyTun arm64 + shell: pwsh + run: | + $Url_TinyTun_arm64 = "https://github.com/v2rayA/TinyTun/releases/download/v0.0.2-alpha.3/tinytun-v0.0.2-alpha.3-aarch64-pc-windows-msvc.zip" + $sha256_TinyTun_arm64 = "b39a39e319f12935580f62b7e93b83a013d45d9aa2ec7ce48433acf77fbafed9" + Invoke-WebRequest $Url_TinyTun_arm64 -OutFile "D:\tinytun-arm64.zip" + $hash_TinyTun_arm64 = Get-FileHash -Path "D:\tinytun-arm64.zip" -Algorithm SHA256 + if ($hash_TinyTun_arm64.Hash -ne $sha256_TinyTun_arm64) { + throw "SHA256 hash mismatch for TinyTun arm64. Expected: $sha256_TinyTun_arm64, Actual: $($hash_TinyTun_arm64.Hash)" + exit 1 + } else { + Write-Host "SHA256 hash verified for TinyTun arm64." + } + - name: Install Inno Setup + shell: pwsh + run: | + choco install innosetup -y + - name: Build Windows Installer + shell: pwsh + run: | + ## Create Destination Directory + $CurrentDir = Get-Location + New-Item -ItemType Directory -Path "D:\v2raya-x64compatible-windows\data" + New-Item -ItemType Directory -Path "D:\v2raya-x64compatible-windows\bin" + New-Item -ItemType Directory -Path "D:\v2raya-arm64-windows\data" + New-Item -ItemType Directory -Path "D:\v2raya-arm64-windows\bin" + ## Copy v2rayA to Destination Directory + Copy-Item -Path D:\Downloads\v2raya_windows_arm64_${{ inputs.version }}.exe -Destination D:\v2raya-arm64-windows\bin\v2raya.exe + Copy-Item -Path D:\Downloads\v2raya_windows_x64_${{ inputs.version }}.exe -Destination D:\v2raya-x64compatible-windows\bin\v2raya.exe + Copy-Item -Path ".\install\windows-inno\v2raya.ico" -Destination "D:\v2raya.ico" + ## Copy TinyTun to Destination Directory + Expand-Archive -Path "D:\tinytun-x64.zip" -DestinationPath "D:\tinytun-x64" + Move-Item -Path "D:\tinytun-x64\tinytun-x86_64-pc-windows-msvc.exe" -Destination "D:\v2raya-x64compatible-windows\bin\tinytun.exe" + Move-Item -Path "D:\tinytun-x64\wintun.dll" -Destination "D:\v2raya-x64compatible-windows\bin\wintun.dll" + Expand-Archive -Path "D:\tinytun-arm64.zip" -DestinationPath "D:\tinytun-arm64" + Move-Item -Path "D:\tinytun-arm64\tinytun-aarch64-pc-windows-msvc.exe" -Destination "D:\v2raya-arm64-windows\bin\tinytun.exe" + Move-Item -Path "D:\tinytun-arm64\wintun.dll" -Destination "D:\v2raya-arm64-windows\bin\wintun.dll" + ## Download and extract v2ray + $Url_v2ray_x64 = "https://github.com/v2fly/v2ray-core/releases/latest/download/v2ray-windows-64.zip" + $Url_v2ray_A64 = "https://github.com/v2fly/v2ray-core/releases/latest/download/v2ray-windows-arm64-v8a.zip" + Invoke-WebRequest $Url_v2ray_x64 -OutFile "D:\v2ray-windows-x64.zip" + Expand-Archive -Path "D:\v2ray-windows-x64.zip" -DestinationPath "D:\v2raya-x64compatible-windows\bin\" + Move-Item -Path "D:\v2raya-x64compatible-windows\bin\*.dat" -Destination "D:\v2raya-x64compatible-windows\data\" + Remove-Item -Path "D:\v2raya-x64compatible-windows\bin\*.json" -Force -Recurse -ErrorAction SilentlyContinue + Invoke-WebRequest $Url_v2ray_A64 -OutFile "D:\v2ray-windows-A64.zip" + Expand-Archive -Path "D:\v2ray-windows-A64.zip" -DestinationPath "D:\v2raya-arm64-windows\bin\" + Move-Item -Path "D:\v2raya-arm64-windows\bin\*.dat" -Destination "D:\v2raya-arm64-windows\data\" + Remove-Item -Path "D:\v2raya-arm64-windows\bin\*.json" -Force -Recurse -ErrorAction SilentlyContinue + Copy-Item -Path ".\LICENSE" -Destination "D:\LICENSE.txt" + Copy-Item -Path ".\install\windows-inno\ChineseSimplified.isl" -Destination "D:\ChineseSimplified.isl" + ## Set Version + $((Get-Content -Path .\install\windows-inno\windows.iss).replace("TheRealVersion", "${{ inputs.version }}")).Replace("TheRealArch", "x64compatible") | Out-File "D:\windows_x86_64.iss" + $((Get-Content -Path .\install\windows-inno\windows.iss).replace("TheRealVersion", "${{ inputs.version }}")).Replace("TheRealArch", "arm64") | Out-File "D:\windows_arm64.iss" + ## Build Installer + Set-Location -Path "D:\" + & 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' "D:\windows_x86_64.iss" + & 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' "D:\windows_arm64.iss" + ## Rename to Friendly Name + cd $CurrentDir + Copy-Item -Path D:\installer_windows_inno_x64compatible.exe -Destination $CurrentDir\installer_windows_inno_x64_${{ inputs.version }}.exe + Copy-Item -Path D:\installer_windows_inno_arm64.exe -Destination $CurrentDir\installer_windows_inno_arm64_${{ inputs.version }}.exe + - name: Upload x64 Build + uses: actions/upload-artifact@v7 + with: + name: installer_windows_inno_x64_${{ inputs.version }}.exe + path: | + installer_windows_inno_x64_${{ inputs.version }}.exe + - name: Upload arm64 Build + uses: actions/upload-artifact@v7 + with: + name: installer_windows_inno_arm64_${{ inputs.version }}.exe + path: | + installer_windows_inno_arm64_${{ inputs.version }}.exe diff --git a/v2raya/.github/workflows/beta_release_main.yml b/v2raya/.github/workflows/beta_release_main.yml index d63500535c..66f87cbce1 100644 --- a/v2raya/.github/workflows/beta_release_main.yml +++ b/v2raya/.github/workflows/beta_release_main.yml @@ -9,327 +9,43 @@ on: jobs: Build_v2rayA_Web: + uses: ./.github/workflows/_build_web.yml + with: + upload_archive: true + + Compute_Version: runs-on: ubuntu-latest + outputs: + version: ${{ steps.prep.outputs.VERSION }} steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Set up Node.js - shell: bash + - name: Compute Version + id: prep run: | - eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" - brew install node@24 - echo "PATH=\"$(brew --prefix)/opt/node@24/bin:$PATH\"" >> $GITHUB_ENV - echo "PATH=\"$(brew --prefix)/opt/node@24/bin:$PATH\"" >> ~/.bash_profile - - name: Install Dependencies - run: | - sudo apt-get update -y && sudo apt-get install -y gzip - - name: Build GUI - run: | - yarn --cwd gui --check-files - yarn --cwd gui build - echo "Use tar to generate web.tar.gz..." - tar -zcvf web.tar.gz web/ - - name: Upload Zip File to Artifacts - uses: actions/upload-artifact@v4 - with: - path: web/* - name: web - - name: Upload the tar archive to Artifacts - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - web.tar.gz + tag=${{ inputs.tag }} + version=$(echo $tag | sed 's/v//g') + echo "VERSION=$version" >> $GITHUB_OUTPUT + Build_v2rayA_Binaries: - runs-on: ubuntu-latest - needs: [Build_v2rayA_Web] - env: - CGO_ENABLED: 0 - NAME: v2raya - DESC: "A web GUI client of Project V which supports VMess, VLESS, SS, SSR, Trojan, Tuic and Juicity protocols" - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - name: web - path: service/server/router/web - - name: Check Version - id: prep - env: - REF: ${{ inputs.tag }} - run: | - tag=${{ inputs.tag }} - version=$(echo $tag | sed 's/v//g') - echo "VERSION=$version" >> $GITHUB_OUTPUT - echo "VERSION=$version" >> $GITHUB_ENV - - name: Set up Go - uses: actions/setup-go@v5 - with: - cache-dependency-path: | - service/go.mod - service/go.sum - go-version: '1.24' - - name: Build v2rayA Binaries - id: build - shell: pwsh - run: | - New-Item -ItemType Directory -Path v2raya_binaries - foreach ($arch in @('amd64', 'arm64', '386', 'riscv64', 'mips64', 'mips64le', 'mipsle', 'mips', 'loong64')) { - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."linux-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arm in @('7')) { - $env:GOARCH = 'arm' - $env:GOARM = $arm - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."linux-arm$arm")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'windows' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."windows-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION}.exe -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'darwin' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."darwin-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'freebsd' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."freebsd-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'openbsd' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."openbsd-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - - name: Upload Artifact - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - v2raya_binaries/* + needs: [Build_v2rayA_Web, Compute_Version] + uses: ./.github/workflows/_build_binaries.yml + with: + version: ${{ needs.Compute_Version.outputs.version }} + Build_Windows_Installers: - runs-on: windows-latest - needs: [Build_v2rayA_Binaries] - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Check Version - id: prep - shell: bash - env: - REF: ${{ inputs.tag }} - run: | - tag=${{ inputs.tag }} - version=$(echo $tag | sed 's/v//g') - echo "VERSION=$version" >> $GITHUB_OUTPUT - echo "VERSION=$version" >> $GITHUB_ENV - - name: Download Artifact Windows x64 - uses: actions/download-artifact@v4 - with: - name: v2raya_windows_x64_${{ steps.prep.outputs.VERSION }}.exe - path: D:\Downloads - - name: Download Artifact Windows arm64 - uses: actions/download-artifact@v4 - with: - name: v2raya_windows_arm64_${{ steps.prep.outputs.VERSION }}.exe - path: D:\Downloads - - name: Install Inno Setup - shell: pwsh - run: | - choco install innosetup -y - - name: Build Windows Installer - shell: pwsh - run: | - ## Create Destination Directory - $CurrentDir = Get-Location - New-Item -ItemType Directory -Path "D:\v2raya-x64compatible-windows\data" - New-Item -ItemType Directory -Path "D:\v2raya-x64compatible-windows\bin" - New-Item -ItemType Directory -Path "D:\v2raya-arm64-windows\data" - New-Item -ItemType Directory -Path "D:\v2raya-arm64-windows\bin" - ## Copy v2rayA to Destination Directory - Copy-Item -Path D:\Downloads\v2raya_windows_arm64_${{ steps.prep.outputs.VERSION }}.exe -Destination D:\v2raya-arm64-windows\bin\v2raya.exe - Copy-Item -Path D:\Downloads\v2raya_windows_x64_${{ steps.prep.outputs.VERSION }}.exe -Destination D:\v2raya-x64compatible-windows\bin\v2raya.exe - Copy-Item -Path ".\install\windows-inno\v2raya.ico" -Destination "D:\v2raya.ico" - ## Download and extract v2ray - $Url_v2ray_x64 = "https://github.com/v2fly/v2ray-core/releases/latest/download/v2ray-windows-64.zip" - $Url_v2ray_A64 = "https://github.com/v2fly/v2ray-core/releases/latest/download/v2ray-windows-arm64-v8a.zip" - Invoke-WebRequest $Url_v2ray_x64 -OutFile "D:\v2ray-windows-x64.zip" - Expand-Archive -Path "D:\v2ray-windows-x64.zip" -DestinationPath "D:\v2raya-x64compatible-windows\bin\" - Move-Item -Path "D:\v2raya-x64compatible-windows\bin\*.dat" -Destination "D:\v2raya-x64compatible-windows\data\" - Remove-Item -Path "D:\v2raya-x64compatible-windows\bin\*.json" -Force -Recurse -ErrorAction SilentlyContinue - Invoke-WebRequest $Url_v2ray_A64 -OutFile "D:\v2ray-windows-A64.zip" - Expand-Archive -Path "D:\v2ray-windows-A64.zip" -DestinationPath "D:\v2raya-arm64-windows\bin\" - Move-Item -Path "D:\v2raya-arm64-windows\bin\*.dat" -Destination "D:\v2raya-arm64-windows\data\" - Remove-Item -Path "D:\v2raya-arm64-windows\bin\*.json" -Force -Recurse -ErrorAction SilentlyContinue - Copy-Item -Path ".\LICENSE" -Destination "D:\LICENSE.txt" - Copy-Item -Path ".\install\windows-inno\ChineseSimplified.isl" -Destination "D:\ChineseSimplified.isl" - ## Set Version - $((Get-Content -Path .\install\windows-inno\windows.iss).replace("TheRealVersion", "${{ steps.prep.outputs.VERSION }}")).Replace("TheRealArch", "x64compatible") | Out-File "D:\windows_x86_64.iss" - $((Get-Content -Path .\install\windows-inno\windows.iss).replace("TheRealVersion", "${{ steps.prep.outputs.VERSION }}")).Replace("TheRealArch", "arm64") | Out-File "D:\windows_arm64.iss" - ## Build Installer - Set-Location -Path "D:\" - & 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' "D:\windows_x86_64.iss" - & 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' "D:\windows_arm64.iss" - ## Rename to Friendly Name - cd $CurrentDir - Copy-Item -Path D:\installer_windows_inno_x64compatible.exe -Destination $CurrentDir\installer_windows_inno_x64_${{ steps.prep.outputs.VERSION }}.exe - Copy-Item -Path D:\installer_windows_inno_arm64.exe -Destination $CurrentDir\installer_windows_inno_arm64_${{ steps.prep.outputs.VERSION }}.exe - - name: Upload Artifact - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - installer_windows_inno_x64_${{ steps.prep.outputs.VERSION }}.exe - installer_windows_inno_arm64_${{ steps.prep.outputs.VERSION }}.exe + needs: [Build_v2rayA_Binaries, Compute_Version] + uses: ./.github/workflows/_build_windows_installer.yml + with: + version: ${{ needs.Compute_Version.outputs.version }} + Build_Linux_Packages: - runs-on: ubuntu-latest - needs: [Build_v2rayA_Binaries] - strategy: - matrix: - goos: [linux] - goarch: [amd64, arm64, 386, riscv64, mips64, mips64le, mipsle, mips, loong64] - include: - - goos: linux - goarch: arm - goarm: 7 - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - GOARM: ${{ matrix.goarm }} - CGO_ENABLED: 0 - NAME: v2raya - DESC: "A web GUI client of Project V which supports VMess, VLESS, SS, SSR, Trojan, Tuic and Juicity protocols" - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Get Friendly File Name - shell: pwsh - id: get_filename - run: | - $build_name = $(((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}").friendlyName) - $friendly_arch = $((((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}").friendlyName).Split('_')[1]) - Write-Output "BUILD_NAME=$build_name" >> ${env:GITHUB_OUTPUT} - Write-Output "BUILD_NAME=$build_name" >> ${env:GITHUB_ENV} - Write-Output "FRIENDLY_ARCH=$friendly_arch" >> ${env:GITHUB_OUTPUT} - Write-Output "FRIENDLY_ARCH=$friendly_arch" >> ${env:GITHUB_ENV} - - name: Check Version - id: prep - env: - REF: ${{ inputs.tag }} - run: | - tag=${{ inputs.tag }} - version=$(echo $tag | sed 's/v//g') - echo "VERSION=$version" >> $GITHUB_OUTPUT - echo "VERSION=$version" >> $GITHUB_ENV - echo "PACKAGE_VERSION=$version" >> $GITHUB_OUTPUT - echo "PACKAGE_VERSION=$version" >> $GITHUB_ENV - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - name: v2raya_${{ steps.get_filename.outputs.BUILD_NAME }}_${{ steps.prep.outputs.VERSION }} - path: build/ - - name: Download x64 Linux Build - uses: actions/download-artifact@v4 - with: - name: v2raya_linux_x64_${{ steps.prep.outputs.VERSION }} - path: test/ - - name: Show Artifacts - run: | - ls -l build/ - - name: Build Packages - run: | - sudo gem install fpm -v 1.15.1 - sudo apt-get install -y libarchive-tools rpm tar unzip - sudo chmod 755 ./test/v2raya_linux_x64_$VERSION - ./test/v2raya_linux_x64_$VERSION --report config | sed '1,6d' | fold -s -w 78 | sed -E 's/^([^#].+)/# \1/' >> install/universal/v2raya.default - if [ -n "$(echo $GOARCH | grep mips)" ]; then - packages="deb rpm" - else - packages="deb rpm pacman" - fi - sudo chmod 755 ./build/v2raya_${{ steps.get_filename.outputs.BUILD_NAME }}_${{ steps.prep.outputs.VERSION }} - for package_manager in $packages; do - if [ "$package_manager" == 'pacman' ];then - if [ "$GOARCH" == 'arm' ] && [ "$GOARM" == '7' ];then - package_arch='arm7hf' - elif [ "$GOARCH" == 'arm64' ];then - package_arch='aarch64' - else - package_arch="$GOARCH" - fi - elif [ "$package_manager" == 'rpm' ];then - if [ "$GOARCH" == 'arm' ] && [ "$GOARM" == '7' ];then - package_arch='armhfp' - elif [ "$GOARCH" == 'arm64' ];then - package_arch='aarch64' - elif [ "$GOARCH" == 'loong64' ];then - package_arch='loongson64' - else - package_arch="$GOARCH" - fi - elif [ "$package_manager" == 'deb' ];then - if [ "$GOARCH" == 'arm' ] && [ "$GOARM" == '7' ];then - package_arch='armhf' - elif [ "$GOARCH" == '386' ];then - package_arch='i386' - elif [ "$GOARCH" == 'mipsle' ];then - package_arch='mips32le' - else - package_arch="$GOARCH" - fi - fi - fpm -s dir -t "$package_manager" -a $package_arch --version "${{ steps.prep.outputs.PACKAGE_VERSION }}" \ - --url 'https://github.com/v2rayA/v2rayA' --description "$DESC" \ - --maintainer "v2raya@v2raya.org" --name v2raya --license 'AGPL' \ - --package installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.$package_manager \ - --after-install ./install/universal/after_install.sh \ - --after-upgrade ./install/universal/after_upgrade.sh \ - ./build/v2raya_${{ steps.get_filename.outputs.BUILD_NAME }}_${{ steps.prep.outputs.VERSION }}=/usr/bin/v2raya \ - ./install/universal/v2raya.service=/usr/lib/systemd/system/v2raya.service \ - ./install/universal/v2raya-lite.service=/usr/lib/systemd/user/v2raya-lite.service \ - ./install/universal/v2raya.png=/usr/share/icons/hicolor/512x512/apps/v2raya.png \ - ./install/universal/v2raya.desktop=/usr/share/applications/v2raya.desktop \ - ./install/universal/v2raya.default=/etc/default/v2raya - done - mkdir fpm_packages - [ -f installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.pacman ] && \ - mv installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.pacman \ - fpm_packages/installer_archlinux_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ steps.prep.outputs.VERSION }}.pkg.tar.zst - mv installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.rpm \ - fpm_packages/installer_redhat_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ steps.prep.outputs.VERSION }}.rpm - mv installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.deb \ - fpm_packages/installer_debian_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ steps.prep.outputs.VERSION }}.deb - - name: Upload Artifact - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - fpm_packages/* + needs: [Build_v2rayA_Binaries, Compute_Version] + uses: ./.github/workflows/_build_linux_packages.yml + with: + version: ${{ needs.Compute_Version.outputs.version }} + GitHub_Release: runs-on: ubuntu-latest - needs: [Build_v2rayA_Binaries, Build_Windows_Installers, Build_Linux_Packages] + needs: [Build_v2rayA_Binaries, Build_Windows_Installers, Build_Linux_Packages] steps: - uses: actions/checkout@v6 with: @@ -344,7 +60,7 @@ jobs: echo "VERSION=$version" >> $GITHUB_OUTPUT echo "VERSION=$version" >> $GITHUB_ENV - name: Download Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: builds/ - name: Move Files to Release Directory @@ -370,8 +86,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - echo "Don't upload SingTun binaries to GitHub Release for they are not stable." - rm -f release/v2raya_with_singtun_* Latest_Releases=$(curl -s -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/v2rayA/v2rayA/releases/latest | jq -r '.tag_name') Latest_tag=v${{ steps.prep.outputs.VERSION }} if [ "$Latest_Releases" != "$Latest_tag" ]; then diff --git a/v2raya/.github/workflows/pull_request.yml b/v2raya/.github/workflows/pull_request.yml index d4e9dc6c22..a63186719e 100644 --- a/v2raya/.github/workflows/pull_request.yml +++ b/v2raya/.github/workflows/pull_request.yml @@ -9,55 +9,36 @@ on: - ".github/workflows/*.yml" jobs: Build_v2rayA_Web: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Set up Node.js - shell: bash - run: | - eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" - brew install node@24 - echo "PATH=\"$(brew --prefix)/opt/node@24/bin:$PATH\"" >> $GITHUB_ENV - echo "PATH=\"$(brew --prefix)/opt/node@24/bin:$PATH\"" >> ~/.bash_profile - - name: Install Dependencies - run: | - sudo apt-get update -y && sudo apt-get install -y gzip - - name: Build GUI - run: | - yarn --cwd gui --check-files - yarn --cwd gui build - echo "Use tar to generate web.tar.gz..." - tar -zcvf web.tar.gz web/ - - name: Upload Zip File to Artifacts - uses: actions/upload-artifact@v4 - with: - path: web/* - name: web - + uses: ./.github/workflows/_build_web.yml + Build_v2rayA: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + include: + - { os: ubuntu-latest, goarch: amd64 } + - { os: ubuntu-latest, goarch: arm64 } + - { os: windows-latest, goarch: amd64 } + - { os: windows-latest, goarch: arm64 } + - { os: macos-latest, goarch: amd64 } + - { os: macos-latest, goarch: arm64 } needs: Build_v2rayA_Web steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Download Artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: web path: service/server/router/web - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: cache-dependency-path: | service/go.mod service/go.sum - go-version: '1.24' + go-version: '1.26' - name: Build v2rayA shell: bash run: | @@ -67,19 +48,17 @@ jobs: count=$(git -C "$CurrentDir" rev-list --count HEAD) commit=$(git -C "$CurrentDir" rev-parse --short HEAD) version="unstable-$date.r${count}.$commit" - if [ "${{ matrix.os }}" = "windows-latest" ]; then - win_ex_name=".exe" - os="windows" - elif [ "${{ matrix.os }}" = "macos-latest" ]; then - os="darwin" - elif [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - os="linux" - fi + case "${{ matrix.os }}" in + windows-latest) os="windows"; ext=".exe" ;; + macos-latest) os="darwin"; ext="" ;; + ubuntu-latest) os="linux"; ext="" ;; + esac cd "$CurrentDir"/service - for arch in amd64 arm64; do - GOARCH="$arch" CGO_ENABLED=0 go build -tags "with_gvisor" -ldflags "-X github.com/v2rayA/v2rayA/conf.Version=$version -s -w" -o "$CurrentDir"/v2raya_bin/v2raya_pr_build_"$os"_"$arch"_"$version""$win_ex_name" - done - - name: Upload Zip File to Artifacts - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 + # All PR builds target amd64/arm64 on linux/windows/darwin — all support TinyTun + GOARCH="${{ matrix.goarch }}" CGO_ENABLED=0 go build -tags "tinytun" \ + -ldflags "-X github.com/v2rayA/v2rayA/conf.Version=$version -s -w" \ + -o "$CurrentDir/v2raya_bin/v2raya_pr_build_${os}_${{ matrix.goarch }}_${version}${ext}" + - name: Upload Binary + uses: actions/upload-artifact@v7 with: - path: v2raya_bin/* + path: v2raya_bin/v2raya_pr_build_${os}_${{ matrix.goarch }}_${version}${ext} diff --git a/v2raya/.github/workflows/release_main.yml b/v2raya/.github/workflows/release_main.yml index d23a1ee275..0ff03f27fc 100644 --- a/v2raya/.github/workflows/release_main.yml +++ b/v2raya/.github/workflows/release_main.yml @@ -8,327 +8,43 @@ on: required: true jobs: Build_v2rayA_Web: + uses: ./.github/workflows/_build_web.yml + with: + upload_archive: true + + Compute_Version: runs-on: ubuntu-latest + outputs: + version: ${{ steps.prep.outputs.VERSION }} steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Set up Node.js - shell: bash + - name: Compute Version + id: prep run: | - eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" - brew install node@24 - echo "PATH=\"$(brew --prefix)/opt/node@24/bin:$PATH\"" >> $GITHUB_ENV - echo "PATH=\"$(brew --prefix)/opt/node@24/bin:$PATH\"" >> ~/.bash_profile - - name: Install Dependencies - run: | - sudo apt-get update -y && sudo apt-get install -y gzip - - name: Build GUI - run: | - yarn --cwd gui --check-files - yarn --cwd gui build - echo "Use tar to generate web.tar.gz..." - tar -zcvf web.tar.gz web/ - - name: Upload Zip File to Artifacts - uses: actions/upload-artifact@v4 - with: - path: web/* - name: web - - name: Upload the tar archive to Artifacts - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - web.tar.gz + tag=${{ inputs.tag }} + version=$(echo $tag | sed 's/v//g') + echo "VERSION=$version" >> $GITHUB_OUTPUT + Build_v2rayA_Binaries: - runs-on: ubuntu-latest - needs: [Build_v2rayA_Web] - env: - CGO_ENABLED: 0 - NAME: v2raya - DESC: "A web GUI client of Project V which supports VMess, VLESS, SS, SSR, Trojan, Tuic and Juicity protocols" - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - name: web - path: service/server/router/web - - name: Check Version - id: prep - env: - REF: ${{ inputs.tag }} - run: | - tag=${{ inputs.tag }} - version=$(echo $tag | sed 's/v//g') - echo "VERSION=$version" >> $GITHUB_OUTPUT - echo "VERSION=$version" >> $GITHUB_ENV - - name: Set up Go - uses: actions/setup-go@v5 - with: - cache-dependency-path: | - service/go.mod - service/go.sum - go-version: '1.24' - - name: Build v2rayA Binaries - id: build - shell: pwsh - run: | - New-Item -ItemType Directory -Path v2raya_binaries - foreach ($arch in @('amd64', 'arm64', '386', 'riscv64', 'mips64', 'mips64le', 'mipsle', 'mips', 'loong64')) { - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."linux-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arm in @('7')) { - $env:GOARCH = 'arm' - $env:GOARM = $arm - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."linux-arm$arm")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'windows' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."windows-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION}.exe -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'darwin' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."darwin-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'freebsd' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."freebsd-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'openbsd' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."openbsd-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - - name: Upload Artifact - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - v2raya_binaries/* + needs: [Build_v2rayA_Web, Compute_Version] + uses: ./.github/workflows/_build_binaries.yml + with: + version: ${{ needs.Compute_Version.outputs.version }} + Build_Windows_Installers: - runs-on: windows-latest - needs: [Build_v2rayA_Binaries] - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Check Version - id: prep - shell: bash - env: - REF: ${{ inputs.tag }} - run: | - tag=${{ inputs.tag }} - version=$(echo $tag | sed 's/v//g') - echo "VERSION=$version" >> $GITHUB_OUTPUT - echo "VERSION=$version" >> $GITHUB_ENV - - name: Download Artifact Windows x64 - uses: actions/download-artifact@v4 - with: - name: v2raya_windows_x64_${{ steps.prep.outputs.VERSION }}.exe - path: D:\Downloads - - name: Download Artifact Windows arm64 - uses: actions/download-artifact@v4 - with: - name: v2raya_windows_arm64_${{ steps.prep.outputs.VERSION }}.exe - path: D:\Downloads - - name: Install Inno Setup - shell: pwsh - run: | - choco install innosetup -y - - name: Build Windows Installer - shell: pwsh - run: | - ## Create Destination Directory - $CurrentDir = Get-Location - New-Item -ItemType Directory -Path "D:\v2raya-x64compatible-windows\data" - New-Item -ItemType Directory -Path "D:\v2raya-x64compatible-windows\bin" - New-Item -ItemType Directory -Path "D:\v2raya-arm64-windows\data" - New-Item -ItemType Directory -Path "D:\v2raya-arm64-windows\bin" - ## Copy v2rayA to Destination Directory - Copy-Item -Path D:\Downloads\v2raya_windows_arm64_${{ steps.prep.outputs.VERSION }}.exe -Destination D:\v2raya-arm64-windows\bin\v2raya.exe - Copy-Item -Path D:\Downloads\v2raya_windows_x64_${{ steps.prep.outputs.VERSION }}.exe -Destination D:\v2raya-x64compatible-windows\bin\v2raya.exe - Copy-Item -Path ".\install\windows-inno\v2raya.ico" -Destination "D:\v2raya.ico" - ## Download and extract v2ray - $Url_v2ray_x64 = "https://github.com/v2fly/v2ray-core/releases/latest/download/v2ray-windows-64.zip" - $Url_v2ray_A64 = "https://github.com/v2fly/v2ray-core/releases/latest/download/v2ray-windows-arm64-v8a.zip" - Invoke-WebRequest $Url_v2ray_x64 -OutFile "D:\v2ray-windows-x64.zip" - Expand-Archive -Path "D:\v2ray-windows-x64.zip" -DestinationPath "D:\v2raya-x64compatible-windows\bin\" - Move-Item -Path "D:\v2raya-x64compatible-windows\bin\*.dat" -Destination "D:\v2raya-x64compatible-windows\data\" - Remove-Item -Path "D:\v2raya-x64compatible-windows\bin\*.json" -Force -Recurse -ErrorAction SilentlyContinue - Invoke-WebRequest $Url_v2ray_A64 -OutFile "D:\v2ray-windows-A64.zip" - Expand-Archive -Path "D:\v2ray-windows-A64.zip" -DestinationPath "D:\v2raya-arm64-windows\bin\" - Move-Item -Path "D:\v2raya-arm64-windows\bin\*.dat" -Destination "D:\v2raya-arm64-windows\data\" - Remove-Item -Path "D:\v2raya-arm64-windows\bin\*.json" -Force -Recurse -ErrorAction SilentlyContinue - Copy-Item -Path ".\LICENSE" -Destination "D:\LICENSE.txt" - Copy-Item -Path ".\install\windows-inno\ChineseSimplified.isl" -Destination "D:\ChineseSimplified.isl" - ## Set Version - $((Get-Content -Path .\install\windows-inno\windows.iss).replace("TheRealVersion", "${{ steps.prep.outputs.VERSION }}")).Replace("TheRealArch", "x64compatible") | Out-File "D:\windows_x86_64.iss" - $((Get-Content -Path .\install\windows-inno\windows.iss).replace("TheRealVersion", "${{ steps.prep.outputs.VERSION }}")).Replace("TheRealArch", "arm64") | Out-File "D:\windows_arm64.iss" - ## Build Installer - Set-Location -Path "D:\" - & 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' "D:\windows_x86_64.iss" - & 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' "D:\windows_arm64.iss" - ## Rename to Friendly Name - cd $CurrentDir - Copy-Item -Path D:\installer_windows_inno_x64compatible.exe -Destination $CurrentDir\installer_windows_inno_x64_${{ steps.prep.outputs.VERSION }}.exe - Copy-Item -Path D:\installer_windows_inno_arm64.exe -Destination $CurrentDir\installer_windows_inno_arm64_${{ steps.prep.outputs.VERSION }}.exe - - name: Upload Artifact - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - installer_windows_inno_x64_${{ steps.prep.outputs.VERSION }}.exe - installer_windows_inno_arm64_${{ steps.prep.outputs.VERSION }}.exe + needs: [Build_v2rayA_Binaries, Compute_Version] + uses: ./.github/workflows/_build_windows_installer.yml + with: + version: ${{ needs.Compute_Version.outputs.version }} + Build_Linux_Packages: - runs-on: ubuntu-latest - needs: [Build_v2rayA_Binaries] - strategy: - matrix: - goos: [linux] - goarch: [amd64, arm64, 386, riscv64, mips64, mips64le, mipsle, mips, loong64] - include: - - goos: linux - goarch: arm - goarm: 7 - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - GOARM: ${{ matrix.goarm }} - CGO_ENABLED: 0 - NAME: v2raya - DESC: "A web GUI client of Project V which supports VMess, VLESS, SS, SSR, Trojan, Tuic and Juicity protocols" - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Get Friendly File Name - shell: pwsh - id: get_filename - run: | - $build_name = $(((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}").friendlyName) - $friendly_arch = $((((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}").friendlyName).Split('_')[1]) - Write-Output "BUILD_NAME=$build_name" >> ${env:GITHUB_OUTPUT} - Write-Output "BUILD_NAME=$build_name" >> ${env:GITHUB_ENV} - Write-Output "FRIENDLY_ARCH=$friendly_arch" >> ${env:GITHUB_OUTPUT} - Write-Output "FRIENDLY_ARCH=$friendly_arch" >> ${env:GITHUB_ENV} - - name: Check Version - id: prep - env: - REF: ${{ inputs.tag }} - run: | - tag=${{ inputs.tag }} - version=$(echo $tag | sed 's/v//g') - echo "VERSION=$version" >> $GITHUB_OUTPUT - echo "VERSION=$version" >> $GITHUB_ENV - echo "PACKAGE_VERSION=$version" >> $GITHUB_OUTPUT - echo "PACKAGE_VERSION=$version" >> $GITHUB_ENV - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - name: v2raya_${{ steps.get_filename.outputs.BUILD_NAME }}_${{ steps.prep.outputs.VERSION }} - path: build/ - - name: Download x64 Linux Build - uses: actions/download-artifact@v4 - with: - name: v2raya_linux_x64_${{ steps.prep.outputs.VERSION }} - path: test/ - - name: Show Artifacts - run: | - ls -l build/ - - name: Build Packages - run: | - sudo gem install fpm -v 1.15.1 - sudo apt-get install -y libarchive-tools rpm tar unzip - sudo chmod 755 ./test/v2raya_linux_x64_$VERSION - ./test/v2raya_linux_x64_$VERSION --report config | sed '1,6d' | fold -s -w 78 | sed -E 's/^([^#].+)/# \1/' >> install/universal/v2raya.default - if [ -n "$(echo $GOARCH | grep mips)" ]; then - packages="deb rpm" - else - packages="deb rpm pacman" - fi - sudo chmod 755 ./build/v2raya_${{ steps.get_filename.outputs.BUILD_NAME }}_${{ steps.prep.outputs.VERSION }} - for package_manager in $packages; do - if [ "$package_manager" == 'pacman' ];then - if [ "$GOARCH" == 'arm' ] && [ "$GOARM" == '7' ];then - package_arch='arm7hf' - elif [ "$GOARCH" == 'arm64' ];then - package_arch='aarch64' - else - package_arch="$GOARCH" - fi - elif [ "$package_manager" == 'rpm' ];then - if [ "$GOARCH" == 'arm' ] && [ "$GOARM" == '7' ];then - package_arch='armhfp' - elif [ "$GOARCH" == 'arm64' ];then - package_arch='aarch64' - elif [ "$GOARCH" == 'loong64' ];then - package_arch='loongson64' - else - package_arch="$GOARCH" - fi - elif [ "$package_manager" == 'deb' ];then - if [ "$GOARCH" == 'arm' ] && [ "$GOARM" == '7' ];then - package_arch='armhf' - elif [ "$GOARCH" == '386' ];then - package_arch='i386' - elif [ "$GOARCH" == 'mipsle' ];then - package_arch='mips32le' - else - package_arch="$GOARCH" - fi - fi - fpm -s dir -t "$package_manager" -a $package_arch --version "${{ steps.prep.outputs.PACKAGE_VERSION }}" \ - --url 'https://github.com/v2rayA/v2rayA' --description "$DESC" \ - --maintainer "v2raya@v2raya.org" --name v2raya --license 'AGPL' \ - --package installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.$package_manager \ - --after-install ./install/universal/after_install.sh \ - --after-upgrade ./install/universal/after_upgrade.sh \ - ./build/v2raya_${{ steps.get_filename.outputs.BUILD_NAME }}_${{ steps.prep.outputs.VERSION }}=/usr/bin/v2raya \ - ./install/universal/v2raya.service=/usr/lib/systemd/system/v2raya.service \ - ./install/universal/v2raya-lite.service=/usr/lib/systemd/user/v2raya-lite.service \ - ./install/universal/v2raya.png=/usr/share/icons/hicolor/512x512/apps/v2raya.png \ - ./install/universal/v2raya.desktop=/usr/share/applications/v2raya.desktop \ - ./install/universal/v2raya.default=/etc/default/v2raya - done - mkdir fpm_packages - [ -f installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.pacman ] && \ - mv installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.pacman \ - fpm_packages/installer_archlinux_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ steps.prep.outputs.VERSION }}.pkg.tar.zst - mv installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.rpm \ - fpm_packages/installer_redhat_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ steps.prep.outputs.VERSION }}.rpm - mv installer_linux_$GOARCH$GOAMD64$GOARM_${{ steps.prep.outputs.VERSION }}.deb \ - fpm_packages/installer_debian_${{ steps.get_filename.outputs.FRIENDLY_ARCH }}_${{ steps.prep.outputs.VERSION }}.deb - - name: Upload Artifact - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - fpm_packages/* + needs: [Build_v2rayA_Binaries, Compute_Version] + uses: ./.github/workflows/_build_linux_packages.yml + with: + version: ${{ needs.Compute_Version.outputs.version }} + GitHub_Release: runs-on: ubuntu-latest - needs: [Build_v2rayA_Binaries, Build_Windows_Installers, Build_Linux_Packages] + needs: [Build_v2rayA_Binaries, Build_Windows_Installers, Build_Linux_Packages] steps: - uses: actions/checkout@v6 with: @@ -343,7 +59,7 @@ jobs: echo "VERSION=$version" >> $GITHUB_OUTPUT echo "VERSION=$version" >> $GITHUB_ENV - name: Download Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: builds/ - name: Move Files to Release Directory @@ -369,8 +85,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - echo "Don't upload SingTun binaries to GitHub Release for they are not stable." - rm -f release/v2raya_with_singtun_* Latest_Releases=$(curl -s -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/v2rayA/v2rayA/releases/latest | jq -r '.tag_name') Latest_tag=v${{ steps.prep.outputs.VERSION }} if [ "$Latest_Releases" != "$Latest_tag" ]; then @@ -407,17 +121,17 @@ jobs: echo "VERSION=$version" >> $GITHUB_OUTPUT echo "VERSION=$version" >> $GITHUB_ENV - name: Download Artifact Linux x64 - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: v2raya_linux_x64_${{ steps.prep.outputs.VERSION }} path: Downloads - name: Download Artifact macOS x64 - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: v2raya_darwin_x64_${{ steps.prep.outputs.VERSION }} path: Downloads - name: Download Artifact macOS arm64 - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: v2raya_darwin_arm64_${{ steps.prep.outputs.VERSION }} path: Downloads @@ -511,7 +225,7 @@ jobs: echo "VERSION=$version" >> $GITHUB_OUTPUT echo "VERSION=$version" >> $GITHUB_ENV - name: Download Web Files - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: web path: web @@ -610,4 +324,4 @@ jobs: echo "${{ secrets.SSH_PRIVATE_KEY }}" > ./key chmod 0600 ./key ./install/tool/ssh-add_expect ./key - bash install/aur/deploy.sh \ No newline at end of file + bash install/aur/deploy.sh diff --git a/v2raya/.github/workflows/test_build_main.yml b/v2raya/.github/workflows/test_build_main.yml index ef6bb95e36..f50fc8369e 100644 --- a/v2raya/.github/workflows/test_build_main.yml +++ b/v2raya/.github/workflows/test_build_main.yml @@ -15,200 +15,33 @@ on: jobs: Build_v2rayA_Web: + uses: ./.github/workflows/_build_web.yml + + Compute_Version: runs-on: ubuntu-latest + outputs: + version: ${{ steps.prep.outputs.VERSION }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Set up Node.js - shell: bash + - name: Compute Version + id: prep run: | - eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" - brew install node@24 - echo "PATH=\"$(brew --prefix)/opt/node@24/bin:$PATH\"" >> $GITHUB_ENV - echo "PATH=\"$(brew --prefix)/opt/node@24/bin:$PATH\"" >> ~/.bash_profile - - name: Install Dependencies - run: | - sudo apt-get update -y && sudo apt-get install -y gzip - - name: Build GUI - run: | - yarn --cwd gui --check-files - yarn --cwd gui build - echo "Use tar to generate web.tar.gz..." - tar -zcvf web.tar.gz web/ - - name: Upload Zip File to Artifacts - uses: actions/upload-artifact@v4 - with: - path: web/* - name: web + date=$(git log -1 --format="%cd" --date=short | sed s/-//g) + count=$(git rev-list --count HEAD) + commit=$(git rev-parse --short HEAD) + version="unstable-$date.r${count}.$commit" + echo "VERSION=$version" >> $GITHUB_OUTPUT Build_v2rayA_Binaries: - runs-on: ubuntu-latest - needs: [Build_v2rayA_Web] - env: - CGO_ENABLED: 0 - NAME: v2raya - DESC: "A web GUI client of Project V which supports VMess, VLESS, SS, SSR, Trojan, Tuic and Juicity protocols" - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - name: web - path: service/server/router/web - - name: Check Version - id: prep - run: | - echo "P_DIR=$(pwd)" >> $GITHUB_OUTPUT - date=$(git log -1 --format="%cd" --date=short | sed s/-//g) - count=$(git rev-list --count HEAD) - commit=$(git rev-parse --short HEAD) - version="unstable-$date.r${count}.$commit" - echo "VERSION=$version" >> $GITHUB_OUTPUT - echo "VERSION=$version" >> $GITHUB_ENV - - name: Set up Go - uses: actions/setup-go@v5 - with: - cache-dependency-path: | - service/go.mod - service/go.sum - go-version: '1.24' - - name: Build v2rayA Binaries - id: build - shell: pwsh - run: | - New-Item -ItemType Directory -Path v2raya_binaries - foreach ($arch in @('amd64', 'arm64', '386', 'riscv64', 'mips64', 'mips64le', 'mipsle', 'mips', 'loong64')) { - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."linux-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arm in @('7')) { - $env:GOARCH = 'arm' - $env:GOARM = $arm - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."linux-arm$arm")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'windows' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."windows-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION}.exe -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'darwin' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."darwin-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'freebsd' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."freebsd-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - foreach ($arch in @('amd64', 'arm64')) { - $env:GOOS = 'openbsd' - $env:GOARCH = $arch - $filename = $((Get-Content ./install/friendly-filenames.json | ConvertFrom-Json)."openbsd-$arch")."friendlyName" - Set-Location -Path service - go build -tags "with_gvisor" -o ../v2raya_binaries/v2raya_${filename}_${env:VERSION}_${env:VERSION} -ldflags="-X github.com/v2rayA/v2rayA/conf.Version=${env:VERSION} -s -w" -trimpath - Set-Location -Path .. - } - - name: Upload Artifact - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - v2raya_binaries/* + needs: [Build_v2rayA_Web, Compute_Version] + uses: ./.github/workflows/_build_binaries.yml + with: + version: ${{ needs.Compute_Version.outputs.version }} - Build_Windows_installer: - runs-on: windows-latest - needs: [Build_v2rayA_Binaries] - env: - CGO_ENABLED: 0 - NAME: v2raya - DESC: "A web GUI client of Project V which supports VMess, VLESS, SS, SSR, Trojan, Tuic and Juicity protocols" - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Check Version - id: prep - shell: bash - run: | - echo "P_DIR=$(pwd)" >> $GITHUB_OUTPUT - date=$(git log -1 --format="%cd" --date=short | sed s/-//g) - count=$(git rev-list --count HEAD) - commit=$(git rev-parse --short HEAD) - version="unstable-$date.r${count}.$commit" - echo "VERSION=$version" >> $GITHUB_OUTPUT - echo "VERSION=$version" >> $GITHUB_ENV - - name: Download Artifact Windows x64 - uses: actions/download-artifact@v4 - with: - name: v2raya_windows_x64_${{ steps.prep.outputs.VERSION }}.exe - path: D:\Downloads - - name: Download Artifact Windows arm64 - uses: actions/download-artifact@v4 - with: - name: v2raya_windows_arm64_${{ steps.prep.outputs.VERSION }}.exe - path: D:\Downloads - - name: Install Inno Setup - shell: pwsh - run: | - choco install innosetup -y - - name: Build Windows Installer - shell: pwsh - run: | - ## Create Destination Directory - $CurrentDir = Get-Location - New-Item -ItemType Directory -Path "D:\v2raya-x64compatible-windows\data" - New-Item -ItemType Directory -Path "D:\v2raya-x64compatible-windows\bin" - New-Item -ItemType Directory -Path "D:\v2raya-arm64-windows\data" - New-Item -ItemType Directory -Path "D:\v2raya-arm64-windows\bin" - ## Copy v2rayA to Destination Directory - Copy-Item -Path D:\Downloads\v2raya_windows_arm64_${{ steps.prep.outputs.VERSION }}.exe -Destination D:\v2raya-arm64-windows\bin\v2raya.exe - Copy-Item -Path D:\Downloads\v2raya_windows_x64_${{ steps.prep.outputs.VERSION }}.exe -Destination D:\v2raya-x64compatible-windows\bin\v2raya.exe - Copy-Item -Path ".\install\windows-inno\v2raya.ico" -Destination "D:\v2raya.ico" - ## Download and extract v2ray - $Url_v2ray_x64 = "https://github.com/v2fly/v2ray-core/releases/latest/download/v2ray-windows-64.zip" - $Url_v2ray_A64 = "https://github.com/v2fly/v2ray-core/releases/latest/download/v2ray-windows-arm64-v8a.zip" - Invoke-WebRequest $Url_v2ray_x64 -OutFile "D:\v2ray-windows-x64.zip" - Expand-Archive -Path "D:\v2ray-windows-x64.zip" -DestinationPath "D:\v2raya-x64compatible-windows\bin\" - Move-Item -Path "D:\v2raya-x64compatible-windows\bin\*.dat" -Destination "D:\v2raya-x64compatible-windows\data\" - Remove-Item -Path "D:\v2raya-x64compatible-windows\bin\*.json" -Force -Recurse -ErrorAction SilentlyContinue - Invoke-WebRequest $Url_v2ray_A64 -OutFile "D:\v2ray-windows-A64.zip" - Expand-Archive -Path "D:\v2ray-windows-A64.zip" -DestinationPath "D:\v2raya-arm64-windows\bin\" - Move-Item -Path "D:\v2raya-arm64-windows\bin\*.dat" -Destination "D:\v2raya-arm64-windows\data\" - Remove-Item -Path "D:\v2raya-arm64-windows\bin\*.json" -Force -Recurse -ErrorAction SilentlyContinue - Copy-Item -Path ".\LICENSE" -Destination "D:\LICENSE.txt" - Copy-Item -Path ".\install\windows-inno\ChineseSimplified.isl" -Destination "D:\ChineseSimplified.isl" - ## Set Version - $((Get-Content -Path .\install\windows-inno\windows.iss).replace("TheRealVersion", "${{ steps.prep.outputs.VERSION }}")).Replace("TheRealArch", "x64compatible") | Out-File "D:\windows_x86_64.iss" - $((Get-Content -Path .\install\windows-inno\windows.iss).replace("TheRealVersion", "${{ steps.prep.outputs.VERSION }}")).Replace("TheRealArch", "arm64") | Out-File "D:\windows_arm64.iss" - ## Build Installer - Set-Location -Path "D:\" - & 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' "D:\windows_x86_64.iss" - & 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' "D:\windows_arm64.iss" - ## Rename to Friendly Name - cd $CurrentDir - Copy-Item -Path D:\installer_windows_inno_x64compatible.exe -Destination $CurrentDir\installer_windows_inno_x64_${{ steps.prep.outputs.VERSION }}.exe - Copy-Item -Path D:\installer_windows_inno_arm64.exe -Destination $CurrentDir\installer_windows_inno_arm64_${{ steps.prep.outputs.VERSION }}.exe - - name: Upload Artifact - uses: nanoufo/action-upload-artifacts-and-release-assets@v2 - with: - path: | - installer_windows_inno_x64_${{ steps.prep.outputs.VERSION }}.exe - installer_windows_inno_arm64_${{ steps.prep.outputs.VERSION }}.exe + Build_Windows_Installer: + needs: [Build_v2rayA_Binaries, Compute_Version] + uses: ./.github/workflows/_build_windows_installer.yml + with: + version: ${{ needs.Compute_Version.outputs.version }} diff --git a/v2raya/build-in-pwsh.ps1 b/v2raya/build-in-pwsh.ps1 index e43072e79f..5492a06946 100644 --- a/v2raya/build-in-pwsh.ps1 +++ b/v2raya/build-in-pwsh.ps1 @@ -20,16 +20,6 @@ Function Get-build-tools(){ } Function Build-v2rayA(){ - #Get OS - if ($env:GOOS -eq "windows") { - $v2rayaBin = "v2raya.exe" - } elseif ($env:GOOS -ne "windows") { - $v2rayaBin = "v2raya" - } elseif ($env:WinDir) { - $v2rayaBin = "v2raya.exe" - } else { - $v2rayaBin = "v2raya" - } #Get Paths $TerminalPath = Get-Item -LiteralPath ./ | ForEach-Object -Process { $_.FullName } $CurrentPath = $PSScriptRoot @@ -50,7 +40,14 @@ Function Build-v2rayA(){ yarn; yarn build #Build v2rayA Set-Location -Path "$CurrentPath/service" - go build -tags "with_gvisor" -ldflags "-X github.com/v2rayA/v2rayA/conf.Version=$version -s -w" -o "$CurrentPath/$v2rayaBin" + if ($env:GOOS -eq "windows") { + $v2rayaBin = "v2raya.exe" + } elseif ($env:WinDir -and !$env:GOOS) { + $v2rayaBin = "v2raya.exe" + } else { + $v2rayaBin = "v2raya" + } + go build -tags "tinytun" -ldflags "-X github.com/v2rayA/v2rayA/conf.Version=$version -s -w" -o "$CurrentPath/$v2rayaBin" Set-Location -Path "$TerminalPath" } diff --git a/v2raya/build.sh b/v2raya/build.sh index 805db40170..60ff7893d0 100755 --- a/v2raya/build.sh +++ b/v2raya/build.sh @@ -13,4 +13,4 @@ else fi # https://github.com/webpack/webpack/issues/14532#issuecomment-947012063 cd "$CurrentDir"/gui && yarn --ignore-engines && OUTPUT_DIR="$CurrentDir"/service/server/router/web yarn --ignore-engines build -cd "$CurrentDir"/service && CGO_ENABLED=0 go build -tags "with_gvisor" -ldflags "-X github.com/v2rayA/v2rayA/conf.Version=$version -s -w" -o "$CurrentDir"/v2raya \ No newline at end of file +cd "$CurrentDir"/service && CGO_ENABLED=0 go build -tags "tinytun" -ldflags "-X github.com/v2rayA/v2rayA/conf.Version=$version -s -w" -o "$CurrentDir"/v2raya \ No newline at end of file diff --git a/v2raya/gui/README.md b/v2raya/gui/README.md index c2858b5fc0..5f6cab4026 100644 --- a/v2raya/gui/README.md +++ b/v2raya/gui/README.md @@ -21,4 +21,4 @@ yarn lint ``` ### Customize configuration -See [Configuration Reference](https://cli.vuejs.org/config/). +See [Vite Configuration Reference](https://vitejs.dev/config/). diff --git a/v2raya/gui/index.html b/v2raya/gui/index.html new file mode 100644 index 0000000000..1fd2767198 --- /dev/null +++ b/v2raya/gui/index.html @@ -0,0 +1,17 @@ + + + + + + + + v2rayA + + + +
+ + + diff --git a/v2raya/gui/package.json b/v2raya/gui/package.json index 207494ed57..6daa272f9a 100644 --- a/v2raya/gui/package.json +++ b/v2raya/gui/package.json @@ -4,20 +4,17 @@ "private": true, "license": "GPL-3.0", "scripts": { - "serve": "vue-cli-service serve", - "build": "vue-cli-service build", - "lint": "vue-cli-service lint" - }, - "resolutions": { - "@achrinza/node-ipc": "^10.1.11", - "@achrinza/event-pubsub": "^5.0.3" + "serve": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint --ext .js,.vue src" }, "dependencies": { - "@achrinza/node-ipc": "^10.1.11", "@mdi/font": "^5.8.55", "@nuintun/qrcode": "^3.3.0", "@vue/babel-preset-app": "^4.2.2", "axios": "^0.21.1", + "bulma": "^0.9.4", "buefy": "^0.9.22", "clipboard": "^2.0.4", "dayjs": "^1.10.6", @@ -35,13 +32,10 @@ "webpack-iconfont-plugin-nodejs": "^1.0.16" }, "devDependencies": { + "@vitejs/plugin-vue2": "^2.3.3", + "@vitejs/plugin-vue2-jsx": "^1.1.1", "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", - "@vue/cli-plugin-babel": "~5.0.8", - "@vue/cli-plugin-eslint": "~5.0.8", - "@vue/cli-plugin-router": "~5.0.8", - "@vue/cli-plugin-vuex": "~5.0.8", - "@vue/cli-service": "~5.0.8", "@vue/eslint-config-prettier": "^5.0.0", "compression-webpack-plugin": "^10.0.0", "css-loader": "^5.2.0", @@ -50,10 +44,12 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^8.0.3", "highlight.js": "^11.4.0", + "autoprefixer": "^10.4.20", "prettier": "^2.4.1", "sass": "^1.19.0", "sass-loader": "^8.0.0", "terser-webpack-plugin": "^5.3.6", - "urijs": "^1.19.11" + "urijs": "^1.19.11", + "vite": "^4.5.14" } } diff --git a/v2raya/gui/src/App.vue b/v2raya/gui/src/App.vue index 75d567eef6..355c1acc37 100644 --- a/v2raya/gui/src/App.vue +++ b/v2raya/gui/src/App.vue @@ -57,6 +57,13 @@ {{ $t("common.log") }} + + + {{ themeSwitchLabel }} + @@ -84,7 +91,7 @@ - + @@ -134,6 +141,8 @@ export default { outbounds: ["proxy"], outboundDropdownHover: {}, updateOutboundDropdown: true, + themePreference: 'auto', + systemDark: window.matchMedia('(prefers-color-scheme: dark)').matches, }; }, computed: { @@ -153,9 +162,20 @@ export default { const lang = this.langs.find(l => l.flag === currentLang); return lang ? lang.code : "zh_CN"; }, + isDarkTheme() { + if (this.themePreference === 'dark') return true; + if (this.themePreference === 'light') return false; + return this.systemDark; + }, + themeSwitchLabel() { + if (this.themePreference === 'auto') return this.$t('common.autoTheme'); + if (this.themePreference === 'dark') return this.$t('common.darkTheme'); + return this.$t('common.lightTheme'); + }, }, mounted() { console.log("app created"); + this.initTheme(); let ba = localStorage.getItem("backendAddress"); if (ba) { let u = parseURL(ba); @@ -222,6 +242,9 @@ export default { if (this.ws) { this.ws.close(); } + if (this._darkMediaQuery && this._onSystemThemeChange) { + this._darkMediaQuery.removeEventListener('change', this._onSystemThemeChange); + } }, methods: { connectWsMessage() { @@ -268,6 +291,9 @@ export default { ) { this.observatory = msg; } + if (msg.type === "running_state" && msg.body && msg.body.running === false) { + this.$refs.nodeRef && this.$refs.nodeRef.notifyStopped(); + } }, handleOutboundDropdownActiveChange(active) { if (active) { @@ -513,6 +539,27 @@ export default { localStorage.removeItem("token"); this.$remount(); }, + initTheme() { + const stored = localStorage.getItem('theme'); + this.themePreference = (stored === 'dark' || stored === 'light') ? stored : 'auto'; + this._darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + this._onSystemThemeChange = (e) => { + this.systemDark = e.matches; + this.applyThemeClass(); + }; + this._darkMediaQuery.addEventListener('change', this._onSystemThemeChange); + this.applyThemeClass(); + }, + applyThemeClass() { + document.body.classList.toggle('theme-dark', this.isDarkTheme); + }, + toggleTheme() { + const order = ['auto', 'light', 'dark']; + const idx = order.indexOf(this.themePreference); + this.themePreference = order[(idx + 1) % order.length]; + localStorage.setItem('theme', this.themePreference); + this.applyThemeClass(); + }, handleClickLogs() { this.$buefy.modal.open({ parent: this, @@ -528,6 +575,7 @@ export default { diff --git a/v2raya/gui/src/components/modalLog.vue b/v2raya/gui/src/components/modalLog.vue index 8ffe2e53ca..2895e30107 100644 --- a/v2raya/gui/src/components/modalLog.vue +++ b/v2raya/gui/src/components/modalLog.vue @@ -193,6 +193,10 @@ export default { return "other"; }, detectSource(text) { + // TinyTun logs are prefixed with [tinytun] by the v2rayA log system. + if (text.includes("[tinytun]")) { + return "tinytun"; + } // 匹配 [xxx.go:123] 或 [xxxService] 格式 const match = text.match(/\[([^\]]+\.go|[A-Za-z]+Service|[A-Za-z]+\.[A-Za-z]+)(?::\d+)?\]/); if (match) { diff --git a/v2raya/gui/src/components/modalServer.vue b/v2raya/gui/src/components/modalServer.vue index af18e5b3e7..744c1a4092 100644 --- a/v2raya/gui/src/components/modalServer.vue +++ b/v2raya/gui/src/components/modalServer.vue @@ -370,6 +370,13 @@ ref="v2ray_key" v-model="v2ray.key" :placeholder="$t('configureServer.password') " label="Path" label-position="on-border"> + + + + + + + @@ -536,6 +543,13 @@ ref="v2ray_key" v-model="v2ray.key" :placeholder="$t('configureServer.password') + + + + + + + @@ -821,6 +835,7 @@ export default { name: "", protocol: "ss", impl: "", + backend: "", }, ssr: { method: "aes-128-cfb", @@ -849,6 +864,7 @@ export default { net: "tcp", obfs: "none" /* websocket */, protocol: "trojan", + backend: "", }, juicity: { name: "", @@ -1053,18 +1069,31 @@ export default { console.log(o); return o; } else if (url.toLowerCase().startsWith("ss://")) { - let decoded = Base64.decode(url.substring(url.indexOf("://") + 3)); - let parts = decoded.split("@"); - let methodAndPassword = parts[0].split(":"); - let serverAndPort = parts[1].split(":"); - + let u = parseURL(url); + let userinfo = u.username; + // Handle SIP002 format: ss://BASE64URL@host:port vs legacy ss://BASE64 + let method = "", password = ""; + try { + let decoded = Base64.decode(userinfo); + let idx = decoded.indexOf(":"); + if (idx > -1) { + method = decoded.substring(0, idx); + password = decoded.substring(idx + 1); + } + } catch (e) { + method = userinfo; + } + const ssPlugin = u.params.plugin || ""; return { - method: methodAndPassword[0], - password: methodAndPassword[1], - server: serverAndPort[0], - port: serverAndPort[1], - plugin: "", // Default empty, can be extended if plugin info is included + method: method, + password: password, + server: u.host, + port: u.port, + name: decodeURIComponent(u.hash || ""), + plugin: ssPlugin.split(";")[0] || "", + plugin_opts: ssPlugin.split(";").slice(1).join(";") || "", protocol: "ss", + backend: u.params["v2raya-backend"] || "", }; } else if (url.toLowerCase().startsWith("ssr://")) { url = Base64.decode(url.substr(6)); @@ -1114,6 +1143,7 @@ export default { ssCipher: "2022-blake3-aes-128-gcm", path: u.params.path || u.params.serviceName || "", protocol: "trojan", + backend: u.params["v2raya-backend"] || "", }; if (url.toLowerCase().startsWith("trojan-go://")) { console.log(u.params.encryption); @@ -1589,13 +1619,18 @@ export default { } coded = this.generateURL(this.v2ray); } else if (this.tabChoice === 1) { - // ss://BASE64(method:password)@server:port#name - const { method, password, server, port, name, plugin, plugin_opts } = this.ss; + // ss://BASE64(method:password)@server:port?plugin=...&v2raya-backend=...#name + const { method, password, server, port, name, plugin, plugin_opts, backend } = this.ss; let userinfo = btoa(`${method}:${password}`); - let url = `ss://${userinfo}@${server}:${port}`; + let params = []; if (plugin) { - url += `?plugin=${encodeURIComponent(plugin + (plugin_opts ? `;${plugin_opts}` : ""))}`; + params.push(`plugin=${encodeURIComponent(plugin + (plugin_opts ? `;${plugin_opts}` : ""))}`); } + if (backend) { + params.push(`v2raya-backend=${encodeURIComponent(backend)}`); + } + let url = `ss://${userinfo}@${server}:${port}`; + if (params.length) url += `?${params.join("&")}`; if (name) url += `#${encodeURIComponent(name)}`; coded = url; } else if (this.tabChoice === 2) { @@ -1608,11 +1643,12 @@ export default { let url = `ssr://${btoa(`${server}:${port}:${proto}:${method}:${obfs}:${pwdB64}/?remarks=${remarksB64}&protoparam=${protoParamB64}&obfsparam=${obfsParamB64}`)}`; coded = url; } else if (this.tabChoice === 3) { - // trojan://password@server:port?allowInsecure=1&sni=sni#name - const { password, server, port, allowInsecure, peer, name } = this.trojan; + // trojan://password@server:port?allowInsecure=1&sni=sni&v2raya-backend=...#name + const { password, server, port, allowInsecure, peer, name, backend } = this.trojan; let params = []; if (allowInsecure) params.push("allowInsecure=1"); if (peer) params.push(`sni=${encodeURIComponent(peer)}`); + if (backend) params.push(`v2raya-backend=${encodeURIComponent(backend)}`); let url = `trojan://${encodeURIComponent(password)}@${server}:${port}`; if (params.length) url += `?${params.join("&")}`; if (name) url += `#${encodeURIComponent(name)}`; diff --git a/v2raya/gui/src/components/modalSetting.vue b/v2raya/gui/src/components/modalSetting.vue index 5ad8f2e5d8..e12d73e211 100644 --- a/v2raya/gui/src/components/modalSetting.vue +++ b/v2raya/gui/src/components/modalSetting.vue @@ -63,8 +63,9 @@ - - + @@ -77,6 +78,21 @@ " outlined @click="handleClickTproxyWhiteIpGroups">{{ $t("operations.tproxyWhiteIpGroups") }} + + - + +
+ + + +
+ {{ iface.name }} +
+ {{ iface.addrs.join(', ') }} +
+
+
+
+ +
+
+ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -173,47 +206,7 @@

- - - - - - - - - - - {{ $t("operations.configure") }} - -

-
- - - - - - - - +