@@ -8,7 +8,9 @@ package control
88import (
99 "context"
1010 "fmt"
11+ "io"
1112 "net/netip"
13+ "strings"
1214 "sync"
1315 "sync/atomic"
1416 "time"
@@ -18,6 +20,7 @@ import (
1820 "github.com/daeuniverse/dae/component/outbound/dialer"
1921 "github.com/daeuniverse/outbound/netproxy"
2022 "github.com/daeuniverse/outbound/pool"
23+ "github.com/sirupsen/logrus"
2124)
2225
2326var UdpRoutingResultCacheTtl = 300 * time .Millisecond
@@ -53,12 +56,35 @@ type UdpEndpoint struct {
5356 dead atomic.Bool
5457}
5558
59+ // isUdpEndpointNormalClose reports whether err represents a normal (non-error) endpoint
60+ // teardown: peer EOF, NatTimeout expiry ("use of closed network connection"), or an explicit
61+ // local close triggered by Reset(0) cleanup.
62+ func isUdpEndpointNormalClose (err error ) bool {
63+ if err == nil {
64+ return true
65+ }
66+ if err == io .EOF {
67+ return true
68+ }
69+ // "use of closed network connection" is returned when Reset(0) fires ue.Close() just
70+ // before ReadFrom returns; this is the expected cleanup path, not an error.
71+ if strings .Contains (err .Error (), "use of closed network connection" ) {
72+ return true
73+ }
74+ return false
75+ }
76+
5677func (ue * UdpEndpoint ) start () {
5778 buf := pool .GetFullCap (consts .EthernetMtu )
5879 defer pool .Put (buf )
5980 for {
6081 n , from , err := ue .conn .ReadFrom (buf [:])
6182 if err != nil {
83+ if ! isUdpEndpointNormalClose (err ) {
84+ logrus .WithError (err ).Warnln ("UdpEndpoint read loop exited" )
85+ } else {
86+ logrus .WithError (err ).Debugln ("UdpEndpoint read loop exited" )
87+ }
6288 // Mark this endpoint as dead so GetOrCreate won't reuse it.
6389 // Also set expiration to past for immediate janitor cleanup.
6490 ue .dead .Store (true )
0 commit comments