Update On Thu Mar 26 20:20:16 CET 2026

This commit is contained in:
github-action[bot]
2026-03-26 20:20:17 +01:00
parent 24b9e30091
commit 766fee8b2d
626 changed files with 30055 additions and 4903 deletions
+1
View File
@@ -1310,3 +1310,4 @@ Update On Sun Mar 22 19:50:55 CET 2026
Update On Mon Mar 23 20:06:04 CET 2026
Update On Tue Mar 24 20:16:12 CET 2026
Update On Wed Mar 25 20:07:22 CET 2026
Update On Thu Mar 26 20:20:07 CET 2026
+344
View File
@@ -0,0 +1,344 @@
From f4de14a515221e27c0d79446b423849a6546e3a6 Mon Sep 17 00:00:00 2001
From: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue, 24 Mar 2026 23:02:09 +0000
Subject: [PATCH] runtime: use uname version check for 64-bit time on 32-bit
arch codepaths
The previous fallback-on-ENOSYS logic causes issues on forks of Linux.
Android: #77621 (CL 750040 added a workaround with a TODO,
this fixes that TODO)
Causes the OS to terminate the program when running on Android
versions <=10 since the seccomp jail does not know about the 64-bit
time syscall and is configured to terminate the program on any
unknown syscall.
Synology's Linux: #77930
On old versions of Synology's Linux they added custom vendor syscalls
without adding a gap in the syscall numbers, that means when we call
the newer Linux syscall which was added later, Synology's Linux
interprets it as a completely different vendor syscall.
Originally by Jorropo in CL 751340.
Fixes golang/go#77930
Updates tailscale/go#162
Originally https://go-review.googlesource.com/c/go/+/758902/2
Co-authored-by: Jorropo <jorropo.pgm@gmail.com>
Change-Id: I90e15495d9249fd7f6e112f9e3ae8ad1322f56e0
---
.../runtime/syscall/linux/defs_linux_386.go | 1 +
.../runtime/syscall/linux/defs_linux_amd64.go | 1 +
.../runtime/syscall/linux/defs_linux_arm.go | 1 +
.../runtime/syscall/linux/defs_linux_arm64.go | 1 +
.../syscall/linux/defs_linux_loong64.go | 1 +
.../syscall/linux/defs_linux_mips64x.go | 1 +
.../runtime/syscall/linux/defs_linux_mipsx.go | 1 +
.../syscall/linux/defs_linux_ppc64x.go | 1 +
.../syscall/linux/defs_linux_riscv64.go | 1 +
.../runtime/syscall/linux/defs_linux_s390x.go | 1 +
.../runtime/syscall/linux/syscall_linux.go | 14 +++++
src/runtime/os_linux.go | 62 +++++++++++++++++++
src/runtime/os_linux32.go | 28 +++------
src/runtime/os_linux64.go | 2 +
14 files changed, 97 insertions(+), 19 deletions(-)
diff --git a/src/internal/runtime/syscall/linux/defs_linux_386.go b/src/internal/runtime/syscall/linux/defs_linux_386.go
index 7fdf5d3f8062fa..4e8e645dc49a66 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_386.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_386.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 295
SYS_PREAD64 = 180
SYS_READ = 3
+ SYS_UNAME = 122
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_amd64.go b/src/internal/runtime/syscall/linux/defs_linux_amd64.go
index 2c8676e6e9b4d9..fa764d9ccd9b8e 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_amd64.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_amd64.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 257
SYS_PREAD64 = 17
SYS_READ = 0
+ SYS_UNAME = 63
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm.go b/src/internal/runtime/syscall/linux/defs_linux_arm.go
index a0b395d6762734..cef556d5f6f986 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_arm.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_arm.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 322
SYS_PREAD64 = 180
SYS_READ = 3
+ SYS_UNAME = 122
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm64.go b/src/internal/runtime/syscall/linux/defs_linux_arm64.go
index 223dce0c5b4281..eabddbac1bc063 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_arm64.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_arm64.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 56
SYS_PREAD64 = 67
SYS_READ = 63
+ SYS_UNAME = 160
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_loong64.go b/src/internal/runtime/syscall/linux/defs_linux_loong64.go
index 8aa61c391dcdcb..08e5d49b83c9bd 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_loong64.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_loong64.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 56
SYS_PREAD64 = 67
SYS_READ = 63
+ SYS_UNAME = 160
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
index 84b760dc1b5545..b5794e5002af5e 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
@@ -19,6 +19,7 @@ const (
SYS_OPENAT = 5247
SYS_PREAD64 = 5016
SYS_READ = 5000
+ SYS_UNAME = 5061
EFD_NONBLOCK = 0x80
diff --git a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
index a9be21414c26f9..1fb4d919d1a318 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
@@ -19,6 +19,7 @@ const (
SYS_OPENAT = 4288
SYS_PREAD64 = 4200
SYS_READ = 4003
+ SYS_UNAME = 4122
EFD_NONBLOCK = 0x80
diff --git a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
index 63f4e5d7864de4..ee93ad345b810f 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
@@ -19,6 +19,7 @@ const (
SYS_OPENAT = 286
SYS_PREAD64 = 179
SYS_READ = 3
+ SYS_UNAME = 122
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
index 8aa61c391dcdcb..08e5d49b83c9bd 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 56
SYS_PREAD64 = 67
SYS_READ = 63
+ SYS_UNAME = 160
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_s390x.go b/src/internal/runtime/syscall/linux/defs_linux_s390x.go
index 52945db0e5b72f..da11c704081abc 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_s390x.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_s390x.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 288
SYS_PREAD64 = 180
SYS_READ = 3
+ SYS_UNAME = 122
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/syscall_linux.go b/src/internal/runtime/syscall/linux/syscall_linux.go
index 8201e7d1907444..b64f511b03c947 100644
--- a/src/internal/runtime/syscall/linux/syscall_linux.go
+++ b/src/internal/runtime/syscall/linux/syscall_linux.go
@@ -86,3 +86,17 @@ func Pread(fd int, p []byte, offset int64) (n int, errno uintptr) {
}
return int(r1), e
}
+
+type Utsname struct {
+ Sysname [65]byte
+ Nodename [65]byte
+ Release [65]byte
+ Version [65]byte
+ Machine [65]byte
+ Domainname [65]byte
+}
+
+func Uname(buf *Utsname) (errno uintptr) {
+ _, _, e := Syscall6(SYS_UNAME, uintptr(unsafe.Pointer(buf)), 0, 0, 0, 0, 0)
+ return e
+}
diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go
index 7e6af22d48a764..493567b5303673 100644
--- a/src/runtime/os_linux.go
+++ b/src/runtime/os_linux.go
@@ -354,6 +354,7 @@ func osinit() {
numCPUStartup = getCPUCount()
physHugePageSize = getHugePageSize()
vgetrandomInit()
+ configure64bitsTimeOn32BitsArchitectures()
}
var urandom_dev = []byte("/dev/urandom\x00")
@@ -935,3 +936,64 @@ func mprotect(addr unsafe.Pointer, n uintptr, prot int32) (ret int32, errno int3
r, _, err := linux.Syscall6(linux.SYS_MPROTECT, uintptr(addr), n, uintptr(prot), 0, 0, 0)
return int32(r), int32(err)
}
+
+type kernelVersion struct {
+ major int
+ minor int
+}
+
+// getKernelVersion returns major and minor kernel version numbers
+// parsed from the uname release field.
+func getKernelVersion() kernelVersion {
+ var buf linux.Utsname
+ if e := linux.Uname(&buf); e != 0 {
+ throw("uname failed")
+ }
+
+ rel := gostringnocopy(&buf.Release[0])
+ major, minor, _, ok := parseRelease(rel)
+ if !ok {
+ throw("failed to parse kernel version from uname")
+ }
+ return kernelVersion{major: major, minor: minor}
+}
+
+// parseRelease parses a dot-separated version number. It follows the
+// semver syntax, but allows the minor and patch versions to be
+// elided.
+func parseRelease(rel string) (major, minor, patch int, ok bool) {
+ // Strip anything after a dash or plus.
+ for i := 0; i < len(rel); i++ {
+ if rel[i] == '-' || rel[i] == '+' {
+ rel = rel[:i]
+ break
+ }
+ }
+
+ next := func() (int, bool) {
+ for i := 0; i < len(rel); i++ {
+ if rel[i] == '.' {
+ ver, err := strconv.Atoi(rel[:i])
+ rel = rel[i+1:]
+ return ver, err == nil
+ }
+ }
+ ver, err := strconv.Atoi(rel)
+ rel = ""
+ return ver, err == nil
+ }
+ if major, ok = next(); !ok || rel == "" {
+ return
+ }
+ if minor, ok = next(); !ok || rel == "" {
+ return
+ }
+ patch, ok = next()
+ return
+}
+
+// GE checks if the running kernel version
+// is greater than or equal to the provided version.
+func (kv kernelVersion) GE(x, y int) bool {
+ return kv.major > x || (kv.major == x && kv.minor >= y)
+}
diff --git a/src/runtime/os_linux32.go b/src/runtime/os_linux32.go
index 16de6fb350f624..02cb18f32d57d2 100644
--- a/src/runtime/os_linux32.go
+++ b/src/runtime/os_linux32.go
@@ -7,27 +7,25 @@
package runtime
import (
- "internal/runtime/atomic"
"unsafe"
)
+func configure64bitsTimeOn32BitsArchitectures() {
+ use64bitsTimeOn32bits = getKernelVersion().GE(5, 1)
+}
+
//go:noescape
func futex_time32(addr unsafe.Pointer, op int32, val uint32, ts *timespec32, addr2 unsafe.Pointer, val3 uint32) int32
//go:noescape
func futex_time64(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32
-var isFutexTime32bitOnly atomic.Bool
+var use64bitsTimeOn32bits bool
//go:nosplit
func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32 {
- if !isFutexTime32bitOnly.Load() {
- ret := futex_time64(addr, op, val, ts, addr2, val3)
- // futex_time64 is only supported on Linux 5.0+
- if ret != -_ENOSYS {
- return ret
- }
- isFutexTime32bitOnly.Store(true)
+ if use64bitsTimeOn32bits {
+ return futex_time64(addr, op, val, ts, addr2, val3)
}
// Downgrade ts.
var ts32 timespec32
@@ -45,17 +43,10 @@ func timer_settime32(timerid int32, flags int32, new, old *itimerspec32) int32
//go:noescape
func timer_settime64(timerid int32, flags int32, new, old *itimerspec) int32
-var isSetTime32bitOnly atomic.Bool
-
//go:nosplit
func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 {
- if !isSetTime32bitOnly.Load() {
- ret := timer_settime64(timerid, flags, new, old)
- // timer_settime64 is only supported on Linux 5.0+
- if ret != -_ENOSYS {
- return ret
- }
- isSetTime32bitOnly.Store(true)
+ if use64bitsTimeOn32bits {
+ return timer_settime64(timerid, flags, new, old)
}
var newts, oldts itimerspec32
@@ -73,6 +64,5 @@ func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 {
old32 = &oldts
}
- // Fall back to 32-bit timer
return timer_settime32(timerid, flags, new32, old32)
}
diff --git a/src/runtime/os_linux64.go b/src/runtime/os_linux64.go
index 7b70d80fbe5a89..f9571dd7586614 100644
--- a/src/runtime/os_linux64.go
+++ b/src/runtime/os_linux64.go
@@ -10,6 +10,8 @@ import (
"unsafe"
)
+func configure64bitsTimeOn32BitsArchitectures() {}
+
//go:noescape
func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32
+54 -42
View File
@@ -1,4 +1,8 @@
Subject: [PATCH] internal/poll: move rsan to heap on windows
From 1a44be4cecdc742ac6cce9825f9ffc19857c99f3 Mon Sep 17 00:00:00 2001
From: database64128 <free122448@hotmail.com>
Date: Mon, 9 Mar 2026 16:25:16 +0800
Subject: [PATCH] [release-branch.go1.26] internal/poll: move rsan to heap on
windows
According to https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom,
the memory pointed to by lpFromlen must remain available during the
@@ -14,20 +18,27 @@ with rsa in the same sync.Pool. The new wsaRsa struct is still in the
same size class, so no additional overhead is introduced by this
change.
Fixes #77975.
Fixes #78041.
Change-Id: I5ffbccb332515116ddc03fb7c40ffc9293cad2ab
Reviewed-on: https://go-review.googlesource.com/c/go/+/753040
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Commit-Queue: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/753480
Reviewed-by: Mark Freeman <markfreeman@google.com>
---
Index: src/internal/poll/fd_windows.go
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
--- a/src/internal/poll/fd_windows.go (revision 8149d992682ce76c6af804b507878e19fc966f7b)
+++ b/src/internal/poll/fd_windows.go (date 1773058735706)
@@ -149,7 +149,7 @@
src/internal/poll/fd_windows.go | 94 +++++++++++++++++++++------------
1 file changed, 59 insertions(+), 35 deletions(-)
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
index 2ba967f990982f..26319548e3c310 100644
--- a/src/internal/poll/fd_windows.go
+++ b/src/internal/poll/fd_windows.go
@@ -149,7 +149,7 @@ var wsaMsgPool = sync.Pool{
// newWSAMsg creates a new WSAMsg with the provided parameters.
// Use [freeWSAMsg] to free it.
-func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMsg {
@@ -35,8 +46,8 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
// The returned object can't be allocated in the stack because it is accessed asynchronously
// by Windows in between several system calls. If the stack frame is moved while that happens,
// then Windows may access invalid memory.
@@ -164,33 +164,45 @@
Buf: unsafe.SliceData(oob),
@@ -166,34 +166,46 @@ func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMs
}
}
msg.Flags = uint32(flags)
- if unconnected {
@@ -48,7 +59,7 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
}
return msg
}
func freeWSAMsg(msg *windows.WSAMsg) {
// Clear pointers to buffers so they can be released by garbage collector.
+ msg.Name = nil
@@ -65,7 +76,7 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
- }
wsaMsgPool.Put(msg)
}
+// wsaRsa bundles a [syscall.RawSockaddrAny] with its length for efficient caching.
+//
+// When used by WSARecvFrom, wsaRsa must be on the heap. See
@@ -81,20 +92,21 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ return new(wsaRsa)
},
}
+
+func newWSARsa() *wsaRsa {
+ rsa := wsaRsaPool.Get().(*wsaRsa)
+ rsa.name = syscall.RawSockaddrAny{}
+ rsa.namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
+ return rsa
+}
+
var operationPool = sync.Pool{
New: func() any {
@@ -737,19 +749,18 @@
return new(operation)
@@ -739,19 +751,18 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) {
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
@@ -113,11 +125,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ sa, _ := rsa.name.Sockaddr()
return n, sa, nil
}
@@ -768,19 +779,18 @@
@@ -770,19 +781,18 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
@@ -136,11 +148,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ rawToSockaddrInet4(&rsa.name, sa4)
return n, err
}
@@ -799,19 +809,18 @@
@@ -801,19 +811,18 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
@@ -159,11 +171,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ rawToSockaddrInet6(&rsa.name, sa6)
return n, err
}
@@ -1371,7 +1380,9 @@
@@ -1373,7 +1382,9 @@ func (fd *FD) ReadMsg(p []byte, oob []byte, flags int) (int, int, int, syscall.S
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
@@ -171,10 +183,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1396,7 +1407,9 @@
@@ -1398,7 +1409,9 @@ func (fd *FD) ReadMsgInet4(p []byte, oob []byte, flags int, sa4 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
@@ -182,10 +194,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1420,7 +1433,9 @@
@@ -1422,7 +1435,9 @@ func (fd *FD) ReadMsgInet6(p []byte, oob []byte, flags int, sa6 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
@@ -193,10 +205,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1444,15 +1459,18 @@
@@ -1446,15 +1461,18 @@ func (fd *FD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (int, int, err
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
@@ -215,10 +227,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1471,11 +1489,14 @@
@@ -1473,11 +1491,14 @@ func (fd *FD) WriteMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
@@ -233,10 +245,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1494,11 +1515,14 @@
@@ -1496,11 +1517,14 @@ func (fd *FD) WriteMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
@@ -250,4 +262,4 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
return qty, err
+13 -7
View File
@@ -173,14 +173,13 @@ jobs:
- name: Set up Go1.26 loongarch abi1
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
run: |
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0/go1.26.0.linux-amd64-abi1.tar.gz
mkdir -p $HOME/usr/local
tar zxf go1.26.0.linux-amd64-abi1.tar.gz -C $HOME/usr/local
echo "$HOME/usr/local/go/bin" >> $GITHUB_PATH
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
with:
go-download-base-url: 'https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0'
go-version: 1.26.0
# TODO: remove after issue77731 merged, see: https://github.com/golang/go/issues/77731
- name: Apply issue77731 for Golang1.26
# TODO: remove after issue77731 fixed, see: https://github.com/golang/go/issues/77731
- name: Fix issue77731 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
@@ -193,6 +192,13 @@ jobs:
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
# TODO: remove after issue77930 fixed, see: https://github.com/golang/go/issues/77930
- name: Fix issue77930 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77930.patch
# this patch file only works on golang1.26.x
# that means after golang1.27 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
@@ -1,14 +1,17 @@
package process
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"net/netip"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"unicode"
@@ -61,10 +64,29 @@ type inetDiagResponse struct {
func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) {
uid, inode, err := resolveSocketByNetlink(network, ip, srcPort)
if runtime.GOOS == "android" {
// on Android (especially recent releases), netlink INET_DIAG can fail or return UID 0 / empty process info for some apps
// so trying fallback to resolve /proc/net/{tcp,tcp6,udp,udp6}
if err != nil {
uid, inode, err = resolveSocketByProcFS(network, ip, srcPort)
} else if uid == 0 {
pUID, pInode, pErr := resolveSocketByProcFS(network, ip, srcPort)
if pErr == nil && pUID != 0 {
uid, inode, err = pUID, pInode, nil
}
}
}
if err != nil {
return 0, "", err
}
pp, err := resolveProcessNameByProcSearch(inode, uid)
if runtime.GOOS == "android" {
// if inode-based /proc/<pid>/fd resolution fails but UID is known,
// fall back to resolving the process/package name by UID (typical on Android where all app processes share one UID).
if err != nil && uid != 0 {
pp, err = resolveProcessNameByUID(uid)
}
}
return uid, pp, err
}
@@ -180,6 +202,47 @@ func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
}
// resolveProcessNameByUID returns a process name for any process with uid.
// On Android all processes of one app share the same UID; used when inode
// lookup fails (socket closed / TIME_WAIT).
func resolveProcessNameByUID(uid uint32) (string, error) {
files, err := os.ReadDir("/proc")
if err != nil {
return "", err
}
for _, f := range files {
if !f.IsDir() || !isPid(f.Name()) {
continue
}
info, err := f.Info()
if err != nil {
continue
}
if info.Sys().(*syscall.Stat_t).Uid != uid {
continue
}
processPath := filepath.Join("/proc", f.Name())
if runtime.GOOS == "android" {
cmdline, err := os.ReadFile(path.Join(processPath, "cmdline"))
if err != nil {
continue
}
if name := splitCmdline(cmdline); name != "" {
return name, nil
}
} else {
if exe, err := os.Readlink(filepath.Join(processPath, "exe")); err == nil {
return exe, nil
}
}
}
return "", fmt.Errorf("no process found with uid %d", uid)
}
func splitCmdline(cmdline []byte) string {
cmdline = bytes.Trim(cmdline, " ")
@@ -198,3 +261,196 @@ func isPid(s string) bool {
return !unicode.IsDigit(r)
}) == -1
}
// resolveSocketByProcFS finds UID and inode from /proc/net/{tcp,tcp6,udp,udp6}.
// In TUN mode metadata sourceIP is often the gateway (e.g. fake-ip range), not
// the socket's real local address; we match by local port first and prefer
// exact IP+port when it matches.
func resolveSocketByProcFS(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) {
var proto string
switch {
case strings.HasPrefix(network, "tcp"):
proto = "tcp"
case strings.HasPrefix(network, "udp"):
proto = "udp"
default:
return 0, 0, ErrInvalidNetwork
}
targetPort := uint16(srcPort)
unmapped := ip.Unmap()
files := []string{"/proc/net/" + proto, "/proc/net/" + proto + "6"}
var bestUID, bestInode uint32
found := false
for _, path := range files {
isV6 := strings.HasSuffix(path, "6")
var matchIP netip.Addr
if unmapped.Is4() {
if isV6 {
matchIP = netip.AddrFrom16(unmapped.As16())
} else {
matchIP = unmapped
}
} else {
if !isV6 {
continue
}
matchIP = unmapped
}
uid, inode, exact, err := searchProcNetFileByPort(path, matchIP, targetPort)
if err != nil {
continue
}
if exact {
return uid, inode, nil
}
if !found || (bestUID == 0 && uid != 0) {
bestUID = uid
bestInode = inode
found = true
}
}
if found {
return bestUID, bestInode, nil
}
return 0, 0, ErrNotFound
}
// searchProcNetFileByPort scans /proc/net/* for local_address matching targetPort.
// Exact IP+port wins; else port-only (skips inode==0 entries used by TIME_WAIT).
func searchProcNetFileByPort(path string, targetIP netip.Addr, targetPort uint16) (uid, inode uint32, exact bool, err error) {
f, err := os.Open(path)
if err != nil {
return 0, 0, false, err
}
defer f.Close()
isV6 := strings.HasSuffix(path, "6")
scanner := bufio.NewScanner(f)
if !scanner.Scan() {
return 0, 0, false, ErrNotFound
}
var bestUID, bestInode uint32
found := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 10 {
continue
}
lineIP, linePort, parseErr := parseHexAddrPort(fields[1], isV6)
if parseErr != nil {
continue
}
if linePort != targetPort {
continue
}
lineUID, parseErr := strconv.ParseUint(fields[7], 10, 32)
if parseErr != nil {
continue
}
lineInode, parseErr := strconv.ParseUint(fields[9], 10, 32)
if parseErr != nil {
continue
}
if lineIP == targetIP {
return uint32(lineUID), uint32(lineInode), true, nil
}
if lineInode == 0 {
continue
}
if !found || (bestUID == 0 && lineUID != 0) {
bestUID = uint32(lineUID)
bestInode = uint32(lineInode)
found = true
}
}
if found {
return bestUID, bestInode, false, nil
}
return 0, 0, false, ErrNotFound
}
func parseHexAddrPort(s string, isV6 bool) (netip.Addr, uint16, error) {
colon := strings.IndexByte(s, ':')
if colon < 0 {
return netip.Addr{}, 0, fmt.Errorf("invalid addr:port: %s", s)
}
port64, err := strconv.ParseUint(s[colon+1:], 16, 16)
if err != nil {
return netip.Addr{}, 0, err
}
var addr netip.Addr
if isV6 {
addr, err = parseHexIPv6(s[:colon])
} else {
addr, err = parseHexIPv4(s[:colon])
}
return addr, uint16(port64), err
}
func parseHexIPv4(s string) (netip.Addr, error) {
if len(s) != 8 {
return netip.Addr{}, fmt.Errorf("invalid ipv4 hex len: %d", len(s))
}
b, err := hex.DecodeString(s)
if err != nil {
return netip.Addr{}, err
}
var ip [4]byte
if littleEndian {
ip[0], ip[1], ip[2], ip[3] = b[3], b[2], b[1], b[0]
} else {
copy(ip[:], b)
}
return netip.AddrFrom4(ip), nil
}
func parseHexIPv6(s string) (netip.Addr, error) {
if len(s) != 32 {
return netip.Addr{}, fmt.Errorf("invalid ipv6 hex len: %d", len(s))
}
var ip [16]byte
for i := 0; i < 4; i++ {
b, err := hex.DecodeString(s[i*8 : (i+1)*8])
if err != nil {
return netip.Addr{}, err
}
if littleEndian {
ip[i*4+0] = b[3]
ip[i*4+1] = b[2]
ip[i*4+2] = b[1]
ip[i*4+3] = b[0]
} else {
copy(ip[i*4:(i+1)*4], b)
}
}
return netip.AddrFrom16(ip), nil
}
var littleEndian = func() bool {
x := uint32(0x01020304)
return *(*byte)(unsafe.Pointer(&x)) == 0x04
}()
+43 -95
View File
@@ -379,7 +379,7 @@ dependencies = [
"objc2-foundation 0.3.2",
"parking_lot",
"percent-encoding",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -1743,7 +1743,7 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1789,12 +1789,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "constant_time_eq"
version = "0.4.2"
@@ -2360,7 +2354,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2430,7 +2424,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.8",
]
[[package]]
@@ -2889,7 +2883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -4913,7 +4907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.53.3",
]
[[package]]
@@ -5015,16 +5009,6 @@ dependencies = [
"which 7.0.3",
]
[[package]]
name = "lzma-rust2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a"
dependencies = [
"crc",
"sha2 0.10.9",
]
[[package]]
name = "lzma-rust2"
version = "0.16.1"
@@ -6323,7 +6307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.45.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6383,9 +6367,9 @@ dependencies = [
[[package]]
name = "oxc_allocator"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17ece0d1edc5e92822be95428460bc6b12f0dce8f95a9efabf751189a75f9f2"
checksum = "ff805b88789451a080b3c4d49fa0ebcd02dc6c0e370ed7a37ef954fbaf79915f"
dependencies = [
"allocator-api2",
"hashbrown 0.16.1",
@@ -6395,9 +6379,9 @@ dependencies = [
[[package]]
name = "oxc_ast"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ec0e9560cce8917197c7b13be7288707177f48a6f0ca116d0b53689e18bbc3"
checksum = "addc03b644cd9f26996bb32883f5cf4f4e46a51d20f5fbdbf675c14b29d38e95"
dependencies = [
"bitflags 2.10.0",
"oxc_allocator",
@@ -6412,9 +6396,9 @@ dependencies = [
[[package]]
name = "oxc_ast_macros"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f266c05258e76cb84d7eee538e4fc75e2687f4220e1b2f141c490b35025a6443"
checksum = "5950f9746248c26af04811e6db0523d354080637995be1dcc1c6bd3fca893bb2"
dependencies = [
"phf 0.13.1",
"proc-macro2",
@@ -6424,9 +6408,9 @@ dependencies = [
[[package]]
name = "oxc_ast_visit"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3477ca0b6dd5bebcb1d3bf4c825b65999975d6ca91d6f535bf067e979fad113a"
checksum = "31da485219d7ca6810872ce84fbcc7d11d8492145012603ead79beaf1476dc92"
dependencies = [
"oxc_allocator",
"oxc_ast",
@@ -6436,15 +6420,15 @@ dependencies = [
[[package]]
name = "oxc_data_structures"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8701946f2acbd655610a331cf56f0aa58349ef792e6bf2fb65c56785b87fe8e"
checksum = "623bffc9732a0d39f248a2e7655d6d1704201790e5a8777aa188a678f1746fe8"
[[package]]
name = "oxc_diagnostics"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b04ea16e6016eceb281fb61bbac5f860f075864e93ae15ec18b6c2d0b152e435"
checksum = "3c612203fb402e998169c3e152a9fc8e736faafea0f13287c92144d4b8bc7b55"
dependencies = [
"cow-utils",
"oxc-miette",
@@ -6453,9 +6437,9 @@ dependencies = [
[[package]]
name = "oxc_ecmascript"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4b107cae9b8bce541a45463623e1c4b1bb073e81d966483720f0e831facdcb1"
checksum = "04c62e45b93f4257f5ca6d00f441e669ad52d98d36332394abe9f5527cf461d6"
dependencies = [
"cow-utils",
"num-bigint",
@@ -6469,9 +6453,9 @@ dependencies = [
[[package]]
name = "oxc_estree"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b79c9e9684eab83293d67dcbbfd2b1a1f062d27a8188411eb700c6e17983fa"
checksum = "8794e3fbcd834e8ae4246dbd3121f9ee82c6ae60bc92615a276d42b6b62a2341"
[[package]]
name = "oxc_index"
@@ -6485,9 +6469,9 @@ dependencies = [
[[package]]
name = "oxc_parser"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f41bdacb3ef9afd8c5b0cb5beceec3ac4ecd0c348804aa1907606d370c731"
checksum = "041125897019b72d23e6549d95985fe379354cf004e69cb811803109375fa91b"
dependencies = [
"bitflags 2.10.0",
"cow-utils",
@@ -6508,9 +6492,9 @@ dependencies = [
[[package]]
name = "oxc_regular_expression"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d495c085efbde1d65636497f9d3e3e58151db614a97e313e2e7a837d81865419"
checksum = "405e9515c3ae4c7227b3596219ec256dd883cb403db3a0d1c10146f82a894c93"
dependencies = [
"bitflags 2.10.0",
"oxc_allocator",
@@ -6524,9 +6508,9 @@ dependencies = [
[[package]]
name = "oxc_span"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3edcf2bc8bc73cd8d252650737ef48a482484a91709b7f7a5c5ce49305f247e8"
checksum = "894327633e5dcaef8baf34815d68100297f9776e20371502458ea3c42b8a710b"
dependencies = [
"compact_str",
"oxc-miette",
@@ -6538,9 +6522,9 @@ dependencies = [
[[package]]
name = "oxc_str"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c60f1570f04257d5678a16391f6d18dc805325e7f876b8e176a3a36fe897be"
checksum = "50e0b900b4f66db7d5b46a454532464861f675d03e16994040484d2c04151490"
dependencies = [
"compact_str",
"hashbrown 0.16.1",
@@ -6550,9 +6534,9 @@ dependencies = [
[[package]]
name = "oxc_syntax"
version = "0.121.0"
version = "0.122.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a10c19c89298c0b126d12c5f545786405efbad9012d956ebb3190b64b29905a"
checksum = "0a5edd0173b4667e5a1775b5d37e06a78c796fab18ee095739186831f2c54400"
dependencies = [
"bitflags 2.10.0",
"cow-utils",
@@ -7305,7 +7289,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -7961,7 +7945,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -8019,7 +8003,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -9709,7 +9693,7 @@ dependencies = [
"getrandom 0.4.1",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -11501,7 +11485,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -12718,20 +12702,6 @@ name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "zerotrie"
@@ -12778,28 +12748,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "zip"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b"
dependencies = [
"aes",
"arbitrary",
"constant_time_eq 0.3.1",
"crc32fast",
"flate2",
"getrandom 0.3.3",
"hmac",
"indexmap 2.13.0",
"lzma-rust2 0.13.0",
"memchr",
"pbkdf2",
"sha1",
"zeroize",
"zopfli",
]
[[package]]
name = "zip"
version = "8.4.0"
@@ -12808,14 +12756,14 @@ checksum = "7756d0206d058333667493c4014f545f4b9603c4330ccd6d9b3f86dcab59f7d9"
dependencies = [
"aes",
"bzip2",
"constant_time_eq 0.4.2",
"constant_time_eq",
"crc32fast",
"deflate64",
"flate2",
"getrandom 0.4.1",
"hmac",
"indexmap 2.13.0",
"lzma-rust2 0.16.1",
"lzma-rust2",
"memchr",
"pbkdf2",
"ppmd-rust",
@@ -12829,12 +12777,12 @@ dependencies = [
[[package]]
name = "zip-extensions"
version = "0.13.1"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9bb4da4a220bfb79c2b7bfa88466181778892ddaffaca9f06d5e9cc5de0b46f"
checksum = "285251480403d7097be64d5c33518279ecf5f816d5ebe403c476e45ea16c5628"
dependencies = [
"ignore",
"zip 6.0.0",
"zip 8.4.0",
]
[[package]]
+7 -7
View File
@@ -107,7 +107,7 @@ semver = "1.0"
# Compression & Encoding
flate2 = "1.0"
zip = "8.0.0"
zip-extensions = "0.13.0"
zip-extensions = "0.14.0"
base64 = "0.22"
adler = "1.0.2"
hex = "0.4"
@@ -169,12 +169,12 @@ display-info = "0.5.0" # should be removed after upgrading to tauri v2
# OXC (The Oxidation Compiler)
# We use it to parse and transpile the old script profile to esm based script profile
oxc_parser = "0.121"
oxc_allocator = "0.121"
oxc_span = "0.121"
oxc_ast = "0.121"
oxc_syntax = "0.121"
oxc_ast_visit = "0.121"
oxc_parser = "0.122"
oxc_allocator = "0.122"
oxc_span = "0.122"
oxc_ast = "0.122"
oxc_syntax = "0.122"
oxc_ast_visit = "0.122"
# Lua Integration
mlua = { version = "0.11", features = [
@@ -155,7 +155,19 @@ async fn subscribe_url(
url: url.to_string(),
source: e,
})?;
let perform_req = || async { client.get(url.as_str()).send().await?.error_for_status() };
let device_info = crate::utils::hwid::get_device_info();
let sanitize = crate::utils::hwid::sanitize_for_header;
let perform_req = || async {
client
.get(url.as_str())
.header("x-hwid", &device_info.hwid)
.header("x-device-os", sanitize(&device_info.device_os))
.header("x-ver-os", sanitize(&device_info.os_version))
.header("x-device-model", sanitize(&device_info.device_model))
.send()
.await?
.error_for_status()
};
let resp = perform_req
.retry(backon::ExponentialBuilder::default())
// Only retry on network errors or server errors
+6
View File
@@ -100,6 +100,12 @@ pub fn is_portable() -> Result<bool> {
Ok(false)
}
#[tauri::command]
#[specta::specta]
pub fn get_device_info() -> Result<crate::utils::hwid::DeviceInfo> {
Ok(crate::utils::hwid::get_device_info())
}
#[tauri::command]
#[specta::specta]
pub async fn enhance_profiles() -> Result {
+1
View File
@@ -249,6 +249,7 @@ pub fn run() -> std::io::Result<()> {
ipc::service::stop_service,
ipc::service::restart_service,
ipc::is_portable,
ipc::get_device_info,
ipc::get_proxies,
ipc::select_proxy,
ipc::update_proxy_provider,
@@ -0,0 +1,270 @@
use anyhow::{Result, anyhow};
use once_cell::sync::Lazy;
use serde::Serialize;
use sha2::{Digest, Sha256};
use specta::Type;
#[derive(Debug, Clone, Serialize, Type)]
pub struct DeviceInfo {
pub hwid: String,
pub device_os: String,
pub os_version: String,
pub device_model: String,
}
impl Default for DeviceInfo {
fn default() -> Self {
Self {
hwid: generate_fallback_hwid(),
device_os: get_device_os().to_string(),
os_version: get_os_version(),
device_model: get_device_model(),
}
}
}
/// Cached device info — computed once, reused for all subscription requests.
/// Starts from `Default` (which already has real OS/model data and a fallback HWID),
/// then tries to replace the HWID with a platform-specific one.
pub static DEVICE_INFO: Lazy<DeviceInfo> = Lazy::new(|| {
let mut info = DeviceInfo::default();
match get_platform_hwid() {
Ok(hwid) => {
tracing::debug!(
"HWID generated: {}...{}",
&hwid[..4],
&hwid[hwid.len() - 4..]
);
info.hwid = hwid;
}
Err(e) => {
tracing::error!("Failed to generate platform HWID, using fallback: {e:?}");
}
}
info
});
/// Public accessor that returns a clone of the cached DeviceInfo.
pub fn get_device_info() -> DeviceInfo {
DEVICE_INFO.clone()
}
/// Generates a platform-specific HWID from the machine ID.
fn get_platform_hwid() -> Result<String> {
let raw_id = get_platform_machine_id()?;
let salted = format!("clash-nyanpasu:{}", raw_id);
let mut hasher = Sha256::new();
hasher.update(salted.as_bytes());
let hash = hasher.finalize();
Ok(hex::encode(&hash[..16])) // 32 hex chars
}
// ---------------------------------------------------------------------------
// Platform machine ID
// ---------------------------------------------------------------------------
#[cfg(target_os = "windows")]
fn get_platform_machine_id() -> Result<String> {
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let key = hklm
.open_subkey("SOFTWARE\\Microsoft\\Cryptography")
.map_err(|e| anyhow!("Failed to open Cryptography registry key: {e}"))?;
let guid: String = key
.get_value("MachineGuid")
.map_err(|e| anyhow!("Failed to read MachineGuid: {e}"))?;
if guid.is_empty() {
return Err(anyhow!("MachineGuid is empty"));
}
Ok(guid)
}
#[cfg(target_os = "macos")]
fn get_platform_machine_id() -> Result<String> {
let output = std::process::Command::new("ioreg")
.args(["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
.map_err(|e| anyhow!("Failed to run ioreg: {e}"))?;
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("IOPlatformUUID") {
if let Some(uuid) = line.split('"').nth(3) {
if !uuid.is_empty() {
return Ok(uuid.to_string());
}
}
}
}
Err(anyhow!("IOPlatformUUID not found in ioreg output"))
}
#[cfg(target_os = "linux")]
fn get_platform_machine_id() -> Result<String> {
for path in &["/etc/machine-id", "/var/lib/dbus/machine-id"] {
if let Ok(id) = std::fs::read_to_string(path) {
let id = id.trim().to_string();
if !id.is_empty() {
return Ok(id);
}
}
}
Err(anyhow!(
"Could not read machine-id from /etc/machine-id or /var/lib/dbus/machine-id"
))
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
fn get_platform_machine_id() -> Result<String> {
Err(anyhow!("Unsupported platform for HWID generation"))
}
// ---------------------------------------------------------------------------
// Fallback HWID
// ---------------------------------------------------------------------------
/// Generates a deterministic 32-char hex fallback HWID when platform-specific
/// machine ID retrieval fails. Uses a fixed seed so the value is stable.
fn generate_fallback_hwid() -> String {
let mut hasher = Sha256::new();
hasher.update(b"clash-nyanpasu:fallback");
let hash = hasher.finalize();
hex::encode(&hash[..16])
}
// ---------------------------------------------------------------------------
// ASCII sanitization for HTTP headers
// ---------------------------------------------------------------------------
/// Strips non-ASCII characters from a string to produce a valid HTTP header value.
/// `reqwest::header::HeaderValue` rejects non-ASCII bytes, so this prevents
/// runtime panics for users with localized hostnames or model names.
pub fn sanitize_for_header(s: &str) -> String {
s.chars().filter(|c| c.is_ascii() && *c >= ' ').collect()
}
// ---------------------------------------------------------------------------
// Device OS
// ---------------------------------------------------------------------------
fn get_device_os() -> &'static str {
if cfg!(target_os = "windows") {
"Windows"
} else if cfg!(target_os = "macos") {
"macOS"
} else if cfg!(target_os = "linux") {
"Linux"
} else {
"Unknown"
}
}
// ---------------------------------------------------------------------------
// OS version
// ---------------------------------------------------------------------------
fn get_os_version() -> String {
sysinfo::System::os_version().unwrap_or_else(|| "unknown".to_string())
}
// ---------------------------------------------------------------------------
// Device model
// ---------------------------------------------------------------------------
#[cfg(target_os = "windows")]
fn get_device_model() -> String {
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
if let Ok(key) = hklm.open_subkey("HARDWARE\\DESCRIPTION\\System\\BIOS") {
if let Ok(name) = key.get_value::<String, _>("SystemProductName") {
if !name.is_empty() {
return name;
}
}
}
whoami::devicename()
}
#[cfg(target_os = "macos")]
fn get_device_model() -> String {
if let Ok(output) = std::process::Command::new("sysctl")
.args(["-n", "hw.model"])
.output()
{
let model = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !model.is_empty() {
return model;
}
}
whoami::devicename()
}
#[cfg(target_os = "linux")]
fn get_device_model() -> String {
if let Ok(name) = std::fs::read_to_string("/sys/devices/virtual/dmi/id/product_name") {
let name = name.trim().to_string();
if !name.is_empty() {
return name;
}
}
whoami::devicename()
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
fn get_device_model() -> String {
whoami::devicename()
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hwid_is_deterministic() {
let info1 = get_device_info();
let info2 = get_device_info();
assert_eq!(info1.hwid, info2.hwid);
assert_eq!(info1.hwid.len(), 32);
}
#[test]
fn test_device_os_not_empty() {
let os = get_device_os();
assert!(!os.is_empty());
assert!(["Windows", "macOS", "Linux", "Unknown"].contains(&os));
}
#[test]
fn test_os_version_not_empty() {
let ver = get_os_version();
assert!(!ver.is_empty());
}
#[test]
fn test_device_model_not_empty() {
let model = get_device_model();
assert!(!model.is_empty());
}
#[test]
fn test_fallback_hwid_is_valid_hex_32() {
let hwid = generate_fallback_hwid();
assert_eq!(hwid.len(), 32);
assert!(hwid.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_sanitize_for_header_strips_non_ascii() {
assert_eq!(sanitize_for_header("Hello"), "Hello");
assert_eq!(sanitize_for_header("Привет"), "");
assert_eq!(sanitize_for_header("PC-Кирилл"), "PC-");
assert_eq!(sanitize_for_header("Model\x00Name"), "ModelName");
}
}
@@ -7,6 +7,7 @@ pub mod init;
pub mod resolve;
// mod winhelp;
pub mod downloader;
pub mod hwid;
#[cfg(windows)]
pub mod winreg;
@@ -44,7 +44,7 @@
"dayjs": "1.11.20",
"framer-motion": "12.38.0",
"i18next": "25.10.9",
"jotai": "2.18.1",
"jotai": "2.19.0",
"json-schema": "0.4.0",
"material-react-table": "3.2.1",
"monaco-editor": "0.55.1",
@@ -69,7 +69,7 @@
"@csstools/normalize.css": "12.1.1",
"@emotion/babel-plugin": "11.13.5",
"@emotion/react": "11.14.0",
"@iconify/json": "2.2.454",
"@iconify/json": "2.2.455",
"@monaco-editor/react": "4.7.0",
"@tanstack/react-query": "5.95.2",
"@tanstack/react-router": "1.167.5",
+2 -2
View File
@@ -5,7 +5,7 @@
"mihomo_alpha": "alpha-d0f3312",
"clash_rs": "v0.9.6",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.6-alpha+sha.7be7e89"
"clash_rs_alpha": "0.9.6-alpha+sha.b667e62"
},
"arch_template": {
"mihomo": {
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2026-03-24T22:23:10.009Z"
"updated_at": "2026-03-25T22:24:04.344Z"
}
+2 -2
View File
@@ -68,7 +68,7 @@
"cross-env": "10.1.0",
"dedent": "1.7.2",
"globals": "17.4.0",
"knip": "6.0.5",
"knip": "6.0.6",
"lint-staged": "16.4.0",
"npm-run-all2": "8.0.4",
"oxlint": "1.57.0",
@@ -80,7 +80,7 @@
"prettier-plugin-ember-template-tag": "2.1.3",
"prettier-plugin-tailwindcss": "0.7.2",
"prettier-plugin-toml": "2.0.6",
"stylelint": "17.5.0",
"stylelint": "17.6.0",
"stylelint-config-html": "1.1.0",
"stylelint-config-recess-order": "7.7.0",
"stylelint-config-standard": "40.0.0",
+66 -76
View File
@@ -59,8 +59,8 @@ importers:
specifier: 17.4.0
version: 17.4.0
knip:
specifier: 6.0.5
version: 6.0.5
specifier: 6.0.6
version: 6.0.6
lint-staged:
specifier: 16.4.0
version: 16.4.0
@@ -95,26 +95,26 @@ importers:
specifier: 2.0.6
version: 2.0.6(prettier@3.8.1)
stylelint:
specifier: 17.5.0
version: 17.5.0(typescript@5.9.3)
specifier: 17.6.0
version: 17.6.0(typescript@5.9.3)
stylelint-config-html:
specifier: 1.1.0
version: 1.1.0(postcss-html@1.8.1)(stylelint@17.5.0(typescript@5.9.3))
version: 1.1.0(postcss-html@1.8.1)(stylelint@17.6.0(typescript@5.9.3))
stylelint-config-recess-order:
specifier: 7.7.0
version: 7.7.0(stylelint-order@8.1.1(stylelint@17.5.0(typescript@5.9.3)))(stylelint@17.5.0(typescript@5.9.3))
version: 7.7.0(stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)))(stylelint@17.6.0(typescript@5.9.3))
stylelint-config-standard:
specifier: 40.0.0
version: 40.0.0(stylelint@17.5.0(typescript@5.9.3))
version: 40.0.0(stylelint@17.6.0(typescript@5.9.3))
stylelint-declaration-block-no-ignored-properties:
specifier: 3.0.0
version: 3.0.0(stylelint@17.5.0(typescript@5.9.3))
version: 3.0.0(stylelint@17.6.0(typescript@5.9.3))
stylelint-order:
specifier: 8.1.1
version: 8.1.1(stylelint@17.5.0(typescript@5.9.3))
version: 8.1.1(stylelint@17.6.0(typescript@5.9.3))
stylelint-scss:
specifier: 7.0.0
version: 7.0.0(stylelint@17.5.0(typescript@5.9.3))
version: 7.0.0(stylelint@17.6.0(typescript@5.9.3))
tailwindcss:
specifier: 4.2.2
version: 4.2.2
@@ -264,8 +264,8 @@ importers:
specifier: 25.10.9
version: 25.10.9(typescript@5.9.3)
jotai:
specifier: 2.18.1
version: 2.18.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4)
specifier: 2.19.0
version: 2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4)
json-schema:
specifier: 0.4.0
version: 0.4.0
@@ -334,8 +334,8 @@ importers:
specifier: 11.14.0
version: 11.14.0(@types/react@19.2.14)(react@19.2.4)
'@iconify/json':
specifier: 2.2.454
version: 2.2.454
specifier: 2.2.455
version: 2.2.455
'@monaco-editor/react':
specifier: 4.7.0
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -600,8 +600,8 @@ importers:
specifier: 2.26.22
version: 2.26.22
undici:
specifier: 7.24.5
version: 7.24.5
specifier: 7.24.6
version: 7.24.6
yargs:
specifier: 18.0.0
version: 18.0.0
@@ -1343,8 +1343,13 @@ packages:
peerDependencies:
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-syntax-patches-for-csstree@1.0.29':
resolution: {integrity: sha512-jx9GjkkP5YHuTmko2eWAvpPnb0mB4mGRr2U7XwVNwevm8nlpobZEVk+GNmiYMk2VuA75v+plfXWyroWKmICZXg==}
'@csstools/css-syntax-patches-for-csstree@1.1.1':
resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==}
peerDependencies:
css-tree: ^3.2.1
peerDependenciesMeta:
css-tree:
optional: true
'@csstools/css-tokenizer@4.0.0':
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
@@ -1704,8 +1709,8 @@ packages:
prettier-plugin-ember-template-tag:
optional: true
'@iconify/json@2.2.454':
resolution: {integrity: sha512-U1c0+LBtcF4YDQGOVZpoA4w96hBcIVHFjBcg60pFdnF+CApRjnlIp27Jf1g7BaZlJ+vYcExyc/1EjUyZrY8MFQ==}
'@iconify/json@2.2.455':
resolution: {integrity: sha512-ruOl4ZTLW/qNDjuEE8Zb+LQPbRiLdRu4JEnewX8OhWDevAjrTY6UyqJdW0o2umOqcg8WmKJBihVI7YwBUCH7/Q==}
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -5653,10 +5658,6 @@ packages:
resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
engines: {node: '>=18'}
get-east-asian-width@1.4.0:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'}
get-east-asian-width@1.5.0:
resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
engines: {node: '>=18'}
@@ -5992,8 +5993,8 @@ packages:
jju@1.4.0:
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
jotai@2.18.1:
resolution: {integrity: sha512-e0NOzK+yRFwHo7DOp0DS0Ycq74KMEAObDWFGmfEL28PD9nLqBTt3/Ug7jf9ca72x0gC9LQZG9zH+0ISICmy3iA==}
jotai@2.19.0:
resolution: {integrity: sha512-r2wwxEXP1F2JteDLZEOPoIpAHhV89paKsN5GWVYndPNMMP/uVZDcC+fNj0A8NjKgaPWzdyO8Vp8YcYKe0uCEqQ==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@babel/core': '>=7.0.0'
@@ -6079,8 +6080,8 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
knip@6.0.5:
resolution: {integrity: sha512-+i9e/ZKuYlECB5iIK82NQwnYso4oNLBhzsTbXhSqCG1qfGi6D84GNtRENafmS3C0lABX8Wf3BKM434nPXi2AbQ==}
knip@6.0.6:
resolution: {integrity: sha512-PA+r1mTDLHH3eShlffn2ZDyH1hHvmgDj7JsTP3JKuhV/jZTyHbRkGcOd+uaSxfJZmcZyOE5zw3naP33WllTIlA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -7475,10 +7476,6 @@ packages:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string-width@8.1.1:
resolution: {integrity: sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==}
engines: {node: '>=20'}
string-width@8.2.0:
resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==}
engines: {node: '>=20'}
@@ -7559,8 +7556,8 @@ packages:
peerDependencies:
stylelint: ^16.8.2 || ^17.0.0
stylelint@17.5.0:
resolution: {integrity: sha512-o/NS6zhsPZFmgUm5tXX4pVNg1XDOZSlucLdf2qow/lVn4JIyzZIQ5b3kad1ugqUj3GSIgr2u5lQw7X8rjqw33g==}
stylelint@17.6.0:
resolution: {integrity: sha512-tokrsMIVAR9vAQ/q3UVEr7S0dGXCi7zkCezPRnS2kqPUulvUh5Vgfwngrk4EoAoW7wnrThqTdnTFN5Ra7CaxIg==}
engines: {node: '>=20.19.0'}
hasBin: true
@@ -7760,8 +7757,8 @@ packages:
resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
engines: {node: '>=14.0'}
undici@7.24.5:
resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==}
undici@7.24.6:
resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==}
engines: {node: '>=20.18.1'}
unicode-canonical-property-names-ecmascript@2.0.1:
@@ -8093,8 +8090,8 @@ packages:
write-file-atomic@1.3.4:
resolution: {integrity: sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==}
write-file-atomic@7.0.0:
resolution: {integrity: sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==}
write-file-atomic@7.0.1:
resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==}
engines: {node: ^20.17.0 || >=22.9.0}
y18n@5.0.8:
@@ -9157,7 +9154,9 @@ snapshots:
dependencies:
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-syntax-patches-for-csstree@1.0.29': {}
'@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)':
optionalDependencies:
css-tree: 3.2.1
'@csstools/css-tokenizer@4.0.0': {}
@@ -9490,7 +9489,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@iconify/json@2.2.454':
'@iconify/json@2.2.455':
dependencies:
'@iconify/types': 2.0.0
pathe: 2.0.3
@@ -12643,7 +12642,7 @@ snapshots:
cli-truncate@5.1.0:
dependencies:
slice-ansi: 7.1.0
string-width: 8.1.1
string-width: 8.2.0
cliui@8.0.1:
dependencies:
@@ -13343,8 +13342,6 @@ snapshots:
get-east-asian-width@1.2.0: {}
get-east-asian-width@1.4.0: {}
get-east-asian-width@1.5.0: {}
get-nonce@1.0.1: {}
@@ -13619,7 +13616,7 @@ snapshots:
is-fullwidth-code-point@5.0.0:
dependencies:
get-east-asian-width: 1.4.0
get-east-asian-width: 1.5.0
is-glob@4.0.3:
dependencies:
@@ -13667,7 +13664,7 @@ snapshots:
jju@1.4.0: {}
jotai@2.18.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4):
jotai@2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4):
optionalDependencies:
'@babel/core': 7.29.0
'@babel/template': 7.28.6
@@ -13721,7 +13718,7 @@ snapshots:
kind-of@6.0.3: {}
knip@6.0.5:
knip@6.0.6:
dependencies:
'@nodelib/fs.walk': 1.2.8
fast-glob: 3.3.3
@@ -13869,7 +13866,7 @@ snapshots:
ansi-escapes: 7.0.0
cli-cursor: 5.0.0
slice-ansi: 7.1.0
strip-ansi: 7.1.0
strip-ansi: 7.2.0
wrap-ansi: 9.0.0
longest-streak@3.1.0: {}
@@ -15298,11 +15295,6 @@ snapshots:
get-east-asian-width: 1.2.0
strip-ansi: 7.1.0
string-width@8.1.1:
dependencies:
get-east-asian-width: 1.4.0
strip-ansi: 7.1.0
string-width@8.2.0:
dependencies:
get-east-asian-width: 1.5.0
@@ -15339,36 +15331,36 @@ snapshots:
dependencies:
inline-style-parser: 0.2.3
stylelint-config-html@1.1.0(postcss-html@1.8.1)(stylelint@17.5.0(typescript@5.9.3)):
stylelint-config-html@1.1.0(postcss-html@1.8.1)(stylelint@17.6.0(typescript@5.9.3)):
dependencies:
postcss-html: 1.8.1
stylelint: 17.5.0(typescript@5.9.3)
stylelint: 17.6.0(typescript@5.9.3)
stylelint-config-recess-order@7.7.0(stylelint-order@8.1.1(stylelint@17.5.0(typescript@5.9.3)))(stylelint@17.5.0(typescript@5.9.3)):
stylelint-config-recess-order@7.7.0(stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)))(stylelint@17.6.0(typescript@5.9.3)):
dependencies:
stylelint: 17.5.0(typescript@5.9.3)
stylelint-order: 8.1.1(stylelint@17.5.0(typescript@5.9.3))
stylelint: 17.6.0(typescript@5.9.3)
stylelint-order: 8.1.1(stylelint@17.6.0(typescript@5.9.3))
stylelint-config-recommended@18.0.0(stylelint@17.5.0(typescript@5.9.3)):
stylelint-config-recommended@18.0.0(stylelint@17.6.0(typescript@5.9.3)):
dependencies:
stylelint: 17.5.0(typescript@5.9.3)
stylelint: 17.6.0(typescript@5.9.3)
stylelint-config-standard@40.0.0(stylelint@17.5.0(typescript@5.9.3)):
stylelint-config-standard@40.0.0(stylelint@17.6.0(typescript@5.9.3)):
dependencies:
stylelint: 17.5.0(typescript@5.9.3)
stylelint-config-recommended: 18.0.0(stylelint@17.5.0(typescript@5.9.3))
stylelint: 17.6.0(typescript@5.9.3)
stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@5.9.3))
stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.5.0(typescript@5.9.3)):
stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.6.0(typescript@5.9.3)):
dependencies:
stylelint: 17.5.0(typescript@5.9.3)
stylelint: 17.6.0(typescript@5.9.3)
stylelint-order@8.1.1(stylelint@17.5.0(typescript@5.9.3)):
stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)):
dependencies:
postcss: 8.5.8
postcss-sorting: 10.0.0(postcss@8.5.8)
stylelint: 17.5.0(typescript@5.9.3)
stylelint: 17.6.0(typescript@5.9.3)
stylelint-scss@7.0.0(stylelint@17.5.0(typescript@5.9.3)):
stylelint-scss@7.0.0(stylelint@17.6.0(typescript@5.9.3)):
dependencies:
css-tree: 3.1.0
is-plain-object: 5.0.0
@@ -15378,13 +15370,13 @@ snapshots:
postcss-resolve-nested-selector: 0.1.6
postcss-selector-parser: 7.1.1
postcss-value-parser: 4.2.0
stylelint: 17.5.0(typescript@5.9.3)
stylelint: 17.6.0(typescript@5.9.3)
stylelint@17.5.0(typescript@5.9.3):
stylelint@17.6.0(typescript@5.9.3):
dependencies:
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-syntax-patches-for-csstree': 1.0.29
'@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1)
'@csstools/css-tokenizer': 4.0.0
'@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1)
@@ -15403,7 +15395,6 @@ snapshots:
html-tags: 5.1.0
ignore: 7.0.5
import-meta-resolve: 4.2.0
imurmurhash: 0.1.4
is-plain-object: 5.0.0
mathml-tag-names: 4.0.0
meow: 14.1.0
@@ -15418,7 +15409,7 @@ snapshots:
supports-hyperlinks: 4.4.0
svg-tags: 1.0.0
table: 6.9.0
write-file-atomic: 7.0.0
write-file-atomic: 7.0.1
transitivePeerDependencies:
- supports-color
- typescript
@@ -15627,7 +15618,7 @@ snapshots:
dependencies:
'@fastify/busboy': 2.1.1
undici@7.24.5: {}
undici@7.24.6: {}
unicode-canonical-property-names-ecmascript@2.0.1: {}
@@ -15956,7 +15947,7 @@ snapshots:
dependencies:
ansi-styles: 6.2.1
string-width: 7.2.0
strip-ansi: 7.1.0
strip-ansi: 7.2.0
wrappy@1.0.2: {}
@@ -15966,9 +15957,8 @@ snapshots:
imurmurhash: 0.1.4
slide: 1.1.6
write-file-atomic@7.0.0:
write-file-atomic@7.0.1:
dependencies:
imurmurhash: 0.1.4
signal-exit: 4.1.0
y18n@5.0.8: {}
+1 -1
View File
@@ -24,7 +24,7 @@
"picocolors": "1.1.1",
"tar": "7.5.13",
"telegram": "2.26.22",
"undici": "7.24.5",
"undici": "7.24.6",
"yargs": "18.0.0"
}
}
+2
View File
@@ -67,6 +67,8 @@ func (c *redisUploadCache) GetLength(filePath string) (int64, error) {
return 0, fmt.Errorf("invalid upload length in cache: %w", err)
}
c.Touch(filePath)
return size, nil
}
@@ -22,6 +22,46 @@ Signed-off-by: Marius Durbaca <mariusd84@gmail.com>
};
pwm-leds {
@@ -113,6 +117,19 @@
reset-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
vpcie3v3-supply = <&vcc3v3_pi6c_05>;
status = "okay";
+
+ bridge@0,0 {
+ reg = <0x00000000 0 0 0 0>;
+ #address-cells = <3>;
+ #size-cells = <2>;
+ ranges;
+
+ ethernet@0,0 {
+ compatible = "pci10ec,8125";
+ reg = <0x000000 0 0 0 0>;
+ led-data = <0 0x3f 0x23f 0>;
+ };
+ };
};
&pcie30phy {
@@ -136,6 +153,19 @@
reset-gpios = <&gpio2 RK_PD6 GPIO_ACTIVE_HIGH>;
vpcie3v3-supply = <&vcc3v3_pi6c_05>;
status = "okay";
+
+ bridge@0,0 {
+ reg = <0x00200000 0 0 0 0>;
+ #address-cells = <3>;
+ #size-cells = <2>;
+ ranges;
+
+ ethernet@0,0 {
+ compatible = "pci10ec,8125";
+ reg = <0x000000 0 0 0 0>;
+ led-data = <0 0x3f 0x23f 0>;
+ };
+ };
};
&pinctrl {
--- a/arch/arm64/boot/dts/rockchip/rk3568-radxa-cm3i.dtsi
+++ b/arch/arm64/boot/dts/rockchip/rk3568-radxa-cm3i.dtsi
@@ -23,7 +23,7 @@
@@ -0,0 +1,75 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Marius Durbaca <mariusd84@gmail.com>
Date: Tue Feb 27 16:25:27 2024 +0200
Subject: [PATCH] arm64: dts: rockchip: Update LED properties for Radxa
E25
Add OpenWrt's LED aliases for showing system status.
Signed-off-by: Marius Durbaca <mariusd84@gmail.com>
---
--- a/arch/arm64/boot/dts/rockchip/rk3568-radxa-e25.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3568-radxa-e25.dts
@@ -9,6 +9,10 @@
aliases {
mmc1 = &sdmmc0;
+ led-boot = &led_user;
+ led-failsafe = &led_user;
+ led-running = &led_user;
+ led-upgrade = &led_user;
};
pwm-leds {
@@ -109,6 +113,19 @@
reset-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
vpcie3v3-supply = <&vcc3v3_pi6c_05>;
status = "okay";
+
+ bridge@0,0 {
+ reg = <0x00000000 0 0 0 0>;
+ #address-cells = <3>;
+ #size-cells = <2>;
+ ranges;
+
+ ethernet@0,0 {
+ compatible = "pci10ec,8125";
+ reg = <0x000000 0 0 0 0>;
+ led-data = <0 0x3f 0x23f 0>;
+ };
+ };
};
&pcie30phy {
@@ -132,6 +149,19 @@
reset-gpios = <&gpio2 RK_PD6 GPIO_ACTIVE_HIGH>;
vpcie3v3-supply = <&vcc3v3_pi6c_05>;
status = "okay";
+
+ bridge@0,0 {
+ reg = <0x00200000 0 0 0 0>;
+ #address-cells = <3>;
+ #size-cells = <2>;
+ ranges;
+
+ ethernet@0,0 {
+ compatible = "pci10ec,8125";
+ reg = <0x000000 0 0 0 0>;
+ led-data = <0 0x3f 0x23f 0>;
+ };
+ };
};
&pinctrl {
--- a/arch/arm64/boot/dts/rockchip/rk3568-radxa-cm3i.dtsi
+++ b/arch/arm64/boot/dts/rockchip/rk3568-radxa-cm3i.dtsi
@@ -23,7 +23,7 @@
gpios = <&gpio0 RK_PA6 GPIO_ACTIVE_HIGH>;
function = LED_FUNCTION_HEARTBEAT;
color = <LED_COLOR_ID_GREEN>;
- linux,default-trigger = "heartbeat";
+ default-state = "on";
pinctrl-names = "default";
pinctrl-0 = <&led_user_en>;
};
+3 -1
View File
@@ -155,7 +155,7 @@ proxies:
multiplexing: MULTIPLEXING_HIGH
```
Clash.Meta / mihomo only support mieru TCP proxy protocol. Use `udp: true` to allow socks5 UDP associate requests. You can't have both `port` and `port-range` properties in a single configuration.
Use `udp: true` to allow socks5 UDP associate requests. You can't have both `port` and `port-range` properties in a single configuration.
## Advanced Settings
@@ -265,4 +265,6 @@ The simple sharing link above is equivalent to the following client configuratio
}
```
You can decode the client configuration from a standard sharing link or a simple sharing link by running `mieru explain config <URL>` command.
Note: a simple sharing link does not contain necessary client configurations such as `socks5Port`. Therefore, importing a simple sharing link on a brand new device will fail.
+3 -1
View File
@@ -155,7 +155,7 @@ proxies:
multiplexing: MULTIPLEXING_HIGH
```
Clash.Meta / mihomo 只支持 mieru TCP 代理协议。使用 `udp: true` 允许 socks5 UDP associate 请求。你不能在一条配置内同时使用 `port``port-range` 属性。
使用 `udp: true` 允许 socks5 UDP associate 请求。你不能在一条配置内同时使用 `port``port-range` 属性。
## 高级设置
@@ -265,4 +265,6 @@ mierus://baozi:manlianpenfen@1.2.3.4?handshake-mode=HANDSHAKE_NO_WAIT&mtu=1400&m
}
```
你可以运行 `mieru explain config <URL>` 指令,以解码标准分享链接或简单分享链接对应的客户端配置。
注意,简单分享链接不含有 `socks5Port` 等必要的客户端配置。因此,在全新的设备上导入简单分享链接会失败。
+344
View File
@@ -0,0 +1,344 @@
From f4de14a515221e27c0d79446b423849a6546e3a6 Mon Sep 17 00:00:00 2001
From: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue, 24 Mar 2026 23:02:09 +0000
Subject: [PATCH] runtime: use uname version check for 64-bit time on 32-bit
arch codepaths
The previous fallback-on-ENOSYS logic causes issues on forks of Linux.
Android: #77621 (CL 750040 added a workaround with a TODO,
this fixes that TODO)
Causes the OS to terminate the program when running on Android
versions <=10 since the seccomp jail does not know about the 64-bit
time syscall and is configured to terminate the program on any
unknown syscall.
Synology's Linux: #77930
On old versions of Synology's Linux they added custom vendor syscalls
without adding a gap in the syscall numbers, that means when we call
the newer Linux syscall which was added later, Synology's Linux
interprets it as a completely different vendor syscall.
Originally by Jorropo in CL 751340.
Fixes golang/go#77930
Updates tailscale/go#162
Originally https://go-review.googlesource.com/c/go/+/758902/2
Co-authored-by: Jorropo <jorropo.pgm@gmail.com>
Change-Id: I90e15495d9249fd7f6e112f9e3ae8ad1322f56e0
---
.../runtime/syscall/linux/defs_linux_386.go | 1 +
.../runtime/syscall/linux/defs_linux_amd64.go | 1 +
.../runtime/syscall/linux/defs_linux_arm.go | 1 +
.../runtime/syscall/linux/defs_linux_arm64.go | 1 +
.../syscall/linux/defs_linux_loong64.go | 1 +
.../syscall/linux/defs_linux_mips64x.go | 1 +
.../runtime/syscall/linux/defs_linux_mipsx.go | 1 +
.../syscall/linux/defs_linux_ppc64x.go | 1 +
.../syscall/linux/defs_linux_riscv64.go | 1 +
.../runtime/syscall/linux/defs_linux_s390x.go | 1 +
.../runtime/syscall/linux/syscall_linux.go | 14 +++++
src/runtime/os_linux.go | 62 +++++++++++++++++++
src/runtime/os_linux32.go | 28 +++------
src/runtime/os_linux64.go | 2 +
14 files changed, 97 insertions(+), 19 deletions(-)
diff --git a/src/internal/runtime/syscall/linux/defs_linux_386.go b/src/internal/runtime/syscall/linux/defs_linux_386.go
index 7fdf5d3f8062fa..4e8e645dc49a66 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_386.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_386.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 295
SYS_PREAD64 = 180
SYS_READ = 3
+ SYS_UNAME = 122
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_amd64.go b/src/internal/runtime/syscall/linux/defs_linux_amd64.go
index 2c8676e6e9b4d9..fa764d9ccd9b8e 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_amd64.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_amd64.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 257
SYS_PREAD64 = 17
SYS_READ = 0
+ SYS_UNAME = 63
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm.go b/src/internal/runtime/syscall/linux/defs_linux_arm.go
index a0b395d6762734..cef556d5f6f986 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_arm.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_arm.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 322
SYS_PREAD64 = 180
SYS_READ = 3
+ SYS_UNAME = 122
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm64.go b/src/internal/runtime/syscall/linux/defs_linux_arm64.go
index 223dce0c5b4281..eabddbac1bc063 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_arm64.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_arm64.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 56
SYS_PREAD64 = 67
SYS_READ = 63
+ SYS_UNAME = 160
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_loong64.go b/src/internal/runtime/syscall/linux/defs_linux_loong64.go
index 8aa61c391dcdcb..08e5d49b83c9bd 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_loong64.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_loong64.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 56
SYS_PREAD64 = 67
SYS_READ = 63
+ SYS_UNAME = 160
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
index 84b760dc1b5545..b5794e5002af5e 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
@@ -19,6 +19,7 @@ const (
SYS_OPENAT = 5247
SYS_PREAD64 = 5016
SYS_READ = 5000
+ SYS_UNAME = 5061
EFD_NONBLOCK = 0x80
diff --git a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
index a9be21414c26f9..1fb4d919d1a318 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
@@ -19,6 +19,7 @@ const (
SYS_OPENAT = 4288
SYS_PREAD64 = 4200
SYS_READ = 4003
+ SYS_UNAME = 4122
EFD_NONBLOCK = 0x80
diff --git a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
index 63f4e5d7864de4..ee93ad345b810f 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
@@ -19,6 +19,7 @@ const (
SYS_OPENAT = 286
SYS_PREAD64 = 179
SYS_READ = 3
+ SYS_UNAME = 122
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
index 8aa61c391dcdcb..08e5d49b83c9bd 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 56
SYS_PREAD64 = 67
SYS_READ = 63
+ SYS_UNAME = 160
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/defs_linux_s390x.go b/src/internal/runtime/syscall/linux/defs_linux_s390x.go
index 52945db0e5b72f..da11c704081abc 100644
--- a/src/internal/runtime/syscall/linux/defs_linux_s390x.go
+++ b/src/internal/runtime/syscall/linux/defs_linux_s390x.go
@@ -17,6 +17,7 @@ const (
SYS_OPENAT = 288
SYS_PREAD64 = 180
SYS_READ = 3
+ SYS_UNAME = 122
EFD_NONBLOCK = 0x800
diff --git a/src/internal/runtime/syscall/linux/syscall_linux.go b/src/internal/runtime/syscall/linux/syscall_linux.go
index 8201e7d1907444..b64f511b03c947 100644
--- a/src/internal/runtime/syscall/linux/syscall_linux.go
+++ b/src/internal/runtime/syscall/linux/syscall_linux.go
@@ -86,3 +86,17 @@ func Pread(fd int, p []byte, offset int64) (n int, errno uintptr) {
}
return int(r1), e
}
+
+type Utsname struct {
+ Sysname [65]byte
+ Nodename [65]byte
+ Release [65]byte
+ Version [65]byte
+ Machine [65]byte
+ Domainname [65]byte
+}
+
+func Uname(buf *Utsname) (errno uintptr) {
+ _, _, e := Syscall6(SYS_UNAME, uintptr(unsafe.Pointer(buf)), 0, 0, 0, 0, 0)
+ return e
+}
diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go
index 7e6af22d48a764..493567b5303673 100644
--- a/src/runtime/os_linux.go
+++ b/src/runtime/os_linux.go
@@ -354,6 +354,7 @@ func osinit() {
numCPUStartup = getCPUCount()
physHugePageSize = getHugePageSize()
vgetrandomInit()
+ configure64bitsTimeOn32BitsArchitectures()
}
var urandom_dev = []byte("/dev/urandom\x00")
@@ -935,3 +936,64 @@ func mprotect(addr unsafe.Pointer, n uintptr, prot int32) (ret int32, errno int3
r, _, err := linux.Syscall6(linux.SYS_MPROTECT, uintptr(addr), n, uintptr(prot), 0, 0, 0)
return int32(r), int32(err)
}
+
+type kernelVersion struct {
+ major int
+ minor int
+}
+
+// getKernelVersion returns major and minor kernel version numbers
+// parsed from the uname release field.
+func getKernelVersion() kernelVersion {
+ var buf linux.Utsname
+ if e := linux.Uname(&buf); e != 0 {
+ throw("uname failed")
+ }
+
+ rel := gostringnocopy(&buf.Release[0])
+ major, minor, _, ok := parseRelease(rel)
+ if !ok {
+ throw("failed to parse kernel version from uname")
+ }
+ return kernelVersion{major: major, minor: minor}
+}
+
+// parseRelease parses a dot-separated version number. It follows the
+// semver syntax, but allows the minor and patch versions to be
+// elided.
+func parseRelease(rel string) (major, minor, patch int, ok bool) {
+ // Strip anything after a dash or plus.
+ for i := 0; i < len(rel); i++ {
+ if rel[i] == '-' || rel[i] == '+' {
+ rel = rel[:i]
+ break
+ }
+ }
+
+ next := func() (int, bool) {
+ for i := 0; i < len(rel); i++ {
+ if rel[i] == '.' {
+ ver, err := strconv.Atoi(rel[:i])
+ rel = rel[i+1:]
+ return ver, err == nil
+ }
+ }
+ ver, err := strconv.Atoi(rel)
+ rel = ""
+ return ver, err == nil
+ }
+ if major, ok = next(); !ok || rel == "" {
+ return
+ }
+ if minor, ok = next(); !ok || rel == "" {
+ return
+ }
+ patch, ok = next()
+ return
+}
+
+// GE checks if the running kernel version
+// is greater than or equal to the provided version.
+func (kv kernelVersion) GE(x, y int) bool {
+ return kv.major > x || (kv.major == x && kv.minor >= y)
+}
diff --git a/src/runtime/os_linux32.go b/src/runtime/os_linux32.go
index 16de6fb350f624..02cb18f32d57d2 100644
--- a/src/runtime/os_linux32.go
+++ b/src/runtime/os_linux32.go
@@ -7,27 +7,25 @@
package runtime
import (
- "internal/runtime/atomic"
"unsafe"
)
+func configure64bitsTimeOn32BitsArchitectures() {
+ use64bitsTimeOn32bits = getKernelVersion().GE(5, 1)
+}
+
//go:noescape
func futex_time32(addr unsafe.Pointer, op int32, val uint32, ts *timespec32, addr2 unsafe.Pointer, val3 uint32) int32
//go:noescape
func futex_time64(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32
-var isFutexTime32bitOnly atomic.Bool
+var use64bitsTimeOn32bits bool
//go:nosplit
func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32 {
- if !isFutexTime32bitOnly.Load() {
- ret := futex_time64(addr, op, val, ts, addr2, val3)
- // futex_time64 is only supported on Linux 5.0+
- if ret != -_ENOSYS {
- return ret
- }
- isFutexTime32bitOnly.Store(true)
+ if use64bitsTimeOn32bits {
+ return futex_time64(addr, op, val, ts, addr2, val3)
}
// Downgrade ts.
var ts32 timespec32
@@ -45,17 +43,10 @@ func timer_settime32(timerid int32, flags int32, new, old *itimerspec32) int32
//go:noescape
func timer_settime64(timerid int32, flags int32, new, old *itimerspec) int32
-var isSetTime32bitOnly atomic.Bool
-
//go:nosplit
func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 {
- if !isSetTime32bitOnly.Load() {
- ret := timer_settime64(timerid, flags, new, old)
- // timer_settime64 is only supported on Linux 5.0+
- if ret != -_ENOSYS {
- return ret
- }
- isSetTime32bitOnly.Store(true)
+ if use64bitsTimeOn32bits {
+ return timer_settime64(timerid, flags, new, old)
}
var newts, oldts itimerspec32
@@ -73,6 +64,5 @@ func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 {
old32 = &oldts
}
- // Fall back to 32-bit timer
return timer_settime32(timerid, flags, new32, old32)
}
diff --git a/src/runtime/os_linux64.go b/src/runtime/os_linux64.go
index 7b70d80fbe5a89..f9571dd7586614 100644
--- a/src/runtime/os_linux64.go
+++ b/src/runtime/os_linux64.go
@@ -10,6 +10,8 @@ import (
"unsafe"
)
+func configure64bitsTimeOn32BitsArchitectures() {}
+
//go:noescape
func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32
+54 -42
View File
@@ -1,4 +1,8 @@
Subject: [PATCH] internal/poll: move rsan to heap on windows
From 1a44be4cecdc742ac6cce9825f9ffc19857c99f3 Mon Sep 17 00:00:00 2001
From: database64128 <free122448@hotmail.com>
Date: Mon, 9 Mar 2026 16:25:16 +0800
Subject: [PATCH] [release-branch.go1.26] internal/poll: move rsan to heap on
windows
According to https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom,
the memory pointed to by lpFromlen must remain available during the
@@ -14,20 +18,27 @@ with rsa in the same sync.Pool. The new wsaRsa struct is still in the
same size class, so no additional overhead is introduced by this
change.
Fixes #77975.
Fixes #78041.
Change-Id: I5ffbccb332515116ddc03fb7c40ffc9293cad2ab
Reviewed-on: https://go-review.googlesource.com/c/go/+/753040
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Commit-Queue: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/753480
Reviewed-by: Mark Freeman <markfreeman@google.com>
---
Index: src/internal/poll/fd_windows.go
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
--- a/src/internal/poll/fd_windows.go (revision 8149d992682ce76c6af804b507878e19fc966f7b)
+++ b/src/internal/poll/fd_windows.go (date 1773058735706)
@@ -149,7 +149,7 @@
src/internal/poll/fd_windows.go | 94 +++++++++++++++++++++------------
1 file changed, 59 insertions(+), 35 deletions(-)
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
index 2ba967f990982f..26319548e3c310 100644
--- a/src/internal/poll/fd_windows.go
+++ b/src/internal/poll/fd_windows.go
@@ -149,7 +149,7 @@ var wsaMsgPool = sync.Pool{
// newWSAMsg creates a new WSAMsg with the provided parameters.
// Use [freeWSAMsg] to free it.
-func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMsg {
@@ -35,8 +46,8 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
// The returned object can't be allocated in the stack because it is accessed asynchronously
// by Windows in between several system calls. If the stack frame is moved while that happens,
// then Windows may access invalid memory.
@@ -164,33 +164,45 @@
Buf: unsafe.SliceData(oob),
@@ -166,34 +166,46 @@ func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMs
}
}
msg.Flags = uint32(flags)
- if unconnected {
@@ -48,7 +59,7 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
}
return msg
}
func freeWSAMsg(msg *windows.WSAMsg) {
// Clear pointers to buffers so they can be released by garbage collector.
+ msg.Name = nil
@@ -65,7 +76,7 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
- }
wsaMsgPool.Put(msg)
}
+// wsaRsa bundles a [syscall.RawSockaddrAny] with its length for efficient caching.
+//
+// When used by WSARecvFrom, wsaRsa must be on the heap. See
@@ -81,20 +92,21 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ return new(wsaRsa)
},
}
+
+func newWSARsa() *wsaRsa {
+ rsa := wsaRsaPool.Get().(*wsaRsa)
+ rsa.name = syscall.RawSockaddrAny{}
+ rsa.namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
+ return rsa
+}
+
var operationPool = sync.Pool{
New: func() any {
@@ -737,19 +749,18 @@
return new(operation)
@@ -739,19 +751,18 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) {
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
@@ -113,11 +125,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ sa, _ := rsa.name.Sockaddr()
return n, sa, nil
}
@@ -768,19 +779,18 @@
@@ -770,19 +781,18 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
@@ -136,11 +148,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ rawToSockaddrInet4(&rsa.name, sa4)
return n, err
}
@@ -799,19 +809,18 @@
@@ -801,19 +811,18 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error)
fd.pin('r', &buf[0])
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
+ rsa := newWSARsa()
defer wsaRsaPool.Put(rsa)
@@ -159,11 +171,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ rawToSockaddrInet6(&rsa.name, sa6)
return n, err
}
@@ -1371,7 +1380,9 @@
@@ -1373,7 +1382,9 @@ func (fd *FD) ReadMsg(p []byte, oob []byte, flags int) (int, int, int, syscall.S
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
@@ -171,10 +183,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1396,7 +1407,9 @@
@@ -1398,7 +1409,9 @@ func (fd *FD) ReadMsgInet4(p []byte, oob []byte, flags int, sa4 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
@@ -182,10 +194,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1420,7 +1433,9 @@
@@ -1422,7 +1435,9 @@ func (fd *FD) ReadMsgInet6(p []byte, oob []byte, flags int, sa6 *syscall.Sockadd
p = p[:maxRW]
}
- msg := newWSAMsg(p, oob, flags, true)
+ rsa := newWSARsa()
+ defer wsaRsaPool.Put(rsa)
@@ -193,10 +205,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
defer freeWSAMsg(msg)
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
@@ -1444,15 +1459,18 @@
@@ -1446,15 +1461,18 @@ func (fd *FD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (int, int, err
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
@@ -215,10 +227,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1471,11 +1489,14 @@
@@ -1473,11 +1491,14 @@ func (fd *FD) WriteMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
@@ -233,10 +245,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
@@ -1494,11 +1515,14 @@
@@ -1496,11 +1517,14 @@ func (fd *FD) WriteMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (in
}
defer fd.writeUnlock()
- msg := newWSAMsg(p, oob, 0, sa != nil)
- defer freeWSAMsg(msg)
+ var rsa *wsaRsa
@@ -250,4 +262,4 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
+ defer freeWSAMsg(msg)
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
return qty, err
return qty, err
+13 -7
View File
@@ -173,14 +173,13 @@ jobs:
- name: Set up Go1.26 loongarch abi1
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
run: |
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0/go1.26.0.linux-amd64-abi1.tar.gz
mkdir -p $HOME/usr/local
tar zxf go1.26.0.linux-amd64-abi1.tar.gz -C $HOME/usr/local
echo "$HOME/usr/local/go/bin" >> $GITHUB_PATH
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
with:
go-download-base-url: 'https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0'
go-version: 1.26.0
# TODO: remove after issue77731 merged, see: https://github.com/golang/go/issues/77731
- name: Apply issue77731 for Golang1.26
# TODO: remove after issue77731 fixed, see: https://github.com/golang/go/issues/77731
- name: Fix issue77731 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
@@ -193,6 +192,13 @@ jobs:
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
# TODO: remove after issue77930 fixed, see: https://github.com/golang/go/issues/77930
- name: Fix issue77930 for Golang1.26
if: ${{ matrix.jobs.goversion == '' }}
run: |
cd $(go env GOROOT)
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77930.patch
# this patch file only works on golang1.26.x
# that means after golang1.27 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
+256
View File
@@ -1,14 +1,17 @@
package process
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"net/netip"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"unicode"
@@ -61,10 +64,29 @@ type inetDiagResponse struct {
func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) {
uid, inode, err := resolveSocketByNetlink(network, ip, srcPort)
if runtime.GOOS == "android" {
// on Android (especially recent releases), netlink INET_DIAG can fail or return UID 0 / empty process info for some apps
// so trying fallback to resolve /proc/net/{tcp,tcp6,udp,udp6}
if err != nil {
uid, inode, err = resolveSocketByProcFS(network, ip, srcPort)
} else if uid == 0 {
pUID, pInode, pErr := resolveSocketByProcFS(network, ip, srcPort)
if pErr == nil && pUID != 0 {
uid, inode, err = pUID, pInode, nil
}
}
}
if err != nil {
return 0, "", err
}
pp, err := resolveProcessNameByProcSearch(inode, uid)
if runtime.GOOS == "android" {
// if inode-based /proc/<pid>/fd resolution fails but UID is known,
// fall back to resolving the process/package name by UID (typical on Android where all app processes share one UID).
if err != nil && uid != 0 {
pp, err = resolveProcessNameByUID(uid)
}
}
return uid, pp, err
}
@@ -180,6 +202,47 @@ func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
}
// resolveProcessNameByUID returns a process name for any process with uid.
// On Android all processes of one app share the same UID; used when inode
// lookup fails (socket closed / TIME_WAIT).
func resolveProcessNameByUID(uid uint32) (string, error) {
files, err := os.ReadDir("/proc")
if err != nil {
return "", err
}
for _, f := range files {
if !f.IsDir() || !isPid(f.Name()) {
continue
}
info, err := f.Info()
if err != nil {
continue
}
if info.Sys().(*syscall.Stat_t).Uid != uid {
continue
}
processPath := filepath.Join("/proc", f.Name())
if runtime.GOOS == "android" {
cmdline, err := os.ReadFile(path.Join(processPath, "cmdline"))
if err != nil {
continue
}
if name := splitCmdline(cmdline); name != "" {
return name, nil
}
} else {
if exe, err := os.Readlink(filepath.Join(processPath, "exe")); err == nil {
return exe, nil
}
}
}
return "", fmt.Errorf("no process found with uid %d", uid)
}
func splitCmdline(cmdline []byte) string {
cmdline = bytes.Trim(cmdline, " ")
@@ -198,3 +261,196 @@ func isPid(s string) bool {
return !unicode.IsDigit(r)
}) == -1
}
// resolveSocketByProcFS finds UID and inode from /proc/net/{tcp,tcp6,udp,udp6}.
// In TUN mode metadata sourceIP is often the gateway (e.g. fake-ip range), not
// the socket's real local address; we match by local port first and prefer
// exact IP+port when it matches.
func resolveSocketByProcFS(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) {
var proto string
switch {
case strings.HasPrefix(network, "tcp"):
proto = "tcp"
case strings.HasPrefix(network, "udp"):
proto = "udp"
default:
return 0, 0, ErrInvalidNetwork
}
targetPort := uint16(srcPort)
unmapped := ip.Unmap()
files := []string{"/proc/net/" + proto, "/proc/net/" + proto + "6"}
var bestUID, bestInode uint32
found := false
for _, path := range files {
isV6 := strings.HasSuffix(path, "6")
var matchIP netip.Addr
if unmapped.Is4() {
if isV6 {
matchIP = netip.AddrFrom16(unmapped.As16())
} else {
matchIP = unmapped
}
} else {
if !isV6 {
continue
}
matchIP = unmapped
}
uid, inode, exact, err := searchProcNetFileByPort(path, matchIP, targetPort)
if err != nil {
continue
}
if exact {
return uid, inode, nil
}
if !found || (bestUID == 0 && uid != 0) {
bestUID = uid
bestInode = inode
found = true
}
}
if found {
return bestUID, bestInode, nil
}
return 0, 0, ErrNotFound
}
// searchProcNetFileByPort scans /proc/net/* for local_address matching targetPort.
// Exact IP+port wins; else port-only (skips inode==0 entries used by TIME_WAIT).
func searchProcNetFileByPort(path string, targetIP netip.Addr, targetPort uint16) (uid, inode uint32, exact bool, err error) {
f, err := os.Open(path)
if err != nil {
return 0, 0, false, err
}
defer f.Close()
isV6 := strings.HasSuffix(path, "6")
scanner := bufio.NewScanner(f)
if !scanner.Scan() {
return 0, 0, false, ErrNotFound
}
var bestUID, bestInode uint32
found := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 10 {
continue
}
lineIP, linePort, parseErr := parseHexAddrPort(fields[1], isV6)
if parseErr != nil {
continue
}
if linePort != targetPort {
continue
}
lineUID, parseErr := strconv.ParseUint(fields[7], 10, 32)
if parseErr != nil {
continue
}
lineInode, parseErr := strconv.ParseUint(fields[9], 10, 32)
if parseErr != nil {
continue
}
if lineIP == targetIP {
return uint32(lineUID), uint32(lineInode), true, nil
}
if lineInode == 0 {
continue
}
if !found || (bestUID == 0 && lineUID != 0) {
bestUID = uint32(lineUID)
bestInode = uint32(lineInode)
found = true
}
}
if found {
return bestUID, bestInode, false, nil
}
return 0, 0, false, ErrNotFound
}
func parseHexAddrPort(s string, isV6 bool) (netip.Addr, uint16, error) {
colon := strings.IndexByte(s, ':')
if colon < 0 {
return netip.Addr{}, 0, fmt.Errorf("invalid addr:port: %s", s)
}
port64, err := strconv.ParseUint(s[colon+1:], 16, 16)
if err != nil {
return netip.Addr{}, 0, err
}
var addr netip.Addr
if isV6 {
addr, err = parseHexIPv6(s[:colon])
} else {
addr, err = parseHexIPv4(s[:colon])
}
return addr, uint16(port64), err
}
func parseHexIPv4(s string) (netip.Addr, error) {
if len(s) != 8 {
return netip.Addr{}, fmt.Errorf("invalid ipv4 hex len: %d", len(s))
}
b, err := hex.DecodeString(s)
if err != nil {
return netip.Addr{}, err
}
var ip [4]byte
if littleEndian {
ip[0], ip[1], ip[2], ip[3] = b[3], b[2], b[1], b[0]
} else {
copy(ip[:], b)
}
return netip.AddrFrom4(ip), nil
}
func parseHexIPv6(s string) (netip.Addr, error) {
if len(s) != 32 {
return netip.Addr{}, fmt.Errorf("invalid ipv6 hex len: %d", len(s))
}
var ip [16]byte
for i := 0; i < 4; i++ {
b, err := hex.DecodeString(s[i*8 : (i+1)*8])
if err != nil {
return netip.Addr{}, err
}
if littleEndian {
ip[i*4+0] = b[3]
ip[i*4+1] = b[2]
ip[i*4+2] = b[1]
ip[i*4+3] = b[0]
} else {
copy(ip[i*4:(i+1)*4], b)
}
}
return netip.AddrFrom16(ip), nil
}
var littleEndian = func() bool {
x := uint32(0x01020304)
return *(*byte)(unsafe.Pointer(&x)) == 0x04
}()
@@ -58,3 +58,4 @@ endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
$(eval $(call BuildPackage,luci-app-adguardhome))
@@ -1,24 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
// NOTE: luci-app-adguardhome 使用多页面 CBI 模板架构,luci 23.05+ 通过 menu.d alias 跳转至 base 子页。
// 此 JS 文件不直接调用,保留供参考。
'use strict';
'require view';
'require rpc';
var callStatus = rpc.declare({
object: 'luci',
method: 'get_status',
params: ['name'],
expect: { '': {} }
});
'require form';
return view.extend({
load: function() {},
render: function() {
var m = new form.Map('config', _('Settings'));
var s = m.section(form.NamedSection, 'main', 'main', _('Configuration'));
s.anonymous = true;
var o = s.option(form.Value, 'name', _('Name'));
return m.render();
// 实际界面由 luasrc/model/cbi/AdGuardHome/ 下多个 CBI 模型提供
return E('p', {}, _('Loading…'));
}
});
@@ -5,19 +5,23 @@ local http = require "luci.http"
local uci = require "luci.model.uci".cursor()
function index()
local page = entry({"admin", "services", "AdGuardHome"},
alias("admin", "services", "AdGuardHome", "base"),
_("AdGuard Home"), 10)
page.dependent = true
page.acl_depends = { "luci-app-adguardhome" }
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复注册
if not nixio.fs.access("/usr/share/luci/menu.d/luci-app-adguardhome.json") then
local page = entry({"admin", "services", "AdGuardHome"},
alias("admin", "services", "AdGuardHome", "base"),
_("AdGuard Home"), 10)
page.dependent = true
page.acl_depends = { "luci-app-adguardhome" }
entry({"admin", "services", "AdGuardHome", "base"},
cbi("AdGuardHome/base"), _("Base Setting"), 1).leaf = true
entry({"admin", "services", "AdGuardHome", "log"},
form("AdGuardHome/log"), _("Log"), 2).leaf = true
entry({"admin", "services", "AdGuardHome", "manual"},
cbi("AdGuardHome/manual"), _("Manual Config"), 3).leaf = true
entry({"admin", "services", "AdGuardHome", "base"},
cbi("AdGuardHome/base"), _("Base Setting"), 1).leaf = true
entry({"admin", "services", "AdGuardHome", "log"},
form("AdGuardHome/log"), _("Log"), 2).leaf = true
entry({"admin", "services", "AdGuardHome", "manual"},
cbi("AdGuardHome/manual"), _("Manual Config"), 3).leaf = true
end
-- API 路由在新旧版本均需注册
entry({"admin", "services", "AdGuardHome", "status"},
call("act_status"), nil).leaf = true
entry({"admin", "services", "AdGuardHome", "check"},
@@ -80,7 +84,9 @@ function act_status()
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
if fs.access(binpath) then
result.running = (luci.sys.call("pgrep -f '" .. binpath .. "' >/dev/null 2>&1") == 0)
-- Security fix: binpath 来自 UCI 用户数据,用单引号包裹防止命令注入
local safe_bin = binpath:gsub("'", "'\\''")
result.running = (luci.sys.call("pgrep -f '" .. safe_bin .. "' >/dev/null 2>&1") == 0)
else
result.running = false
end
@@ -105,6 +111,7 @@ function do_update()
local script = "/usr/share/AdGuardHome/update_core.sh"
if fs.access("/var/run/update_core") then
if arg == "force" then
-- Security fix: script 路径是固定常量,arg 只能是 "force" 或 "",无注入风险
luci.sys.exec("pkill -f '" .. script .. "' 2>/dev/null; " .. script .. " " .. arg .. " >/tmp/AdGuardHome_update.log 2>&1 &")
end
else
@@ -0,0 +1,37 @@
{
"admin/services/AdGuardHome": {
"title": "AdGuard Home",
"order": 10,
"action": {
"type": "alias",
"path": "admin/services/AdGuardHome/base"
},
"depends": {
"acl": [ "luci-app-adguardhome" ]
}
},
"admin/services/AdGuardHome/base": {
"title": "Base Setting",
"order": 1,
"action": {
"type": "cbi",
"path": "AdGuardHome/base"
}
},
"admin/services/AdGuardHome/log": {
"title": "Log",
"order": 2,
"action": {
"type": "form",
"path": "AdGuardHome/log"
}
},
"admin/services/AdGuardHome/manual": {
"title": "Manual Config",
"order": 3,
"action": {
"type": "cbi",
"path": "AdGuardHome/manual"
}
}
}
@@ -0,0 +1,26 @@
# Copyright (C) 2019 sirpdboy <https://github.com/sirpdboy/luci-app-advanced/>
# Maintained by kenzok78
#
# This is free software, licensed under the Apache License, Version 2.0 .
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-advanced
PKG_VERSION:=1.20
PKG_RELEASE:=1
PKG_MAINTAINER:=kenzok78 <https://github.com/kenzok78>
LUCI_TITLE:=LuCI Support for Advanced Settings and File Manager
LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/install
$(call Package/$(PKG_NAME)/install/default,$(1))
$(INSTALL_DIR) $(1)/bin
$(INSTALL_BIN) ./root/bin/* $(1)/bin/
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
$(eval $(call BuildPackage,$(PKG_NAME)))
@@ -0,0 +1,114 @@
# luci-app-advanced
[![license](https://img.shields.io/badge/license-Apache2-brightgreen.svg)](LICENSE)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/kenzok78/luci-app-advanced/pulls)
[![Lastest Release](https://img.shields.io/github/release/sirpdboy/luci-app-advanced.svg?style=flat)](https://github.com/kenzok78/luci-app-advanced/releases)
LuCI 高级设置插件,提供系统配置、网络设置、防火墙等功能的管理界面。
<small>
## 功能特性
- 系统高级设置
- 网络配置管理
- 防火墙规则配置
- DHCP 设置
- 文件浏览器
- 文件管理器
## 系统要求
- OpenWrt 23.* 或更高版本
- LuCI 23.* + Web 界面
## 安装
### 从源码编译
```bash
git clone https://github.com/kenzok78/luci-app-advanced.git
mv luci-app-advanced /path/to/openwrt/package/feeds/luci/
make package/luci-app-advanced/compile V=99
```
### 在线安装
```bash
opkg update
opkg install luci-app-advanced
```
## 配置
1. 登录 LuCI 管理界面
2. 进入 **系统 → 高级设置**
3. 根据需要进行配置
4. 保存并应用
## 代码优化
### 修复的问题
- uci-defaults 脚本:添加文件存在性检查,避免文件不存在时报错
## 目录结构
```
luci-app-advanced/
├── htdocs/
│ └── luci-static/
│ └── resources/
│ └── fileassistant/
│ ├── fb.js
│ ├── fb.css
│ ├── folder-icon.png
│ ├── file-icon.png
│ └── link-icon.png
├── luasrc/
│ ├── controller/
│ │ ├── advanced.lua
│ │ └── fileassistant.lua
│ ├── model/
│ │ └── cbi/
│ │ └── advanced.lua
│ └── view/
│ ├── filebrowser.htm
│ └── fileassistant.htm
├── root/
│ ├── bin/
│ │ ├── normalmode
│ │ ├── nuc
│ │ ├── ipmode4
│ │ └── ipmode6
│ ├── etc/
│ │ ├── config/
│ │ │ └── advanced
│ │ └── uci-defaults/
│ │ └── 40_luci-fb
│ └── usr/
│ └── share/
│ └── rpcd/
│ └── acl.d/
│ └── luci-app-advanced.json
├── Makefile
└── README.md
```
## 许可证
Apache License 2.0
## 致谢
- 原始项目:[sirpdboy/luci-app-advanced](https://github.com/sirpdboy/luci-app-advanced)
## 更新日志
### v1.20 (2026-03-24)
- 标准化代码结构
- 修复 uci-defaults 脚本
- 添加中文 README
</small>
@@ -0,0 +1,68 @@
.fb-container {
margin-top: 1rem;
}
.fb-container .cbi-button {
height: 2.6rem;
}
.fb-container .cbi-input-text {
margin-bottom: 1rem;
width: 100%;
}
.fb-container .panel-title {
padding-bottom: 0;
width: 50%;
border-bottom: none;
}
.fb-container .panel-container {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 1rem;
border-bottom: 1px solid #aaa;
}
.fb-container .upload-container {
display: none;
margin: 1rem 0;
}
.fb-container .upload-file {
margin-right: 2rem;
}
.fb-container .cbi-value-field {
text-align: left;
}
.fb-container .parent-icon strong {
margin-left: 1rem;
}
.fb-container td[class$="-icon"] {
cursor: pointer;
}
.fb-container .file-icon, .fb-container .folder-icon, .fb-container .link-icon {
position: relative;
}
.fb-container .file-icon:before, .fb-container .folder-icon:before, .fb-container .link-icon:before {
display: inline-block;
width: 1.5rem;
height: 1.5rem;
content: '';
background-size: contain;
margin: 0 0.5rem 0 1rem;
vertical-align: middle;
}
.fb-container .file-icon:before {
background-image: url(file-icon.png);
}
.fb-container .folder-icon:before {
background-image: url(folder-icon.png);
}
.fb-container .link-icon:before {
background-image: url(link-icon.png);
}
@media screen and (max-width: 480px) {
.fb-container .upload-file {
width: 14.6rem;
}
.fb-container .cbi-value-owner,
.fb-container .cbi-value-perm {
display: none;
}
}
@@ -0,0 +1,288 @@
String.prototype.replaceAll = function(search, replacement) {
var target = this;
return target.replace(new RegExp(search, 'g'), replacement);
};
(function () {
var iwxhr = new XHR();
var listElem = document.getElementById("list-content");
listElem.onclick = handleClick;
var currentPath;
var pathElem = document.getElementById("current-path");
pathElem.onblur = function () {
update_list(this.value.trim());
};
pathElem.onkeyup = function (evt) {
if (evt.keyCode == 13) {
this.blur();
}
};
function removePath(filename, isdir) {
var c = confirm('你确定要删除 ' + filename + ' 吗?');
if (c) {
iwxhr.get('/cgi-bin/luci/admin/system/fileassistant/delete',
{
path: concatPath(currentPath, filename),
isdir: isdir
},
function (x, res) {
if (res.ec === 0) {
refresh_list(res.data, currentPath);
}
});
}
}
function installPath(filename, isdir) {
if (isdir === "1") {
alert('这是一个目录,请选择 ipk 文件进行安装!');
return;
}
var isipk = isIPK(filename);
if (isipk === 0) {
alert('只允许安装 ipk 格式的文件!');
return;
}
var c = confirm('你确定要安装 ' + filename + ' 吗?');
if (c) {
iwxhr.get('/cgi-bin/luci/admin/system/fileassistant/install',
{
filepath: concatPath(currentPath, filename),
isdir: isdir
},
function (x, res) {
if (res.ec === 0) {
location.reload();
alert('安装成功!');
} else {
alert('安装失败,请检查文件格式!');
}
});
}
}
function isIPK(filename) {
var index= filename.lastIndexOf(".");
var ext = filename.substr(index+1);
if (ext === 'ipk') {
return 1;
} else {
return 0;
}
}
function renamePath(filename) {
var newname = prompt('请输入新的文件名:', filename);
if (newname) {
newname = newname.trim();
if (newname != filename) {
var newpath = concatPath(currentPath, newname);
iwxhr.get('/cgi-bin/luci/admin/system/fileassistant/rename',
{
filepath: concatPath(currentPath, filename),
newpath: newpath
},
function (x, res) {
if (res.ec === 0) {
refresh_list(res.data, currentPath);
}
}
);
}
}
}
function openpath(filename, dirname) {
dirname = dirname || currentPath;
window.open('/cgi-bin/luci/admin/system/fileassistant/open?path='
+ encodeURIComponent(dirname) + '&filename='
+ encodeURIComponent(filename));
}
function getFileElem(elem) {
if (elem.className.indexOf('-icon') > -1) {
return elem;
}
else if (elem.parentNode.className.indexOf('-icon') > -1) {
return elem.parentNode;
}
}
function concatPath(path, filename) {
if (path === '/') {
return path + filename;
}
else {
return path.replace(/\/$/, '') + '/' + filename;
}
}
function handleClick(evt) {
var targetElem = evt.target;
var infoElem;
if (targetElem.className.indexOf('cbi-button-remove') > -1) {
infoElem = targetElem.parentNode.parentNode;
removePath(infoElem.dataset['filename'] , infoElem.dataset['isdir'])
}
else if (targetElem.className.indexOf('cbi-button-install') > -1) {
infoElem = targetElem.parentNode.parentNode;
installPath(infoElem.dataset['filename'] , infoElem.dataset['isdir'])
}
else if (targetElem.className.indexOf('cbi-button-edit') > -1) {
renamePath(targetElem.parentNode.parentNode.dataset['filename']);
}
else if (targetElem = getFileElem(targetElem)) {
if (targetElem.className.indexOf('parent-icon') > -1) {
update_list(currentPath.replace(/\/[^/]+($|\/$)/, ''));
}
else if (targetElem.className.indexOf('file-icon') > -1) {
openpath(targetElem.parentNode.dataset['filename']);
}
else if (targetElem.className.indexOf('link-icon') > -1) {
infoElem = targetElem.parentNode;
var filepath = infoElem.dataset['linktarget'];
if (filepath) {
if (infoElem.dataset['isdir'] === "1") {
update_list(filepath);
}
else {
var lastSlash = filepath.lastIndexOf('/');
openpath(filepath.substring(lastSlash + 1), filepath.substring(0, lastSlash));
}
}
}
else if (targetElem.className.indexOf('folder-icon') > -1) {
update_list(concatPath(currentPath, targetElem.parentNode.dataset['filename']))
}
}
}
function refresh_list(filenames, path) {
var listHtml = '<table class="cbi-section-table"><tbody>';
if (path !== '/') {
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-2"><td class="parent-icon" colspan="6"><strong>..返回上级目录</strong></td></tr>';
}
if (filenames) {
for (var i = 0; i < filenames.length; i++) {
var line = filenames[i];
if (line) {
var f = line.match(/(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([\S\s]+)/);
var isLink = f[1][0] === 'z' || f[1][0] === 'l' || f[1][0] === 'x';
var o = {
displayname: f[9],
filename: isLink ? f[9].split(' -> ')[0] : f[9],
perms: f[1],
date: f[7] + ' ' + f[6] + ' ' + f[8],
size: f[5],
owner: f[3],
icon: (f[1][0] === 'd') ? "folder-icon" : (isLink ? "link-icon" : "file-icon")
};
var install_btn = ' <button class="cbi-button cbi-button-install" style="visibility: hidden;">安装</button>';
var index= o.filename.lastIndexOf(".");
var ext = o.filename.substr(index+1);
if (ext === 'ipk') {
install_btn = ' <button class="cbi-button cbi-button-install">安装</button>';
}
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-' + (1 + i%2)
+ '" data-filename="' + o.filename + '" data-isdir="' + Number(f[1][0] === 'd' || f[1][0] === 'z') + '"'
+ ((f[1][0] === 'z' || f[1][0] === 'l') ? (' data-linktarget="' + f[9].split(' -> ')[1]) : '')
+ '">'
+ '<td class="cbi-value-field ' + o.icon + '">'
+ '<strong>' + o.displayname + '</strong>'
+ '</td>'
+ '<td class="cbi-value-field cbi-value-date">'+o.date+'</td>'
+ '<td class="cbi-value-field cbi-value-size">'+o.size+'</td>'
+ '<td class="cbi-value-field cbi-value-perm">'+o.perms+'</td>'
+ '<td class="cbi-section-table-cell">\
<button class="cbi-button cbi-button-edit">重命名</button>\
<button class="cbi-button cbi-button-remove">删除</button>'
+ install_btn
+ '</td>'
+ '</tr>';
}
}
}
listHtml += "</table>";
listElem.innerHTML = listHtml;
}
function update_list(path, opt) {
opt = opt || {};
path = concatPath(path, '');
if (currentPath != path) {
iwxhr.get('/cgi-bin/luci/admin/system/fileassistant/list',
{path: path},
function (x, res) {
if (res.ec === 0) {
refresh_list(res.data, path);
}
else {
refresh_list([], path);
}
}
);
if (!opt.popState) {
history.pushState({path: path}, null, '?path=' + path);
}
currentPath = path;
pathElem.value = currentPath;
}
};
var uploadToggle = document.getElementById('upload-toggle');
var uploadContainer = document.getElementById('upload-container');
var isUploadHide = true;
uploadToggle.onclick = function() {
if (isUploadHide) {
uploadContainer.style.display = 'inline-flex';
}
else {
uploadContainer.style.display = 'none';
}
isUploadHide = !isUploadHide;
};
var uploadBtn = uploadContainer.getElementsByClassName('cbi-input-apply')[0];
uploadBtn.onclick = function (evt) {
var uploadinput = document.getElementById('upload-file');
var fullPath = uploadinput.value;
if (!fullPath) {
evt.preventDefault();
}
else {
var formData = new FormData();
var startIndex = (fullPath.indexOf('\\') >= 0 ? fullPath.lastIndexOf('\\') : fullPath.lastIndexOf('/'));
formData.append('upload-filename', fullPath.substring(startIndex + 1));
formData.append('upload-dir', concatPath(currentPath, ''));
formData.append('upload-file', uploadinput.files[0]);
var xhr = new XMLHttpRequest();
xhr.open("POST", "/cgi-bin/luci/admin/system/fileassistant/upload", true);
xhr.onload = function() {
if (xhr.status == 200) {
var res = JSON.parse(xhr.responseText);
refresh_list(res.data, currentPath);
uploadinput.value = '';
}
else {
alert('上传失败,请稍后再试...');
}
};
xhr.send(formData);
}
};
document.addEventListener('DOMContentLoaded', function(evt) {
var initPath = '/';
if (/path=([/\w]+)/.test(location.search)) {
initPath = RegExp.$1;
}
update_list(initPath, {popState: true});
});
window.addEventListener('popstate', function (evt) {
var path = '/';
if (evt.state && evt.state.path) {
path = evt.state.path;
}
update_list(path, {popState: true});
});
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,13 @@
module("luci.controller.advanced",package.seeall)
function index()
if not nixio.fs.access("/etc/config/advanced")then
return
end
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复
if nixio.fs.access("/usr/share/luci/menu.d/luci-app-advanced.json")then
return
end
local e
e=entry({"admin","system","advanced"},cbi("advanced"),_("高级设置"),60)
e.dependent=true
end
@@ -0,0 +1,224 @@
module("luci.controller.fileassistant", package.seeall)
local util = require "luci.util"
function index()
-- API 路由前置,供页面 XHR 使用
local function leaf(name, func)
entry({"admin", "system", "fileassistant", name}, call(func), nil).leaf = true
end
leaf("list", "fileassistant_list")
leaf("open", "fileassistant_open")
leaf("delete", "fileassistant_delete")
leaf("rename", "fileassistant_rename")
leaf("upload", "fileassistant_upload")
leaf("install", "fileassistant_install")
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复
if nixio.fs.access("/usr/share/luci/menu.d/luci-app-advanced.json") then
return
end
local page
page = entry({"admin", "system", "fileassistant"}, template("fileassistant"), _("文件管理"), 84)
page.i18n = "base"
page.dependent = true
end
function list_response(path, success)
luci.http.prepare_content("application/json")
local result
if success then
local rv = scandir(path)
result = {
ec = 0,
data = rv
}
else
result = {
ec = 1
}
end
luci.http.write_json(result)
end
function fileassistant_list()
local path = luci.http.formvalue("path")
list_response(path, true)
end
function fileassistant_open()
local path = luci.http.formvalue("path")
local filename = luci.http.formvalue("filename")
local io = require "io"
local mime = to_mime(filename)
file = path..filename
local download_fpi = io.open(file, "r")
luci.http.header('Content-Disposition', 'inline; filename="'..filename..'"' )
luci.http.prepare_content(mime)
luci.ltn12.pump.all(luci.ltn12.source.file(download_fpi), luci.http.write)
end
function fileassistant_delete()
local path = luci.http.formvalue("path")
local isdir = luci.http.formvalue("isdir")
path = path:gsub("<>", "/")
local success
if isdir then
success = os.execute('rm -r ' .. util.shellquote(path)) == 0
else
success = os.remove(path)
end
list_response(nixio.fs.dirname(path), success)
end
function fileassistant_rename()
local filepath = luci.http.formvalue("filepath")
local newpath = luci.http.formvalue("newpath")
local success = os.execute('mv ' .. util.shellquote(filepath) .. ' ' .. util.shellquote(newpath)) == 0
list_response(nixio.fs.dirname(filepath), success)
end
function fileassistant_install()
local filepath = luci.http.formvalue("filepath")
local isdir = luci.http.formvalue("isdir")
local ext = filepath:match(".+%%.(%w+)$")
filepath = filepath:gsub("<>", "/")
local success
if isdir == "1" then
success = false
elseif ext == "ipk" then
success = installIPK(filepath)
else
success = false
end
list_response(nixio.fs.dirname(filepath), success)
end
function installIPK(filepath)
luci.sys.exec('opkg --force-depends install ' .. util.shellquote(filepath))
luci.sys.exec('rm -rf /tmp/luci-*')
return true
end
function fileassistant_upload()
local filecontent = luci.http.formvalue("upload-file")
local filename = luci.http.formvalue("upload-filename")
local uploaddir = luci.http.formvalue("upload-dir")
local filepath = uploaddir..filename
local fp
luci.http.setfilehandler(
function(meta, chunk, eof)
if not fp and meta and meta.name == "upload-file" then
fp = io.open(filepath, "w")
end
if fp and chunk then
fp:write(chunk)
end
if fp and eof then
fp:close()
end
end
)
list_response(uploaddir, true)
end
function scandir(directory)
local i, t, popen = 0, {}, io.popen
local qdir = util.shellquote(directory)
local pfile = popen("ls -lh " .. qdir .. " | egrep '^d' ; ls -lh " .. qdir .. " | egrep -v '^d|^l'")
for fileinfo in pfile:lines() do
i = i + 1
t[i] = fileinfo
end
pfile:close()
pfile = popen("ls -lh " .. qdir .. " | egrep '^l' ;")
for fileinfo in pfile:lines() do
i = i + 1
linkindex, _, linkpath = string.find(fileinfo, "->%s+(.+)$")
local finalpath;
if string.sub(linkpath, 1, 1) == "/" then
finalpath = linkpath
else
finalpath = nixio.fs.realpath(directory..linkpath)
end
local linktype;
if not finalpath then
finalpath = linkpath;
linktype = 'x'
elseif nixio.fs.stat(finalpath, "type") == "dir" then
linktype = 'z'
else
linktype = 'l'
end
fileinfo = string.sub(fileinfo, 2, linkindex - 1)
fileinfo = linktype..fileinfo.."-> "..finalpath
t[i] = fileinfo
end
pfile:close()
return t
end
MIME_TYPES = {
["txt"] = "text/plain";
["conf"] = "text/plain";
["ovpn"] = "text/plain";
["log"] = "text/plain";
["js"] = "text/javascript";
["json"] = "application/json";
["css"] = "text/css";
["htm"] = "text/html";
["html"] = "text/html";
["patch"] = "text/x-patch";
["c"] = "text/x-csrc";
["h"] = "text/x-chdr";
["o"] = "text/x-object";
["ko"] = "text/x-object";
["bmp"] = "image/bmp";
["gif"] = "image/gif";
["png"] = "image/png";
["jpg"] = "image/jpeg";
["jpeg"] = "image/jpeg";
["svg"] = "image/svg+xml";
["zip"] = "application/zip";
["pdf"] = "application/pdf";
["xml"] = "application/xml";
["xsl"] = "application/xml";
["doc"] = "application/msword";
["ppt"] = "application/vnd.ms-powerpoint";
["xls"] = "application/vnd.ms-excel";
["odt"] = "application/vnd.oasis.opendocument.text";
["odp"] = "application/vnd.oasis.opendocument.presentation";
["pl"] = "application/x-perl";
["sh"] = "application/x-shellscript";
["php"] = "application/x-php";
["deb"] = "application/x-deb";
["iso"] = "application/x-cd-image";
["tgz"] = "application/x-compressed-tar";
["mp3"] = "audio/mpeg";
["ogg"] = "audio/x-vorbis+ogg";
["wav"] = "audio/x-wav";
["mpg"] = "video/mpeg";
["mpeg"] = "video/mpeg";
["avi"] = "video/x-msvideo";
}
function to_mime(filename)
if type(filename) == "string" then
local ext = filename:match("[^%.]+$")
if ext and MIME_TYPES[ext:lower()] then
return MIME_TYPES[ext:lower()]
end
end
return "application/octet-stream"
end
@@ -0,0 +1,340 @@
local e=require"nixio.fs"
local t=require"luci.sys"
local uci=luci.model.uci.cursor()
m=Map("advanced",translate("高级进阶设置"),translate("<font color=\"Red\"><strong>配置文档是直接编辑的除非你知道自己在干什么,否则请不要轻易修改这些配置文档。配置不正确可能会导致不能开机等错误。</strong></font><br/>"))
m.apply_on_parse=true
s=m:section(TypedSection,"advanced")
s.anonymous=true
if nixio.fs.access("/etc/dnsmasq.conf")then
s:tab("dnsmasqconf",translate("dnsmasq"),translate("本页是配置/etc/dnsmasq.conf的文档内容。应用保存后自动重启生效"))
conf=s:taboption("dnsmasqconf",Value,"dnsmasqconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/dnsmasq.conf")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/dnsmasq.conf",t)
if(luci.sys.call("cmp -s /tmp/dnsmasq.conf /etc/dnsmasq.conf")==1)then
e.writefile("/etc/dnsmasq.conf",t)
luci.sys.call("/etc/init.d/dnsmasq restart >/dev/null")
end
e.remove("/tmp/dnsmasq.conf")
end
end
end
if nixio.fs.access("/etc/config/network")then
s:tab("netwrokconf",translate("网络"),translate("本页是配置/etc/config/network包含网络配置文档内容。应用保存后自动重启生效"))
conf=s:taboption("netwrokconf",Value,"netwrokconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/network")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/network",t)
if(luci.sys.call("cmp -s /tmp/network /etc/config/network")==1)then
e.writefile("/etc/config/network",t)
luci.sys.call("/etc/init.d/network restart >/dev/null")
end
e.remove("/tmp/network")
end
end
end
if nixio.fs.access("/etc/config/wireless")then
s:tab("wirelessconf",translate("无线"), translate("本页是/etc/config/wireless的配置文件内容,应用保存后自动重启生效."))
conf=s:taboption("wirelessconf",Value,"wirelessconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/wireless")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/etc/config/wireless.tmp",t)
if(luci.sys.call("cmp -s /etc/config/wireless.tmp /etc/config/wireless")==1)then
e.writefile("/etc/config/wireless",t)
luci.sys.call("wifi reload >/dev/null &")
end
e.remove("/etc/config/wireless.tmp")
end
end
end
if nixio.fs.access("/etc/hosts")then
s:tab("hostsconf",translate("hosts"),translate("本页是配置/etc/hosts的文档内容。应用保存后自动重启生效"))
conf=s:taboption("hostsconf",Value,"hostsconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/hosts")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/hosts.tmp",t)
if(luci.sys.call("cmp -s /tmp/hosts.tmp /etc/hosts")==1)then
e.writefile("/etc/hosts",t)
luci.sys.call("/etc/init.d/dnsmasq restart >/dev/null")
end
e.remove("/tmp/hosts.tmp")
end
end
end
if nixio.fs.access("/etc/config/arpbind")then
s:tab("arpbindconf",translate("ARP绑定"),translate("本页是配置/etc/config/arpbind包含APR绑定MAC地址文档内容。应用保存后自动重启生效"))
conf=s:taboption("arpbindconf",Value,"arpbindconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/arpbind")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/arpbind",t)
if(luci.sys.call("cmp -s /tmp/arpbind /etc/config/arpbind")==1)then
e.writefile("/etc/config/arpbind",t)
luci.sys.call("/etc/init.d/arpbind restart >/dev/null")
end
e.remove("/tmp/arpbind")
end
end
end
if nixio.fs.access("/etc/config/firewall")then
s:tab("firewallconf",translate("防火墙"),translate("本页是配置/etc/config/firewall包含防火墙协议设置文档内容。应用保存后自动重启生效"))
conf=s:taboption("firewallconf",Value,"firewallconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/firewall")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/firewall",t)
if(luci.sys.call("cmp -s /tmp/firewall /etc/config/firewall")==1)then
e.writefile("/etc/config/firewall",t)
luci.sys.call("/etc/init.d/firewall restart >/dev/null")
end
e.remove("/tmp/firewall")
end
end
end
if nixio.fs.access("/etc/config/mwan3")then
s:tab("mwan3conf",translate("负载均衡"),translate("本页是配置/etc/config/mwan3包含负载均衡设置文档内容。应用保存后自动重启生效"))
conf=s:taboption("mwan3conf",Value,"mwan3conf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/mwan3")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/mwan3",t)
if(luci.sys.call("cmp -s /tmp/mwan3 /etc/config/mwan3")==1)then
e.writefile("/etc/config/mwan3",t)
luci.sys.call("/etc/init.d/mwan3 restart >/dev/null")
end
e.remove("/tmp/mwan3")
end
end
end
if nixio.fs.access("/etc/config/dhcp")then
s:tab("dhcpconf",translate("DHCP"),translate("本页是配置/etc/config/DHCP包含机器名等设置文档内容。应用保存后自动重启生效"))
conf=s:taboption("dhcpconf",Value,"dhcpconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/dhcp")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/dhcp",t)
if(luci.sys.call("cmp -s /tmp/dhcp /etc/config/dhcp")==1)then
e.writefile("/etc/config/dhcp",t)
luci.sys.call("/etc/init.d/network restart >/dev/null")
end
e.remove("/tmp/dhcp")
end
end
end
if nixio.fs.access("/etc/config/ddns")then
s:tab("ddnsconf",translate("DDNS"),translate("本页是配置/etc/config/ddns包含动态域名设置文档内容。应用保存后自动重启生效"))
conf=s:taboption("ddnsconf",Value,"ddnsconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/ddns")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/ddns",t)
if(luci.sys.call("cmp -s /tmp/ddns /etc/config/ddns")==1)then
e.writefile("/etc/config/ddns",t)
luci.sys.call("/etc/init.d/ddns restart >/dev/null")
end
e.remove("/tmp/ddns")
end
end
end
if nixio.fs.access("/etc/config/parentcontrol")then
s:tab("parentcontrolconf",translate("家长控制"),translate("本页是配置/etc/config/parentcontrol包含家长控制配置文档内容。应用保存后自动重启生效"))
conf=s:taboption("parentcontrolconf",Value,"parentcontrolconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/parentcontrol")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/parentcontrol",t)
if(luci.sys.call("cmp -s /tmp/parentcontrol /etc/config/parentcontrol")==1)then
e.writefile("/etc/config/parentcontrol",t)
luci.sys.call("/etc/init.d/parentcontrol restart >/dev/null")
end
e.remove("/tmp/parentcontrol")
end
end
end
if nixio.fs.access("/etc/config/autotimeset")then
s:tab("autotimesetconf",translate("定时设置"),translate("本页是配置/etc/config/autotimeset包含定时设置任务配置文档内容。应用保存后自动重启生效"))
conf=s:taboption("autotimesetconf",Value,"autotimesetconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/autotimeset")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/autotimeset",t)
if(luci.sys.call("cmp -s /tmp/autotimeset /etc/config/autotimeset")==1)then
e.writefile("/etc/config/autotimeset",t)
luci.sys.call("/etc/init.d/autotimeset restart >/dev/null")
end
e.remove("/tmp/autotimeset")
end
end
end
if nixio.fs.access("/etc/config/wolplus")then
s:tab("wolplusconf",translate("网络唤醒"),translate("本页是配置/etc/config/wolplus包含网络唤醒配置文档内容。应用保存后自动重启生效"))
conf=s:taboption("wolplusconf",Value,"wolplusconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/wolplus")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/wolplus",t)
if(luci.sys.call("cmp -s /tmp/wolplus /etc/config/wolplus")==1)then
e.writefile("/etc/config/wolplus",t)
luci.sys.call("/etc/init.d/wolplus restart >/dev/null")
end
e.remove("/tmp/wolplus")
end
end
end
if nixio.fs.access("/etc/config/smartdns")then
s:tab("smartdnsconf",translate("SMARTDNS"),translate("本页是配置/etc/config/smartdns包含smartdns配置文档内容。应用保存后自动重启生效"))
conf=s:taboption("smartdnsconf",Value,"smartdnsconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/smartdns")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/smartdns",t)
if(luci.sys.call("cmp -s /tmp/smartdns /etc/config/smartdns")==1)then
e.writefile("/etc/config/smartdns",t)
luci.sys.call("/etc/init.d/smartdns restart >/dev/null")
end
e.remove("/tmp/smartdns")
end
end
end
if nixio.fs.access("/etc/config/bypass")then
s:tab("bypassconf",translate("BYPASS"),translate("本页是配置/etc/config/bypass包含bypass配置文档内容。应用保存后自动重启生效"))
conf=s:taboption("bypassconf",Value,"bypassconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/bypass")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/bypass",t)
if(luci.sys.call("cmp -s /tmp/bypass /etc/config/bypass")==1)then
e.writefile("/etc/config/bypass",t)
luci.sys.call("/etc/init.d/bypass restart >/dev/null")
end
e.remove("/tmp/bypass")
end
end
end
if nixio.fs.access("/etc/config/openclash")then
s:tab("openclashconf",translate("openclash"),translate("本页是配置/etc/config/openclash的文档内容。应用保存后自动重启生效"))
conf=s:taboption("openclashconf",Value,"openclashconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
conf.template="cbi/tvalue"
conf.rows=20
conf.wrap="off"
conf.cfgvalue=function(t,t)
return e.readfile("/etc/config/openclash")or""
end
conf.write=function(a,a,t)
if t then
t=t:gsub("\r\n?","\n")
e.writefile("/tmp/openclash",t)
if(luci.sys.call("cmp -s /tmp/openclash /etc/config/openclash")==1)then
e.writefile("/etc/config/openclash",t)
luci.sys.call("/etc/init.d/openclash restart >/dev/null")
end
e.remove("/tmp/openclash")
end
end
end
return m
@@ -0,0 +1,20 @@
<%+header%>
<link rel="stylesheet" href="/luci-static/resources/fileassistant/fb.css?v=@ver">
<h2 name="content">文件管理【集成上传删除及安装,非专业人员请谨慎操作】</h2>
<fieldset class="cbi-section fb-container">
<input id="current-path" type="text" class="current-path cbi-input-text" value="/"/>
<div class="panel-container">
<div class="panel-title">文件列表</div>
<button id="upload-toggle" class="upload-toggle cbi-button cbi-button-edit">上传文件</button>
</div>
<div class="upload-container" id="upload-container">
<input id="upload-file" name="upload-file" class="upload-file" type="file">
<button type="button" class="cbi-button cbi-input-apply">执行上传</button>
</div>
<div id="list-content"></div>
</fieldset>
<script src="/luci-static/resources/fileassistant/fb.js?v=@ver"></script>
<%+footer%>
@@ -0,0 +1,20 @@
<%+header%>
<link rel="stylesheet" href="/luci-static/resources/fb/fb.css?v=@ver">
<h2 name="content">文件管理</h2>
<fieldset class="cbi-section fb-container">
<input id="current-path" type="text" class="current-path cbi-input-text" value="/"/>
<div class="panel-container">
<div class="panel-title">文件列表:</div>
<button id="upload-toggle" class="upload-toggle cbi-button cbi-button-edit">上传</button>
</div>
<div class="upload-container" id="upload-container">
<input id="upload-file" name="upload-file" class="upload-file" type="file">
<button type="button" class="cbi-button cbi-input-apply">点我上传</button>
</div>
<div id="list-content"></div>
</fieldset>
<script src="/luci-static/resources/fb/fb.js?v=@ver"></script>
<%+footer%>
@@ -0,0 +1,26 @@
#!/bin/sh
uci set network.@globals[0].ula_prefix=''
uci set network.lan.delegate='0'
uci set network.wan.mtu=1460
uci set network.wan.metric='41'
uci set network.wan.delegate='0'
uci set network.wan.ipv6='0'
uci commit network
uci set dhcp.@dnsmasq[0].cachesize='15000'
uci set dhcp.@dnsmasq[0].min_ttl='3600'
uci set dhcp.@dnsmasq[0].filter_aaaa='1'
uci set dhcp.@dnsmasq[0].localservice='0'
uci set dhcp.@dnsmasq[0].nonwildcard='0'
uci set dhcp.@dnsmasq[0].rebind_protection='0'
uci set dhcp.@dnsmasq[0].noresolv='0'
uci set dhcp.lan.ra=''
uci set dhcp.lan.ndp=''
uci set dhcp.lan.dhcpv6=''
uci set dhcp.lan.ignore='0'
uci set dhcp.lan.ra_management='1'
uci set dhcp.lan.ra_default='1'
uci set dhcp.lan.force='1'
uci commit dhcp
sed -i "/list server/d" /etc/config/dhcp
/etc/init.d/network restart
/etc/init.d/dnsmasq restart
@@ -0,0 +1,27 @@
#!/bin/sh
uci set dhcp.@dnsmasq[0].cachesize='15000'
uci set dhcp.@dnsmasq[0].min_ttl='3600'
uci set dhcp.@dnsmasq[0].filter_aaaa='0'
uci set dhcp.@dnsmasq[0].localservice='0'
uci set dhcp.@dnsmasq[0].nonwildcard='0'
uci set dhcp.@dnsmasq[0].rebind_protection='0'
uci set dhcp.@dnsmasq[0].noresolv='1'
uci set dhcp.lan.ra='server'
uci set dhcp.lan.ndp=''
uci set dhcp.lan.dhcpv6=''
uci set dhcp.lan.ignore='0'
uci set dhcp.lan.ra_management='1'
uci set dhcp.lan.ra_default='1'
uci set dhcp.lan.force='1'
uci commit dhcp
uci set network.@globals[0].ula_prefix=''
uci set network.lan.delegate='0'
uci set network.wan.mtu=1460
uci set network.wan.metric='41'
uci set network.wan.delegate='0'
uci set network.wan.ipv6='auto'
uci commit network
sed -i "/list server/d" /etc/config/dhcp
/etc/init.d/network restart
/etc/init.d/dnsmasq restart
@@ -0,0 +1,30 @@
#!/bin/sh
uci set system.@system[0].hostname="Openwrt"
uci commit
cat > /etc/config/network <<EOF
config interface 'loopback'
option ifname 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config interface 'lan'
option type 'bridge'
option proto 'static'
option ipaddr '192.168.1.1'
option netmask '255.255.255.0'
option ifname 'eth0 eth2 eth3'
config interface 'wan'
option ifname 'eth1'
option proto 'dhcp'
option hostname 'Openwrt'
EOF
sed -i '/REDIRECT --to-ports 53/d' /etc/firewall.user
sed -i '/MASQUERADE/d' /etc/firewall.user
echo "iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 53" >> /etc/firewall.user
echo "iptables -t nat -A PREROUTING -p tcp --dport 53 -j REDIRECT --to-ports 53" >> /etc/firewall.user
reboot
@@ -0,0 +1,80 @@
#!/bin/sh
ip=/usr/sbin/ip
vconfig=/sbin/vconfig
ifconfig=/sbin/ifconfig
uci set system.@system[0].hostname="Openwrt"
uci commit
cat > /etc/config/network <<EOF
config interface 'loopback'
option ifname 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config interface 'lan'
option type 'bridge'
option ifname 'eth0 eth1 eth2 eth3 eth4 eth5 eth0.12 eth0.13 eth0.14 eth0.15 eth0.16 eth0.17 eth0.18'
option proto 'static'
option ipaddr '192.168.1.2'
option netmask '255.255.255.0'
option gateway '192.168.1.1'
option dns '223.5.5.5'
config interface 'wan'
option ifname 'eth0.11'
option proto 'dhcp'
option hostname 'Openwrt'
EOF
#for eth1
$ip link add link eth0 eth0.11 type vlan proto 802.1ad id 11
$ifconfig eth0.11 hw ether 58:b0:35:86:cf:11
$vconfig set_flag eth0.11 1 1
$ifconfig eth0.11 up
#for eth2
$ip link add link eth0 eth0.12 type vlan proto 802.1ad id 12
$ifconfig eth0.12 hw ether 58:b0:35:86:cf:12
$vconfig set_flag eth0.12 1 2
$ifconfig eth0.12 up
#for eth3
$ip link add link eth0 eth0.13 type vlan proto 802.1ad id 13
$ifconfig eth0.13 hw ether 58:b0:35:86:cf:13
$vconfig set_flag eth0.13 1 3
$ifconfig eth0.13 up
#for eth4
$ip link add link eth0 eth0.14 type vlan proto 802.1ad id 14
$ifconfig eth0.14 hw ether 58:b0:35:86:cf:14
$vconfig set_flag eth0.14 1 3
$ifconfig eth0.14 up
#for eth5
$ip link add link eth0 eth0.15 type vlan proto 802.1ad id 15
$ifconfig eth0.15 hw ether 58:b0:35:86:cf:15
$vconfig set_flag eth0.15 1 3
$ifconfig eth0.15 up
#for eth6
$ip link add link eth0 eth0.16 type vlan proto 802.1ad id 16
$ifconfig eth0.16 hw ether 58:b0:35:86:cf:16
$vconfig set_flag eth0.16 1 3
$ifconfig eth0.16 up
#for eth7
$ip link add link eth0 eth0.17 type vlan proto 802.1ad id 17
$ifconfig eth0.17 hw ether 58:b0:35:86:cf:17
$vconfig set_flag eth0.17 1 3
$ifconfig eth0.17 up
#for eth8
$ip link add link eth0 eth0.18 type vlan proto 802.1ad id 18
$ifconfig eth0.18 hw ether 58:b0:35:86:cf:18
$vconfig set_flag eth0.18 1 3
$ifconfig eth0.18 up
sed -i '/MASQUERADE/d' /etc/firewall.user
echo "iptables -t nat -I POSTROUTING -j MASQUERADE" >> /etc/firewall.user
reboot
@@ -0,0 +1,2 @@
config advanced
option enabled '1'
@@ -0,0 +1,7 @@
#!/bin/sh
ver=$(date +%s)
[ -f /usr/lib/lua/luci/view/filebrowser.htm ] && sed -i "s/@ver/$ver/g" /usr/lib/lua/luci/view/filebrowser.htm
rm -f /tmp/luci-indexcache
exit 0
@@ -0,0 +1,24 @@
{
"admin/system/advanced": {
"title": "高级设置",
"order": 60,
"action": {
"type": "cbi",
"path": "advanced"
},
"acl": {
"anonymous": false
}
},
"admin/system/fileassistant": {
"title": "文件管理",
"order": 84,
"action": {
"type": "template",
"path": "fileassistant"
},
"acl": {
"anonymous": false
}
}
}
@@ -0,0 +1,11 @@
{
"luci-app-advanced": {
"description": "Grant UCI access for luci-app-advanced",
"read": {
"uci": [ "advanced" ]
},
"write": {
"uci": [ "advanced" ]
}
}
}
@@ -1,24 +1,24 @@
<div align="center">
</a><a href="https://github.com/gngpp/luci-app-design-config/releases">
<img src="https://img.shields.io/github/release/gngpp/luci-app-design-config.svg?style=flat">
</a><a href="https://github.com/gngpp/luci-app-design-config/releases">
<img src="https://img.shields.io/github/downloads/gngpp/luci-app-design-config/total?style=flat">
</a><a href="https://github.com/kenzok78/luci-design-bundle/releases">
<img src="https://img.shields.io/github/release/kenzok78/luci-design-bundle.svg?style=flat">
</a><a href="https://github.com/kenzok78/luci-design-bundle/releases">
<img src="https://img.shields.io/github/downloads/kenzok78/luci-design-bundle/total?style=flat">
</a>
</div>
<br>
# luci-app-design-config
Design Theme Config Plugin
Design 主题配置插件
### Features
- Support changing theme dark/light mode
- Support display/hide navbar
- Support for replacing commonly used proxy icons
### 功能特性
- 支持更改主题深色/浅色模式
- 支持显示/隐藏导航栏
- 支持更换常用的代理图标
### Compile
### 编译
```
git clone https://github.com/gngpp/luci-app-design-config.git package/luci-app-design-config
make menuconfig # choose LUCI->Applications->luci-app-design-config
git clone https://github.com/kenzok78/luci-design-bundle.git package/luci-app-design-config
make menuconfig # 选择 LUCI->Applications->luci-app-design-config
make V=s
```
@@ -4,10 +4,15 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-easymesh
LUCI_TITLE:=LuCI Support for easymesh
LUCI_DEPENDS:= +kmod-cfg80211 +batctl-default +kmod-batman-adv +wpad-mesh-openssl +dawn
PKG_VERSION:=2.0
PKG_RELEASE:=1
PKG_MAINTAINER:=kenzok78 <https://github.com/kenzok78>
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
$(eval $(call BuildPackage,$(PKG_NAME)))
+64 -5
View File
@@ -1,10 +1,69 @@
# luci-app-easymesh
水平有限写了基于kmod-batman-adv+802.11s 有线+无线回程的mesh luci设置插件。
新增ap设置让设置更加方便。
## 软件包描述
新增KVR设置并添加dawn依赖在ap之间切换延迟明显降低
LuCI 管理界面 for EasyMesh。基于 LuCI 的 Batman-Adv mesh 网络配置界面,支持 802.11s,可在 OpenWrt 上实现无缝无线/有线回程 mesh 网络
插件只对https://github.com/coolsnowwolf/lede lean版openwrt做了测试 其他源码出现问题请自行解决。
## 功能特性
写的很烂暂时能用
<small>
- Batman-Adv mesh 网络配置
- 802.11s 无线 mesh 支持
- 有线/无线回程支持
- AP 模式,支持自定义 IP 配置
- K/V/R802.11k/v/r)支持,优化漫游
- 与 DAWN 集成,实现动态漫游决策
</small>
## 依赖项
<small>
- `kmod-cfg80211`
- `kmod-batman-adv`
- `batctl-default`
- `wpad-mesh-openssl`
- `dawn`
</small>
## 硬件要求
<small>
- 支持 802.11s mesh 的无线网卡(如 MediaTek MT76、Qualcomm Atheros 等)
- 已加载 Batman-Adv 内核模块
</small>
## 软件包路径说明
这是原始 `kenzok8/openwrt-packages` 软件包的标准化修复版本,遵循标准 OpenWrt LuCI 应用布局。
## 修复的问题
<small>
- CBI 模型 `detect_Node()` 函数中的语法错误(括号不平衡)
- CBI 模型中的全局变量泄漏(Lua 全局变量 `v``s``apRadio``enable`
- 控制器中缺少 `nixio` require
- 控制器中 `nixio.fs.access` 调用未 require `nixio.fs`
- init 脚本 shell 函数中缺少 `local` 声明
- shell 脚本中未引用的变量
- 脆弱的 `grep` 命令解析替换为健壮的 `uci show` 解析
- `add_wifi_mesh` 函数重构为接受 `apall` 参数,而不是依赖全局作用域
- `uci commit` 调用适当批处理
- `batctl n` 命令输出解析修复,使用正确的 `io.popen` 而不是 `util.execi`
- `tail -n +2` 与标题跳过逻辑一致
- `encryption` 字符串比较从数字修复为字符串
- `po/zh-cn` 目录已移除(`po/zh_Hans` 的重复)
- uci-defaults 脚本添加了 `IPKG_INSTROOT` 检查
- Makefile 添加了缺少的 `PKG_MAINTAINER``PKG_RELEASE` 字段
</small>
## 原始作者
dz &lt;dingzhong110@gmail.com&gt;
@@ -2,8 +2,15 @@
module("luci.controller.easymesh", package.seeall)
local fs = require "nixio.fs"
function index()
if not nixio.fs.access("/etc/config/easymesh") then
if not fs.access("/etc/config/easymesh") then
return
end
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复
if fs.access("/usr/share/luci/menu.d/luci-app-easymesh.json") then
return
end
@@ -1,49 +1,50 @@
-- Copyright (C) 2021 dz <dingzhong110@gmail.com>
local m,s,o
local m, s, o
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local util = require "luci.util"
m = Map("easymesh")
function detect_Node()
local function detect_Node()
local data = {}
local lps = luci.util.execi(" batctl n 2>/dev/null | tail +2 | sed 's/^[ ][ ]*//g' | sed 's/[ ][ ]*/ /g' | sed 's/$/ /g' ")
for value in lps do
local row = {}
local pos = string.find(value, " ")
local IFA = string.sub(value, 1, pos - 1)
local value = string.sub(value, pos + 1, string.len(value))
pos = string.find(value, " ")
local pos = string.find(value, " ")
local Neighbora = string.sub(value, 1, pos - 1)
local value = string.sub(value, pos + 1, string.len(value))
pos = string.find(value, " ")
local pos = string.find(value, " ")
local lastseena = string.sub(value, 1, pos - 1)
local value = string.sub(value, pos + 1, string.len(value))
pos = string.find(value, " ")
row["IF"] = IFA
row["Neighbor"] = Neighbora
row["lastseen"] = lastseena
table.insert(data, row)
local cmd = "batctl n 2>/dev/null | tail -n +2 | sed 's/^[ ]*//g' | sed 's/[ ]*/ /g'"
local fp = io.popen(cmd)
if not fp then return data end
for line in fp:lines() do
local pos = string.find(line, " ")
if pos then
local ifa = string.sub(line, 1, pos - 1)
local rest = string.sub(line, pos + 1)
pos = string.find(rest, " ")
if pos then
local neighbor = string.sub(rest, 1, pos - 1)
rest = string.sub(rest, pos + 1)
pos = string.find(rest, " ")
if pos then
local lastseen = string.sub(rest, 1, pos - 1)
table.insert(data, { IF = ifa, Neighbor = neighbor, lastseen = lastseen })
end
end
end
end
fp:close()
return data
end
local Nodes = luci.sys.exec("batctl n 2>/dev/null| tail +3 | wc -l")
local Nodes = sys.exec("batctl n 2>/dev/null | tail -n +2 | wc -l")
local Node = detect_Node()
v = m:section(Table, Node, "" ,"<b>" .. translate("Active node") .. "" .. Nodes .. "</b>")
local v = m:section(Table, Node, "", "<b>" .. translate("Active node") .. "" .. Nodes .. "</b>")
v:option(DummyValue, "IF", translate("IF"))
v:option(DummyValue, "Neighbor", translate("Neighbor"))
v:option(DummyValue, "lastseen", translate("lastseen"))
-- Basic
s = m:section(TypedSection, "easymesh", translate("Settings"), translate("General Settings"))
s.anonymous = true
---- Eanble
o = s:option(Flag, "enabled", translate("Enable"), translate("Enable or disable EASY MESH"))
o.default = 0
o.default = "0"
o.rmempty = false
o = s:option(ListValue, "role", translate("role"))
@@ -52,76 +53,68 @@ o:value("server", translate("host MESH"))
o:value("client", translate("son MESH"))
o.rmempty = false
apRadio = s:option(ListValue, "apRadio", translate("MESH Radio device"), translate("The radio device which MESH use"))
local apRadio = s:option(ListValue, "apRadio", translate("MESH Radio device"), translate("The radio device which MESH use"))
uci:foreach("wireless", "wifi-device",
function(s)
apRadio:value(s['.name'])
end)
function(sect)
apRadio:value(sect['.name'])
end)
apRadio:value("all", translate("ALL"))
o.default = "radio0"
o.rmempty = false
apRadio.default = "radio0"
apRadio.rmempty = false
---- mesh
o = s:option(Value, "mesh_id", translate("MESH ID"))
o.default = "easymesh"
o.description = translate("MESH ID")
enable = s:option(Flag, "encryption", translate("Encryption"), translate(""))
enable.default = 0
enable.rmempty = false
o = s:option(Flag, "encryption", translate("Encryption"), "")
o.default = "0"
o.rmempty = false
o = s:option(Value, "key", translate("Key"))
o.default = "easymesh"
o:depends("encryption", 1)
o:depends("encryption", "1")
---- kvr
enable = s:option(Flag, "kvr", translate("K/V/R"), translate(""))
enable.default = 1
enable.rmempty = false
o = s:option(Flag, "kvr", translate("K/V/R"), "")
o.default = "1"
o.rmempty = false
o = s:option(Value, "mobility_domain", translate("Mobility Domain"), translate("4-character hexadecimal ID"))
o.default = "4f57"
o.datatype = "and(hexstring,rangelength(4,4))"
o:depends("kvr", 1)
o:depends("kvr", "1")
o = s:option(Value, "rssi_val", translate("Threshold for an good RSSI"))
o = s:option(Value, "rssi_val", translate("Threshold for a good RSSI"))
o.default = "-60"
o.atatype = "range(-1,-120)"
o:depends("kvr", 1)
o.datatype = "range(-120,-1)"
o:depends("kvr", "1")
o = s:option(Value, "low_rssi_val", translate("Threshold for an bad RSSI"))
o = s:option(Value, "low_rssi_val", translate("Threshold for a bad RSSI"))
o.default = "-88"
o.atatype = "range(-1,-120)"
o:depends("kvr", 1)
o.datatype = "range(-120,-1)"
o:depends("kvr", "1")
---- 802.11F
--enable = s:option(Flag, "iapp", translate("inter-access point protocol"), translate("Wireless Access Points (APs) running on different vendors can communicate with each other"))
--enable.default = 0
--enable.rmempty = false
---- ap_mode
enable = s:option(Flag, "ap_mode", translate("AP MODE Enable"), translate("Enable or disable AP MODE"))
enable.default = 0
enable.rmempty = false
o = s:option(Flag, "ap_mode", translate("AP MODE Enable"), translate("Enable or disable AP MODE"))
o.default = "0"
o.rmempty = false
o = s:option(Value, "ipaddr", translate("IPv4-Address"))
o.default = "192.168.1.10"
o.datatype = "ip4addr"
o:depends("ap_mode", 1)
o:depends("ap_mode", "1")
o = s:option(Value, "netmask", translate("IPv4 netmask"))
o.default = "255.255.255.0"
o.datatype = "ip4addr"
o:depends("ap_mode", 1)
o:depends("ap_mode", "1")
o = s:option(Value, "gateway", translate("IPv4 gateway"))
o.default = "192.168.1.1"
o.datatype = "ip4addr"
o:depends("ap_mode", 1)
o:depends("ap_mode", "1")
o = s:option(Value, "dns", translate("Use custom DNS servers"))
o.default = "192.168.1.1"
o.datatype = "ip4addr"
o:depends("ap_mode", 1)
o:depends("ap_mode", "1")
return m
@@ -1 +0,0 @@
zh-cn
@@ -1,35 +1,37 @@
#!/bin/sh /etc/rc.common
START=99
STOP=70
load_easymesh_config() {
enable=$(uci -q get easymesh.config.enabled)
mesh_bat0=$(uci -q get network.bat0)
ap_mode=$(uci -q get easymesh.config.ap_mode)
lan=$(uci -q get network.lan.ifname)
ipaddr=$(uci -q get easymesh.config.ipaddr)
netmask=$(uci -q get easymesh.config.netmask)
gateway=$(uci -q get easymesh.config.gateway)
dns=$(uci -q get easymesh.config.dns)
ap_ipaddr=$(uci -q get network.lan.ipaddr)
ap_ipaddr1=$(sed -n '1p' /etc/easymesh 2>/dev/null)
apRadio=$(uci -q get easymesh.config.apRadio)
kvr=$(uci -q get easymesh.config.kvr)
iapp=$(uci -q get easymesh.config.iapp)
brlan=$(uci -q get network.@device[0].name)
role=$(uci -q get easymesh.config.role)
local enable=$(uci -q get easymesh.config.enabled)
local mesh_bat0=$(uci -q get network.bat0)
local ap_mode=$(uci -q get easymesh.config.ap_mode)
local lan=$(uci -q get network.lan.ifname)
local ipaddr=$(uci -q get easymesh.config.ipaddr)
local netmask=$(uci -q get easymesh.config.netmask)
local gateway=$(uci -q get easymesh.config.gateway)
local dns=$(uci -q get easymesh.config.dns)
local ap_ipaddr=$(uci -q get network.lan.ipaddr)
local ap_ipaddr1=$(sed -n '1p' /etc/easymesh 2>/dev/null)
local apRadio=$(uci -q get easymesh.config.apRadio)
local kvr=$(uci -q get easymesh.config.kvr)
local iapp=$(uci -q get easymesh.config.iapp)
local brlan=$(uci -q get network.@device[0].name)
local role=$(uci -q get easymesh.config.role)
}
ap_mode_stop() {
ap_ipaddr=$(uci -q get network.lan.ipaddr)
ap_ipaddr1=$(sed -n '1p' /etc/easymesh 2>/dev/null)
dns1=$(sed -n '2p' /etc/easymesh 2>/dev/null)
local ap_ipaddr=$(uci -q get network.lan.ipaddr)
local ap_ipaddr1=$(sed -n '1p' /etc/easymesh 2>/dev/null)
local dns1=$(sed -n '2p' /etc/easymesh 2>/dev/null)
if [ "$ap_ipaddr" = "$ap_ipaddr1" ]; then
uci -q delete network.lan.gateway
uci -q del_list network.lan.dns=$dns1
uci -q del_list network.lan.dns="$dns1"
uci commit network
echo "" >/etc/easymesh
echo "" > /etc/easymesh
uci -q delete dhcp.lan.dynamicdhcp
uci -q delete dhcp.lan.ignore
@@ -41,94 +43,98 @@ ap_mode_stop() {
}
add_wifi_mesh() {
mesh_nwi_mesh=$(uci -q get network.nwi_mesh_${apall})
mesh_apRadio=$(uci -q get wireless.mesh_${apall}.device)
mesh_mesh=$(uci -q get wireless.mesh_${apall})
mesh_id=$(uci -q get easymesh.config.mesh_id)
mobility_domain=$(uci -q get easymesh.config.mobility_domain)
key=$(uci -q get easymesh.config.key)
encryption=$(uci -q get easymesh.config.encryption)
local apall="$1"
local mesh_nwi_mesh=$(uci -q get network.nwi_mesh_${apall})
local mesh_apRadio=$(uci -q get wireless.mesh_${apall}.device)
local mesh_mesh=$(uci -q get wireless.mesh_${apall})
local mesh_id=$(uci -q get easymesh.config.mesh_id)
local mobility_domain=$(uci -q get easymesh.config.mobility_domain)
local key=$(uci -q get easymesh.config.key)
local encryption=$(uci -q get easymesh.config.encryption)
if [ "$mesh_nwi_mesh" != "interface" ]; then
uci set network.nwi_mesh_$apall=interface
uci set network.nwi_mesh_$apall.proto='batadv_hardif'
uci set network.nwi_mesh_$apall.master='bat0'
uci set network.nwi_mesh_$apall.mtu='1536'
uci set network.nwi_mesh_${apall}=interface
uci set network.nwi_mesh_${apall}.proto='batadv_hardif'
uci set network.nwi_mesh_${apall}.master='bat0'
uci set network.nwi_mesh_${apall}.mtu='1536'
uci commit network
fi
if [ "$mesh_mesh" != "wifi-iface" ]; then
uci set wireless.mesh_$apall=wifi-iface
uci set wireless.mesh_$apall.device=$apall
uci set wireless.mesh_$apall.ifname=mesh_${apall}
uci set wireless.mesh_$apall.network=nwi_mesh_${apall}
uci set wireless.mesh_$apall.mode='mesh'
uci set wireless.mesh_$apall.mesh_id=$mesh_id
uci set wireless.mesh_$apall.mesh_fwding='0'
uci set wireless.mesh_$apall.mesh_ttl='1'
uci set wireless.mesh_$apall.mcast_rate='24000'
uci set wireless.mesh_$apall.disabled='0'
uci set wireless.mesh_${apall}=wifi-iface
uci set wireless.mesh_${apall}.device="${apall}"
uci set wireless.mesh_${apall}.ifname="mesh_${apall}"
uci set wireless.mesh_${apall}.network="nwi_mesh_${apall}"
uci set wireless.mesh_${apall}.mode='mesh'
uci set wireless.mesh_${apall}.mesh_id="${mesh_id}"
uci set wireless.mesh_${apall}.mesh_fwding='0'
uci set wireless.mesh_${apall}.mesh_ttl='1'
uci set wireless.mesh_${apall}.mcast_rate='24000'
uci set wireless.mesh_${apall}.disabled='0'
uci commit wireless
fi
if [ "$mesh_mesh" = "wifi-iface" ]; then
if [ "$mesh_apRadio" != "$apall" ]; then
uci set wireless.mesh_$apall.device=$apall
uci commit wireless
fi
if [ "$mesh_mesh" = "wifi-iface" ] && [ "$mesh_apRadio" != "$apall" ]; then
uci set wireless.mesh_${apall}.device="${apall}"
uci commit wireless
fi
if [ "$encryption" != 1 ]; then
uci set wireless.mesh_$apall.encryption='none'
if [ "$encryption" != "1" ]; then
uci set wireless.mesh_${apall}.encryption='none'
uci commit wireless
else
uci set wireless.mesh_$apall.encryption='sae'
uci set wireless.mesh_$apall.key=$key
uci set wireless.mesh_${apall}.encryption='sae'
uci set wireless.mesh_${apall}.key="${key}"
uci commit wireless
fi
}
add_kvr() {
kvr=$(uci -q get easymesh.config.kvr)
mobility_domain=$(uci -q get easymesh.config.mobility_domain)
iapp=$(uci -q get easymesh.config.iapp)
for apall in $(uci -X show wireless | grep wifi-device | awk -F'[.=]' '{print $2}'); do
if [ "$kvr" = 1 ]; then
uci set wireless.default_$apall.ieee80211k='1'
uci set wireless.default_$apall.rrm_neighbor_report='1'
uci set wireless.default_$apall.rrm_beacon_report='1'
uci set wireless.default_$apall.ieee80211v='1'
uci set wireless.default_$apall.bss_transition='1'
uci set wireless.default_$apall.ieee80211r='1'
uci set wireless.default_$apall.encryption='psk2+ccmp'
uci set wireless.default_$apall.mobility_domain=$mobility_domain
uci set wireless.default_$apall.ft_over_ds='1'
uci set wireless.default_$apall.ft_psk_generate_local='1'
uci commit wireless
local kvr=$(uci -q get easymesh.config.kvr)
local mobility_domain=$(uci -q get easymesh.config.mobility_domain)
local iapp=$(uci -q get easymesh.config.iapp)
local commit_wireless=
for apall in $(uci show wireless 2>/dev/null | grep "=wifi-device" | sed -n 's/^wireless\.\([^=]*\)=.*/\1/p'); do
if [ "$kvr" = "1" ]; then
uci set wireless.default_${apall}.ieee80211k='1'
uci set wireless.default_${apall}.rrm_neighbor_report='1'
uci set wireless.default_${apall}.rrm_beacon_report='1'
uci set wireless.default_${apall}.ieee80211v='1'
uci set wireless.default_${apall}.bss_transition='1'
uci set wireless.default_${apall}.ieee80211r='1'
uci set wireless.default_${apall}.encryption='psk2+ccmp'
uci set wireless.default_${apall}.mobility_domain="${mobility_domain}"
uci set wireless.default_${apall}.ft_over_ds='1'
uci set wireless.default_${apall}.ft_psk_generate_local='1'
commit_wireless=1
else
uci -q delete wireless.default_$apall.ieee80211k
uci -q delete wireless.default_$apall.ieee80211v
uci -q delete wireless.default_$apall.ieee80211r
uci commit wireless
uci -q delete wireless.default_${apall}.ieee80211k
uci -q delete wireless.default_${apall}.ieee80211v
uci -q delete wireless.default_${apall}.ieee80211r
commit_wireless=1
fi
if [ "$iapp" = 1 ]; then
uci set wireless.default_$apall.iapp_interface='br-lan'
uci commit wireless
if [ "$iapp" = "1" ]; then
uci set wireless.default_${apall}.iapp_interface='br-lan'
commit_wireless=1
else
uci -q delete wireless.default_$apall.iapp_interface
uci commit wireless
uci -q delete wireless.default_${apall}.iapp_interface
commit_wireless=1
fi
done
[ -n "$commit_wireless" ] && uci commit wireless
}
add_dawn() {
kvr=$(uci -q get easymesh.config.kvr)
rssi_val=$(uci -q get easymesh.config.rssi_val)
low_rssi_val=$(uci -q get easymesh.config.low_rssi_val)
local kvr=$(uci -q get easymesh.config.kvr)
local rssi_val=$(uci -q get easymesh.config.rssi_val)
local low_rssi_val=$(uci -q get easymesh.config.low_rssi_val)
if [ "$kvr" = 1 ]; then
uci set dawn.@metric[0].rssi_val=$rssi_val
uci set dawn.@metric[0].low_rssi_val=$low_rssi_val
if [ "$kvr" = "1" ]; then
uci set dawn.@metric[0].rssi_val="${rssi_val}"
uci set dawn.@metric[0].low_rssi_val="${low_rssi_val}"
uci commit dawn
/etc/init.d/dawn enable && /etc/init.d/dawn start
else
@@ -138,7 +144,8 @@ add_dawn() {
set_easymesh() {
load_easymesh_config
if [ "$enable" = 1 ]; then
if [ "$enable" = "1" ]; then
if [ "$mesh_bat0" != "interface" ]; then
uci set network.bat0=interface
uci set network.bat0.proto='batadv'
@@ -149,8 +156,6 @@ set_easymesh() {
uci set network.bat0.bridge_loop_avoidance='1'
uci set network.bat0.distributed_arp_table='1'
uci set network.bat0.fragmentation='1'
# uci set network.bat0.gw_bandwidth='10000/2000'
# uci set network.bat0.gw_sel_class='20'
uci set network.bat0.hop_penalty='30'
uci set network.bat0.isolation_mark='0x00000000/0x00000000'
uci set network.bat0.log_level='0'
@@ -176,28 +181,27 @@ set_easymesh() {
fi
if [ "$apRadio" = "all" ]; then
for apall in $(uci -X show wireless | grep wifi-device | awk -F'[.=]' '{print $2}'); do
add_wifi_mesh
for apall in $(uci show wireless 2>/dev/null | grep "=wifi-device" | sed -n 's/^wireless\.\([^=]*\)=.*/\1/p'); do
add_wifi_mesh "${apall}"
done
else
apall=$apRadio
add_wifi_mesh
add_wifi_mesh "${apRadio}"
fi
add_kvr
add_dawn
if [ "$ap_mode" = 1 ]; then
if [ "$ap_mode" = "1" ]; then
if [ "$ap_ipaddr" != "$ipaddr" ]; then
uci set network.lan.ipaddr=$ipaddr
uci set network.lan.netmask=$netmask
uci set network.lan.gateway=$gateway
uci add_list network.lan.dns=$dns
uci set network.lan.ipaddr="${ipaddr}"
uci set network.lan.netmask="${netmask}"
uci set network.lan.gateway="${gateway}"
uci add_list network.lan.dns="${dns}"
uci commit network
echo "" >/etc/easymesh
echo "$ipaddr" >/etc/easymesh
echo "$dns" >>/etc/easymesh
echo "" > /etc/easymesh
echo "${ipaddr}" > /etc/easymesh
echo "${dns}" >> /etc/easymesh
uci set dhcp.lan.dynamicdhcp='0'
uci set dhcp.lan.ignore='1'
@@ -223,17 +227,17 @@ set_easymesh() {
uci commit network
fi
for apall in $(uci -X show wireless | grep wifi-device | awk -F'[.=]' '{print $2}'); do
mesh_nwi_mesh=$(uci -q get network.nwi_mesh_${apall})
mesh_mesh=$(uci -q get wireless.mesh_${apall})
for apall in $(uci show wireless 2>/dev/null | grep "=wifi-device" | sed -n 's/^wireless\.\([^=]*\)=.*/\1/p'); do
local mesh_nwi_mesh=$(uci -q get network.nwi_mesh_${apall})
local mesh_mesh=$(uci -q get wireless.mesh_${apall})
if [ "$mesh_nwi_mesh" = "interface" ]; then
uci -q delete network.nwi_mesh_$apall
uci -q delete network.nwi_mesh_${apall}
uci commit network
fi
if [ "$mesh_mesh" = "wifi-iface" ]; then
uci -q delete wireless.mesh_$apall
uci -q delete wireless.mesh_${apall}
uci commit wireless
fi
done
@@ -241,10 +245,11 @@ set_easymesh() {
add_kvr
add_dawn
if [ "$ap_mode" = 1 ]; then
if [ "$ap_mode" = "1" ]; then
ap_mode_stop
fi
fi
/etc/init.d/network restart
}
@@ -1,4 +1,5 @@
#!/bin/sh
[ -z "${IPKG_INSTROOT}" ] || exit 0
uci -q batch <<-EOF >/dev/null
delete ucitrack.@easymesh[-1]
@@ -0,0 +1,14 @@
{
"admin/network/easymesh": {
"title": "EASY MESH",
"order": 60,
"action": {
"type": "cbi",
"path": "easymesh"
},
"depends": {
"acl": [ "luci-app-easymesh" ],
"uci": { "easymesh": true }
}
}
}
@@ -2,7 +2,7 @@
"luci-app-easymesh": {
"description": "Grant UCI access for luci-app-easymesh",
"read": {
"uci": [ "easymesh" ]
"uci": [ "easymesh", "wireless", "network" ]
},
"write": {
"uci": [ "easymesh" ]
+16 -10
View File
@@ -1,20 +1,26 @@
#
# Copyright (C) 2006-2017 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-eqos
PKG_VERSION:=2.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=Jianhui Zhao <jianhuizhao329@gmail.com> GaryPang <https://github.com/garypang13/luci-app-eqos>
PKG_MAINTAINER:=kenzok78 <admin@kenzok78.com>
PKG_LICENSE:=GPL-2.0
LUCI_TITLE:=EQOS - LuCI interface
LUCI_TITLE:=LuCI support for EQOS
LUCI_DEPENDS:=+luci-base +tc +kmod-sched-core +kmod-ifb
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
define Package/luci-app-eqos/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/etc/config/eqos $(1)/etc/config/eqos
$(INSTALL_BIN) ./files/etc/init.d/eqos $(1)/etc/init.d/eqos
$(INSTALL_BIN) ./files/etc/hotplug.d/iface/10-eqos $(1)/etc/hotplug.d/iface/10-eqos
$(INSTALL_BIN) ./files/usr/sbin/eqos $(1)/usr/sbin/eqos
endef
$(eval $(call BuildPackage,luci-app-eqos))
+114 -28
View File
@@ -1,32 +1,118 @@
# Easy QoS for OpenWRT/Lede([中文](https://github.com/garypang13/luci-app-eqos/blob/master/README_ZH.md))
# luci-app-eqos
![](https://img.shields.io/badge/license-GPLV3-brightgreen.svg?style=plastic "License")
[![license](https://img.shields.io/badge/license-GPLv2-brightgreen.svg)](LICENSE)
![](https://github.com/lwxlwxlwx/blob/master/eqos.png)
EQOS (Easy QoS) for OpenWrt LuCI - 基于IP限速的流量控制工具
# Features
* Support speed limit based on IP address
* No marking by iptables
* Support LuCI interface
## 特性
# Install to OpenWRT/LEDE
git clone https://github.com/garypang13/luci-app-eqos
cp -r eqos LEDE_DIR/package/eqos
cd LEDE_DIR
./scripts/feeds update -a
./scripts/feeds install -a
make menuconfig
LuCI --->
1. Collections --->
<*> luci
3. Applications --->
<*> luci-app-eqos...................................... EQOS - LuCI interface
4. Themes --->
<*> luci-theme-material
Network --->
-*- eqos................... Easy QoS(Support speed limit based on IP address)
make package/eqos/compile V=s
- 支持基于IP地址限速
- 支持自定义WAN设备
- 不使用iptables MARK,更高效
- 提供直观的LuCI Web界面
- 支持IPv4地址自动发现
## 系统要求
- OpenWrt 18.06 或更高版本
- LuCI Web 界面
- kmod-sched-core
- kmod-ifb
- tc (iproute2)
## 安装
### 编译安装
```bash
# 克隆到 OpenWrt SDK
git clone https://github.com/kenzok78/luci-app-eqos.git
# 放入 packages 目录
mv luci-app-eqos /path/to/openwrt/package/feeds/luci/
# 编译
make package/luci-app-eqos/compile V=s
```
### 在线安装
```bash
opkg update
opkg install luci-app-eqos
```
## 配置
1. 登录 LuCI 管理界面
2. 进入 **网络 → EQOS**
3. 启用 EQOS 并设置总带宽
4. 添加需要限速的 IP 地址及带宽
## 命令行用法
```bash
# 启动
eqos start
# 停止
eqos stop
# 重启
eqos restart
# 添加规则
eqos add 192.168.1.100 10 5
# 查看状态
eqos show
# 清除所有规则
eqos flush
```
## 目录结构
```
luci-app-eqos/
├── Makefile
├── luasrc/
│ ├── controller/eqos.lua
│ └── model/cbi/eqos.lua
├── po/
│ └── zh_Hans/eqos.po
├── root/
│ ├── etc/
│ │ ├── config/eqos
│ │ ├── init.d/eqos
│ │ └── hotplug.d/iface/10-eqos
│ └── usr/
│ ├── sbin/eqos
│ └── share/rpcd/acl.d/luci-app-eqos.json
└── README.md
```
## 工作原理
1. 使用 HTB (Hierarchical Token Bucket) 队列规则
2. 下载流量通过 ifb (Intermediate Functional Block) 设备控制
3. 上传流量直接在 WAN 接口控制
## 许可证
GPL-2.0
## 致谢
- 原始项目: [luci-app-eqos](https://github.com/garypang13/luci-app-eqos) by GaryPang
## 更新日志
### v2.0.0 (2026-03-22)
- 重构 Lua CBI 模型,修复全局变量泄漏
- 添加输入验证 (IP 地址、速度)
- 添加日志记录
- 支持自定义 WAN 设备
- 添加 flush/show 命令
- 完善中文翻译
@@ -1,32 +0,0 @@
# Easy QoS for OpenWRT/Lede
![](https://img.shields.io/badge/license-GPLV3-brightgreen.svg?style=plastic "License")
![](https://github.com/lwxlwxlwx/blob/master/eqos_zh.png)
# 特性
* 支持基于IP地址限速
* 未使用iptables打MARK
* 提供Luci界面
# 安装到OpenWRT/LEDE
git clone https://github.com/garypang13/luci-app-eqos
cp -r eqos LEDE_DIR/package/eqos
cd LEDE_DIR
./scripts/feeds update -a
./scripts/feeds install -a
make menuconfig
LuCI --->
1. Collections --->
<*> luci
3. Applications --->
<*> luci-app-eqos...................................... EQOS - LuCI interface
4. Themes --->
<*> luci-theme-material
Network --->
-*- eqos................... Easy QoS(Support speed limit based on IP address)
make package/eqos/compile V=s
@@ -1,10 +1,11 @@
module("luci.controller.eqos", package.seeall)
function index()
if not nixio.fs.access("/etc/config/eqos") then
return
end
local page
entry({"admin", "network", "eqos"}, cbi("eqos"), _("EQoS"), 121).dependent = true
if not nixio.fs.access("/etc/config/eqos") then
return
end
local page = entry({"admin", "network", "eqos"}, cbi("eqos"), _("EQOS"), 121)
page.dependent = true
page.acl_depends = { "luci-app-eqos" }
end
@@ -1,39 +1,51 @@
local ipc = require "luci.ip"
local sys = require "luci.sys"
local m = Map("eqos", translate("Quality of Service"))
local s = m:section(TypedSection, "eqos", "")
s.anonymous = true
local global_section = m:section(NamedSection, "global", "eqos", translate("Global Settings"))
global_section.addremove = false
global_section.cfgsections = {"global"}
local e = s:option(Flag, "enabled", translate("Enable"))
e.rmempty = false
local enabled = global_section:option(Flag, "enabled", translate("Enable EQOS"))
enabled.rmempty = false
local dl = s:option(Value, "download", translate("Download speed (Mbit/s)"), translate("Total bandwidth"))
dl.datatype = "and(uinteger,min(1))"
local wan_device = global_section:option(Value, "wan", translate("WAN Device"))
wan_device.datatype = "network"
wan_device.default = "wan"
local ul = s:option(Value, "upload", translate("Upload speed (Mbit/s)"), translate("Total bandwidth"))
ul.datatype = "and(uinteger,min(1))"
local dl_speed = global_section:option(Value, "download", translate("Download Speed (Mbit/s)"), translate("Total bandwidth"))
dl_speed.datatype = "and(uinteger,min(1))"
dl_speed.default = "100"
s = m:section(TypedSection, "device", translate("Speed limit based on IP address"))
s.template = "cbi/tblsection"
s.anonymous = true
s.addremove = true
s.sortable = true
local ul_speed = global_section:option(Value, "upload", translate("Upload Speed (Mbit/s)"), translate("Total bandwidth"))
ul_speed.datatype = "and(uinteger,min(1))"
ul_speed.default = "50"
local ip = s:option(Value, "ip", translate("IP address"))
local device_section = m:section(TypedSection, "device", translate("Speed Limit by IP Address"))
device_section.template = "cbi/tblsection"
device_section.anonymous = true
device_section.addremove = true
local ip_addr = device_section:option(Value, "ip", translate("IP Address"))
ip_addr.datatype = "ipaddr"
ip_addr.placeholder = "192.168.1.100"
ipc.neighbors({family = 4, dev = "br-lan"}, function(n)
if n.mac and n.dest then
ip:value(n.dest:string(), "%s (%s)" %{ n.dest:string(), n.mac })
ip_addr:value(n.dest:string(), "%s (%s)" %{ n.dest:string(), n.mac })
end
end)
dl = s:option(Value, "download", translate("Download speed (Mbit/s)"))
dl.datatype = "and(uinteger,min(1))"
local dl_limit = device_section:option(Value, "download", translate("Download (Mbit/s)"))
dl_limit.datatype = "and(uinteger,min(1))"
dl_limit.placeholder = "10"
ul = s:option(Value, "upload", translate("Upload speed (Mbit/s)"))
ul.datatype = "and(uinteger,min(1))"
local ul_limit = device_section:option(Value, "upload", translate("Upload (Mbit/s)"))
ul_limit.datatype = "and(uinteger,min(1))"
ul_limit.placeholder = "5"
comment = s:option(Value, "comment", translate("Comment"))
local comment = device_section:option(Value, "comment", translate("Comment"))
comment.placeholder = translate("Description")
return m
+37
View File
@@ -0,0 +1,37 @@
msgid ""
msgstr ""
"Project-Id-Version: LuCi Chinese Translation\n"
"Report-Msgid-Bugs-To: \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Pootle 2.0.6\n"
msgid "EQoS"
msgstr "IP限速"
msgid "Comment"
msgstr "注解"
msgid "Download speed (Mbit/s)"
msgstr "下载速度 (Mbit/s)"
msgid "Enable"
msgstr "开启"
msgid "QoS"
msgstr "QoS"
msgid "Quality of Service"
msgstr "IP限速"
msgid "Upload speed (Mbit/s)"
msgstr "上传速度 (Mbit/s)"
msgid "Speed limit based on IP address"
msgstr "IP限速"
msgid "Total bandwidth"
msgstr "总带宽"
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"Project-Id-Version: LuCi Chinese Translation\n"
"Project-Id-Version: LuCI Chinese Translation\n"
"Report-Msgid-Bugs-To: \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
@@ -9,29 +9,44 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Pootle 2.0.6\n"
msgid "EQoS"
msgstr "IP限速"
msgid "Comment"
msgstr "注解"
msgid "Download speed (Mbit/s)"
msgstr "下载速度 (Mbit/s)"
msgid "Enable"
msgstr "开启"
msgid "QoS"
msgstr "QoS"
msgid "EQOS"
msgstr "EQOS"
msgid "Quality of Service"
msgstr "IP限速"
msgid "Upload speed (Mbit/s)"
msgstr "上传速度 (Mbit/s)"
msgid "Global Settings"
msgstr "全局设置"
msgid "Speed limit based on IP address"
msgstr "IP限速"
msgid "Enable EQOS"
msgstr "启用EQOS"
msgid "WAN Device"
msgstr "WAN设备"
msgid "Download Speed (Mbit/s)"
msgstr "下载速度 (Mbit/s)"
msgid "Upload Speed (Mbit/s)"
msgstr "上传速度 (Mbit/s)"
msgid "Total bandwidth"
msgstr "总带宽"
msgid "Speed Limit by IP Address"
msgstr "按IP限速"
msgid "IP Address"
msgstr "IP地址"
msgid "Download (Mbit/s)"
msgstr "下载 (Mbit/s)"
msgid "Upload (Mbit/s)"
msgstr "上传 (Mbit/s)"
msgid "Comment"
msgstr "备注"
msgid "Description"
msgstr "描述"
@@ -1,12 +1,5 @@
# The bandwidth unit is Mbit/s
config eqos
option enabled 0
option download 50
option upload 20
# Limiting the bandwidth of a single Device
#config device
# option ip "192.168.1.100"
# option download 10
# option upload 5
# option comment "test"
config eqos 'global'
option enabled '0'
option wan 'wan'
option download '100'
option upload '50'
@@ -1,39 +1,22 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2006 OpenWrt.org
# Copyright (C) 2024 kenzok78
START=50
STOP=10
USE_PROCD=0
parse_device() {
local cfg="$1" ip download upload
config_get ip "$cfg" ip
config_get download "$cfg" download
config_get upload "$cfg" upload
eqos add $ip $download $upload
start_service() {
eqos start
}
eqos_start() {
local cfg="$1" enabled download upload
config_get_bool enabled "$cfg" enabled 0
[ $enabled -eq 0 ] && return 0
config_get download "$cfg" download
config_get upload "$cfg" upload
eqos start $download $upload
config_foreach parse_device device
stop_service() {
eqos stop
}
start() {
eqos stop
config_load eqos
config_foreach eqos_start eqos
reload_service() {
eqos restart
}
stop() {
eqos stop
}
service_triggers() {
procd_add_reload_trigger eqos
}
@@ -1,11 +0,0 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
delete ucitrack.@eqos[-1]
add ucitrack eqos
set ucitrack.@eqos[-1].init=eqos
commit ucitrack
EOF
rm -f /tmp/luci-indexcache
exit 0
+185 -51
View File
@@ -1,60 +1,194 @@
#!/bin/sh
# EQOS - Easy QoS for OpenWrt
# Copyright (C) 2024 kenzok78
# Licensed under GPL v2
dev=br-lan
LOGGER="logger -t eqos"
$LOGGER "eqos service started"
[ -f /etc/config/eqos ] || exit 1
. /lib/functions.sh
. /lib/functions/network.sh
validate_ip() {
local ip="$1"
echo "$ip" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$' || return 1
for octet in $(echo "$ip" | tr '.' ' '); do
[ "$octet" -gt 255 ] 2>/dev/null && return 1
done
return 0
}
validate_speed() {
local speed="$1"
echo "$speed" | grep -qE '^[0-9]+$' && [ "$speed" -gt 0 ] && [ "$speed" -le 10000 ]
}
get_wan_device() {
local wan_cfg="$1"
local wan_dev=$(uci get network.$wan_cfg.ifname 2>/dev/null)
if [ -z "$wan_dev" ]; then
wan_dev=$(uci get eqos.$wan_cfg.wan 2>/dev/null)
fi
echo "${wan_dev:-br-lan}"
}
stop_qos() {
tc qdisc del dev $dev root 2>/dev/null
tc qdisc del dev $dev ingress 2>/dev/null
tc qdisc del dev ${dev}-ifb root 2>/dev/null
ip link del dev ${dev}-ifb 2>/dev/null
local dev="$1"
$LOGGER "Stopping QoS on device: $dev"
tc qdisc del dev "$dev" root 2>/dev/null
tc qdisc del dev "$dev" ingress 2>/dev/null
tc qdisc del dev "${dev}-ifb" root 2>/dev/null
ip link del dev "${dev}-ifb" 2>/dev/null
return 0
}
start_qos() {
local dl=$1
local up=$2
tc qdisc add dev $dev root handle 1: htb
tc class add dev $dev parent 1: classid 1:1 htb rate ${dl}mbit
ip link add dev ${dev}-ifb name ${dev}-ifb type ifb
ip link set dev ${dev}-ifb up
tc qdisc add dev ${dev}-ifb root handle 1: htb
tc class add dev ${dev}-ifb parent 1: classid 1:1 htb rate ${up}mbit
tc qdisc add dev $dev ingress
tc filter add dev $dev parent ffff: protocol ip u32 match u32 0 0 flowid 1:1 action mirred egress redirect dev ${dev}-ifb
local dl="$1"
local up="$2"
local dev="$3"
$LOGGER "Starting QoS: DL=$dl Mbit/s UP=$up Mbit/s DEV=$dev"
validate_speed "$dl" || { $LOGGER "Invalid download speed: $dl"; return 1; }
validate_speed "$up" || { $LOGGER "Invalid upload speed: $up"; return 1; }
stop_qos "$dev"
tc qdisc add dev "$dev" root handle 1: htb 2>&1 | while read line; do $LOGGER "$line"; done
tc class add dev "$dev" parent 1: classid 1:1 htb rate "${dl}mbit" 2>&1 | while read line; do $LOGGER "$line"; done
ip link add dev "${dev}-ifb" name "${dev}-ifb" type ifb 2>/dev/null
ip link set dev "${dev}-ifb" up 2>/dev/null
tc qdisc add dev "${dev}-ifb" root handle 1: htb 2>&1 | while read line; do $LOGGER "$line"; done
tc class add dev "${dev}-ifb" parent 1: classid 1:1 htb rate "${up}mbit" 2>&1 | while read line; do $LOGGER "$line"; done
tc qdisc add dev "$dev" ingress 2>&1 | while read line; do $LOGGER "$line"; done
tc filter add dev "$dev" parent ffff: protocol ip u32 match u32 0 0 flowid 1:1 action mirred egress redirect dev "${dev}-ifb" 2>&1 | while read line; do $LOGGER "$line"; done
$LOGGER "QoS started successfully"
return 0
}
case "$1" in
"stop")
stop_qos
;;
"start")
stop_qos
start_qos $2 $3
;;
"add")
ip="$2"
dl="$3"
up="$4"
cnt=$(tc class show dev $dev | wc -l)
tc class add dev $dev parent 1:1 classid 1:1$cnt htb rate ${dl}mbit ceil ${dl}mbit
tc filter add dev $dev parent 1:0 protocol ip u32 match ip dst $ip flowid 1:1$cnt
tc class add dev ${dev}-ifb parent 1:1 classid 1:1$cnt htb rate ${up}mbit ceil ${up}mbit
tc filter add dev ${dev}-ifb parent 1:0 protocol ip u32 match ip src $ip flowid 1:1$cnt
;;
*)
echo "Usage: $0 <command> [options]"
echo "Commands:"
echo " start dl_rate up_rate #Total bandwidth (Mbit/s)"
echo " stop"
echo " add ip dl_rate up_rate #Limiting the bandwidth of a single IP (Mbit/s)"
echo "Example:"
echo " $0 start 30 20 # Total bandwidth: down 30Mbit/s up 20Mbit/s"
echo " $0 add 192.168.22.12 10 2 # down 10Mbit/s up 2Mbit/s"
;;
esac
add_device() {
local ip="$1"
local dl="$2"
local up="$3"
local dev="$4"
validate_ip "$ip" || { $LOGGER "Invalid IP address: $ip"; return 1; }
validate_speed "$dl" || { $LOGGER "Invalid download speed: $dl"; return 1; }
validate_speed "$up" || { $LOGGER "Invalid upload speed: $up"; return 1; }
local cnt=$(tc class show dev "$dev" 2>/dev/null | grep -c "class htb" || echo "0")
local classid=$((100 + cnt))
$LOGGER "Adding QoS rule: IP=$ip DL=$dl Mbit/s UP=$up Mbit/s"
tc class add dev "$dev" parent 1:1 classid "1:${classid}" htb rate "${dl}mbit" ceil "${dl}mbit" 2>/dev/null
tc filter add dev "$dev" parent 1:0 protocol ip prio 1 u32 match ip dst "$ip/32" flowid "1:${classid}" 2>/dev/null
tc class add dev "${dev}-ifb" parent 1:1 classid "1:${classid}" htb rate "${up}mbit" ceil "${up}mbit" 2>/dev/null
tc filter add dev "${dev}-ifb" parent 1:0 protocol ip prio 1 u32 match ip src "$ip/32" flowid "1:${classid}" 2>/dev/null
$LOGGER "QoS rule added for $ip"
return 0
}
flush_devices() {
local dev="$1"
$LOGGER "Flushing all device QoS rules"
local class_list=$(tc class show dev "$dev" 2>/dev/null | grep -oE '1:[0-9]+' | grep -v '^1:1$' | sort -u)
for class in $class_list; do
local filter_list=$(tc filter show dev "$dev" 2>/dev/null | grep "$class" | grep -oE 'pref [0-9]+' | awk '{print $2}')
for pref in $filter_list; do
tc filter del dev "$dev" parent 1:0 prio "$pref" 2>/dev/null
done
tc class del dev "$dev" "$class" 2>/dev/null
local filter_list_ifb=$(tc filter show dev "${dev}-ifb" 2>/dev/null | grep "$class" | grep -oE 'pref [0-9]+' | awk '{print $2}')
for pref in $filter_list_ifb; do
tc filter del dev "${dev}-ifb" parent 1:0 prio "$pref" 2>/dev/null
done
tc class del dev "${dev}-ifb" "$class" 2>/dev/null
done
$LOGGER "All device QoS rules flushed"
}
cmd="$1"
shift
case "$cmd" in
start)
config_load eqos
config_get enabled global enabled 0
[ "$enabled" = "1" ] || { $LOGGER "EQOS is disabled"; exit 0; }
config_get wan_dev global wan "wan"
config_get dl global download "100"
config_get up global upload "50"
real_wan=$(get_wan_device "$wan_dev")
start_qos "$dl" "$up" "$real_wan" || exit 1
config_foreach add_device device
;;
stop)
config_load eqos
config_get wan_dev global wan "wan"
real_wan=$(get_wan_device "$wan_dev")
stop_qos "$real_wan"
;;
restart)
config_load eqos
config_get wan_dev global wan "wan"
real_wan=$(get_wan_device "$wan_dev")
stop_qos "$real_wan"
config_get enabled global enabled 0
[ "$enabled" = "1" ] || exit 0
config_get dl global download "100"
config_get up global upload "50"
start_qos "$dl" "$up" "$real_wan"
;;
add)
local ip="$1"; local dl="$2"; local up="$3"
[ -z "$ip" ] || [ -z "$dl" ] || [ -z "$up" ] && { $LOGGER "Usage: eqos add <ip> <dl> <up>"; exit 1; }
config_load eqos
config_get wan_dev global wan "wan"
real_wan=$(get_wan_device "$wan_dev")
add_device "$ip" "$dl" "$up" "$real_wan"
;;
flush)
config_load eqos
config_get wan_dev global wan "wan"
real_wan=$(get_wan_device "$wan_dev")
flush_devices "$real_wan"
;;
show)
config_load eqos
config_get wan_dev global wan "wan"
real_wan=$(get_wan_device "$wan_dev")
echo "=== QoS Classes on $real_wan ==="
tc class show dev "$real_wan" 2>/dev/null
echo "=== QoS Filters on $real_wan ==="
tc filter show dev "$real_wan" 2>/dev/null
;;
*)
echo "Usage: $0 <command> [options]"
echo "Commands:"
echo " start - Start EQOS service"
echo " stop - Stop EQOS service"
echo " restart - Restart EQOS service"
echo " add <ip> <dl> <up> - Add IP rule (Mbit/s)"
echo " flush - Flush all device rules"
echo " show - Show QoS status"
echo ""
echo "Examples:"
echo " $0 start"
echo " $0 add 192.168.1.100 10 5"
;;
esac
@@ -1,11 +1,11 @@
{
"luci-app-eqos": {
"description": "Grant UCI access for luci-app-eqos",
"description": "Grant access to EQOS",
"read": {
"uci": [ "eqos" ]
"uci": { "eqos": ["global", "device"] }
},
"write": {
"uci": [ "eqos" ]
"uci": { "eqos": ["global", "device"] }
}
}
}
@@ -10,3 +10,4 @@ LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
$(eval $(call BuildPackage,luci-app-fileassistant))
@@ -1,24 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// NOTE: luci-app-fileassistant 使用模板视图(luasrc/view/fileassistant.htm
// 此文件在 luci 23.05+ 下通过 menu.d 的 action.type=template 加载,
// 不会直接调用本 JS view,保留此文件仅供参考。
'use strict';
'require view';
'require rpc';
var callStatus = rpc.declare({
object: 'luci',
method: 'get_status',
params: ['name'],
expect: { '': {} }
});
'require form';
return view.extend({
load: function() {},
render: function() {
var m = new form.Map('config', _('Settings'));
var s = m.section(form.NamedSection, 'main', 'main', _('Configuration'));
s.anonymous = true;
var o = s.option(form.Value, 'name', _('Name'));
return m.render();
// 实际界面由 luasrc/view/fileassistant.htm 模板提供
return E('p', {}, _('Loading…'));
}
});
@@ -13,14 +13,18 @@ local ALLOWED_PATHS = {
local MAX_UPLOAD_SIZE = 500 * 1024 * 1024
function index()
entry({"admin", "nas"}, firstchild(), _("NAS"), 44).dependent = false
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复注册菜单项
if not nixio.fs.access("/usr/share/luci/menu.d/luci-app-fileassistant.json") then
entry({"admin", "nas"}, firstchild(), _("NAS"), 44).dependent = false
local page
page = entry({"admin", "nas", "fileassistant"}, template("fileassistant"), _("文件助手"), 1)
page.i18n = "base"
page.dependent = true
page.acl_depends = { "luci-app-fileassistant" }
local page
page = entry({"admin", "nas", "fileassistant"}, template("fileassistant"), _("文件助手"), 1)
page.i18n = "base"
page.dependent = true
page.acl_depends = { "luci-app-fileassistant" }
end
-- API 路由在新旧版本均需注册(fb.js 的 AJAX 请求依赖这些路由)
entry({"admin", "nas", "fileassistant", "list"}, call("fileassistant_list"), nil)
entry({"admin", "nas", "fileassistant", "open"}, call("fileassistant_open"), nil)
entry({"admin", "nas", "fileassistant", "delete"}, call("fileassistant_delete"), nil)
@@ -67,7 +71,8 @@ function sanitize_path(path)
if not path then
return nil
end
path = path:gsub("<>", "/"):gsub("//+", "/"):gsub("/$", "")
-- Bug fix: 原代码将 "<>" 替换为 "/",应为 "\\"Windows 路径反斜杠)
path = path:gsub("\\\\", "/"):gsub("//+", "/"):gsub("/$", "")
if path == "" then
return "/"
end
@@ -81,7 +86,8 @@ function fileassistant_list()
list_response(path, false, "Path not allowed")
return
end
if not nixio.fs.stat(realpath, "type") == "dir" and not nixio.fs.stat(realpath) then
-- Bug fix: Lua 中 not 优先级高于 ==,原写法恒为 false
if not nixio.fs.stat(realpath) or nixio.fs.stat(realpath, "type") ~= "dir" then
list_response(path, false, "Invalid directory")
return
end
@@ -218,7 +224,9 @@ function fileassistant_install()
end
function installIPK(filepath)
local output = luci.sys.exec('opkg --force-depends install "' .. filepath .. '" 2>&1')
-- Security fix: 用单引号包裹路径,防止命令注入
local safe_path = filepath:gsub("'", "'\\''")
local output = luci.sys.exec("opkg --force-depends install '" .. safe_path .. "' 2>&1")
luci.sys.exec('rm -rf /tmp/luci-*')
if output:match("Installing") and output:match("completed") then
return true
@@ -233,7 +241,8 @@ function fileassistant_upload()
list_response(uploaddir, false, "Path not allowed")
return
end
if not nixio.fs.stat(realpath, "type") == "dir" then
-- Bug fix: Lua 中 not 优先级高于 ==,原写法恒为 false
if not nixio.fs.stat(realpath) or nixio.fs.stat(realpath, "type") ~= "dir" then
list_response(uploaddir, false, "Invalid directory")
return
end
@@ -0,0 +1,17 @@
{
"admin/nas": {
"title": "NAS",
"order": 44
},
"admin/nas/fileassistant": {
"title": "文件助手",
"order": 1,
"action": {
"type": "template",
"path": "fileassistant"
},
"depends": {
"acl": [ "luci-app-fileassistant" ]
}
}
}
+27 -8
View File
@@ -1,21 +1,40 @@
# Copyright (C) 2018-2024 OpenWrt luci-app-filebrowser contributors
#
# Copyright (C) 2021 ImmortalWrt
# <https://immortalwrt.org>
#
# This is free software, licensed under the GNU General Public License v3.
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-filebrowser
PKG_MAINTAINER:=kenzok8 <https://github.com/kenzok78>
PKG_VERSION:=1.0
PKG_RELEASE:=1
LUCI_TITLE:=LuCI Support for FileBrowser
LUCI_DEPENDS:=+filebrowser
LUCI_PKGARCH:=all
LUCI_DESCRIPTION:=FileBrowser - a web-based file manager for your OpenWrt
PKG_NAME:=luci-app-filebrowser
PKG_VERSION:=snapshot
PKG_RELEASE:=118071b
define Package/$(PKG_NAME)/conffiles
/etc/config/filebrowser
/etc/init.d/filebrowser
endef
PKG_LICENSE:=GPLv3
define Package/$(PKG_NAME)/postinst
#!/bin/sh
[ -z "$${IPKG_INSTROOT}" ] && /etc/init.d/filebrowser enable 2>/dev/null
rm -f /tmp/luci-indexcache
exit 0
endef
define Package/$(PKG_NAME)/prerm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
/etc/init.d/filebrowser disable 2>/dev/null
/etc/init.d/filebrowser stop 2>/dev/null
fi
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
@@ -0,0 +1,126 @@
# luci-app-filebrowser
适用于 OpenWrt/Lede 的 FileBrowser LuCI 控制面板插件。
## 功能特性
- **Web 文件管理**: 通过美观的 Web 界面管理服务器上的文件
- **多架构支持**: 自动检测 x86_64、aarch64、ramips、ar71xx、armv5/6/7/8 等架构
- **自动下载更新**: 自动从 GitHub releases 获取最新版本并下载
- **配置管理**: 支持 SSL、自定义端口、数据库路径等
- **日志查看**: 实时查看 FileBrowser 运行日志
## 依赖
- `luci`
- `luci-base`
- `filebrowser` (主程序)
## 安装
### OpenWrt Feed 方式
```bash
# 添加 feed
echo 'src-git filebrowser https://github.com/kenzok78/luci-app-filebrowser' >> feeds.conf.default
./scripts/feeds update -a
./scripts/feeds install -a -p filebrowser
# 编译
make package/luci-app-filebrowser/compile V=s
```
### 直接编译
```bash
git clone https://github.com/kenzok78/luci-app-filebrowser.git package/luci-app-filebrowser
make menuconfig # 选择 LuCI -> Applications -> luci-app-filebrowser
make -j$(nproc) V=s
```
## 使用说明
### 基本配置
1. 访问 OpenWrt Web 管理界面 → 服务 → File Browser
2. 启用插件
3. 设置可执行文件存放目录(建议使用 `/tmp` 或挂载的 USB 存储)
4. 点击"手动下载"获取 FileBrowser 二进制文件
5. 保存并应用配置
### 配置说明
| 选项 | 说明 | 默认值 |
|------|------|--------|
| 监听地址 | 绑定地址 | 0.0.0.0 |
| 监听端口 | Web 界面端口 | 8088 |
| 数据库路径 | SQLite 数据库文件位置 | /etc/filebrowser.db |
| 初始账户 | 登录用户名 | admin |
| 初始密码 | 登录密码 | admin |
| SSL 证书 | HTTPS 证书路径 | 空 |
| SSL 私钥 | HTTPS 私钥路径 | 空 |
| 根目录 | 文件浏览的起始目录 | /root |
| 可执行文件目录 | 二进制文件存放位置 | /tmp |
## 配置示例
### /etc/config/filebrowser
```
config global
option address '0.0.0.0'
option port '8088'
option database '/etc/filebrowser.db'
option username 'admin'
option password 'admin'
option ssl_cert ''
option ssl_key ''
option root_path '/root'
option executable_directory '/tmp'
option enable '1'
```
## 目录结构
```
luci-app-filebrowser/
├── luasrc/
│ ├── controller/ # LuCI 控制器
│ ├── model/cbi/ # CBI 模型
│ └── view/ # HTML 视图模板
├── po/ # 翻译文件
├── root/
│ ├── etc/
│ │ ├── config/ # UCI 配置
│ │ └── init.d/ # 启动脚本
│ └── usr/share/rpcd/ # ACL 配置
├── Makefile
└── README.md
```
## 常见问题
### Q: 下载失败怎么办?
检查网络连接,确保可以访问 GitHub。也可以手动下载 FileBrowser 二进制文件并放置到指定目录。
### Q: 如何手动下载二进制文件?
访问 [FileBrowser Releases](https://github.com/filebrowser/filebrowser/releases) 下载对应架构的版本,解压后放置到可执行文件目录。
### Q: 如何查看日志?
通过 LuCI 界面的日志区域查看,或:
```bash
cat /var/log/filebrowser.log
```
## 许可证
Apache-2.0
## 来源
- [FileBrowser](https://github.com/filebrowser/filebrowser)
- 基于 Lienol 的 luci-app-filebrowser 维护
@@ -0,0 +1,43 @@
'use strict';
'require view';
'require rpc';
'require uci';
'require form';
var callStatus = rpc.declare({
object: 'luci.filebrowser',
method: 'status',
expect: { '': {} }
});
return view.extend({
load: function() {
return uci.load('filebrowser');
},
render: function() {
var m = new form.Map('filebrowser', _('File Browser'), _('FileBrowser - a web-based file manager'));
var s = m.section(form.NamedSection, 'global', 'global', _('Running Status'));
s.anonymous = true;
var o = s.option(form.Value, 'port', _('Port'));
o.optional = true;
o.default = '8088';
var status = s.option(form.DummyValue, '_status', _('Status'));
status.load = function() {
return callStatus().then(function(res) {
this._running = res.status;
}.bind(this));
};
status.render = function() {
var running = this._running;
var cls = running ? 'green' : 'red';
var text = running ? _('RUNNING') : _('NOT RUNNING');
return E('p', {}, E('span', { style: 'color:' + cls + ';' }, text));
};
return m.render();
}
});
@@ -1,19 +1,78 @@
module("luci.controller.filebrowser", package.seeall)
function index()
if not nixio.fs.access("/etc/config/filebrowser") then
return
end
entry({"admin", "nas"}, firstchild(), _("NAS") , 45).dependent = false
local page
page = entry({"admin", "nas", "filebrowser"}, cbi("filebrowser"), _("文件管理器"), 100)
page.dependent = true
entry({"admin","nas","filebrowser","status"},call("act_status")).leaf=true
end
function act_status()
local e={}
e.running=luci.sys.call("pgrep filebrowser >/dev/null")==0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
module("luci.controller.filebrowser", package.seeall)
local http = require "luci.http"
local fs = require "nixio.fs"
local api = require "luci.model.cbi.filebrowser.api"
function index()
if not fs.access("/etc/config/filebrowser") then return end
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复注册
if fs.access("/usr/share/luci/menu.d/luci-app-filebrowser.json") then return end
entry({"admin", "services"}, firstchild(), "Services", 44).dependent = false
entry({"admin", "services", "filebrowser"}, cbi("filebrowser/settings"),
_("File Browser"), 2).dependent = true
entry({"admin", "services", "filebrowser", "check"}, call("action_check")).leaf = true
entry({"admin", "services", "filebrowser", "download"}, call("action_download")).leaf = true
entry({"admin", "services", "filebrowser", "status"}, call("act_status")).leaf = true
entry({"admin", "services", "filebrowser", "get_log"}, call("get_log")).leaf = true
entry({"admin", "services", "filebrowser", "clear_log"}, call("clear_log")).leaf = true
end
local function http_write_json(content)
http.prepare_content("application/json")
http.write_json(content or {code = 1})
end
function act_status()
local result = {}
local uci = require "luci.model.uci".cursor()
local exec_dir = uci:get("filebrowser", "@global[0]", "executable_directory") or "/tmp"
local fb_bin = exec_dir .. "/filebrowser"
if fs.access(fb_bin) then
result.status = (luci.sys.call("pgrep -f '" .. fb_bin .. "' >/dev/null 2>&1") == 0)
else
result.status = false
end
http_write_json(result)
end
function action_check()
local json = api.to_check()
http_write_json(json)
end
function action_download()
local task = http.formvalue("task")
local json
if task == "extract" then
json = api.to_extract(http.formvalue("file"))
elseif task == "move" then
json = api.to_move(http.formvalue("file"))
else
json = api.to_download(http.formvalue("url"))
end
http_write_json(json)
end
function get_log()
http.prepare_content("text/plain; charset=utf-8")
local log_path = "/var/log/filebrowser.log"
if fs.access(log_path) then
http.write(fs.readfile(log_path) or "")
else
http.write("")
end
end
function clear_log()
local log_path = "/var/log/filebrowser.log"
fs.writefile(log_path, "")
http.prepare_content("application/json")
http.write_json({code = 0})
end
@@ -1,42 +0,0 @@
m = Map("filebrowser", translate("文件管理器"), translate("FileBrowser是一个基于Go的在线文件管理器,助您方便的管理设备上的文件。"))
m:section(SimpleSection).template = "filebrowser/filebrowser_status"
s = m:section(TypedSection, "filebrowser")
s.addremove = false
s.anonymous = true
enable = s:option(Flag, "enabled", translate("启用"))
enable.rmempty = false
o = s:option(ListValue, "addr_type", translate("监听地址"))
o:value("local", translate("监听本机地址"))
o:value("lan", translate("监听局域网地址"))
o:value("wan", translate("监听全部地址"))
o.default = "lan"
o.rmempty = false
o = s:option(Value, "port", translate("监听端口"))
o.placeholder = 8989
o.default = 8989
o.datatype = "port"
o.rmempty = false
o = s:option(Value, "root_dir", translate("开放目录"))
o.placeholder = "/"
o.default = "/"
o.rmempty = false
o = s:option(Value, "db_dir", translate("数据库目录"))
o.placeholder = "/etc"
o.default = "/etc"
o.rmempty = false
o.description = translate("普通用户请勿随意更改")
o = s:option(Value, "db_name", translate("数据库名"))
o.placeholder = "filebrowser.db"
o.default = "filebrowser.db"
o.rmempty = false
o.description = translate("普通用户请勿随意更改")
return m
@@ -0,0 +1,383 @@
local fs = require "nixio.fs"
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local util = require "luci.util"
local i18n = require "luci.i18n"
module("luci.model.cbi.filebrowser.api", package.seeall)
local appname = "filebrowser"
local api_url = "https://api.github.com/repos/filebrowser/filebrowser/releases/latest"
local function get_downloader()
if fs.access("/usr/bin/curl") or fs.access("/bin/curl") then
return "/usr/bin/curl", {
"-L", "-k", "--retry", "2", "--connect-timeout", "10", "-o"
}
end
if fs.access("/usr/bin/wget") then
return "/usr/bin/wget", {
"--no-check-certificate", "--quiet", "--timeout=10", "--tries=2", "-O"
}
end
return nil, {}
end
local command_timeout = 300
local lede_board = nil
local distrib_target = nil
local function uci_get_type(t, config, default)
local value
value = uci:get_first(appname, t, config)
if not value or value == "" then
value = sys.exec("uci -q get " .. appname .. ".@" .. t .. "[0]." .. config)
end
if (value == nil or value == "") and default and default ~= "" then
value = default
end
return value
end
local function exec(cmd, args, writer, timeout)
local os = require "os"
local nixio = require "nixio"
local fdi, fdo = nixio.pipe()
local pid = nixio.fork()
if pid > 0 then
fdo:close()
if writer or timeout then
local starttime = os.time()
while true do
if timeout and os.difftime(os.time(), starttime) >= timeout then
nixio.kill(pid, nixio.const.SIGTERM)
return 1
end
if writer then
local buffer = fdi:read(2048)
if buffer and #buffer > 0 then
writer(buffer)
end
end
local wpid, stat, code = nixio.waitpid(pid, "nohang")
if wpid and stat == "exited" then return code end
if not writer and timeout then nixio.nanosleep(1) end
end
else
local wpid, stat, code = nixio.waitpid(pid)
return wpid and stat == "exited" and code
end
elseif pid == 0 then
nixio.dup(fdo, nixio.stdout)
fdi:close()
fdo:close()
nixio.exece(cmd, args, nil)
nixio.stdout:close()
os.exit(1)
end
end
local function compare_versions(ver1, comp, ver2)
local av1 = util.split(ver1, "[%.%-]", nil, true)
local av2 = util.split(ver2, "[%.%-]", nil, true)
local max = #av1
local n2 = #av2
if max < n2 then max = n2 end
for i = 1, max do
local s1 = av1[i] or ""
local s2 = av2[i] or ""
if comp == "~=" and (s1 ~= s2) then return true end
if (comp == "<" or comp == "<=") and (s1 < s2) then return true end
if (comp == ">" or comp == ">=") and (s1 > s2) then return true end
if (s1 ~= s2) then return false end
end
return not (comp == "<" or comp == ">")
end
local function auto_get_arch()
local nixio = require "nixio"
local arch = nixio.uname().machine or ""
if fs.access("/etc/openwrt_release") then
local target = sys.exec("grep 'DISTRIB_TARGET=' /etc/openwrt_release 2>/dev/null | cut -d\"'\" -f2 | head -1")
distrib_target = target
end
if fs.access("/usr/lib/os-release") then
local board = sys.exec("grep 'OPENWRT_BOARD=' /usr/lib/os-release 2>/dev/null | cut -d\"'\" -f2 | head -1")
if board and board ~= "" then
lede_board = board
end
end
if arch == "mips" then
local target_str = distrib_target or lede_board or ""
if target_str:match("ramips") then
arch = "ramips"
elseif target_str:match("ar71xx") then
arch = "ar71xx"
end
end
return util.trim(arch)
end
local function get_file_info(arch)
local file_tree = ""
local sub_version = ""
if arch == "x86_64" then
file_tree = "amd64"
elseif arch == "aarch64" then
file_tree = "arm64"
elseif arch == "ramips" then
file_tree = "mipsle"
elseif arch == "ar71xx" then
file_tree = "mips"
elseif arch:match("^i[%d]86$") then
file_tree = "386"
elseif arch:match("^armv[5-8]") then
file_tree = "armv"
sub_version = arch:match("armv([5-8])")
local target_str = lede_board or distrib_target or ""
if target_str:match("bcm53xx") then
sub_version = "5"
end
end
return file_tree, sub_version
end
local function get_api_json(url)
local jsonc = require "luci.jsonc"
local downloader, args = get_downloader()
if not downloader then
return {}
end
local tmpfile = "/tmp/filebrowser_api_json"
if downloader:match("curl") then
local ret = sys.call(downloader .. " -L -k --connect-timeout 10 -s -o " .. tmpfile .. " " .. url)
if ret ~= 0 then
return {}
end
else
local ret = sys.call(downloader .. " --no-check-certificate --timeout=10 -t 2 -O " .. tmpfile .. " " .. url)
if ret ~= 0 then
return {}
end
end
local content = fs.readfile(tmpfile) or ""
fs.remove(tmpfile)
if content == "" then return {} end
local json, err = jsonc.parse(content)
return json or {}
end
function get_version()
return uci_get_type("global", "version", "0")
end
function to_check(arch)
if not arch or arch == "" then
arch = auto_get_arch()
end
local file_tree, sub_version = get_file_info(arch)
if file_tree == "" then
return {
code = 1,
error = i18n.translate("Can't determine ARCH, or ARCH not supported.")
}
end
local json = get_api_json(api_url)
if not json or json.tag_name == nil then
return {
code = 1,
error = i18n.translate("Get remote version info failed.")
}
end
local remote_version = json.tag_name:match("[^v]+")
local needs_update = compare_versions(get_version(), "<", remote_version)
local html_url, download_url
if needs_update then
html_url = json.html_url
for _, v in ipairs(json.assets or {}) do
if v.name then
local pattern = "linux%-" .. file_tree
if sub_version and sub_version ~= "" then
pattern = pattern .. sub_version .. "$"
end
if v.name:match(pattern) then
download_url = v.browser_download_url
break
end
end
end
end
if needs_update and not download_url then
return {
code = 1,
version = remote_version,
html_url = html_url,
error = i18n.translate("New version found, but failed to get new version download url.")
}
end
return {
code = 0,
update = needs_update,
version = remote_version,
url = {html = html_url, download = download_url}
}
end
function to_download(url)
if not url or url == "" then
return {code = 1, error = i18n.translate("Download url is required.")}
end
sys.call("rm -f /tmp/filebrowser_download.*")
local tmp_file = sys.exec("mktemp -u -t filebrowser_download.XXXXXX 2>/dev/null")
if not tmp_file or tmp_file == "" then
return {code = 1, error = i18n.translate("Failed to create temp file.")}
end
tmp_file = util.trim(tmp_file)
local downloader, args = get_downloader()
if not downloader then
return {code = 1, error = i18n.translate("No downloader available (curl or wget).")}
end
local outfile = "/tmp/filebrowser_download.bin"
local cmd
if downloader:match("curl") then
cmd = downloader .. " -L -k --connect-timeout 10 -o " .. outfile .. " " .. url .. " 2>/dev/null"
else
cmd = downloader .. " --no-check-certificate --timeout=10 -t 2 -O " .. outfile .. " " .. url .. " 2>/dev/null"
end
local ret = sys.call(cmd)
if ret ~= 0 or not fs.access(outfile) then
sys.call("rm -f " .. outfile)
return {
code = 1,
error = i18n.translatef("File download failed or timed out: %s", url)
}
end
return {code = 0, file = outfile}
end
function to_extract(file)
if not file or file == "" or not fs.access(file) then
return {code = 1, error = i18n.translate("File path required.")}
end
sys.call("rm -rf /tmp/filebrowser_extract.*")
local tmp_dir = sys.exec("mktemp -d -t filebrowser_extract.XXXXXX 2>/dev/null")
if not tmp_dir or tmp_dir == "" then
return {code = 1, error = i18n.translate("Failed to create temp directory.")}
end
tmp_dir = util.trim(tmp_dir)
local output = {}
exec("/bin/tar", {"-C", tmp_dir, "-zxvf", file},
function(chunk)
if chunk then
output[#output + 1] = chunk
end
end)
local files = util.split(table.concat(output))
exec("/bin/rm", {"-f", file})
local new_file
for _, f in pairs(files) do
if f and f:match("filebrowser") then
local candidate = tmp_dir .. "/" .. util.trim(f)
if fs.access(candidate) then
new_file = candidate
break
end
end
end
if not new_file then
exec("/bin/rm", {"-rf", tmp_dir})
return {
code = 1,
error = i18n.translatef("Can't find client in file: %s", file)
}
end
return {code = 0, file = new_file}
end
function to_move(file)
if not file or file == "" or not fs.access(file) then
sys.call("rm -rf /tmp/filebrowser_extract.*")
return {code = 1, error = i18n.translate("Client file is required.")}
end
local executable_directory = uci_get_type("global", "executable_directory", "/tmp")
if not fs.access(executable_directory) then
fs.mkdir(executable_directory)
end
local client_path = executable_directory .. "/filebrowser"
local client_path_bak
if fs.access(client_path) then
client_path_bak = "/tmp/filebrowser.bak"
exec("/bin/mv", {"-f", client_path, client_path_bak})
end
local result = exec("/bin/mv", {"-f", file, client_path}, nil, command_timeout) == 0
if not result or not fs.access(client_path) then
if client_path_bak and fs.access(client_path_bak) then
exec("/bin/mv", {"-f", client_path_bak, client_path})
end
return {
code = 1,
error = i18n.translatef("Can't move new file to path: %s", client_path)
}
end
exec("/bin/chmod", {"755", client_path})
if client_path_bak then
exec("/bin/rm", {"-f", client_path_bak})
end
sys.call("rm -rf /tmp/filebrowser_extract.*")
return {code = 0}
end
@@ -0,0 +1,59 @@
m = Map("filebrowser",
translate("FileBrowser"),
translate("File explorer is software that creates your own cloud that you can install on a server, point it to a path, and then access your files through a beautiful web interface. You have many features available!"))
m:append(Template("filebrowser/status"))
s = m:section(TypedSection, "global", translate("Global Settings"))
s.anonymous = true
s.addremove = false
o = s:option(Flag, "enable", translate("Enable"))
o.rmempty = false
o = s:option(Value, "address", translate("Listen address"))
o.default = "0.0.0.0"
o.rmempty = false
o = s:option(Value, "port", translate("Listen port"))
o.datatype = "port"
o.default = "8088"
o.rmempty = false
o = s:option(Value, "database", translate("Database path"))
o.default = "/etc/filebrowser.db"
o.rmempty = false
o = s:option(Value, "username", translate("Initial username"))
o.default = "admin"
o.rmempty = false
o = s:option(Value, "password", translate("Initial password"))
o.default = "admin"
o.rmempty = false
o = s:option(Value, "ssl_cert", translate("SSL cert"))
o.default = ""
o = s:option(Value, "ssl_key", translate("SSL key"))
o.default = ""
o = s:option(Value, "root_path", translate("Root path"),
translate("Point to a path to access your files in the web interface, default is /root"))
o.default = "/root"
o.rmempty = false
o = s:option(Value, "executable_directory", translate("Executable directory"),
translate("The file size is large, requiring at least 32M space. It is recommended to insert a usb flash drive or hard disk, or use it in the tmp directory.<br />For example: /mnt/sda1<br />For example: /tmp"))
o.default = "/tmp"
o.rmempty = false
o = s:option(Button, "_download", translate("Manually download"),
translate("Make sure you have enough space.<br /><font style='color:red'>Be sure to fill out the executable storage directory the first time you run it, and then save the application. Then manually download, otherwise can not use!</font>"))
o.template = "filebrowser/download"
o.inputstyle = "apply"
o.id = "download_btn"
m:append(Template("filebrowser/log"))
return m
@@ -0,0 +1,171 @@
<%
local dsp = require "luci.dispatcher"
-%>
<script type="text/javascript">//<![CDATA[
(function() {
var msgInfo = null;
var tokenStr = '<%=token%>';
var clickToDownloadText = '<%:Click to download%>';
var inProgressText = '<%:Downloading...%>';
var downloadInProgressNotice = '<%:Download, are you sure to close?%>';
var downloadSuccessText = '<%:Download successful%>';
var unexpectedErrorText = '<%:Unexpected error%>';
function addPageNotice() {
window.onbeforeunload = function(e) {
e.returnValue = downloadInProgressNotice;
return downloadInProgressNotice;
};
}
function removePageNotice() {
window.onbeforeunload = null;
}
function onUpdateSuccess(btn) {
alert(downloadSuccessText);
if (btn) {
btn.value = downloadSuccessText;
btn.placeholder = downloadSuccessText;
btn.disabled = true;
}
window.setTimeout(function() {
window.location.reload();
}, 1000);
}
function onRequestError(btn, errorMessage) {
if (!btn) return;
btn.disabled = false;
btn.value = btn.placeholder || clickToDownloadText;
if (errorMessage) {
alert(errorMessage);
}
}
function doAjaxGet(url, data, onResult) {
new XHR().get(url, data, function(_, json) {
var resultJson = json || {
code: 1,
error: unexpectedErrorText
};
if (typeof onResult === 'function') {
onResult(resultJson);
}
});
}
function checkUpdate(btn) {
if (!btn) return;
btn.disabled = true;
btn.value = inProgressText;
addPageNotice();
var detailEl = document.getElementById((btn.id || btn.name) + '-detail');
doAjaxGet('<%=dsp.build_url("admin/services/filebrowser/check")%>/', {
token: tokenStr
}, function(json) {
removePageNotice();
if (json.code) {
msgInfo = null;
onRequestError(btn, json.error);
} else {
msgInfo = json;
btn.disabled = false;
btn.value = clickToDownloadText;
btn.placeholder = clickToDownloadText;
}
if (detailEl && json.version) {
var urlNode = '<em style="color:red;"><%:The latest version:%>' + json.version + '</em>';
if (json.url && json.url.html) {
urlNode = '<a href="' + json.url.html + '" target="_blank">' + urlNode + '</a>';
}
detailEl.innerHTML = urlNode;
}
});
}
function doDownload(btn) {
if (!btn) return;
btn.disabled = true;
btn.value = '<%:Downloading...%>';
addPageNotice();
var updateUrl = '<%=dsp.build_url("admin/services/filebrowser/download")%>';
doAjaxGet(updateUrl, {
token: tokenStr,
url: msgInfo && msgInfo.url ? msgInfo.url.download : ''
}, function(json) {
if (json.code) {
removePageNotice();
onRequestError(btn, json.error);
return;
}
btn.value = '<%:Unpacking...%>';
doAjaxGet(updateUrl, {
token: tokenStr,
task: 'extract',
file: json.file
}, function(json) {
if (json.code) {
removePageNotice();
onRequestError(btn, json.error);
return;
}
btn.value = '<%:Moving...%>';
doAjaxGet(updateUrl, {
token: tokenStr,
task: 'move',
file: json.file
}, function(json) {
removePageNotice();
if (json.code) {
onRequestError(btn, json.error);
} else {
onUpdateSuccess(btn);
}
});
});
});
}
window.downloadClick = function(btn) {
if (msgInfo === null || msgInfo === undefined) {
checkUpdate(btn);
} else {
doDownload(btn);
}
};
})();
//]]>
</script>
<%+cbi/valueheader%>
<% if self:cfgvalue(section) ~= false then %>
<input class="cbi-button cbi-input-<%=self.inputstyle or "button" %>" type="button"<%=
attr("name", cbid) ..
attr("id", self.id or cbid) ..
attr("value", self.inputtitle or self.title) ..
ifattr(self.placeholder, "placeholder")
%> />
<span id="<%=self.id or cbid%>-detail"></span>
<% else %>
-
<% end %>
<%+cbi/valuefooter%>
@@ -1,32 +0,0 @@
<script type="text/javascript">//<![CDATA[
XHR.poll(1, '<%=url([[admin]], [[nas]], [[filebrowser]], [[status]])%>', null,
function(x, data) {
var tb = document.getElementById('filebrowser_status');
if (data && tb) {
if (data.running) {
var links = '<font color=green>Filebrowser <%:运行中%></font><input class="cbi-button mar-10" type="button" value="<%:打开管理界面%>" onclick="openClient();" />';
tb.innerHTML = links;
} else {
tb.innerHTML = '<font color=red>Filebrowser <%:未运行%></font>';
}
}
}
);
function openClient() {
var curWwwPath = window.document.location.href;
var pathName = window.document.location.pathname;
var pos = curWwwPath.indexOf(pathName);
var localhostPath = curWwwPath.substring(0, pos);
var clientPort = window.document.getElementById("cbid.filebrowser.config.port").value
var url = localhostPath + ":" + clientPort;
window.open(url)
};
//]]>
</script>
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
<fieldset class="cbi-section">
<p id="filebrowser_status">
<em><%:Collecting data...%></em>
</p>
</fieldset>
@@ -0,0 +1,31 @@
<script type="text/javascript">
//<![CDATA[
function clear_log(btn) {
XHR.get('<%=url([[admin]], [[services]], [[filebrowser]], [[clear_log]])%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = "";
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
}
XHR.poll(3, '<%=url([[admin]], [[services]], [[filebrowser]], [[get_log]])%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = x.responseText;
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
//]]>
</script>
<fieldset class="cbi-section" id="_log_fieldset">
<legend>
<%:Logs%>
</legend>
<input class="cbi-button cbi-input-remove" type="button" onclick="clear_log()" value="<%:Clear logs%>" style="margin-left: 10px;">
<textarea id="log_textarea" class="cbi-input-textarea" style="width: calc(100% - 20px); margin: 10px;" data-update="change" rows="5" wrap="off" readonly="readonly"></textarea>
</fieldset>
@@ -0,0 +1,46 @@
<%
local dsp = require "luci.dispatcher"
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local exec_dir = uci:get("filebrowser", "@global[0]", "executable_directory") or "/tmp"
local fb_bin = exec_dir .. "/filebrowser"
-%>
<fieldset class="cbi-section">
<legend><%:Running Status%></legend>
<fieldset class="cbi-section">
<div class="cbi-value">
<label class="cbi-value-title"><%:Status%></label>
<div class="cbi-value-field" id="_status"><p><span><%:Collecting data...%></span></p></div>
</div>
</fieldset>
</fieldset>
<script type="text/javascript">//<![CDATA[
(function() {
var statusEl = document.getElementById('_status');
XHR.poll(3, '<%=dsp.build_url("admin/services/filebrowser/status")%>', null,
function(x, json) {
if (x && x.status == 200 && statusEl) {
if (json.status) {
statusEl.innerHTML = '<p><span style="color:green;"><%:RUNNING%></span> <input type="button" class="cbi-button cbi-input-apply" value="<%:Enter interface%>" onclick="openwebui()" /></p>';
} else {
statusEl.innerHTML = '<p><span style="color:red;"><%:NOT RUNNING%></span></p>';
}
}
});
window.openwebui = function() {
var url = window.location.hostname;
var port = '<%=uci:get("filebrowser", "@global[0]", "port") or "8088"%>';
var sslCert = '<%=uci:get("filebrowser", "@global[0]", "ssl_cert") or ""%>';
var sslKey = '<%=uci:get("filebrowser", "@global[0]", "ssl_key") or ""%>';
var protocol = 'http';
if (sslCert !== '' && sslKey !== '') {
protocol = 'https';
}
window.open(protocol + '://' + url + ':' + port, '_blank');
};
})();
//]]></script>
+1
View File
@@ -0,0 +1 @@
zh_Hans
@@ -0,0 +1,125 @@
msgid "File Browser"
msgstr "文件浏览器"
msgid "File explorer is software that creates your own cloud that you can install on a server, point it to a path, and then access your files through a beautiful web interface. You have many features available!"
msgstr "文件浏览器是一种创建你自己的云的软件,你可以在服务器上安装它,将它指向一个路径,然后通过一个漂亮的web界面访问你的文件。您有许多可用的特性!"
msgid "RUNNING"
msgstr "运行中"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "Enter interface"
msgstr "进入界面"
msgid "Global Settings"
msgstr "全局设置"
msgid "Enable"
msgstr "启用"
msgid "Listen address"
msgstr "监听地址"
msgid "Listen port"
msgstr "监听端口"
msgid "Initial username"
msgstr "初始账户"
msgid "Initial password"
msgstr "初始密码"
msgid "SSL cert"
msgstr "SSL 证书"
msgid "SSL key"
msgstr "SSL 私钥"
msgid "Database path"
msgstr "数据库路径"
msgid "Root path"
msgstr "指向路径"
msgid "Point to a path to access your files in the web interface, default is /root"
msgstr "指向一个路径,可在web界面访问你的文件,默认为 /root"
msgid "Executable directory"
msgstr "可执行文件存放目录"
msgid "The file size is large, requiring at least 32M space. It is recommended to insert a usb flash drive or hard disk, or use it in the tmp directory<br />For example, /mnt/sda1<br />For example, /tmp"
msgstr "文件较大,至少需要32M空间。建议插入U盘或硬盘,或放入tmp目录里使用<br />例如:/mnt/sda1<br />例如:/tmp"
msgid "Manually download"
msgstr "手动下载"
msgid "Make sure you have enough space. <br /><font style='color:red'>Be sure to fill out the executable storage directory the first time you run it, and then save the application. Then manually download, otherwise can not use!</font>"
msgstr "请确保具有足够的空间。<br /><font style='color:red'>第一次运行务必填好项目存放目录,然后保存应用。再手动下载,否则无法使用!</font>"
msgid "Logs"
msgstr "日志"
msgid "Clear logs"
msgstr "清空日志"
msgid "It is the latest version"
msgstr "已是最新版本"
msgid "Download successful"
msgstr "下载成功"
msgid "Click to download"
msgstr "点击下载"
msgid "Updating..."
msgstr "更新中"
msgid "Unexpected error"
msgstr "意外错误"
msgid "Download, are you sure to close?"
msgstr "正在下载,你确认要关闭吗?"
msgid "Downloading..."
msgstr "下载中"
msgid "Unpacking..."
msgstr "解压中"
msgid "Moving..."
msgstr "移动中"
msgid "The latest version:"
msgstr "最新版本:"
msgid "Can't determine ARCH, or ARCH not supported."
msgstr "无法确认ARCH架构,或是不支持。"
msgid "Get remote version info failed."
msgstr "获取远程版本信息失败。"
msgid "New version found, but failed to get new version download url."
msgstr "发现新版本,但未能获得新版本的下载地址。"
msgid "Download url is required."
msgstr "请指定下载地址。"
msgid "File download failed or timed out: %s"
msgstr "文件下载失败或超时:%s"
msgid "File path required."
msgstr "请指定文件路径。"
msgid "Can't find client in file: %s"
msgstr "无法在文件中找到客户端:%s"
msgid "Client file is required."
msgstr "请指定客户端文件。"
msgid "The client file is not suitable for current device."
msgstr "客户端文件不适合当前设备。"
msgid "Can't move new file to path: %s"
msgstr "无法移动新文件到:%s"
@@ -0,0 +1,13 @@
config global
option address '0.0.0.0'
option port '8088'
option database '/etc/filebrowser.db'
option username 'admin'
option password 'admin'
option ssl_cert ''
option ssl_key ''
option root_path '/root'
option executable_directory '/tmp'
option enable '0'
@@ -0,0 +1,107 @@
#!/bin/sh /etc/rc.common
START=99
USE_PROCD=1
LOG_PATH="/var/log/filebrowser.log"
echolog() {
echo -e "$(date "+%Y-%m-%d %H:%M:%S") $1" >> $LOG_PATH
}
config_t_get() {
local index=0
[ -n "$4" ] && index="$4"
local ret
ret="$(uci get "filebrowser.@$1[$index].$2" 2>/dev/null)"
[ -z "$ret" ] && ret="$3"
echo "$ret"
}
start_service() {
local enabled
enabled="$(config_t_get global enable 0)"
[ "$enabled" = "0" ] && return
local address
address="$(config_t_get global address 0.0.0.0)"
local port
port="$(config_t_get global port 8088)"
local database
database="$(config_t_get global database /etc/filebrowser.db)"
local username
username="$(config_t_get global username admin)"
local password
password="$(config_t_get global password admin)"
local ssl_cert
ssl_cert="$(config_t_get global ssl_cert '')"
local ssl_key
ssl_key="$(config_t_get global ssl_key '')"
local root_path
root_path="$(config_t_get global root_path /root)"
local executable_directory
executable_directory="$(config_t_get global executable_directory /tmp)"
[ ! -f "$executable_directory/filebrowser" ] && {
echolog "$executable_directory/filebrowser not found, please download first"
return 1
}
local ssl_params=""
if [ -n "$ssl_cert" ] && [ -n "$ssl_key" ]; then
ssl_params="--ssl-cert $ssl_cert --ssl-key $ssl_key"
fi
local hash_pass
hash_pass="$("$executable_directory/filebrowser" hash "$password" 2>/dev/null)"
mkdir -p "$(dirname "$LOG_PATH")"
"$executable_directory/filebrowser" \
-a "$address" \
-p "$port" \
-r "$root_path" \
-d "$database" \
--username "$username" \
--password "$hash_pass" \
$ssl_params \
-l "$LOG_PATH" >/dev/null 2>&1 &
}
stop_service() {
local executable_directory
executable_directory="$(config_t_get global executable_directory /tmp)"
local fb_bin="${executable_directory}/filebrowser"
local pids
pids="$(pgrep -f "$fb_bin" 2>/dev/null)"
if [ -n "$pids" ]; then
for pid in $pids; do
kill -TERM "$pid" 2>/dev/null
done
sleep 2
pids="$(pgrep -f "$fb_bin" 2>/dev/null)"
[ -n "$pids" ] && kill -KILL $pids 2>/dev/null
fi
rm -f "$LOG_PATH"
}
reload_service() {
stop_service
sleep 1
start_service
}
start() {
start_service
}
stop() {
stop_service
}
restart() {
stop_service
sleep 1
start_service
}
@@ -1,11 +1,11 @@
#!/bin/sh
uci -q batch <<-EOF >/dev/null
delete ucitrack.@filebrowser[-1]
add ucitrack filebrowser
set ucitrack.@filebrowser[-1].init=filebrowser
commit ucitrack
EOF
rm -f /tmp/luci-indexcache
exit 0
#!/bin/sh
uci -q batch <<-EOF >/dev/null
delete ucitrack.@filebrowser[-1]
add ucitrack filebrowser
set ucitrack.@filebrowser[-1].init=filebrowser
commit ucitrack
EOF
rm -f /tmp/luci-indexcache
exit 0
@@ -0,0 +1,10 @@
{
"admin/services/filebrowser": {
"title": "File Browser",
"order": 2,
"action": {
"type": "view",
"path": "filebrowser/status"
}
}
}
@@ -0,0 +1,17 @@
{
"luci-app-filebrowser": {
"description": "Grant UCI access for luci-app-filebrowser",
"read": {
"ubus": {
"luci.filebrowser": [ "status" ]
},
"uci": [ "filebrowser" ],
"file": {
"/var/log/filebrowser.log": [ "read" ]
}
},
"write": {
"uci": [ "filebrowser" ]
}
}
}
@@ -0,0 +1,34 @@
'use strict';
import { cursor } from 'uci';
import { popen, access } from 'fs';
function find_binary() {
const uci = cursor();
const exec_dir = uci.get('filebrowser', '@global[0]', 'executable_directory') || '/tmp';
const fb_bin = exec_dir + '/filebrowser';
return access(fb_bin) ? fb_bin : null;
}
return {
status: {
call: function() {
const fb_bin = find_binary();
if (!fb_bin) return { status: false, version: '' };
const f = popen(`pgrep -f '${replace(fb_bin, "'", "'\\''")}' >/dev/null 2>&1; echo $?`);
if (!f) return { status: false };
const code = trim(f.read('all'));
f.close();
let version = '';
const vf = popen(`"${fb_bin}" version 2>/dev/null | head -1`);
if (vf) {
version = trim(vf.read('all'));
vf.close();
}
return { status: (code === '0'), version: version };
}
}
};
+4 -12
View File
@@ -1,21 +1,13 @@
# Copyright (C) 2020 Openwrt.org
# SPDX-License-Identifier: Apache-2.0
#
# This is a free software, use it under GNU General Public License v3.0.
#
# Created By ImmortalWrt
# https://github.com/project-openwrt
# Copyright (C) 2025 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-gost
PKG_VERSION:=1.0
PKG_RELEASE:=1
LUCI_TITLE:=LuCI support for Gost
LUCI_TITLE:=LuCI support for GOST
LUCI_DEPENDS:=+gost
LUCI_PKGARCH:=all
PKG_MAINTAINER:=ImmortalWrt
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
$(eval $(call BuildPackage,luci-app-gost))
+127
View File
@@ -0,0 +1,127 @@
# luci-app-gost
适用于 OpenWrt/Lede 的 GOST 隧道 LuCI 控制面板插件。
## 简介
GOST (GO Simple Tunnel) 是一个用 Go 语言编写的简单安全隧道工具,支持多种协议和转发方式。
## 功能特性
- **隧道服务**: 支持多种协议的隧道转发
- **配置管理**: 支持配置文件和命令行参数两种方式
- **状态监控**: 实时显示服务运行状态
- **动态参数**: 支持自定义启动参数
## 依赖
- `luci`
- `luci-base`
- `gost` (主程序)
## 安装
### OpenWrt Feed 方式
```bash
# 添加 feed
echo 'src-git gost https://github.com/kenzok78/luci-app-gost' >> feeds.conf.default
./scripts/feeds update -a
./scripts/feeds install -a -p gost
# 编译
make package/luci-app-gost/compile V=s
```
### 直接编译
```bash
git clone https://github.com/kenzok78/luci-app-gost.git package/luci-app-gost
make menuconfig # 选择 LuCI -> Applications -> luci-app-gost
make -j$(nproc) V=s
```
## 使用说明
### 基本配置
1. 访问 OpenWrt Web 管理界面 → 服务 → GOST
2. 启用插件
3. 选择配置方式:
- **配置文件**: 指定 `gost.json` 配置文件路径
- **命令行参数**: 添加启动参数
4. 保存并应用配置
### 配置说明
| 选项 | 说明 | 默认值 |
|------|------|--------|
| 启用 | 开启/关闭 GOST 服务 | 关闭 |
| 配置文件 | GOST 配置文件路径 | /etc/gost/gost.json |
| 参数 | 启动命令行参数 | 无 |
### 配置文件示例
`/etc/gost/gost.json`:
```json
{
" Serve": [
{
"chain": "/tcp:0.0.0.0:8080"
}
]
}
```
## 目录结构
```
luci-app-gost/
├── htdocs/ # Web 静态资源
│ └── luci-static/
│ └── resources/
│ └── view/ # 视图模板
│ └── gost.js
├── root/ # 文件系统文件
│ ├── etc/
│ │ ├── config/ # UCI 配置
│ │ └── init.d/ # 启动脚本
│ └── usr/
│ └── share/
│ ├── luci/
│ │ └── menu.d/ # 菜单定义
│ └── rpcd/
│ └── acl.d/ # ACL 权限
├── po/ # 翻译文件
│ ├── zh-cn/
│ └── zh_Hans/
└── Makefile
```
## 常见问题
### Q: 启动失败怎么办?
检查 gost 二进制文件是否已安装,以及配置文件路径是否正确。
### Q: 如何查看日志?
```bash
logread -e gost
```
### Q: 如何手动启动?
```bash
/etc/init.d/gost start
```
## 许可证
Apache-2.0
## 来源
- [GOST](https://gost.run/)
- [go-gost/gost](https://github.com/go-gost/gost)
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: Apache-2.0
/*
* Copyright (C) 2025 ImmortalWrt.org
*/
'use strict';
'require form';
'require poll';
'require rpc';
'require view';
const callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { '': {} }
});
function getServiceStatus() {
return L.resolveDefault(callServiceList('gost'), {}).then(function(res) {
let isRunning = false;
try {
isRunning = res['gost']['instances']['instance1']['running'];
} catch (e) {}
return isRunning;
});
}
function renderStatus(isRunning) {
let spanTmpl = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
return spanTmpl.format(
isRunning ? 'green' : 'red',
_('GOST'),
isRunning ? _('RUNNING') : _('NOT RUNNING')
);
}
return view.extend({
render: function() {
let m, s, o;
m = new form.Map('gost', _('GOST'),
_('A simple security tunnel written in Golang.'));
/* Bug fix: 为状态栏段指定一个不存在的 UCI 类型,避免匹配实际配置节 */
s = m.section(form.TypedSection, '_status');
s.anonymous = true;
s.render = function() {
poll.add(function() {
return L.resolveDefault(getServiceStatus()).then(function(isRunning) {
/* Bug fix: 重命名变量以避免遮蔽外层 'require view' 中的 view 模块 */
let statusEl = document.getElementById('service_status');
if (statusEl)
statusEl.innerHTML = renderStatus(isRunning);
});
});
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
E('p', { id: 'service_status' }, _('Collecting data…'))
]);
};
s = m.section(form.NamedSection, 'config', 'gost');
o = s.option(form.Flag, 'enabled', _('Enable'));
o.rmempty = false;
o = s.option(form.Value, 'config_file', _('Configuration file'));
o.value('/etc/gost/gost.json');
o.datatype = 'path';
o = s.option(form.DynamicList, 'arguments', _('Arguments'));
o.validate = function(section_id, value) {
if (section_id) {
let config_file = this.section.formvalue(section_id, 'config_file');
/* Bug fix: 避免使用 ES2020 可选链 (?.) 以兼容旧版 JS 运行时 */
let args = this.section.formvalue(section_id, 'arguments');
if (!config_file && (!args || !args.length))
return _('Expecting: %s').format(_('non-empty value'));
}
return true;
};
return m.render();
}
});

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