• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

asciimoth / socksgo / 26957812116

04 Jun 2026 02:17PM UTC coverage: 94.838% (-0.08%) from 94.917%
26957812116

push

github

asciimoth
fix: fix flaky client connection closer detection

55 of 66 new or added lines in 1 file covered. (83.33%)

16 existing lines in 1 file now uncovered.

3546 of 3739 relevant lines covered (94.84%)

1.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.52
/server_handler_bind.go
1
package socksgo
2

3
import (
4
        "context"
5
        "errors"
6
        "net"
7
        "os"
8
        "strings"
9
        "sync"
10
        "time"
11

12
        "github.com/asciimoth/gonnect"
13
        "github.com/asciimoth/putback"
14
        "github.com/asciimoth/socksgo/protocol"
15
)
16

17
// DefaultBindHandler handles the BIND command.
18
//
19
// DefaultBindHandler creates a TCP listener on the proxy server
20
// that forwards incoming connections to the client.
21
//
22
// # Protocol Support
23
//
24
//   - SOCKS4: Yes
25
//   - SOCKS4a: Yes
26
//   - SOCKS5: Yes
27
//   - TLS: Yes
28
//
29
// # Behavior
30
//
31
//  1. Applies default listen host if address is unspecified
32
//  2. Validates local address against LaddrFilter
33
//  3. Creates TCP listener on requested address
34
//  4. Sends first reply with bound address (listener.Addr())
35
//  5. Waits for single incoming connection
36
//  6. Sends second reply with remote address (incoming client)
37
//  7. Pipes data bidirectionally
38
//
39
// # Two-Reply Protocol
40
//
41
// BIND uses a two-reply protocol:
42
//
43
//  1. First reply: Contains the address where the client should
44
//     direct the peer to connect (listener address)
45
//  2. Second reply: Contains the address of the incoming
46
//     connection (after Accept returns)
47
//
48
// # Use Cases
49
//
50
// BIND is used for protocols that require reverse connections:
51
//   - FTP passive mode
52
//   - Remote debugging
53
//   - Any protocol requiring server-to-client connections
54
//
55
// # Reply
56
//
57
// Sends success reply (0x00) with:
58
//   - First reply: Listener address
59
//   - Second reply: Incoming connection's remote address
60
//
61
// # Errors
62
//
63
// Returns error and sends appropriate reply status:
64
//   - DisallowReply (0x02): Address filtered
65
//   - FailReply (0x01): Listen failed
66
//
67
// # Examples
68
//
69
//        // Default handler is used automatically
70
//        server := &socksgo.Server{
71
//            Handlers: socksgo.DefaultCommandHandlers,
72
//        }
73
//
74
// # See Also
75
//
76
//   - RFC 1928: SOCKS5 Protocol (Section 4)
77
//   - protocol.PipeConn: Connection piping implementation
78
//   - server_handler_mbind.go: Gost multiplexed BIND
79
var DefaultBindHandler = CommandHandler{
80
        Socks4:    true,
81
        Socks5:    true,
82
        TLSCompat: true,
83
        Handler: func(
84
                ctx context.Context,
85
                server *Server,
86
                conn net.Conn,
87
                ver string,
88
                info protocol.AuthInfo,
89
                cmd protocol.Cmd,
90
                addr protocol.Addr) error {
1✔
91
                pool := server.GetPool()
1✔
92
                addr = addr.WithDefaultHost(server.GetDefaultListenHost())
1✔
93

1✔
94
                if err := server.CheckLaddr(&addr); err != nil {
2✔
95
                        protocol.Reject(ver, conn, protocol.DisallowReply, pool)
1✔
96
                        return err
1✔
97
                }
1✔
98

99
                tcp := "tcp"
1✔
100
                if ver == "4" || ver == "4a" {
2✔
101
                        tcp = "tcp4"
1✔
102
                }
1✔
103

104
                listener, err := server.GetListener()(ctx, tcp, addr.ToHostPort())
1✔
105
                if err != nil {
2✔
106
                        protocol.Reject(ver, conn, socksBindErrorToReplyStatus(err), pool)
1✔
107
                        return err
1✔
108
                }
1✔
109

110
                closeListener := sync.OnceFunc(func() {
2✔
111
                        _ = listener.Close()
1✔
112
                })
1✔
113
                defer closeListener()
1✔
114

1✔
115
                err = protocol.Reply(
1✔
116
                        ver,
1✔
117
                        conn,
1✔
118
                        protocol.SuccReply,
1✔
119
                        protocol.AddrFromNetAddr(listener.Addr()),
1✔
120
                        pool,
1✔
121
                )
1✔
122
                if err != nil {
2✔
123
                        return err
1✔
124
                }
1✔
125

126
                var stopWatchControl func() []byte
1✔
127
                if _, ok := conn.(*wsCoderConn); ok {
2✔
128
                        stopWatch := closeSocksBindOnContextDone(ctx, closeListener)
1✔
129
                        stopWatchControl = func() []byte {
2✔
130
                                stopWatch()
1✔
131
                                return nil
1✔
132
                        }
1✔
133
                } else {
1✔
134
                        stopWatchControl = watchSocksBindControl(ctx, conn, closeListener)
1✔
135
                }
1✔
136
                proxy, err := listener.Accept()
1✔
137
                putBack := stopWatchControl()
1✔
138
                if err != nil {
2✔
139
                        if ctxErr := ctx.Err(); ctxErr != nil {
2✔
140
                                return ctxErr
1✔
141
                        }
1✔
142
                        return err
1✔
143
                }
144

145
                closeListener()
1✔
146
                defer func() {
2✔
147
                        _ = proxy.Close()
1✔
148
                }()
1✔
149

150
                err = protocol.Reply(
1✔
151
                        ver,
1✔
152
                        conn,
1✔
153
                        protocol.SuccReply,
1✔
154
                        protocol.AddrFromNetAddr(proxy.RemoteAddr()),
1✔
155
                        pool,
1✔
156
                )
1✔
157
                if err != nil {
2✔
158
                        return err
1✔
159
                }
1✔
160
                if len(putBack) > 0 {
1✔
NEW
161
                        conn = putback.WrapConn(conn, putBack, pool)
×
NEW
162
                }
×
163

164
                stopWatchPipe := closeSocksBindOnContextDone(ctx, func() {
2✔
165
                        _ = conn.Close()
1✔
166
                        _ = proxy.Close()
1✔
167
                })
1✔
168
                defer stopWatchPipe()
1✔
169

1✔
170
                err = gonnect.PipeConn(conn, proxy)
1✔
171
                return err
1✔
172
        },
173
}
174

175
func watchSocksBindControl(
176
        ctx context.Context,
177
        conn net.Conn,
178
        closeFn func(),
179
) func() []byte {
1✔
180
        done := make(chan struct{})
1✔
181
        result := make(chan []byte, 1)
1✔
182
        var once sync.Once
1✔
183

1✔
184
        go func() {
2✔
185
                defer close(result)
1✔
186
                buf := []byte{0}
1✔
187
                for {
2✔
188
                        select {
1✔
189
                        case <-ctx.Done():
1✔
190
                                closeFn()
1✔
191
                                return
1✔
192
                        case <-done:
1✔
193
                                return
1✔
194
                        default:
1✔
195
                        }
196

197
                        err := conn.SetReadDeadline(
1✔
198
                                time.Now().Add(50 * time.Millisecond),
1✔
199
                        )
1✔
200
                        if err != nil {
1✔
NEW
201
                                return
×
NEW
202
                        }
×
203
                        n, err := conn.Read(buf)
1✔
204
                        if n > 0 {
1✔
NEW
205
                                result <- append([]byte(nil), buf[:n]...)
×
NEW
206
                                return
×
NEW
207
                        }
×
208
                        if err == nil {
1✔
NEW
209
                                continue
×
210
                        }
211
                        if isTimeout(err) {
2✔
212
                                continue
1✔
213
                        }
214
                        closeFn()
1✔
215
                        return
1✔
216
                }
217
        }()
218

219
        return func() []byte {
2✔
220
                once.Do(func() {
2✔
221
                        close(done)
1✔
222
                        _ = conn.SetReadDeadline(time.Now())
1✔
223
                })
1✔
224
                putBack := <-result
1✔
225
                _ = conn.SetReadDeadline(time.Time{})
1✔
226
                return putBack
1✔
227
        }
228
}
229

230
func closeSocksBindOnContextDone(ctx context.Context, closeFn func()) func() {
1✔
231
        done := make(chan struct{})
1✔
232
        var once sync.Once
1✔
233

1✔
234
        go func() {
2✔
235
                select {
1✔
236
                case <-ctx.Done():
1✔
237
                        closeFn()
1✔
238
                case <-done:
1✔
239
                }
240
        }()
241

242
        return func() {
2✔
243
                once.Do(func() {
2✔
244
                        close(done)
1✔
245
                })
1✔
246
        }
247
}
248

249
func socksBindErrorToReplyStatus(err error) protocol.ReplyStatus {
1✔
250
        if err == nil {
1✔
UNCOV
251
                return protocol.SuccReply
×
UNCOV
252
        }
×
253

254
        var unwrapped = err
1✔
255
        for {
2✔
256
                u := errors.Unwrap(unwrapped)
1✔
257
                if u == nil {
2✔
258
                        break
1✔
259
                }
260
                unwrapped = u
×
261
        }
262

263
        var opErr *net.OpError
1✔
264
        if errors.As(err, &opErr) && opErr.Err != nil {
1✔
UNCOV
265
                unwrapped = opErr.Err
×
UNCOV
266
        }
×
267

268
        var dnsErr *net.DNSError
1✔
269
        if errors.As(err, &dnsErr) {
1✔
UNCOV
270
                return protocol.HostUnreachReply
×
UNCOV
271
        }
×
272

273
        errStr := strings.ToLower(unwrapped.Error())
1✔
274
        if strings.Contains(errStr, "connection refused") {
1✔
UNCOV
275
                return protocol.ConnRefusedReply
×
UNCOV
276
        }
×
277
        if strings.Contains(errStr, "network unreachable") {
1✔
UNCOV
278
                return protocol.NetUnreachReply
×
UNCOV
279
        }
×
280
        if strings.Contains(errStr, "host unreachable") {
1✔
UNCOV
281
                return protocol.HostUnreachReply
×
UNCOV
282
        }
×
283
        if strings.Contains(errStr, "connection timed out") ||
1✔
284
                strings.Contains(errStr, "i/o timeout") {
1✔
UNCOV
285
                return protocol.TTLExpiredReply
×
286
        }
×
287
        if strings.Contains(errStr, "permission denied") {
1✔
UNCOV
288
                return protocol.DisallowReply
×
UNCOV
289
        }
×
290

291
        return protocol.FailReply
1✔
292
}
293

294
func isTimeout(err error) bool {
1✔
295
        if err == nil {
1✔
NEW
296
                return false
×
NEW
297
        }
×
298

299
        if errors.Is(err, context.DeadlineExceeded) {
1✔
NEW
300
                return true
×
UNCOV
301
        }
×
302

303
        if errors.Is(err, os.ErrDeadlineExceeded) {
2✔
304
                return true
1✔
305
        }
1✔
306

307
        var netErr net.Error
1✔
308
        return errors.As(err, &netErr) && netErr.Timeout()
1✔
309
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc