Skip to content

Commit 317adfb

Browse files
authored
Merge pull request #1 from anywherelan/improve-proxy-perf
feat: add ProxyStream with more performant data forwarding
2 parents bb9bca4 + 3f08306 commit 317adfb

6 files changed

Lines changed: 526 additions & 31 deletions

File tree

auth.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func (a UserPassAuthenticator) Authenticate(reader io.Reader, writer io.Writer)
166166
// authenticate is used to handle connection authentication
167167
func (s *Server) authenticate(conn io.Writer, bufConn io.Reader) (*AuthContext, error) {
168168
// Get the methods
169-
methods, err := readMethods(bufConn)
169+
methods, err := ReadMethods(bufConn)
170170
if err != nil {
171171
return nil, fmt.Errorf("failed to get auth methods: %v", err)
172172
}
@@ -190,9 +190,9 @@ func noAcceptableAuth(conn io.Writer) error {
190190
return ErrNoSupportedAuth
191191
}
192192

193-
// readMethods is used to read the number of methods
193+
// ReadMethods is used to read the number of methods
194194
// and proceeding auth methods
195-
func readMethods(r io.Reader) ([]byte, error) {
195+
func ReadMethods(r io.Reader) ([]byte, error) {
196196
header := []byte{0}
197197
if _, err := r.Read(header); err != nil {
198198
return nil, err

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/haxii/socks5
22

3-
go 1.12
3+
go 1.22

proxy_stream.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package socks5
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"sync"
8+
)
9+
10+
type closeWriter interface {
11+
CloseWrite() error
12+
}
13+
14+
type closeReader interface {
15+
CloseRead() error
16+
}
17+
18+
// Ring buffer size. Must be a power of two so that `x & mask` is equivalent
19+
// to `x % proxyBufSize` (see ProxyStream for the indexing scheme).
20+
const (
21+
proxyBufSize uint64 = 64 << 10
22+
proxyBufMask = proxyBufSize - 1
23+
)
24+
25+
// proxyBufPool recycles the per-connection 64 KiB ring buffers. The buffer
26+
// dominates ProxyStream's allocation footprint, and under sustained load
27+
// each connection would otherwise force a fresh 64 KiB heap allocation.
28+
// Storing a pointer to a fixed-size array (rather than a slice) avoids the
29+
// extra slice-header allocation that sync.Pool would otherwise introduce.
30+
var proxyBufPool = sync.Pool{
31+
New: func() any {
32+
return new([proxyBufSize]byte)
33+
},
34+
}
35+
36+
// ProxyStream forwards data from src to dst, similar to io.Copy, but with improved performance.
37+
// Unlike io.Copy’s sequential read/write model, it allows reads to continue while writes are in progress,
38+
// using a single 64 KiB ring buffer shared between the reader and writer.
39+
// ProxyStream closes both the read and write sides when the transfer completes.
40+
func ProxyStream(src io.Reader, dst io.Writer) error {
41+
// head and tail are monotonically increasing byte counters, not positions
42+
// inside buf. Indexing into buf is done via `& proxyBufMask`, which is
43+
// equivalent to `% proxyBufSize` but cheaper — this requires proxyBufSize
44+
// to be a power of two. With this scheme, `head - tail` directly yields
45+
// the number of occupied bytes regardless of wrap, and we never need to
46+
// reset the counters.
47+
bufArr := proxyBufPool.Get().(*[proxyBufSize]byte)
48+
defer proxyBufPool.Put(bufArr)
49+
buf := bufArr[:]
50+
var (
51+
head uint64 // total bytes written into buf by reader
52+
tail uint64 // total bytes consumed from buf by writer
53+
mu sync.Mutex
54+
readDone bool
55+
writeErr error
56+
)
57+
cond := sync.NewCond(&mu)
58+
59+
writerDone := make(chan struct{})
60+
61+
// Writer goroutine: drains [tail, head) from the ring buffer into dst.
62+
go func() {
63+
defer close(writerDone)
64+
for {
65+
mu.Lock()
66+
for head == tail && !readDone {
67+
cond.Wait()
68+
}
69+
if head == tail {
70+
// Reader finished and buffer drained.
71+
mu.Unlock()
72+
return
73+
}
74+
75+
// Contiguous readable region starting at rIdx. When the occupied
76+
// range wraps past proxyBufSize, only the first segment
77+
// [rIdx, proxyBufSize) is taken here; the remainder [0, hIdx) is
78+
// handled on the next iteration once tail has advanced past the
79+
// wrap point.
80+
rIdx := tail & proxyBufMask
81+
hIdx := head & proxyBufMask
82+
var data []byte
83+
if hIdx > rIdx {
84+
data = buf[rIdx:hIdx]
85+
} else {
86+
data = buf[rIdx:proxyBufSize]
87+
}
88+
mu.Unlock()
89+
90+
n, err := dst.Write(data)
91+
92+
mu.Lock()
93+
tail += uint64(n)
94+
cond.Signal()
95+
if err != nil {
96+
writeErr = err
97+
mu.Unlock()
98+
// Unblock reader if it’s stuck on src.Read.
99+
if cr, ok := src.(closeReader); ok {
100+
_ = cr.CloseRead()
101+
}
102+
return
103+
}
104+
mu.Unlock()
105+
}
106+
}()
107+
108+
// Reader loop: fills [head, tail+proxyBufSize) in the ring buffer from src.
109+
var readErr error
110+
for {
111+
mu.Lock()
112+
for head-tail == proxyBufSize && writeErr == nil {
113+
cond.Wait()
114+
}
115+
if writeErr != nil {
116+
mu.Unlock()
117+
break
118+
}
119+
120+
// Contiguous writable region starting at wIdx. Mirrors the writer
121+
// side: if the free range wraps, only [wIdx, proxyBufSize) is taken
122+
// here and [0, tIdx) is picked up on the next iteration after head
123+
// wraps.
124+
wIdx := head & proxyBufMask
125+
tIdx := tail & proxyBufMask
126+
var space []byte
127+
if tIdx > wIdx {
128+
space = buf[wIdx:tIdx]
129+
} else {
130+
space = buf[wIdx:proxyBufSize]
131+
}
132+
mu.Unlock()
133+
134+
n, err := src.Read(space)
135+
136+
mu.Lock()
137+
head += uint64(n)
138+
if err != nil {
139+
readDone = true
140+
if !errors.Is(err, io.EOF) {
141+
readErr = err
142+
}
143+
cond.Signal()
144+
mu.Unlock()
145+
break
146+
}
147+
if n > 0 {
148+
cond.Signal()
149+
}
150+
mu.Unlock()
151+
}
152+
153+
<-writerDone
154+
155+
// Close both sides.
156+
if cr, ok := src.(closeReader); ok {
157+
_ = cr.CloseRead()
158+
}
159+
if cw, ok := dst.(closeWriter); ok {
160+
_ = cw.CloseWrite()
161+
}
162+
163+
if readErr != nil {
164+
readErr = fmt.Errorf("read error: %v", readErr)
165+
}
166+
if writeErr != nil {
167+
writeErr = fmt.Errorf("write error: %v", writeErr)
168+
}
169+
170+
return errors.Join(writeErr, readErr)
171+
}

0 commit comments

Comments
 (0)