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

Reply via email to