Update On Sun Apr 19 21:03:21 CEST 2026

This commit is contained in:
github-action[bot]
2026-04-19 21:03:22 +02:00
parent 3e632818f9
commit afd1803d58
191 changed files with 7023 additions and 9333 deletions
+1
View File
@@ -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
+17
View File
@@ -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
}
@@ -22,6 +22,7 @@ var (
bucketFakeip6 = []byte("fakeip6")
bucketETag = []byte("etag")
bucketSubscriptionInfo = []byte("subscriptioninfo")
bucketStorage = []byte("storage")
)
// CacheFile store and update the cache file
@@ -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())
}
}
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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=
+1
View File
@@ -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())
}
+59
View File
@@ -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)
}
+6 -6
View File
@@ -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"`
+167 -160
View File
@@ -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 {
+18
View File
@@ -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)
+16 -9
View File
@@ -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{
+17 -7
View File
@@ -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"
@@ -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",
+2 -2
View File
@@ -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"
}
+1 -1
View File
@@ -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",
+24 -24
View File
@@ -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:
@@ -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
+17
View File
@@ -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
}
@@ -22,6 +22,7 @@ var (
bucketFakeip6 = []byte("fakeip6")
bucketETag = []byte("etag")
bucketSubscriptionInfo = []byte("subscriptioninfo")
bucketStorage = []byte("storage")
)
// CacheFile store and update the cache file
@@ -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())
}
}
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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=
+1
View File
@@ -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())
}
+59
View File
@@ -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)
}
+6 -6
View File
@@ -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"`
+167 -160
View File
@@ -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 {
+18
View File
@@ -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)
+16 -9
View File
@@ -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{
@@ -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 )<br>" ..
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"),
"<font color='red'>" ..
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!") ..
"</font>")
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
s.description = "<font color='red'>" ..
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.") .. "<br>" ..
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!") .. "</font>"
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
@@ -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 节点,否则健康检查将无效。"
@@ -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))
@@ -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)
@@ -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)
@@ -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)
@@ -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"),
"<font color='red'>" ..
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!") ..
"</font>")
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
s.description = "<font color='red'>" ..
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.") .. "<br>" ..
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!") .. "</font>"
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
@@ -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)
@@ -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" })
@@ -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" })
@@ -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" })
@@ -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,
@@ -0,0 +1,149 @@
<%+cbi/valueheader%>
<%
-- Template Developers:
-- - lwb1978
-- - snowie2000
-- Copyright: copyright(c)20252027
-- 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,
}
%>
<script>
window.dl_dropdown_data = window.dl_dropdown_data || {};
window.dl_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
</script>
<div id="<%=cbid%>.main" class="dl-dropdown-container">
<select id="<%=cbid%>.ref" class="cbi-input-select" style="display:none !important;">
<option value>placeholder</option>
</select>
<!-- 已选节点显示区 -->
<div id="<%=cbid%>.selected" class="dl-selected-list"></div>
<!-- 模拟 listvalue 控件外观 -->
<div class="cbi-input-value cbi-input-select dl-dropdown-display" id="<%=cbid%>.display" tabindex="0">
<span id="<%=cbid%>.label" class="dl-dropdown-label">
<%=translate("-- Please choose --")%>
</span>
<span class="dl-arrow-down"></span>
</div>
<!-- 下拉面板 -->
<div id="<%=cbid%>.panel" class="cbi-dynamiclist-panel dl-dropdown-panel" style="display:none;">
<!-- 搜索框 -->
<div style="padding:8px;border-bottom:1px solid #f0f0f0;">
<input id="<%=cbid%>.search" class="cbi-input-text dl-dropdown-search" type="text" placeholder="🔍 <%:Search nodes...%>" inputmode="search" enterkeyhint="done" />
</div>
<!-- 列表容器 -->
<div id="<%=cbid%>.list" style="padding:8px;">
<!-- 首次点击 display 时由 JS 填充 -->
</div>
</div>
</div>
<script type="text/javascript">
//<![CDATA[
(function(){
const cbid = "<%=cbid%>";
const panel = document.getElementById(cbid + ".panel");
const display = document.getElementById(cbid + ".display");
const labelSpan = document.getElementById(cbid + ".label");
const searchInput = document.getElementById(cbid + ".search");
const listContainer = document.getElementById(cbid + ".list");
function init(){
const hidden = document.getElementById(cbid + ".hidden");
const data = window.dl_dropdown_data[cbid];
let arr = data.current_key;
if (!arr || arr.length === 0) return;
arr.forEach(key=>{
let label = "";
data.ungrouped.concat(...Object.values(data.groups)).forEach(v=>{
if (v.key == key) label = v.label;
});
if (label)
dl_add_item(cbid, key, label);
});
}
// 点击 display
display.addEventListener("click", function(e){
e.stopPropagation();
dl_render_dropdown_list(cbid,panel,listContainer,labelSpan,searchInput,display);
document.querySelectorAll(".cbi-dynamiclist-panel").forEach(p=>{
if (p !== panel) p.style.display = "none";
});
if (panel.style.display !== "none") {
dl_closePanel(cbid,panel,listContainer,searchInput);
} else {
dl_openPanel(cbid,display,panel,listContainer,searchInput);
}
});
dl_registerAdaptive(cbid);
init();
new Sortable(document.getElementById(cbid + ".selected"), {
animation: 150,
ghostClass: "dragging-row",
onEnd: function(){
dl_update_hidden(cbid);
}
});
})();
//]]>
</script>
<%+cbi/valuefooter%>
@@ -0,0 +1,654 @@
<%
-- Template Developers:
-- - lwb1978
-- - snowie2000
-- Copyright: copyright(c)20252027
-- Description: Passwall(2) UI template
-- It is the common part of the template and cannot be used independently
%>
<script src="<%=resource%>/view/passwall2/Sortable.min.js"></script>
<style>
/* 主下拉按钮的下箭头 */
.dl-arrow-down {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #666;
margin-left: 6px;
display: inline-block;
vertical-align: middle;
}
/* 组标题的右箭头(折叠) */
.dl-arrow-right {
width: 0;
height: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 5px solid #555;
display: inline-block;
vertical-align: middle;
}
/* 组标题的下箭头(展开) */
.dl-arrow-down-small {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #555;
display: inline-block;
vertical-align: middle;
}
/* 基础列表项样式 */
.cbi-dynamiclist-panel li[data-key] {
padding: 6px;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background-color 0.15s ease;
}
/* 鼠标悬停效果 - 使用透明度避免覆盖问题 */
.cbi-dynamiclist-panel li[data-key]:hover {
background-color: rgba(0, 123, 255, 0.1);
}
.dl-dropdown-container {
display: inline-block;
position: relative;
min-width: 220px;
white-space: nowrap;
}
@media (max-width: 1152px) {
.dl-dropdown-container {
white-space: normal;
}
}
@media (max-width: 600px) {
.dl-dropdown-container {
display: block;
white-space: normal;
}
}
.dl-dropdown-display {
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.dl-dropdown-label,
.dl-selected-label {
display: inline-block;
overflow: hidden;
white-space: nowrap;
width: 100%;
text-align: left;
}
.dl-dropdown-panel {
position: fixed;
top: 0;
left: 0;
z-index: 2147483647;
border: 1px solid #dcdcdc;
border-radius: 4px;
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
max-height: 50vh;
overflow: auto;
overscroll-behavior: contain;
}
.dl-dropdown-search {
width: 100%;
max-width: 100% !important;
min-width: 0 !important;
box-sizing: border-box;
padding: 6px;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.dl-group-title {
cursor: pointer;
padding: 6px;
border-radius: 4px;
display: flex;
align-items: center;
line-height: normal;
white-space: nowrap;
}
.dl-group-list {
list-style: none;
margin: 6px 0 0 8px;
padding: 0;
}
.dl-group-item {
padding: 6px;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left !important;
}
.dl-selected-list {
display: flex;
flex-direction: column;
gap: 5px;
}
.dl-remove {
margin-left: 8px;
cursor: pointer;
color: red;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
<script type="text/javascript">
//<![CDATA[
// css helper functions
function dl_camelToKebab(str) {
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
}
function dl_style2Css(styleDeclaration, properties) {
const cssRules = properties.map(prop => {
const kebabCaseProp = dl_camelToKebab(prop);[1, 5]
const value = styleDeclaration[prop]
if (value) {
return `${kebabCaseProp}: ${value};`
}
return ''
})
// Filter out any empty strings and join the rules
return cssRules.filter(Boolean).join(' ')
}
const dl_parseColorToRgba = (function() {
// Create canvas and context once (Closure)
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
return function(colorStr) {
if (!colorStr)
return null;
ctx.clearRect(0, 0, 1, 1);
// 2. Apply the color
ctx.fillStyle = colorStr;
// 3. Fill a single pixel
ctx.fillRect(0, 0, 1, 1);
// 4. Extract pixel data [R, G, B, A]
const data = ctx.getImageData(0, 0, 1, 1).data;
return {
r: data[0],
g: data[1],
b: data[2],
// Convert alpha from 0-255 to 0-1 (rounded to 3 decimal places)
a: Math.round((data[3] / 255) * 1000) / 1000
};
};
})();
// Helper to convert back to Hex (for output consistency)
function dl_rgbToHex(r, g, b) {
const toHex = (n) => {
const hex = Math.max(0, Math.min(255, n)).toString(16)
return hex.length === 1 ? '0' + hex : hex
}
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}
function dl_isTransparent(color) {
const cleanColor = dl_parseColorToRgba(color);
// check #RRGGBBAA for transparency
return !cleanColor || (cleanColor.a !== undefined && !cleanColor.a);
}
function dl_getColorSchema(color) {
const rgb = dl_parseColorToRgba(color);
if (!rgb) return 'unknown'; // Handle invalid colors
// Calculate YIQ brightness (human eye perception)
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
return brightness > 128 ? 'light' : 'dark';
}
function dl_lighter(color, amount) {
const rgb = dl_parseColorToRgba(color);
if (!rgb) return color;
// Add amount to each channel
const r = rgb.r + amount;
const g = rgb.g + amount;
const b = rgb.b + amount;
// Convert back to Hex (clamping happens inside rgbToHex)
return dl_rgbToHex(r, g, b);
}
function dl_darker(color, amount) {
const rgb = dl_parseColorToRgba(color);
if (!rgb) return color;
// Subtract amount from each channel
const r = rgb.r - amount;
const g = rgb.g - amount;
const b = rgb.b - amount;
return dl_rgbToHex(r, g, b);
}
// copy select styles
function dl_adaptiveStyle(cbid) {
const display = document.getElementById(cbid + ".display");
const hiddenRef = document.getElementById(cbid + ".ref");
const panel = document.getElementById(cbid + ".panel");
if (hiddenRef && display) {
const elOption = hiddenRef.getElementsByTagName("option")[0]
const styleSelect = window.getComputedStyle(hiddenRef)
const styleOption = window.getComputedStyle(elOption)
const styleBody = window.getComputedStyle(document.body)
const styleNode = document.createElement('style')
const styleNames = ["width", "color", "height", "padding", "margin", "lineHeight", "border", "borderRadius", "minWidth", "minHeight"]
if (styleSelect.borderBottomStyle !== "none") {
styleNames.push("borderBottomWidth", "borderBottomStyle", "borderBottomColor");
}
document.head.appendChild(styleNode)
// trace back from option -> select -> body for background color
const panelRadius = styleSelect.borderRadius;
const optionColor = !dl_isTransparent(styleOption.backgroundColor) ? styleOption.backgroundColor : !dl_isTransparent(styleSelect.backgroundColor) ? styleSelect.backgroundColor : styleBody.backgroundColor
const titleColor = dl_getColorSchema(optionColor) === "light" ? dl_darker(optionColor, 30) : dl_lighter(optionColor, 30)
const selectStyleCSS = [`#${CSS.escape(cbid + ".display")}, .dl-selected-item {`, dl_style2Css(styleSelect, styleNames), dl_style2Css(styleSelect, ["backgroundColor"]), "}"]
const optionStyleCSS = [`#${CSS.escape(cbid + ".panel")} {`, dl_style2Css(styleOption, styleNames), `background-color: ${optionColor};`, `border-radius: ${panelRadius};`, "}"]
const titleStyleCSS = [`#${CSS.escape(cbid + ".panel")} .dl-group-title {`, `background-color: ${titleColor} !important;`, "}"]
styleNode.textContent = [].concat(selectStyleCSS, optionStyleCSS, titleStyleCSS).join("\n")
}
}
function dl_idSafe(id) {
return id
.trim()
.replace(/\s+/g, "-")
.replace(/[\x00-\x1F\x7F]/g, "");
}
// 更新组内选中计数
function dl_updateGroupCounts(cbid, listContainer, searchInput) {
const groups = listContainer.querySelectorAll(".dl-group");
const isSearching = searchInput.value.trim() !== "";
groups.forEach(group => {
const gname = group.getAttribute("data-group");
const items = group.querySelectorAll("li[data-key]");
const span = document.getElementById("group-count-" + cbid + "-" + gname);
if (!span) return;
if (isSearching) {
// 搜索状态:显示匹配数量
let matchCount = 0;
items.forEach(li => {
if (li.style.display !== "none") matchCount++;
});
span.textContent = "(" + matchCount + "/" + items.length + ")";
if (matchCount > 0) {
span.style.color = "#28a745";
span.style.fontWeight = "600";
} else {
span.style.color = "#dc3545";
span.style.fontWeight = "normal";
}
} else {
// 默认状态
span.textContent = "(" + items.length + ")";
span.style.color = "";
span.style.fontWeight = "normal";
}
});
}
//搜索过滤器:按 name 或 label 做模糊匹配,搜索时自动展开所有组并隐藏不匹配条目
function dl_filterList(keyword, cbid, listContainer, searchInput) {
keyword = (keyword || "").toLowerCase().trim();
const topItems = listContainer.querySelectorAll("ul li[data-key]");
topItems.forEach(li=>{
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
if (!keyword || name.indexOf(keyword) !== -1) {
li.style.display = "block";
} else {
li.style.display = "none";
}
});
const groups = listContainer.querySelectorAll(".dl-group");
groups.forEach(group=>{
const items = group.querySelectorAll("li[data-key]");
let matchCount = 0;
items.forEach(li=>{
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
if (!keyword || name.indexOf(keyword) !== -1) {
li.style.display = "block";
matchCount++;
} else {
li.style.display = "none";
}
});
group.style.display = (matchCount === 0 && keyword !== "") ? "none" : "block";
const ul = group.querySelector(".dl-group-list");
const gname = group.getAttribute("data-group");
const arrow = document.getElementById("arrow-" + cbid + "-" + gname);
if (keyword) {
if (ul) ul.style.display = (matchCount > 0 ? "block" : "none");
if (arrow) arrow.className = (matchCount > 0 ? "dl-arrow-down-small" : "dl-arrow-right");
} else {
if (ul) ul.style.display = "none";
if (arrow) arrow.className = "dl-arrow-right";
}
});
dl_updateGroupCounts(cbid, listContainer, searchInput);
}
// 切换单个组(点击组标题)
function dl_toggleGroup(listContainer, cbid, g) {
g = dl_idSafe(g);
const group = listContainer.querySelector(".dl-group[data-group='" + g + "']");
if (!group) return;
const ul = group.querySelector(".dl-group-list");
const arrow = document.getElementById("arrow-" + cbid + "-" + g);
if (!ul) return;
const searchInput = document.getElementById(cbid + ".search");
const isSearching = searchInput?.value.trim() !== "";
const isExpanded = ul.style.display !== "none";
if (isExpanded) {
ul.style.display = "none";
if (arrow) arrow.className = "dl-arrow-right";
} else {
ul.style.display = "block";
if (arrow) arrow.className = "dl-arrow-down-small";
if (!isSearching) {
const allGroups = listContainer.querySelectorAll(".dl-group");
allGroups.forEach(otherGroup => {
if (otherGroup !== group) {
const otherUl = otherGroup.querySelector(".dl-group-list");
const otherGname = otherGroup.getAttribute("data-group");
const otherArrow = document.getElementById("arrow-" + cbid + "-" + otherGname);
if (otherUl) otherUl.style.display = "none";
if (otherArrow) otherArrow.className = "dl-arrow-right";
}
});
}
}
}
// 计算panel位置
function dl_repositionPanel(panel, display) {
if (!panel || panel.style.display === "none") return;
const rect = display.getBoundingClientRect();
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
panel.style.visibility = "hidden";
panel.style.display = "block";
panel.style.minHeight = "100px";
panel.style.maxHeight = Math.min(0.5*viewportHeight, 550) + "px";
const panelHeight = panel.offsetHeight;
const spaceBelow = viewportHeight - rect.bottom;
const spaceAbove = rect.top;
let top, isUp = false;
if (spaceBelow >= panelHeight) {
top = rect.bottom + 2;
isUp = false;
} else if (spaceAbove >= panelHeight) {
top = rect.top - panelHeight - 2;
isUp = true;
} else {
if (spaceBelow >= spaceAbove) {
top = Math.max(rect.bottom - 2, viewportHeight - panelHeight - 2);
isUp = false;
} else {
top = Math.min(rect.top - panelHeight + 2, 2);
isUp = true;
}
}
panel.style.left = rect.left + "px";
panel.style.top = top + "px";
const panelRect = panel.getBoundingClientRect();
const displayWidth = rect.width;
const remainingWidth = window.innerWidth - panelRect.left - 12;
const maxWidth = Math.max(displayWidth, Math.floor(remainingWidth));
panel.style.maxWidth = maxWidth + "px";
panel.style.minWidth = displayWidth + "px";
panel.style.width = "auto";
panel.style.visibility = "";
}
// 打开/关闭面板
function dl_openPanel(cbid, display, panel, listContainer, searchInput) {
if (!panel._moved) {
document.body.appendChild(panel);
panel._moved = true;
}
panel.style.display = "block";
dl_repositionPanel(panel, display);
// 失焦监听
const handler = function(e){
const target = e.target;
if (panel.style.display !== "none") {
if (!panel.contains(target) && !display.contains(target)) {
dl_closePanel(cbid, panel, listContainer, searchInput, display);
}
}
}
panel._docClickHandler = handler;
document.addEventListener("click", handler);
// 滚动 / resize 自动 reposition
let ticking = false;
const repositionHandler = function () {
if (ticking) return;
ticking = true;
requestAnimationFrame(function () {
ticking = false;
dl_repositionPanel(panel, display);
});
};
panel._repositionHandler = repositionHandler;
window.addEventListener("scroll", repositionHandler, true);
window.addEventListener("resize", repositionHandler);
}
function dl_closePanel(cbid, panel, listContainer, searchInput) {
panel.style.display = "none";
searchInput.value = "";
dl_filterList("", cbid, listContainer, searchInput);
// document click
if (panel._docClickHandler) {
document.removeEventListener("click", panel._docClickHandler);
panel._docClickHandler = null;
}
// scroll / resize
if (panel._repositionHandler) {
window.removeEventListener("scroll", panel._repositionHandler, true);
window.removeEventListener("resize", panel._repositionHandler);
panel._repositionHandler = null;
}
}
function dl_add_item(cbid, key, label) {
const container = document.getElementById(cbid + ".selected");
const hidden = document.getElementById(cbid + ".hidden");
if (container.querySelector(`[data-key="${key}"]`)) return;
const item = document.createElement("div");
item.className = "cbi-input-value cbi-input-select dl-dropdown-display dl-selected-item";
item.setAttribute("data-key", key);
item.innerHTML = `
<span class="dl-selected-label" style="cursor:grab;" title="${label}">${label}</span>
<span class="dl-remove">×</span>
`;
item.querySelector(".dl-remove").onclick = function(){
item.remove();
dl_update_hidden(cbid);
};
container.appendChild(item);
dl_update_hidden(cbid);
}
function dl_update_hidden(cbid) {
const main = document.getElementById(cbid + ".main");
const container = document.getElementById(cbid + ".selected");
main.querySelectorAll(`input[type="hidden"][name="${cbid}"]`).forEach(el => el.remove());
let items = container.querySelectorAll(".dl-selected-item");
items.forEach(el => {
let key = el.getAttribute("data-key");
let input = document.createElement("input");
input.type = "hidden";
input.name = cbid;
input.value = key;
main.insertBefore(input, container);
});
}
// 动态生成下拉框
window.dl_dropdown_rendered = {};
function dl_escape_html(s) {
return s.replace(/[&<>"']/g, c => ({
"&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;"
}[c]));
}
function dl_render_dropdown_list(cbid, panel, listContainer, labelSpan, searchInput, display) {
if (window.dl_dropdown_rendered[cbid]) return;
const data = window.dl_dropdown_data[cbid];
if (!data) return;
if (!listContainer) return;
let html = "";
// 无组项
if (data.ungrouped && data.ungrouped.length > 0) {
html += `<ul style="list-style:none;padding:0;margin:0 0 8px 0;">`;
data.ungrouped.forEach(item => {
html += `
<li data-key="${item.key}"
data-node-name="${dl_escape_html(item.label.toLowerCase())}"
class="list-item">
<span class="dl-item-label" style="margin-left:12px;">
${dl_escape_html(item.label)}
</span>
</li>`;
});
html += `</ul>`;
}
// 分组项
data.group_order.forEach(gname => {
const items = data.groups[gname];
html += `
<div class="dl-group" data-group="${dl_idSafe(gname)}" style="margin-bottom:8px;">
<div class="dl-group-title" data-group-name="${dl_idSafe(gname)}">
<span class="dl-arrow-right" id="arrow-${cbid}-${dl_idSafe(gname)}"></span>
<b style="margin-left:6px;">${dl_escape_html(gname)}</b>
<span id="group-count-${cbid}-${dl_idSafe(gname)}"
style="margin-left:8px;">(${items.length})</span>
</div>
<ul id="group-${cbid}-${dl_idSafe(gname)}" class="dl-group-list" style="display:none">
`;
items.forEach(item => {
html += `
<li data-key="${item.key}"
data-node-name="${dl_escape_html(item.label.toLowerCase())}"
class="dl-group-item">
<span class="dl-item-label" title="${dl_escape_html(item.label)}">
${dl_escape_html(item.label)}
</span>
</li>`;
});
html += `
</ul>
</div>
`;
});
listContainer.innerHTML = html;
window.dl_dropdown_rendered[cbid] = true;
dl_adaptiveStyle(cbid);
// 点击项(无组与组内项都使用 li[data-key]
listContainer.addEventListener("click", function(e){
let li = e.target;
while(li && li !== listContainer && !li.hasAttribute('data-key')) li = li.parentNode;
if(!li || li === listContainer) return;
const key = li.getAttribute('data-key') || "";
const text = li.querySelector(".dl-item-label")?.textContent || li.textContent || key;
//改值
dl_add_item(cbid, key, text);
labelSpan.innerText = "<%=translate("-- Please choose --")%>";
dl_closePanel(cbid,panel,listContainer,searchInput);
});
// 搜索功能
searchInput.addEventListener("input", function() {
dl_filterList(this.value, cbid, listContainer, searchInput);
dl_repositionPanel(panel, display);
});
searchInput.addEventListener('keydown', function(e) {
const isEnter = e.key === "Enter" || e.keyCode === 13;
if (!isEnter) return;
e.stopPropagation();
e.preventDefault();
searchInput.blur();
});
// 切换组
listContainer.querySelectorAll(".dl-group-title").forEach(title => {
title.addEventListener("click", function() {
const g = this.closest(".dl-group")?.getAttribute("data-group");
if (g) {
dl_toggleGroup(listContainer, cbid, g);
dl_repositionPanel(panel, display);
}
});
});
// 防止 panel 惯性滚动穿透
panel.addEventListener('wheel', function (e) {
const deltaY = e.deltaY;
const scrollTop = panel.scrollTop;
const scrollHeight = panel.scrollHeight;
const clientHeight = panel.clientHeight;
const isAtTop = scrollTop === 0;
const isAtBottom = scrollTop + clientHeight >= scrollHeight;
if (deltaY < 0 && isAtTop) {
e.preventDefault();
return;
}
if (deltaY > 0 && isAtBottom) {
e.preventDefault();
return;
}
e.stopPropagation();
}, { passive: false });
}
const dl_adaptiveControls = new Set();
function dl_registerAdaptive(cbid) {
dl_adaptiveControls.add(cbid);
dl_adaptiveStyle(cbid);
}
let dl_adaptiveTicking = false;
window.addEventListener("resize", () => {
if (!dl_adaptiveTicking) {
dl_adaptiveTicking = true;
requestAnimationFrame(() => {
dl_adaptiveControls.forEach(cbid => {
dl_adaptiveStyle(cbid);
});
dl_adaptiveTicking = false;
});
}
});
//]]>
</script>
@@ -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}
}
@@ -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() {
@@ -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)))
@@ -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)
@@ -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
;;
@@ -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
;;
@@ -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("&amp;", "&"):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
+19 -19
View File
@@ -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]]
@@ -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;
+1 -1
View File
@@ -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)
+4 -4
View File
@@ -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
+8 -8
View File
@@ -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=
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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_
@@ -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 )<br>" ..
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"),
"<font color='red'>" ..
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!") ..
"</font>")
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
s.description = "<font color='red'>" ..
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.") .. "<br>" ..
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!") .. "</font>"
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
@@ -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 节点,否则健康检查将无效。"
@@ -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))
@@ -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)
+85
View File
@@ -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 }}
+139
View File
@@ -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
+39
View File
@@ -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
+115
View File
@@ -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
+30 -316
View File
@@ -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
+24 -45
View File
@@ -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}
+35 -321
View File
@@ -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
bash install/aur/deploy.sh
+21 -188
View File
@@ -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 }}
+8 -11
View File
@@ -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"
}
+1 -1
View File
@@ -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
cd "$CurrentDir"/service && CGO_ENABLED=0 go build -tags "tinytun" -ldflags "-X github.com/v2rayA/v2rayA/conf.Version=$version -s -w" -o "$CurrentDir"/v2raya
+1 -1
View File
@@ -21,4 +21,4 @@ yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
See [Vite Configuration Reference](https://vitejs.dev/config/).
+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="icon" href="./favicon.ico" />
<title>v2rayA</title>
</head>
<body>
<noscript>
<strong>We're sorry but v2rayA-GUI doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+10 -14
View File
@@ -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"
}
}
+52 -4
View File
@@ -57,6 +57,13 @@
<i class="iconfont icon-info" style="font-size: 1.25em"></i>
{{ $t("common.log") }}
</b-navbar-item>
<b-navbar-item tag="a" @click.native="toggleTheme">
<i
:class="themePreference === 'auto' ? 'mdi mdi-theme-light-dark' : (isDarkTheme ? 'mdi mdi-weather-sunny' : 'mdi mdi-weather-night')"
style="font-size: 1.25em"
></i>
{{ themeSwitchLabel }}
</b-navbar-item>
<b-dropdown position="is-bottom-left" aria-role="menu" class="langdropdown">
<a slot="trigger" class="navbar-item" role="button">
<i class="iconfont icon-earth" style="font-size: 1.25em; margin-right: 4px"></i>
@@ -84,7 +91,7 @@
</b-dropdown>
</template>
</b-navbar>
<node v-model="runningState" :outbound="outboundName" :observatory="observatory" />
<node ref="nodeRef" v-model="runningState" :outbound="outboundName" :observatory="observatory" />
<b-modal :active.sync="showCustomPorts" has-modal-card trap-focus aria-role="dialog" aria-modal
class="modal-custom-ports">
<ModalCustomAddress @close="showCustomPorts = false" />
@@ -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 {
<style lang="scss">
@import "assets/iconfont/fonts/font.css";
@import "assets/scss/reset.scss";
@import "assets/scss/dark-theme.scss";
</style>
<style lang="scss" scoped>
@@ -557,7 +605,7 @@ export default {
<style lang="scss">
html {
// &::-webkit-scrollbar {
// //
// // remove annoying scrollbar
// display: none;
// }
@@ -571,7 +619,7 @@ html {
@media screen and (max-width: 1023px) {
.dropdown.is-mobile-modal .dropdown-menu {
// modal
// fix modal blur issues
left: 0 !important;
right: 0 !important;
margin: auto;
@@ -580,7 +628,7 @@ html {
}
.dropdown-item:focus {
// outline
// remove ugly outline
outline: none !important;
}
+1 -1
View File
@@ -1,5 +1,5 @@
import CONST from "./const.js";
const URI = require("urijs");
import URI from "urijs";
function _locateServer(touch, whichServer) {
let ind = whichServer.id - 1;
+3 -3
View File
@@ -1,5 +1,5 @@
// Import Bulma's core
@import "~bulma/sass/utilities/_all";
@import "bulma/sass/utilities/_all.sass";
// Set your colors
$green: $success;
@@ -47,5 +47,5 @@ $link-invert: $primary-invert;
$link-focus-border: $primary;
// Import Bulma and Buefy styles
@import "~bulma";
@import "~buefy/src/scss/buefy";
@import "bulma/bulma.sass";
@import "buefy/src/scss/buefy.scss";
+517
View File
@@ -0,0 +1,517 @@
body.theme-dark {
/* Material 3 dark roles */
--md-bg: #1c1b1f;
--md-surface-container: #211f26;
--md-surface-variant: #49454f;
--md-outline: #938f99;
--md-on-bg: #e6e1e5;
--md-on-surface: #e6e1e5;
--md-on-surface-variant: #cac4d0;
--md-primary: #d0bcff;
--md-primary-container: #4f378b;
--md-on-primary-container: #eaddff;
background-color: var(--md-bg);
color: var(--md-on-bg);
#app,
.hero,
.hero-body,
.container,
.b-table,
.table-wrapper,
.b-table .table,
table.table {
background-color: var(--md-bg) !important;
color: var(--md-on-bg);
}
.navbar.is-light,
.navbar-dropdown,
.dropdown-content,
.navbar-menu {
background-color: var(--md-surface-container) !important;
color: var(--md-on-surface);
}
.navbar.is-fixed-top.has-shadow,
.navbar.is-fixed-top.has-shadow::after {
box-shadow: 0 2px 0 0 var(--md-surface-variant);
}
.navbar.is-light {
.navbar-brand > .navbar-item,
.navbar-brand .navbar-link,
.navbar-item,
.navbar-link {
color: var(--md-on-surface) !important;
background-color: transparent !important;
}
.navbar-item:hover,
.navbar-link:hover,
.navbar-item:focus,
.navbar-link:focus,
.navbar-item:focus-within,
.navbar-link:focus-within,
.navbar-item.is-active,
.navbar-link.is-active {
background-color: transparent !important;
color: var(--md-primary) !important;
}
}
.navbar-end > a.navbar-item:hover,
.navbar-end > a.navbar-item:focus,
.navbar-end > a.navbar-item:focus-within,
.langdropdown .navbar-item:hover,
.menudropdown .navbar-item:hover,
.langdropdown .navbar-item:focus,
.menudropdown .navbar-item:focus,
.langdropdown .navbar-item:focus-within,
.menudropdown .navbar-item:focus-within {
background-color: transparent !important;
color: var(--md-primary) !important;
}
.navbar-end > a.navbar-item,
.langdropdown .navbar-item,
.menudropdown .navbar-item {
color: var(--md-on-surface) !important;
}
.dropdown-item,
.dropdown-content .dropdown-item {
color: var(--md-on-surface);
background-color: transparent !important;
}
.dropdown .dropdown-menu .has-link a:hover,
a.dropdown-item:hover,
button.dropdown-item:hover {
background-color: #2b2930 !important;
color: var(--md-on-surface) !important;
}
.dropdown-divider,
hr.dropdown-divider {
background-color: var(--md-surface-variant);
}
.navbar-burger {
color: var(--md-on-surface);
span {
background-color: var(--md-on-surface) !important;
}
&:hover {
background-color: #2b2930;
}
}
.box,
.card,
.modal-card,
.modal-card-head,
.modal-card-body,
.modal-card-foot,
.sidebar-content,
.message,
.message-body,
.message-header {
background-color: var(--md-surface-container) !important;
color: var(--md-on-surface) !important;
border-color: var(--md-surface-variant);
}
.modal-card-title,
.message .title,
.message .subtitle,
.label,
.help,
.content,
.content p,
.title,
.subtitle,
.modal-card-body p {
color: var(--md-on-surface);
}
.modal-card-head .delete::before,
.modal-card-head .delete::after {
color: var(--md-on-surface);
}
.input,
.textarea,
.select select,
.taginput .taginput-container {
background-color: var(--md-surface-container);
border-color: var(--md-surface-variant);
color: var(--md-on-surface);
}
.input::placeholder,
.textarea::placeholder {
color: var(--md-on-surface-variant);
}
.select {
select option {
background-color: #2b2930;
color: var(--md-on-surface);
}
&:not(.is-multiple):not(.is-loading)::after {
border-color: var(--md-on-surface-variant) !important;
}
&:not(.is-multiple):not(.is-loading):hover::after,
&:not(.is-multiple):not(.is-loading).is-focused::after {
border-color: var(--md-primary) !important;
}
}
.field.is-floating-label {
.label {
color: var(--md-on-surface-variant);
&:before {
background-color: var(--md-surface-container);
}
}
}
.button {
&.is-primary,
&.is-primary:hover,
&.is-primary:focus,
&.is-primary.is-focused,
&.is-primary.is-hovered,
&.is-primary:active,
&.is-primary.is-active {
background-color: var(--md-primary-container) !important;
border-color: var(--md-primary-container) !important;
color: var(--md-on-primary-container) !important;
box-shadow: none !important;
}
&:hover,
&:focus,
&.is-hovered,
&.is-focused {
box-shadow: none !important;
}
&:focus:not(:active),
&.is-focused:not(:active) {
border-color: var(--md-outline) !important;
}
&:not(.is-primary):not(.is-link):not(.is-info):not(.is-success):not(.is-warning):not(.is-danger):not(.is-delete),
&.is-light,
&.is-white {
background-color: #2b2930;
border-color: var(--md-surface-variant);
color: var(--md-on-surface);
}
&:not(.is-primary):not(.is-link):not(.is-info):not(.is-success):not(.is-warning):not(.is-danger):not(.is-delete):hover,
&.is-light:hover,
&.is-white:hover {
background-color: #35333a;
border-color: var(--md-outline);
}
&.is-outlined {
background-color: transparent;
border-color: var(--md-outline);
color: var(--md-on-surface);
&:hover,
&:focus {
background-color: #35333a;
border-color: var(--md-on-surface-variant);
color: var(--md-on-surface);
}
}
&.is-warning {
background-color: #ffb300;
border-color: #ffb300;
color: #2a1900;
&.is-outlined {
background-color: transparent;
border-color: #ffb300;
color: #ffb300;
&:hover,
&:focus {
background-color: #ffb300;
color: #2a1900;
}
}
}
&.is-info:not(.is-outlined) {
background-color: #4fc3d9;
border-color: #4fc3d9;
color: #00363d;
}
&.is-success:not(.is-outlined) {
background-color: #7eaee6;
border-color: #7eaee6;
color: #0a305f;
}
&.is-danger:not(.is-outlined),
&.is-delete {
background-color: #ff8a80 !important;
border-color: #ff8a80 !important;
color: #690005 !important;
}
&.is-info.is-outlined {
border-color: #4fc3d9 !important;
color: #4fc3d9 !important;
&:hover,
&:focus {
background-color: #4fc3d9 !important;
border-color: #4fc3d9 !important;
color: #1c1b1f !important;
}
}
&.is-success.is-outlined {
border-color: #7eaee6 !important;
color: #7eaee6 !important;
&:hover,
&:focus {
background-color: #7eaee6 !important;
border-color: #7eaee6 !important;
color: #1c1b1f !important;
}
}
&.is-danger.is-outlined,
&.is-delete.is-outlined {
background-color: transparent !important;
border-color: #ff8a80 !important;
color: #ff8a80 !important;
&:hover,
&:focus {
background-color: #ff8a80 !important;
border-color: #ff8a80 !important;
color: #1c1b1f !important;
}
}
}
.button.is-outlined[style*="0, 0, 0, 0.75"] {
color: var(--md-on-surface) !important;
border-color: var(--md-surface-variant) !important;
}
.checkbox.is-primary input:checked + .check,
.b-checkbox.checkbox.is-primary input:checked + .check,
.checkbox.button.is-primary input:checked + .check,
.switch input[type="checkbox"]:checked + .check {
background: var(--md-primary-container) !important;
border-color: var(--md-primary-container) !important;
box-shadow: none !important;
color: var(--md-on-primary-container) !important;
}
.checkbox.is-primary input:checked + .check::before,
.b-checkbox.checkbox.is-primary input:checked + .check::before,
.checkbox.button.is-primary input:checked + .check::before {
color: var(--md-on-primary-container) !important;
}
.b-checkbox.checkbox input[type="checkbox"]:checked + .check.is-primary {
background:
var(--md-primary-container)
url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23eaddff' d='M.04.627.146.52.43.804.323.91zm.177.177L.854.167.96.273.323.91z'/%3E%3C/svg%3E")
no-repeat 50% !important;
border-color: var(--md-primary-container) !important;
}
.b-checkbox.checkbox input[type="checkbox"]:focus:checked + .check.is-primary {
box-shadow: 0 0 0.35em rgba(79, 55, 139, 0.65) !important;
}
.b-checkbox.checkbox:hover input[type="checkbox"]:not(:disabled) + .check.is-primary {
border-color: var(--md-primary-container) !important;
}
/* Keep primary checkbox border non-yellow in all checked/hover permutations */
.b-checkbox.checkbox input[type="checkbox"]:checked + .check.is-primary,
.b-checkbox.checkbox:hover input[type="checkbox"]:checked + .check.is-primary,
.b-checkbox.checkbox:hover input[type="checkbox"]:not(:disabled):checked + .check.is-primary {
border-color: var(--md-primary-container) !important;
}
.tabs {
li a {
border-color: var(--md-surface-variant);
color: var(--md-on-surface-variant);
background-color: transparent !important;
}
li:not(.is-active) a:hover {
background-color: #2b2930;
color: var(--md-on-surface);
}
}
.tabs.main-tabs li.is-active a,
.main-tabs .tabs li.is-active a,
.tabs.is-toggle li.is-active a,
.tabs.is-toggle-rounded li.is-active a,
.main-tabs.tabs.is-toggle-rounded li.is-active a {
background-color: var(--md-primary-container) !important;
border-color: var(--md-primary-container) !important;
color: var(--md-on-primary-container) !important;
}
.b-tabs .tabs.is-toggle li:not(.is-active) a:focus,
.b-tabs .tabs.is-toggle-rounded li:not(.is-active) a:focus,
.tabs.is-toggle li:not(.is-active) a:focus,
.tabs.is-toggle-rounded li:not(.is-active) a:focus {
border-color: var(--md-primary-container) !important;
}
.tabs.is-boxed li a {
background-color: var(--md-surface-container) !important;
border-color: var(--md-surface-variant) !important;
color: var(--md-on-surface-variant) !important;
}
.tabs.is-boxed li.is-active a,
.tabs.is-boxed li a:hover {
background-color: #2b2930 !important;
border-color: var(--md-outline) !important;
color: var(--md-on-surface) !important;
}
.is-twitter .is-active a {
color: var(--md-primary) !important;
}
.b-table .table,
table.table {
th,
td {
background-color: var(--md-bg) !important;
color: var(--md-on-surface) !important;
border-color: var(--md-bg) !important;
}
thead th {
border-bottom-color: var(--md-surface-variant) !important;
color: var(--md-on-bg) !important;
}
thead td {
color: var(--md-on-bg) !important;
}
thead th.is-current-sort {
background-color: transparent !important;
color: var(--md-primary) !important;
border-bottom-color: var(--md-primary) !important;
box-shadow: inset 0 -2px 0 var(--md-primary);
}
tr.is-unselectable td {
background-color: inherit !important;
color: inherit !important;
border-bottom-color: var(--md-surface-variant) !important;
}
}
.b-sidebar.node-status-sidebar > .sidebar-content.is-fixed,
.b-sidebar.node-status-sidebar-reduced > .sidebar-content.is-fixed {
background-color: var(--md-surface-container) !important;
color: var(--md-on-surface);
}
.rules::-webkit-scrollbar,
.modal-card-body::-webkit-scrollbar {
width: 0.85em;
height: 0.85em;
}
.rules::-webkit-scrollbar-track,
.modal-card-body::-webkit-scrollbar-track,
&::-webkit-scrollbar-track,
nav::-webkit-scrollbar-track {
background-color: var(--md-surface-container);
}
.rules::-webkit-scrollbar-thumb,
.modal-card-body::-webkit-scrollbar-thumb,
&::-webkit-scrollbar-thumb,
nav::-webkit-scrollbar-thumb {
background-color: var(--md-surface-variant);
border-radius: 6px;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
background: transparent !important;
color: var(--md-on-surface) !important;
filter: invert(0.75);
}
a:not(.navbar-item):not(.dropdown-item):not(.button),
.is-link,
code {
color: var(--md-primary) !important;
}
.modal-background {
background-color: rgba(0, 0, 0, 0.62);
}
/* Final high-priority overrides for Buefy/Bulma defaults */
.button.is-primary,
.button.is-primary:hover,
.button.is-primary:focus,
.button.is-primary.is-focused,
.button.is-primary.is-hovered,
.button.is-primary:active,
.button.is-primary.is-active {
background-color: var(--md-primary-container) !important;
border-color: var(--md-primary-container) !important;
color: var(--md-on-primary-container) !important;
box-shadow: none !important;
}
.checkbox.is-primary input:checked + .check,
.b-checkbox.checkbox.is-primary input:checked + .check,
.checkbox.button.is-primary input:checked + .check,
.b-checkbox.checkbox.button.is-primary input:checked + .check,
.switch input[type="checkbox"]:checked + .check {
background: var(--md-primary-container) !important;
border-color: var(--md-primary-container) !important;
color: var(--md-on-primary-container) !important;
box-shadow: none !important;
}
.checkbox.is-primary input:checked + .check::before,
.b-checkbox.checkbox.is-primary input:checked + .check::before,
.checkbox.button.is-primary input:checked + .check::before,
.b-checkbox.checkbox.button.is-primary input:checked + .check::before {
color: var(--md-on-primary-container) !important;
}
.b-tabs .tabs.is-toggle li:not(.is-active) a:hover,
.b-tabs .tabs.is-toggle li:not(.is-active) a:focus,
.b-tabs .tabs.is-toggle-rounded li:not(.is-active) a:hover,
.b-tabs .tabs.is-toggle-rounded li:not(.is-active) a:focus,
.tabs.is-toggle li:not(.is-active) a:hover,
.tabs.is-toggle li:not(.is-active) a:focus,
.tabs.is-toggle-rounded li:not(.is-active) a:hover,
.tabs.is-toggle-rounded li:not(.is-active) a:focus {
border-color: var(--md-primary-container) !important;
}
}
+1 -1
View File
@@ -68,7 +68,7 @@ html{
@font-face {
font-family: sarasa inter regular;
src: url("~@/assets/font/Inter-Regular.woff2")
src: url("/src/assets/font/Inter-Regular.woff2")
}
.code-font {
+204 -55
View File
@@ -1,35 +1,86 @@
<template>
<div class="modal-card" style="max-width: 450px; margin: auto">
<div class="modal-card dns-setting-modal" style="width: auto; min-width: 680px; max-width: 95vw; margin: auto">
<header class="modal-card-head">
<p class="modal-card-title">
{{ $t("dns.title") }}
</p>
<p class="modal-card-title">{{ $t("dns.title") }}</p>
<a
class="help-link"
href="https://www.v2fly.org/config/dns.html"
target="_blank"
rel="noopener noreferrer"
:title="$t('dns.helpTooltip')"
>
<b-icon icon=" iconfont icon-help-circle-outline" size="is-small" />
{{ $t("dns.help") }}
</a>
</header>
<section class="modal-card-body">
<b-message type="is-info" class="after-line-dot5">
<p>{{ $t("dns.messages.0") }}</p>
</b-message>
<b-field :label="$t('dns.internalQueryServers')">
<b-input
v-model="internal"
type="textarea"
custom-class="full-min-height horizon-scroll code-font"
/>
</b-field>
<b-field :label="$t('dns.externalQueryServers')">
<b-input
v-model="external"
type="textarea"
custom-class="full-min-height horizon-scroll code-font"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</b-field>
<b-message type="is-danger" class="after-line-dot5">
<p>{{ $t("dns.messages.1") }}</p>
</b-message>
<!-- DNS rules table -->
<div class="dns-table">
<!-- Header row -->
<div class="dns-row dns-header">
<div class="col-server">{{ $t("dns.colServer") }}</div>
<div class="col-domains">{{ $t("dns.colDomains") }}</div>
<div class="col-outbound">{{ $t("dns.colOutbound") }}</div>
<div class="col-actions"></div>
</div>
<!-- Rule rows -->
<div
v-for="(rule, index) in rules"
:key="index"
class="dns-row dns-data-row"
>
<div class="col-server">
<b-input
v-model="rule.server"
size="is-small"
:placeholder="$t('dns.serverPlaceholder')"
class="code-font"
/>
</div>
<div class="col-domains">
<b-input
v-model="rule.domains"
type="textarea"
size="is-small"
:placeholder="$t('dns.domainsPlaceholder')"
class="code-font dns-domains-input"
rows="2"
/>
</div>
<div class="col-outbound">
<b-select v-model="rule.outbound" size="is-small" expanded>
<option value="direct">direct</option>
<option
v-for="out in outbounds"
:key="out"
:value="out"
>{{ out }}</option>
</b-select>
</div>
<div class="col-actions">
<b-button
size="is-small"
type="is-danger"
icon-left=" iconfont icon-delete"
@click="removeRule(index)"
/>
</div>
</div>
</div>
<div class="dns-add-row">
<b-button
size="is-small"
type="is-primary"
@click="addRule"
>+ {{ $t("dns.addRule") }}</b-button>
<b-button
size="is-small"
@click="resetDefault"
style="margin-left: 8px"
>{{ $t("dns.resetDefault") }}</b-button>
</div>
</section>
<footer class="modal-card-foot flex-end">
<button class="button" @click="$emit('close')">
@@ -45,59 +96,157 @@
<script>
import { handleResponse } from "@/assets/js/utils";
const DEFAULT_RULES = [
{ server: "localhost", domains: "geosite:private", outbound: "direct" },
{ server: "223.5.5.5", domains: "geosite:cn", outbound: "direct" },
{ server: "8.8.8.8", domains: "", outbound: "proxy" },
];
export default {
name: "ModalDnsSetting",
data: () => ({
internal: "",
external: "",
rules: DEFAULT_RULES.map((r) => ({ ...r })),
outbounds: ["proxy"],
}),
created() {
this.$axios({
url: apiRoot + "/dnsList",
}).then((res) => {
// Load available outbounds
this.$axios({ url: apiRoot + "/outbounds" }).then((res) => {
if (res.data && res.data.data && res.data.data.outbounds) {
this.outbounds = res.data.data.outbounds;
}
});
// Load current DNS rules
this.$axios({ url: apiRoot + "/dnsRules" }).then((res) => {
handleResponse(res, this, () => {
if (res.data.data.internal) {
let internal = res.data.data.internal;
let external = res.data.data.external;
console.log(res.data.data);
if (internal.length) {
this.internal = internal.join("\n");
}
if (external.length) {
this.external = external.join("\n");
}
if (res.data.data && res.data.data.rules && res.data.data.rules.length > 0) {
this.rules = res.data.data.rules.map((r) => ({
server: r.server || "",
domains: r.domains || "",
outbound: r.outbound || "direct",
}));
}
});
});
},
methods: {
addRule() {
this.rules.push({ server: "", domains: "", outbound: "direct" });
},
removeRule(index) {
this.rules.splice(index, 1);
},
resetDefault() {
this.rules = DEFAULT_RULES.map((r) => ({ ...r }));
},
handleClickSubmit() {
const validRules = this.rules.filter((r) => r.server.trim() !== "");
if (validRules.length === 0) {
this.$buefy.toast.open({
message: this.$t("dns.errNoRules"),
type: "is-danger",
position: "is-top",
});
return;
}
this.$axios({
url: apiRoot + "/dnsList",
url: apiRoot + "/dnsRules",
method: "put",
data: {
internal: this.internal,
external: this.external,
},
data: validRules,
}).then((res) => {
handleResponse(res, this, () => {
this.$emit("close");
});
});
},
beforeAdding(tag) {
return /^\d+$/.test(tag) || /^\d+:\d+$/.test(tag);
},
},
};
</script>
<style lang="scss" scoped>
.after-line-dot5 {
font-size: 14px;
.dns-setting-modal {
.dns-info-msg {
margin-bottom: 12px;
}
p {
font-size: 14px;
.modal-card-head {
display: flex;
align-items: center;
}
.help-link {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #888;
margin-left: auto;
text-decoration: none;
white-space: nowrap;
&:hover {
color: #3273dc;
}
}
.dns-table {
border: 1px solid #dbdbdb;
border-radius: 4px;
overflow: hidden;
}
.dns-row {
display: grid;
grid-template-columns: 220px 1fr 120px 42px;
gap: 0;
align-items: start;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
> div {
padding: 6px 8px;
}
}
.dns-header {
background: #f8f8f8;
font-size: 12px;
font-weight: 600;
color: #555;
align-items: center;
> div {
padding: 8px 10px;
}
}
.dns-data-row {
background: #fff;
&:hover {
background: #fafafa;
}
}
.col-actions {
display: flex;
align-items: flex-start;
padding-top: 8px;
justify-content: center;
}
.dns-domains-input ::v-deep textarea {
min-height: 52px;
resize: vertical;
font-family: monospace;
font-size: 12px;
}
.dns-add-row {
margin-top: 12px;
display: flex;
align-items: center;
}
}
</style>
+4
View File
@@ -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) {
+52 -16
View File
@@ -370,6 +370,13 @@ ref="v2ray_key" v-model="v2ray.key" :placeholder="$t('configureServer.password')
" label="Path" label-position="on-border">
<b-input ref="ss_path" v-model="ss.path" placeholder="/" expanded />
</b-field>
<b-field :label="$t('setting.nodeBackend')" label-position="on-border">
<b-select v-model="ss.backend" expanded>
<option value="">{{ $t("setting.options.backendSystemDefault") }}</option>
<option value="daeuniverse">{{ $t("setting.options.backendDaeuniverse") }}</option>
<option value="v2ray">{{ $t("setting.options.backendV2ray") }}</option>
</b-select>
</b-field>
</b-tab-item>
<b-tab-item label="SSR">
<b-field label="Name" label-position="on-border">
@@ -536,6 +543,13 @@ ref="v2ray_key" v-model="v2ray.key" :placeholder="$t('configureServer.password')
<b-field v-show="trojan.net === 'grpc'" label="Service Name" label-position="on-border">
<b-input ref="trojan_service_name" v-model="trojan.path" type="text" expanded />
</b-field>
<b-field :label="$t('setting.nodeBackend')" label-position="on-border">
<b-select v-model="trojan.backend" expanded>
<option value="">{{ $t("setting.options.backendSystemDefault") }}</option>
<option value="daeuniverse">{{ $t("setting.options.backendDaeuniverse") }}</option>
<option value="v2ray">{{ $t("setting.options.backendV2ray") }}</option>
</b-select>
</b-field>
</b-tab-item>
<b-tab-item label="Juicity">
@@ -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)}`;
+189 -106
View File
@@ -63,8 +63,9 @@
<b-select v-model="transparentType" expanded>
<option v-show="!lite && os === 'linux'" value="redirect">redirect</option>
<option v-show="!lite && os === 'linux'" value="tproxy">tproxy</option>
<option v-show="!lite" value="gvisor_tun">gvisor tun</option>
<option v-show="!lite" value="system_tun">system tun</option>
<option v-show="!lite" value="tun" :disabled="!tinytunSupported">
tun (TinyTun){{ !tinytunSupported ? ' — ' + $t("setting.options.notIntegrated") : '' }}
</option>
<option v-show="!(isRoot && (os === 'linux' || os === 'darwin'))" value="system_proxy">system proxy</option>
</b-select>
@@ -77,6 +78,21 @@
" outlined @click="handleClickTproxyWhiteIpGroups">{{ $t("operations.tproxyWhiteIpGroups") }}
</b-button>
</template>
<template v-if="transparentType === 'tun' && tinytunSupported">
<b-tooltip type="is-dark" multilined :label="$t('setting.messages.tunAutoRoute')" position="is-top">
<b-checkbox-button v-model="tunAutoRoute" :native-value="true" style="position: relative; left: -1px">
{{ $t("setting.tunAutoRoute") }}
</b-checkbox-button>
</b-tooltip>
<b-button v-if="!tunAutoRoute" style="
margin-left: 0;
border-bottom-left-radius: 0;
border-top-left-radius: 0;
color: rgba(0, 0, 0, 0.75);
" outlined @click="handleClickTunRouteScript">{{ $t("operations.configureTunRouteScript") }}
</b-button>
</template>
</b-field>
<b-field v-show="transparent !== 'close' && (transparentType === 'tproxy' || transparentType === 'redirect')"
@@ -91,47 +107,64 @@
<b-input v-model="tproxyExcludedInterfaces" expanded placeholder="docker*, veth*, wg*, ppp*, br-*" />
</b-field>
<b-field v-show="tunEnabled" label-position="on-border">
<b-field v-show="transparent !== 'close' && transparentType === 'tun' && tinytunSupported"
label-position="on-border">
<template slot="label">
{{ $t("setting.tunMode") }}
<b-tooltip type="is-dark" multilined :label="$t('setting.messages.tunMode')" position="is-right">
{{ $t("setting.tunBypassInterfaces") }}
</template>
<div style="width: 100%; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap">
<b-dropdown
v-model="tunBypassInterfacesList"
multiple
scrollable
max-height="260"
:disabled="availableInterfaces.filter(i => !i.isLoopback).length === 0"
style="flex-shrink: 0"
>
<template #trigger>
<b-button icon-right="menu-down" style="min-width: 160px; justify-content: space-between">
<span v-if="tunBypassInterfacesList.length === 0" style="color: #aaa">
{{ $t("setting.tunBypassSelectPlaceholder") }}
</span>
<span v-else>
{{ $t("setting.tunBypassSelected", { n: tunBypassInterfacesList.length }) }}
</span>
</b-button>
</template>
<b-dropdown-item
v-for="iface in availableInterfaces.filter(i => !i.isLoopback)"
:key="iface.name"
:value="iface.name"
>
<div>
<span style="font-weight: 500">{{ iface.name }}</span>
<div v-if="iface.addrs && iface.addrs.length" style="font-size: 0.8em; color: #888; margin-top: 1px">
{{ iface.addrs.join(', ') }}
</div>
</div>
</b-dropdown-item>
</b-dropdown>
<b-input
v-model="tunBypassCustom"
expanded
:placeholder="$t('setting.tunBypassCustomPlaceholder')"
style="flex: 1; min-width: 180px"
/>
</div>
</b-field>
<b-field v-show="transparent !== 'close' && transparentType === 'tun' && tinytunSupported && os === 'linux'"
:label="$t('setting.tunProcessBackend')" label-position="on-border">
<template slot="label">
{{ $t("setting.tunProcessBackend") }}
<b-tooltip type="is-dark" multilined :label="$t('setting.messages.tunProcessBackend')" position="is-right">
<b-icon size="is-small" icon=" iconfont icon-help-circle-outline"
style="position: relative; top: 2px; right: 3px; font-weight: normal" />
</b-tooltip>
</template>
<b-select v-model="tunFakeIP" expanded>
<option :value="true">FakeIP</option>
<option :value="false">RealIP</option>
</b-select>
</b-field>
<b-field v-show="tunEnabled" label-position="on-border">
<template slot="label">
{{ $t("setting.tunIPv6") }}
<b-tooltip type="is-dark" multilined :label="$t('setting.messages.tunIPv6')" position="is-right">
<b-icon size="is-small" icon=" iconfont icon-help-circle-outline"
style="position: relative; top: 2px; right: 3px; font-weight: normal" />
</b-tooltip>
</template>
<b-select v-model="tunIPv6" expanded>
<option :value="true">{{ $t("setting.options.enabled") }}</option>
<option :value="false">{{ $t("setting.options.disabled") }}</option>
</b-select>
</b-field>
<b-field v-show="tunEnabled" label-position="on-border">
<template slot="label">StrictRoute</template>
<b-select v-model="tunStrictRoute" expanded>
<option :value="true">{{ $t("setting.options.enabled") }}</option>
<option :value="false">{{ $t("setting.options.disabled") }}</option>
</b-select>
</b-field>
<b-field v-show="tunEnabled" label-position="on-border">
<template slot="label">AutoRoute</template>
<b-select v-model="tunAutoRoute" expanded>
<option :value="true">{{ $t("setting.options.enabled") }}</option>
<option :value="false">{{ $t("setting.options.disabled") }}</option>
<b-select v-model="tunProcessBackend" expanded>
<option value="">{{ $t("setting.options.tunBackendTun") }}</option>
<option value="ebpf">{{ $t("setting.options.tunBackendEbpf") }}</option>
</b-select>
</b-field>
@@ -173,47 +206,7 @@
</template>
<p></p>
</b-field>
<b-field label-position="on-border">
<template slot="label">
{{ $t("setting.preventDnsSpoofing") }}
<b-tooltip type="is-dark" :label="$t('setting.messages.preventDnsSpoofing')" multilined position="is-right">
<b-icon size="is-small" icon=" iconfont icon-help-circle-outline"
style="position: relative; top: 2px; right: 3px; font-weight: normal" />
</b-tooltip>
</template>
<b-select v-model="antipollution" expanded class="left-border">
<option value="closed">{{ $t("setting.options.closed") }}</option>
<option value="none">
{{ $t("setting.options.antiDnsHijack") }}
</option>
<option value="dnsforward">
{{ $t("setting.options.forwardDnsRequest") }}
</option>
<option value="doh">{{ $t("setting.options.doh") }}</option>
<option value="advanced">{{ $t("setting.options.advanced") }}</option>
</b-select>
<b-button v-if="antipollution === 'advanced'" :class="{
'right-extra-button': antipollution === 'closed',
'no-border-radius': antipollution !== 'closed',
}" @click="handleClickDnsSetting">
{{ $t("operations.configure") }}
</b-button>
<p></p>
</b-field>
<b-field v-show="showSpecialMode" label-position="on-border">
<template slot="label">
{{ $t("setting.specialMode") }}
<b-tooltip type="is-dark" multilined :label="$t('setting.messages.specialMode')" position="is-right">
<b-icon size="is-small" icon=" iconfont icon-help-circle-outline"
style="position: relative; top: 2px; right: 3px; font-weight: normal" />
</b-tooltip>
</template>
<b-select v-model="specialMode" expanded class="left-border">
<option value="none">{{ $t("setting.options.closed") }}</option>
<option value="supervisor">supervisor</option>
<option v-show="antipollution !== 'closed'" value="fakedns">fakedns</option>
</b-select>
</b-field>
<b-field label-position="on-border">
<template slot="label">
TCPFastOpen
@@ -284,6 +277,21 @@
custom-class="no-shadow" type="number" min="1" max="1024" validation-icon=" iconfont icon-alert"
style="flex: 1" />
</b-field>
<b-field :label="$t('setting.ssBackend')" label-position="on-border">
<b-select v-model="ssBackend" expanded>
<option value="">{{ $t("setting.options.backendDaeuniverse") }}</option>
<option value="v2ray">{{ $t("setting.options.backendV2ray") }}</option>
</b-select>
</b-field>
<b-field :label="$t('setting.trojanBackend')" label-position="on-border">
<b-select v-model="trojanBackend" expanded>
<option value="">{{ $t("setting.options.backendDaeuniverse") }}</option>
<option value="v2ray">{{ $t("setting.options.backendV2ray") }}</option>
</b-select>
</b-field>
<b-field v-show="pacMode === 'gfwlist' || transparent === 'gfwlist'" :label="$t('setting.autoUpdateGfwlist')"
label-position="on-border">
<b-select v-model="pacAutoUpdateMode" expanded>
@@ -328,9 +336,14 @@
</b-field>
</section>
<footer class="modal-card-foot flex-end">
<button class="button footer-absolute-left" type="button" @click="$emit('clickPorts')">
{{ $t("customAddressPort.title") }}
</button>
<div class="footer-absolute-left" style="display: flex; gap: 8px;">
<button class="button" type="button" @click="$emit('clickPorts')">
{{ $t("customAddressPort.title") }}
</button>
<button class="button" type="button" @click="handleClickDnsSetting">
{{ $t("dns.title") }}
</button>
</div>
<button class="button" type="button" @click="$parent.close()">
{{ $t("operations.cancel") }}
</button>
@@ -349,6 +362,7 @@ import ModalCustomRoutingA from "@/components/modalCustomRoutingA";
import modalDomainsExcluded from "@/components/modalDomainsExcluded";
import modalTproxyWhiteIpGroups from "@/components/modalTproxyWhiteIpGroups";
import modalUpdateGfwList from "@/components/modalUpdateGfwList";
import modalTinyTunRouteScript from "@/components/modalTinyTunRouteScript";
import CusBInput from "./input/Input.vue";
import { parseURL, toInt } from "@/assets/js/utils";
import BButton from "buefy/src/components/button/Button";
@@ -369,18 +383,23 @@ export default {
mux: "8",
transparent: "close",
transparentType: "tproxy",
tunFakeIP: true,
tunIPv6: false,
tunStrictRoute: false,
tunAutoRoute: true,
ipforward: false,
portSharing: false,
dnsForceMode: false,
dnsforward: "no",
antipollution: "none",
routeOnly: false,
specialMode: "none",
tproxyExcludedInterfaces: "",
tunAutoRoute: true,
tunBypassInterfaces: "",
tunBypassInterfacesList: [],
tunBypassCustom: "",
availableInterfaces: [],
tunRouteShellType: "",
tunRouteShellPath: "",
tunSetupScript: "",
tunTeardownScript: "",
tunProcessBackend: "",
ssBackend: "",
trojanBackend: "",
pacAutoUpdateMode: "none",
pacAutoUpdateIntervalHour: 0,
subscriptionAutoUpdateMode: "none",
@@ -392,11 +411,36 @@ export default {
serverListMode: "noSubscription",
remoteGFWListVersion: "checking...",
localGFWListVersion: "checking...",
showSpecialMode: true,
os: "",
isRoot: false,
tinytunSupported: false,
}),
computed: {
tunBypassInterfacesComputed: {
get() {
const parts = [];
if (this.tunBypassInterfacesList.length > 0) {
parts.push(...this.tunBypassInterfacesList);
}
if (this.tunBypassCustom.trim()) {
parts.push(
...this.tunBypassCustom
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0)
);
}
return [...new Set(parts)].join(',');
},
set(val) {
const parts = val
? val.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
: [];
const known = (this.availableInterfaces || []).map((i) => i.name);
this.tunBypassInterfacesList = parts.filter((p) => known.includes(p));
this.tunBypassCustom = parts.filter((p) => !known.includes(p)).join(',');
},
},
lite() {
return window.localStorage["lite"] && parseInt(window.localStorage["lite"]) > 0;
},
@@ -411,16 +455,16 @@ export default {
}
return toInt(port);
},
tunEnabled() {
const isTunType =
this.transparentType === "gvisor_tun" || this.transparentType === "system_tun";
return this.transparent !== "close" && isTunType;
},
},
watch: {
antipollution(val) {
if (val === "closed" && this.specialMode === "fakedns") {
this.specialMode = "none";
transparentType(val) {
if (val === 'tun' && this.tinytunSupported) {
this.fetchNetworkInterfaces();
}
},
tinytunSupported(val) {
if (val && this.transparentType === 'tun') {
this.fetchNetworkInterfaces();
}
},
},
@@ -431,6 +475,17 @@ export default {
dayjs() {
return dayjs.apply(this, arguments);
},
fetchNetworkInterfaces() {
this.$axios({ url: apiRoot + '/networkInterfaces' }).then((res) => {
if (res.data && res.data.data && res.data.data.interfaces) {
this.availableInterfaces = res.data.data.interfaces;
// Re-apply the tunBypassInterfaces string now that we know available names
if (this.tunBypassInterfaces) {
this.tunBypassInterfacesComputed = this.tunBypassInterfaces;
}
}
});
},
getSettingData() {
this.$axios({
url: apiRoot + "/remoteGFWListVersion",
@@ -455,11 +510,14 @@ export default {
if (versionRes.data && versionRes.data.data) {
this.os = versionRes.data.data.os || "";
this.isRoot = versionRes.data.data.isRoot || false;
this.tinytunSupported = versionRes.data.data.tinytunSupported || false;
}
if (this.transparentType === 'tun' && this.tinytunSupported) {
this.fetchNetworkInterfaces();
}
});
if (this.lite) {
this.transparentType = "system_proxy";
this.showSpecialMode = false;
}
});
});
@@ -487,17 +545,19 @@ export default {
mux: parseInt(this.mux),
transparent: this.transparent,
transparentType: this.transparentType,
tunFakeIP: this.tunFakeIP,
tunIPv6: this.tunIPv6,
tunStrictRoute: this.tunStrictRoute,
tunAutoRoute: this.tunAutoRoute,
ipforward: this.ipforward,
portSharing: this.portSharing,
routeOnly: this.routeOnly,
dnsforward: this.antipollution === "dnsforward" ? "yes" : "no", //
antipollution: this.antipollution,
specialMode: this.specialMode,
tproxyExcludedInterfaces: this.tproxyExcludedInterfaces,
tunAutoRoute: this.tunAutoRoute,
tunBypassInterfaces: this.tunBypassInterfacesComputed,
tunRouteShellType: this.tunRouteShellType,
tunRouteShellPath: this.tunRouteShellPath,
tunSetupScript: this.tunSetupScript,
tunTeardownScript: this.tunTeardownScript,
tunProcessBackend: this.tunProcessBackend,
ssBackend: this.ssBackend,
trojanBackend: this.trojanBackend,
},
cancelToken: new axios.CancelToken(function executor(c) {
cancel = c;
@@ -597,6 +657,29 @@ export default {
canCancel: true,
});
},
handleClickTunRouteScript() {
this.$buefy.modal.open({
parent: this,
component: modalTinyTunRouteScript,
hasModalCard: true,
canCancel: true,
props: {
os: this.os,
shellType: this.tunRouteShellType,
shellPath: this.tunRouteShellPath,
setupScript: this.tunSetupScript,
teardownScript: this.tunTeardownScript,
},
events: {
save: (data) => {
this.tunRouteShellType = data.shellType;
this.tunRouteShellPath = data.shellPath;
this.tunSetupScript = data.setupScript;
this.tunTeardownScript = data.teardownScript;
},
},
});
},
},
};
</script>
@@ -0,0 +1,110 @@
<template>
<div class="modal-card" style="width: 680px; max-width: 100%; margin: auto">
<header class="modal-card-head">
<p class="modal-card-title">{{ $t("tinytun.routeScript.title") }}</p>
</header>
<section class="modal-card-body">
<!-- Warning -->
<b-message type="is-warning" has-icon>
{{ $t("tinytun.routeScript.warning") }}
</b-message>
<!-- Shell type selector -->
<b-field :label="$t('tinytun.routeScript.shellType')" label-position="on-border">
<b-select v-model="localShellType" expanded @input="onShellTypeChange">
<template v-if="isWindows">
<option value="windows_powershell">Windows PowerShell</option>
<option value="pwsh">PowerShell Core (pwsh)</option>
<option value="cmd">Command Prompt (cmd)</option>
<option value="git_bash">Git Bash</option>
</template>
<template v-else>
<option value="bash">bash</option>
<option value="zsh">zsh</option>
<option value="sh">POSIX sh</option>
<option value="fish">fish</option>
</template>
<option value="custom">{{ $t("tinytun.routeScript.customShell") }}</option>
</b-select>
</b-field>
<!-- Custom shell path (shown when shell type is "custom" or has a custom path) -->
<b-field :label="$t('tinytun.routeScript.shellPath')" label-position="on-border"
v-if="localShellType === 'custom'">
<b-input v-model="localShellPath" :placeholder="$t('tinytun.routeScript.shellPathPlaceholder')" expanded />
</b-field>
<!-- Setup script -->
<b-field :label="$t('tinytun.routeScript.setupScript')" label-position="on-border" style="margin-top: 1rem">
<b-input v-model="localSetupScript" type="textarea" rows="6"
:placeholder="$t('tinytun.routeScript.setupScriptPlaceholder')" custom-class="code-font horizon-scroll"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
</b-field>
<!-- Teardown script -->
<b-field :label="$t('tinytun.routeScript.teardownScript')" label-position="on-border" style="margin-top: 1rem">
<b-input v-model="localTeardownScript" type="textarea" rows="6"
:placeholder="$t('tinytun.routeScript.teardownScriptPlaceholder')" custom-class="code-font horizon-scroll"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
</b-field>
</section>
<footer class="modal-card-foot" style="justify-content: flex-end">
<button class="button" type="button" @click="$parent.close()">
{{ $t("operations.cancel") }}
</button>
<button class="button is-primary" @click="handleClickSave">
{{ $t("operations.save") }}
</button>
</footer>
</div>
</template>
<script>
export default {
name: "ModalTinyTunRouteScript",
props: {
os: { type: String, default: "" },
shellType: { type: String, default: "" },
shellPath: { type: String, default: "" },
setupScript: { type: String, default: "" },
teardownScript: { type: String, default: "" },
},
data() {
const defaultShell = this.os === "windows" ? "windows_powershell" : "bash";
return {
localShellType: this.shellType || defaultShell,
localShellPath: this.shellPath || "",
localSetupScript: this.setupScript || "",
localTeardownScript: this.teardownScript || "",
};
},
computed: {
isWindows() {
return this.os === "windows";
},
},
methods: {
onShellTypeChange(val) {
// Clear custom path when switching away from custom
if (val !== "custom") {
this.localShellPath = "";
}
},
handleClickSave() {
this.$emit("save", {
shellType: this.localShellType,
shellPath: this.localShellPath,
setupScript: this.localSetupScript,
teardownScript: this.localTeardownScript,
});
this.$parent.close();
},
},
};
</script>
<style lang="scss" scoped>
.code-font {
font-family: monospace;
}
</style>
+51 -25
View File
@@ -18,6 +18,9 @@ export default {
optional: "optional",
loadBalance: "Load Balance",
log: "Logs",
darkTheme: "Dark Theme",
lightTheme: "Light Theme",
autoTheme: "Auto Theme",
},
welcome: {
title: "Welcome",
@@ -93,7 +96,8 @@ export default {
switchSite: "Switch to alternate site",
addOutbound: "Add an outbound",
domainsExcluded: "Domains Excluded",
tproxyExcludedInterfaces: "Excluded Interface Prefixes"
tproxyExcludedInterfaces: "Excluded Interface Prefixes",
configureTunRouteScript: "Configure Route Script"
},
register: {
title: "Create an admin account first",
@@ -111,21 +115,26 @@ export default {
setting: {
transparentProxy: "Transparent Proxy/System Proxy",
transparentType: "Transparent Proxy/System Proxy Implementation",
tunMode: "TUN Mode",
tunIPv6: "TUN IPv6",
logLevel: "Log Level",
pacMode: "Traffic Splitting Mode of Rule Port",
preventDnsSpoofing: "Prevent DNS Spoofing",
specialMode: "Special Mode",
mux: "Multiplex",
autoUpdateSub: "Automatically Update Subscriptions",
autoUpdateGfwlist: "Automatically Update GFWList",
preferModeWhenUpdate: "Mode when Update Subscriptions and GFWList",
tproxyExcludedInterfaces: "Excluded Interface Prefixes",
tunAutoRoute: "Auto Route",
tunBypassInterfaces: "TUN Bypassed Interfaces",
tunBypassCustomPlaceholder: "Custom glob patterns, e.g. docker*, vmnet*",
tunBypassSelectPlaceholder: "Select interfaces...",
tunBypassSelected: "{n} interface(s) selected",
ipForwardOn: "IP Forward",
portSharingOn: "Port Sharing",
concurrency: "Concurrency",
inboundSniffing: "Sniffing",
tunProcessBackend: "Process Exclusion Backend",
ssBackend: "Shadowsocks Backend",
trojanBackend: "Trojan Backend",
nodeBackend: "Backend",
options: {
trace: "Trace",
debug: "Debug",
@@ -139,12 +148,10 @@ export default {
gfwlist: "Proxy only GFWList",
sameAsPacMode: "Traffic Splitting Mode is the Same as the Rule Port",
customRouting: "Customized Routing",
antiDnsHijack: "Prevent DNS Hijack Only (fast)",
forwardDnsRequest: "Forward DNS Request",
doh: "DoH(dns-over-https)",
default: "Keep Default",
on: "On",
off: "Off",
notIntegrated: "not integrated",
updateSubWhenStart: "Update Subscriptions When Service Starts",
updateSubAtIntervals: "Update Subscriptions Regularly (Unit: hour)",
updateGfwlistWhenStart: "Update GFWList When Service Starts",
@@ -153,6 +160,11 @@ export default {
closed: "Off",
advanced: "Advanced Setting",
leastPing: "Least Latency First",
tunBackendTun: "TUN (default, /proc process lookup)",
tunBackendEbpf: "eBPF (cgroupv2 process exclusion)",
backendDaeuniverse: "daeuniverse/outbound",
backendV2ray: "v2ray / xray",
backendSystemDefault: "System Default",
},
messages: {
inboundSniffing: "Sniff inbound traffic. If it is not turned on, some traffic may not be diverted correctly.",
@@ -161,19 +173,15 @@ export default {
transparentProxy:
"If transparent proxy on, no extra configure needed and all TCP traffic will pass through the v2rayA. Providing proxy service to other computers and docker as the gateway should make option 'Share in LAN' on.",
transparentType:
"★tproxy: support UDP, but not support docker. ★redirect: friendly for docker, but does not support UDP and need to occupy local port 53 for dns anti-pollution.",
"★tproxy: support UDP, but not support docker. ★redirect: friendly for docker, but does not support UDP and need to occupy local port 53 for dns anti-pollution. ★tun (TinyTun): cross-platform TUN-based proxy, requires tinytun binary; on Linux the data path is handled by TinyTun itself, so the generated YAML must match the TinyTun version.",
tproxyExcludedInterfaces:
"Set the network interface prefixes that should not pass through the transparent proxy. Wildcard * is supported (automatically converted to + in iptables mode). For example: docker*, veth*, wg*, ppp*, br-*. Use commas to separate multiple prefixes.",
tunMode:
"★FakeIP: Use fake IPs to accelerate DNS resolution and improve performance. ★RealIP: Use real IPs, more suitable for certain special applications.",
tunIPv6:
"Enable IPv6 traffic support in TUN interface. Note: Requires IPv6 network support from your system.",
tunAutoRoute:
"When enabled, TinyTun automatically configures system routes. When disabled, you must provide custom setup/teardown scripts to configure routing yourself.",
tunBypassInterfaces: "Select interfaces to exclude from TUN proxying, or enter custom glob patterns below.",
tunProcessBackend: "Choose TinyTun's process exclusion method on Linux. TUN mode uses /proc scanning for broad compatibility; eBPF mode uses cgroupv2 hooks for more accurate process-level exclusion, requiring tinytun-ebpf.o at /usr/lib/tinytun/.",
pacMode: `Here you can set the splitting traffic rule of the rule port. By default, "Rule of Splitting Traffic" port is 20172 and HTTP protocol.`,
preventDnsSpoofing:
"★Forward DNS Request: DNS requests will be forwarded by proxy server." +
"★DoH(dns-over-https, v2ray-core: 4.22.0+): DNS over HTTPS.",
specialMode:
"★supervisorMonitor dns pollution, intercept in advance, use the sniffing mechanism of v2ray-core to prevent pollution. ★fakednsUse the fakens strategy to speed up the resolving.",
preventDnsSpoofing: "",
tcpFastOpen:
"Simplify TCP handshake process to speed up connection establishment. Risk of emphasizing characteristics of packets exists. It may cause failed to connect if your system does not support it.",
mux:
@@ -241,13 +249,17 @@ export default {
],
},
dns: {
title: "Configure DNS Server",
internalQueryServers: "Domain Query Servers",
externalQueryServers: "External Domain Query Servers",
messages: [
'"@:(dns.internalQueryServers)" are designed to be used to look up domain names in China, while "@:(dns.externalQueryServers)" be used to look up others.',
'"@:(dns.internalQueryServers)" will be used to look up all domain names if "@:(dns.externalQueryServers)" is empty.',
],
title: "DNS Settings",
help: "DNS Help",
helpTooltip: "View v2fly DNS documentation",
colServer: "DNS Server",
colDomains: "Domain List",
colOutbound: "Outbound",
serverPlaceholder: "e.g. 8.8.8.8 or https://dns.google/dns-query",
domainsPlaceholder: "One per line, e.g. geosite:cn\nLeave empty for fallback DNS",
addRule: "Add Rule",
resetDefault: "Reset to Defaults",
errNoRules: "At least one DNS rule is required",
},
egressPortWhitelist: {
title: "Egress Port Whitelist",
@@ -393,5 +405,19 @@ export default {
],
formName: "Custom Download Link",
wrongCustomLink: "wrong custom download link"
},
tinytun: {
routeScript: {
title: "TinyTun Custom Route Script",
warning: "Warning: Incorrect scripts may break your network or system routing. Make sure you know what you are doing before saving.",
shellType: "Shell Type",
customShell: "Custom (specify path below)",
shellPath: "Shell Path",
shellPathPlaceholder: "/usr/bin/bash",
setupScript: "Setup Script (runs after TinyTun starts)",
setupScriptPlaceholder: "# Script to configure routes when TinyTun starts\n# e.g. ip route add default dev tun0",
teardownScript: "Teardown Script (runs before TinyTun stops)",
teardownScriptPlaceholder: "# Script to remove routes when TinyTun stops\n# e.g. ip route del default dev tun0",
}
}
};
+21 -17
View File
@@ -18,6 +18,9 @@ export default {
optional: "اختیاری",
loadBalance: "متعادل کردن",
log: "گزارش ها",
darkTheme: "تم تیره",
lightTheme: "تم روشن",
autoTheme: "تم خودکار",
},
welcome: {
title: "خوش آمدی",
@@ -106,8 +109,6 @@ export default {
transparentType: "Transparent Proxy/System Proxy Implementation",
logLevel: "Log Level",
pacMode: "Traffic Splitting Mode of Rule Port",
preventDnsSpoofing: "جلوگیری از هک DNS",
specialMode: "حالت ویژه",
mux: "Multiplex",
autoUpdateSub: "بروزرسانی خودکار اشتراک ها",
autoUpdateGfwlist: "بروزرسانی خودکار GFWList",
@@ -115,6 +116,9 @@ export default {
ipForwardOn: "IP Forward",
portSharingOn: "به اشتراک گذاری پورت",
concurrency: "همزمانی",
ssBackend: "Shadowsocks Backend",
trojanBackend: "Trojan Backend",
nodeBackend: "Backend",
options: {
trace: "Trace",
debug: "Debug",
@@ -128,9 +132,6 @@ export default {
gfwlist: "فقط GFWList را پروکسی کن.",
sameAsPacMode: "Traffic Splitting Mode is the Same as the Rule Port",
customRouting: "Customized Routing",
antiDnsHijack: "Prevent DNS Hijack Only (fast)",
forwardDnsRequest: "Forward DNS Request",
doh: "DoH(dns-over-https)",
default: "پیشفرض",
on: "روشن",
off: "خاموش",
@@ -143,6 +144,9 @@ export default {
closed: "خاموش",
advanced: "تنظیمات پیشرفته",
leastPing: "اول سرور با پینگ کمتر",
backendDaeuniverse: "daeuniverse/outbound",
backendV2ray: "v2ray / xray",
backendSystemDefault: "System Default",
},
messages: {
gfwlist:
@@ -152,11 +156,7 @@ export default {
transparentType:
"★tproxy: support UDP, but not support docker. ★redirect: friendly for docker, but does not support UDP and need to occupy local port 53 for dns anti-pollution.",
pacMode: `Here you can set the splitting traffic rule of the rule port. By default, "Rule of Splitting Traffic" port is 20172 and HTTP protocol.`,
preventDnsSpoofing:
"★Forward DNS Request: DNS requests will be forwarded by proxy server." +
"★DoH(dns-over-https, v2ray-core: 4.22.0+): DNS over HTTPS.",
specialMode:
"★supervisorMonitor dns pollution, intercept in advance, use the sniffing mechanism of v2ray-core to prevent pollution. ★fakednsUse the fakens strategy to speed up the resolving.",
preventDnsSpoofing: "",
tcpFastOpen:
"Simplify TCP handshake process to speed up connection establishment. Risk of emphasizing characteristics of packets exists. It may cause failed to connect if your system does not support it.",
mux:
@@ -222,13 +222,17 @@ export default {
],
},
dns: {
title: "پیکربندی سرور DNS",
internalQueryServers: "Domain Query Servers",
externalQueryServers: "External Domain Query Servers",
messages: [
'"@:(dns.internalQueryServers)" are designed to be used to look up domain names in China, while "@:(dns.externalQueryServers)" be used to look up others.',
'"@:(dns.internalQueryServers)" will be used to look up all domain names if "@:(dns.externalQueryServers)" is empty.',
],
title: "DNS تنظیمات",
help: "راهنمای DNS",
helpTooltip: "مشاهده مستندات v2fly DNS",
colServer: "سرور DNS",
colDomains: "لیست دامنه",
colOutbound: "خروجی",
serverPlaceholder: "مثلاً 8.8.8.8 یا https://dns.google/dns-query",
domainsPlaceholder: "یک دامنه در هر خط, مثلاً geosite:cn\nبرای DNS پشتیبان خالی بگذارید",
addRule: "افزودن قانون",
resetDefault: "بازگشت به پیشفرض",
errNoRules: "حداقل یک قانون DNS لازم است",
},
egressPortWhitelist: {
title: "Egress Port Whitelist",
+21 -17
View File
@@ -18,6 +18,9 @@ export default {
optional: "opcional",
loadBalance: "Balanceamento de Carga",
log: "Logs",
darkTheme: "Tema Escuro",
lightTheme: "Tema Claro",
autoTheme: "Tema Automático",
},
welcome: {
title: "Bem-vindo",
@@ -106,8 +109,6 @@ export default {
transparentType: "Implementação do Proxy Transparente/Proxy do Sistema",
logLevel: "Nível de Log",
pacMode: "Modo de Divisão de Tráfego da Porta de Regra",
preventDnsSpoofing: "Prevenir Falsificação de DNS",
specialMode: "Modo Especial",
mux: "Multiplexação",
autoUpdateSub: "Atualizar Assinaturas Automaticamente",
autoUpdateGfwlist: "Atualizar GFWList Automaticamente",
@@ -115,6 +116,9 @@ export default {
ipForwardOn: "Encaminhamento de IP",
portSharingOn: "Compartilhamento de Porta",
concurrency: "Concorrência",
ssBackend: "Backend do Shadowsocks",
trojanBackend: "Backend do Trojan",
nodeBackend: "Backend",
options: {
trace: "Trace",
debug: "Debug",
@@ -129,9 +133,6 @@ export default {
sameAsPacMode:
"Modo de Divisão de Tráfego é o Mesmo que a Porta de Regra",
customRouting: "Roteamento Personalizado",
antiDnsHijack: "Prevenir apenas sequestro de DNS (rápido)",
forwardDnsRequest: "Encaminhar Solicitação de DNS",
doh: "DoH(dns-over-https)",
default: "Manter Padrão",
on: "Ligado",
off: "Desligado",
@@ -145,6 +146,9 @@ export default {
closed: "Desligado",
advanced: "Configuração Avançada",
leastPing: "Menor Latência Primeiro",
backendDaeuniverse: "daeuniverse/outbound",
backendV2ray: "v2ray / xray",
backendSystemDefault: "Padrão do Sistema",
},
messages: {
gfwlist:
@@ -154,11 +158,7 @@ export default {
transparentType:
"★tproxy: suporta UDP, mas não suporta docker. ★redirect: amigável para docker, mas não suporta UDP e precisa ocupar a porta local 53 para dns anti-poluição.",
pacMode: `Aqui você pode definir a regra de divisão de tráfego da porta de regra. Por padrão, a "Regra de Divisão de Tráfego" porta é 20172 e protocolo HTTP.`,
preventDnsSpoofing:
"★Encaminhar Solicitação de DNS: As solicitações de DNS serão encaminhadas pelo servidor proxy." +
"★DoH(dns-over-https, v2ray-core: 4.22.0+): DNS sobre HTTPS.",
specialMode:
"★supervisorMonitora a poluição dns, intercepta antecipadamente, usa o mecanismo de sniffing do v2ray-core para prevenir a poluição. ★fakednsUse a estratégia fakens para acelerar a resolução.",
preventDnsSpoofing: "",
tcpFastOpen:
"Simplifica o processo de handshake do TCP para acelerar o estabelecimento da conexão. Risco de enfatizar as características dos pacotes existe. Pode causar falha na conexão se o seu sistema não suportar.",
mux:
@@ -225,13 +225,17 @@ export default {
],
},
dns: {
title: "Configurar Servidor DNS",
internalQueryServers: "Servidores de Consulta de Domínio Internos",
externalQueryServers: "Servidores de Consulta de Domínio Externos",
messages: [
'"@:(dns.internalQueryServers)" são projetados para serem usados para pesquisar nomes de domínio na China, enquanto "@:(dns.externalQueryServers)" é usado para pesquisar outros.',
'"@:(dns.internalQueryServers)" será usado para pesquisar todos os nomes de domínio se "@:(dns.externalQueryServers)" estiver vazio.',
],
title: "Configurações de DNS",
help: "Ajuda DNS",
helpTooltip: "Ver documentação v2fly DNS",
colServer: "Servidor DNS",
colDomains: "Lista de Domínios",
colOutbound: "Saída",
serverPlaceholder: "ex. 8.8.8.8 ou https://dns.google/dns-query",
domainsPlaceholder: "Um por linha, ex. geosite:cn\nDeixe vazio para DNS de fallback",
addRule: "Adicionar Regra",
resetDefault: "Restaurar Padrões",
errNoRules: "Pelo menos uma regra DNS é necessária",
},
egressPortWhitelist: {
title: "Lista de Portas de Saída",
+21 -17
View File
@@ -18,6 +18,9 @@ export default {
optional: "необязательно",
loadBalance: "Load Balance",
log: "Журнал",
darkTheme: "Темная тема",
lightTheme: "Светлая тема",
autoTheme: "Авто тема",
},
welcome: {
title: "Добро пожаловать",
@@ -109,8 +112,6 @@ export default {
transparentType: "Реализация прозрачного прокси/Системного прокси",
logLevel: "Уровень журнала",
pacMode: "Режим разделения трафика на порте с правилами",
preventDnsSpoofing: "Предотвратить DNS-спуфинг",
specialMode: "Специальный режим",
mux: "Мультиплекс",
autoUpdateSub: "Автоматически обновлять подписки",
autoUpdateGfwlist: "Автоматически обновлять GFWList",
@@ -119,6 +120,9 @@ export default {
portSharingOn: "Port Sharing",
concurrency: "Параллелизм",
inboundSniffing: "Сниффер",
ssBackend: "Shadowsocks Backend",
trojanBackend: "Trojan Backend",
nodeBackend: "Backend",
options: {
trace: "Trace",
debug: "Debug",
@@ -133,9 +137,6 @@ export default {
sameAsPacMode:
"Режим разделения трафика такой же, как у порта с правилами",
customRouting: "Настраиваемая адресация",
antiDnsHijack: "Только защита от перехвата DNS (быстро)",
forwardDnsRequest: "Перенаправлять DNS запросы",
doh: "DoH (dns-over-https)",
default: "По-умолчанию",
on: "Включено",
off: "Выключено",
@@ -147,6 +148,9 @@ export default {
closed: "Выключено",
advanced: "Расширенная настройка",
leastPing: "С наименьшей задержкой",
backendDaeuniverse: "daeuniverse/outbound",
backendV2ray: "v2ray / xray",
backendSystemDefault: "По умолчанию",
},
messages: {
inboundSniffing: "Анализировать входящий трафик. Если эта опция выключена, часть трафик может быть не перенаправлена корректно.",
@@ -157,11 +161,7 @@ export default {
transparentType:
"★tproxy: поддерживает UDP, но не поддерживает docker. ★redirect: подходит для docker, но не поддерживает UDP и требуется занять локальный порт 53 для защиты dns от загрязнения.",
pacMode: `Здесь вы можете выбрать правила разделения трафика для порта с правилами. По-умолчанию, порт для разделения трафика это 20172 с протоколом HTTP.`,
preventDnsSpoofing:
"★Перенаправлять DNS запросы: DNS запросы будут отправляться через прокси-сервер." +
"★DoH(dns-over-https, v2ray-core: 4.22.0+): DNS over HTTPS.",
specialMode:
"★supervisor:Мониторинг загрязнения dns, перехват заранее, используется механизм сниффинга v2ray-core для предотвращения загрязнения. ★fakedns:Использование стратегии fakedns для ускорения резолвинга.",
preventDnsSpoofing: "",
tcpFastOpen:
"Упростить процесс TCP рукопожатия для ускорения установки соединения. Есть риск распознавания характеристик пакетов. Это может помешать установить соединение если ваша система не подергивает это",
mux:
@@ -227,13 +227,17 @@ export default {
],
},
dns: {
title: "Настройка DNS сервера",
internalQueryServers: "Серверы запросов домена",
externalQueryServers: "Внешние серверы запросов домена",
messages: [
'"@:(dns.internalQueryServers)" применяются для просмотра доменных имен в Китае, в то время как "@:(dns.externalQueryServers)" используется для других доменов.',
'"@:(dns.internalQueryServers)" будет использован для всех доменных имен если "@:(dns.externalQueryServers)" пуст.',
],
title: "DNS настройки",
help: "DNS справка",
helpTooltip: "Просмотреть документацию v2fly DNS",
colServer: "DNS сервер",
colDomains: "Список доменов",
colOutbound: "Исходящий",
serverPlaceholder: "Напр. 8.8.8.8 или https://dns.google/dns-query",
domainsPlaceholder: "По одному на строку, напр. geosite:cn\nОставьте пустым для резервного DNS",
addRule: "Добавить правило",
resetDefault: "Сбросить настройки",
errNoRules: "Необходимо хотя бы одно правило DNS",
},
egressPortWhitelist: {
title: "Настройка белового списка портов",
+52 -26
View File
@@ -18,6 +18,9 @@ export default {
optional: "可选",
loadBalance: "负载均衡",
log: "日志",
darkTheme: "深色主题",
lightTheme: "浅色主题",
autoTheme: "自动主题",
},
welcome: {
title: "初来乍到,请多关照",
@@ -92,7 +95,8 @@ export default {
switchSite: "切换至备用站点",
addOutbound: "新增一个出站 (outbound)",
domainsExcluded: "排除域名",
tproxyExcludedInterfaces: "不走代理的网卡前缀"
tproxyExcludedInterfaces: "不走代理的网卡前缀",
configureTunRouteScript: "配置路由脚本"
},
register: {
title: "初来乍到,创建一个管理员账号",
@@ -111,20 +115,25 @@ export default {
inboundSniffing: "嗅探",
transparentProxy: "透明代理/系统代理",
transparentType: "透明代理/系统代理实现方式",
tunMode: "TUN模式",
tunIPv6: "TUN IPv6",
logLevel: "日志等级",
pacMode: "规则端口的分流模式",
preventDnsSpoofing: "防止DNS污染",
specialMode: "特殊模式",
mux: "多路复用",
autoUpdateSub: "自动更新订阅",
autoUpdateGfwlist: "自动更新GFWList",
preferModeWhenUpdate: "解析订阅链接/更新时优先使用",
tproxyExcludedInterfaces: "不走代理的网卡前缀",
tunAutoRoute: "自动路由",
tunBypassInterfaces: "TUN 不走代理的网卡",
tunBypassCustomPlaceholder: "自定义通配符,例如: docker*, vmnet*",
tunBypassSelectPlaceholder: "选择网卡...",
tunBypassSelected: "已选 {n} 个网卡",
ipForwardOn: "开启IP转发",
portSharingOn: "开启端口分享",
portSharingOn: "允许局域网的连接",
concurrency: "最大并发数",
tunProcessBackend: "TinyTun 进程排除方式",
ssBackend: "Shadowsocks 后端",
trojanBackend: "Trojan 后端",
nodeBackend: "后端",
options: {
trace: "跟踪",
debug: "调试",
@@ -138,12 +147,10 @@ export default {
gfwlist: "GFWList模式",
sameAsPacMode: "分流规则与规则端口所选模式一致",
customRouting: "自定义路由规则",
antiDnsHijack: "仅防止DNS劫持(快速)",
forwardDnsRequest: "转发DNS请求",
doh: "DoH(DNS-over-HTTPS)",
default: "保持系统默认",
on: "启用",
off: "关闭",
notIntegrated: "未集成",
updateSubWhenStart: "服务端启动时更新订阅",
updateSubAtIntervals: "每隔一段时间更新订阅(单位:小时)",
updateGfwlistWhenStart: "服务端启动时更新GFWList",
@@ -152,6 +159,11 @@ export default {
closed: "关闭",
advanced: "自定义高级设置",
leastPing: "最小时延优先",
tunBackendTun: "TUN(默认,/proc 进程查找)",
tunBackendEbpf: "eBPFcgroupv2 进程排除)",
backendDaeuniverse: "daeuniverse/outbound",
backendV2ray: "v2ray / xray",
backendSystemDefault: "跟随系统设置",
},
messages: {
inboundSniffing: "嗅探入站域名,如不开启,可能导致部分域名分流错误",
@@ -159,20 +171,16 @@ export default {
transparentProxy:
"全局代理开启后,无需经过额外设置,任何TCP流量均会经过V2RayA。另外,如需作为网关使得连接本机的其他主机或docker也享受代理,请勾选“开启局域网共享”。",
transparentType:
"★tproxy: 支持udp,不支持docker。★redirect: docker友好,不支持udp,需要占用本地53端口以应对dns污染。",
"★tproxy: 支持udp,不支持docker。★redirect: docker友好,不支持udp,需要占用本地53端口以应对dns污染。★tun (TinyTun): 跨平台TUN透明代理,需要tinytun二进制文件;Linux 下的数据面由 TinyTun 自身后端处理,生成的 YAML 配置需与 TinyTun 版本匹配。",
tproxyExcludedInterfaces:
"设置不经过透明代理的网卡前缀。支持通配符 * (iptables模式下会自动转换为 +)。例如: docker*, veth*, wg*, ppp*, br-*。多个前缀用逗号隔开。",
tunMode:
"★FakeIP: 使用虚拟IP加速DNS解析,提高性能。★RealIP: 使用真实IP,更适合某些特殊应用。",
tunIPv6:
"开启后TUN接口将支持IPv6流量。注意:需要系统支持IPv6网络。",
tunAutoRoute:
"开启时,TinyTun 自动配置系统路由。关闭时,需要提供自定义的启动/停止脚本手动配置路由。",
tunBypassInterfaces: "勾选不走 TUN 代理的网卡,或在下方输入自定义通配符。",
tunProcessBackend: "Linux 下选择 TinyTun 的进程排除实现。TUN 模式使用 /proc 查找,兼容性强;eBPF 模式通过 cgroupv2 钉子实现更准确的进程级排除,需安装 tinytun-ebpf.o/usr/lib/tinytun/。",
pacMode:
"该选项设置规则分流端口所使用的路由模式。默认情况下规则分流端口为20172,HTTP协议。",
preventDnsSpoofing:
"★转发DNS查询: 通过代理服务器转发DNS请求。" +
"★DoH(v2ray-core: 4.22.0+): DNS over HTTPS。",
specialMode:
"★supervisor:监控dns污染,提前拦截,利用v2ray-core的sniffing解决污染。★fakedns:使用fakedns策略加速解析。",
preventDnsSpoofing: "",
tcpFastOpen:
"简化TCP握手流程以加速建立连接,可能会增加封包的特征。若系统不支持可能会导致无法正常连接。",
mux: "复用TCP连接以减少握手次数,但会影响吞吐量大的使用场景,如观看视频、下载、测速。当前仅支持vmess节点。可能会增加特征造成断流。",
@@ -238,13 +246,17 @@ export default {
],
},
dns: {
title: "配置DNS服务器",
internalQueryServers: "域名查询服务器",
externalQueryServers: "国外域名查询服务器",
messages: [
"“@:(dns.internalQueryServers)” 用于查询国内域名,而 “@:(dns.externalQueryServers)” 用于查询国外域名。",
"如果将 “@:(dns.externalQueryServers)” 留空,“@:(dns.internalQueryServers)” 将会负责查询所有域名。",
],
title: "DNS 设置",
help: "DNS 帮助",
helpTooltip: "查看 v2fly DNS 文档",
colServer: "DNS 服务器",
colDomains: "域名列表",
colOutbound: "出口",
serverPlaceholder: "如 8.8.8.8 或 https://dns.google/dns-query",
domainsPlaceholder: "每行一个,如 geosite:cn\n留空表示兜底 DNS",
addRule: "添加规则",
resetDefault: "恢复默认",
errNoRules: "至少需要一条 DNS 规则",
},
egressPortWhitelist: {
title: "出方向端口白名单",
@@ -396,5 +408,19 @@ export default {
],
formName: "自定义下载链接",
wrongCustomLink: "错误的自定义下载链接"
},
tinytun: {
routeScript: {
title: "TinyTun 自定义路由脚本",
warning: "警告:错误的脚本可能会破坏您的网络或系统路由。请确保您清楚自己正在做什么再保存。",
shellType: "Shell 类型",
customShell: "自定义(在下方指定路径)",
shellPath: "Shell 路径",
shellPathPlaceholder: "/usr/bin/bash",
setupScript: "启动脚本(TinyTun 启动后执行)",
setupScriptPlaceholder: "# TinyTun 启动时配置路由的脚本\n# 例如: ip route add default dev tun0",
teardownScript: "停止脚本(TinyTun 停止前执行)",
teardownScriptPlaceholder: "# TinyTun 停止时移除路由的脚本\n# 例如: ip route del default dev tun0",
}
}
};
+119 -10
View File
@@ -24,10 +24,7 @@
>
<b-message
v-for="v of connectedServerInfo"
:key="v.value"
:title="`${v.info.name}${
v.info.subscription_name ? ` [${v.info.subscription_name}]` : ''
}`"
:key="connectedServerKey(v.which)"
:closable="false"
size="is-small"
:type="
@@ -41,7 +38,26 @@
"
@click.native="handleClickConnectedServer(v.which)"
>
<div v-if="v.showContent">
<template #header>
<div class="node-status-card__header">
<span class="node-status-card__title">
{{ formatServerName(v.info) }}
<span
v-if="v.info.subscription_name"
class="node-status-card__subscription"
>
[{{ v.info.subscription_name }}]
</span>
</span>
<span
v-if="formatOutboundLabel(v.which)"
class="node-status-card__group"
>
{{ formatOutboundLabel(v.which) }}
</span>
</div>
</template>
<div v-if="v.showContent" class="node-status-card__body">
<p>{{ $t("server.protocol") }}: {{ v.info.net }}</p>
<p v-if="v.info.delay && v.info.delay < 99999">
{{ $t("server.latency") }}: {{ v.info.delay }}ms
@@ -718,7 +734,7 @@ import ModalSubscription from "@/components/modalSubcription";
import ModalSharing from "@/components/modalSharing";
import { waitingConnected } from "@/assets/js/networkInspect";
import axios from "@/plugins/axios";
import * as dayjs from "dayjs";
import dayjs from "dayjs";
export default {
name: "Node",
@@ -884,6 +900,37 @@ export default {
}
},
methods: {
connectedServerKey(which = {}) {
const parts = [
which._type || "unknown",
which.sub !== undefined ? which.sub : "na",
which.id || "0",
which.outbound || "default",
];
return parts.join("-");
},
formatServerName(info = {}) {
if (info.name) {
return info.name;
}
if (info.address) {
return info.address;
}
return this.$t("server.name");
},
formatOutboundLabel(which = {}) {
if (!which.outbound) {
return null;
}
const outbound = which.outbound;
const mapping = this.runningState.outboundToServerName
? this.runningState.outboundToServerName[outbound]
: null;
if (typeof mapping === "number") {
return `${outbound.toUpperCase()} - ${this.$t("common.loadBalance")} (${mapping})`;
}
return outbound.toUpperCase();
},
refreshTableData(touch, running) {
touch.servers.forEach((v) => {
v.connected = false;
@@ -1004,7 +1051,7 @@ export default {
}
const reader = new FileReader();
reader.onload = function (e) {
// target.result DataURL
// target.result property represents the DataURL of the target object
// console.log(e.target.result);
const file = e.target.result;
const qrcode = new Decoder();
@@ -1180,6 +1227,18 @@ export default {
}
this.$emit("input", this.runningState);
},
// notifyStopped is called by the parent (App.vue) when a WebSocket
// running_state message with running=false is received (e.g. TinyTun
// crashed). It immediately updates the local running state so the UI
// reflects the correct status without waiting for the next /touch poll.
notifyStopped() {
if (this.runningState.running !== this.$t("common.notRunning")) {
Object.assign(this.runningState, {
running: this.$t("common.notRunning"),
});
this.$emit("input", this.runningState);
}
},
locateTabToConnected(which) {
let whichServer = which;
if (!whichServer) {
@@ -1293,7 +1352,7 @@ export default {
handleClickAboutConnection(row, sub) {
let cancel;
if (!row.connected) {
//
// The node is not connected, initiating connection
let loading = this.$buefy.loading.open();
waitingConnected(
this.$axios({
@@ -1369,7 +1428,7 @@ export default {
handleClickLatency(ping) {
let touches = JSON.stringify(
this.checkedRows.map((x) => {
//sub
// iterate through subscriptions
let sub = this.tableData.subscriptions.findIndex((subscription) =>
subscription.servers.some((y) => x === y)
);
@@ -1603,7 +1662,7 @@ td {
</style>
<style lang="scss">
@import "~bulma/sass/utilities/all";
@import "bulma/sass/utilities/all.sass";
#toolbar {
@media screen and (max-width: 450px) {
@@ -1795,6 +1854,56 @@ $coverBackground: rgba(0, 0, 0, 0.6);
}
}
.node-status-card__header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
gap: 0.5rem;
}
.node-status-card__title {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.35rem;
font-size: 0.95rem;
}
.node-status-card__subscription {
font-size: 0.75rem;
opacity: 0.85;
}
.node-status-card__group {
font-size: 0.7rem;
letter-spacing: 0.05em;
text-transform: uppercase;
border-radius: 999px;
padding: 0.1rem 0.6rem;
border: 1px solid rgba(255, 255, 255, 0.6);
background-color: rgba(255, 255, 255, 0.15);
color: inherit;
white-space: nowrap;
}
.message.is-light .node-status-card__group {
border-color: rgba(0, 0, 0, 0.45);
background-color: rgba(0, 0, 0, 0.05);
}
.node-status-card__body {
font-size: 0.85rem;
}
.node-status-card__body p {
margin-bottom: 0.2rem;
}
.node-status-card__body p:last-child {
margin-bottom: 0;
}
tr.highlight-row-connected {
transition: background-color 0.05s linear;
background-color: #a8cff0;
+6 -5
View File
@@ -91,7 +91,7 @@ axios.interceptors.response.use(
host = u.host;
}
if (err.response && err.response.status === 401) {
//401未授权
// 401 Unauthorized
new Vue({
components: { Modal, ModalLogin },
data: () => ({
@@ -128,11 +128,12 @@ axios.interceptors.response.use(
location.protocol.substr(0, 5) === "https" &&
u.protocol === "http"
) {
//https前端通信http后端
// https frontend communicating with http backend
let msg = i18n.t("axios.messages.cannotCommunicate.0");
if (host === "localhost" || host === "local" || host === "127.0.0.1") {
if (browser.versions.webKit) {
//Chrome等webkit内核浏览器允许访问http://localhost,只有可能是服务端未启动
// Chrome and other WebKit browsers allow access to http://localhost,
// failures are likely due to backend service not being started.
informNotRunning(u.source.replace(u.relative, ""));
return;
}
@@ -173,13 +174,13 @@ axios.interceptors.response.use(
) {
informNotRunning(u.source.replace(u.relative, ""));
} else {
//其他错误
// other errors
if (
!err.message ||
(err.message && err.message.indexOf("404") >= 0) ||
(err.response && err.response.status === 404)
) {
//接口不存在,或是正常错误(如取消),可能服务端是老旧版本,不管
// Interface doesn't exist, or expected error (e.g. cancellation), maybe legacy server version - ignore
return Promise.reject(err);
}
console.log("!other");
+33
View File
@@ -0,0 +1,33 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue2";
import vueJsx from "@vitejs/plugin-vue2-jsx";
import path from "path";
export default defineConfig(({ mode }) => ({
plugins: [
vue(),
vueJsx({
include: [/\.[jt]sx$/, /\.js$/],
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
vue$: "vue/dist/vue.esm.js",
},
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
},
define: {
apiRoot: '`${localStorage["backendAddress"]}/api`',
},
server: {
port: 8081,
},
build: {
outDir: process.env.OUTPUT_DIR || "../web",
sourcemap: false,
assetsDir: "static",
emptyOutDir: true,
},
base: process.env.publicPath || (mode === "production" ? "./" : "/"),
}));
+576 -3464
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -60,8 +60,6 @@ Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environmen
; 创建服务
Filename: "{sys}\sc.exe"; Parameters: "create v2rayA binPath= ""{app}\bin\v2raya.exe"" DisplayName= ""v2rayA Service"" start= auto"; Flags: runhidden waituntilterminated
Filename: "{sys}\sc.exe"; Parameters: "description v2rayA ""v2rayA - A web GUI client of Project V"""; Flags: runhidden waituntilterminated
; 启动服务
Filename: "{sys}\sc.exe"; Parameters: "start v2rayA"; Flags: runhidden waituntilterminated
[UninstallRun]
; 停止并删除服务
@@ -168,12 +166,21 @@ begin
end;
end;
procedure StartService();
var
ResultCode: Integer;
begin
Exec(ExpandConstant('{sys}\sc.exe'), 'start v2rayA', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
CreateEnvConfigFile();
AddBinToPath();
// PATH 修改完成后再启动服务
StartService();
end;
end;
+2 -6
View File
@@ -1,8 +1,9 @@
package common
import (
"github.com/v2rayA/v2rayA/db/configure"
"testing"
"github.com/v2rayA/v2rayA/db/configure"
)
func TestUrlEncoded(t *testing.T) {
@@ -24,16 +25,11 @@ func TestFillEmpty(t *testing.T) {
Transparent: "10",
IpForward: false,
PortSharing: false,
SpecialMode: "",
TransparentType: "",
AntiPollution: "",
}
if err := FillEmpty(setting, configure.NewSetting()); err != nil {
t.Fatal(err)
}
if setting.SpecialMode != configure.NewSetting().SpecialMode {
t.Fatal()
}
emptySetting := &configure.Setting{}
if err := FillEmpty(emptySetting, configure.NewSetting()); err != nil {
t.Fatal(err)
+5
View File
@@ -23,6 +23,7 @@ type Params struct {
CoreStartupTimeout int64 `id:"core-startup-timeout" default:"15" desc:"Timeout duration in seconds for starting v2ray or xray core. On devices with lower performance, consider increasing this value."`
TransparentHook string `id:"transparent-hook" desc:"the executable file to run in the transparent proxy life-cycle. v2rayA will pass in the --transparent-type (tproxy, redirect) and --stage (pre-start, post-start, pre-stop, post-stop) arguments."`
CoreHook string `id:"core-hook" desc:"the executable file to run in the v2ray-core life-cycle. v2rayA will pass in the --stage (pre-start, post-start, pre-stop, post-stop) argument."`
TinyTunBin string `id:"tinytun-bin" desc:"Executable tinytun binary path for TUN transparent proxy mode. Auto-detect from PATH if empty."`
PluginManager string `id:"plugin-manager" desc:"the executable file to run in the v2ray-core life-cycle. v2rayA will pass in the --stage (pre-start, post-start, pre-stop, post-stop) argument."`
WebDir string `id:"webdir" desc:"v2rayA web files directory. use embedded files if not specify."`
IPV6Support string `id:"ipv6-support" default:"auto" desc:"Optional values: auto, on, off. Make sure your IPv6 network works fine before you turn it on."`
@@ -66,6 +67,8 @@ func initFunc() {
if params.Config == "" {
params.Config = defaultConfigDir(params.Lite)
}
// Fix mis-detected Linux-style config path on Windows
params.Config = sanitizeConfigDirForPlatform(params.Config, params.Lite)
// replace all dots of the filename with underlines
params.Config = filepath.Join(
filepath.Dir(params.Config),
@@ -81,6 +84,8 @@ func initFunc() {
params.Config = strings.ReplaceAll(params.Config, "$HOME", h)
}
}
// 展开各路径参数中的平台相关环境变量(Windows 上处理 %VAR% 风格)
expandPlatformConfigPaths(&params)
if _, err := os.Stat(params.Config); os.IsNotExist(err) {
_ = os.MkdirAll(params.Config, os.ModeDir|0750)
} else if err != nil {
@@ -12,6 +12,12 @@ func loadPlatformEnv() error {
return nil
}
func expandPlatformConfigPaths(p *Params) {}
func sanitizeConfigDirForPlatform(config string, _ bool) string {
return config
}
func defaultConfigDir(isLite bool) string {
if isLite {
if userConfigDir, err := os.UserConfigDir(); err == nil {
@@ -21,6 +21,42 @@ import (
var platformEnvOnce sync.Once
// expandWindowsEnv 使用 Windows API 展开 %VAR% 风格的环境变量引用
func expandWindowsEnv(s string) string {
if !strings.Contains(s, "%") {
return s
}
utf16Src, err := windows.UTF16FromString(s)
if err != nil {
return s
}
// 先探测所需缓冲区大小
n, _ := windows.ExpandEnvironmentStrings(&utf16Src[0], nil, 0)
if n == 0 {
return s
}
buf := make([]uint16, n)
n, err = windows.ExpandEnvironmentStrings(&utf16Src[0], &buf[0], n)
if err != nil || n == 0 {
return s
}
return windows.UTF16ToString(buf[:n])
}
// expandPlatformConfigPaths 展开 Params 中所有路径字段里的 Windows %VAR% 环境变量
func expandPlatformConfigPaths(p *Params) {
p.Config = expandWindowsEnv(p.Config)
p.V2rayBin = expandWindowsEnv(p.V2rayBin)
p.V2rayConfigDirectory = expandWindowsEnv(p.V2rayConfigDirectory)
p.V2rayAssetsDirectory = expandWindowsEnv(p.V2rayAssetsDirectory)
p.LogFile = expandWindowsEnv(p.LogFile)
p.TransparentHook = expandWindowsEnv(p.TransparentHook)
p.CoreHook = expandWindowsEnv(p.CoreHook)
p.PluginManager = expandWindowsEnv(p.PluginManager)
p.WinEnvFile = expandWindowsEnv(p.WinEnvFile)
p.WebDir = expandWindowsEnv(p.WebDir)
}
func loadPlatformEnv() error {
var loadErr error
platformEnvOnce.Do(func() {
@@ -95,6 +131,9 @@ func loadPlatformEnv() error {
}
}
// 展开值中 %VAR% 风格的 Windows 环境变量引用
value = expandWindowsEnv(value)
if err := os.Setenv(key, value); err != nil {
log.Warn("Failed to set env var %s: %v", key, err)
} else {
@@ -172,3 +211,18 @@ func defaultConfigDir(isLite bool) string {
return filepath.Join(os.TempDir(), "v2rayA")
}
// sanitizeConfigDirForPlatform avoids creating Linux-style paths like C:\\etc\\v2raya on Windows
func sanitizeConfigDirForPlatform(config string, isLite bool) string {
cleaned := filepath.Clean(config)
normalized := strings.ToLower(filepath.ToSlash(cleaned))
// Detect Linux default path on Windows where volume is missing and path starts with /etc/v2raya
if filepath.VolumeName(cleaned) == "" && strings.HasPrefix(normalized, "/etc/v2raya") {
fallback := defaultConfigDir(isLite)
log.Warn("Detected Linux-style config path on Windows (%s); falling back to %s", config, fallback)
return fallback
}
return config
}
+3 -13
View File
@@ -8,7 +8,6 @@ import (
"github.com/v2rayA/v2rayA/common/cmds"
"github.com/v2rayA/v2rayA/core/v2ray/asset"
"github.com/v2rayA/v2rayA/db/configure"
)
var (
@@ -40,9 +39,6 @@ func (t *legacyTproxy) AddIPWhitelist(cidr string) {
// avoid duplication
t.RemoveIPWhitelist(cidr)
pos := 7
if configure.GetSettingNotNil().AntiPollution != configure.AntipollutionClosed {
pos += 3
}
if notSkip, _ := strconv.ParseBool(TproxyNotSkipBr); notSkip {
pos--
}
@@ -95,13 +91,11 @@ iptables -w 2 -t mangle -A TP_RULE -m mark --mark 0x40/0xc0 -j RETURN
for _, v := range GetExcludedInterfaces() {
commands += fmt.Sprintf("iptables -w 2 -t mangle -A TP_RULE -i %s -j RETURN\n", strings.ReplaceAll(v, "*", "+"))
}
if configure.GetSettingNotNil().AntiPollution != configure.AntipollutionClosed {
commands += `
commands += `
iptables -w 2 -t mangle -A TP_RULE -p udp --dport 53 -j TP_MARK
iptables -w 2 -t mangle -A TP_RULE -p tcp --dport 53 -j TP_MARK
iptables -w 2 -t mangle -A TP_RULE -m mark --mark 0x40/0xc0 -j RETURN
`
}
if IsEnabledTproxyWhiteIpGroups() {
whiteIpv4List, _ := GetWhiteListIPs()
@@ -145,13 +139,11 @@ ip6tables -w 2 -t mangle -A TP_RULE -m mark --mark 0x40/0xc0 -j RETURN
for _, v := range GetExcludedInterfaces() {
commands += fmt.Sprintf("ip6tables -w 2 -t mangle -A TP_RULE -i %s -j RETURN\n", strings.ReplaceAll(v, "*", "+"))
}
if configure.GetSettingNotNil().AntiPollution != configure.AntipollutionClosed {
commands += `
commands += `
ip6tables -w 2 -t mangle -A TP_RULE -p udp --dport 53 -j TP_MARK
ip6tables -w 2 -t mangle -A TP_RULE -p tcp --dport 53 -j TP_MARK
ip6tables -w 2 -t mangle -A TP_RULE -m mark --mark 0x40/0xc0 -j RETURN
`
}
if IsEnabledTproxyWhiteIpGroups() {
_, whiteIpv6List := GetWhiteListIPs()
for _, v := range whiteIpv6List {
@@ -322,12 +314,10 @@ func (t *nftTproxy) GetSetupCommands() Setter {
}
}
`
if configure.GetSettingNotNil().AntiPollution != configure.AntipollutionClosed {
table = strings.ReplaceAll(table, "# anti-pollution", `
table = strings.ReplaceAll(table, "# anti-pollution", `
meta l4proto { tcp, udp } th dport 53 jump tp_mark
meta mark & 0xc0 == 0x40 return
`)
}
if !IsIPv6Supported() {
// drop ipv6 packets hooks
@@ -36,6 +36,12 @@ type PriorInfo struct {
CoreVersion string
Tag string
PluginPort int
Backend string // effective backend: "v2ray" or "" (daeuniverse/system default)
}
// BackendGetter is implemented by server types that support backend selection.
type BackendGetter interface {
GetBackend() string
}
func (info *PriorInfo) PluginObj() coreObj.OutboundObject {

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