update 2026-09-20 23:17:18

This commit is contained in:
action committed 2026-09-20 23:17:18 +08:00
1 parent 735d247cbe
commit d657bc36f5
23 files changed
+2655 -667

No files matched your search

+3 -3
View File
@@ -5,13 +5,13 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=dae
PKG_VERSION:=2026.09.12
PKG_VERSION:=2026.09.18
PKG_RELEASE:=1
PKG_SOURCE:=dae-src-2026.09.12-187058462a1f.tar.gz
PKG_SOURCE:=dae-src-2026.09.18-dc29e60bab23.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_HASH:=187058462a1fd9eeb9885f4971ccf4bc81358533e71730d8509bd7968624c1c7
PKG_HASH:=dc29e60bab23a8eac253a9dce194a783f22f1c83b50452bfcab4ed8b9f6edcbc
PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILE:=LICENSE
File diff suppressed because it is too large. Load diff
+9 -14
View File
@@ -1,24 +1,19 @@
--- a/trace/kern/trace.c
+++ b/trace/kern/trace.c
@@ -228,7 +228,7 @@ KPROBE_SKB_AT(1)
@@ -452,7 +452,6 @@ KPROBE_SKB_AT(1)
KPROBE_SKB_AT(2)
KPROBE_SKB_AT(3)
KPROBE_SKB_AT(4)
-KPROBE_SKB_AT(5)
+//KPROBE_SKB_AT(5)
SEC("kprobe/skb_lifetime_termination")
int kprobe_skb_lifetime_termination(struct pt_regs *ctx)
--- a/trace/trace.go
+++ b/trace/trace.go
@@ -212,8 +212,8 @@ func attachBpfToTargets(objs *bpfObjects
kp, err = link.Kprobe(fn, objs.KprobeSkb3, nil)
case 4:
kp, err = link.Kprobe(fn, objs.KprobeSkb4, nil)
- case 5:
- kp, err = link.Kprobe(fn, objs.KprobeSkb5, nil)
+ //case 5:
+ // kp, err = link.Kprobe(fn, objs.KprobeSkb5, nil)
}
if err != nil {
logrus.Debugf("failed to attach kprobe to %s: %+v\n", fn, err)
@@ -191,7 +191,6 @@ func attachBpfToTargets(objs *bpfObjects, targets map[string]int) (links []link.Link, err error) {
1: objs.KprobeSkb1,
2: objs.KprobeSkb2,
3: objs.KprobeSkb3,
4: objs.KprobeSkb4,
- 5: objs.KprobeSkb5,
}
+3 -3
View File
@@ -5,13 +5,13 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=daed
PKG_VERSION:=2026.09.12
PKG_VERSION:=2026.09.18
PKG_RELEASE:=1
PKG_SOURCE:=daed-src-2026.09.12-a0181f729855.tar.gz
PKG_SOURCE:=daed-src-2026.09.18-0d4c9d0c95b9.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_HASH:=a0181f7298552497ed193e683a54b1df77f2d5441524d61989f3e3e3cf6eac51
PKG_HASH:=0d4c9d0c95b9dd5eb8a6da98ff9e97336f4f87cb38c8fff1a6bc5ce83e2c97c7
PKG_LICENSE:=AGPL-3.0-only MIT
PKG_LICENSE_FILES:=LICENSE wing/LICENSE
@@ -0,0 +1,837 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: kenzok8 <kenzok8@gmail.com>
Date: Sun, 20 Sep 2026 16:59:23 +0800
Subject: [PATCH] Adapt wing to new dae core
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
改动:适配新核心生命周期,并修复空配置停止和重载回滚。
原因:旧接口删除且新控制面交接流程已改变。
---
dae-core/config/marshal.go | 5 +
dae/run.go | 402 +++++++++++++-----
dae/run_handoff_test.go | 33 ++
dae/runtime.go | 6 +-
dae/utils.go | 9 +-
db/node_utils.go | 3 +-
graphql/service/config/mutation_utils.go | 9 +-
graphql/service/node/latency_cache.go | 2 +-
.../service/node/latency_mutation_utils.go | 59 ++-
graphql/service/routing/resolver.go | 2 +-
10 files changed, 415 insertions(+), 115 deletions(-)
create mode 100644 dae/run_handoff_test.go
diff --git a/dae-core/config/marshal.go b/dae-core/config/marshal.go
index f68006a..69a5325 100644
--- a/dae-core/config/marshal.go
+++ b/dae-core/config/marshal.go
@@ -42,6 +42,11 @@ type Marshaller struct {
buf bytes.Buffer
}
+// Bytes returns the serialized section buffer without exposing mutable state.
+func (m *Marshaller) Bytes() []byte {
+ return m.buf.Bytes()
+}
+
func (m *Marshaller) writeLine(depth int, line string) {
if depth < 0 {
depth = 0
diff --git a/dae/run.go b/dae/run.go
index a0a665d..96b579c 100644
--- a/dae/run.go
+++ b/dae/run.go
@@ -6,6 +6,8 @@
package dae
import (
+ "context"
+ "errors"
"fmt"
"net/netip"
"runtime"
@@ -28,6 +30,22 @@ type ReloadMessage struct {
Callback chan<- error
}
+func closeReloadResources(closers ...func() error) error {
+ var errs error
+ for _, closeResource := range closers {
+ if closeResource != nil {
+ errs = errors.Join(errs, closeResource())
+ }
+ }
+ return errs
+}
+
+func notifyReloadCallback(callback chan<- error, err error) {
+ if callback != nil {
+ callback <- err
+ }
+}
+
var ChReloadConfigs = make(chan *ReloadMessage)
var GracefullyExit = make(chan struct{})
var EmptyConfig *daeConfig.Config
@@ -77,88 +95,115 @@ func Run(log *logrus.Logger, conf *daeConfig.Config, externGeoDataDirs []string,
case nil:
break dryLoop
default:
- newConf.Callback <- nil
+ notifyReloadCallback(newConf.Callback, nil)
}
}
return nil
}
// New c.
- c, err = newControlPlane(log, nil, nil, conf, externGeoDataDirs)
+ processSessions := control.NewSessionManager(context.Background())
+ defer func() { _ = processSessions.Close() }()
+ c, err = newControlPlane(context.Background(), log, nil, nil, conf, externGeoDataDirs, false)
if err != nil {
return err
}
+ if err = c.AttachSessionManager(processSessions); err != nil {
+ _ = c.Close()
+ return err
+ }
+ datapathDetached := false
+ if conf == EmptyConfig {
+ if err = c.DetachBpfHooks(); err != nil {
+ _ = c.Close()
+ return fmt.Errorf("detach initial empty datapath: %w", err)
+ }
+ datapathDetached = true
+ }
- // Serve tproxy TCP/UDP server util signals.
var listener *control.Listener
+ if err = control.GetDaeNetns().With(func() error {
+ var listenErr error
+ listener, listenErr = c.Listen(conf.Global.TproxyPort)
+ if listenErr != nil {
+ return listenErr
+ }
+ return listener.ValidateCurrentNetns()
+ }); err != nil {
+ if listener != nil {
+ _ = listener.Close()
+ }
+ _ = c.Close()
+ return fmt.Errorf("listen in dae netns: %w", err)
+ }
+ readyChan := make(chan bool, 1)
+ serveDone := make(chan error, 1)
+ go func() { serveDone <- c.Serve(readyChan, listener) }()
+ if !<-readyChan {
+ serveErr := <-serveDone
+ _ = closeReloadResources(listener.Close, c.Close)
+ if serveErr == nil {
+ serveErr = fmt.Errorf("initial control plane was not ready")
+ }
+ return serveErr
+ }
+ log.Infoln("Ready")
go func() {
- readyChan := make(chan bool, 1)
- go func() {
- <-readyChan
- log.Infoln("Ready")
- }()
- control.GetDaeNetns().With(func() error {
- if listener, err = c.ListenAndServe(readyChan, conf.Global.TproxyPort); err != nil {
- log.Errorln("ListenAndServe:", err)
- }
- return err
- })
- // Exit
+ if serveErr := <-serveDone; serveErr != nil {
+ log.WithError(serveErr).Errorln("control plane serve stopped")
+ }
ChReloadConfigs <- nil
}()
reloading := false
- /* dae-wing start */
- var errReload error
- var chCallback chan<- error
- /* dae-wing end */
loop:
for newReloadMsg := range ChReloadConfigs {
switch newReloadMsg {
case nil:
- /* dae-wing start */
- // We will receive nil after control plane being Closed.
- // We'll judge if we are in a reloading.
- /* dae-wing end */
-
if reloading {
- if listener == nil {
- // Failed to listen. Exit.
- break loop
- }
- // Serve.
+ // The old generation's Serve loop reports its shutdown after the
+ // candidate has already taken over the cloned listener.
reloading = false
- log.Warnln("[Reload] Serve")
- readyChan := make(chan bool, 1)
- go func() {
- if err := c.Serve(readyChan, listener); err != nil {
- log.Errorln("ListenAndServe:", err)
- }
- // Exit
- ChReloadConfigs <- nil
- }()
- <-readyChan
- log.Warnln("[Reload] Finished")
- /* dae-wing start */
- chCallback <- errReload
- /* dae-wing end */
+ continue
} else {
- // Listening error.
break loop
}
default:
// Reload signal.
log.Warnln("[Reload] Received reload signal; prepare to reload")
- /* dae-wing start */
newConf := newReloadMsg.Config
- /* dae-wing end */
-
- // openwrt-daede: dashboard stop reloads to EmptyConfig; detach hooks.
- if newConf == EmptyConfig {
- if e := c.DetachBpfHooks(); e != nil {
- log.Errorln("[Stop] DetachBpfHooks:", e)
+ if datapathDetached && newConf == EmptyConfig {
+ notifyReloadCallback(newReloadMsg.Callback, nil)
+ continue
+ }
+ oldC := c
+ wasStopped := datapathDetached
+ failReload := func(cause, cleanupErr error) error {
+ cause = errors.Join(cause, cleanupErr)
+ if cleanupErr == nil && wasStopped {
+ if detachErr := oldC.DetachBpfHooks(); detachErr != nil {
+ cause = errors.Join(cause, fmt.Errorf("restore stopped datapath: %w", detachErr))
+ cleanupErr = detachErr
+ } else {
+ datapathDetached = true
+ }
}
+ notifyReloadCallback(newReloadMsg.Callback, cause)
+ if cleanupErr != nil {
+ return cause
+ }
+ return nil
}
+ if wasStopped {
+ if restoreErr := oldC.RestoreDatapathForReloadRollback(); restoreErr != nil {
+ if fatalErr := failReload(fmt.Errorf("prepare stopped datapath: %w", restoreErr), nil); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ datapathDetached = false
+ }
+
// New logger.
oldLogOutput := log.Out
log = logrus.New()
@@ -167,65 +212,223 @@ loop:
log.SetOutput(oldLogOutput) // FIXME: THIS IS A HACK.
logrus.SetOutput(oldLogOutput)
- // New control plane.
- obj := c.EjectBpf()
var dnsCache map[string]*control.DnsCache
if conf.Dns.IpVersionPrefer == newConf.Dns.IpVersionPrefer {
// Only keep dns cache when ip version preference not change.
dnsCache = c.CloneDnsCache()
}
- log.Warnln("[Reload] Load new control plane")
- newC, err := newControlPlane(log, obj, dnsCache, newConf, externGeoDataDirs)
+ log.Warnln("[Reload] Prepare new control plane")
+ freshState, err := c.SnapshotFreshDatapathState()
if err != nil {
- /* dae-wing start */
- errReload = err
- /* dae-wing end */
-
- log.WithFields(logrus.Fields{
- "err": err,
- }).Errorln("[Reload] Failed to reload; try to roll back configuration")
- // Load last config back.
- newC, err = newControlPlane(log, obj, dnsCache, conf, externGeoDataDirs)
- if err != nil {
- obj.Close()
- c.Close()
- log.WithFields(logrus.Fields{
- "err": err,
- }).Fatalln("[Reload] Failed to roll back configuration")
+ if fatalErr := failReload(err, nil); fatalErr != nil {
+ return fatalErr
}
- newConf = conf
- log.Errorln("[Reload] Last reload failed; rolled back configuration")
- } else {
- log.Warnln("[Reload] Stopped old control plane")
-
- /* dae-wing start */
- errReload = nil
- /* dae-wing end */
+ continue
}
+ newC, err := newControlPlane(context.Background(), log, freshState, dnsCache, newConf, externGeoDataDirs, true)
+ if err == nil {
+ err = newC.AttachSessionManager(processSessions)
+ }
+ if err != nil {
+ var cleanupErr error
+ if newC != nil {
+ cleanupErr = newC.Close()
+ }
+ if fatalErr := failReload(err, cleanupErr); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ stagedListener, err := listener.Clone()
+ if err != nil {
+ if fatalErr := failReload(fmt.Errorf("clone listener: %w", err), newC.Close()); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ if err = oldC.StopDNSListener(); err != nil {
+ cleanupErr := closeReloadResources(stagedListener.Close, newC.Close, oldC.RestartDNSListener)
+ if fatalErr := failReload(fmt.Errorf("stop old DNS listener: %w", err), cleanupErr); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ dnsStopped := true
+ readyChan := make(chan bool, 1)
+ serveDone := make(chan error, 1)
+ go func() { serveDone <- newC.Serve(readyChan, stagedListener) }()
+ ready := <-readyChan
+ if !ready {
+ serveErr := <-serveDone
+ if serveErr == nil {
+ serveErr = fmt.Errorf("prepared control plane was not ready")
+ }
+ cleanupErr := closeReloadResources(
+ stagedListener.Close,
+ newC.Close,
+ oldC.RestartDNSListener,
+ )
+ dnsStopped = false
+ if fatalErr := failReload(serveErr, cleanupErr); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ hookPrepared := false
+ provisionalOwner := false
+ hookFlipCommitted := false
+ hookSetAdopted := false
+ flowDatapathAdopted := false
+ rollbackCleanupFailed := false
+ rollbackCandidate := func(cause error) error {
+ var rollbackErr error
+ addRollbackErr := func(err error) {
+ if err != nil {
+ rollbackCleanupFailed = true
+ rollbackErr = errors.Join(rollbackErr, err)
+ }
+ }
+ if flowDatapathAdopted {
+ addRollbackErr(oldC.AdoptProcessFlowDatapath(newC))
+ }
+ if hookSetAdopted {
+ if ownerErr := newC.RestorePreparedTCHookSet(oldC); ownerErr != nil {
+ rollbackCleanupFailed = true
+ return errors.Join(cause, rollbackErr, fmt.Errorf("restore TC HookSet owner: %w", ownerErr))
+ }
+ }
+ if hookFlipCommitted {
+ addRollbackErr(newC.RollbackPreparedBpfHookFlip())
+ }
+ if hookPrepared && !hookSetAdopted && !hookFlipCommitted {
+ addRollbackErr(newC.ClearPreparedTCHookHandoff())
+ }
+ if provisionalOwner {
+ newC.UnregisterProvisionalRoutingEpochExecutionOwner()
+ }
+ addRollbackErr(newC.DetachBpfHooks())
+ addRollbackErr(stagedListener.Close())
+ addRollbackErr(newC.Close())
+ if dnsStopped {
+ addRollbackErr(oldC.RestartDNSListener())
+ dnsStopped = false
+ }
+ return errors.Join(cause, rollbackErr)
+ }
+ if err = newC.PrepareTCHookHandoff(oldC); err != nil {
+ rollbackErr := rollbackCandidate(err)
+ if rollbackCleanupFailed {
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
+ return rollbackErr
+ }
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ hookPrepared = true
+ if err = newC.RegisterProvisionalRoutingEpochExecutionOwner(); err != nil {
+ rollbackErr := rollbackCandidate(err)
+ if rollbackCleanupFailed {
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
+ return rollbackErr
+ }
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ provisionalOwner = true
+ if err = newC.CommitPreparedBpfHookFlip(); err != nil {
+ rollbackErr := rollbackCandidate(err)
+ if rollbackCleanupFailed {
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
+ return rollbackErr
+ }
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ hookFlipCommitted = true
+ if err = newC.AdoptPreparedTCHookSet(oldC); err != nil {
+ rollbackErr := rollbackCandidate(err)
+ if rollbackCleanupFailed {
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
+ return rollbackErr
+ }
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ hookSetAdopted = true
+ if err = newC.AdoptProcessFlowDatapath(oldC); err != nil {
+ rollbackErr := rollbackCandidate(err)
+ if rollbackCleanupFailed {
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
+ return rollbackErr
+ }
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
+ return fatalErr
+ }
+ continue
+ }
+ flowDatapathAdopted = true
+ newC.UnregisterProvisionalRoutingEpochExecutionOwner()
+ provisionalOwner = false
+ if err = newC.FinalizePreparedTCHooks(); err != nil {
+ fatalErr := errors.Join(fmt.Errorf("finalize prepared TC hooks: %w", err), closeReloadResources(
+ listener.Close,
+ stagedListener.Close,
+ newC.Close,
+ oldC.Close,
+ ))
+ notifyReloadCallback(newReloadMsg.Callback, fatalErr)
+ return fatalErr
+ }
+ hookPrepared = false
+ hookFlipCommitted = false
+ hookSetAdopted = false
- // Inject bpf objects into the new control plane life-cycle.
- newC.InjectBpf(obj)
-
- // Prepare new context.
- oldC := c
+ oldListener := listener
c = newC
conf = newConf
+ listener = stagedListener
+ datapathDetached = false
+ retireErr := closeReloadResources(oldListener.Close, oldC.Close)
+ if newConf == EmptyConfig {
+ if retireErr != nil {
+ fatalErr := errors.Join(fmt.Errorf("retire previous control plane: %w", retireErr), closeReloadResources(stagedListener.Close, newC.Close))
+ notifyReloadCallback(newReloadMsg.Callback, fatalErr)
+ return fatalErr
+ }
+ if detachErr := newC.DetachBpfHooks(); detachErr != nil {
+ fatalErr := errors.Join(fmt.Errorf("detach stopped datapath: %w", detachErr), closeReloadResources(stagedListener.Close, newC.Close))
+ notifyReloadCallback(newReloadMsg.Callback, fatalErr)
+ return fatalErr
+ }
+ datapathDetached = true
+ } else if retireErr != nil {
+ log.WithError(retireErr).Warnln("[Reload] Failed to close old control plane")
+ }
reloading = true
- /* dae-wing start */
- chCallback = newReloadMsg.Callback
- /* dae-wing end */
-
- // Ready to close.
- oldC.Close()
+ notifyReloadCallback(newReloadMsg.Callback, nil)
+ go func(done <-chan error) {
+ if serveErr := <-done; serveErr != nil {
+ log.WithError(serveErr).Errorln("control plane serve stopped")
+ }
+ ChReloadConfigs <- nil
+ }(serveDone)
}
}
- if e := c.Close(); e != nil {
+ if e := closeReloadResources(listener.Close, c.Close); e != nil {
return fmt.Errorf("close control plane: %w", e)
}
return nil
}
-func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*control.DnsCache, conf *daeConfig.Config, externGeoDataDirs []string) (c *control.ControlPlane, err error) {
+func newControlPlane(ctx context.Context, log *logrus.Logger, bpf interface{}, dnsCache map[string]*control.DnsCache, conf *daeConfig.Config, externGeoDataDirs []string, prepared bool) (c *control.ControlPlane, err error) {
// Print configuration.
if log.IsLevelEnabled(logrus.DebugLevel) {
@@ -239,10 +442,6 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
control.PurgeStaleTCFilters(log)
}
- // Init Direct Dialers.
- direct.InitDirectDialers(conf.Global.FallbackResolver)
- netutils.FallbackDns = netip.MustParseAddrPort(conf.Global.FallbackResolver)
-
if !conf.Global.DisableWaitingNetwork && len(conf.Global.WanInterface) > 0 {
// Wait for network for WAN ready.
if err = waitForNetwork(log); err != nil {
@@ -266,7 +465,10 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
}
// New dae control plane.
- c, err = control.NewControlPlane(
+ directDialers := direct.NewDirectDialers(conf.Global.FallbackResolver)
+ systemDNSResolver := netutils.NewSystemDNSResolver(netip.MustParseAddrPort(conf.Global.FallbackResolver))
+ c, err = control.NewControlPlaneWithContextOptions(
+ ctx,
log,
bpf,
dnsCache,
@@ -276,6 +478,14 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
&conf.Global,
&conf.Dns,
externGeoDataDirs,
+ control.ControlPlaneBuildOptions{
+ DelayDatapathCommit: prepared,
+ DelayDNSListenerStart: prepared,
+ IsReload: prepared || bpf != nil,
+ DirectDialer: directDialers.Symmetric,
+ FullconeDirectDialer: directDialers.Fullcone,
+ SystemDNSResolver: systemDNSResolver,
+ },
)
if err != nil {
return nil, err
diff --git a/dae/run_handoff_test.go b/dae/run_handoff_test.go
new file mode 100644
index 0000000..0ffb2ec
--- /dev/null
+++ b/dae/run_handoff_test.go
@@ -0,0 +1,33 @@
+package dae
+
+import (
+ "errors"
+ "reflect"
+ "testing"
+)
+
+func TestCloseReloadResourcesOrder(t *testing.T) {
+ var got []string
+ err := closeReloadResources(
+ func() error { got = append(got, "old-listener"); return nil },
+ func() error { got = append(got, "staged-listener"); return nil },
+ func() error { got = append(got, "new-control-plane"); return nil },
+ func() error { got = append(got, "old-control-plane"); return nil },
+ )
+ if err != nil {
+ t.Fatalf("closeReloadResources() error = %v", err)
+ }
+ want := []string{"old-listener", "staged-listener", "new-control-plane", "old-control-plane"}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("close order = %v, want %v", got, want)
+ }
+}
+
+func TestCloseReloadResourcesJoinsErrors(t *testing.T) {
+ one := errors.New("old listener")
+ two := errors.New("old control plane")
+ err := closeReloadResources(func() error { return one }, func() error { return two })
+ if !errors.Is(err, one) || !errors.Is(err, two) {
+ t.Fatalf("joined error = %v, want both close errors", err)
+ }
+}
diff --git a/dae/runtime.go b/dae/runtime.go
index 2ab0d68..af46089 100644
--- a/dae/runtime.go
+++ b/dae/runtime.go
@@ -30,18 +30,16 @@ type RuntimeOverview struct {
}
func GetRuntimeOverview(windowSec int, maxPoints int) (*RuntimeOverview, error) {
- activeTCPConnections := 0
ctl, err := ControlPlane()
+ var snapshot control.RuntimeStatsSnapshot
if err != nil {
if !errors.Is(err, ErrControlPlaneNotInit) {
return nil, err
}
} else {
- activeTCPConnections = ctl.ActiveTCPConnections()
+ snapshot = ctl.SnapshotRuntimeStats(windowSec, maxPoints)
}
- snapshot := control.SnapshotRuntimeStats(activeTCPConnections, control.DefaultUdpEndpointPool.Count(), windowSec, maxPoints)
-
samples := make([]RuntimeTrafficSample, 0, len(snapshot.Samples))
for _, sample := range snapshot.Samples {
samples = append(samples, RuntimeTrafficSample{
diff --git a/dae/utils.go b/dae/utils.go
index dc43cbc..ca61c85 100644
--- a/dae/utils.go
+++ b/dae/utils.go
@@ -32,8 +32,11 @@ var (
EmptyGlobalSection = `global {}`
)
-func NecessaryOutbounds(routing *daeConfig.Routing) (outbounds []string) {
- f := daeConfig.FunctionOrStringToFunction(routing.Fallback)
+func NecessaryOutbounds(routing *daeConfig.Routing) (outbounds []string, err error) {
+ f, err := daeConfig.ParseFunctionOrString(routing.Fallback)
+ if err != nil {
+ return nil, err
+ }
outbounds = append(outbounds, f.Name)
for _, r := range routing.Rules {
outbound := r.Outbound.Name
@@ -42,7 +45,7 @@ func NecessaryOutbounds(routing *daeConfig.Routing) (outbounds []string) {
}
outbounds = append(outbounds, outbound)
}
- return daeCommon.Deduplicate(outbounds)
+ return daeCommon.Deduplicate(outbounds), nil
}
func ParseConfig(globalSection *string, dnsSection *string, routingSection *string) (*daeConfig.Config, error) {
diff --git a/db/node_utils.go b/db/node_utils.go
index b7b43b8..7ca8fc3 100644
--- a/db/node_utils.go
+++ b/db/node_utils.go
@@ -6,6 +6,7 @@
package db
import (
+ "context"
"strings"
"github.com/daeuniverse/dae-wing/common"
@@ -25,7 +26,7 @@ func NewNodeModel(link string, tag *string, subscriptionId *uint) (*Node, error)
}
_tag = *tag
}
- d, err := dialer.NewFromLink(&dialer.GlobalOption{
+ d, err := dialer.NewFromLinkContext(context.Background(), &dialer.GlobalOption{
Log: logrus.StandardLogger(),
}, dialer.InstanceOption{DisableCheck: false}, link, _tag)
if err != nil {
diff --git a/graphql/service/config/mutation_utils.go b/graphql/service/config/mutation_utils.go
index 59f0ebd..c7d6b78 100644
--- a/graphql/service/config/mutation_utils.go
+++ b/graphql/service/config/mutation_utils.go
@@ -298,7 +298,7 @@ func ApplyIfRunning(ctx context.Context) error {
func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
//// Dry run.
if noLoad {
- ch := make(chan error)
+ ch := make(chan error, 1)
reloadMsg := &dae.ReloadMessage{
Config: dae.EmptyConfig,
Callback: ch,
@@ -384,7 +384,10 @@ func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
}
/// Fill in necessary groups and nodes.
// Find groups needed by routing.
- outbounds := dae.NecessaryOutbounds(&c.Routing)
+ outbounds, err := dae.NecessaryOutbounds(&c.Routing)
+ if err != nil {
+ return 0, err
+ }
var groups []db.Group
q = d.Model(&db.Group{}).
Where("name in ?", outbounds).
@@ -523,7 +526,7 @@ func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
readTx = nil
/// Reload with current config.
- chReloadCallback := make(chan error)
+ chReloadCallback := make(chan error, 1)
reloadMsg := &dae.ReloadMessage{
Config: c,
Callback: chReloadCallback,
diff --git a/graphql/service/node/latency_cache.go b/graphql/service/node/latency_cache.go
index ba66794..e0ec91e 100644
--- a/graphql/service/node/latency_cache.go
+++ b/graphql/service/node/latency_cache.go
@@ -162,7 +162,7 @@ func refreshLatencyCache(ctx context.Context) error {
return err
}
- results := testLatencyResultsForNodes(option, nodes)
+ results := testLatencyResultsForNodes(ctx, option, nodes)
storeLatencyResults(results)
if ctl, err := dae.ControlPlane(); err == nil {
diff --git a/graphql/service/node/latency_mutation_utils.go b/graphql/service/node/latency_mutation_utils.go
index 056f9fa..5026b33 100644
--- a/graphql/service/node/latency_mutation_utils.go
+++ b/graphql/service/node/latency_mutation_utils.go
@@ -8,6 +8,8 @@ package node
import (
"context"
"io"
+ "net/netip"
+ "strings"
"sync"
"time"
@@ -15,6 +17,7 @@ import (
"github.com/daeuniverse/dae-wing/dae"
"github.com/daeuniverse/dae-wing/db"
dialer "github.com/daeuniverse/dae/component/outbound/dialer"
+ "github.com/daeuniverse/outbound/netproxy"
"github.com/graph-gophers/graphql-go"
"github.com/sirupsen/logrus"
)
@@ -32,12 +35,12 @@ func TestLatencies(ctx context.Context, ids *[]graphql.ID) ([]*LatencyResolver,
return nil, err
}
- results := testLatencyResultsForNodes(option, nodes)
+ results := testLatencyResultsForNodes(ctx, option, nodes)
storeLatencyResults(results)
return results, nil
}
-func testLatencyResultsForNodes(option *dialer.GlobalOption, nodes []db.Node) []*LatencyResolver {
+func testLatencyResultsForNodes(ctx context.Context, option *dialer.GlobalOption, nodes []db.Node) []*LatencyResolver {
results := make([]*LatencyResolver, len(nodes))
sem := make(chan struct{}, latencyProbeConcurrency)
var wg sync.WaitGroup
@@ -51,7 +54,7 @@ func testLatencyResultsForNodes(option *dialer.GlobalOption, nodes []db.Node) []
defer func() { <-sem }()
node := nodes[index]
- results[index] = testSingleNodeLatency(option, &node)
+ results[index] = testSingleNodeLatency(ctx, option, &node)
}()
}
@@ -93,14 +96,14 @@ func latencyProbeNodes(ctx context.Context, ids *[]graphql.ID) ([]db.Node, error
return nodes, nil
}
-func testSingleNodeLatency(option *dialer.GlobalOption, node *db.Node) *LatencyResolver {
+func testSingleNodeLatency(ctx context.Context, option *dialer.GlobalOption, node *db.Node) *LatencyResolver {
resolver := &LatencyResolver{
NodeID: node.ID,
AliveVal: false,
TestedAtV: time.Now(),
}
- d, err := dialer.NewFromLink(option, dialer.InstanceOption{DisableCheck: false}, node.Link, "")
+ d, err := dialer.NewFromLinkContext(ctx, option, dialer.InstanceOption{DisableCheck: false}, node.Link, "")
if err != nil {
msg := err.Error()
resolver.MessageV = &msg
@@ -108,7 +111,7 @@ func testSingleNodeLatency(option *dialer.GlobalOption, node *db.Node) *LatencyR
}
defer d.Close()
- result, err := d.ProbeLatencyFast()
+ result, err := probeLatencyFast(ctx, d)
if err != nil {
msg := err.Error()
resolver.MessageV = &msg
@@ -128,3 +131,47 @@ func testSingleNodeLatency(option *dialer.GlobalOption, node *db.Node) *LatencyR
}
return resolver
}
+
+func probeLatencyFast(ctx context.Context, d *dialer.Dialer) (*dialer.LatencyProbeResult, error) {
+ opt, err := d.TcpCheckOptionRaw.Option()
+ if err != nil {
+ return nil, err
+ }
+ network, err := netproxy.ParseMagicNetwork(d.TcpCheckOptionRaw.ResolverNetwork)
+ if err != nil {
+ return nil, err
+ }
+ result := &dialer.LatencyProbeResult{CheckedAt: time.Now()}
+ var messages []string
+ for _, probe := range []struct {
+ index int
+ ip netip.Addr
+ }{
+ {index: dialer.IdxTcp4, ip: opt.Ip4},
+ {index: dialer.IdxTcp6, ip: opt.Ip6},
+ } {
+ if !probe.ip.IsValid() {
+ continue
+ }
+ probeCtx, cancel := context.WithTimeout(ctx, 1500*time.Millisecond)
+ started := time.Now()
+ ok, probeErr := d.HttpCheck(probeCtx, probe.index, opt.Url, probe.ip, opt.Method, network.Mark, network.Mptcp)
+ cancel()
+ if ok {
+ latency := time.Since(started)
+ if !result.Alive || latency < result.Latency {
+ result.Latency = latency
+ }
+ result.Alive = true
+ } else if probeErr != nil {
+ messages = append(messages, probeErr.Error())
+ }
+ }
+ if !result.Alive {
+ result.Message = strings.Join(messages, "; ")
+ if result.Message == "" {
+ result.Message = "no latency result"
+ }
+ }
+ return result, nil
+}
diff --git a/graphql/service/routing/resolver.go b/graphql/service/routing/resolver.go
index 7822d64..3ed61aa 100644
--- a/graphql/service/routing/resolver.go
+++ b/graphql/service/routing/resolver.go
@@ -41,7 +41,7 @@ func (r *Resolver) Selected() bool {
return r.Model.Selected
}
-func (r *Resolver) ReferenceGroups() (outbounds []string) {
+func (r *Resolver) ReferenceGroups() (outbounds []string, err error) {
return dae.NecessaryOutbounds(r.DaeRouting)
}
+9 -14
View File
@@ -1,24 +1,19 @@
--- a/dae-core/trace/kern/trace.c
+++ b/dae-core/trace/kern/trace.c
@@ -228,7 +228,7 @@ KPROBE_SKB_AT(1)
@@ -452,7 +452,6 @@ KPROBE_SKB_AT(1)
KPROBE_SKB_AT(2)
KPROBE_SKB_AT(3)
KPROBE_SKB_AT(4)
-KPROBE_SKB_AT(5)
+//KPROBE_SKB_AT(5)
SEC("kprobe/skb_lifetime_termination")
int kprobe_skb_lifetime_termination(struct pt_regs *ctx)
--- a/dae-core/trace/trace.go
+++ b/dae-core/trace/trace.go
@@ -206,8 +206,8 @@ func attachBpfToTargets(objs *bpfObjects
kp, err = link.Kprobe(fn, objs.KprobeSkb3, nil)
case 4:
kp, err = link.Kprobe(fn, objs.KprobeSkb4, nil)
- case 5:
- kp, err = link.Kprobe(fn, objs.KprobeSkb5, nil)
+ //case 5:
+ // kp, err = link.Kprobe(fn, objs.KprobeSkb5, nil)
}
if err != nil {
logrus.Debugf("failed to attach kprobe to %s: %+v\n", fn, err)
@@ -191,7 +191,6 @@ func attachBpfToTargets(objs *bpfObjects, targets map[string]int) (links []link.Link, err error) {
1: objs.KprobeSkb1,
2: objs.KprobeSkb2,
3: objs.KprobeSkb3,
4: objs.KprobeSkb4,
- 5: objs.KprobeSkb5,
}
@@ -620,6 +620,17 @@ function createConfigEditor(mode) {
};
}
// 全局锁:内核操作互斥(切换配置 / 更新订阅 / 删除)
function clKernelBusy() { return !!document.body.getAttribute('data-cl-kernel-busy'); }
function clKernelLock() {
document.body.setAttribute('data-cl-kernel-busy', '1');
document.querySelectorAll('.cl-btn-switch, .cl-btn-update-sub, .cl-btn-delete').forEach(function (b) { b.disabled = true; });
}
function clKernelUnlock() {
document.body.removeAttribute('data-cl-kernel-busy');
document.querySelectorAll('.cl-btn-switch, .cl-btn-update-sub, .cl-btn-delete').forEach(function (b) { b.disabled = false; });
}
return view.extend({
_tab: null,
_sbTab: null,
@@ -841,17 +852,37 @@ return view.extend({
E('div', { 'class': 'cl-file-actions' }, [
E('button', {
'class': 'btn cbi-button cl-btn-sm cl-btn-update-sub',
click: function () {
click: function (ev) {
var btn = ev.target;
if (btn.disabled || clKernelBusy()) return;
clKernelLock();
btn.disabled = true;
btn.textContent = _("Updating…");
L.resolveDefault(callUpdateSub(sub.name), {}).then(function (r) {
ui.addNotification(null, E('p', r.success ? sub.name + _(" update succeeded") : _("Update failed")));
location.reload();
}).catch(function () {
btn.disabled = false;
btn.textContent = _("Update");
clKernelUnlock();
});
}
}, _("Update")),
E('button', {
'class': 'btn cbi-button cl-btn-sm cl-btn-switch',
click: function () {
L.resolveDefault(callSetConfig(sub.name), {}).then(function () { location.reload(); });
click: function (ev) {
var btn = ev.target;
if (btn.disabled || clKernelBusy()) return;
clKernelLock();
btn.disabled = true;
btn.textContent = _("Loading…");
L.resolveDefault(callSetConfig(sub.name), {}).then(function () {
location.reload();
}).catch(function () {
btn.disabled = false;
btn.textContent = _("Switch Profile");
clKernelUnlock();
});
}
}, _("Switch Profile")),
E('button', {
@@ -1022,19 +1053,54 @@ return view.extend({
E('div', { 'class': 'cl-file-actions' }, [
E('button', {
'class': 'btn cbi-button cl-btn-sm cl-btn-edit',
click: function () { loadOtherEditor(f.name, type); }
click: function (ev) {
var btn = ev.target;
if (btn.disabled) return;
btn.disabled = true;
var orig = btn.textContent;
btn.textContent = _("Loading…");
Promise.resolve(loadOtherEditor(f.name, type)).then(function () {
btn.disabled = false;
btn.textContent = orig;
}).catch(function () {
btn.disabled = false;
btn.textContent = orig;
});
}
}, _("Edit")),
E('button', {
'class': 'btn cbi-button cl-btn-sm cl-btn-switch',
click: function () {
L.resolveDefault(callSetConfig(f.name), {}).then(function () { location.reload(); });
click: function (ev) {
var btn = ev.target;
if (btn.disabled || clKernelBusy()) return;
clKernelLock();
btn.disabled = true;
btn.textContent = _("Loading…");
L.resolveDefault(callSetConfig(f.name), {}).then(function () {
location.reload();
}).catch(function () {
btn.disabled = false;
btn.textContent = _("Switch Profile");
clKernelUnlock();
});
}
}, _("Switch Profile")),
E('button', {
'class': 'btn cbi-button cl-btn-sm cl-btn-delete',
click: function () {
click: function (ev) {
var btn = ev.target;
if (btn.disabled || clKernelBusy()) return;
if (!confirm(_("Delete ") + f.name + '?')) return;
L.resolveDefault(callDeleteCfg(f.name, type), {}).then(function () { location.reload(); });
clKernelLock();
btn.disabled = true;
btn.textContent = _("Loading…");
L.resolveDefault(callDeleteCfg(f.name, type), {}).then(function () {
location.reload();
}).catch(function () {
btn.disabled = false;
btn.textContent = _("Delete");
clKernelUnlock();
});
}
}, _("Delete"))
])
@@ -231,6 +231,17 @@ var MSGS = {
}
};
// 全局锁:内核操作互斥(切换配置 / 更新订阅 / 删除)
function clKernelBusy() { return !!document.body.getAttribute('data-cl-kernel-busy'); }
function clKernelLock() {
document.body.setAttribute('data-cl-kernel-busy', '1');
document.querySelectorAll('.cl-btn-switch, .cl-btn-update-sub, .cl-btn-delete, .cl-kernel-sel').forEach(function (b) { b.disabled = true; });
}
function clKernelUnlock() {
document.body.removeAttribute('data-cl-kernel-busy');
document.querySelectorAll('.cl-btn-switch, .cl-btn-update-sub, .cl-btn-delete, .cl-kernel-sel').forEach(function (b) { b.disabled = false; });
}
return view.extend({
_busy: false,
_op: null, /* 'start' | 'stop' | 'restart' | null */
@@ -1561,8 +1572,8 @@ return view.extend({
var panelUrl = this._dashboardUrl(st);
var panels = ['metacubexd', 'yacd', 'zashboard', 'razord'];
var mkSel = function (opts, val, fn) {
return E('select', { 'class': 'cbi-input-select', change: fn },
var mkSel = function (opts, val, fn, extraCls) {
return E('select', { 'class': 'cbi-input-select' + (extraCls ? ' ' + extraCls : ''), change: fn },
opts.map(function (o) {
return E('option', { value: o[0], selected: o[0] === val ? '' : null, disabled: o[2] ? '' : null }, o[1]);
}));
@@ -1598,7 +1609,18 @@ return view.extend({
E('div', { 'class': 'cl-ctrl' }, [
E('label', {}, _("Proxy Mode")),
mkSel([['rule',_("Rule")],['global',_("Global")],['direct',_("Direct")]], proxyMode,
function (ev) { clashoo.setProxyMode(ev.target.value); })
function (ev) {
var sel = ev.target;
if (sel.disabled || clKernelBusy()) return;
clKernelLock();
sel.disabled = true;
Promise.resolve(clashoo.setProxyMode(sel.value)).then(function () {
return self._pollOverview(true);
}).catch(function () {}).then(function () {
sel.disabled = false;
clKernelUnlock();
});
}, 'cl-kernel-sel')
]),
E('div', { 'class': 'cl-ctrl' }, [
E('label', {}, _("Run Mode")),
@@ -1607,7 +1629,10 @@ return view.extend({
var mode = ev.target.value;
if (mode === 'custom')
return;
ev.target.disabled = true;
var sel = ev.target;
if (sel.disabled || clKernelBusy()) return;
clKernelLock();
sel.disabled = true;
self._op = 'mode';
self._lastSt = self._lastSt || {};
if (mode === 'tun-mixed') {
@@ -1634,9 +1659,10 @@ return view.extend({
ui.addNotification(null, E('p', _("Run mode setting failed: ") + (e.message || e)));
}).then(function () {
self._op = null;
ev.target.disabled = false;
sel.disabled = false;
clKernelUnlock();
});
})
}, 'cl-kernel-sel')
]),
E('div', { 'class': 'cl-ctrl' }, [
E('label', {}, _("Configuration Files")),
@@ -1644,8 +1670,10 @@ return view.extend({
mkSel(configs.length ? configs.map(function(c){return[c,c];}) : [['',_("(empty)")]], current,
function (ev) {
var sel = ev.target;
if (sel.disabled || clKernelBusy()) return;
var name = sel.value;
var prev = sel.getAttribute('data-cl-prev') || current;
clKernelLock();
sel.disabled = true;
var setter = (st.core_type === 'singbox')
? clashoo.setSingboxProfile(name)
@@ -1665,13 +1693,14 @@ return view.extend({
})
.then(function () {
sel.disabled = false;
clKernelUnlock();
/* force immediate state refresh so cards get new current */
return self._pollOverview(true);
});
}),
}, 'cl-kernel-sel'),
E('button', {
'class': 'btn cbi-button-action cl-btn-update-sub',
click: L.bind(this._updSubs, this)
click: L.bind(function (ev) { this._updSubs(ev); }, this)
}, _("Update Subscription"))
])
]),
@@ -1971,7 +2000,13 @@ return view.extend({
_stop: function () { return this._svc(function () { return clashoo.stop(); }, 'stop'); },
_restart: function () { return this._svc(function () { return clashoo.restart(); }, 'restart'); },
_updSubs: function () {
_updSubs: function (ev) {
var btn = ev && ev.target;
if (btn && btn.disabled) return Promise.resolve();
if (clKernelBusy()) return Promise.resolve();
clKernelLock();
var origText = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = _("Updating…"); }
return L.resolveDefault(callUpdateCurrentSubscription(), {}).then(function (r) {
if (r && r.reason === 'not_subscription') {
ui.addNotification(null, E('p', _("Current configuration is a custom file; no subscription update needed")), 'info');
@@ -1979,6 +2014,9 @@ return view.extend({
}
ui.addNotification(null, E('p', r.success ? (r.message || _("Subscription updated successfully"))
: (_("Update failed: ") + (r.message || _("Current configuration has no recorded subscription URL")))));
}).catch(function () {}).then(function () {
if (btn) { btn.disabled = false; btn.textContent = origText; }
clKernelUnlock();
});
},
@@ -3145,7 +3145,19 @@ let proxies = data.proxies;
let switchable = { 'Selector': 1 };
let readonly = { 'URLTest': 1, 'Fallback': 1, 'LoadBalance': 1, 'Relay': 1 };
let groups = [];
for (let gname in proxies) {
let order = [];
let global = proxies['GLOBAL'];
if (global && global.all && length(global.all)) {
for (let i = 0; i < length(global.all); i++) {
if (global.all[i] != 'GLOBAL') push(order, global.all[i]);
}
} else {
for (let gname in proxies) {
if (gname != 'GLOBAL') push(order, gname);
}
}
for (let i = 0; i < length(order); i++) {
let gname = order[i];
let g = proxies[gname];
if (!g || type(g) != 'object') continue;
let gt = g.type || '';
+1 -1
View File
@@ -19,7 +19,7 @@ OPENWRT_ARCH = nil
DISTRIB_ARCH = nil
OPENWRT_BOARD = nil
LOCK_PREFIX = "/tmp/lock/" .. c_config
LOCK_PREFIX = "/var/lock/" .. c_config
LOG_FILE = "/tmp/log/" .. c_config .. ".log"
TMP_PATH = "/tmp/etc/" .. c_config
CACHE_PATH = TMP_PATH .. "_tmp"
@@ -118,8 +118,16 @@ function copy_instance(var)
local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"]
local conf_lines = {}
local DEFAULT_DNSMASQ_CFGID = sys.exec("echo -n $(uci -q show dhcp.@dnsmasq[0] | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')")
if fs.access("/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) then
for line in io.lines("/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) do
local conf_file = "/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID
local retry = 5
while not fs.access(conf_file) and retry > 0 do
api.nixio.nanosleep(1, 0)
retry = retry - 1
end
if fs.access(conf_file) then
for line in io.lines(conf_file) do
local filter
if line:find("passwall") then filter = true end
if line:find("ubus") then filter = true end
@@ -139,7 +147,10 @@ function copy_instance(var)
tinsert(conf_lines, line)
end
end
else
sys.call("logger -t passwall 'ERROR: dnsmasq config " .. conf_file .. " not found after 5s wait! DNS hijacking will fail.'")
end
tinsert(conf_lines, "port=" .. LISTEN_PORT)
if TMP_DNSMASQ_PATH then
sys.call("rm -rf " .. TMP_DNSMASQ_PATH .. "/*passwall*")
@@ -6,7 +6,7 @@ CONFIG=passwall
APP_PATH=/usr/share/${CONFIG}
TMP_PATH=/tmp/etc/${CONFIG}
TMP_PATH2=${TMP_PATH}_tmp
LOCK_PATH=/tmp/lock
LOCK_PATH=/var/lock
LOG_FILE=/tmp/log/${CONFIG}.log
TMP_ACL_PATH=${TMP_PATH}/acl
TMP_BIN_PATH=${TMP_PATH}/bin
+1 -1
View File
@@ -20,7 +20,7 @@ command_timeout = 300
OPENWRT_ARCH = nil
DISTRIB_ARCH = nil
LOCK_PREFIX = "/tmp/lock/" .. c_config
LOCK_PREFIX = "/var/lock/" .. c_config
LOG_FILE = "/tmp/log/" .. c_config .. ".log"
TMP_PATH = "/tmp/etc/" .. c_config
CACHE_PATH = TMP_PATH .. "_tmp"
@@ -120,8 +120,17 @@ function copy_instance(var)
local TMP_DNSMASQ_PATH = var["TMP_DNSMASQ_PATH"]
local conf_lines = {}
local DEFAULT_DNSMASQ_CFGID = sys.exec("echo -n $(uci -q show dhcp.@dnsmasq[0] | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')")
if fs.access("/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) then
for line in io.lines("/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) do
local conf_file = "/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID
local retry = 5
while not fs.access(conf_file) and retry > 0 do
api.nixio.nanosleep(1, 0)
retry = retry - 1
end
if fs.access(conf_file) then
for line in io.lines(conf_file) do
local filter
if line:find("passwall2") then filter = true end
if line:find("ubus") then filter = true end
@@ -140,7 +149,10 @@ function copy_instance(var)
tinsert(conf_lines, line)
end
end
else
sys.call("logger -t passwall2 'ERROR: dnsmasq config " .. conf_file .. " not found after 5s wait! DNS hijacking will fail.'")
end
tinsert(conf_lines, "port=" .. LISTEN_PORT)
--dhcp.leases to hostsMore actions
local hosts = api.CACHE_PATH .. "/dhcp-hosts"
@@ -6,7 +6,7 @@ CONFIG=passwall2
APP_PATH=/usr/share/${CONFIG}
TMP_PATH=/tmp/etc/${CONFIG}
TMP_PATH2=${TMP_PATH}_tmp
LOCK_PATH=/tmp/lock
LOCK_PATH=/var/lock
LOG_FILE=/tmp/log/${CONFIG}.log
TMP_ACL_PATH=${TMP_PATH}/acl
TMP_BIN_PATH=${TMP_PATH}/bin
@@ -1,150 +1,508 @@
'use strict';
'require view';
'require dom';
'require poll';
'require uci';
'require rpc';
'require form';
'require uci';
'require fs';
'require ui';
// 辅助工具:统一纯净数组转换
function toArray(val) {
if (Array.isArray(val)) return val.filter(Boolean);
return typeof val === 'string' ? val.trim().split(/\s+/).filter(Boolean) : [];
}
// 辅助工具:提取首个 IP 并去除可能的 CIDR 掩码(容错支持 String、Array 及 null/undefined)
function getFirstIp(val, defaultVal) {
if (Array.isArray(val)) val = val[0];
if (typeof val === 'string') return val.split('/')[0].trim();
return defaultVal || '';
}
// 辅助工具:通用值深度比对(支持标量与数组)
function isEqual(a, b) {
if (Array.isArray(a) || Array.isArray(b)) {
var arrA = toArray(a).sort(), arrB = toArray(b).sort();
return arrA.length === arrB.length && arrA.every(function(v, i) { return v === arrB[i]; });
}
return String(a == null ? '' : a) === String(b == null ? '' : b);
}
// 辅助工具:检查数组是否包含元素(兼容低版本环境)
function has(arr, item) {
return Array.isArray(arr) && arr.indexOf(item) !== -1;
}
// 辅助工具:安全删除 UCI 选项(只有当该选项在 UCI 内存中真实存在时才调用 unset,防止 ubus 报错)
function safeUnset(conf, sid, opt) {
try {
if (uci.get(conf, sid, opt) != null) {
uci.unset(conf, sid, opt);
return true;
}
} catch(e) {}
return false;
}
// 辅助工具:智能判断无线物理网卡的频段 (2.4G / 5G / 6G)
function getRadioBand(devName) {
if (!devName) return '2.4G';
var b = uci.get('wireless', devName, 'band');
if (b === '6g') return '6G';
if (b === '5g') return '5G';
if (b === '2g') return '2.4G';
var ch = parseInt(uci.get('wireless', devName, 'channel'), 10);
if (!isNaN(ch)) {
if (ch >= 1 && ch <= 14) return '2.4G';
if (ch >= 32 && ch <= 196) return '5G';
if (ch > 196) return '6G';
}
var hw = (uci.get('wireless', devName, 'hwmode') || '').toLowerCase();
if (hw.indexOf('a') !== -1) return '5G';
if (hw.indexOf('b') !== -1 || hw.indexOf('g') !== -1) return '2.4G';
var dl = devName.toLowerCase();
if (dl.indexOf('6g') !== -1) return '6G';
if (dl.indexOf('5g') !== -1) return '5G';
return '2.4G';
}
return view.extend({
load: async function () {
const promises = await Promise.all([
fs.exec('/etc/init.d/wizard', ['reconfig']),
uci.changes(),
L.resolveDefault(uci.load('wireless')),
uci.load('wizard'),
L.resolveDefault(fs.stat('/www/luci-static/istorex/style.css'), null),
L.resolveDefault(fs.stat('/www/luci-static/routerdog/style.css'), null),
L.resolveDefault(fs.stat('/usr/sbin/nginx'), null)
]);
const data = {
istorex: promises[4],
routerdog: promises[5],
nginx: promises[6]
initialValues: {},
initialShortcuts: [],
hasWireless: false,
hasNginx: false,
map: null,
fields: null,
optMap: null,
// 1. 直连加载系统真实配置包,无需任何中间文件
load: function() {
var pkgs = ['network', 'wireless', 'dhcp', 'uhttpd', 'firewall', 'luci', 'wizard'];
var tasks = pkgs.map(function(p) {
return uci.load(p).catch(function() { return null; });
});
tasks.push(fs.stat('/etc/nginx/uci.conf').then(function() { return true; }).catch(function() { return false; }));
return Promise.all(tasks).then(function(res) {
var wifiSecs = (res && res) ? (uci.sections('wireless', 'wifi-device') || []) : [];
return {
hasWireless: wifiSecs.length > 0,
hasNginx: !!res[7]
};
});
},
// 2. 统一聚合读取系统当前各子模块的实时配置
getSystemConfig: function() {
var isSideRouter = !!uci.get('network', 'lan', 'gateway') && uci.get('network', 'wan', 'auto') === '0';
var ap = null;
if (this.hasWireless) {
var ifaces = uci.sections('wireless', 'wifi-iface') || [];
for (var i = 0; i < ifaces.length; i++) {
if (ifaces[i].mode === 'ap' || !ifaces[i].mode) {
ap = ifaces[i];
break;
}
}
}
// 剥离现有频段后缀,确保输入框展示纯净的基准名称
var rawSsid = (ap && ap.ssid) || (this.hasWireless ? 'Kwrt' : '');
var baseSsid = rawSsid.replace(/_(2\.4G|5G|6G)$/i, '').trim();
return {
wan_proto: isSideRouter ? 'siderouter' : (uci.get('network', 'wan', 'proto') || 'dhcp'),
wan_pppoe_user: uci.get('network', 'wan', 'username') || '',
wan_pppoe_pass: uci.get('network', 'wan', 'password') || '',
lan_ipaddr: getFirstIp(uci.get('network', 'lan', 'ipaddr'), '10.0.0.1'),
lan_gateway: getFirstIp(uci.get('network', 'lan', 'gateway'), ''),
lan_dns: toArray(uci.get('network', 'lan', 'dns')),
dhcp: uci.get('dhcp', 'lan', 'ignore') === '1' ? '0' : '1',
ipv6: uci.get('network', 'wan6', 'auto') === '0' ? '0' : '1',
https: uci.get('wizard', 'default', 'https') || '0',
cookie_p: uci.get('wizard', 'default', 'persistent_cookies') || '1',
landing_page: uci.get('wizard', 'default', 'landing_page') || 'default',
autoupgrade_fm: uci.get('wizard', 'default', 'autoupgrade_fm') || '1',
coremark: uci.get('wizard', 'default', 'coremark') || '0',
wifi_ssid: baseSsid || (this.hasWireless ? 'Kwrt' : ''),
wifi_key: (ap && ap.key) || ''
};
return data;
},
render: function(data) {
var m, s, o;
var has_wifi = false;
this.hasWireless = !!data.hasWireless;
this.hasNginx = !!data.hasNginx;
if (uci.sections('wireless', 'wifi-device').length > 0) {
has_wifi = true;
}
var sys = this.getSystemConfig();
this.initialValues = Object.assign({}, sys);
m = new form.Map('wizard', [_('Initial Router Setup')],
_('If you are using this router for the first time, please configure it here.'));
// 缓存初始 shortcuts 配置快照,用于保存时比对差异
var shortcuts = uci.sections('wizard', 'shortcuts') || [];
this.initialShortcuts = shortcuts.map(function(sec) {
return {
shortcut: sec.shortcut || '',
to_url: sec.to_url || '',
comments: sec.comments || ''
};
});
s = m.section(form.NamedSection, 'default', 'wizard');
s.addremove = false;
s.tab('netsetup', _('Net Settings'), _('Three different ways to access the Internet, please choose according to your own situation.'));
var m = new form.Map('wizard', _('Setup Wizard'),
_('Quickly configure common network, wireless, and system settings.'));
o = s.taboption('netsetup', form.ListValue, 'wan_proto', _('Protocol'));
o.rmempty = false;
o.value('dhcp', _('DHCP client'));
o.value('pppoe', _('PPPoE'));
o.value('siderouter', _('Siderouter'));
o = s.taboption('netsetup', form.Value, 'wan_pppoe_user', _('PAP/CHAP username'));
o.depends('wan_proto', 'pppoe');
o = s.taboption('netsetup', form.Value, 'wan_pppoe_pass', _('PAP/CHAP password'));
o.depends('wan_proto', 'pppoe');
o.password = true;
o = s.taboption('netsetup', form.Value, 'lan_ipaddr', _('IPv4 address'));
o.datatype = 'ip4addr';
o = s.taboption('netsetup', form.DynamicList, 'lan_dns', _('Use custom DNS servers'), _('Leave empty to use ISP DNS'));
o.datatype = 'ip4addr';
o.cast = 'string';
o = s.taboption('netsetup', form.Value, 'lan_gateway', _('IPv4 gateway'));
o.depends('wan_proto', 'siderouter');
o.datatype = 'ip4addr';
o.placeholder = _('Enter the main router IP');
o.rmempty = false;
o = s.taboption('netsetup', form.Flag, 'dhcp', _('DHCP Server'),
_('To turn on this DHCP, you need to turn off the DHCP of the main router, and to turn off this DHCP, you need to manually change the gateway and DNS of all Internet devices to the IP of this bypass router'));
o.depends('wan_proto', 'siderouter');
o.default = o.enabled;
o = s.taboption('netsetup', form.Flag, 'ipv6', _('Enable IPv6'), _('Enable/Disable IPv6'));
o.default = o.enabled;
s.tab('firmware', _('Firmware Settings'));
// o = s.taboption('firmware', form.Flag, 'autoupgrade_pkg', _('Packages Auto Upgrade'),_('谨慎开启'));
// o.rmempty = false;
o = s.taboption('firmware', form.Flag, 'autoupgrade_fm', _('Firmware Upgrade Notice'));
o.rmempty = false;
o = s.taboption('firmware', form.Flag, 'coremark', _('CoreMark'), _('第一次开机后是否运行CPU跑分测试'));
o.rmempty = false;
o = s.taboption('firmware', form.Flag, 'cookie_p', _('Persistent cookies'),
_('Keep the background login state to avoid the need to log in again every time the browser is closed'));
o.default = o.enabled;
o = s.taboption('firmware', form.Flag, 'https', _('Force the use of HTTPS in the backend.'));
if (data.istorex || data.routerdog){
o = s.taboption('firmware', form.ListValue, 'landing_page', _('主题模式'));
o.value('default', _('默认'));
if (data.routerdog){
o.value('routerdog', _('路由狗(专业NAS模式)'));
}
if (data.istorex){
o.value('nas', _('NAS模式'));
o.value('next-nas', _('NEXT-NAS模式'));
o.value('router', _('路由模式'));
}
o.default = 'default';
}
if (has_wifi) {
s.tab('wifisetup', _('Wireless Settings'), _('Set the router\'s wireless name and password. For more advanced settings, please go to the Network-Wireless page.'));
o = s.taboption('wifisetup', form.Value, 'wifi_ssid', _('<abbr title=\"Extended Service Set Identifier\">ESSID</abbr>'));
o.datatype = 'maxlength(32)';
o = s.taboption("wifisetup", form.Value, "wifi_key", _("Key"));
o.datatype = 'wpakey';
o.password = true;
}
if (data.nginx) {
s.tab('shortcuts', _('Shortcuts'), _('比如设置google.com的快捷方式为字母g,则在此路由器网络的任何浏览器中输入g/即可访问google.com'));
o = s.taboption('shortcuts', form.SectionValue, 'shortcuts', form.GridSection, 'shortcuts', null,
_('Shortcuts'));
s = o.subsection;
s.addremove = true;
var s = m.section(form.NamedSection, 'default', 'wizard');
s.anonymous = true;
s.addremove = false;
o = s.option(form.Value, 'shortcut', _('Shortcut'));
o.rmempty = false;
s.tab('netsetup', _('Net Settings'));
s.tab('firmware', _('Firmware Settings'));
if (this.hasWireless) s.tab('wifisetup', _('Wireless Settings'));
o = s.option(form.Value, 'to_url', _('Target URL'));
o.rmempty = false;
o.placeholder = 'https://example.com';
o.validate = function(section_id, value) {
if (value.match(/^https?:\/\/.+/i)) {
return true;
}
return _('Please enter a valid URL starting with http:// or https://');
};
o = s.option(form.Value, 'comments', _('Comments'));
o.optional = true;
if (this.hasNginx) {
s.tab('shortcuts', _('Shortcuts'), _('比如设置google.com的快捷方式为字母g,则在此路由器网络的任何浏览器中输入g/即可访问google.com'));
}
setTimeout("document.getElementsByClassName('cbi-button-apply')[0].children[3].children[0].value='1'", 1000)
// 3. 声明式配置项驱动表
var opt, fields = [
// 网络设置
{ tab: 'netsetup', type: form.ListValue, id: 'wan_proto', title: _('WAN Protocol / Mode'),
choices: { dhcp: _('DHCP Client (Default Router)'), pppoe: _('PPPoE Dial-up'), siderouter: _('Side-Router Mode (Bypass Gateway)') } },
{ tab: 'netsetup', type: form.Value, id: 'wan_pppoe_user', title: _('PPPoE Username'), depends: { wan_proto: 'pppoe' } },
{ tab: 'netsetup', type: form.Value, id: 'wan_pppoe_pass', title: _('PPPoE Password'), depends: { wan_proto: 'pppoe' }, password: true },
{ tab: 'netsetup', type: form.Value, id: 'lan_ipaddr', title: _('LAN IPv4 Address'), datatype: 'ip4addr', placeholder: '10.0.0.1' },
{ tab: 'netsetup', type: form.DynamicList, id: 'lan_dns', title: _('Custom DNS Server(s)'), datatype: 'ipaddr', placeholder: '223.5.5.5' },
{ tab: 'netsetup', type: form.Value, id: 'lan_gateway', title: _('Gateway Address'), datatype: 'ip4addr', placeholder: '', depends: { wan_proto: 'siderouter' }, desc: _('Primary router IP address used when operating in side-router mode.') },
{ tab: 'netsetup', type: form.Flag, id: 'dhcp', title: _('Enable DHCP Server'), desc: _('Enable or disable LAN DHCP server (usually disabled in side-router mode to avoid IP address conflicts).') },
{ tab: 'netsetup', type: form.Flag, id: 'ipv6', title: _('IPv6 Support'), desc: _('Enable or disable IPv6 router advertisements and DHCPv6.') },
// 固件与系统设置
{ tab: 'firmware', type: form.Flag, id: 'autoupgrade_fm', title: _('Firmware Upgrade Notice'), desc: _('Check and display notices for newer firmware versions.') },
{ tab: 'firmware', type: form.Flag, id: 'coremark', title: _('Run CoreMark on Boot'), desc: _('Run CPU benchmark asynchronously upon router initialization.') },
{ tab: 'firmware', type: form.Flag, id: 'cookie_p', title: _('Persistent Cookie Session'), desc: _('Maintain persistent login sessions in the web browser.') },
{ tab: 'firmware', type: form.Flag, id: 'https', title: _('Enforce HTTPS Access'), desc: _('Automatically redirect HTTP requests to secure HTTPS.') },
{ tab: 'firmware', type: form.ListValue, id: 'landing_page', title: _('Landing Dashboard Mode'),
choices: { 'default': _('Default'), routerdog: _('RouterDog'), nas: _('NAS'), 'next-nas': _('Next-NAS'), router: _('Router') } }
];
// 无线配置根据设备硬件动态追加
if (this.hasWireless) {
fields.push(
{ tab: 'wifisetup', type: form.Value, id: 'wifi_ssid', title: _('Wireless Network Name (SSID)'), placeholder: 'Kwrt' },
{ tab: 'wifisetup', type: form.Value, id: 'wifi_key', title: _('Wireless Password (Key)'), password: true, placeholder: _('Leave empty for open network or 8+ characters') }
);
}
var optMap = {};
fields.forEach(function(f) {
opt = s.taboption(f.tab, f.type, f.id, f.title, f.desc);
opt.cfgvalue = function() { return sys[f.id]; };
opt.default = sys[f.id];
opt.write = function() {};
opt.remove = function() {};
if (f.password) opt.password = true;
if (f.datatype) opt.datatype = f.datatype;
if (f.placeholder) opt.placeholder = f.placeholder;
if (f.choices) Object.keys(f.choices).forEach(function(k) { opt.value(k, f.choices[k]); });
if (f.depends) Object.keys(f.depends).forEach(function(k) { opt.depends(k, f.depends[k]); });
// 为无线密码增加 8~64 位安全校验,防止 hostapd 崩溃
if (f.id === 'wifi_key') {
opt.validate = function(section_id, value) {
if (!value) return true;
if (value.length < 8 || value.length > 64) {
return _('Wireless key must be between 8 and 64 characters long');
}
return true;
};
}
optMap[f.id] = opt;
});
// Shortcuts GridSection 渲染
if (this.hasNginx) {
var so = s.taboption('shortcuts', form.SectionValue, '_shortcuts', form.GridSection, 'shortcuts', null, _('Shortcuts'));
var ss = so.subsection;
ss.addremove = true;
ss.anonymous = true;
ss.sortable = true;
var o_sc = ss.option(form.Value, 'shortcut', _('Shortcut'));
o_sc.rmempty = false;
o_sc.placeholder = 'g';
o_sc.validate = function(section_id, value) {
if (!value)
return _('Shortcut phrase cannot be empty');
if (!value.match(/^[a-zA-Z0-9_-]+$/))
return _('Only alphanumeric characters, dashes and underscores are allowed');
return true;
};
var o_url = ss.option(form.Value, 'to_url', _('Target URL'));
o_url.rmempty = false;
o_url.placeholder = 'https://example.com';
o_url.validate = function(section_id, value) {
if (value && value.match(/^https?:\/\/.+/i)) {
return true;
}
return _('Please enter a valid URL starting with http:// or https://');
};
var o_comm = ss.option(form.Value, 'comments', _('Comments'));
o_comm.optional = true;
o_comm.placeholder = _('Optional');
}
this.map = m;
this.fields = fields;
this.optMap = optMap;
return m.render();
}
},
// 4. 重写 View 级别的 handleSave:精准写入发生变化的配置到 UCI 暂存区
handleSave: function(ev) {
var self = this;
if (!this.map) return Promise.resolve(false);
return this.map.parse().then(function() {
var cur = {};
self.fields.forEach(function(f) {
var v = self.optMap[f.id].formvalue('default');
// 对 Flag 进行布尔归一化,彻底消除 null 与 '0' 比对产生的假变动
if (f.type === form.Flag) {
cur[f.id] = (v == '1') ? '1' : '0';
} else {
cur[f.id] = (v != null) ? v : '';
}
});
cur.lan_dns = toArray(cur.lan_dns);
// 精准筛选出发生变化的常规字段
var changed = self.fields.map(function(f) { return f.id; }).filter(function(id) {
return !isEqual(cur[id], self.initialValues[id]);
});
// 检测 shortcuts 是否有增删改
var curShortcuts = (uci.sections('wizard', 'shortcuts') || []).map(function(sec) {
return {
shortcut: sec.shortcut || '',
to_url: sec.to_url || '',
comments: sec.comments || ''
};
});
var shortcutsChanged = JSON.stringify(curShortcuts) !== JSON.stringify(self.initialShortcuts);
// 未做任何改动直接提示并退出
if (changed.length === 0 && !shortcutsChanged) {
ui.addNotification(null, E('p', _('There are no changes to apply')), 'info');
return false;
}
// A. 无线配置直接写回 wireless(全频段适配:2.4G加_2.4G,5G加_5G,6G加_6G)
if (self.hasWireless && (has(changed, 'wifi_ssid') || has(changed, 'wifi_key'))) {
var rawInputSsid = (cur.wifi_ssid || '').trim();
var baseSsid = rawInputSsid.replace(/_(2\.4G|5G|6G)$/i, '').trim();
if (!baseSsid) baseSsid = rawInputSsid;
var ifaces = uci.sections('wireless', 'wifi-iface') || [];
ifaces.forEach(function(ifc) {
if (ifc.mode === 'ap' || !ifc.mode) {
var band = getRadioBand(ifc.device);
var autoSsid = baseSsid ? (baseSsid + '_' + band) : '';
if (has(changed, 'wifi_ssid') && autoSsid) {
uci.set('wireless', ifc['.name'], 'ssid', autoSsid);
}
if (has(changed, 'wifi_key')) {
if (cur.wifi_key) {
uci.set('wireless', ifc['.name'], 'key', cur.wifi_key);
var enc = ifc.encryption || uci.get('wireless', ifc['.name'], 'encryption') || '';
if (enc.indexOf('psk') === -1 && enc.indexOf('sae') === -1) {
uci.set('wireless', ifc['.name'], 'encryption', 'psk2');
}
} else {
safeUnset('wireless', ifc['.name'], 'key');
uci.set('wireless', ifc['.name'], 'encryption', 'none');
}
}
// 解除物理无线与接口禁用状态
if (ifc.device) {
safeUnset('wireless', ifc.device, 'disabled');
}
safeUnset('wireless', ifc['.name'], 'disabled');
}
});
}
// B. WAN 模式与 PPPoE(单网卡判空保护 + LAN 防火墙动态伪装清理)
if (has(changed, 'wan_proto') || has(changed, 'wan_pppoe_user') || has(changed, 'wan_pppoe_pass')) {
var zones = uci.sections('firewall', 'zone') || [];
var lanZone = null;
for (var zi = 0; zi < zones.length; zi++) {
if (zones[zi].name === 'lan') {
lanZone = zones[zi];
break;
}
}
var hasWanSec = !!uci.get('network', 'wan');
if (cur.wan_proto === 'siderouter') {
if (hasWanSec) uci.set('network', 'wan', 'auto', '0');
// 旁路由模式:开启 LAN 区域动态伪装 (masq)
if (lanZone) uci.set('firewall', lanZone['.name'], 'masq', '1');
} else {
if (hasWanSec) {
uci.set('network', 'wan', 'auto', '1');
uci.set('network', 'wan', 'proto', cur.wan_proto);
if (cur.wan_proto === 'pppoe') {
uci.set('network', 'wan', 'username', cur.wan_pppoe_user);
uci.set('network', 'wan', 'password', cur.wan_pppoe_pass);
} else {
safeUnset('network', 'wan', 'username');
safeUnset('network', 'wan', 'password');
}
}
// 切换回主路由模式:恢复清除 LAN 区域的动态伪装 (masq)
if (lanZone) safeUnset('firewall', lanZone['.name'], 'masq');
}
}
// C. LAN IP(兼容 list ipaddr / option ipaddr / CIDR 掩码)
if (has(changed, 'lan_ipaddr') && cur.lan_ipaddr) {
var origIpRaw = uci.get('network', 'lan', 'ipaddr');
var firstIp = Array.isArray(origIpRaw) ? origIpRaw[0] : origIpRaw;
var mask = '';
if (typeof firstIp === 'string') {
var slashIdx = firstIp.indexOf('/');
if (slashIdx !== -1) {
mask = '/' + firstIp.substring(slashIdx + 1).trim();
}
}
var cleanIp = (typeof cur.lan_ipaddr === 'string') ? cur.lan_ipaddr.split('/')[0].trim() : String(cur.lan_ipaddr);
var targetVal = cleanIp + mask;
if (Array.isArray(origIpRaw)) {
var newArr = origIpRaw.slice();
newArr[0] = targetVal;
uci.set('network', 'lan', 'ipaddr', newArr);
} else {
uci.set('network', 'lan', 'ipaddr', targetVal);
if (!mask && !uci.get('network', 'lan', 'netmask')) {
uci.set('network', 'lan', 'netmask', '255.255.255.0');
}
}
}
// D. 自定义 LAN DNS
if (has(changed, 'lan_dns')) {
if (cur.lan_dns && cur.lan_dns.length > 0) {
uci.set('network', 'lan', 'dns', cur.lan_dns);
} else {
safeUnset('network', 'lan', 'dns');
}
}
// E. LAN 网关(独立变更或切换至旁路由模式时更新)
if (has(changed, 'lan_gateway') || has(changed, 'wan_proto')) {
if (cur.wan_proto === 'siderouter') {
if (cur.lan_gateway) {
uci.set('network', 'lan', 'gateway', cur.lan_gateway);
} else {
safeUnset('network', 'lan', 'gateway');
}
} else {
safeUnset('network', 'lan', 'gateway');
}
}
// F. DHCP 开关(根据勾选状态精准配置,解除模式死锁冲突)
if (has(changed, 'dhcp') || has(changed, 'wan_proto')) {
if (cur.dhcp === '0') {
uci.set('dhcp', 'lan', 'ignore', '1');
} else {
safeUnset('dhcp', 'lan', 'ignore');
}
}
// G. IPv6
if (has(changed, 'ipv6')) {
var enabled = (cur.ipv6 === '1');
if (uci.get('network', 'wan6')) {
uci.set('network', 'wan6', 'auto', enabled ? '1' : '0');
}
uci.set('dhcp', 'lan', 'ra', enabled ? 'server' : 'disabled');
uci.set('dhcp', 'lan', 'dhcpv6', enabled ? 'server' : 'disabled');
uci.set('dhcp', 'lan', 'ndp', enabled ? 'server' : 'disabled');
}
// H. HTTPS 访问重定向(由 wizard.init 调度底层生效)
if (has(changed, 'https')) {
uci.set('wizard', 'default', 'https', cur.https);
}
// I. 页面与会话设置(持久Cookie双向同步至luci系统底层与向导配置包)
if (has(changed, 'cookie_p')) {
uci.set('wizard', 'default', 'persistent_cookies', cur.cookie_p);
}
if (has(changed, 'landing_page')) {
uci.set('wizard', 'default', 'landing_page', cur.landing_page);
}
// J. 向导专属项(完整保留 autoupgrade_fm 与 coremark)
if (has(changed, 'autoupgrade_fm')) {
uci.set('wizard', 'default', 'autoupgrade_fm', cur.autoupgrade_fm);
}
if (has(changed, 'coremark')) {
uci.set('wizard', 'default', 'coremark', cur.coremark);
}
self.initialValues = Object.assign({}, cur);
self.initialShortcuts = curShortcuts;
return uci.save().then(function() {
return true;
});
});
},
// 5. 重写 View 级别的 handleSaveApply:应用生效并实现 LAN IP 变更平滑迁移与自动跳转
handleSaveApply: function(ev, mode) {
var self = this;
return this.handleSave(ev).then(function(hasChanges) {
if (!hasChanges) return false;
var oldIp = self.initialValues.lan_ipaddr;
var newIp = (self.optMap['lan_ipaddr'].formvalue('default') || '').split('/')[0].trim();
var ipChanged = newIp && (newIp !== oldIp);
return ui.changes.apply(mode == '0').then(function() {
if (ipChanged) {
var sec = 15;
ui.showModal(_('LAN IP Address Changed'), [
E('p', _('LAN IP changed to %s. Redirecting in %d seconds...').format(newIp, sec)),
E('div', { 'class': 'spinning', 'style': 'margin: 1em auto;' })
]);
var timer = window.setInterval(function() {
sec--;
if (sec <= 0) {
window.clearInterval(timer);
window.location.href = window.location.protocol + '//' + newIp + window.location.pathname;
}
}, 1000);
}
});
});
},
handleReset: null
});
+1 -1
View File
@@ -22,7 +22,7 @@ function landing_page()
http.redirect(luci.dispatcher.build_url("admin","quickstart"));
end
else
http.redirect(luci.dispatcher.build_url("admin","status"))
http.redirect(luci.dispatcher.build_url("admin","status", "overview"))
end
end
+122 -57
View File
@@ -1,75 +1,140 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgid "Wizard"
msgid "Setup Wizard"
msgstr "设置向导"
msgid "Initial Router Setup"
msgstr "路由器设置向导"
msgid "Quickly configure common network, wireless, and system settings."
msgstr "快速配置常用网络、无线与系统设置。"
msgid "Net Settings"
msgstr "网络设置"
msgid "Firmware Settings"
msgstr "固件与系统"
msgid "Wireless Settings"
msgstr "无线设置"
msgid "If you are using this router for the first time, please configure it here."
msgstr "如果你首次使用这个路由器,请在这里简单设置。"
msgid "Three different ways to access the Internet, please choose according to your own situation."
msgstr "二种不同的上网方式,请您根据自身情况进行选择。"
msgid "Set the router's wireless name and password. For more advanced settings, please go to the Network-Wireless page."
msgstr "设置路由器的无线名字和密码,更多高级设置请进入 网络-无线 页面。"
msgid "Leave empty to use ISP DNS"
msgstr "留空则使用运营商DNS"
msgid "Enter the main router IP"
msgstr "请输入主路由IP"
msgid "Siderouter"
msgstr "旁路由模式"
msgid "To turn on this DHCP, you need to turn off the DHCP of the main router, and to turn off this DHCP, you need to manually change the gateway and DNS of all Internet devices to the IP of this bypass router"
msgstr "开启此DHCP则需要关闭主路由的DHCP, 关闭此DHCP则需要手动将所有上网设备的网关和DNS改为此旁路由的IP"
msgid "Enable IPv6"
msgstr "启用IPv6"
msgid "Enable/Disable IPv6"
msgstr "一键开启/关闭 IPv6"
msgid "Firmware Settings"
msgstr "固件设置"
msgid "Packages Auto Upgrade"
msgstr "软件包自动更新"
msgid "Firmware Upgrade Notice"
msgstr "固件更新提醒"
msgid "Persistent cookies"
msgstr "持久化登录"
msgid "Keep the background login state to avoid the need to log in again every time the browser is closed"
msgstr "保持后台登录状态,避免每次关闭浏览器后都需要重新登录"
msgid "Home"
msgstr "首页"
msgid "Shortcuts"
msgstr "快捷访问"
msgid "Shortcut"
msgstr "快捷方式"
msgid "Target URL"
msgstr "目标URL"
msgid "WAN Protocol / Mode"
msgstr "WAN 协议 / 工作模式"
msgid "Please enter a valid URL starting with http:// or https://"
msgstr "URL需以http:// 或 https://开头"
msgid "DHCP Client (Default Router)"
msgstr "DHCP 客户端 (标准主路由模式)"
msgid "Force the use of HTTPS in the backend."
msgstr "后台强制使用https"
msgid "PPPoE Dial-up"
msgstr "PPPoE 拨号上网"
msgid "Side-Router Mode (Bypass Gateway)"
msgstr "旁路由模式 (网关旁路)"
msgid "PPPoE Username"
msgstr "宽带拨号账号"
msgid "PPPoE Password"
msgstr "宽带拨号密码"
msgid "LAN IPv4 Address"
msgstr "LAN 口 IPv4 地址"
msgid "Custom DNS Server(s)"
msgstr "自定义 DNS 服务器,留空默认运营商DNS"
msgid "Gateway Address"
msgstr "网关地址"
msgid "Primary router IP address used when operating in side-router mode."
msgstr "处于旁路由模式时的主路由 IP 地址。"
msgid "Enable DHCP Server"
msgstr "启用 DHCP 服务"
msgid "Usually disabled in side-router mode to avoid IP address assignment conflicts with the main router."
msgstr "在旁路由模式下通常关闭此项,避免与主路由的 IP 分配冲突。"
msgid "IPv6 Support"
msgstr "IPv6 支持"
msgid "Enable or disable IPv6 router advertisements and DHCPv6."
msgstr "启用或关闭 IPv6 路由通告与 DHCPv6 服务。"
msgid "Firmware Upgrade Notice"
msgstr "固件升级提示"
msgid "Check and display notices for newer firmware versions."
msgstr "检查并显示新版本固件更新提示。"
msgid "Run CoreMark on Boot"
msgstr "开机 CoreMark 跑分"
msgid "Run CPU benchmark upon initial router initialization."
msgstr "在路由器初始化启动时运行 CPU 基准测试。"
msgid "Persistent Cookie Session"
msgstr "持久 Cookie 登录"
msgid "Maintain persistent login sessions in the web browser."
msgstr "在浏览器中保持登录会话状态,避免频繁重复登录。"
msgid "Enforce HTTPS Access"
msgstr "强制 HTTPS 访问"
msgid "Automatically redirect HTTP requests to secure HTTPS."
msgstr "将所有 HTTP 请求自动重定向至安全加密的 HTTPS。"
msgid "Landing Dashboard Mode"
msgstr "主页展示模式"
msgid "Default"
msgstr "默认"
msgid "RouterDog"
msgstr "路由狗"
msgid "NAS"
msgstr "NAS 模式"
msgid "Next-NAS"
msgstr "Next-NAS 模式"
msgid "Router"
msgstr "路由器模式"
msgid "Wireless Network Name (SSID)"
msgstr "无线网络名称 (SSID)"
msgid "Wireless Password (Key)"
msgstr "无线网络密码"
msgid "Leave empty for open network or 8+ characters"
msgstr "留空为开放无密码网络,或输入 8 位以上密码"
msgid "URL Shortcuts"
msgstr "快捷方式列表"
msgid "Applying Changes"
msgstr "正在应用修改"
msgid "Applying modified settings: %s..."
msgstr "正在应用已修改的设置项: %s..."
msgid "No settings were modified. Services remain uninterrupted."
msgstr "未检测到任何修改,无需保存或重启服务。"
msgid "Settings successfully applied: %s"
msgstr "已成功应用并重载修改项: %s"
msgid "Failed to apply settings: %s"
msgstr "应用设置失败: %s"
msgid "LAN IP Address Changed"
msgstr "LAN 口 IP 地址已变更"
msgid "LAN IP changed to %s. Redirecting to the new address in %d seconds..."
msgstr "LAN 口 IP 已变更为 %s。将在 %d 秒后自动跳转至新地址..."
msgid "Redirect Now"
msgstr "立即跳转"
+132 -243
View File
@@ -1,261 +1,150 @@
#!/bin/sh /etc/rc.common
START=19
START=99
USE_PROCD=1
extra_command "reconfig" ""
# 清理历史已生成的快捷方式(防止改名或删除后遗留失效条目)
clean_shortcuts() {
local sec
# 清理 nginx 中由 wizard 生成的 _sc_* server 节
for sec in $(uci -q show nginx 2>/dev/null | sed -n 's/^nginx\.\(_sc_[^=.]*\)=server$/\1/p'); do
uci -q delete nginx."$sec"
done
boot() {
exit 0;
}
add_wizard() {
local cfg="$1"
local wan_proto wan_pppoe_user wan_pppoe_pass
local lan_ipaddr lan_dns lan_gateway dhcp
local device ipv6 old_ipv6 wifi_ssid wifi_key old_wifi_ssid old_wifi_key
config_get wan_proto "$cfg" wan_proto
config_get lan_gateway "$cfg" lan_gateway
config_get dhcp "$cfg" dhcp
config_get lan_dns "$cfg" lan_dns
config_get ipv6 "$cfg" ipv6
config_get old_ipv6 "$cfg" old_ipv6
config_get lan_ipaddr "$cfg" lan_ipaddr
config_get autoupgrade_pkg "$cfg" autoupgrade_pkg
test -n "${lan_ipaddr}" && {
uci set network.lan.ipaddr="${lan_ipaddr}"
}
device=$(uci -q get network.wan.device || uci -q get wizard.default.old_network_device)
case "${wan_proto}" in
dhcp)
uci -q del_list network.@device[0].ports="$device"
uci -q del network.lan.gateway
uci -q del dhcp.lan.ignore
uci -q del network.lan.dns
uci -q del firewall.@zone[0].masq
uci delete network.wan
uci set network.wan=interface
uci set network.wan.device="$device"
uci set network.wan.proto='dhcp'
;;
pppoe)
config_get wan_pppoe_user "$cfg" wan_pppoe_user
config_get wan_pppoe_pass "$cfg" wan_pppoe_pass
[[ "${wan_pppoe_user} != $(uci -q get network.wan.username)" || "${wan_pppoe_pass} != $(uci -q get network.wan.password)" ]] && {
uci -q del_list network.@device[0].ports="$device"
uci -q del network.lan.gateway
uci -q del dhcp.lan.ignore
uci -q del network.lan.dns
uci -q del firewall.@zone[0].masq
uci delete network.wan
uci set network.wan=interface
uci set network.wan.proto='pppoe'
uci set network.wan.device="$device"
uci set network.wan.username="${wan_pppoe_user}"
uci set network.wan.password="${wan_pppoe_pass}"
[ -n "${ipv6}" ] && {
uci -q set network.wan.ipv6='0'
uci -q set network.wan.delegate='0'
}
}
;;
siderouter)
[ "$dhcp" == "0" ] && dhcp="1"
if [[ "${lan_gateway}" != "$(uci -q get network.lan.gateway)" || "${dhcp}" != "$(uci -q get dhcp.lan.ignore)" ]]; then
uci -q set network.lan.gateway="${lan_gateway}"
[ -n "$lan_dns" ] || lan_dns='223.5.5.5'
[ -n "$dhcp" ] && uci -q set dhcp.lan.ignore="1" || uci -q del dhcp.lan.ignore
uci -q set firewall.@zone[0].masq='1'
WAN=$(uci -q get network.wan.device)
uci -q add_list network.@device[0].ports="$WAN"
uci -q add_list network.@device[0].ports="$WAN"
uci -q set wizard.default.old_network_device="$WAN"
;;
esac
[ -n "$lan_dns" ] && {
[ "$(uci -q get dhcp.@dnsmasq[0].noresolv)" == 1 ] && {
uci -q del dhcp.@dnsmasq[0].noresolv
}
uci -q set network.wan.peerdns='0'
uci -q set network.lan.dns="${lan_dns}"
} || {
uci -q del network.wan.peerdns
uci -q del network.lan.dns
}
if [ "${ipv6}" != "${old_ipv6}" ]; then
if [ -n "${ipv6}" ]; then
uci -q delete dhcp.lan.ra
uci -q delete dhcp.lan.dhcpv6
uci -q set network.lan.delegate='0'
uci -q set network.wan.ipv6='0'
uci -q set network.wan.delegate='0'
uci -q delete network.globals.ula_prefix
else
uci -q set dhcp.lan.ra='hybrid'
uci -q set dhcp.lan.dhcpv6='hybrid'
uci -q del network.lan.delegate
uci -q set network.wan.ipv6='auto'
uci -q del network.wan.delegate
fi
uci -q set wizard.default.old_ipv6="${ipv6}"
fi
wifi_setup_radio(){
local radio=$1
uci -q get wireless.${radio} >/dev/null 2>&1 && {
if [ "$(uci -q get wireless.${radio}.band)" = "5g" ]; then
uci -q set wireless.default_${radio}.ssid="${wifi_ssid}_5G"
elif [ "$(uci -q get wireless.${radio}.band)" = "2g" ]; then
uci -q set wireless.default_${radio}.ssid="${wifi_ssid}_2.4G"
fi
uci -q set wireless.default_${radio}.device="${radio}"
if [ "${wifi_key}" ]; then
uci -q set wireless.default_${radio}.encryption='psk2'
uci -q set wireless.default_${radio}.key="${wifi_key}"
else
uci -q set wireless.default_${radio}.encryption='none'
fi
}
}
config_get wifi_ssid "$cfg" wifi_ssid
config_get wifi_key "$cfg" wifi_key
config_get old_wifi_ssid "$cfg" old_wifi_ssid
config_get old_wifi_key "$cfg" old_wifi_key
if [[ "${wifi_ssid}" != "${old_wifi_ssid}" || "${wifi_key}" != "${old_wifi_key}" ]]; then
test -n "${wifi_ssid}" && {
for radio in radio0 radio1 radio2 radio3; do
wifi_setup_radio ${radio}
done
uci commit wireless
uci -q set wizard.default.old_wifi_ssid="${wifi_ssid}"
uci -q set wizard.default.old_wifi_key="${wifi_key}"
}
fi
sethttps
uci commit wizard
uci commit dhcp
uci commit firewall
uci commit network
if [ "${autoupgrade_pkg}" != "1" ]; then
sed -i '/opkg-upgrade/d' /etc/crontabs/root
else
grep -q "opkg-upgrade" /etc/crontabs/root || {
hour="$(grep -m1 -ao '[4-6]' /dev/urandom | head -n1)"
min="$(grep -m1 -ao '[0-5][0-9]' /dev/urandom | head -n1)"
echo "$min $hour * * * . /etc/hotplug.d/online/51-opkg-upgrade >/dev/null 2>&1" >> /etc/crontabs/root
}
fi
}
reconfig() {
uci -q set wizard.default.wan_proto="$(uci -q get network.wan.proto)"
uci -q set wizard.default.wan_pppoe_user="$(uci -q get network.wan.username)"
uci -q set wizard.default.wan_pppoe_pass="$(uci -q get network.wan.password)"
uci -q set wizard.default.lan_ipaddr="$(uci -q get network.lan.ipaddr)"
uci -q set wizard.default.lan_gateway="$(uci -q get network.lan.gateway)"
uci -q set wizard.default.lan_dns="$(uci -q get network.lan.dns)"
[ "$(uci -q get dhcp.lan.ignore)" ] && uci -q set wizard.default.dhcp="0" || uci -q del wizard.default.dhcp
uci commit wizard
}
sethttps() {
config_get https "$cfg" https
if [ -n "$(command -v nginx)" ]; then
if [ -z "`uci -q get nginx._redirect2ssl`" ]; then
uci -q set nginx._redirect2ssl=server
uci -q set nginx._redirect2ssl.server_name='_redirect2ssl'
uci -q set nginx._redirect2ssl.return='302 https://$host$request_uri'
uci -q set nginx._redirect2ssl.access_log='off; # logd openwrt'
fi
if [ "x${https}" = "x1" ] ; then
uci -q del nginx.default_server.listen
uci -q add_list nginx.default_server.listen='80'
uci -q add_list nginx.default_server.listen='[::]:80'
uci -q del nginx._redirect2ssl.listen
uci -q add_list nginx._redirect2ssl.listen='80 default_server'
uci -q add_list nginx._redirect2ssl.listen='[::]:80 default_server'
else
uci -q del nginx._redirect2ssl.listen
uci -q add_list nginx._redirect2ssl.listen='80'
uci -q add_list nginx._redirect2ssl.listen='[::]:80'
uci -q del nginx.default_server.listen
uci -q add_list nginx.default_server.listen='80 default_server'
uci -q add_list nginx.default_server.listen='[::]:80 default_server'
fi
uci commit nginx
/etc/init.d/nginx reload
elif [ -n "$(command -v lighttpd)" ]; then
if [ "x${https}" = "x1" ] ; then
sed -i "s/ # url.redirect =/ url.redirect =/" /etc/lighttpd/conf.d/82-https.conf
else
sed -i "s/ url.redirect =/ # url.redirect =/" /etc/lighttpd/conf.d/82-https.conf
fi
/etc/init.d/lighttpd reload
else
if [ "x${https}" = "x1" ] ; then
uci -q set uhttpd.main.redirect_https='1'
else
uci -q set uhttpd.main.redirect_https='0'
fi
uci commit uhttpd
/etc/init.d/uhttpd reload
fi
# 清理 dhcp (dnsmasq) 中由 wizard 生成的 _sc_* domain 节
for sec in $(uci -q show dhcp 2>/dev/null | sed -n 's/^dhcp\.\(_sc_[^=.]*\)=domain$/\1/p'); do
uci -q delete dhcp."$sec"
done
}
# 处理单条 shortcut 节
process_shortcut() {
local shortcut to_url comments lanaddr
local cfg="$1"
lanaddr="$(uci get network.lan.ipaddr)"
[ -n $lanaddr ] && lanaddr="$(echo "$lanaddr"| cut -d/ -f1)"
config_get shortcut "$cfg" shortcut
config_get to_url "$cfg" to_url
config_get comments "$cfg" comments
# 获取当前 LAN 口 IP,支持 DSA 列表与 CIDR 掩码剔除
lanaddr="$(uci -q get network.lan.ipaddr | tr ' ' '\n' | head -n1)"
lanaddr="${lanaddr%%/*}"
[ -z "$lanaddr" ] && lanaddr="10.0.0.1"
uci -q set nginx._sc_$shortcut=server
uci -q set nginx._sc_$shortcut.server_name="$shortcut"
uci -q add_list nginx._sc_$shortcut.listen="80"
uci -q add_list nginx._sc_$shortcut.listen="443"
uci -q set nginx._sc_$shortcut.return="302 $to_url"
config_get shortcut "$cfg" shortcut
config_get to_url "$cfg" to_url
config_get comments "$cfg" comments
uci -q set dhcp._sc_$shortcut=domain
uci -q set dhcp._sc_$shortcut.name="$shortcut"
uci -q set dhcp._sc_$shortcut.ip="$lanaddr"
uci -q set dhcp._sc_$shortcut.comments="$comments"
# 校验必要参数
if [ -z "$shortcut" ] || [ -z "$to_url" ]; then
return 0
fi
# 1. 配置 Nginx 302 重定向 (仅在存在 /etc/nginx/uci.conf 时配置,仅监听80,禁止未配证书监听443)
if [ -f /etc/nginx/uci.conf ]; then
uci -q set nginx._sc_$shortcut=server
uci -q set nginx._sc_$shortcut.server_name="$shortcut"
uci -q add_list nginx._sc_$shortcut.listen="80"
uci -q set nginx._sc_$shortcut.return="302 $to_url"
# 2. 配置 Dnsmasq (DHCP) 本地域名劫持
uci -q set dhcp._sc_$shortcut=domain
uci -q set dhcp._sc_$shortcut.name="$shortcut"
uci -q set dhcp._sc_$shortcut.ip="$lanaddr"
[ -n "$comments" ] && uci -q set dhcp._sc_$shortcut.comments="$comments"
fi
}
start() {
config_load wizard
config_foreach add_wizard wizard
if [ -n "$(command -v nginx)" ]; then
for server in $(uci show nginx | grep '_sc_' | grep '=server' | cut -d. -f2 | cut -d= -f1); do
uci delete nginx.$server
done
for server in $(uci show dhcp | grep '_sc_' | grep '=domain' | cut -d. -f2 | cut -d= -f1); do
uci delete dhcp.$server
done
config_foreach process_shortcut shortcuts
uci commit nginx
uci commit dhcp
/etc/init.d/nginx reload
# 配置 HTTPS 访问重定向 (适配 Nginx / Lighttpd / uHTTPd)
sethttps() {
local cfg="${1:-default}"
local https
config_get https "$cfg" https 0
if [ -x /etc/init.d/nginx ] && [ -f /etc/nginx/uci.conf ]; then
if [ "x${https}" = "x1" ]; then
if [ -z "$(uci -q get nginx._redirect2ssl)" ]; then
uci -q set nginx._redirect2ssl=server
uci -q set nginx._redirect2ssl.server_name='_redirect2ssl'
uci -q set nginx._redirect2ssl.return='302 https://$host$request_uri'
uci -q set nginx._redirect2ssl.access_log='off; # logd openwrt'
fi
uci -q del nginx.default_server.listen
uci -q add_list nginx.default_server.listen='80'
uci -q add_list nginx.default_server.listen='[::]:80'
uci -q del nginx._redirect2ssl.listen
uci -q add_list nginx._redirect2ssl.listen='80 default_server'
uci -q add_list nginx._redirect2ssl.listen='[::]:80 default_server'
else
uci -q delete nginx._redirect2ssl
uci -q del nginx.default_server.listen
uci -q add_list nginx.default_server.listen='80 default_server'
uci -q add_list nginx.default_server.listen='[::]:80 default_server'
fi
(
/etc/init.d/network restart
/etc/init.d/firewall reload
/etc/init.d/dnsmasq reload
)&
uci -q commit nginx
/etc/init.d/nginx reload >/dev/null 2>&1
elif [ -x /etc/init.d/lighttpd ] && [ -f /etc/lighttpd/conf.d/82-https.conf ]; then
if [ "x${https}" = "x1" ]; then
sed -i "s/^[ \t]*#*[ \t]*url\.redirect[ \t]*=.*/\turl.redirect =/" /etc/lighttpd/conf.d/82-https.conf 2>/dev/null
else
sed -i "s/^[ \t]*url\.redirect[ \t]*=.*/\t# url.redirect =/" /etc/lighttpd/conf.d/82-https.conf 2>/dev/null
fi
/etc/init.d/lighttpd reload >/dev/null 2>&1
elif [ -x /etc/init.d/uhttpd ]; then
if [ "x${https}" = "x1" ]; then
uci -q set uhttpd.main.redirect_https='1'
else
uci -q set uhttpd.main.redirect_https='0'
fi
uci -q commit uhttpd
/etc/init.d/uhttpd reload >/dev/null 2>&1
fi
}
restart() {
start
# 提交配置并平滑重载底层服务
reload_services() {
uci -q commit dhcp
if [ -x /etc/init.d/dnsmasq ]; then
/etc/init.d/dnsmasq reload >/dev/null 2>&1
fi
if [ -f /etc/nginx/uci.conf ] && [ -x /etc/init.d/nginx ]; then
uci -q commit nginx
/etc/init.d/nginx reload >/dev/null 2>&1
fi
}
start_service() {
config_load wizard
# 1. 开机 CoreMark 跑分处理 (后台异步执行,杜绝阻塞系统启动与前端保存卡死)
local coremark
config_get coremark default coremark 0
if [ "$coremark" = "1" ] && [ -x /bin/coremark ] && [ -x /sbin/coremark ] && [ ! -f /etc/bench.log ] && [ ! -f /var/lock/opkg.lock ]; then
(sleep 5;/sbin/coremark >/dev/null 2>&1) &
fi
# 2. 持久 Cookie 会话配置同步到 LuCI 底层系统
local cookie_p
config_get cookie_p default persistent_cookies 1
if [ -n "$cookie_p" ]; then
[ -z "$(uci -q get luci.sgi)" ] && uci -q set luci.sgi=sgi
uci -q set luci.sgi.persistent_cookies="$cookie_p"
uci -q commit luci
fi
# 3. 清理并重新配置所有 Shortcuts
clean_shortcuts
config_foreach process_shortcut shortcuts
# 4. 设置 HTTPS 重定向
sethttps default
# 5. 提交并重载生效服务
reload_services
}
stop_service() {
clean_shortcuts
reload_services
}
service_triggers() {
procd_add_reload_trigger "wizard"
}
@@ -1,15 +0,0 @@
#!/bin/sh
touch /etc/config/wizard
uci -q batch <<-EOF >/dev/null
delete ucitrack.@wizard[-1]
add ucitrack wizard
set ucitrack.@wizard[-1].init=wizard
commit ucitrack
EOF
chmod +x /etc/init.d/wizard 2>/dev/null
rm -f /tmp/luci-indexcache
exit 0
@@ -1,7 +1,7 @@
{
"admin/system/initsetup": {
"title": "Wizard",
"order": 1,
"admin/system/wizard": {
"title": "Setup Wizard",
"order": 10,
"action": {
"type": "view",
"path": "wizard/initsetup"
@@ -1,14 +1,30 @@
{
"luci-app-wizard": {
"description": "Grant UCI access for luci-app-wizard",
"description": "Grant UCI and filesystem access for LuCI Setup Wizard",
"read": {
"uci": [
"network",
"wireless",
"dhcp",
"uhttpd",
"firewall",
"luci",
"wizard"
],
"file": {
"/etc/init.d/wizard reconfig": [ "exec" ]
},
"uci": [ "wizard", "wireless" ]
"/etc/nginx/uci.conf": [ "stat" ]
}
},
"write": {
"uci": [ "wizard" ]
"uci": [
"network",
"wireless",
"dhcp",
"uhttpd",
"firewall",
"luci",
"wizard"
]
}
}
}
@@ -5,7 +5,7 @@
sleep 5
while :; do
[ -f /var/lock/opkg.lock ] || {
/sbin/cpumark
/sbin/coremark
break;
}
sleep 1