Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package mihomo for openSUSE:Factory checked in at 2026-10-02 23:03:05 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/mihomo (Old) and /work/SRC/openSUSE:Factory/.mihomo.new.1631729 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "mihomo" Fri Oct 2 23:03:05 2026 rev:6 rq:1381965 version:1.19.32 Changes: -------- --- /work/SRC/openSUSE:Factory/mihomo/mihomo.changes 2026-09-16 17:44:29.442399104 +0200 +++ /work/SRC/openSUSE:Factory/.mihomo.new.1631729/mihomo.changes 2026-10-02 23:03:59.129904374 +0200 @@ -1,0 +2,24 @@ +Thu Oct 1 19:37:16 UTC 2026 - Martin Pluskal <[email protected]> + +- Update to version 1.19.32: + * The default IP stack is now mips, for the tun listener too + (#3264), and tun gains a "congestion-controller" option + * Add a load-balance hash-key, to pin a session to the inbound + user (#3133) + * mipstack: RX checksum offload, lazy receive buffer reads, TCP + options counted in the effective MSS, and unconnected-UDP and + raw-IP ICMP errors aligned with Linux + * OpenVPN: exclude the opcode from the P_DATA_V1 AEAD additional + data (#3237) + * Fix the EasyTier outbound not restarting after a silent overlay + failure (#3215), an anytls race in idleCleanupExpTime() + (#3225), a nil pconn deref when the context is cancelled + during h2 ClientConn setup, missing half-close handling in + sing-mux, no UDP InUser metadata on the mieru inbound (#3236), + and HWCap not being populated from auxv on Linux + * xhttp: extra.headers is now supported (#3230) + * Maintenance: mieru to v3.38.0 (#3242), plus the gvisor, utls, + mipstack, sing, sing-mux and sing-tun updates this vendor tree + carries + +------------------------------------------------------------------- Old: ---- mihomo-1.19.31.tar.gz New: ---- mihomo-1.19.32.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ mihomo.spec ++++++ --- /var/tmp/diff_new_pack.eK6sKG/_old 2026-10-02 23:04:00.333954769 +0200 +++ /var/tmp/diff_new_pack.eK6sKG/_new 2026-10-02 23:04:00.335954852 +0200 @@ -17,7 +17,7 @@ Name: mihomo -Version: 1.19.31 +Version: 1.19.32 Release: 0 Summary: The universal proxy platform # Legal-Review-Notice: mihomo's own code is GPL-3.0-only, but the Go binary ++++++ _service ++++++ --- /var/tmp/diff_new_pack.eK6sKG/_old 2026-10-02 23:04:00.386956987 +0200 +++ /var/tmp/diff_new_pack.eK6sKG/_new 2026-10-02 23:04:00.390957155 +0200 @@ -3,7 +3,7 @@ <service name="tar_scm" mode="manual"> <param name="url">https://github.com/MetaCubeX/mihomo.git</param> <param name="scm">git</param> - <param name="revision">v1.19.31</param> + <param name="revision">v1.19.32</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="match-tag">v*</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.eK6sKG/_old 2026-10-02 23:04:00.419958368 +0200 +++ /var/tmp/diff_new_pack.eK6sKG/_new 2026-10-02 23:04:00.423958536 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/MetaCubeX/mihomo.git</param> - <param name="changesrevision">ab405bad5beeeac8b003bb01f60f134f6df54471</param></service></servicedata> + <param name="changesrevision">88dcbf7f1614a67c3b36b848ee3592dfa92ada36</param></service></servicedata> (No newline at EOF) ++++++ mihomo-1.19.31.tar.gz -> mihomo-1.19.32.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/adapter/outbound/base.go new/mihomo-1.19.32/adapter/outbound/base.go --- old/mihomo-1.19.31/adapter/outbound/base.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/adapter/outbound/base.go 2026-09-30 16:35:49.000000000 +0200 @@ -274,8 +274,20 @@ c.ExtendedConn = N.NewRefConn(c.ExtendedConn, ref) // add ref for autoCloseProxyAdapter } +// ipstackConn is a common experimental API interface implemented by: +// *mipstack.TCPConn, *mipstack.UDPConn, *mipstack.IPConn +// it's also implemented by our gVisor fork +type ipstackConn interface { + ReadWithBuffer(getBuffer func(sizeHint int) []byte) (int, error) +} + func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { - if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn + switch c.(type) { + case syscall.Conn: // exclusion system conn like *net.TCPConn + break + case ipstackConn: // exclusion *mipstack.TCPConn + break + default: c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly } cc := &conn{N.NewExtendedConn(c), nil, nil, a.Addr()} @@ -341,7 +353,12 @@ func NewPacketConn(pc net.PacketConn, a ProxyAdapter) C.PacketConn { epc := N.NewEnhancePacketConn(pc) - if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn + switch pc.(type) { + case syscall.Conn: // exclusion system conn like *net.UDPConn + break + case ipstackConn: // exclusion *mipstack.UDPConn + break + default: epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly } cpc := &packetConn{epc, nil, nil, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/adapter/outbound/easytier.go new/mihomo-1.19.32/adapter/outbound/easytier.go --- old/mihomo-1.19.31/adapter/outbound/easytier.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/adapter/outbound/easytier.go 2026-09-30 16:35:49.000000000 +0200 @@ -12,6 +12,7 @@ "path/filepath" "strings" "sync" + "time" "github.com/metacubex/mihomo/component/easytier" "github.com/metacubex/mihomo/component/resolver" @@ -27,6 +28,8 @@ easyTierDefaultStateDir = "easytier" easyTierInstanceIDFile = "instance_id" easyTierDNSTTL = 60 + easyTierMinBackoff = time.Second + easyTierMaxBackoff = 30 * time.Second ) var errEasyTierClosed = errors.New("easytier outbound closed") @@ -40,8 +43,10 @@ zone string ctx context.Context cancel context.CancelFunc - startOnce sync.Once - startErr error + loopOnce sync.Once + startMu sync.Mutex + closed bool + readyCh chan struct{} mu sync.Mutex host *corehost.Host instance *corehost.Instance @@ -164,26 +169,115 @@ return outbound, nil } -func (e *EasyTier) start() error { - e.startOnce.Do(func() { - if err := e.init(); err != nil { - e.startErr = err - _ = e.shutdown() - } +func (e *EasyTier) ensureStarted(ctx context.Context) error { + e.loopOnce.Do(func() { + go e.loop() }) - return e.startErr + for { + if err := e.ctx.Err(); err != nil { + return errEasyTierClosed + } + e.startMu.Lock() + closed := e.closed + readyCh := e.readyCh + e.startMu.Unlock() + if closed { + return errEasyTierClosed + } + e.mu.Lock() + instance := e.instance + e.mu.Unlock() + if instance != nil && instance.State() == corehost.StateRunning { + return nil + } + if readyCh == nil { + e.startMu.Lock() + if e.readyCh == nil { + e.readyCh = make(chan struct{}) + } + readyCh = e.readyCh + e.startMu.Unlock() + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-e.ctx.Done(): + return errEasyTierClosed + case <-readyCh: + } + } } -func (e *EasyTier) ensureStarted(ctx context.Context) error { - done := make(chan error, 1) - go func() { - done <- e.start() - }() - select { - case err := <-done: - return err - case <-ctx.Done(): - return ctx.Err() +func (e *EasyTier) signalReady() { + e.startMu.Lock() + if e.readyCh != nil { + close(e.readyCh) + e.readyCh = nil + } + e.startMu.Unlock() +} + +func (e *EasyTier) loop() { + backoff := easyTierMinBackoff + for { + if e.ctx.Err() != nil { + return + } + e.startMu.Lock() + closed := e.closed + e.startMu.Unlock() + if closed { + return + } + err := e.init() + if err != nil { + log.Warnln("[EasyTier](%s) start failed: %v; retry in %s", e.Name(), err, backoff) + _ = e.shutdown() + timer := time.NewTimer(backoff) + select { + case <-e.ctx.Done(): + timer.Stop() + return + case <-timer.C: + } + if backoff < easyTierMaxBackoff { + backoff *= 2 + if backoff > easyTierMaxBackoff { + backoff = easyTierMaxBackoff + } + } + continue + } + backoff = easyTierMinBackoff + e.signalReady() + reason := e.serve() + _ = e.shutdown() + if e.ctx.Err() != nil { + return + } + e.startMu.Lock() + closed = e.closed + e.startMu.Unlock() + if closed { + return + } + if reason == "" { + reason = "instance stopped" + } + log.Warnln("[EasyTier](%s) %s; restarting in %s", e.Name(), reason, backoff) + timer := time.NewTimer(backoff) + select { + case <-e.ctx.Done(): + timer.Stop() + return + case <-timer.C: + } + if backoff < easyTierMaxBackoff { + backoff *= 2 + if backoff > easyTierMaxBackoff { + backoff = easyTierMaxBackoff + } + } } } @@ -233,6 +327,41 @@ return e.instance, nil } +func (e *EasyTier) serve() string { + e.mu.Lock() + instance := e.instance + e.mu.Unlock() + if instance == nil { + return "instance is not ready" + } + // Manual connectors retry inside core (reconnect_interval, default 1s). + // Events() is best-effort: a full host queue drops the event and does not + // stall the guest. Drain it for logs; recreate only when the stream closes. + events := instance.Events() + if events == nil { + if err := instance.Wait(e.ctx); err != nil && e.ctx.Err() == nil { + return err.Error() + } + return "instance stopped" + } + for { + select { + case <-e.ctx.Done(): + return "" + case event, ok := <-events: + if !ok { + return "instance stopped" + } + switch event.Kind { + case "peer_added", "peer_removed": + log.Infoln("[EasyTier](%s) %s: %s", e.Name(), event.Kind, event.Message) + default: + log.Debugln("[EasyTier](%s) %s: %s", e.Name(), event.Kind, event.Message) + } + } + } +} + func (e *EasyTier) overlayNodes(ctx context.Context) ([]easytier.Node, error) { instance, err := e.currentInstance() if err != nil { @@ -378,9 +507,13 @@ if e.unregister != nil { e.unregister() } - e.startOnce.Do(func() { - e.startErr = errEasyTierClosed - }) + e.startMu.Lock() + e.closed = true + if e.readyCh != nil { + close(e.readyCh) + e.readyCh = nil + } + e.startMu.Unlock() return e.shutdown() } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/adapter/outbound/wireguard.go new/mihomo-1.19.32/adapter/outbound/wireguard.go --- old/mihomo-1.19.31/adapter/outbound/wireguard.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/adapter/outbound/wireguard.go 2026-09-30 16:35:49.000000000 +0200 @@ -193,11 +193,7 @@ func newIPStack(option IPStackOption, localAddresses []netip.Prefix, mtu uint32) (ipStack, error) { mode := option.Mode if mode == ipStackAuto { - if features.WithGVisor { - mode = ipStackGVisor - } else { - mode = ipStackMips - } + mode = ipStackMips } switch mode { case ipStackGVisor: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/adapter/outboundgroup/loadbalance.go new/mihomo-1.19.32/adapter/outboundgroup/loadbalance.go --- old/mihomo-1.19.31/adapter/outboundgroup/loadbalance.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/adapter/outboundgroup/loadbalance.go 2026-09-30 16:35:49.000000000 +0200 @@ -21,6 +21,7 @@ type LoadBalanceOption struct { Strategy string `group:"strategy,omitempty"` + HashKey string `group:"hash-key,omitempty"` } type LoadBalance struct { @@ -34,6 +35,10 @@ type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy var errStrategy = errors.New("unsupported strategy") +var errHashKey = errors.New("unsupported hash-key") + +// keyFn derives the value a hashing strategy pins a request on. +type keyFn = func(metadata *C.Metadata) string func getKey(metadata *C.Metadata) string { if metadata == nil { @@ -68,6 +73,39 @@ return fmt.Sprintf("%s%s", src, dst) } +// getKeyWithInUser pins on the authenticated inbound user instead of on an +// address. Both address-derived keys assume one client's traffic to one +// destination is one unit of work, which is false for a client whose single +// unit of work walks several destinations: the hash moves with the host, and +// the egress IP changes underneath a session the destination is tracking. +// The inbound user is the only identity the client itself controls, and +// `IN-USER` rules already match on it -- hence the option value `in-user`, +// which names the same thing those rules do. An unauthenticated request keeps +// the strategy's own key rather than collapsing every such request onto one +// node. +func getKeyWithInUser(fallback keyFn) keyFn { + return func(metadata *C.Metadata) string { + if metadata != nil && metadata.InUser != "" { + return metadata.InUser + } + + return fallback(metadata) + } +} + +// hashKey resolves the `hash-key` option into a decorator over whichever key +// the chosen strategy derives by default. +func hashKey(name string) (func(keyFn) keyFn, error) { + switch name { + case "": + return func(fn keyFn) keyFn { return fn }, nil + case "in-user": + return getKeyWithInUser, nil + } + + return nil, fmt.Errorf("%w: %s", errHashKey, name) +} + func jumpHash(key uint64, buckets int32) int32 { var b, j int64 @@ -155,10 +193,10 @@ } } -func strategyConsistentHashing(url string) strategyFn { +func strategyConsistentHashing(url string, keyOf keyFn) strategyFn { maxRetry := 5 return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { - key := utils.MapHash(getKey(metadata)) + key := utils.MapHash(keyOf(metadata)) buckets := int32(len(proxies)) for i := 0; i < maxRetry; i, key = i+1, key+1 { idx := jumpHash(key, buckets) @@ -179,14 +217,14 @@ } } -func strategyStickySessions(url string) strategyFn { +func strategyStickySessions(url string, keyOf keyFn) strategyFn { ttl := time.Minute * 10 maxRetry := 5 lruCache := lru.New[uint64, int]( lru.WithAge[uint64, int](int64(ttl.Seconds())), lru.WithSize[uint64, int](1000)) return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { - key := utils.MapHash(getKeyWithSrcAndDst(metadata)) + key := utils.MapHash(keyOf(metadata)) length := len(proxies) idx, has := lruCache.Get(key) if !has || idx >= length { @@ -249,13 +287,22 @@ func NewLoadBalance(option GroupCommonOption, loadBalanceOption LoadBalanceOption, emptyFallback C.Proxy, providers []P.ProxyProvider) (lb *LoadBalance, err error) { var strategyFn strategyFn + withKey, err := hashKey(loadBalanceOption.HashKey) + if err != nil { + return nil, err + } switch loadBalanceOption.Strategy { case "", "consistent-hashing": - strategyFn = strategyConsistentHashing(option.URL) + strategyFn = strategyConsistentHashing(option.URL, withKey(getKey)) case "round-robin": + // Rejected rather than ignored: round-robin hashes nothing, so a + // hash-key here means the config expects stickiness it will not get. + if loadBalanceOption.HashKey != "" { + return nil, fmt.Errorf("%w: round-robin does not hash", errHashKey) + } strategyFn = strategyRoundRobin(option.URL) case "sticky-sessions": - strategyFn = strategyStickySessions(option.URL) + strategyFn = strategyStickySessions(option.URL, withKey(getKeyWithSrcAndDst)) default: return nil, fmt.Errorf("%w: %s", errStrategy, loadBalanceOption.Strategy) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/adapter/outboundgroup/loadbalance_test.go new/mihomo-1.19.32/adapter/outboundgroup/loadbalance_test.go --- old/mihomo-1.19.31/adapter/outboundgroup/loadbalance_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/mihomo-1.19.32/adapter/outboundgroup/loadbalance_test.go 2026-09-30 16:35:49.000000000 +0200 @@ -0,0 +1,140 @@ +package outboundgroup + +import ( + "net/netip" + "testing" + + "github.com/metacubex/mihomo/adapter" + "github.com/metacubex/mihomo/adapter/outbound" + C "github.com/metacubex/mihomo/constant" + + "github.com/stretchr/testify/require" +) + +const testUrl = "https://www.gstatic.com/generate_204" + +func balancedProxies(count int) []C.Proxy { + proxies := make([]C.Proxy, 0, count) + for i := 0; i < count; i++ { + proxies = append(proxies, adapter.NewProxy(outbound.NewDirect())) + } + return proxies +} + +// The proxies are indistinguishable by name, so identity is the pointer. +func indexOf(t *testing.T, proxies []C.Proxy, selected C.Proxy) int { + t.Helper() + for i, proxy := range proxies { + if proxy == selected { + return i + } + } + require.Fail(t, "selected proxy is not a member of the group") + return -1 +} + +func request(user, host string) *C.Metadata { + return &C.Metadata{ + NetWork: C.TCP, + Host: host, + DstPort: 443, + SrcIP: netip.MustParseAddr("127.0.0.1"), + InUser: user, + } +} + +// One unit of work walking several destinations is the case both address-derived +// keys get wrong: the group is meant to hold that work on one egress, and the +// default key moves it as soon as the host changes. +func TestLoadBalanceHashKeyInUserSurvivesADestinationChange(t *testing.T) { + proxies := balancedProxies(8) + hosts := []string{"a.example.com", "b.example.org", "c.example.net", "d.example.io"} + + byUser := strategyConsistentHashing(testUrl, getKeyWithInUser(getKey)) + pinned := indexOf(t, proxies, byUser(proxies, request("job-1", hosts[0]), false)) + for _, host := range hosts { + selected := byUser(proxies, request("job-1", host), false) + require.Equal(t, pinned, indexOf(t, proxies, selected), + "hash-key: user must ignore the destination") + } + + byDestination := strategyConsistentHashing(testUrl, getKey) + seen := map[int]bool{} + for _, host := range hosts { + seen[indexOf(t, proxies, byDestination(proxies, request("job-1", host), false))] = true + } + require.Greater(t, len(seen), 1, + "the default key is expected to move with the destination") +} + +// Pinning must not become a single node: distinct users still spread. +func TestLoadBalanceHashKeyInUserSpreadsUsers(t *testing.T) { + proxies := balancedProxies(8) + strategy := strategyConsistentHashing(testUrl, getKeyWithInUser(getKey)) + + seen := map[int]bool{} + for _, user := range []string{"job-1", "job-2", "job-3", "job-4", "job-5", "job-6"} { + selected := strategy(proxies, request(user, "a.example.com"), false) + seen[indexOf(t, proxies, selected)] = true + } + require.Greater(t, len(seen), 1) +} + +// Sticky sessions keys on source and destination; a client behind one source +// address cannot separate its own concurrent jobs without a supplied identity. +func TestLoadBalanceHashKeyInUserSeparatesJobsSharingASourceAddress(t *testing.T) { + proxies := balancedProxies(8) + strategy := strategyStickySessions(testUrl, getKeyWithInUser(getKeyWithSrcAndDst)) + + first := indexOf(t, proxies, strategy(proxies, request("job-1", "a.example.com"), false)) + require.Equal(t, first, + indexOf(t, proxies, strategy(proxies, request("job-1", "b.example.org"), false))) + + shared := strategyStickySessions(testUrl, getKeyWithSrcAndDst) + require.Equal(t, + indexOf(t, proxies, shared(proxies, request("job-1", "a.example.com"), false)), + indexOf(t, proxies, shared(proxies, request("job-2", "a.example.com"), false)), + "without a supplied key the two jobs are one session") +} + +// An unauthenticated request keeps the strategy's own key. Returning a constant +// instead would herd every anonymous request onto one member. +func TestLoadBalanceHashKeyInUserFallsBackWhenUnauthenticated(t *testing.T) { + keyed := getKeyWithInUser(getKey) + require.Equal(t, "example.com", keyed(request("", "a.example.com"))) + require.Equal(t, "job-1", keyed(request("job-1", "a.example.com"))) + require.Equal(t, getKey(nil), keyed(nil)) +} + +// The option name is the contract with the config file, and nothing else here +// exercises it: every other test reaches the decorator directly, so renaming +// the case would leave them all green while `hash-key: in-user` stopped working. +func TestLoadBalanceHashKeyResolvesTheOptionName(t *testing.T) { + withInUser, err := hashKey("in-user") + require.NoError(t, err) + require.Equal(t, "job-1", withInUser(getKey)(request("job-1", "a.example.com"))) + + identity, err := hashKey("") + require.NoError(t, err) + require.Equal(t, getKey(request("job-1", "a.example.com")), + identity(getKey)(request("job-1", "a.example.com"))) +} + +func TestLoadBalanceHashKeyRejectsUnusableConfigs(t *testing.T) { + _, err := hashKey("session") + require.ErrorIs(t, err, errHashKey) + + // `user` was the name this option carried before review. Rejecting it keeps + // the rename honest: without this the case above could still read `user` + // and every test here would stay green. + _, err = hashKey("user") + require.ErrorIs(t, err, errHashKey) + + _, err = NewLoadBalance(GroupCommonOption{Name: "lb"}, + LoadBalanceOption{Strategy: "round-robin", HashKey: "in-user"}, nil, nil) + require.ErrorIs(t, err, errHashKey) + + _, err = NewLoadBalance(GroupCommonOption{Name: "lb"}, + LoadBalanceOption{Strategy: "consistent-hashing", HashKey: "nonsense"}, nil, nil) + require.ErrorIs(t, err, errHashKey) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/common/convert/v.go new/mihomo-1.19.32/common/convert/v.go --- old/mihomo-1.19.31/common/convert/v.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/common/convert/v.go 2026-09-30 16:35:49.000000000 +0200 @@ -196,6 +196,10 @@ return reuse } + if headers, ok := extra["headers"].(map[string]any); ok && len(headers) > 0 { + opts["headers"] = headers + } + if v, ok := extra["noGRPCHeader"].(bool); ok && v { opts["no-grpc-header"] = true } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/common/net/packet/packet.go new/mihomo-1.19.32/common/net/packet/packet.go --- old/mihomo-1.19.31/common/net/packet/packet.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/common/net/packet/packet.go 2026-09-30 16:35:49.000000000 +0200 @@ -25,6 +25,9 @@ if singPC, isSingPC := pc.(SingPacketConn); isSingPC { return newEnhanceSingPacketConn(singPC) } + if withBufferPC, isWithBufferPC := pc.(withBufferPacketConn); isWithBufferPC { + return &enhanceWithBufferPacketConn{withBufferPacketConn: withBufferPC} + } return &enhancePacketConn{PacketConn: pc} } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/common/net/packet/packet_with_buffer.go new/mihomo-1.19.32/common/net/packet/packet_with_buffer.go --- old/mihomo-1.19.31/common/net/packet/packet_with_buffer.go 1970-01-01 01:00:00.000000000 +0100 +++ new/mihomo-1.19.32/common/net/packet/packet_with_buffer.go 2026-09-30 16:35:49.000000000 +0200 @@ -0,0 +1,48 @@ +package packet + +import ( + "net" + + "github.com/metacubex/mihomo/common/pool" +) + +type withBufferPacketConn interface { + net.PacketConn + ReadFromWithBuffer(func(sizeHint int) []byte) (int, net.Addr, error) +} + +type enhanceWithBufferPacketConn struct { + withBufferPacketConn +} + +func (c *enhanceWithBufferPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { + var readBuf []byte + getBuffer := func(sizeHint int) []byte { + readBuf = pool.Get(sizeHint) + put = func() { + _ = pool.Put(readBuf) + } + return readBuf + } + var readN int + readN, addr, err = c.ReadFromWithBuffer(getBuffer) + if readN > 0 && readBuf != nil { + data = readBuf[:readN] + } else if put != nil { + put() + put = nil + } + return +} + +func (c *enhanceWithBufferPacketConn) Upstream() any { + return c.withBufferPacketConn +} + +func (c *enhanceWithBufferPacketConn) WriterReplaceable() bool { + return true +} + +func (c *enhanceWithBufferPacketConn) ReaderReplaceable() bool { + return true +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/config/config.go new/mihomo-1.19.32/config/config.go --- old/mihomo-1.19.31/config/config.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/config/config.go 2026-09-30 16:35:49.000000000 +0200 @@ -314,6 +314,7 @@ UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` ICMPTimeout int64 `yaml:"icmp-timeout" json:"icmp-timeout,omitempty"` DisableICMPForwarding bool `yaml:"disable-icmp-forwarding" json:"disable-icmp-forwarding,omitempty"` + CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` @@ -543,7 +544,7 @@ Tun: RawTun{ Enable: false, Device: "", - Stack: C.TunGvisor, + Stack: C.TunMips, DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query AutoRoute: true, AutoDetectInterface: true, @@ -1733,6 +1734,7 @@ UDPTimeout: rawTun.UDPTimeout, ICMPTimeout: rawTun.ICMPTimeout, DisableICMPForwarding: rawTun.DisableICMPForwarding, + CongestionController: rawTun.CongestionController, FileDescriptor: rawTun.FileDescriptor, Inet4RouteAddress: rawTun.Inet4RouteAddress, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/docs/config.yaml new/mihomo-1.19.32/docs/config.yaml --- old/mihomo-1.19.31/docs/config.yaml 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/docs/config.yaml 2026-09-30 16:35:49.000000000 +0200 @@ -133,7 +133,7 @@ # Tun 配置 tun: enable: false - stack: system # gvisor/mixed/mips + stack: mips # gvisor/mixed/system dns-hijack: - 0.0.0.0:53 # 需要劫持的 DNS # auto-detect-interface: true # 自动识别出口网卡 @@ -144,6 +144,7 @@ auto-redirect: false # 自动配置 iptables 以重定向 TCP 连接。仅支持 Linux。带有 auto-redirect 的 auto-route 现在可以在路由器上按预期工作,无需干预。 # strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问 # disable-icmp-forwarding: true # 禁用 ICMP 转发,防止某些情况下的 ICMP 环回问题,ping 将不会显示真实的延迟 + # congestion-controller: cubic # TCP congestion controller: cubic, reno, bbr, or bbr3; only effective on mips route-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 不匹配的流量将绕过路由, 仅支持 Linux,且需要 nftables,`auto-route` 和 `auto-redirect` 已启用。 - ruleset-1 - ruleset-2 @@ -1294,7 +1295,7 @@ # reserved: [209,98,59] # persistent-keepalive: 0 # ip-stack: - # mode: auto # options: auto, gvisor, mips; auto uses gVisor when compiled in and mihomo IP stack (MIPS) otherwise + # mode: auto # options: auto, mips, gvisor; auto uses mihomo IP stack (MIPS), gvisor needs compiled with -tags with_gvisor # congestion-controller: cubic # TCP congestion controller: cubic, reno, bbr, or bbr3; ignored by gVisor # 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接 # dialer-proxy: "ss1" @@ -1373,7 +1374,7 @@ # mtu: 1400 # optional local MTU override; cannot exceed the controller MTU # physical-mtu: 1432 # optional ZeroTier UDP payload MTU (510-10324; default 1432) # ip-stack: - # mode: auto # options: auto, gvisor, mips; auto uses gVisor when compiled in and mihomo IP stack (MIPS) otherwise + # mode: auto # options: auto, mips, gvisor; auto uses mihomo IP stack (MIPS), gvisor needs compiled with -tags with_gvisor # congestion-controller: cubic # TCP congestion controller: cubic, reno, bbr, or bbr3; ignored by gVisor # primary-port: 0 # primary UDP port; 0 selects an available port # secondary-port: 0 # second UDP port; 0 selects an available port, -1 disables it @@ -1494,7 +1495,7 @@ # mtu: 1500 udp: true # ip-stack: - # mode: auto # options: auto, gvisor, mips; auto uses gVisor when compiled in and mihomo IP stack (MIPS) otherwise + # mode: auto # options: auto, mips, gvisor; auto uses mihomo IP stack (MIPS), gvisor needs compiled with -tags with_gvisor # congestion-controller: cubic # TCP congestion controller: cubic, reno, bbr, or bbr3; ignored by gVisor # 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接 # dialer-proxy: "ss1" @@ -1513,7 +1514,7 @@ mtu: 1280 udp: true # ip-stack: - # mode: auto # options: auto, gvisor, mips; auto uses gVisor when compiled in and mihomo IP stack (MIPS) otherwise + # mode: auto # options: auto, mips, gvisor; auto uses mihomo IP stack (MIPS), gvisor needs compiled with -tags with_gvisor # congestion-controller: cubic # TCP congestion controller: cubic, reno, bbr, or bbr3; ignored by gVisor # 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接 # dialer-proxy: "ss1" @@ -1551,7 +1552,7 @@ udp: true network: h2 # ip-stack: - # mode: auto # options: auto, gvisor, mips; auto uses gVisor when compiled in and mihomo IP stack (MIPS) otherwise + # mode: auto # options: auto, mips, gvisor; auto uses mihomo IP stack (MIPS), gvisor needs compiled with -tags with_gvisor # congestion-controller: cubic # TCP congestion controller: cubic, reno, bbr, or bbr3; ignored by gVisor # 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接 # dialer-proxy: "ss1" @@ -1790,6 +1791,15 @@ url: "https://cp.cloudflare.com/generate_204" interval: 300 # strategy: consistent-hashing # 可选 round-robin 和 sticky-sessions + # hash-key: in-user # 可选,仅 consistent-hashing 和 sticky-sessions 支持; + # # round-robin 不做哈希,配置它会报错而不是被忽略。 + # # 只改变「拿什么值去哈希」,不改变策略拿到哈希值之后如何选节点; + # # 不填时行为与旧版完全一致。 + # # in-user 用 inbound 的认证用户名作 key(与 IN-USER 规则读取的是 + # # 同一个字段),它不随目标地址变化,因此一件跨多个域名的任务不会 + # # 中途换节点、换出口 IP。未认证的请求回退到该策略原本的 key。 + # # 两种策略给出的一致性时长不同(前者永久、后者 10 分钟), + # # 取舍见 PR #3133。 # select 用户自行选择节点 - name: Proxy @@ -2788,7 +2798,7 @@ type: tun # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) - stack: system # gvisor / mixed / mips + stack: mips # gvisor / mixed / system dns-hijack: - 0.0.0.0:53 # 需要劫持的 DNS # auto-detect-interface: false # 自动识别出口网卡 @@ -2830,6 +2840,7 @@ # exclude-package: # 排除被路由的 Android 应用包名 # - com.android.captiveportallogin # disable-icmp-forwarding: true # 禁用 ICMP 转发,防止某些情况下的 ICMP 环回问题,ping 将不会显示真实的延迟 + # congestion-controller: cubic # TCP congestion controller: cubic, reno, bbr, or bbr3; only effective on mips # 入口配置与 Listener 等价,传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理 # shadowsocks,vmess 入口配置(传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理) # ss-config: ss://2022-blake3-aes-256-gcm:vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=@:23456 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/go.mod new/mihomo-1.19.32/go.mod --- old/mihomo-1.19.31/go.mod 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/go.mod 2026-09-30 16:35:49.000000000 +0200 @@ -7,7 +7,7 @@ github.com/coreos/go-iptables v0.8.0 github.com/dlclark/regexp2 v1.12.0 github.com/easytier/easytier/easytier-go v0.0.0-20260910071355-3d0c9c3ca5e2 - github.com/enfein/mieru/v3 v3.37.0 + github.com/enfein/mieru/v3 v3.38.0 github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.4.0 github.com/golang/snappy v1.0.0 @@ -19,27 +19,27 @@ github.com/metacubex/chacha v0.1.5 github.com/metacubex/chi v0.1.1 github.com/metacubex/connect-ip-go v0.0.0-20260727083417-67ccdb0cf771 - github.com/metacubex/cpu v0.1.1 + github.com/metacubex/cpu v0.1.2 github.com/metacubex/edwards25519 v1.2.0 github.com/metacubex/fswatch v0.1.1 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 - github.com/metacubex/http v0.1.7 + github.com/metacubex/http v0.1.8 github.com/metacubex/jls-quic-go v0.0.0-20260727080412-732f2fc9a34d github.com/metacubex/jls-tls v0.0.0-20260723084315-67adc0e2f796 github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 github.com/metacubex/mhurl v0.1.0 - github.com/metacubex/mipstack v0.0.0-20260910230046-ba762df4c91d + github.com/metacubex/mipstack v0.0.0-20260930071539-961d4b1c1983 github.com/metacubex/mlkem v0.1.0 github.com/metacubex/quic-go v0.61.1-0.20260727080200-2548683b76f4 github.com/metacubex/randv2 v0.2.0 github.com/metacubex/restls-client-go v0.1.9 github.com/metacubex/sevenzip v1.6.4 - github.com/metacubex/sing v0.5.7 - github.com/metacubex/sing-mux v0.3.10 + github.com/metacubex/sing v0.5.8 + github.com/metacubex/sing-mux v0.3.12 github.com/metacubex/sing-quic v0.0.0-20260904234848-1c242664697a github.com/metacubex/sing-shadowsocks v0.2.13 github.com/metacubex/sing-shadowsocks2 v0.2.8 - github.com/metacubex/sing-tun v0.4.24 + github.com/metacubex/sing-tun v0.4.27 github.com/metacubex/sing-vmess v0.2.5 github.com/metacubex/sing-wireguard v0.0.0-20260826105301-c3ae17d19f9e github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141 @@ -47,7 +47,7 @@ github.com/metacubex/tailscale v0.0.0-20260821153257-ff0ecd818181 github.com/metacubex/tfo-go v0.0.0-20260623020846-376a77860b8c github.com/metacubex/tls v0.1.8 - github.com/metacubex/utls v1.8.7 + github.com/metacubex/utls v1.8.8 github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f github.com/metacubex/zerotier-go v0.0.0-20260813124750-13fa6f45da5f github.com/mroth/weightedrand/v2 v2.1.0 @@ -61,7 +61,7 @@ gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 go.uber.org/automaxprocs v1.6.0 go.yaml.in/yaml/v3 v3.0.5 - go4.org/netipx v0.0.0-20231129151722-fdeea329fbba + go4.org/netipx v0.0.0-20260823151212-3075585bcbeb ) // lastest version compatible with golang1.20 @@ -110,7 +110,7 @@ github.com/klauspost/reedsolomon v1.12.3 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/metacubex/ascon v0.1.0 // indirect - github.com/metacubex/gvisor v0.0.0-20260826100401-79317d808312 // indirect + github.com/metacubex/gvisor v0.0.0-20260922041103-e2cbcd6e7400 // indirect github.com/metacubex/hkdf v0.1.0 // indirect github.com/metacubex/hpke v0.1.0 // indirect github.com/metacubex/jsonv2 v0.0.0-20260721082349-16b4998c8f89 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/go.sum new/mihomo-1.19.32/go.sum --- old/mihomo-1.19.31/go.sum 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/go.sum 2026-09-30 16:35:49.000000000 +0200 @@ -29,8 +29,8 @@ github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/easytier/easytier/easytier-go v0.0.0-20260910071355-3d0c9c3ca5e2 h1:2/9iRUcMR3QPrT2XFf/i7QV5YoSHPFjkgBM5tP8xbcc= github.com/easytier/easytier/easytier-go v0.0.0-20260910071355-3d0c9c3ca5e2/go.mod h1:fCEi5+K2yt+sPDONSWO1uNpOifrwPgFGvm3J0YrFRvk= -github.com/enfein/mieru/v3 v3.37.0 h1:DKBDeBmlbpNKiOXtKXqxJo/BF+KvJKnbChn6fryeyHg= -github.com/enfein/mieru/v3 v3.37.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= +github.com/enfein/mieru/v3 v3.38.0 h1:+DuixHFoCGEYEopoEg8fHmDJ5Ak30PuXuf1GIh2Xpu0= +github.com/enfein/mieru/v3 v3.38.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= @@ -109,22 +109,22 @@ github.com/metacubex/chi v0.1.1/go.mod h1:/CvDXe8jZD/ecU8Y7fmS5XKLRR44UDmC6/IBkRIb6Z4= github.com/metacubex/connect-ip-go v0.0.0-20260727083417-67ccdb0cf771 h1:nLGBvwQ2vmsD+gU/XWBQH3xaYCM3vQCJIqPfw3mc/L0= github.com/metacubex/connect-ip-go v0.0.0-20260727083417-67ccdb0cf771/go.mod h1:9FjDcopUc+tgUva3dauAVzrYPHd0rant+zkvyECzkMg= -github.com/metacubex/cpu v0.1.1 h1:rRV5HGmeuGzjiKI3hYbL0dCd0qGwM7VUtk4ICXD06mI= -github.com/metacubex/cpu v0.1.1/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU= +github.com/metacubex/cpu v0.1.2 h1:xJHPWLkGilSWm6vEvzz8Loe+UusODRkVnuXz96TWAZo= +github.com/metacubex/cpu v0.1.2/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU= github.com/metacubex/edwards25519 v1.2.0 h1:pIQZLBsjQgg3Nl/c86YYFEUAbL5qQRnPq4LrgIw0KK4= github.com/metacubex/edwards25519 v1.2.0/go.mod h1:NCQF3J/Ki7382FJuokwsywEIIEI/gro/3smyXgQJsx0= github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU= github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= -github.com/metacubex/gvisor v0.0.0-20260826100401-79317d808312 h1:GDSN5oSkugLGImDSM0W4UCovF5FLpTa6vL1g2f2+1do= -github.com/metacubex/gvisor v0.0.0-20260826100401-79317d808312/go.mod h1:mBJW3UXUusd8ZYO5M6mig0uScIey9iIubgdIiQAk2ug= +github.com/metacubex/gvisor v0.0.0-20260922041103-e2cbcd6e7400 h1:r9YO+xZUWi+xQROzdYkKL2twoliSJh34vOHMVAqsaL8= +github.com/metacubex/gvisor v0.0.0-20260922041103-e2cbcd6e7400/go.mod h1:mBJW3UXUusd8ZYO5M6mig0uScIey9iIubgdIiQAk2ug= github.com/metacubex/hkdf v0.1.0 h1:fPA6VzXK8cU1foc/TOmGCDmSa7pZbxlnqhl3RNsthaA= github.com/metacubex/hkdf v0.1.0/go.mod h1:3seEfds3smgTAXqUGn+tgEJH3uXdsUjOiduG/2EtvZ4= github.com/metacubex/hpke v0.1.0 h1:gu2jUNhraehWi0P/z5HX2md3d7L1FhPQE6/Q0E9r9xQ= github.com/metacubex/hpke v0.1.0/go.mod h1:vfDm6gfgrwlXUxKDkWbcE44hXtmc1uxLDm2BcR11b3U= -github.com/metacubex/http v0.1.7 h1:dEaQmijUaR/2QKqgjBUmPZCRuh1zBfDqB6ZRNLwvbHg= -github.com/metacubex/http v0.1.7/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= +github.com/metacubex/http v0.1.8 h1:d91onDan2ZXYE8W/uAy5oFmXHH2xi1L7LbGPV5EmCM0= +github.com/metacubex/http v0.1.8/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= github.com/metacubex/jls-quic-go v0.0.0-20260727080412-732f2fc9a34d h1:OF3TUGKdHRrRMp7nb0Pa72QYwKPol9NqSy2yTcP2Sog= github.com/metacubex/jls-quic-go v0.0.0-20260727080412-732f2fc9a34d/go.mod h1:dH8StPlpZ7atSUuTnfTnRXvJK22nOtQVimNwQ9Gmk58= github.com/metacubex/jls-tls v0.0.0-20260723084315-67adc0e2f796 h1:1iI4np/Dm1ztCfTW2LoN166O6n728HXMc11aIazYdps= @@ -135,8 +135,8 @@ github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604/go.mod h1:lpmN3m269b3V5jFCWtffqBLS4U3QQoIid9ugtO+OhVc= github.com/metacubex/mhurl v0.1.0 h1:ZdW4Zxe3j3uJ89gNytOazHu6kbHn5owutN/VfXOI8GE= github.com/metacubex/mhurl v0.1.0/go.mod h1:2qpQImCbXoUs6GwJrjuEXKelPyoimsIXr07eNKZdS00= -github.com/metacubex/mipstack v0.0.0-20260910230046-ba762df4c91d h1:hc1OeKdo7YIcmTrzwlJLjNZ4EZDKRHi/Ntv7GdYs2YI= -github.com/metacubex/mipstack v0.0.0-20260910230046-ba762df4c91d/go.mod h1:+bbwALZI0pbi2auSG5A3ptdpV2DZ2eLObziXo+P7oj0= +github.com/metacubex/mipstack v0.0.0-20260930071539-961d4b1c1983 h1:BGb9PDJrrZSsDnJyeFWFY9v6uTr6/LxokQgwuHVAsDI= +github.com/metacubex/mipstack v0.0.0-20260930071539-961d4b1c1983/go.mod h1:+bbwALZI0pbi2auSG5A3ptdpV2DZ2eLObziXo+P7oj0= github.com/metacubex/mlkem v0.1.0 h1:wFClitonSFcmipzzQvax75beLQU+D7JuC+VK1RzSL8I= github.com/metacubex/mlkem v0.1.0/go.mod h1:amhaXZVeYNShuy9BILcR7P0gbeo/QLZsnqCdL8U2PDQ= github.com/metacubex/nftables v0.0.0-20260426003805-208c2c1ba2cb h1:wk6mHYPURSUvWcUv72gNP79oiylFsscBSDPJ6ieV6Iw= @@ -153,18 +153,18 @@ github.com/metacubex/restls-client-go v0.1.9/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g= github.com/metacubex/sevenzip v1.6.4 h1:OIL+DeOeSAbKNsjqxcYUMiarRmX6Kaxakb0GT7E9Oik= github.com/metacubex/sevenzip v1.6.4/go.mod h1:FP3X9bzFKj9wPxifGN9B3w2fIEicMjzKYIGIhnu+1pw= -github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM= -github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= -github.com/metacubex/sing-mux v0.3.10 h1:r5CuZ/KuwFsEcRRwpLvzLncW4fDzNfmSEcBEWcy/+94= -github.com/metacubex/sing-mux v0.3.10/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc= +github.com/metacubex/sing v0.5.8 h1:OwJDSPP+pza6Ywt7NMR4kEYq+UNrsIywSk/eAMlqxz8= +github.com/metacubex/sing v0.5.8/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= +github.com/metacubex/sing-mux v0.3.12 h1:80jrJsGgRW7ZHpJCRpwJhG3yzT5muEHIWjaOsDJZ2wE= +github.com/metacubex/sing-mux v0.3.12/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc= github.com/metacubex/sing-quic v0.0.0-20260904234848-1c242664697a h1:qZzQwtWDrPU1LXoMow0/iLHY3mb7uauS/mho8hYcenE= github.com/metacubex/sing-quic v0.0.0-20260904234848-1c242664697a/go.mod h1:6ayFGfzzBE85csgQkM3gf4neFq6s0losHlPRSxY+nuk= github.com/metacubex/sing-shadowsocks v0.2.13 h1:PKmInHy9+4nY3FLhmKcWdASUBo9WZ/KrSPWjNpGaUTQ= github.com/metacubex/sing-shadowsocks v0.2.13/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU= github.com/metacubex/sing-shadowsocks2 v0.2.8 h1:6MoODu7BAcnwIC0w9muq+2LZVz01wWD07L5IRhOty9Q= github.com/metacubex/sing-shadowsocks2 v0.2.8/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= -github.com/metacubex/sing-tun v0.4.24 h1:2H/RhlwQxUgc1K8h46giwV5uTUovx+DhQGpVPlUB4Yo= -github.com/metacubex/sing-tun v0.4.24/go.mod h1:YLk0jq8ITecb0LlNKAXC+NTkYTYmvihQxFAR495LN8c= +github.com/metacubex/sing-tun v0.4.27 h1:dWCpFDlWelQrhH3y5KkMrJXEXYeJ0V2LROP8Cxv96MI= +github.com/metacubex/sing-tun v0.4.27/go.mod h1:5nlmrOy+FG0yhiLm+O/pzmirZVrpLuDQZuA+qI9x5kg= github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE= github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q= github.com/metacubex/sing-wireguard v0.0.0-20260826105301-c3ae17d19f9e h1:KGKZt//rrELT/dEwgYcHCqNYEGr3a7uVutlZrzrZZWY= @@ -181,8 +181,8 @@ github.com/metacubex/tfo-go v0.0.0-20260623020846-376a77860b8c/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tls v0.1.8 h1:BHF2payjtxC7wdR3tnq8vl3FAgEQB6/pPjP2tJNTsP4= github.com/metacubex/tls v0.1.8/go.mod h1:0XeVdL0cBw+8i5Hqy3lVeP9IyD/LFTq02ExvHM6rzEM= -github.com/metacubex/utls v1.8.7 h1:Cp+yWkNTFkSihETgGWq34hlVFds5HpYWVOR1xovUVTs= -github.com/metacubex/utls v1.8.7/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= +github.com/metacubex/utls v1.8.8 h1:7fiMYahL+tT1I/vG1MWidv88r/3A5NnPrurjzApZGyQ= +github.com/metacubex/utls v1.8.8/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= github.com/metacubex/wazero v0.0.0-20260628025728-9ae6bdcf2a7d h1:UMFI+kdp0jA8s9tC7oul0pWeTW6s8lnm0dD5PT+RY14= github.com/metacubex/wazero v0.0.0-20260628025728-9ae6bdcf2a7d/go.mod h1:p48xp436h1oGfoXuEnVONeDhTW+E2UpY94GAVxA62oI= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk= @@ -284,8 +284,8 @@ go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= -go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= -go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +go4.org/netipx v0.0.0-20260823151212-3075585bcbeb h1:XBM4hvfwGAttkkiTIFfeigdfcL1xIfdKXqFdgiHGtDs= +go4.org/netipx v0.0.0-20260823151212-3075585bcbeb/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/hub/route/configs.go new/mihomo-1.19.32/hub/route/configs.go --- old/mihomo-1.19.31/hub/route/configs.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/hub/route/configs.go 2026-09-30 16:35:49.000000000 +0200 @@ -98,6 +98,7 @@ EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` ICMPTimeout *int64 `yaml:"icmp-timeout" json:"icmp-timeout,omitempty"` + CongestionController *string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` FileDescriptor *int `yaml:"file-descriptor" json:"file-descriptor"` Inet4RouteAddress *[]netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` @@ -264,6 +265,9 @@ if p.ICMPTimeout != nil { def.ICMPTimeout = *p.ICMPTimeout } + if p.CongestionController != nil { + def.CongestionController = *p.CongestionController + } if p.FileDescriptor != nil { def.FileDescriptor = *p.FileDescriptor } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/listener/config/tun.go new/mihomo-1.19.32/listener/config/tun.go --- old/mihomo-1.19.31/listener/config/tun.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/listener/config/tun.go 2026-09-30 16:35:49.000000000 +0200 @@ -53,6 +53,7 @@ UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` ICMPTimeout int64 `yaml:"icmp-timeout" json:"icmp-timeout,omitempty"` DisableICMPForwarding bool `yaml:"disable-icmp-forwarding" json:"disable-icmp-forwarding,omitempty"` + CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` @@ -211,6 +212,9 @@ if t.DisableICMPForwarding != other.DisableICMPForwarding { return false } + if t.CongestionController != other.CongestionController { + return false + } if t.FileDescriptor != other.FileDescriptor { return false } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/listener/inbound/tun.go new/mihomo-1.19.32/listener/inbound/tun.go --- old/mihomo-1.19.31/listener/inbound/tun.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/listener/inbound/tun.go 2026-09-30 16:35:49.000000000 +0200 @@ -54,6 +54,7 @@ UDPTimeout int64 `inbound:"udp-timeout,omitempty"` ICMPTimeout int64 `inbound:"icmp-timeout,omitempty"` DisableICMPForwarding bool `inbound:"disable-icmp-forwarding,omitempty"` + CongestionController string `inbound:"congestion-controller,omitempty"` FileDescriptor int `inbound:"file-descriptor,omitempty"` Inet4RouteAddress []netip.Prefix `inbound:"inet4-route-address,omitempty"` @@ -135,6 +136,7 @@ UDPTimeout: options.UDPTimeout, ICMPTimeout: options.ICMPTimeout, DisableICMPForwarding: options.DisableICMPForwarding, + CongestionController: options.CongestionController, FileDescriptor: options.FileDescriptor, Inet4RouteAddress: options.Inet4RouteAddress, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/listener/mieru/server.go new/mihomo-1.19.32/listener/mieru/server.go --- old/mihomo-1.19.31/listener/mieru/server.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/listener/mieru/server.go 2026-09-30 16:35:49.000000000 +0200 @@ -29,7 +29,7 @@ conn.Close() return } - + user := conn.(mierucommon.UserContext).UserName() // Handle the connection with tunnel. switch request.Command { case mieruconstant.Socks5ConnectCmd: // TCP @@ -46,13 +46,14 @@ } inbound.ApplyAdditions( metadata, - inbound.WithInName(conn.(mierucommon.UserContext).UserName()), + inbound.WithInUser(user), inbound.WithSrcAddr(conn.RemoteAddr()), inbound.WithInAddr(conn.LocalAddr()), ) inbound.ApplyAdditions(metadata, additions...) tunnel.HandleTCPConn(conn, metadata) case mieruconstant.Socks5UDPAssociateCmd: // UDP + additions = append(additions, inbound.WithInUser(user)) pc := mierucommon.NewPacketOverStreamTunnel(conn) ep := N.NewEnhancePacketConn(pc) defer ep.Close() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/listener/parse.go new/mihomo-1.19.32/listener/parse.go --- old/mihomo-1.19.31/listener/parse.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/listener/parse.go 2026-09-30 16:35:49.000000000 +0200 @@ -64,7 +64,7 @@ listener, err = IN.NewTunnel(tunnelOption) case "tun": tunOption := &IN.TunOption{ - Stack: C.TunGvisor, + Stack: C.TunMips, DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query } err = decoder.Decode(mapping, tunOption) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/listener/sing_tun/server.go new/mihomo-1.19.32/listener/sing_tun/server.go --- old/mihomo-1.19.31/listener/sing_tun/server.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/listener/sing_tun/server.go 2026-09-30 16:35:49.000000000 +0200 @@ -497,6 +497,7 @@ Logger: log.SingLogger, ForwarderBindInterface: forwarderBindInterface, InterfaceFinder: interfaceFinder, + TCPCongestionControl: options.CongestionController, EnforceBindInterface: EnforceBindInterface, } l.tunIf = tunIf diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/transport/anytls/session/client.go new/mihomo-1.19.32/transport/anytls/session/client.go --- old/mihomo-1.19.31/transport/anytls/session/client.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/transport/anytls/session/client.go 2026-09-30 16:35:49.000000000 +0200 @@ -174,9 +174,10 @@ func (c *Client) idleCleanupExpTime(expTime time.Time) { activeCount := 0 - sessionToClose := make([]*Session, 0, c.idleSession.Len()) c.idleSessionLock.Lock() + sessionToClose := make([]*Session, 0, c.idleSession.Len()) + it := c.idleSession.Iterate() for it.IsNotEnd() { session := it.Value() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/transport/openvpn/data.go new/mihomo-1.19.32/transport/openvpn/data.go --- old/mihomo-1.19.31/transport/openvpn/data.go 2026-09-14 13:59:30.000000000 +0200 +++ new/mihomo-1.19.32/transport/openvpn/data.go 2026-09-30 16:35:49.000000000 +0200 @@ -220,9 +220,7 @@ var packetIDBytes [4]byte binary.BigEndian.PutUint32(packetIDBytes[:], packetID) nonce := d.nonce(packetID, d.sendImplicitIV) - ad := make([]byte, 0, len(header)+len(packetIDBytes)) - ad = append(ad, header...) - ad = append(ad, packetIDBytes[:]...) + ad := aeadAdditionalData(header, packetIDBytes[:]) sealed := d.sendAEAD.Seal(nil, nonce[:], packet, ad) out := make([]byte, 0, len(header)+4+DataChannelTagSize+len(packet)) @@ -288,9 +286,7 @@ combined := make([]byte, 0, len(ciphertext)+DataChannelTagSize) combined = append(combined, ciphertext...) combined = append(combined, tag...) - ad := make([]byte, 0, len(header)+len(packetIDBytes)) - ad = append(ad, header...) - ad = append(ad, packetIDBytes...) + ad := aeadAdditionalData(header, packetIDBytes) nonce := d.nonce(packetID, d.recvImplicitIV) plain, err := d.recvAEAD.Open(nil, nonce[:], combined, ad) @@ -426,6 +422,23 @@ return nil } +// aeadAdditionalData returns the AEAD additional data of a data channel packet. +// +// OpenVPN only authenticates the opcode/peer-id header for P_DATA_V2. For +// P_DATA_V1 the additional data is the packet ID alone: in OpenVPN's +// handle_data_channel_packet (ssl.c) ad_start is set after the opcode byte has +// been skipped for P_DATA_V1, but before it for P_DATA_V2. Servers that only +// speak P_DATA_V1 (e.g. SoftEther) reject packets that include the opcode. +func aeadAdditionalData(header, packetID []byte) []byte { + ad := make([]byte, 0, len(header)+len(packetID)) + if len(header) > 0 { + if opcode, _ := parseOpcodeKeyID(header[0]); opcode == PDataV2 { + ad = append(ad, header...) + } + } + return append(ad, packetID...) +} + func dataHeader(peerID uint32, keyID uint8) []byte { if peerID != PeerIDUnset { return []byte{ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mihomo-1.19.31/transport/openvpn/data_v1_aead_test.go new/mihomo-1.19.32/transport/openvpn/data_v1_aead_test.go --- old/mihomo-1.19.31/transport/openvpn/data_v1_aead_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/mihomo-1.19.32/transport/openvpn/data_v1_aead_test.go 2026-09-30 16:35:49.000000000 +0200 @@ -0,0 +1,185 @@ +package openvpn + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/binary" + "testing" +) + +// These tests pin the P_DATA_V1 / P_DATA_V2 AEAD wire format against the +// OpenVPN protocol definition rather than against mihomo itself, so that a +// consistently-wrong implementation on both ends cannot pass. +// +// OpenVPN 2.x (src/openvpn/ssl.c, handle_data_channel_packet): for P_DATA_V2 +// ad_start is taken *before* skipping the opcode byte, so the additional data +// is opcode|peer-id|packet-id; for P_DATA_V1 it is taken *after* skipping the +// opcode, so the additional data is the 4-byte packet-id only. SoftEther's +// OpenVPN server (Interop_OpenVPN.c) does the same for V1. + +func v1TestKeys() (client, server *KeyMaterial) { + client = &KeyMaterial{ + SendCipherKey: bytes.Repeat([]byte{0x11}, 32), + SendHMACKey: bytes.Repeat([]byte{0x22}, maxHMACKeyLength), + RecvCipherKey: bytes.Repeat([]byte{0x33}, 32), + RecvHMACKey: bytes.Repeat([]byte{0x44}, maxHMACKeyLength), + } + server = &KeyMaterial{ + SendCipherKey: client.RecvCipherKey, + SendHMACKey: client.RecvHMACKey, + RecvCipherKey: client.SendCipherKey, + RecvHMACKey: client.SendHMACKey, + } + return +} + +// refAEAD is an independent reference implementation of the OpenVPN AEAD +// data channel: nonce = packet-id || implicit IV (first 8 bytes of the HMAC key), +// wire = header || packet-id || tag || ciphertext. +func refAEAD(t *testing.T, cipherKey, hmacKey []byte) (cipher.AEAD, func(uint32) []byte) { + t.Helper() + block, err := aes.NewCipher(cipherKey) + if err != nil { + t.Fatal(err) + } + aead, err := cipher.NewGCM(block) + if err != nil { + t.Fatal(err) + } + nonce := func(pid uint32) []byte { + n := make([]byte, 12) + binary.BigEndian.PutUint32(n[:4], pid) + copy(n[4:], hmacKey[:8]) + return n + } + return aead, nonce +} + +func refSeal(t *testing.T, cipherKey, hmacKey, header []byte, pid uint32, ad, plain []byte) []byte { + aead, nonce := refAEAD(t, cipherKey, hmacKey) + sealed := aead.Seal(nil, nonce(pid), plain, ad) + tagStart := len(sealed) - DataChannelTagSize + var pidb [4]byte + binary.BigEndian.PutUint32(pidb[:], pid) + out := append([]byte{}, header...) + out = append(out, pidb[:]...) + out = append(out, sealed[tagStart:]...) + return append(out, sealed[:tagStart]...) +} + +func refOpen(t *testing.T, cipherKey, hmacKey []byte, packet []byte, headerSize int, ad []byte) ([]byte, error) { + aead, nonce := refAEAD(t, cipherKey, hmacKey) + pid := binary.BigEndian.Uint32(packet[headerSize : headerSize+4]) + tag := packet[headerSize+4 : headerSize+4+DataChannelTagSize] + ct := packet[headerSize+4+DataChannelTagSize:] + return aead.Open(nil, nonce(pid), append(append([]byte{}, ct...), tag...), ad) +} + +var testIPPacket = []byte{0x45, 0, 0, 20, 1, 2, 3, 4, 64, 6, 0, 0, 10, 8, 0, 2, 1, 1, 1, 1} + +func TestDataChannelAESGCMV1RoundTrip(t *testing.T) { + ck, sk := v1TestKeys() + client, err := NewDataChannel(ck, CipherAES256GCM, AuthSHA1, PeerIDUnset, 0) + if err != nil { + t.Fatal(err) + } + server, err := NewDataChannel(sk, CipherAES256GCM, AuthSHA1, PeerIDUnset, 0) + if err != nil { + t.Fatal(err) + } + enc, err := client.Encrypt(testIPPacket) + if err != nil { + t.Fatal(err) + } + if opcode, _ := parseOpcodeKeyID(enc[0]); opcode != PDataV1 { + t.Fatalf("unexpected data opcode: %s", opcode) + } + plain, err := server.Decrypt(enc) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(plain, testIPPacket) { + t.Fatalf("unexpected decrypted packet: %x", plain) + } +} + +// mihomo -> standard OpenVPN/SoftEther peer: our V1 packet must authenticate +// with AD = packet-id only, and must NOT authenticate with opcode|packet-id. +func TestDataChannelAEADV1EncryptMatchesOpenVPN(t *testing.T) { + ck, _ := v1TestKeys() + client, err := NewDataChannel(ck, CipherAES256GCM, AuthSHA1, PeerIDUnset, 4) + if err != nil { + t.Fatal(err) + } + enc, err := client.Encrypt(testIPPacket) + if err != nil { + t.Fatal(err) + } + if enc[0] != 0x34 { // P_DATA_V1, key-id 4, as seen on the wire from OpenVPN 2.6 + t.Fatalf("unexpected first byte %#02x", enc[0]) + } + pidOnly := enc[1:5] + plain, err := refOpen(t, ck.SendCipherKey, ck.SendHMACKey, enc, 1, pidOnly) + if err != nil { + t.Fatalf("standard peer rejects our P_DATA_V1 packet (AD must be packet-id only): %v", err) + } + if !bytes.Equal(plain, testIPPacket) { + t.Fatalf("unexpected plaintext: %x", plain) + } + if _, err := refOpen(t, ck.SendCipherKey, ck.SendHMACKey, enc, 1, enc[:5]); err == nil { + t.Fatal("P_DATA_V1 must not authenticate the opcode byte") + } +} + +// standard OpenVPN/SoftEther peer -> mihomo: a V1 packet sealed with +// AD = packet-id only must decrypt. +func TestDataChannelAEADV1DecryptMatchesOpenVPN(t *testing.T) { + ck, sk := v1TestKeys() + client, err := NewDataChannel(ck, CipherAES256GCM, AuthSHA1, PeerIDUnset, 4) + if err != nil { + t.Fatal(err) + } + header := []byte{0x34} + var pidb [4]byte + binary.BigEndian.PutUint32(pidb[:], 1) + pkt := refSeal(t, sk.SendCipherKey, sk.SendHMACKey, header, 1, pidb[:], testIPPacket) + plain, err := client.Decrypt(pkt) + if err != nil { + t.Fatalf("failed to decrypt standard P_DATA_V1 packet: %v", err) + } + if !bytes.Equal(plain, testIPPacket) { + t.Fatalf("unexpected plaintext: %x", plain) + } + // tampering with the (unauthenticated-by-AD) opcode key-id must not be + // what makes or breaks decryption, but tampering with the packet id must. + binary.BigEndian.PutUint32(pidb[:], 2) + pkt = refSeal(t, sk.SendCipherKey, sk.SendHMACKey, header, 2, pidb[:], testIPPacket) + pkt[4] ^= 0xff + if _, err := client.Decrypt(pkt); err == nil { + t.Fatal("expected authentication failure after packet id tamper") + } +} + +// Regression guard: P_DATA_V2 must keep authenticating opcode|peer-id|packet-id. +func TestDataChannelAEADV2StillAuthenticatesHeader(t *testing.T) { + ck, sk := v1TestKeys() + client, err := NewDataChannel(ck, CipherAES256GCM, AuthSHA1, 7, 0) + if err != nil { + t.Fatal(err) + } + enc, err := client.Encrypt(testIPPacket) + if err != nil { + t.Fatal(err) + } + if _, err := refOpen(t, ck.SendCipherKey, ck.SendHMACKey, enc, 4, enc[:8]); err != nil { + t.Fatalf("standard peer rejects our P_DATA_V2 packet: %v", err) + } + header := []byte{opcodeKeyID(PDataV2, 0), 0, 0, 7} + var pidb [4]byte + binary.BigEndian.PutUint32(pidb[:], 1) + pkt := refSeal(t, sk.SendCipherKey, sk.SendHMACKey, header, 1, append(append([]byte{}, header...), pidb[:]...), testIPPacket) + if _, err := client.Decrypt(pkt); err != nil { + t.Fatalf("failed to decrypt standard P_DATA_V2 packet: %v", err) + } +} ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/mihomo/vendor.tar.gz /work/SRC/openSUSE:Factory/.mihomo.new.1631729/vendor.tar.gz differ: char 5, line 1
