mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Sun Apr 19 21:03:21 CEST 2026
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
Generated
+17
-7
@@ -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,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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+24
-24
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+20
-11
@@ -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
|
||||
|
||||
|
||||
+17
-30
@@ -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)
|
||||
|
||||
+8
-27
@@ -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" })
|
||||
|
||||
+16
-6
@@ -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)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
|
||||
local json = require "luci.jsonc"
|
||||
|
||||
-- 读取值(keylist/vallist/group)
|
||||
local values = {}
|
||||
for i, key in pairs(self.keylist) do
|
||||
values[#values + 1] = {
|
||||
key = key,
|
||||
label = self.vallist[i] or key,
|
||||
group = self.group and self.group[i] or nil
|
||||
}
|
||||
end
|
||||
|
||||
-- 获取当前配置值
|
||||
local current_key = {}
|
||||
local cval = self:cfgvalue(section)
|
||||
if type(cval) == "table" then
|
||||
for _, v in ipairs(cval) do
|
||||
for item in string.gmatch(v, "[^,]+") do
|
||||
current_key[#current_key+1] = item
|
||||
end
|
||||
end
|
||||
elseif type(cval) == "string" then
|
||||
for item in string.gmatch(cval, "[^,]+") do
|
||||
current_key[#current_key+1] = item
|
||||
end
|
||||
end
|
||||
|
||||
-- 分离无组节点(ungrouped)与有组节点(grouped),并保持原有顺序
|
||||
local ungrouped = {}
|
||||
local groups = {}
|
||||
local group_order = {}
|
||||
for _, item in ipairs(values) do
|
||||
if not item.group or item.group == "" then
|
||||
table.insert(ungrouped, item)
|
||||
else
|
||||
local g = item.group
|
||||
if not groups[g] then
|
||||
groups[g] = {}
|
||||
table.insert(group_order, g)
|
||||
end
|
||||
table.insert(groups[g], item)
|
||||
end
|
||||
end
|
||||
|
||||
-- 打包数据给 JS
|
||||
local dropdown_data = {
|
||||
current_key = current_key,
|
||||
ungrouped = ungrouped,
|
||||
groups = groups,
|
||||
group_order = group_order,
|
||||
cbid = cbid,
|
||||
}
|
||||
%>
|
||||
<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%>
|
||||
+654
@@ -0,0 +1,654 @@
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- - snowie2000
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
-- It is the common part of the template and cannot be used independently
|
||||
%>
|
||||
<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 => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
}[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("&", "&"):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
|
||||
|
||||
Generated
+19
-19
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -21,4 +21,4 @@ yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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;
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
@@ -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>
|
||||
@@ -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:
|
||||
"★supervisor:Monitor dns pollution, intercept in advance, use the sniffing mechanism of v2ray-core to prevent pollution. ★fakedns:Use 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",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
"★supervisor:Monitor dns pollution, intercept in advance, use the sniffing mechanism of v2ray-core to prevent pollution. ★fakedns:Use 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",
|
||||
|
||||
@@ -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:
|
||||
"★supervisor:Monitora a poluição dns, intercepta antecipadamente, usa o mecanismo de sniffing do v2ray-core para prevenir a poluição. ★fakedns:Use 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",
|
||||
|
||||
@@ -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: "Настройка белового списка портов",
|
||||
|
||||
@@ -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: "eBPF(cgroupv2 进程排除)",
|
||||
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
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(¶ms)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user