diff --git a/bitsrunlogin-go/Makefile b/bitsrunlogin-go/Makefile index fe4335210..e304159f4 100644 --- a/bitsrunlogin-go/Makefile +++ b/bitsrunlogin-go/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=BitSrunLoginGo -PKG_VERSION:=1.6.7 -PKG_RELEASE:=6 +PKG_VERSION:=1.6.8 +PKG_RELEASE:=7 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/Mmx233/BitSrunLoginGo/tar.gz/v$(PKG_VERSION)? diff --git a/dae/Makefile b/dae/Makefile index 73398689d..33f29905f 100644 --- a/dae/Makefile +++ b/dae/Makefile @@ -5,10 +5,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dae -PKG_VERSION:=2026.09.12 -PKG_RELEASE:=44 +PKG_VERSION:=2026.09.18 +PKG_RELEASE:=45 -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:=skip diff --git a/dae/patches/010-dns-response-ttl.patch b/dae/patches/010-dns-response-ttl.patch index 6f67538c6..6b869b7a2 100644 --- a/dae/patches/010-dns-response-ttl.patch +++ b/dae/patches/010-dns-response-ttl.patch @@ -1,135 +1,539 @@ -diff --git a/cmd/reload_manager.go b/cmd/reload_manager.go -index c78910c..bcc1a37 100644 ---- a/cmd/reload_manager.go -+++ b/cmd/reload_manager.go -@@ -423,5 +423,8 @@ func dnsConfigFingerprint(dns config.Dns) string { - b.WriteString("max_cache_size=") - b.WriteString(strconv.Itoa(dns.MaxCacheSize)) - b.WriteByte(';') -+ b.WriteString("response_ttl=") -+ b.WriteString(strconv.Itoa(dns.ResponseTtl)) -+ b.WriteByte(';') - return b.String() - } diff --git a/cmd/run_shutdown_test.go b/cmd/run_shutdown_test.go -index f8f731f..4dcdcff 100644 +index 66c99238fdd4cfb552ae7b515bde2e7f0ec6098e..31719b3f64df5ba93d79b67d30e0293a16c613fb 100644 --- a/cmd/run_shutdown_test.go +++ b/cmd/run_shutdown_test.go -@@ -609,6 +609,7 @@ func TestDNSConfigFingerprintCoversAllDnsFields(t *testing.T) { - "OptimisticCache": {}, - "OptimisticCacheTtl": {}, - "MaxCacheSize": {}, -+ "ResponseTtl": {}, +@@ -857,6 +857,7 @@ func TestDNSConfigFingerprintCoversAllDnsFields(t *testing.T) { + "OptimisticCacheTtl": {}, + "OptimisticStaleReplyTtl": {}, + "MaxCacheSize": {}, ++ "ResponseTtl": {}, } - dnsType := reflect.TypeOf(config.Dns{}) + dnsType := reflect.TypeFor[config.Dns]() diff --git a/config/config.go b/config/config.go -index e688e88..af28da5 100644 +index cc2f7f0e02b63ca49cc59c0eceb2c2b0ce55c257..b1b13f12fe2ceb6f54163304a4a7c8ca7fd1bcea 100644 --- a/config/config.go +++ b/config/config.go -@@ -160,6 +160,7 @@ type Dns struct { - OptimisticCache bool `mapstructure:"optimistic_cache" default:"true"` - OptimisticCacheTtl int `mapstructure:"optimistic_cache_ttl" default:"60"` - MaxCacheSize int `mapstructure:"max_cache_size" default:"0"` -+ ResponseTtl int `mapstructure:"response_ttl" default:"0"` +@@ -130,7 +130,7 @@ type KeyableString string + // Dns is intentionally mirrored by cmd.dnsConfigFingerprint for staged reload + // DNS reuse decisions. Only routing-affecting fields are covered by the + // fingerprint; runtime-tunable parameters (OptimisticCache, OptimisticCacheTtl, +-// MaxCacheSize) are excluded because they are hot-updated via ++// MaxCacheSize, ResponseTtl) are excluded because they are hot-updated via + // DnsController.UpdateRuntime. Keep the fingerprint in sync with any new + // routing-affecting top-level fields; TestDNSConfigFingerprintCoversAllDnsFields + // guards the contract. +@@ -146,6 +146,7 @@ type Dns struct { + // 8767) response is served. 0 keeps the previously packed TTL. + OptimisticStaleReplyTtl int `mapstructure:"optimistic_stale_reply_ttl" default:"30"` + MaxCacheSize int `mapstructure:"max_cache_size" default:"65536"` ++ ResponseTtl int `mapstructure:"response_ttl" default:"0"` } type Routing struct { diff --git a/config/desc.go b/config/desc.go -index 513cf7e..9b3c1a2 100644 +index b61f4ca30286619ea34b47dfc592d50f8c549c28..61a5d1ea65a75e46f580c09faec02e26b73adf74 100644 --- a/config/desc.go +++ b/config/desc.go @@ -66,6 +66,7 @@ var GlobalDesc = Desc{ var DnsDesc = Desc{ "ipversion_prefer": "For example, if ipversion_prefer is 4 and the domain name has both type A and type AAAA records, the dae will only respond to type A queries and response empty answer to type AAAA queries.", "fixed_domain_ttl": "Give a fixed ttl for domains. Zero means that dae will request to upstream every time and not cache DNS results for these domains.", -+ "response_ttl": "Override the TTL returned to downstream DNS clients. Zero keeps dae's default behavior.", ++ "response_ttl": "Override the TTL of A and AAAA answers returned to downstream DNS clients. Zero keeps dae's default behavior.", "upstream": "Value can be scheme://host:port, where the scheme can be tcp/udp/tcp+udp.\nIf host is a domain and has both IPv4 and IPv6 record, dae will automatically choose IPv4 or IPv6 to use according to group policy (such as min latency policy).\nPlease make sure DNS traffic will go through and be forwarded by dae, which is REQUIRED for domain routing.\nIf dial_mode is \"ip\", the upstream DNS answer SHOULD NOT be polluted, so domestic public DNS is not recommended.", "request": `DNS request routing for ordinary client traffic uses qname and qtype. Built-in outbounds for ordinary DNS requests: asis, reject. diff --git a/control/control_plane.go b/control/control_plane.go -index 5df98db..a543f3b 100644 +index e420286bbcab1832bc4f0dc8e1e5b05a03badfeb..eb41072ec7af5171ef356ae5160b9f279fd9f88a 100644 --- a/control/control_plane.go +++ b/control/control_plane.go -@@ -771,6 +771,7 @@ func newControlPlaneWithContextOptions( - } - plane.dnsRouting = dnsUpstream - plane.dnsFixedDomainTtl = fixedDomainTtl +@@ -918,6 +918,7 @@ func NewControlPlaneWithContextOptions( + plane.dnsOptimisticStaleReplyTtl = dnsConfig.OptimisticStaleReplyTtl + plane.dnsMaxCacheSize = dnsConfig.MaxCacheSize + plane.dnsIpVersionPrefer = dnsConfig.IpVersionPrefer + plane.dnsResponseTtl = dnsConfig.ResponseTtl - dnsControllerOption := plane.dnsControllerOption() - dnsControllerOption.OptimisticCache = dnsConfig.OptimisticCache - dnsControllerOption.OptimisticCacheTtl = dnsConfig.OptimisticCacheTtl -@@ -1256,6 +1257,7 @@ func (c *ControlPlane) dnsControllerOption() *DnsControllerOption { - Log: c.log, - LifecycleContext: c.ctx, - ConcurrencyLimit: 0, -+ ResponseTtl: c.dnsResponseTtl, - CacheAccessCallback: func(cache *DnsCache) (err error) { - if err = c.core.BatchUpdateDomainRouting(cache); err != nil { - return fmt.Errorf("BatchUpdateDomainRouting: %w", err) -diff --git a/control/dns_control.go b/control/dns_control.go -index d5bf872..b52132d 100644 ---- a/control/dns_control.go -+++ b/control/dns_control.go -@@ -82,6 +82,7 @@ type DnsControllerOption struct { - OptimisticCache bool - OptimisticCacheTtl int // 0 means never expire (rely on LRU eviction) - MaxCacheSize int // maximum number of cache entries (0 = unlimited) -+ ResponseTtl int + plane.dnsController, err = NewDnsController(dnsUpstream, plane.dnsControllerOption()) + if err != nil { + return nil, err +@@ -1293,6 +1294,7 @@ func (c *ControlPlane) dnsControllerOption() *DnsControllerOption { + OptimisticStaleReplyTtl: c.dnsOptimisticStaleReplyTtl, + MaxCacheSize: c.dnsMaxCacheSize, + IpVersionPrefer: c.dnsIpVersionPrefer, ++ ResponseTtl: c.dnsResponseTtl, + } } - type dnsControllerRuntimeState struct { -@@ -94,6 +95,7 @@ type dnsControllerRuntimeState struct { - bestDialerChooser func(ctx context.Context, req *udpRequest, upstream *dns.Upstream) (*dialArgument, error) - timeoutExceedCallback func(dialArgument *dialArgument, err error) - fixedDomainTtl map[string]int -+ responseTtl int +diff --git a/control/dns_cache.go b/control/dns_cache.go +index 5375997a257fc7106aa61419f2a35764a87da574..a2c564f6900a62b5fb5e240ec544ab305554035b 100644 +--- a/control/dns_cache.go ++++ b/control/dns_cache.go +@@ -35,6 +35,7 @@ const ( + type dnsPackedResponse struct { + wire []byte + ttl uint32 ++ responseTtl int + createdAtUnixNano int64 + } + +@@ -266,10 +267,27 @@ func setSectionTTL(rrs []dnsmessage.RR, ttl uint32, scratch []uint32) { + } + } + +-// copySectionWithTTL deep-copies a record section and stamps the remaining +-// lifetime onto every record. This is the single place where the "materialize a +-// stored section onto the wire at a given remaining TTL" policy lives, so the +-// pre-packed path and the in-place fallback cannot drift apart. ++func addressAnswerTTL(rr dnsmessage.RR, ttl uint32, responseTtl int) uint32 { ++ if responseTtl > 0 { ++ switch rr.Header().Rrtype { ++ case dnsmessage.TypeA, dnsmessage.TypeAAAA: ++ return uint32(responseTtl) ++ } ++ } ++ return ttl ++} ++ ++func setAnswerSectionTTL(rrs []dnsmessage.RR, ttl uint32, responseTtl int, scratch []uint32) { ++ for i, rr := range rrs { ++ scratch[i] = rr.Header().Ttl ++ setRecordTTL(rr, addressAnswerTTL(rr, ttl, responseTtl)) ++ } ++} ++ ++// copySectionWithTTL deep-copies a non-answer record section and stamps the ++// remaining lifetime onto every record. Answer sections use ++// copyAnswerSectionWithTTL so an optional downstream A/AAAA override can be ++// applied without changing CNAMEs, authority/additional records, or OPT flags. + // + // Answers carry a real TTL: the first answer carries the TTL the upstream + // returned, a cache hit the remaining lifetime of the entry. dae tracks +@@ -278,13 +296,17 @@ func setSectionTTL(rrs []dnsmessage.RR, ttl uint32, scratch []uint32) { + // rewrite a lifetime to zero. The EDNS OPT pseudo-record is never treated as a + // TTL (see setRecordTTL). + func copySectionWithTTL(rrs []dnsmessage.RR, ttl uint32) []dnsmessage.RR { ++ return copyAnswerSectionWithTTL(rrs, ttl, 0) ++} ++ ++func copyAnswerSectionWithTTL(rrs []dnsmessage.RR, ttl uint32, responseTtl int) []dnsmessage.RR { + if rrs == nil { + return nil + } + copied := make([]dnsmessage.RR, len(rrs)) + for i, rr := range rrs { + record := dnsmessage.Copy(rr) +- setRecordTTL(record, ttl) ++ setRecordTTL(record, addressAnswerTTL(record, ttl, responseTtl)) + copied[i] = record + } + return copied +@@ -302,6 +324,10 @@ func restoreSectionTTL(rrs []dnsmessage.RR, scratch []uint32) { + // returning. This preserves the stored RR values while avoiding deep copies on + // the cold cache-insert path. + func (c *DnsCache) prepackResponseBeforeStore(qname string, qtype uint16, ttl uint32, now time.Time) error { ++ return c.prepackResponseBeforeStoreWithResponseTtl(qname, qtype, ttl, 0, now) ++} ++ ++func (c *DnsCache) prepackResponseBeforeStoreWithResponseTtl(qname string, qtype uint16, ttl uint32, responseTtl int, now time.Time) error { + var question [1]dnsmessage.Question + question[0] = dnsmessage.Question{Name: qname, Qtype: qtype, Qclass: dnsmessage.ClassINET} + +@@ -329,7 +355,7 @@ func (c *DnsCache) prepackResponseBeforeStore(qname string, qtype uint16, ttl ui + nsTTLs := ttlScratchSlice(len(c.NS), &nsStack) + extraTTLs := ttlScratchSlice(len(c.Extra), &extraStack) + +- setSectionTTL(c.Answer, ttl, answerTTLs) ++ setAnswerSectionTTL(c.Answer, ttl, responseTtl, answerTTLs) + setSectionTTL(c.NS, ttl, nsTTLs) + setSectionTTL(c.Extra, ttl, extraTTLs) + defer func() { +@@ -346,6 +372,7 @@ func (c *DnsCache) prepackResponseBeforeStore(qname string, qtype uint16, ttl ui + c.packedResponse.Store(&dnsPackedResponse{ + wire: packed, + ttl: ttl, ++ responseTtl: responseTtl, + createdAtUnixNano: now.UnixNano(), + }) + // deadlineNano gates the packed fast path and stale-while-revalidate +@@ -360,6 +387,10 @@ func (c *DnsCache) prepackResponseBeforeStore(qname string, qtype uint16, ttl ui + // OPTIMIZED: Uses Copy-on-Write with atomic pointer swap for thread-safe updates. + // Creates a new []byte slice and atomically swaps the pointer - no blocking readers. + func (c *DnsCache) prepackResponseWithTTL(qname string, qtype uint16, ttl uint32, now time.Time) error { ++ return c.prepackResponseWithTTLAndResponseTtl(qname, qtype, ttl, 0, now) ++} ++ ++func (c *DnsCache) prepackResponseWithTTLAndResponseTtl(qname string, qtype uint16, ttl uint32, responseTtl int, now time.Time) error { + msg := &dnsmessage.Msg{ + MsgHdr: dnsmessage.MsgHdr{ + Rcode: dnsmessage.RcodeSuccess, +@@ -374,7 +405,7 @@ func (c *DnsCache) prepackResponseWithTTL(qname string, qtype uint16, ttl uint32 + Compress: true, + } + +- msg.Answer = copySectionWithTTL(c.Answer, ttl) ++ msg.Answer = copyAnswerSectionWithTTL(c.Answer, ttl, responseTtl) + msg.Ns = copySectionWithTTL(c.NS, ttl) + msg.Extra = copySectionWithTTL(c.Extra, ttl) + +@@ -386,6 +417,7 @@ func (c *DnsCache) prepackResponseWithTTL(qname string, qtype uint16, ttl uint32 + c.packedResponse.Store(&dnsPackedResponse{ + wire: packed, + ttl: ttl, ++ responseTtl: responseTtl, + createdAtUnixNano: now.UnixNano(), + }) + return nil +@@ -399,6 +431,10 @@ func (c *DnsCache) prepackResponseWithTTL(qname string, qtype uint16, ttl uint32 + // PERFORMANCE: Eliminates deep copy + Pack() bottleneck. 10-100x faster for cache hits. + // NOTE: Only returns fresh (unexpired) responses. For stale responses, use GetStaleResponse. + func (c *DnsCache) GetPackedResponseWithApproximateTTL(qname string, qtype uint16, now time.Time) []byte { ++ return c.getPackedResponseWithApproximateTTL(qname, qtype, 0, now) ++} ++ ++func (c *DnsCache) getPackedResponseWithApproximateTTL(qname string, qtype uint16, responseTtl int, now time.Time) []byte { + nowNano := now.UnixNano() + deadlineNano := c.deadlineNano.Load() + +@@ -416,7 +452,7 @@ func (c *DnsCache) GetPackedResponseWithApproximateTTL(qname string, qtype uint1 + // Lock-free read: one load observes wire bytes and TTL metadata from the + // same immutable publication. + packed := c.packedResponse.Load() +- if packed != nil && packed.wire != nil { ++ if packed != nil && packed.wire != nil && packed.responseTtl == responseTtl { + if packed.ttl >= currentTTL { + if packed.ttl-currentTTL <= ttlRefreshThresholdSeconds { + return packed.wire +@@ -429,17 +465,18 @@ func (c *DnsCache) GetPackedResponseWithApproximateTTL(qname string, qtype uint1 + // The separate flag is only a best-effort refresh admission gate; it does + // not describe the published response and therefore cannot create a mixed + // response generation. +- if (packed == nil || nowNano-packed.createdAtUnixNano > 1e9) && c.packedResponseRefreshing.CompareAndSwap(false, true) { ++ policyChanged := packed != nil && packed.responseTtl != responseTtl ++ if (packed == nil || policyChanged || nowNano-packed.createdAtUnixNano > 1e9) && c.packedResponseRefreshing.CompareAndSwap(false, true) { + current := c.packedResponse.Load() +- if current == nil || nowNano-current.createdAtUnixNano > 1e9 { +- _ = c.prepackResponseWithTTL(qname, qtype, currentTTL, now) ++ if current == nil || current.responseTtl != responseTtl || nowNano-current.createdAtUnixNano > 1e9 { ++ _ = c.prepackResponseWithTTLAndResponseTtl(qname, qtype, currentTTL, responseTtl, now) + } + c.packedResponseRefreshing.Store(false) + } + + // Return the latest complete response (it may have a slightly stale TTL). + packed = c.packedResponse.Load() +- if packed == nil { ++ if packed == nil || packed.responseTtl != responseTtl { + return nil + } + return packed.wire +@@ -451,6 +488,10 @@ func (c *DnsCache) GetPackedResponseWithApproximateTTL(qname string, qtype uint1 + // Returns nil if cache is too stale (beyond staleTtl seconds). + // Caller should check refreshing flag and trigger background refresh if needed. + func (c *DnsCache) GetStaleResponse(now time.Time, staleTtl int) []byte { ++ return c.getStaleResponseWithResponseTtl("", 0, now, staleTtl, 0) ++} ++ ++func (c *DnsCache) getStaleResponseWithResponseTtl(qname string, qtype uint16, now time.Time, staleTtl int, responseTtl int) []byte { + nowNano := now.UnixNano() + deadlineNano := c.deadlineNano.Load() + +@@ -474,6 +515,18 @@ func (c *DnsCache) GetStaleResponse(now time.Time, staleTtl int) []byte { + if packed == nil { + return nil + } ++ if packed.responseTtl != responseTtl { ++ // Keep the published stale snapshot's record TTLs for non-address ++ // records. Only the runtime-tunable A/AAAA policy changes here; the ++ // caller applies the independent optimistic stale reply cap below. ++ if err := c.prepackResponseWithTTLAndResponseTtl(qname, qtype, packed.ttl, responseTtl, now); err != nil { ++ return nil ++ } ++ packed = c.packedResponse.Load() ++ if packed == nil || packed.responseTtl != responseTtl { ++ return nil ++ } ++ } + return packed.wire + } + +@@ -495,6 +548,10 @@ func (c *DnsCache) MarkRefreshed() { + // of degrading into a silent cache miss: the caller turns it into an operator + // visible warning and then resolves the name upstream. + func (c *DnsCache) fillIntoWithTTLInPlace(req *dnsmessage.Msg, now time.Time) ([]byte, error) { ++ return c.fillIntoWithTTLAndResponseTtlInPlace(req, now, 0) ++} ++ ++func (c *DnsCache) fillIntoWithTTLAndResponseTtlInPlace(req *dnsmessage.Msg, now time.Time, responseTtl int) ([]byte, error) { + if req == nil { + return nil, nil + } +@@ -504,21 +561,14 @@ func (c *DnsCache) fillIntoWithTTLInPlace(req *dnsmessage.Msg, now time.Time) ([ + req.RecursionAvailable = true + req.Truncated = false + +- if c.Answer == nil { +- req.Compress = true +- resp, err := req.Pack() +- if err != nil { +- return nil, fmt.Errorf("pack cached DNS response without answer: %w", err) +- } +- return resp, nil +- } +- + // Calculate remaining TTL based on the provided time + remainingTTL := ttlFromDeadline(c.Deadline, now) + + // Copy answers with updated TTL. Shared with the pre-packed path so both + // materializations apply the same record-level TTL policy. +- req.Answer = copySectionWithTTL(c.Answer, remainingTTL) ++ req.Answer = copyAnswerSectionWithTTL(c.Answer, remainingTTL, responseTtl) ++ req.Ns = copySectionWithTTL(c.NS, remainingTTL) ++ req.Extra = copySectionWithTTL(c.Extra, remainingTTL) + + req.Compress = true + resp, err := req.Pack() +diff --git a/control/dns_control.go b/control/dns_control.go +index 80ef962dd74735ede69d5aa852afb5f38d899745..77a9ec0e582b3845b39c00b71077c0603a9c15a2 100644 +--- a/control/dns_control.go ++++ b/control/dns_control.go +@@ -92,6 +92,7 @@ type DnsControllerOption struct { + // answers (RFC 8767); 0 keeps the previously packed TTL. + OptimisticStaleReplyTtl int + MaxCacheSize int // maximum number of cache entries (0 = unlimited) ++ ResponseTtl int // downstream A/AAAA answer TTL override (0 = unchanged) } type dnsControllerStore struct { -@@ -170,6 +172,9 @@ func normalizeDnsRuntimeBehavior(option *DnsControllerOption) (qtypePrefer uint1 - if err != nil { - return 0, false, 0, 0, err - } -+ if option.ResponseTtl < 0 { -+ return 0, false, 0, 0, fmt.Errorf("response_ttl must be greater than or equal to 0") +diff --git a/control/dns_controller_cache.go b/control/dns_controller_cache.go +index b2a0d1d2279060957c82bac28c69e51793c7083a..a70b50650e7f9bd1ad69186236f821898fa5df66 100644 +--- a/control/dns_controller_cache.go ++++ b/control/dns_controller_cache.go +@@ -984,6 +984,26 @@ func (c *DnsController) startDnsCacheJanitor() { + // TTL is refreshed when difference exceeds ttlRefreshThresholdSeconds (15 seconds by default). + // OPTIMISTIC CACHE (RFC 8767): Returns stale response while background refresh is in progress. + // Falls back to an owned in-place TTL-aware pack if pre-packed response is not available. ++func applyResponseTtlOverride(msg *dnsmessage.Msg, responseTtl int) { ++ if msg == nil || responseTtl <= 0 { ++ return + } - optimisticCacheTtl = option.OptimisticCacheTtl - maxCacheSize = option.MaxCacheSize - if optimisticCacheTtl == 0 && maxCacheSize == 0 { -@@ -422,6 +427,7 @@ func (c *DnsController) updateRuntime(option *DnsControllerOption, routing *dns. - bestDialerChooser: option.BestDialerChooser, - timeoutExceedCallback: option.TimeoutExceedCallback, - fixedDomainTtl: option.FixedDomainTtl, -+ responseTtl: option.ResponseTtl, - }) - return nil - } -@@ -1547,10 +1553,16 @@ func (c *DnsController) NormalizeAndCacheDnsResp_(msg *dnsmessage.Msg, responseC - ttl = 31536000 - } - -- // For A/AAAA records, we set TTL to 0 to prevent downstream caching while we manage it. -+ responseTtl := 0 -+ if rt := c.runtime(); rt != nil { -+ responseTtl = rt.responseTtl ++ answer := append([]dnsmessage.RR(nil), msg.Answer...) ++ changed := false ++ for i, rr := range answer { ++ switch rr.Header().Rrtype { ++ case dnsmessage.TypeA, dnsmessage.TypeAAAA: ++ record := dnsmessage.Copy(rr) ++ record.Header().Ttl = uint32(responseTtl) ++ answer[i] = record ++ changed = true ++ } + } ++ if changed { ++ msg.Answer = answer ++ } ++} + -+ // For A/AAAA records, we set TTL to 0 by default to prevent downstream caching while we manage it. -+ // When response_ttl is set, use it as the downstream-visible TTL. - if q.Qtype == dnsmessage.TypeA || q.Qtype == dnsmessage.TypeAAAA { - for i := range msg.Answer { -- msg.Answer[i].Header().Ttl = 0 -+ msg.Answer[i].Header().Ttl = uint32(responseTtl) + func (c *DnsController) LookupDnsRespCache_(msg *dnsmessage.Msg, cacheKey string, ignoreFixedTtl bool) (resp []byte, needRefresh bool) { + c.requireStore() + // Load cache directly without expiry check (to support optimistic cache) +@@ -994,6 +1014,7 @@ func (c *DnsController) LookupDnsRespCache_(msg *dnsmessage.Msg, cacheKey string + cache := val.(*DnsCache) + + now := time.Now() ++ responseTtl := c.currentResponseTtl() + + // Update last access time for LRU eviction (atomic operation) + cache.lastAccessNano.Store(now.UnixNano()) +@@ -1016,7 +1037,7 @@ func (c *DnsController) LookupDnsRespCache_(msg *dnsmessage.Msg, cacheKey string + qtype = msg.Question[0].Qtype } + +- if resp := cache.GetPackedResponseWithApproximateTTL(qname, qtype, now); resp != nil { ++ if resp := cache.getPackedResponseWithApproximateTTL(qname, qtype, responseTtl, now); resp != nil { + // Fresh cache hit - return immediately + // Trigger async BPF update if needed + c.triggerBpfUpdateIfNeeded(cache, now) +@@ -1028,7 +1049,7 @@ func (c *DnsController) LookupDnsRespCache_(msg *dnsmessage.Msg, cacheKey string + // to mutate it in place, so this avoids the extra request copy on the + // remaining TTL-aware cache-hit fallback. A pack failure degrades into an + // upstream lookup, so it must be visible instead of silent. +- resp, packErr := cache.fillIntoWithTTLInPlace(msg, now) ++ resp, packErr := cache.fillIntoWithTTLAndResponseTtlInPlace(msg, now, responseTtl) + if packErr != nil { + if c.log != nil { + c.log.Warnf("failed to pack cached DNS response for %q, falling back to an upstream lookup: %v", cacheKey, packErr) +@@ -1046,7 +1067,13 @@ func (c *DnsController) LookupDnsRespCache_(msg *dnsmessage.Msg, cacheKey string + if optimisticCacheEnabled { + // Try stale response (RFC 8767) + // Use optimisticCacheTtl (0 means never expire) +- if resp = cache.GetStaleResponse(now, optimisticCacheTtl); resp != nil { ++ var qname string ++ var qtype uint16 ++ if len(msg.Question) > 0 { ++ qname = msg.Question[0].Name ++ qtype = msg.Question[0].Qtype ++ } ++ if resp = cache.getStaleResponseWithResponseTtl(qname, qtype, now, optimisticCacheTtl, responseTtl); resp != nil { + // Within stale window - return stale response and trigger background refresh + // Use CAS to ensure only one goroutine triggers refresh + if cache.refreshing.CompareAndSwap(false, true) { +@@ -1225,7 +1252,9 @@ func (c *DnsController) NormalizeAndCacheDnsResp_(msg *dnsmessage.Msg, responseC + // and the stale window, not by suppressing downstream caching. + + // Update DnsCache. +- return c.updateDnsCache(msg, responseCacheKey, ttl, &q) ++ err = c.updateDnsCache(msg, responseCacheKey, ttl, &q) ++ applyResponseTtlOverride(msg, c.currentResponseTtl()) ++ return err + } + + // dnsCacheStoreFailureLogInterval paces the failed-cache-store warning. A +@@ -1309,7 +1338,7 @@ func (c *DnsController) updateDnsCacheDeadline(cacheKey string, host string, dns + // Pre-pack before publication so cache readers only observe a complete + // entry. The cache/runtime locks below make the entry, its projection, + // and reload epoch one atomic publication unit. +- if err = newCache.prepackResponseBeforeStore(fqdn, dnsTyp, ttlFromDeadline(deadline, now), now); err != nil { ++ if err = newCache.prepackResponseBeforeStoreWithResponseTtl(fqdn, dnsTyp, ttlFromDeadline(deadline, now), rt.responseTtl, now); err != nil { + if c.log != nil { + c.log.Warnf("failed to prepack DNS response: %v", err) + } +diff --git a/control/dns_controller_forwarder.go b/control/dns_controller_forwarder.go +index ce74a8493e5185d3e2570b8b84f7122a1b119341..d506cd87d2af82dedf3e52a18ab08cd2ce0b9b40 100644 +--- a/control/dns_controller_forwarder.go ++++ b/control/dns_controller_forwarder.go +@@ -557,7 +557,12 @@ func (c *DnsController) dialSend( + // Pack into a pooled DNS response buffer; data is consumed synchronously by the send. + bufPtr := dnsResponseBufPool.Get().(*[]byte) + defer dnsResponseBufPool.Put(bufPtr) +- data, err = respMsg.PackBuffer((*bufPtr)[:cap(*bufPtr)]) ++ wireMsg := respMsg ++ if responseTtl := c.currentResponseTtl(); responseTtl > 0 { ++ wireMsg = respMsg.Copy() ++ applyResponseTtlOverride(wireMsg, responseTtl) ++ } ++ data, err = wireMsg.PackBuffer((*bufPtr)[:cap(*bufPtr)]) + if err != nil { + return err + } +diff --git a/control/dns_controller_option_test.go b/control/dns_controller_option_test.go +index 00aca1f2df19c15619d91af8cc6a3d3095a15ce9..e910dff03bd3ef6de0c8ece26f9738f1421d602e 100644 +--- a/control/dns_controller_option_test.go ++++ b/control/dns_controller_option_test.go +@@ -21,6 +21,7 @@ func TestDnsControllerOptionCarriesRuntimeTunables(t *testing.T) { + dnsOptimisticCacheTtl: 600, + dnsMaxCacheSize: 65536, + dnsIpVersionPrefer: 4, ++ dnsResponseTtl: 45, + }, } +@@ -39,6 +40,7 @@ func TestDnsControllerOptionCarriesRuntimeTunables(t *testing.T) { + {field: "OptimisticCacheTtl", got: option.OptimisticCacheTtl, want: 600}, + {field: "MaxCacheSize", got: option.MaxCacheSize, want: 65536}, + {field: "IpVersionPrefer", got: option.IpVersionPrefer, want: 4}, ++ {field: "ResponseTtl", got: option.ResponseTtl, want: 45}, + } { + t.Run(tc.field, func(t *testing.T) { + if tc.got != tc.want { +diff --git a/control/dns_controller_runtime.go b/control/dns_controller_runtime.go +index e11505dcd6c11644313a87432d9eefee5f787916..69f90fc54ad298bc6d3ddd58aa60237525ab211f 100644 +--- a/control/dns_controller_runtime.go ++++ b/control/dns_controller_runtime.go +@@ -31,15 +31,22 @@ type dnsControllerRuntimeState struct { + optimisticCacheTtl int + optimisticStaleReplyTtl int + maxCacheSize int ++ responseTtl int + } + +-func normalizeDnsRuntimeBehavior(option *DnsControllerOption) (qtypePrefer uint16, optimisticCacheEnabled bool, optimisticCacheTtl int, optimisticStaleReplyTtl int, maxCacheSize int, err error) { ++func normalizeDnsRuntimeBehavior(option *DnsControllerOption) (qtypePrefer uint16, optimisticCacheEnabled bool, optimisticCacheTtl int, optimisticStaleReplyTtl int, maxCacheSize int, responseTtl int, err error) { + if option == nil { + option = &DnsControllerOption{} + } + qtypePrefer, err = parseIpVersionPreference(option.IpVersionPrefer) + if err != nil { +- return 0, false, 0, 0, 0, err ++ return 0, false, 0, 0, 0, 0, err ++ } ++ if option.ResponseTtl < 0 { ++ return 0, false, 0, 0, 0, 0, fmt.Errorf("response_ttl must be greater than or equal to 0") ++ } ++ if uint64(option.ResponseTtl) > uint64(^uint32(0)) { ++ return 0, false, 0, 0, 0, 0, fmt.Errorf("response_ttl must be less than or equal to %d", ^uint32(0)) + } + optimisticCacheTtl = option.OptimisticCacheTtl + optimisticStaleReplyTtl = option.OptimisticStaleReplyTtl +@@ -47,7 +54,7 @@ func normalizeDnsRuntimeBehavior(option *DnsControllerOption) (qtypePrefer uint1 + if optimisticCacheTtl == 0 && maxCacheSize == 0 { + optimisticCacheTtl = 60 + } +- return qtypePrefer, option.OptimisticCache, optimisticCacheTtl, optimisticStaleReplyTtl, maxCacheSize, nil ++ return qtypePrefer, option.OptimisticCache, optimisticCacheTtl, optimisticStaleReplyTtl, maxCacheSize, option.ResponseTtl, nil + } + + func (c *DnsController) currentQtypePrefer() uint16 { +@@ -66,6 +73,14 @@ func (c *DnsController) currentOptimisticCacheConfig() (enabled bool, ttl int, s + return rt.optimisticCacheEnabled, rt.optimisticCacheTtl, rt.optimisticStaleReplyTtl, rt.maxCacheSize + } + ++func (c *DnsController) currentResponseTtl() int { ++ rt := c.runtime() ++ if rt == nil { ++ return 0 ++ } ++ return rt.responseTtl ++} ++ + // ReuseForReload updates the current facade to the replacement generation's + // runtime and returns a fresh facade that shares the same long-lived store. + // The shared store carries DNS cache, forwarders, janitors, async BPF workers, +@@ -122,7 +137,7 @@ func (c *DnsController) updateRuntime(option *DnsControllerOption, routing *dns. + if option == nil { + option = &DnsControllerOption{} + } +- qtypePrefer, optimisticCacheEnabled, optimisticCacheTtl, optimisticStaleReplyTtl, maxCacheSize, err := normalizeDnsRuntimeBehavior(option) ++ qtypePrefer, optimisticCacheEnabled, optimisticCacheTtl, optimisticStaleReplyTtl, maxCacheSize, responseTtl, err := normalizeDnsRuntimeBehavior(option) + if err != nil { + return err + } +@@ -153,6 +168,7 @@ func (c *DnsController) updateRuntime(option *DnsControllerOption, routing *dns. + optimisticCacheTtl: optimisticCacheTtl, + optimisticStaleReplyTtl: optimisticStaleReplyTtl, + maxCacheSize: maxCacheSize, ++ responseTtl: responseTtl, + } + c.runtimeMu.Lock() + c.runtimeState.Store(runtimeState) +diff --git a/control/dns_corpus_support_test.go b/control/dns_corpus_support_test.go +index 37255bd1249751c6932c961c5f9aae681351d356..6efcdb1bec004671e44976a3df290a7602e8edd9 100644 +--- a/control/dns_corpus_support_test.go ++++ b/control/dns_corpus_support_test.go +@@ -182,6 +182,7 @@ func newCorpusDnsController(t testing.TB, cfg *config.Dns) *DnsController { + LifecycleContext: context.Background(), + OptimisticCache: true, + OptimisticCacheTtl: 60, ++ ResponseTtl: cfg.ResponseTtl, + CacheAccessCallback: func(*DnsCache) error { + return nil + }, +diff --git a/control/dns_runtime.go b/control/dns_runtime.go +index 8f15b714209ab7fd7171745efcc0a6f88ee6fc45..009677fd1bc308c9fdeee54dc892119b75033dad 100644 +--- a/control/dns_runtime.go ++++ b/control/dns_runtime.go +@@ -30,6 +30,7 @@ type controlPlaneDNSRuntime struct { + dnsOptimisticStaleReplyTtl int + dnsMaxCacheSize int + dnsIpVersionPrefer int ++ dnsResponseTtl int + + dnsListener *DNSListener + dnsListenerStopRegistered bool + diff --git a/control/dns_response_ttl_test.go b/control/dns_response_ttl_test.go new file mode 100644 -index 0000000..0e5406e +index 0000000000000000000000000000000000000000..a33d75a18bc1cbae73d778336ee6c405f8e1a6da --- /dev/null +++ b/control/dns_response_ttl_test.go -@@ -0,0 +1,64 @@ +@@ -0,0 +1,389 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) 2022-2026, daeuniverse Organization @@ -138,79 +542,384 @@ index 0000000..0e5406e +package control + +import ( ++ "context" + "net" ++ "strconv" ++ "sync/atomic" + "testing" + "time" + ++ "github.com/daeuniverse/dae/common/consts" ++ componentdns "github.com/daeuniverse/dae/component/dns" + dnsmessage "github.com/miekg/dns" -+ "github.com/stretchr/testify/require" ++ "github.com/sirupsen/logrus" +) + -+func TestDnsController_ResponseTtlOverride(t *testing.T) { -+ for _, tt := range []struct { -+ name string -+ responseTtl int -+ wantTtl uint32 -+ }{ -+ {name: "default_zero", responseTtl: 0, wantTtl: 0}, -+ {name: "override", responseTtl: 60, wantTtl: 60}, -+ } { -+ t.Run(tt.name, func(t *testing.T) { -+ ctrl := setTestDnsControllerRuntime(newTestDnsController(), func(rt *dnsControllerRuntimeState) { -+ rt.responseTtl = tt.responseTtl -+ rt.newCache = func(fqdn string, answers, ns, extra []dnsmessage.RR, deadline time.Time, originalDeadline time.Time) (*DnsCache, error) { -+ return &DnsCache{ -+ Answer: answers, -+ NS: ns, -+ Extra: extra, -+ Deadline: deadline, -+ OriginalDeadline: originalDeadline, -+ }, nil -+ } -+ }) ++func responseTtlTestCache(now time.Time) *DnsCache { ++ return &DnsCache{ ++ Deadline: now.Add(30 * time.Second), ++ OriginalDeadline: now.Add(30 * time.Second), ++ Answer: []dnsmessage.RR{ ++ &dnsmessage.CNAME{Hdr: dnsmessage.RR_Header{Name: "alias.test.", Rrtype: dnsmessage.TypeCNAME, Class: dnsmessage.ClassINET, Ttl: 300}, Target: "address.test."}, ++ &dnsmessage.A{Hdr: dnsmessage.RR_Header{Name: "address.test.", Rrtype: dnsmessage.TypeA, Class: dnsmessage.ClassINET, Ttl: 300}, A: net.ParseIP("192.0.2.1").To4()}, ++ &dnsmessage.AAAA{Hdr: dnsmessage.RR_Header{Name: "address.test.", Rrtype: dnsmessage.TypeAAAA, Class: dnsmessage.ClassINET, Ttl: 300}, AAAA: net.ParseIP("2001:db8::1")}, ++ }, ++ NS: []dnsmessage.RR{ ++ &dnsmessage.TXT{Hdr: dnsmessage.RR_Header{Name: "authority.test.", Rrtype: dnsmessage.TypeTXT, Class: dnsmessage.ClassINET, Ttl: 300}, Txt: []string{"authority"}}, ++ }, ++ Extra: []dnsmessage.RR{ ++ &dnsmessage.A{Hdr: dnsmessage.RR_Header{Name: "additional.test.", Rrtype: dnsmessage.TypeA, Class: dnsmessage.ClassINET, Ttl: 300}, A: net.ParseIP("192.0.2.2").To4()}, ++ &dnsmessage.OPT{Hdr: dnsmessage.RR_Header{Name: ".", Rrtype: dnsmessage.TypeOPT, Class: 1232, Ttl: 0x00008000}}, ++ }, ++ } ++} + -+ msg := new(dnsmessage.Msg) -+ msg.SetReply(&dnsmessage.Msg{ -+ Question: []dnsmessage.Question{{ -+ Name: "example.com.", -+ Qtype: dnsmessage.TypeA, -+ Qclass: dnsmessage.ClassINET, -+ }}, ++func assertResponseTtlPolicy(t *testing.T, wire []byte, responseTtl, remainingTtl uint32) { ++ t.Helper() ++ var msg dnsmessage.Msg ++ if err := msg.Unpack(wire); err != nil { ++ t.Fatalf("unpack response: %v", err) ++ } ++ if got := msg.Answer[0].Header().Ttl; got != remainingTtl { ++ t.Fatalf("CNAME TTL = %d, want remaining TTL %d", got, remainingTtl) ++ } ++ for _, i := range []int{1, 2} { ++ if got := msg.Answer[i].Header().Ttl; got != responseTtl { ++ t.Fatalf("address answer %d TTL = %d, want %d", i, got, responseTtl) ++ } ++ } ++ if len(msg.Ns) != 1 || msg.Ns[0].Header().Ttl != remainingTtl { ++ t.Fatalf("authority section = %#v, want one record with TTL %d", msg.Ns, remainingTtl) ++ } ++ if len(msg.Extra) != 2 { ++ t.Fatalf("additional section length = %d, want 2", len(msg.Extra)) ++ } ++ if got := msg.Extra[0].Header().Ttl; got != remainingTtl { ++ t.Fatalf("additional A TTL = %d, want remaining TTL %d", got, remainingTtl) ++ } ++ if got := msg.Extra[1].Header().Ttl; got != 0x00008000 { ++ t.Fatalf("OPT flags = %#x, want unchanged", got) ++ } ++} ++ ++func TestResponseTtlMaterializationPolicy(t *testing.T) { ++ now := time.Now() ++ for _, responseTtl := range []int{0, 45} { ++ cache := responseTtlTestCache(now) ++ if err := cache.prepackResponseWithTTLAndResponseTtl("alias.test.", dnsmessage.TypeA, 30, responseTtl, now); err != nil { ++ t.Fatalf("response_ttl=%d prepack: %v", responseTtl, err) ++ } ++ wantAddressTtl := uint32(30) ++ if responseTtl > 0 { ++ wantAddressTtl = uint32(responseTtl) ++ } ++ assertResponseTtlPolicy(t, cache.packedResponse.Load().wire, wantAddressTtl, 30) ++ ++ req := new(dnsmessage.Msg) ++ req.SetQuestion("alias.test.", dnsmessage.TypeA) ++ wire, err := cache.fillIntoWithTTLAndResponseTtlInPlace(req, now, responseTtl) ++ if err != nil { ++ t.Fatalf("response_ttl=%d in-place: %v", responseTtl, err) ++ } ++ assertResponseTtlPolicy(t, wire, wantAddressTtl, 30) ++ } ++} ++ ++func TestResponseTtlInPlacePreservesNodataSections(t *testing.T) { ++ now := time.Now() ++ cache := &DnsCache{ ++ Deadline: now.Add(30 * time.Second), ++ OriginalDeadline: now.Add(30 * time.Second), ++ NS: []dnsmessage.RR{ ++ &dnsmessage.SOA{ ++ Hdr: dnsmessage.RR_Header{Name: "nodata.test.", Rrtype: dnsmessage.TypeSOA, Class: dnsmessage.ClassINET, Ttl: 300}, ++ Ns: "ns.nodata.test.", ++ Mbox: "hostmaster.nodata.test.", ++ Serial: 1, ++ Refresh: 3600, ++ Retry: 600, ++ Expire: 86400, ++ Minttl: 300, ++ }, ++ }, ++ Extra: []dnsmessage.RR{ ++ &dnsmessage.A{Hdr: dnsmessage.RR_Header{Name: "ns.nodata.test.", Rrtype: dnsmessage.TypeA, Class: dnsmessage.ClassINET, Ttl: 300}, A: net.ParseIP("192.0.2.53").To4()}, ++ &dnsmessage.OPT{Hdr: dnsmessage.RR_Header{Name: ".", Rrtype: dnsmessage.TypeOPT, Class: 1232, Ttl: 0x00008000}}, ++ }, ++ } ++ req := new(dnsmessage.Msg) ++ req.SetQuestion("nodata.test.", dnsmessage.TypeA) ++ req.Answer = []dnsmessage.RR{&dnsmessage.A{Hdr: dnsmessage.RR_Header{Name: "old.test.", Rrtype: dnsmessage.TypeA, Class: dnsmessage.ClassINET, Ttl: 1}, A: net.ParseIP("192.0.2.1").To4()}} ++ req.Ns = []dnsmessage.RR{&dnsmessage.TXT{Hdr: dnsmessage.RR_Header{Name: "old.test.", Rrtype: dnsmessage.TypeTXT, Class: dnsmessage.ClassINET, Ttl: 1}, Txt: []string{"old"}}} ++ req.Extra = []dnsmessage.RR{&dnsmessage.OPT{Hdr: dnsmessage.RR_Header{Name: ".", Rrtype: dnsmessage.TypeOPT, Class: 512, Ttl: 0}}} ++ ++ wire, err := cache.fillIntoWithTTLAndResponseTtlInPlace(req, now, 45) ++ if err != nil { ++ t.Fatal(err) ++ } ++ var got dnsmessage.Msg ++ if err := got.Unpack(wire); err != nil { ++ t.Fatal(err) ++ } ++ if len(got.Answer) != 0 { ++ t.Fatalf("NODATA answer count = %d, want 0", len(got.Answer)) ++ } ++ if len(got.Ns) != 1 { ++ t.Fatalf("authority count = %d, want 1", len(got.Ns)) ++ } ++ soa, ok := got.Ns[0].(*dnsmessage.SOA) ++ if !ok || soa.Ns != "ns.nodata.test." || soa.Hdr.Ttl != 30 { ++ t.Fatalf("authority SOA = %#v, want ns.nodata.test. with TTL 30", got.Ns[0]) ++ } ++ if len(got.Extra) != 2 { ++ t.Fatalf("additional count = %d, want 2", len(got.Extra)) ++ } ++ additional, ok := got.Extra[0].(*dnsmessage.A) ++ if !ok || !additional.A.Equal(net.ParseIP("192.0.2.53")) || additional.Hdr.Ttl != 30 { ++ t.Fatalf("additional A = %#v, want 192.0.2.53 with TTL 30", got.Extra[0]) ++ } ++ opt, ok := got.Extra[1].(*dnsmessage.OPT) ++ if !ok || opt.Hdr.Class != 1232 || opt.Hdr.Ttl != 0x00008000 { ++ t.Fatalf("OPT = %#v, want class 1232 and flags %#x", got.Extra[1], uint32(0x00008000)) ++ } ++} ++ ++func TestResponseTtlRefreshesPackedPolicy(t *testing.T) { ++ now := time.Now() ++ cache := responseTtlTestCache(now) ++ cache.deadlineNano.Store(cache.Deadline.UnixNano()) ++ if err := cache.prepackResponseWithTTLAndResponseTtl("alias.test.", dnsmessage.TypeA, 30, 0, now); err != nil { ++ t.Fatal(err) ++ } ++ clone := cache.CloneForReload() ++ wire := clone.getPackedResponseWithApproximateTTL("alias.test.", dnsmessage.TypeA, 45, now) ++ assertResponseTtlPolicy(t, wire, 45, 30) ++} ++ ++func TestResponseTtlRefreshesStalePackedPolicy(t *testing.T) { ++ now := time.Now() ++ cache := responseTtlTestCache(now) ++ cache.Deadline = now.Add(-time.Second) ++ cache.deadlineNano.Store(cache.Deadline.UnixNano()) ++ if err := cache.prepackResponseWithTTLAndResponseTtl("alias.test.", dnsmessage.TypeA, 30, 0, now.Add(-time.Minute)); err != nil { ++ t.Fatal(err) ++ } ++ wire := cache.getStaleResponseWithResponseTtl("alias.test.", dnsmessage.TypeA, now, 60, 45) ++ assertResponseTtlPolicy(t, wire, 45, 30) ++ bounded := clampWireRecordTtls(wire, 30) ++ var stale dnsmessage.Msg ++ if err := stale.Unpack(bounded); err != nil { ++ t.Fatal(err) ++ } ++ if got := stale.Answer[1].Header().Ttl; got != 30 { ++ t.Fatalf("stale address TTL = %d, want min(response_ttl=45, stale_reply_ttl=30)", got) ++ } ++} ++ ++func TestResponseTtlRejectsNegativeRuntimeValue(t *testing.T) { ++ ctrl := newTestDnsController() ++ if err := ctrl.TryUpdateRuntime(&DnsControllerOption{ResponseTtl: -1}, nil); err == nil { ++ t.Fatal("negative response_ttl was accepted") ++ } ++} ++ ++func TestResponseTtlRejectsValuesAboveWireRange(t *testing.T) { ++ if strconv.IntSize <= 32 { ++ t.Skip("int cannot represent a value above uint32 on 32-bit hosts") ++ } ++ ctrl := newTestDnsController() ++ maxUint32 := uint64(^uint32(0)) ++ tooLarge := int(maxUint32) + 1 ++ if err := ctrl.TryUpdateRuntime(&DnsControllerOption{ResponseTtl: tooLarge}, nil); err == nil { ++ t.Fatal("response_ttl above uint32 wire range was accepted") ++ } ++} ++ ++func TestResponseTtlCachePublicationUsesCopyOnWrite(t *testing.T) { ++ now := time.Now() ++ ctrl := newTestDnsController() ++ ctrl.log = logrus.New() ++ setTestDnsControllerRuntime(ctrl, func(rt *dnsControllerRuntimeState) { ++ rt.responseTtl = 45 ++ rt.newCache = func(_ string, answers, ns, extra []dnsmessage.RR, deadline, originalDeadline time.Time) (*DnsCache, error) { ++ return &DnsCache{Answer: answers, NS: ns, Extra: extra, Deadline: deadline, OriginalDeadline: originalDeadline}, nil ++ } ++ }) ++ msg := new(dnsmessage.Msg) ++ msg.SetReply(&dnsmessage.Msg{Question: []dnsmessage.Question{{Name: "copy-on-write.test.", Qtype: dnsmessage.TypeA, Qclass: dnsmessage.ClassINET}}}) ++ msg.Answer = responseTtlTestCache(now).Answer ++ msg.Ns = responseTtlTestCache(now).NS ++ msg.Extra = responseTtlTestCache(now).Extra ++ if err := ctrl.NormalizeAndCacheDnsResp_(msg, "copy-on-write"); err != nil { ++ t.Fatal(err) ++ } ++ if got := msg.Answer[1].Header().Ttl; got != 45 { ++ t.Fatalf("outgoing A TTL = %d, want 45", got) ++ } ++ if got := msg.Answer[0].Header().Ttl; got != 300 { ++ t.Fatalf("outgoing CNAME TTL = %d, want upstream 300", got) ++ } ++ if got := msg.Ns[0].Header().Ttl; got != 300 { ++ t.Fatalf("outgoing authority TTL = %d, want upstream 300", got) ++ } ++ if got := msg.Extra[0].Header().Ttl; got != 300 { ++ t.Fatalf("outgoing additional A TTL = %d, want upstream 300", got) ++ } ++ if got := msg.Extra[1].Header().Ttl; got != 0x00008000 { ++ t.Fatalf("outgoing OPT flags = %#x, want unchanged", got) ++ } ++ value, ok := ctrl.dnsCache.Load("copy-on-write") ++ if !ok { ++ t.Fatal("cache entry was not published") ++ } ++ cache := value.(*DnsCache) ++ if got := cache.Answer[1].Header().Ttl; got != 300 { ++ t.Fatalf("published cache A TTL = %d, want upstream 300", got) ++ } ++ for name, deadline := range map[string]time.Time{"deadline": cache.Deadline, "original deadline": cache.OriginalDeadline} { ++ remaining := deadline.Sub(now) ++ if remaining < 299*time.Second || remaining > 301*time.Second { ++ t.Fatalf("%s remaining = %v, want upstream TTL 300s", name, remaining) ++ } ++ } ++} ++ ++func TestResponseTtlRawUdpMissAndCachedHit(t *testing.T) { ++ for _, responseTtl := range []int{0, 45} { ++ t.Run(dnsmessage.TypeToString[dnsmessage.TypeA]+"/ttl="+time.Duration(responseTtl).String(), func(t *testing.T) { ++ req, client := newDNSResponseUDPTestPair(t) ++ logger := newDNSListenerTestLogger() ++ routing := newPhase0NamedUpstreamRouting(t, logger, "u", "192.0.2.11:53") ++ response := dnsAResponseMsg(phase0NamedUpstreamScopeQName, "198.51.100.11") ++ response.Answer = append([]dnsmessage.RR{ ++ &dnsmessage.CNAME{Hdr: dnsmessage.RR_Header{Name: phase0NamedUpstreamScopeQName, Rrtype: dnsmessage.TypeCNAME, Class: dnsmessage.ClassINET, Ttl: 60}, Target: "address.test."}, ++ }, response.Answer...) ++ response.Ns = []dnsmessage.RR{&dnsmessage.TXT{Hdr: dnsmessage.RR_Header{Name: "authority.test.", Rrtype: dnsmessage.TypeTXT, Class: dnsmessage.ClassINET, Ttl: 60}, Txt: []string{"authority"}}} ++ response.Extra = []dnsmessage.RR{&dnsmessage.A{Hdr: dnsmessage.RR_Header{Name: "additional.test.", Rrtype: dnsmessage.TypeA, Class: dnsmessage.ClassINET, Ttl: 60}, A: net.ParseIP("192.0.2.2").To4()}} ++ response.SetEdns0(1232, true) ++ response.IsEdns0().SetZ(0x0040) ++ optTtl := response.IsEdns0().Hdr.Ttl ++ ++ var forwards atomic.Int32 ++ installCorpusDnsForwarderFactory(t, func(*componentdns.Upstream, dialArgument, *logrus.Logger) (DnsForwarder, error) { ++ return &stubDnsForwarder{forward: func(context.Context, []byte) (*dnsmessage.Msg, error) { ++ forwards.Add(1) ++ return response.Copy(), nil ++ }}, nil + }) -+ msg.Answer = []dnsmessage.RR{ -+ &dnsmessage.A{ -+ Hdr: dnsmessage.RR_Header{ -+ Name: "example.com.", -+ Rrtype: dnsmessage.TypeA, -+ Class: dnsmessage.ClassINET, -+ Ttl: 300, -+ }, -+ A: net.IPv4(93, 184, 216, 34), -+ }, ++ stored := make(chan struct{}, 1) ++ option := phase0NamedUpstreamControllerOption(logger) ++ option.ResponseTtl = responseTtl ++ option.CacheAccessCallback = func(*DnsCache) error { ++ select { ++ case stored <- struct{}{}: ++ default: ++ } ++ return nil ++ } ++ controller, err := NewDnsController(routing, option) ++ if err != nil { ++ t.Fatal(err) ++ } ++ t.Cleanup(func() { _ = controller.Close() }) ++ ++ query := new(dnsmessage.Msg) ++ query.SetQuestion(phase0NamedUpstreamScopeQName, dnsmessage.TypeA) ++ queryWire, err := query.Pack() ++ if err != nil { ++ t.Fatal(err) ++ } ++ index, upstream, err := routing.RequestSelect(context.Background(), phase0NamedUpstreamScopeQName, dnsmessage.TypeA) ++ if err != nil { ++ t.Fatal(err) ++ } ++ cacheKey := controller.responseCacheKey(controller.cacheKey(phase0NamedUpstreamScopeQName, dnsmessage.TypeA), req, index, upstream) ++ if err := client.SetReadDeadline(time.Now().Add(time.Second)); err != nil { ++ t.Fatal(err) ++ } ++ if err := controller.dialSend(context.Background(), req, queryWire, query.Id, upstream, nil, cacheKey); err != nil { ++ t.Fatal(err) ++ } ++ buf := make([]byte, dnsmessage.MaxMsgSize) ++ n, _, err := client.ReadFromUDPAddrPort(buf) ++ if err != nil { ++ t.Fatal(err) ++ } ++ var miss dnsmessage.Msg ++ if err := miss.Unpack(buf[:n]); err != nil { ++ t.Fatal(err) ++ } ++ wantAddressTtl := uint32(60) ++ if responseTtl > 0 { ++ wantAddressTtl = uint32(responseTtl) ++ } ++ if miss.Answer[0].Header().Ttl != 60 || miss.Answer[1].Header().Ttl != wantAddressTtl || miss.Ns[0].Header().Ttl != 60 || miss.Extra[0].Header().Ttl != 60 || miss.Extra[1].Header().Ttl != optTtl { ++ t.Fatalf("raw miss changed non-address data or returned wrong address TTL: answer=%v ns=%v extra=%v", miss.Answer, miss.Ns, miss.Extra) ++ } ++ select { ++ case <-stored: ++ case <-time.After(time.Second): ++ t.Fatal("asynchronous cache was not populated") + } + -+ require.NoError(t, ctrl.NormalizeAndCacheDnsResp_(msg, "example.com.:1")) -+ require.Equal(t, tt.wantTtl, msg.Answer[0].Header().Ttl) ++ writer := &dnsCorpusCaptureWriter{} ++ if err := controller.HandleWithResponseWriter_(context.Background(), query.Copy(), req, writer); err != nil { ++ t.Fatal(err) ++ } ++ hit := writer.Message() ++ if hit == nil || len(hit.Answer) != 2 { ++ t.Fatalf("cached hit = %#v, want two answers", hit) ++ } ++ if responseTtl > 0 { ++ if got := hit.Answer[1].Header().Ttl; got != uint32(responseTtl) { ++ t.Fatalf("cached hit A TTL = %d, want %d", got, responseTtl) ++ } ++ } else if got := hit.Answer[1].Header().Ttl; got == 0 || got > 60 { ++ t.Fatalf("cached hit A TTL = %d, want real remaining TTL in [1,60]", got) ++ } ++ if got := forwards.Load(); got != 1 { ++ t.Fatalf("forwarder calls = %d, want 1 (second request must hit cache)", got) ++ } + }) + } +} -diff --git a/control/dns_runtime.go b/control/dns_runtime.go -index b871cc3..0c1d9c6 100644 ---- a/control/dns_runtime.go -+++ b/control/dns_runtime.go -@@ -20,6 +20,7 @@ type controlPlaneDNSRuntime struct { - dnsController *DnsController - dnsRouting *dns.Dns - dnsFixedDomainTtl map[string]int -+ dnsResponseTtl int - dnsListener *DNSListener - dnsListenerStopRegistered bool - delayDNSListenerStart bool -@@ -262,6 +263,7 @@ func (r *controlPlaneDNSRuntime) releaseRetainedState() { - r.dnsController = nil - r.dnsRouting = nil - r.dnsFixedDomainTtl = nil -+ r.dnsResponseTtl = 0 - r.dnsListener = nil - r.dnsListenerStopRegistered = false - r.delayDNSListenerStart = false ++ ++type responseTtlDomainMatcher struct{} ++ ++func (responseTtlDomainMatcher) AddSet(int, []string, consts.RoutingDomainKey) {} ++func (responseTtlDomainMatcher) Build() error { return nil } ++func (responseTtlDomainMatcher) MatchDomainBitmap(string) []uint32 { return nil } ++ ++func TestResponseTtlSurvivesReuseAndRollback(t *testing.T) { ++ controller := newTestDnsController() ++ controller.log = logrus.New() ++ oldHash := [32]byte{1} ++ if err := controller.TryUpdateRuntime(&DnsControllerOption{ResponseTtl: 15, RouteProjectionHash: oldHash}, nil); err != nil { ++ t.Fatal(err) ++ } ++ reused, err := controller.ReuseForReload(&DnsControllerOption{ResponseTtl: 45, RouteProjectionHash: oldHash}, nil) ++ if err != nil { ++ t.Fatal(err) ++ } ++ if got := reused.currentResponseTtl(); got != 45 { ++ t.Fatalf("reused response_ttl = %d, want 45", got) ++ } ++ ++ candidate := &ControlPlane{controlPlaneDNSRuntime: controlPlaneDNSRuntime{dnsController: reused}} ++ previous := &ControlPlane{ ++ ctx: context.Background(), ++ controlPlaneGenerationState: controlPlaneGenerationState{ ++ routingMatcher: &RoutingMatcher{domainMatcher: responseTtlDomainMatcher{}}, ++ }, ++ controlPlaneDNSRuntime: controlPlaneDNSRuntime{dnsResponseTtl: 15}, ++ } ++ if _, err := candidate.RestorePreparedDNSRuntimeForRollback(previous, true, false); err != nil { ++ t.Fatal(err) ++ } ++ if candidate.dnsController != nil || previous.dnsController == nil { ++ t.Fatal("rollback did not transfer the DNS controller to the previous generation") ++ } ++ if got := previous.dnsController.currentResponseTtl(); got != 15 { ++ t.Fatalf("rollback response_ttl = %d, want previous value 15", got) ++ } ++} diff --git a/dae/patches_arm/0003-drop-kprobe-skb-5.patch b/dae/patches_arm/0003-drop-kprobe-skb-5.patch index f04c0f410..49bfccb7c 100644 --- a/dae/patches_arm/0003-drop-kprobe-skb-5.patch +++ b/dae/patches_arm/0003-drop-kprobe-skb-5.patch @@ -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, + } diff --git a/daed/Makefile b/daed/Makefile index b8fbf3188..3f7f8cae5 100644 --- a/daed/Makefile +++ b/daed/Makefile @@ -5,10 +5,10 @@ include $(TOPDIR)/rules.mk PKG_NAME:=daed -PKG_VERSION:=2026.09.12 -PKG_RELEASE:=55 +PKG_VERSION:=2026.09.18 +PKG_RELEASE:=56 -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:=skip diff --git a/daed/patches/0012-adapt-wing-to-new-core.patch b/daed/patches/0012-adapt-wing-to-new-core.patch new file mode 100644 index 000000000..ec5af516d --- /dev/null +++ b/daed/patches/0012-adapt-wing-to-new-core.patch @@ -0,0 +1,837 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kenzok8 +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) + } + diff --git a/daed/patches_arm/0003-drop-kprobe-skb-5.patch b/daed/patches_arm/0003-drop-kprobe-skb-5.patch index 26f67ab55..967807e83 100644 --- a/daed/patches_arm/0003-drop-kprobe-skb-5.patch +++ b/daed/patches_arm/0003-drop-kprobe-skb-5.patch @@ -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, + } diff --git a/gowebdav/Makefile b/gowebdav/Makefile index 44ae9a4fe..4f716faac 100644 --- a/gowebdav/Makefile +++ b/gowebdav/Makefile @@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=gowebdav -PKG_VERSION:=5.15.1 -PKG_RELEASE:=21 +PKG_VERSION:=5.16.0 +PKG_RELEASE:=22 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/hacdias/webdav/tar.gz/v$(PKG_VERSION)? diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index 9fa57c796..73a262b31 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter LUCI_PKGARCH:=all PKG_VERSION:=0.1.44 -PKG_RELEASE:=26 +PKG_RELEASE:=27 # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). ifdef SOURCE_DATE_EPOCH @@ -22,12 +22,12 @@ endif PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Lucas Albers -include $(TOPDIR)/feeds/luci/luci.mk - define Package/luci-app-fwlive/prerm #!/bin/sh -[ -n "$${IPKG_INSTROOT}" ] && exit 0 -[ "$$1" = "remove" ] || exit 0 +case "$${IPKG_INSTROOT}" in + ''|/) ;; + *) exit 0 ;; +esac . /usr/libexec/fwlive-logging.sh # Best-effort restore: always exit 0 so a transient lock/pending-staging # failure cannot strand the package half-removed. Baseline is kept on @@ -37,4 +37,6 @@ restore_wan_log_baseline || logger -t fwlive "WAN log baseline restore failed du exit 0 endef +include $(TOPDIR)/feeds/luci/luci.mk + # call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-fwlive/README.md b/luci-app-fwlive/README.md index aeeb696ce..7697cc602 100644 --- a/luci-app-fwlive/README.md +++ b/luci-app-fwlive/README.md @@ -38,7 +38,7 @@ No `luasrc/` — modern JS-only app. - Optional reverse DNS uses BusyBox `nslookup` (stock image; not a package depend) - No hard `firewall4` dependency - Menu depends on ACL only (no `fs` AND of `nft`+`iptables` — that hid the entry on stock fw3 and fw4) -- Runtime backend detection selects **fw4/nft** (22.03+) or **iptables LOG** (21.02 fw3); best-effort iptables when nft absent +- Runtime backend detection selects **fw4/nft** on supported **23.05+** images; log lines tagged **iptables** still classify. OpenWrt **21.02** / **22.03** are unsupported ## Maintenance diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js index 9ad561102..24f436907 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js @@ -16,7 +16,9 @@ * Modules must not mutate state. host is cleared then rebuilt (idempotent replace). */ -function chipValueNodes(field, val) { +function chipValueNodes(spec, val) { + const key = spec.key; + const label = spec.label || key; const p = log.parseFilterValue(val); if (!p.value) return ['']; @@ -26,19 +28,19 @@ function chipValueNodes(field, val) { return [ E('span', { 'class': 'fwlive-chip-polarity' }, [_('is')]), ' ', - log.formatFilterChipLabel(field, val) + log.formatFilterChipLabel(label, val) ]; } - if (field === 'q' || field === 'src' || field === 'dst') + if (key === 'q' || key === 'src' || key === 'dst') return [ - field + ': ', + label + ': ', E('strong', { 'class': 'fwlive-chip-not' }, [_('not')]), ' contains ', valueNode ]; - return [field + ': ', E('strong', { 'class': 'fwlive-chip-not' }, [_('not')]), ' ', valueNode]; + return [label + ': ', E('strong', { 'class': 'fwlive-chip-not' }, [_('not')]), ' ', valueNode]; } function chipLeadingSym(negated) { @@ -69,7 +71,7 @@ function renderFilterChips(host, state, callbacks) { const lead = chipLeadingSym(negated); if (lead) kids.push(lead); - kids.push(E('span', { 'class': 'fwlive-chip-label' }, chipValueNodes(spec.label, val))); + kids.push(E('span', { 'class': 'fwlive-chip-label' }, chipValueNodes(spec, val))); kids.push( E( 'span', diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js index e2f5ec2ee..d2ce6acd7 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js @@ -93,28 +93,19 @@ function addrFilterLink(field, ip, showHostnames, hostnameCache, onFilterClick) ); } -/** - * @param {string} hint - rule hint token - * @param {string} firewallBackend - 'nft' or 'iptables' - */ -function ruleAdminPath(hint, firewallBackend) { - if (hint === 'fw4') return 'admin/network/firewall/rules'; - - if (firewallBackend === 'iptables') return 'admin/status/iptables'; - - return 'admin/status/nftables'; +function ruleAdminPath() { + return 'admin/network/firewall/rules'; } /** * @param {string} hint - rule hint token * @param {string} label - display label - * @param {string} firewallBackend - 'nft' or 'iptables' * @param {function} onFilterClick - callback(field, value, ev) */ -function ruleAdminLink(hint, label, firewallBackend, onFilterClick) { +function ruleAdminLink(hint, label, onFilterClick) { if (!hint) return log.formatCell(hint); - const path = ruleAdminPath(hint, firewallBackend); + const path = ruleAdminPath(); const url = '%s#%s'.format(luciUrl(path), encodeURIComponent(hint)); const text = label || hint; diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js index e4a6c2d38..404d6663a 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js @@ -17,7 +17,7 @@ * * renderManualTestNodes(host, state, callbacks) → void * host -
    element inside #fwlive-help (cleared and rebuilt) - * state - { firewallBackend } + * state - unused; nft-only instruction * * Empty-state helpers: * buildEmptyStateNodes(state, callbacks) → Node[] @@ -330,29 +330,19 @@ function renderEmptyState(host, state, callbacks) { } /** - * renderManualTestNodes — fills a
  • host element with the backend-specific - * manual test instruction. Call from addFooter() after render() has inserted + * renderManualTestNodes — fills a
  • host element with the nft manual test + * instruction. Call from addFooter() after render() has inserted * the placeholder
  • . */ -function renderManualTestNodes(host, state, _callbacks) { +function renderManualTestNodes(host, _state, _callbacks) { host.innerHTML = ''; - if (state.firewallBackend === 'iptables') { - host.appendChild(document.createTextNode(_('Manual test (System → Terminal): '))); - host.appendChild( - E('code', {}, [ - 'iptables -I INPUT -p icmp --icmp-type echo-request -j LOG --log-prefix "fwlive-ping: "' - ]) - ); - host.appendChild(document.createTextNode(_(' then ping the router.'))); - } else { - host.appendChild(document.createTextNode(_('Manual test (System → Terminal): '))); - host.appendChild( - E('code', {}, [ - 'nft insert rule inet fw4 input ip protocol icmp icmp type echo-request log prefix "fwlive-ping " accept' - ]) - ); - host.appendChild(document.createTextNode(_(' then ping the router.'))); - } + host.appendChild(document.createTextNode(_('Manual test (System → Terminal): '))); + host.appendChild( + E('code', {}, [ + 'nft insert rule inet fw4 input ip protocol icmp icmp type echo-request log prefix "fwlive-ping " accept' + ]) + ); + host.appendChild(document.createTextNode(_(' then ping the router.'))); } return baseclass.extend({ diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js index 3a182d3a2..6e705c717 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js @@ -17,7 +17,7 @@ * host - element (kept; normal polls reuse keyed rows) * state - shallow copy: { rows, columns, viewMode, messageLayout, * expandedRowId, rowTint, showHostnames, - * hostnameCache, firewallBackend } + * hostnameCache } * callbacks - { onRowClick(rowId, ev), onFilterClick(field, value, ev), * actionRowTintClass(action) } * @@ -147,12 +147,7 @@ function buildColumnCell(col, row, state, callbacks) { return E('td', { 'class': log.actionRowClass(row.action) }, [actionCell]); case 'rule': return E('td', { 'class': columnCellClass(col) }, [ - links.ruleAdminLink( - row.rule_hint, - row.rule_label, - state.firewallBackend, - onFilterClick - ) + links.ruleAdminLink(row.rule_hint, row.rule_label, onFilterClick) ]); case 'iface': return E('td', { 'class': columnCellClass(col) }, [ @@ -279,8 +274,7 @@ function rowRenderKey(row, state, columns) { state.messageLayout, state.expandedRowId === row.id, !!state.rowTint, - !!state.showHostnames, - state.firewallBackend + !!state.showHostnames ]); } diff --git a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js index 533a8a90f..3dbe7f869 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js @@ -635,7 +635,6 @@ return view.extend({ }, backendDisplayLabel() { - if (this.firewallBackend === 'iptables') return _('using iptables'); if (this.firewallBackend === 'nft') return _('using fw4'); return ''; }, @@ -664,6 +663,13 @@ return view.extend({ text = text ? text + ' \u00b7 ' + warn : warn; degraded = true; } + if (warnings.indexOf('legacy_iptables_detected') >= 0) { + const warn = _( + 'Live view may be incomplete — a legacy iptables table is registered' + ); + text = text ? text + ' \u00b7 ' + warn : warn; + degraded = true; + } label.textContent = text; label.classList.toggle('fwlive-backend-warn', degraded); } @@ -1847,8 +1853,7 @@ return view.extend({ expandedRowId: this.expandedRowId, rowTint: this.rowTintEnabled(), showHostnames: !!this.showHostnames, - hostnameCache: this.hostnameCache, - firewallBackend: this.firewallBackend + hostnameCache: this.hostnameCache }, { onRowClick: (rowId, ev) => this.onRowClick(rowId, ev), @@ -2471,8 +2476,7 @@ return view.extend({ this.updateTintWarnUi(); this.renderRows(true); const testLi = document.getElementById('fwlive-manual-test'); - if (testLi) - logging.renderManualTestNodes(testLi, { firewallBackend: this.firewallBackend }, {}); + if (testLi) logging.renderManualTestNodes(testLi, {}, {}); if (this.showHostnames) this.resolveHostnamesForEntries(this.filteredRows()); } }); diff --git a/luci-app-fwlive/po/de/luci-app-fwlive.po b/luci-app-fwlive/po/de/luci-app-fwlive.po index 11c409bdf..4b16af1c8 100644 --- a/luci-app-fwlive/po/de/luci-app-fwlive.po +++ b/luci-app-fwlive/po/de/luci-app-fwlive.po @@ -753,9 +753,9 @@ msgstr "turn it back off with the WAN logging on control on the watch strip." msgid "using fw4" msgstr "mit fw4" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 -msgid "using iptables" -msgstr "mit iptables" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js +msgid "Live view may be incomplete — a legacy iptables table is registered" +msgstr "Live view may be incomplete — a legacy iptables table is registered" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 msgid "· %s" diff --git a/luci-app-fwlive/po/ru/luci-app-fwlive.po b/luci-app-fwlive/po/ru/luci-app-fwlive.po index 8dc110638..a68be6aa4 100644 --- a/luci-app-fwlive/po/ru/luci-app-fwlive.po +++ b/luci-app-fwlive/po/ru/luci-app-fwlive.po @@ -758,9 +758,9 @@ msgstr "turn it back off with the WAN logging on control on the watch strip." msgid "using fw4" msgstr "используется fw4" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 -msgid "using iptables" -msgstr "используется iptables" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js +msgid "Live view may be incomplete — a legacy iptables table is registered" +msgstr "Live view may be incomplete — a legacy iptables table is registered" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 msgid "· %s" diff --git a/luci-app-fwlive/po/templates/luci-app-fwlive.pot b/luci-app-fwlive/po/templates/luci-app-fwlive.pot index 103c85712..3acac351e 100644 --- a/luci-app-fwlive/po/templates/luci-app-fwlive.pot +++ b/luci-app-fwlive/po/templates/luci-app-fwlive.pot @@ -687,8 +687,8 @@ msgstr "" msgid "using fw4" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 -msgid "using iptables" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js +msgid "Live view may be incomplete — a legacy iptables table is registered" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 diff --git a/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po b/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po index a0def56d3..fef5da89e 100644 --- a/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po +++ b/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po @@ -722,9 +722,9 @@ msgstr "turn it back off with the WAN logging on control on the watch strip." msgid "using fw4" msgstr "使用 fw4" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:510 -msgid "using iptables" -msgstr "使用 iptables" +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js +msgid "Live view may be incomplete — a legacy iptables table is registered" +msgstr "Live view may be incomplete — a legacy iptables table is registered" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89 msgid "· %s" diff --git a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive index 484e3382d..8e461c819 100755 --- a/luci-app-fwlive/root/usr/libexec/rpcd/fwlive +++ b/luci-app-fwlive/root/usr/libexec/rpcd/fwlive @@ -39,8 +39,6 @@ RESOLVE_BUDGET=5 POLL_LINES_MAX=2000 # Bound nft ruleset dumps (corrupted/hung nft must not block LuCI rules load). NFT_TIMEOUT=5 -# CLI-only iptables fixture path for __rulesmap_iptables (no arbitrary paths). -RULESMAP_IPTABLES_FILE='/tmp/rulesmap' # json_escape lives in fwlive-logging.sh so prerm can source that file alone. # shellcheck disable=SC1090,SC1091 # LOGGING_SH resolves to sibling ../fwlive-logging.sh @@ -55,9 +53,6 @@ slug_key() { # Cap rules JSON size so a huge ruleset cannot unbound an rpcd reply. RULES_MAP_MAX_KEYS=512 RULES_MAP_MAX_BYTES=65536 -# Same wall-clock bound as nft for iptables-save / ip6tables-save. -# Overridable for host tests (e.g. FWLIVE_IPTABLES_TIMEOUT=1). -IPTABLES_TIMEOUT="${FWLIVE_IPTABLES_TIMEOUT:-5}" map_add() { key="$1" @@ -147,25 +142,6 @@ run_with_timeout() { fi } -map_log_prefix_entry() { - prefix="$1" - comment="$2" - pass="${3:-all}" - label='' - - if [ -n "$comment" ] && is_uci_style_name "$comment"; then - label="$comment" - fi - - [ -n "$prefix" ] || return 0 - case "$pass" in - labeled) [ -n "$label" ] || return 0 ;; - unlabeled) [ -z "$label" ] || return 0 ;; - esac - - map_prefix_with_label "$prefix" "$label" -} - map_uci_rule_names() { # Line-wise (here-doc, not an unquoted for-loop) so a name with # spaces stays one token. Names that fail is_uci_style_name are @@ -221,20 +197,6 @@ map_from_nft_stream() { done } -map_from_iptables_save_stream() { - pass="${1:-all}" - while IFS= read -r line; do - case "$line" in - -*) ;; - *) continue ;; - esac - prefix=$(echo "$line" | sed -n 's/.*--log-prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p') - comment=$(echo "$line" | sed -n 's/.*--comment "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p') - [ -n "$prefix" ] || continue - map_log_prefix_entry "$prefix" "$comment" "$pass" - done -} - nft_list_ruleset() { run_with_timeout "$NFT_TIMEOUT" nft list ruleset } @@ -244,10 +206,6 @@ detect_firewall_backend() { printf '%s' nft return 0 fi - if command -v iptables-save >/dev/null 2>&1; then - printf '%s' iptables - return 0 - fi printf '%s' unknown } @@ -282,57 +240,16 @@ _fwlive_mktemp() { enrich_rules_dump() { _dump_prefix="$1" - _dump_kind="$2" - - case "$_dump_kind" in - nft) - _dump_error='nft_failed' - ;; - iptables) - _dump_error='iptables_failed' - ;; - ip6tables) - _dump_error='ip6tables_failed' - ;; - *) - return 1 - ;; - esac _rules_dump=$(_fwlive_mktemp "$_dump_prefix") || _rules_dump='' if [ -n "$_rules_dump" ]; then - # Keep the dump command and stream parser paired by backend. The - # ip6tables caller is availability-gated below; its empty-error - # precedence is intentionally preserved in the mktemp branch. - if case "$_dump_kind" in - nft) - nft_list_ruleset >"$_rules_dump" 2>/dev/null - ;; - iptables|ip6tables) - run_with_timeout "$IPTABLES_TIMEOUT" "${_dump_kind}-save" >"$_rules_dump" 2>/dev/null - ;; - esac - then - case "$_dump_kind" in - nft) - map_from_nft_stream labeled <"$_rules_dump" - map_from_nft_stream unlabeled <"$_rules_dump" - ;; - iptables|ip6tables) - map_from_iptables_save_stream labeled <"$_rules_dump" - map_from_iptables_save_stream unlabeled <"$_rules_dump" - ;; - esac + if nft_list_ruleset >"$_rules_dump" 2>/dev/null; then + map_from_nft_stream labeled <"$_rules_dump" + map_from_nft_stream unlabeled <"$_rules_dump" elif [ -z "$_enrich_error" ]; then - _enrich_error="$_dump_error" + _enrich_error='nft_failed' fi rm -f "$_rules_dump" - elif [ "$_dump_kind" = ip6tables ]; then - # IPv6 enrichment is optional and must not replace an earlier - # IPv4/backend error when its temp file cannot be allocated. - if [ -z "$_enrich_error" ]; then - _enrich_error='mktemp_failed' - fi else _enrich_error='mktemp_failed' fi @@ -349,17 +266,9 @@ build_rules_map() { # First-wins keeps UCI over nft and !fw4: over cosmetic. map_uci_rule_names - case "$backend" in - nft) - enrich_rules_dump fwlive-nft nft - ;; - iptables) - enrich_rules_dump fwlive-ipt iptables - if command -v ip6tables-save >/dev/null 2>&1; then - enrich_rules_dump fwlive-ip6t ip6tables - fi - ;; - esac + if [ "$backend" = nft ]; then + enrich_rules_dump fwlive-nft + fi if [ "$backend" = unknown ] && [ -z "$_enrich_error" ]; then _enrich_error='no_backend' @@ -373,22 +282,6 @@ build_rules_map() { printf '%s}' "$_rules_json" } -rulesmap_from_iptables_file() { - file="$1" - OUT='' - _map_keys=0 - _map_overflow='' - [ "$file" = "$RULESMAP_IPTABLES_FILE" ] || return 1 - [ -f "$file" ] || return 1 - map_from_iptables_save_stream labeled < "$file" - map_from_iptables_save_stream unlabeled < "$file" - _rules_json="{\"backend\":\"iptables\",\"rules\":{$OUT}" - if [ -n "$_map_overflow" ]; then - _rules_json="${_rules_json},\"error\":\"rules_truncated\"" - fi - printf '%s}' "$_rules_json" -} - poll_clamp_lines() { # Contract: non-digit input returns default 50; digit strings are # clamped to 1..POLL_LINES_MAX with leading zeros stripped. Validates @@ -927,11 +820,6 @@ case "${1:-}" in run_selftest exit $? ;; - __rulesmap_iptables) - # CLI selftest only — never a ubus method. Fixed path only (no argv file read). - rulesmap_from_iptables_file "$RULESMAP_IPTABLES_FILE" - exit $? - ;; __poll_clamp) poll_clamp_lines "${2:-}" exit $? @@ -988,4 +876,8 @@ case "${1:-}" in ;; esac ;; + *) + # Removed CLI hooks (including __rulesmap_iptables) must not succeed. + exit 1 + ;; esac diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index f5b98da35..9ec6fb8cd 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.9.16 -PKG_RELEASE:=284 +PKG_RELEASE:=285 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua index fe6fd30eb..1a95a0adb 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua @@ -96,6 +96,7 @@ if load_urltest_options then -- [[ URLTest Start ]] o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, document")) o:depends({ node_add_mode = "manual" }) o.widget = "checkbox" + o.cast = "table" o.template = m:template_path("/cbi/nodes_multivalue") o.group = {} for k1, v1 in pairs(node_list) do @@ -106,30 +107,6 @@ if load_urltest_options then -- [[ URLTest Start ]] end end end - -- 读取旧 DynamicList - function o.cfgvalue(self, section) - return table.concat(m:get(section, "urltest_node") or {}, " ") - end - -- 写入保持 DynamicList - function o.write(self, section, value) - local old = m:get(section, "urltest_node") or {} - local new, set = {}, {} - for v in value:gmatch("%S+") do - new[#new + 1] = v - set[v] = 1 - end - for _, v in ipairs(old) do - if not set[v] then - m:set(section, "urltest_node", new) - return - end - set[v] = nil - end - for _ in pairs(set) do - m:set(section, "urltest_node", new) - return - end - end o = s:option(MultiValue, "node_group", translate("Select Group")) o:depends({ node_add_mode = "batch" }) diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua index c4a564a87..24ac84e95 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua @@ -90,6 +90,7 @@ if load_balancing_options then -- [[ Load balancing Start ]] o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, document")) o:depends({ node_add_mode = "manual" }) o.widget = "checkbox" + o.cast = "table" o.template = m:template_path("/cbi/nodes_multivalue") o.group = {} for k1, v1 in pairs(node_list) do @@ -100,30 +101,6 @@ if load_balancing_options then -- [[ Load balancing Start ]] end end end - -- 读取旧 DynamicList - function o.cfgvalue(self, section) - return table.concat(m:get(section, "balancing_node") or {}, " ") - end - -- 写入保持 DynamicList - function o.write(self, section, value) - local old = m:get(section, "balancing_node") or {} - local new, set = {}, {} - for v in value:gmatch("%S+") do - new[#new + 1] = v - set[v] = 1 - end - for _, v in ipairs(old) do - if not set[v] then - m:set(section, "balancing_node", new) - return - end - set[v] = nil - end - for _ in pairs(set) do - m:set(section, "balancing_node", new) - return - end - end o = s:option(MultiValue, "node_group", translate("Select Group")) o:depends({ node_add_mode = "batch" }) diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/0_socks.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/0_socks.lua index 91acbf79e..c81782318 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/0_socks.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/0_socks.lua @@ -36,7 +36,7 @@ o = s:option(Flag, "firewall_allow", translate("Firewall Allow")) o.default = "0" o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.rmempty = false +o.rmempty = not m.is_js_luci o.nocreate = true o.allowany = true o.default = "wan" diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua index 65e69f993..eba1dbc91 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua @@ -50,7 +50,7 @@ o.validate = function(self, value) if v then return v end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") end -o.cfgvalue = function(self, section, value) +o.cfgvalue = function(self, section) local config_str = m:get(section, "config_str") if config_str then return api.base64Decode(config_str) @@ -79,7 +79,8 @@ o = s:option(Value, "port", translate("Listen Port")) o.datatype = "port" o:depends({ custom = false }) -o = s:option(DynamicList, "users", translate("User")) +o = s:option(MultiValue, "users", translate("User")) +o.cast = "table" for i, v in ipairs(user_list) do o:value(v[".name"], v.username) end @@ -535,7 +536,7 @@ o.default = "0" o:depends({ custom = false }) o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.rmempty = false +o.rmempty = not m.is_js_luci o.nocreate = true o.allowany = true o.default = "wan" diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua index b126f0211..9f7e7d929 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua @@ -46,7 +46,7 @@ o.validate = function(self, value) if v then return v end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") end -o.cfgvalue = function(self, section, value) +o.cfgvalue = function(self, section) local config_str = m:get(section, "config_str") if config_str then return api.base64Decode(config_str) @@ -72,7 +72,8 @@ o = s:option(Value, "port", translate("Listen Port")) o.datatype = "port" o:depends({ custom = false }) -o = s:option(DynamicList, "users", translate("User")) +o = s:option(MultiValue, "users", translate("User")) +o.cast = "table" for i, v in ipairs(user_list) do o:value(v[".name"], v.username) end @@ -521,7 +522,7 @@ o.default = "0" o:depends({ custom = false }) o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.rmempty = false +o.rmempty = not m.is_js_luci o.nocreate = true o.allowany = true o.default = "wan" diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/3_ss-rust.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/3_ss-rust.lua index 9c7f4cc27..ff5da7919 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/3_ss-rust.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/3_ss-rust.lua @@ -41,7 +41,7 @@ o.validate = function(self, value) if v then return v end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") end -o.cfgvalue = function(self, section, value) +o.cfgvalue = function(self, section) local config_str = m:get(section, "config_str") if config_str then return api.base64Decode(config_str) @@ -79,7 +79,7 @@ o.default = "0" o:depends({ custom = false }) o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.rmempty = false +o.rmempty = not m.is_js_luci o.nocreate = true o.allowany = true o.default = "wan" diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/4_ssr.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/4_ssr.lua index 9453bdf4d..898dedd91 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/4_ssr.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/4_ssr.lua @@ -54,7 +54,7 @@ o.validate = function(self, value) if v then return v end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") end -o.cfgvalue = function(self, section, value) +o.cfgvalue = function(self, section) local config_str = m:get(section, "config_str") if config_str then return api.base64Decode(config_str) @@ -110,7 +110,7 @@ o.default = "0" o:depends({ custom = false }) o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.rmempty = false +o.rmempty = not m.is_js_luci o.nocreate = true o.allowany = true o.default = "wan" diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua index b526498be..dadc54f1d 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua @@ -31,7 +31,7 @@ o.validate = function(self, value) if v then return v end return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") end -o.cfgvalue = function(self, section, value) +o.cfgvalue = function(self, section) local config_str = m:get(section, "config_str") if config_str then return api.base64Decode(config_str) @@ -45,7 +45,8 @@ o = s:option(Value, "port", translate("Listen Port")) o.datatype = "port" o:depends({ custom = false }) -o = s:option(DynamicList, "users", translate("User")) +o = s:option(MultiValue, "users", translate("User")) +o.cast = "table" for i, v in ipairs(user_list) do o:value(v[".name"], v.username) end @@ -168,7 +169,7 @@ o.default = "0" o:depends({ custom = false }) o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.rmempty = false +o.rmempty = not m.is_js_luci o.nocreate = true o.allowany = true o.default = "wan" diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua index 93173da55..c75139da5 100644 --- a/luci-app-passwall/luasrc/passwall/api.lua +++ b/luci-app-passwall/luasrc/passwall/api.lua @@ -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" @@ -1542,11 +1542,18 @@ function set_type_cbi(s) obj.option_prefix = s_self.option_prefix obj.option = s_self.option_prefix .. option obj.cfgvalue = function(self, section) + local v if self.rewrite_option then - return self.map:get(section, self.rewrite_option) + v = self.map:get(section, self.rewrite_option) else - return self.map:get(section, self.config_option) + v = self.map:get(section, self.config_option) end + if util.instanceof(self, cbi.MultiValue) then + if v and self.cast == "table" then + return table.concat(v, " ") + end + end + return v end obj.write = function(self, section, value) if s1.fields["type"]:formvalue(s_self.section) == s_self.type_name then @@ -1564,6 +1571,21 @@ function set_type_cbi(s) new_val = new_t end end + if util.instanceof(self, cbi.MultiValue) then + local new_t = {} + if type(value) == "table" then + new_t = table_remove_duplicates(value) + else + string.gsub(value, '[^' .. " " .. ']+', function(v) + new_t[#new_t + 1] = v + end) + end + if self.cast == "string" then + new_val = table.concat(new_t, " ") + else + new_val = new_t + end + end if self.rewrite_option then self.map:set(section, self.rewrite_option, new_val) else @@ -1619,6 +1641,7 @@ end function type_cbi_section(s, s2) for i, v in ipairs(s2.children) do local o = s2.children[i] + o.section = s s:append(o) s.fields[o.option] = o end diff --git a/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua b/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua index 5a8d4f0e9..20b5704f4 100644 --- a/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua +++ b/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua @@ -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*") diff --git a/luci-app-passwall/root/usr/share/passwall/utils.sh b/luci-app-passwall/root/usr/share/passwall/utils.sh index 7a06d7e9e..9b0839ce6 100755 --- a/luci-app-passwall/root/usr/share/passwall/utils.sh +++ b/luci-app-passwall/root/usr/share/passwall/utils.sh @@ -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 diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 6a4d2e9d5..269c477a6 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.9.16 -PKG_RELEASE:=130 +PKG_RELEASE:=131 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua index b1c3bd14c..444b52b05 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua @@ -99,6 +99,7 @@ if load_urltest_options then -- [[ URLTest Start ]] o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, document")) o:depends({ node_add_mode = "manual" }) o.widget = "checkbox" + o.cast = "table" o.template = m:template_path("/cbi/nodes_multivalue") o.group = {} for k1, v1 in pairs(node_list) do @@ -109,28 +110,6 @@ if load_urltest_options then -- [[ URLTest Start ]] end end end - function o.cfgvalue(self, section) - return table.concat(m:get(section, "urltest_node") or {}, " ") - end - function o.write(self, section, value) - local old = m:get(section, "urltest_node") or {} - local new, set = {}, {} - for v in value:gmatch("%S+") do - new[#new + 1] = v - set[v] = 1 - end - for _, v in ipairs(old) do - if not set[v] then - m:set(section, "urltest_node", new) - return - end - set[v] = nil - end - for _ in pairs(set) do - m:set(section, "urltest_node", new) - return - end - end o = s:option(MultiValue, "node_group", translate("Select Group")) o:depends({ node_add_mode = "batch" }) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua index 0af6b4fc5..9091b6a92 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua @@ -91,6 +91,7 @@ if load_balancing_options then -- [[ Load balancing Start ]] o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, document")) o:depends({ node_add_mode = "manual" }) o.widget = "checkbox" + o.cast = "table" o.template = m:template_path("/cbi/nodes_multivalue") o.group = {} for k1, v1 in pairs(node_list) do @@ -101,28 +102,6 @@ if load_balancing_options then -- [[ Load balancing Start ]] end end end - function o.cfgvalue(self, section) - return table.concat(m:get(section, "balancing_node") or {}, " ") - end - function o.write(self, section, value) - local old = m:get(section, "balancing_node") or {} - local new, set = {}, {} - for v in value:gmatch("%S+") do - new[#new + 1] = v - set[v] = 1 - end - for _, v in ipairs(old) do - if not set[v] then - m:set(section, "balancing_node", new) - return - end - set[v] = nil - end - for _ in pairs(set) do - m:set(section, "balancing_node", new) - return - end - end o = s:option(MultiValue, "node_group", translate("Select Group")) o:depends({ node_add_mode = "batch" }) diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua index edcca378e..c0ef7b770 100644 --- a/luci-app-passwall2/luasrc/passwall2/api.lua +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -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" diff --git a/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.lua b/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.lua index c3e902a31..8520ea009 100644 --- a/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.lua @@ -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" diff --git a/luci-app-passwall2/root/usr/share/passwall2/utils.sh b/luci-app-passwall2/root/usr/share/passwall2/utils.sh index fc5f08902..87eaf2c7b 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/utils.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/utils.sh @@ -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 diff --git a/luci-app-wizard/htdocs/luci-static/resources/view/wizard/initsetup.js b/luci-app-wizard/htdocs/luci-static/resources/view/wizard/initsetup.js index d00a8b679..583e0f321 100644 --- a/luci-app-wizard/htdocs/luci-static/resources/view/wizard/initsetup.js +++ b/luci-app-wizard/htdocs/luci-static/resources/view/wizard/initsetup.js @@ -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', _('ESSID')); - 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 }); diff --git a/luci-app-wizard/luasrc/controller/wizard.lua b/luci-app-wizard/luasrc/controller/wizard.lua index 0f247c6c0..31544c2dd 100644 --- a/luci-app-wizard/luasrc/controller/wizard.lua +++ b/luci-app-wizard/luasrc/controller/wizard.lua @@ -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 diff --git a/luci-app-wizard/po/zh_Hans/wizard.po b/luci-app-wizard/po/zh_Hans/wizard.po index 8117518ec..9fe61ece7 100644 --- a/luci-app-wizard/po/zh_Hans/wizard.po +++ b/luci-app-wizard/po/zh_Hans/wizard.po @@ -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 "立即跳转" diff --git a/luci-app-wizard/root/etc/init.d/wizard b/luci-app-wizard/root/etc/init.d/wizard index 8c3fb606d..2bed96891 100755 --- a/luci-app-wizard/root/etc/init.d/wizard +++ b/luci-app-wizard/root/etc/init.d/wizard @@ -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" } diff --git a/luci-app-wizard/root/etc/uci-defaults/40-luci-wizard b/luci-app-wizard/root/etc/uci-defaults/40-luci-wizard deleted file mode 100644 index 6fbc5380f..000000000 --- a/luci-app-wizard/root/etc/uci-defaults/40-luci-wizard +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -[ ! -f "/usr/share/ucitrack/luci-app-wizard.json" ] && { - cat > /usr/share/ucitrack/luci-app-wizard.json << EEOF -{ - "config": "wizard", - "init": "wizard" -} -EEOF -} - -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 diff --git a/luci-app-wizard/root/usr/share/luci/menu.d/luci-app-wizard.json b/luci-app-wizard/root/usr/share/luci/menu.d/luci-app-wizard.json index cc6810dbf..a8169512d 100644 --- a/luci-app-wizard/root/usr/share/luci/menu.d/luci-app-wizard.json +++ b/luci-app-wizard/root/usr/share/luci/menu.d/luci-app-wizard.json @@ -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" diff --git a/luci-app-wizard/root/usr/share/rpcd/acl.d/luci-app-wizard.json b/luci-app-wizard/root/usr/share/rpcd/acl.d/luci-app-wizard.json index 2641777e5..ddd223ad3 100644 --- a/luci-app-wizard/root/usr/share/rpcd/acl.d/luci-app-wizard.json +++ b/luci-app-wizard/root/usr/share/rpcd/acl.d/luci-app-wizard.json @@ -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" + ] } } } diff --git a/luci-app-wizard/root/usr/share/ucitrack/luci-app-wizard.json b/luci-app-wizard/root/usr/share/ucitrack/luci-app-wizard.json deleted file mode 100644 index 87e0a684a..000000000 --- a/luci-app-wizard/root/usr/share/ucitrack/luci-app-wizard.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "config": "wizard", - "init": "wizard" -} diff --git a/luci-app-wwand/Makefile b/luci-app-wwand/Makefile index c1113923d..663ef7f1d 100644 --- a/luci-app-wwand/Makefile +++ b/luci-app-wwand/Makefile @@ -17,12 +17,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-wwand -PKG_VERSION:=1.6.7_p5 -PKG_RELEASE:=23 +PKG_VERSION:=1.6.7_p11 +PKG_RELEASE:=24 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git -PKG_SOURCE_VERSION:=28b49c1034a2944b6b0b3a76746938eb9813cdd9 +PKG_SOURCE_VERSION:=2ca9a94506f972d89a398d560d5667a809e89d9f PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only diff --git a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/css/custom.css b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/css/custom.css index 5e3e9f41f..b09a2fbc7 100644 --- a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/css/custom.css +++ b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/css/custom.css @@ -8,7 +8,7 @@ .Dashboard { --dashboard-surface-alt: var(--background-color-medium, #f2f4f7); --dashboard-text: var(--text-color-high, #1f2933); - --dashboard-text-muted: var(--text-color-medium, #6b7280); + --dashboard-text-muted: var(--text-color-high, #4b5563); --dashboard-border: var(--border-color-medium, rgba(0, 0, 0, 0.12)); --dashboard-primary: var(--primary-color-high, #1976d2); --dashboard-success: var(--success-color-high, #00ac59); @@ -33,7 +33,7 @@ [data-darkmode="true"] .Dashboard { --dashboard-surface-alt: var(--background-color-medium, #262b34); --dashboard-text: var(--text-color-high, #e6e8eb); - --dashboard-text-muted: var(--text-color-medium, #9aa3ad); + --dashboard-text-muted: var(--text-color-high, #9aa3ad); --dashboard-border: var(--border-color-medium, rgba(255, 255, 255, 0.12)); --dashboard-series-4: #a98cf0; --dashboard-series-5: #e5652c; @@ -46,7 +46,7 @@ :root:not([data-darkmode="false"]) .Dashboard { --dashboard-surface-alt: var(--background-color-medium, #262b34); --dashboard-text: var(--text-color-high, #e6e8eb); - --dashboard-text-muted: var(--text-color-medium, #9aa3ad); + --dashboard-text-muted: var(--text-color-high, #9aa3ad); --dashboard-border: var(--border-color-medium, rgba(255, 255, 255, 0.12)); --dashboard-series-4: #a98cf0; --dashboard-series-5: #e5652c; @@ -337,7 +337,7 @@ .dashboard-bars-stack i.dashboard-bar-mid { background: var(--dashboard-warn); } .dashboard-bars-stack i.dashboard-bar-up { background: var(--dashboard-series-1); } .dashboard-bars-stack i.dashboard-bar-down { background: var(--dashboard-series-2); } -.dashboard-bars-stack i.dashboard-bar-none { background: var(--dashboard-text-muted); } +.dashboard-bars-stack i.dashboard-bar-none { background: var(--dashboard-border); } .dashboard-bars-label { font-size: 10.5px; diff --git a/luci-proto-wwand/Makefile b/luci-proto-wwand/Makefile index 0d6ee0e73..b3dd2a1ee 100644 --- a/luci-proto-wwand/Makefile +++ b/luci-proto-wwand/Makefile @@ -20,12 +20,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-proto-wwand -PKG_VERSION:=1.6.7_p2 -PKG_RELEASE:=12 +PKG_VERSION:=1.6.7_p3 +PKG_RELEASE:=13 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/luci-proto-wwand.git -PKG_SOURCE_VERSION:=ebacded705a3bd5886c42c54ba66a1e26b7e4f42 +PKG_SOURCE_VERSION:=0e8dad065d301320163f65a34469d8f57b44b2da PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only diff --git a/luci-theme-shadcn/.dev/docs/patches.md b/luci-theme-shadcn/.dev/docs/patches.md index 2d43fb35a..0509454e0 100644 --- a/luci-theme-shadcn/.dev/docs/patches.md +++ b/luci-theme-shadcn/.dev/docs/patches.md @@ -10,7 +10,7 @@ Write narrow `[data-page]`/class-scoped overrides with native declarations, stil The `@reference` + `@apply` route still _compiles_ for theme-repo patches and has real upsides — build-time validation (a typo'd utility fails the build; a typo'd `var()` fails silently at runtime) and the shared `dark:`/`md:`/`hover:` vocabulary — native is the default for the size numbers above, not a hard gate. App-shipped patches bypass the build entirely and have always been plain-CSS-only. -Globally-applicable chrome tweaks (e.g. icon opacity) belong in `_shared.css`, not here. +Before writing a patch, ask whether the symptom is really page-specific. Something that breaks the same way on any app's page is a theme bug: fix it in the core CSS so every page gets the fix, and keep globally-applicable chrome tweaks (e.g. icon opacity) in `_shared.css`, not here. A patch is for what is genuinely local to one app — or, for a preference the theme should not adopt for everyone, the global `admin.css` case below. ## Discovery and matching @@ -20,6 +20,8 @@ Globally-applicable chrome tweaks (e.g. icon opacity) belong in `_shared.css`, n Because discovery is at render time, **any package — not just the theme — may drop a `.css` into `luci-static/shadcn/patches/`** and it takes effect immediately, no theme rebuild required. +**`patches/admin.css` is therefore the global patch.** Every admin page's path starts with `admin`, so the shortest prefix matches all of them; sorting loads it first and the page-specific patches cascade on top (the login page loads no patches at all). That is how a user carries a preference the theme should not ship for everyone — a CJK-friendly monospace UI font, say — without forking it: write the file by hand on the device at `/www/luci-static/shadcn/patches/admin.css`. The theme neither builds nor installs that path, so a theme upgrade leaves it alone. + `PATCH_ALIASES` in `vite.config.ts` can duplicate one built payload (CSS and JS) under several page names when unrelated pages share it. It is currently empty — the log viewer ships only `patches/admin-status-logs.js`, whose prefix covers both log tabs on every supported release; its CSS is core-page styling and lives in `components/_syslog.css` inside `main.css`, where the `.syslog-view` markup contract is deliberately global so other packages can reuse the viewer. `_`-prefixed files in `patches/` are `@import`-only fragments, never entries. ## JS payloads diff --git a/luci-theme-shadcn/Makefile b/luci-theme-shadcn/Makefile index 351de5f90..37dd91089 100644 --- a/luci-theme-shadcn/Makefile +++ b/luci-theme-shadcn/Makefile @@ -9,7 +9,7 @@ LUCI_TITLE:=A modern sidebar LuCI theme for OpenWrt, built with shadcn/ui design LUCI_DEPENDS:=+luci-base PKG_VERSION:=0.6.0 -PKG_RELEASE:=6 +PKG_RELEASE:=7 PKG_LICENSE:=Apache-2.0 LUCI_MINIFY_CSS:= diff --git a/mise/Makefile b/mise/Makefile index 5df318d63..9b7024cbf 100644 --- a/mise/Makefile +++ b/mise/Makefile @@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264 MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437 PKG_NAME:=mise -PKG_VERSION:=2026.9.11 -PKG_RELEASE:=17 +PKG_VERSION:=2026.9.12 +PKG_RELEASE:=18 PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/ PKG_HASH:=skip diff --git a/opera-proxy/Makefile b/opera-proxy/Makefile index 1e83d02aa..9b92b4462 100644 --- a/opera-proxy/Makefile +++ b/opera-proxy/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=opera-proxy -PKG_VERSION:=1.29.0 -PKG_RELEASE:=10 +PKG_VERSION:=1.30.0 +PKG_RELEASE:=11 PKG_MAINTAINER:=Konstantine Shevlakov PKG_LICENSE:=MIT diff --git a/shadowsocksr-libev/Makefile b/shadowsocksr-libev/Makefile index 3a20fe4f7..7779fee3b 100644 --- a/shadowsocksr-libev/Makefile +++ b/shadowsocksr-libev/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=shadowsocksr-libev PKG_VERSION:=2.5.6 -PKG_RELEASE:=4 +PKG_RELEASE:=5 PKG_LICENSE:=GPL-3.0 PKG_LICENSE_FILES:=LICENSE diff --git a/shadowsocksr-libev/patches/0002-Revert-verify_simple-and-auth_simple.patch b/shadowsocksr-libev/patches/0002-Revert-verify_simple-and-auth_simple.patch index 0f8114656..ab21c87fd 100644 --- a/shadowsocksr-libev/patches/0002-Revert-verify_simple-and-auth_simple.patch +++ b/shadowsocksr-libev/patches/0002-Revert-verify_simple-and-auth_simple.patch @@ -1,6 +1,6 @@ --- a/src/obfs/obfs.c +++ b/src/obfs/obfs.c -@@ -88,7 +88,7 @@ obfs_class *new_obfs_class(const char *p +@@ -88,10 +88,11 @@ obfs_class *new_obfs_class(const char *p plugin->client_decode = tls12_ticket_auth_client_decode; return plugin; @@ -9,7 +9,19 @@ obfs_class * plugin = (obfs_class*)malloc(sizeof(obfs_class)); plugin->init_data = init_data; plugin->new_obfs = verify_simple_new_obfs; -@@ -115,7 +115,7 @@ obfs_class *new_obfs_class(const char *p ++ plugin->get_overhead = get_overhead; + plugin->get_server_info = get_server_info; + plugin->set_server_info = set_server_info; + plugin->dispose = verify_simple_dispose; +@@ -106,6 +107,7 @@ obfs_class *new_obfs_class(const char *p + obfs_class * plugin = (obfs_class*)malloc(sizeof(obfs_class)); + plugin->init_data = auth_simple_init_data; + plugin->new_obfs = auth_simple_new_obfs; ++ plugin->get_overhead = get_overhead; + plugin->get_server_info = get_server_info; + plugin->set_server_info = set_server_info; + plugin->dispose = auth_simple_dispose; +@@ -115,7 +117,7 @@ obfs_class *new_obfs_class(const char *p plugin->client_udp_pre_encrypt = NULL; plugin->client_udp_post_decrypt = NULL; diff --git a/shadowsocksr-libev/patches/108-Enlarge-the-socket-buffer-size-to-16KB.patch b/shadowsocksr-libev/patches/108-Enlarge-the-socket-buffer-size-to-16KB.patch new file mode 100644 index 000000000..bc52165de --- /dev/null +++ b/shadowsocksr-libev/patches/108-Enlarge-the-socket-buffer-size-to-16KB.patch @@ -0,0 +1,610 @@ +From a7291ea725b74dc64c5806385100c535a15e889e Mon Sep 17 00:00:00 2001 +From: Max Lv +Date: Tue, 30 Apr 2019 09:22:54 +0800 +Subject: [PATCH] Enlarge the socket buffer size to 16KB. + +After five years, memory footprint is not a problem for most of +low-end boxes and high-end routers now. + +It should also improve the speed on loopback and high-speed LAN +networks. +--- + src/local.c | 26 +++++++++++--------------- + src/netutils.h | 2 ++ + src/redir.c | 24 ++++++++++++------------ + src/server.c | 24 ++++++++++-------------- + src/tunnel.c | 18 +++++++----------- + 5 files changed, 42 insertions(+), 52 deletions(-) + +--- a/server/netutils.h ++++ b/server/netutils.h +@@ -60,6 +60,8 @@ + /** byte size of ip6 address */ + #define INET6_SIZE 16 + ++#define SOCKET_BUF_SIZE (16 * 1024) // 16KB ++ + size_t get_sockaddr_len(struct sockaddr *addr); + ssize_t get_sockaddr(char *host, char *port, + struct sockaddr_storage *storage, int block, +--- a/server/server.c ++++ b/server/server.c +@@ -73,10 +73,6 @@ + #define EWOULDBLOCK EAGAIN + #endif + +-#ifndef BUF_SIZE +-#define BUF_SIZE 2048 +-#endif +- + #ifndef SSMAXCONN + #define SSMAXCONN 1024 + #endif +@@ -142,13 +138,13 @@ stat_update_cb(EV_P_ ev_timer *watcher, + struct sockaddr_un svaddr, claddr; + int sfd = -1; + size_t msgLen; +- char resp[BUF_SIZE]; ++ char resp[SOCKET_BUF_SIZE]; + + if (verbose) { + LOGI("update traffic stat: tx: %" PRIu64 " rx: %" PRIu64 "", tx, rx); + } + +- snprintf(resp, BUF_SIZE, "stat: {\"%s\":%" PRIu64 "}", server_port, tx + rx); ++ snprintf(resp, SOCKET_BUF_SIZE, "stat: {\"%s\":%" PRIu64 "}", server_port, tx + rx); + msgLen = strlen(resp) + 1; + + ss_addr_t ip_addr = { .host = NULL, .port = NULL }; +@@ -625,14 +621,14 @@ server_recv_cb(EV_P_ ev_io *w, int reven + ev_timer_again(EV_A_ & server->recv_ctx->watcher); + } + +- if (len > BUF_SIZE) { ++ if (len > SOCKET_BUF_SIZE) { + ERROR("out of recv buffer"); + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; + } + +- ssize_t r = recv(server->fd, buf->array + len, BUF_SIZE - len, 0); ++ ssize_t r = recv(server->fd, buf->array + len, SOCKET_BUF_SIZE - len, 0); + + if (r == 0) { + // connection closed +@@ -712,7 +708,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + } + + if (needsendback) { +- size_t capacity = BUF_SIZE; ++ size_t capacity = SOCKET_BUF_SIZE; + char *sendback_buf = (char*)malloc(capacity); + obfs_class *obfs_plugin = server->obfs_plugin; + if (obfs_plugin->server_encode) { +@@ -725,7 +721,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + } + } + +- int err = ss_decrypt(buf, server->d_ctx, BUF_SIZE); ++ int err = ss_decrypt(buf, server->d_ctx, SOCKET_BUF_SIZE); + + if (err) { + report_addr(server->fd, MALICIOUS); +@@ -797,7 +793,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + + if (server->stage == STAGE_HANDSHAKE) { + size_t header_len = server->header_buf->len; +- if (brealloc(server->header_buf, server->buf->len + header_len, BUF_SIZE) != 0) { ++ if (brealloc(server->header_buf, server->buf->len + header_len, SOCKET_BUF_SIZE) != 0) { + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; +@@ -809,7 +805,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + int ret = is_header_complete(server->buf); + + if (ret == 1) { +- brealloc(server->buf, server->header_buf->len, BUF_SIZE); ++ brealloc(server->buf, server->header_buf->len, SOCKET_BUF_SIZE); + memcpy(server->buf->array, server->header_buf->array, server->header_buf->len); + server->buf->len = server->header_buf->len; + bfree(server->header_buf); +@@ -826,7 +822,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + + // handshake and transmit data + if (server->stage == STAGE_STREAM) { +- if (server->auth && !ss_check_hash(remote->buf, server->chunk, server->d_ctx, BUF_SIZE)) { ++ if (server->auth && !ss_check_hash(remote->buf, server->chunk, server->d_ctx, SOCKET_BUF_SIZE)) { + LOGE("hash error"); + report_addr(server->fd, BAD); + close_and_free_server(EV_A_ server); +@@ -1040,7 +1036,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + LOGI("connect to %s:%d", host, ntohs(port)); + } + +- if (server->auth && !ss_check_hash(server->buf, server->chunk, server->d_ctx, BUF_SIZE)) { ++ if (server->auth && !ss_check_hash(server->buf, server->chunk, server->d_ctx, SOCKET_BUF_SIZE)) { + LOGE("hash error"); + report_addr(server->fd, BAD); + close_and_free_server(EV_A_ server); +@@ -1258,7 +1254,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + + ev_timer_again(EV_A_ & server->recv_ctx->watcher); + +- ssize_t r = recv(remote->fd, server->buf->array, BUF_SIZE, 0); ++ ssize_t r = recv(remote->fd, server->buf->array, SOCKET_BUF_SIZE, 0); + + if (r == 0) { + // connection closed +@@ -1300,7 +1296,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + } + } + +- int err = ss_encrypt(server->buf, server->e_ctx, BUF_SIZE); ++ int err = ss_encrypt(server->buf, server->e_ctx, SOCKET_BUF_SIZE); + + if (err) { + LOGE("invalid password or cipher"); +@@ -1459,7 +1455,7 @@ new_remote(int fd) + ev_io_init(&remote->recv_ctx->io, remote_recv_cb, fd, EV_READ); + ev_io_init(&remote->send_ctx->io, remote_send_cb, fd, EV_WRITE); + +- balloc(remote->buf, BUF_SIZE); ++ balloc(remote->buf, SOCKET_BUF_SIZE); + + return remote; + } +@@ -1538,8 +1534,8 @@ new_server(int fd, listen_ctx_t *listene + ev_timer_init(&server->recv_ctx->watcher, server_timeout_cb, + request_timeout, listener->timeout); + +- balloc(server->buf, BUF_SIZE); +- balloc(server->header_buf, BUF_SIZE); ++ balloc(server->buf, SOCKET_BUF_SIZE); ++ balloc(server->header_buf, SOCKET_BUF_SIZE); + + server->chunk = (chunk_t *)malloc(sizeof(chunk_t)); + memset(server->chunk, 0, sizeof(chunk_t)); +--- a/src/local.c ++++ b/src/local.c +@@ -92,10 +92,6 @@ + #define EWOULDBLOCK EAGAIN + #endif + +-#ifndef BUF_SIZE +-#define BUF_SIZE 2048 +-#endif +- + int verbose = 0; + int keep_resolving = 1; + +@@ -305,7 +301,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + buf = remote->buf; + } + +- r = recv(server->fd, buf->array + buf->len, BUF_SIZE - buf->len, 0); ++ r = recv(server->fd, buf->array + buf->len, SOCKET_BUF_SIZE - buf->len, 0); + + if (r == 0) { + // connection closed +@@ -372,7 +368,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + ); + } + } +- int err = ss_encrypt(&server_env->cipher, remote->buf, server->e_ctx, BUF_SIZE); ++ int err = ss_encrypt(&server_env->cipher, remote->buf, server->e_ctx, SOCKET_BUF_SIZE); + + if (err) { + LOGE("server invalid password or cipher"); +@@ -592,7 +588,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + + buffer_t resp_to_send; + buffer_t *resp_buf = &resp_to_send; +- balloc(resp_buf, BUF_SIZE); ++ balloc(resp_buf, SOCKET_BUF_SIZE); + + memcpy(resp_buf->array, &response, sizeof(struct socks5_response)); + memcpy(resp_buf->array + sizeof(struct socks5_response), +@@ -624,7 +620,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + + buffer_t ss_addr_to_send; + buffer_t *abuf = &ss_addr_to_send; +- balloc(abuf, BUF_SIZE); ++ balloc(abuf, SOCKET_BUF_SIZE); + + abuf->array[abuf->len++] = request->atyp; + int atyp = request->atyp; +@@ -694,7 +690,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + else if (p == tls_protocol->default_port) + ret = tls_protocol->parse_packet(buf->array + 3 + abuf->len, + buf->len - 3 - abuf->len, &hostname); +- if (ret == -1 && buf->len < BUF_SIZE) { ++ if (ret == -1 && buf->len < SOCKET_BUF_SIZE) { + server->stage = STAGE_PARSE; + bfree(abuf); + return; +@@ -880,7 +876,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + _server_info.key = enc_get_key(&server_env->cipher); + _server_info.key_len = enc_get_key_len(&server_env->cipher); + _server_info.tcp_mss = 1452; +- _server_info.buffer_size = BUF_SIZE; ++ _server_info.buffer_size = SOCKET_BUF_SIZE; + _server_info.cipher_env = &server_env->cipher; + + _server_info.param = server_env->obfs_param; +@@ -909,7 +905,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + } + // SSR end + +- brealloc(remote->buf, buf->len + abuf->len, BUF_SIZE); ++ brealloc(remote->buf, buf->len + abuf->len, SOCKET_BUF_SIZE); + memcpy(remote->buf->array, abuf->array, abuf->len); + remote->buf->len = buf->len + abuf->len; + +@@ -1012,7 +1008,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + stat_update_cb(); + #endif + +- ssize_t r = recv(remote->fd, server->buf->array, BUF_SIZE, 0); ++ ssize_t r = recv(remote->fd, server->buf->array, SOCKET_BUF_SIZE, 0); + + if (r == 0) { + // connection closed +@@ -1094,7 +1090,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + } + } + if (server->buf->len > 0) { +- int err = ss_decrypt(&server_env->cipher, server->buf, server->d_ctx, BUF_SIZE); ++ int err = ss_decrypt(&server_env->cipher, server->buf, server->d_ctx, SOCKET_BUF_SIZE); + if (err) { + LOGE("remote invalid password or cipher"); + close_and_free_remote(EV_A_ remote); +@@ -1220,7 +1216,7 @@ new_remote(int fd, int timeout) { + remote->buf = ss_malloc(sizeof(buffer_t)); + remote->recv_ctx = ss_malloc(sizeof(remote_ctx_t)); + remote->send_ctx = ss_malloc(sizeof(remote_ctx_t)); +- balloc(remote->buf, BUF_SIZE); ++ balloc(remote->buf, SOCKET_BUF_SIZE); + memset(remote->recv_ctx, 0, sizeof(remote_ctx_t)); + memset(remote->send_ctx, 0, sizeof(remote_ctx_t)); + remote->recv_ctx->connected = 0; +@@ -1276,7 +1272,7 @@ new_server(int fd, listen_ctx_t *profile + server->recv_ctx = ss_malloc(sizeof(server_ctx_t)); + server->send_ctx = ss_malloc(sizeof(server_ctx_t)); + server->buf = ss_malloc(sizeof(buffer_t)); +- balloc(server->buf, BUF_SIZE); ++ balloc(server->buf, SOCKET_BUF_SIZE); + memset(server->recv_ctx, 0, sizeof(server_ctx_t)); + memset(server->send_ctx, 0, sizeof(server_ctx_t)); + server->stage = STAGE_INIT; +--- a/src/netutils.h ++++ b/src/netutils.h +@@ -60,6 +60,8 @@ + /** byte size of ip6 address */ + #define INET6_SIZE 16 + ++#define SOCKET_BUF_SIZE (16 * 1024) // 16KB ++ + size_t get_sockaddr_len(struct sockaddr *addr); + ssize_t get_sockaddr(char *host, char *port, + struct sockaddr_storage *storage, int block, +--- a/src/redir.c ++++ b/src/redir.c +@@ -63,10 +63,6 @@ + #define EWOULDBLOCK EAGAIN + #endif + +-#ifndef BUF_SIZE +-#define BUF_SIZE 2048 +-#endif +- + #ifndef IP6T_SO_ORIGINAL_DST + #define IP6T_SO_ORIGINAL_DST 80 + #endif +@@ -226,7 +222,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + server_def_t *server_env = server->server_env; + + ssize_t r = recv(server->fd, remote->buf->array + remote->buf->len, +- BUF_SIZE - remote->buf->len, 0); ++ SOCKET_BUF_SIZE - remote->buf->len, 0); + + if (r == 0) { + // connection closed +@@ -299,7 +295,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + remote->buf->len = protocol_plugin->client_pre_encrypt(server->protocol, &remote->buf->array, remote->buf->len, &remote->buf->capacity); + } + } +- int err = ss_encrypt(&server_env->cipher, remote->buf, server->e_ctx, BUF_SIZE); ++ int err = ss_encrypt(&server_env->cipher, remote->buf, server->e_ctx, SOCKET_BUF_SIZE); + + if (err) { + LOGE("invalid password or cipher"); +@@ -416,7 +412,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + + ev_timer_again(EV_A_ & remote->recv_ctx->watcher); + +- ssize_t r = recv(remote->fd, server->buf->array, BUF_SIZE, 0); ++ ssize_t r = recv(remote->fd, server->buf->array, SOCKET_BUF_SIZE, 0); + + if (r == 0) { + // connection closed +@@ -482,7 +478,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + if ( server->buf->len == 0 ) + return; + +- int err = ss_decrypt(&server_env->cipher, server->buf, server->d_ctx, BUF_SIZE); ++ int err = ss_decrypt(&server_env->cipher, server->buf, server->d_ctx, SOCKET_BUF_SIZE); + if (err) { + LOGE("invalid password or cipher"); + close_and_free_remote(EV_A_ remote); +@@ -550,7 +546,7 @@ remote_send_cb(EV_P_ ev_io *w, int reven + // send destaddr + buffer_t ss_addr_to_send; + buffer_t *abuf = &ss_addr_to_send; +- balloc(abuf, BUF_SIZE); ++ balloc(abuf, SOCKET_BUF_SIZE); + + if (server->hostname_len > 0 + && validate_hostname(server->hostname, server->hostname_len)) { // HTTP/SNI +@@ -591,12 +587,12 @@ remote_send_cb(EV_P_ ev_io *w, int reven + abuf->len += 2; + + if (remote->buf->len > 0) { +- brealloc(remote->buf, remote->buf->len + abuf->len, BUF_SIZE); ++ brealloc(remote->buf, remote->buf->len + abuf->len, SOCKET_BUF_SIZE); + memmove(remote->buf->array + abuf->len, remote->buf->array, remote->buf->len); + memcpy(remote->buf->array, abuf->array, abuf->len); + remote->buf->len += abuf->len; + } else { +- brealloc(remote->buf, abuf->len, BUF_SIZE); ++ brealloc(remote->buf, abuf->len, SOCKET_BUF_SIZE); + memcpy(remote->buf->array, abuf->array, abuf->len); + remote->buf->len = abuf->len; + } +@@ -616,7 +612,7 @@ remote_send_cb(EV_P_ ev_io *w, int reven + } + } + +- int err = ss_encrypt(&server_env->cipher, remote->buf, server->e_ctx, BUF_SIZE); ++ int err = ss_encrypt(&server_env->cipher, remote->buf, server->e_ctx, SOCKET_BUF_SIZE); + if (err) { + LOGE("invalid password or cipher"); + close_and_free_remote(EV_A_ remote); +@@ -683,7 +679,7 @@ new_remote(int fd, int timeout) + remote->buf = ss_malloc(sizeof(buffer_t)); + remote->recv_ctx = ss_malloc(sizeof(remote_ctx_t)); + remote->send_ctx = ss_malloc(sizeof(remote_ctx_t)); +- balloc(remote->buf, BUF_SIZE); ++ balloc(remote->buf, SOCKET_BUF_SIZE); + memset(remote->recv_ctx, 0, sizeof(remote_ctx_t)); + memset(remote->send_ctx, 0, sizeof(remote_ctx_t)); + remote->recv_ctx->connected = 0; +@@ -741,7 +737,7 @@ new_server(int fd, listen_ctx_t* profile + server->recv_ctx = ss_malloc(sizeof(server_ctx_t)); + server->send_ctx = ss_malloc(sizeof(server_ctx_t)); + server->buf = ss_malloc(sizeof(buffer_t)); +- balloc(server->buf, BUF_SIZE); ++ balloc(server->buf, SOCKET_BUF_SIZE); + memset(server->recv_ctx, 0, sizeof(server_ctx_t)); + memset(server->send_ctx, 0, sizeof(server_ctx_t)); + server->recv_ctx->connected = 0; +@@ -1028,7 +1024,7 @@ accept_cb(EV_P_ ev_io *w, int revents) + _server_info.key = enc_get_key(&server_env->cipher); + _server_info.key_len = enc_get_key_len(&server_env->cipher); + _server_info.tcp_mss = 1452; +- _server_info.buffer_size = BUF_SIZE; ++ _server_info.buffer_size = SOCKET_BUF_SIZE; + _server_info.cipher_env = &server_env->cipher; + + if (server_env->obfs_plugin) { +--- a/src/server.c ++++ b/src/server.c +@@ -71,10 +71,6 @@ + #define EWOULDBLOCK EAGAIN + #endif + +-#ifndef BUF_SIZE +-#define BUF_SIZE 2048 +-#endif +- + #ifndef SSMAXCONN + #define SSMAXCONN 1024 + #endif +@@ -138,13 +134,13 @@ stat_update_cb(EV_P_ ev_timer *watcher, + + int sfd = -1; + size_t msgLen; +- char resp[BUF_SIZE]; ++ char resp[SOCKET_BUF_SIZE]; + + if (verbose) { + LOGI("update traffic stat: tx: %" PRIu64 " rx: %" PRIu64 "", tx, rx); + } + +- snprintf(resp, BUF_SIZE, "stat: {\"%s\":%" PRIu64 "}", server_port, tx + rx); ++ snprintf(resp, SOCKET_BUF_SIZE, "stat: {\"%s\":%" PRIu64 "}", server_port, tx + rx); + msgLen = strlen(resp) + 1; + + ss_addr_t ip_addr = { .host = NULL, .port = NULL }; +@@ -600,14 +596,14 @@ server_recv_cb(EV_P_ ev_io *w, int reven + ev_timer_again(EV_A_ & server->recv_ctx->watcher); + } + +- if (len > BUF_SIZE) { ++ if (len > SOCKET_BUF_SIZE) { + ERROR("out of recv buffer"); + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; + } + +- ssize_t r = recv(server->fd, buf->array + len, BUF_SIZE - len, 0); ++ ssize_t r = recv(server->fd, buf->array + len, SOCKET_BUF_SIZE - len, 0); + + if (r == 0) { + // connection closed +@@ -649,7 +645,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + buf->len = r; + } + +- int err = ss_decrypt(&cipher_env, buf, server->d_ctx, BUF_SIZE); ++ int err = ss_decrypt(&cipher_env, buf, server->d_ctx, SOCKET_BUF_SIZE); + + if (err) { + report_addr(server->fd, MALICIOUS); +@@ -678,7 +674,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + + if (server->stage == STAGE_HANDSHAKE) { + size_t header_len = server->header_buf->len; +- brealloc(server->header_buf, server->buf->len + header_len, BUF_SIZE); ++ brealloc(server->header_buf, server->buf->len + header_len, SOCKET_BUF_SIZE); + memcpy(server->header_buf->array + header_len, + server->buf->array, server->buf->len); + server->header_buf->len = server->buf->len + header_len; +@@ -686,7 +682,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + int ret = is_header_complete(server->buf); + + if (ret == 1) { +- brealloc(server->buf, server->header_buf->len, BUF_SIZE); ++ brealloc(server->buf, server->header_buf->len, SOCKET_BUF_SIZE); + memcpy(server->buf->array, server->header_buf->array, server->header_buf->len); + server->buf->len = server->header_buf->len; + bfree(server->header_buf); +@@ -1086,7 +1082,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + + ev_timer_again(EV_A_ & server->recv_ctx->watcher); + +- ssize_t r = recv(remote->fd, server->buf->array, BUF_SIZE, 0); ++ ssize_t r = recv(remote->fd, server->buf->array, SOCKET_BUF_SIZE, 0); + + if (r == 0) { + // connection closed +@@ -1112,7 +1108,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + rx += r; + + server->buf->len = r; +- int err = ss_encrypt(&cipher_env, server->buf, server->e_ctx, BUF_SIZE); ++ int err = ss_encrypt(&cipher_env, server->buf, server->e_ctx, SOCKET_BUF_SIZE); + + if (err) { + LOGE("invalid password or cipher"); +@@ -1249,7 +1245,7 @@ new_remote(int fd) + remote->recv_ctx = ss_malloc(sizeof(remote_ctx_t)); + remote->send_ctx = ss_malloc(sizeof(remote_ctx_t)); + remote->buf = ss_malloc(sizeof(buffer_t)); +- balloc(remote->buf, BUF_SIZE); ++ balloc(remote->buf, SOCKET_BUF_SIZE); + memset(remote->recv_ctx, 0, sizeof(remote_ctx_t)); + memset(remote->send_ctx, 0, sizeof(remote_ctx_t)); + remote->fd = fd; +@@ -1313,8 +1309,8 @@ new_server(int fd, listen_ctx_t *listene + server->header_buf = ss_malloc(sizeof(buffer_t)); + memset(server->recv_ctx, 0, sizeof(server_ctx_t)); + memset(server->send_ctx, 0, sizeof(server_ctx_t)); +- balloc(server->buf, BUF_SIZE); +- balloc(server->header_buf, BUF_SIZE); ++ balloc(server->buf, SOCKET_BUF_SIZE); ++ balloc(server->header_buf, SOCKET_BUF_SIZE); + server->fd = fd; + server->recv_ctx->server = server; + server->recv_ctx->connected = 0; +--- a/src/tunnel.c ++++ b/src/tunnel.c +@@ -67,10 +67,6 @@ + #define EWOULDBLOCK EAGAIN + #endif + +-#ifndef BUF_SIZE +-#define BUF_SIZE 2048 +-#endif +- + #include "includeobfs.h" // I don't want to modify makefile + + static void accept_cb(EV_P_ ev_io *w, int revents); +@@ -180,7 +176,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + return; + } + +- ssize_t r = recv(server->fd, remote->buf->array, BUF_SIZE, 0); ++ ssize_t r = recv(server->fd, remote->buf->array, SOCKET_BUF_SIZE, 0); + + if (r == 0) { + // connection closed +@@ -209,7 +205,7 @@ server_recv_cb(EV_P_ ev_io *w, int reven + remote->buf->len = protocol_plugin->client_pre_encrypt(server->protocol, &remote->buf->array, remote->buf->len, &remote->buf->capacity); + } + } +- int err = ss_encrypt(&cipher_env, remote->buf, server->e_ctx, BUF_SIZE); ++ int err = ss_encrypt(&cipher_env, remote->buf, server->e_ctx, SOCKET_BUF_SIZE); + + if (err) { + LOGE("server invalid password or cipher"); +@@ -325,7 +321,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + remote_t *remote = remote_recv_ctx->remote; + server_t *server = remote->server; + +- ssize_t r = recv(remote->fd, server->buf->array, BUF_SIZE, 0); ++ ssize_t r = recv(remote->fd, server->buf->array, SOCKET_BUF_SIZE, 0); + + if (r == 0) { + // connection closed +@@ -391,7 +387,7 @@ remote_recv_cb(EV_P_ ev_io *w, int reven + if ( server->buf->len == 0 ) + return; + +- int err = ss_decrypt(&cipher_env, server->buf, server->d_ctx, BUF_SIZE); ++ int err = ss_decrypt(&cipher_env, server->buf, server->d_ctx, SOCKET_BUF_SIZE); + + if (err) { + LOGE("invalid password or cipher"); +@@ -456,7 +452,7 @@ remote_send_cb(EV_P_ ev_io *w, int reven + + buffer_t ss_addr_to_send; + buffer_t *abuf = &ss_addr_to_send; +- balloc(abuf, BUF_SIZE); ++ balloc(abuf, SOCKET_BUF_SIZE); + + ss_addr_t *sa = &server->destaddr; + struct cork_ip ip; +@@ -503,12 +499,12 @@ remote_send_cb(EV_P_ ev_io *w, int reven + abuf->len += 2; + + if (remote->buf->len > 0) { +- brealloc(remote->buf, remote->buf->len + abuf->len, BUF_SIZE); ++ brealloc(remote->buf, remote->buf->len + abuf->len, SOCKET_BUF_SIZE); + memmove(remote->buf->array + abuf->len, remote->buf->array, remote->buf->len); + memcpy(remote->buf->array, abuf->array, abuf->len); + remote->buf->len += abuf->len; + } else { +- brealloc(remote->buf, abuf->len, BUF_SIZE); ++ brealloc(remote->buf, abuf->len, SOCKET_BUF_SIZE); + memcpy(remote->buf->array, abuf->array, abuf->len); + remote->buf->len = abuf->len; + } +@@ -528,7 +524,7 @@ remote_send_cb(EV_P_ ev_io *w, int reven + } + } + +- int err = ss_encrypt(&cipher_env, remote->buf, server->e_ctx, BUF_SIZE); ++ int err = ss_encrypt(&cipher_env, remote->buf, server->e_ctx, SOCKET_BUF_SIZE); + if (err) { + LOGE("invalid password or cipher"); + close_and_free_remote(EV_A_ remote); +@@ -610,7 +606,7 @@ new_remote(int fd, int timeout) + remote->buf = ss_malloc(sizeof(buffer_t)); + remote->recv_ctx = ss_malloc(sizeof(remote_ctx_t)); + remote->send_ctx = ss_malloc(sizeof(remote_ctx_t)); +- balloc(remote->buf, BUF_SIZE); ++ balloc(remote->buf, SOCKET_BUF_SIZE); + memset(remote->recv_ctx, 0, sizeof(remote_ctx_t)); + memset(remote->send_ctx, 0, sizeof(remote_ctx_t)); + remote->fd = fd; +@@ -665,7 +661,7 @@ new_server(int fd, int method) + server->buf = ss_malloc(sizeof(buffer_t)); + server->recv_ctx = ss_malloc(sizeof(server_ctx_t)); + server->send_ctx = ss_malloc(sizeof(server_ctx_t)); +- balloc(server->buf, BUF_SIZE); ++ balloc(server->buf, SOCKET_BUF_SIZE); + memset(server->recv_ctx, 0, sizeof(server_ctx_t)); + memset(server->send_ctx, 0, sizeof(server_ctx_t)); + server->fd = fd; diff --git a/shadowsocksr-libev/patches/109-Make-sure-socket-buffer-size-no-larger-than-max-chunk-siz.patch b/shadowsocksr-libev/patches/109-Make-sure-socket-buffer-size-no-larger-than-max-chunk-siz.patch new file mode 100644 index 000000000..e16a74fe2 --- /dev/null +++ b/shadowsocksr-libev/patches/109-Make-sure-socket-buffer-size-no-larger-than-max-chunk-siz.patch @@ -0,0 +1,31 @@ +From 38e83d6b5d4648fb87ae6680c8fa159df54c73ce Mon Sep 17 00:00:00 2001 +From: Max Lv +Date: Tue, 30 Apr 2019 09:34:31 +0800 +Subject: [PATCH] Make sure socket buffer size no larger than max chunk size + +--- + src/netutils.h | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/server/netutils.h ++++ b/server/netutils.h +@@ -60,7 +60,7 @@ + /** byte size of ip6 address */ + #define INET6_SIZE 16 + +-#define SOCKET_BUF_SIZE (16 * 1024) // 16KB ++#define SOCKET_BUF_SIZE (16 * 1024 - 1) // 16383 Byte, equals to the max chunk size + + size_t get_sockaddr_len(struct sockaddr *addr); + ssize_t get_sockaddr(char *host, char *port, +--- a/src/netutils.h ++++ b/src/netutils.h +@@ -60,7 +60,7 @@ + /** byte size of ip6 address */ + #define INET6_SIZE 16 + +-#define SOCKET_BUF_SIZE (16 * 1024) // 16KB ++#define SOCKET_BUF_SIZE (16 * 1024 - 1) // 16383 Byte, equals to the max chunk size + + size_t get_sockaddr_len(struct sockaddr *addr); + ssize_t get_sockaddr(char *host, char *port, diff --git a/shadowsocksr-libev/patches/110-Allow-larger-TCP-reads-in-SSR-protocol-reassembly.patch b/shadowsocksr-libev/patches/110-Allow-larger-TCP-reads-in-SSR-protocol-reassembly.patch new file mode 100644 index 000000000..f4fd52a07 --- /dev/null +++ b/shadowsocksr-libev/patches/110-Allow-larger-TCP-reads-in-SSR-protocol-reassembly.patch @@ -0,0 +1,205 @@ +--- a/server/auth.c ++++ b/server/auth.c +@@ -27,7 +27,7 @@ typedef struct auth_simple_local_data { + + void auth_simple_local_data_init(auth_simple_local_data* local) { + local->has_sent_header = 0; +- local->recv_buffer = (char*)malloc(16384); ++ local->recv_buffer = (char*)malloc(SSR_RECV_BUF_SIZE); + local->recv_buffer_size = 0; + local->recv_id = 1; + local->pack_id = 1; +@@ -173,7 +173,7 @@ int auth_simple_client_post_decrypt(obfs + char *plaindata = *pplaindata; + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +@@ -290,7 +290,7 @@ int auth_sha1_client_post_decrypt(obfs * + char *plaindata = *pplaindata; + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +@@ -428,7 +428,7 @@ int auth_sha1_v2_client_post_decrypt(obf + char *plaindata = *pplaindata; + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +@@ -586,7 +586,7 @@ int auth_sha1_v4_client_post_decrypt(obf + char *plaindata = *pplaindata; + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +@@ -843,7 +843,7 @@ int auth_aes128_sha1_client_post_decrypt + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + //server_info *server = (server_info*)&self->server; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +--- a/server/obfs.h ++++ b/server/obfs.h +@@ -10,6 +10,9 @@ + #include + #include + ++/* Keep room for a TCP read, a pending obfs record and a protocol frame. */ ++#define SSR_RECV_BUF_SIZE (32 * 1024) ++ + typedef struct server_info { + char host[64]; + uint16_t port; +--- a/server/verify.c ++++ b/server/verify.c +@@ -9,7 +9,7 @@ typedef struct verify_simple_local_data + }verify_simple_local_data; + + void verify_simple_local_data_init(verify_simple_local_data* local) { +- local->recv_buffer = (char*)malloc(16384); ++ local->recv_buffer = (char*)malloc(SSR_RECV_BUF_SIZE); + local->recv_buffer_size = 0; + } + +@@ -74,7 +74,7 @@ int verify_simple_client_post_decrypt(ob + char *plaindata = *pplaindata; + verify_simple_local_data *local = (verify_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +@@ -144,7 +144,7 @@ int verify_simple_server_post_decrypt(ob + char *plaindata = *pplaindata; + verify_simple_local_data *local = (verify_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + { + LOGE("verify_simple: wrong buf length %d", local->recv_buffer_size + datalength); + return -1; +--- a/src/obfs/auth.c ++++ b/src/obfs/auth.c +@@ -33,7 +33,7 @@ typedef struct auth_simple_local_data { + + void auth_simple_local_data_init(auth_simple_local_data* local) { + local->has_sent_header = 0; +- local->recv_buffer = (char*)malloc(16384); ++ local->recv_buffer = (char*)malloc(SSR_RECV_BUF_SIZE); + local->recv_buffer_size = 0; + local->recv_id = 1; + local->pack_id = 1; +@@ -176,7 +176,7 @@ int auth_simple_client_post_decrypt(obfs + char *plaindata = *pplaindata; + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +@@ -293,7 +293,7 @@ int auth_sha1_client_post_decrypt(obfs * + char *plaindata = *pplaindata; + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +@@ -431,7 +431,7 @@ int auth_sha1_v2_client_post_decrypt(obf + char *plaindata = *pplaindata; + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +@@ -589,7 +589,7 @@ int auth_sha1_v4_client_post_decrypt(obf + char *plaindata = *pplaindata; + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +@@ -859,7 +859,7 @@ int auth_aes128_sha1_client_post_decrypt + auth_simple_local_data *local = (auth_simple_local_data*)self->l_data; + //server_info *server = (server_info*)&self->server; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +--- a/src/obfs/auth_chain.c ++++ b/src/obfs/auth_chain.c +@@ -130,7 +130,7 @@ typedef struct auth_chain_local_data { + + void auth_chain_local_data_init(auth_chain_local_data *local) { + local->has_sent_header = 0; +- local->recv_buffer = (char *) malloc(16384); ++ local->recv_buffer = (char *) malloc(SSR_RECV_BUF_SIZE); + local->recv_buffer_size = 0; + local->recv_id = 1; + local->pack_id = 1; +@@ -904,7 +904,7 @@ int auth_chain_a_client_post_decrypt(obf + auth_chain_local_data *local = (auth_chain_local_data *) self->l_data; + server_info *server = (server_info *) &self->server; + uint8_t *recv_buffer = (uint8_t *) local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; +--- a/src/obfs/obfs.h ++++ b/src/obfs/obfs.h +@@ -13,6 +13,9 @@ + + #define OBFS_HMAC_SHA1_LEN 10 + ++/* Keep room for a TCP read, a pending obfs record and a protocol frame. */ ++#define SSR_RECV_BUF_SIZE (32 * 1024) ++ + typedef struct { + char host[256]; + uint16_t port; +--- a/src/obfs/verify.c ++++ b/src/obfs/verify.c +@@ -12,7 +12,7 @@ typedef struct verify_simple_local_data + }verify_simple_local_data; + + void verify_simple_local_data_init(verify_simple_local_data* local) { +- local->recv_buffer = (char*)malloc(16384); ++ local->recv_buffer = (char*)malloc(SSR_RECV_BUF_SIZE); + local->recv_buffer_size = 0; + } + +@@ -77,7 +77,7 @@ int verify_simple_client_post_decrypt(ob + char *plaindata = *pplaindata; + verify_simple_local_data *local = (verify_simple_local_data*)self->l_data; + uint8_t * recv_buffer = (uint8_t *)local->recv_buffer; +- if (local->recv_buffer_size + datalength > 16384) ++ if (local->recv_buffer_size + datalength > SSR_RECV_BUF_SIZE) + return -1; + memmove(recv_buffer + local->recv_buffer_size, plaindata, datalength); + local->recv_buffer_size += datalength; diff --git a/wwand/Makefile b/wwand/Makefile index 951873582..9ce8011bf 100644 --- a/wwand/Makefile +++ b/wwand/Makefile @@ -11,6 +11,7 @@ # wwand-ncm - NCM/ECM backend (DEPENDS wwand) # wwand-mhi - PCIe/MHI transport + drivers (DEPENDS wwand; add a backend) # wwand-esim - eSIM management (DEPENDS wwand + lpac; no backend) +# wwand-gps - GNSS/NMEA glue for ugps (DEPENDS wwand + ugps; no backend) # A typical QMI router installs `wwand-qmi` (which pulls in `wwand`); add # `wwand-mbim` / `wwand-ncm` for those modems, `wwand-mhi` for a PCIe/MHI modem. # The ucode tree ships PRECOMPILED to bytecode (repo-root CMakeLists.txt, @@ -49,12 +50,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=wwand -PKG_VERSION:=1.6.7_p46 -PKG_RELEASE:=47 +PKG_VERSION:=1.6.7_p55 +PKG_RELEASE:=48 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git -PKG_SOURCE_VERSION:=6ae4cab239ecd02e570d7cd51584d60101704732 +PKG_SOURCE_VERSION:=aaf2ba064bbf751eaa0237e00f94c1619f05b648 PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only @@ -437,6 +438,45 @@ define Package/wwand-esim/install $(INSTALL_DATA) $(WWAND_UCODE)/esim_bridge.uc $(1)$(UCDIR)/ endef +# --------------------------------------------------------------------------- +# wwand-gps: point ugps at the modem's NMEA port. Backend-neutral — finding the +# port and starting the receiver are both in the base package, and neither +# depends on the control protocol. +# --------------------------------------------------------------------------- +define Package/wwand-gps + SECTION:=net + CATEGORY:=Network + SUBMENU:=WWAN + TITLE:=GNSS/NMEA glue for wwand (ugps) + # ugps is OpenWrt base: it parses NMEA, can set the clock, and publishes a + # `gps` ubus object. It knows nothing about modems, and it reads its tty from + # a STATIC /etc/config/gps — which is exactly the gap this package fills, + # because wwand's NMEA port is discovered and can move between boots or when + # a modem is replaced. + DEPENDS:=+wwand +ugps +endef + +define Package/wwand-gps/description + Joins three pieces that already existed with nothing between them: wwand finds + the modem's NMEA port during enumeration (reported as `gps_port`), `option + gnss` starts the receiver with the vendor AT command — QMI's LOC service is + broken on Quectel and AT is what works, and only wwand has the port — and ugps + reads the NMEA and publishes a position. + + What this adds is the config write that connects them, plus a `modem_gps` ubus + method that answers with both halves at once: the port and receiver state wwand + knows, merged with whatever ugps reports. + + Good citizen: it manages exactly one `config gps` section and only one it + created itself (marked `option wwand '1'`). An operator's own section — a hat + GPS on a serial port, a second receiver — is never touched or repointed. +endef + +define Package/wwand-gps/install + $(INSTALL_DIR) $(1)$(UCDIR) + $(INSTALL_DATA) $(WWAND_UCODE)/gps.uc $(1)$(UCDIR)/ +endef + # --------------------------------------------------------------------------- # wwand-datapath-rmnet_nss: Qualcomm NSS offload behind the VENDOR qmi_wwan_q. # A datapath add-on, not a control backend — it plugs into wwand's datapath @@ -527,6 +567,7 @@ $(eval $(call BuildPackage,wwand-mbim)) $(eval $(call BuildPackage,wwand-ncm)) $(eval $(call BuildPackage,wwand-mhi)) $(eval $(call BuildPackage,wwand-esim)) +$(eval $(call BuildPackage,wwand-gps)) $(eval $(call BuildPackage,wwand-apntest)) define Package/wwand-datapath-rmnet_nss_mhi SECTION:=net