Skip to content

Commit 3f541bb

Browse files
authored
Merge pull request #194 from anywherelan/socks5-fix-perf
socks5: improve throughput
2 parents 6d19268 + 655c624 commit 3f541bb

7 files changed

Lines changed: 320 additions & 90 deletions

File tree

application_simnet_test.go

Lines changed: 232 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package awl
22

33
import (
4+
"bytes"
45
"fmt"
6+
"io"
7+
"net"
58
"os"
69
"testing"
710
"time"
811

9-
"github.com/libp2p/go-libp2p"
10-
simlibp2p "github.com/libp2p/go-libp2p/x/simlibp2p"
11-
"github.com/marcopolo/simnet"
12-
"github.com/multiformats/go-multiaddr"
1312
"github.com/olekukonko/tablewriter"
13+
"golang.org/x/net/proxy"
14+
15+
"github.com/anywherelan/awl/entity"
16+
"github.com/anywherelan/awl/vpn"
1417
)
1518

1619
/*
@@ -123,47 +126,23 @@ func TestSimulatedTunnelPerformance(t *testing.T) {
123126
},
124127
}
125128

129+
// TODO: try with different packet sizes
130+
// we probably should aim for one packet per UDP datagram
131+
const packetSize = vpn.InterfaceMTU
132+
const testDuration = 30 * time.Second
133+
126134
table := tablewriter.NewWriter(os.Stdout)
127135
table.SetHeader([]string{"Scenario", "Latency", "Bandwidth Limit", "Actual Throughput", "Utilization", "Packet Loss"})
128136

129137
for _, sc := range scenarios {
130138
t.Run(sc.name, func(t *testing.T) {
131139
ts := NewSimnetTestSuite(t)
132140

133-
net := &simnet.Simnet{}
134-
net.LatencyFunc = simnet.StaticLatency(sc.latency)
135-
net.Start()
136-
defer net.Close()
137-
138-
// TODO: try with different packet sizes
139-
// we probably should aim for one packet per UDP datagram
140-
const packetSize = 3500 // Typical VPN packet size
141-
const testDuration = 30 * time.Second
142-
143-
// Setup link properties for the simulation
144-
// We simulate a symmetric link between the two peers
145-
linkSettings := simnet.NodeBiDiLinkSettings{
146-
Downlink: simnet.LinkSettings{BitsPerSecond: sc.bandwidthMbps},
147-
Uplink: simnet.LinkSettings{BitsPerSecond: sc.bandwidthMbps},
148-
}
149-
150-
// Create two peers
141+
// Create two peers connected over simulated network
151142
// uncomment to debug QUIC events
152143
// t.Setenv("QLOGDIR", "./test-simnet")
153144
ctx := t.Context()
154-
extraLibp2pOpts := []libp2p.Option{
155-
simlibp2p.QUICSimnet(net, linkSettings),
156-
}
157-
158-
listenAddrs1 := []multiaddr.Multiaddr{
159-
multiaddr.StringCast("/ip4/1.2.3.1/udp/1234/quic-v1"),
160-
}
161-
peer1 := ts.newTestPeer(true, listenAddrs1, extraLibp2pOpts)
162-
listenAddrs2 := []multiaddr.Multiaddr{
163-
multiaddr.StringCast("/ip4/1.2.3.2/udp/1234/quic-v1"),
164-
}
165-
peer2 := ts.newTestPeer(true, listenAddrs2, extraLibp2pOpts)
166-
ts.makeFriendsSimnet(peer1, peer2)
145+
peer1, peer2 := ts.NewSimnetPeerPair(sc.latency, sc.bandwidthMbps, nil, nil)
167146

168147
packet := testPacket(packetSize)
169148
peer2.tun.ReferenceInboundPacketLen = packetSize
@@ -233,3 +212,221 @@ func TestSimulatedTunnelPerformance(t *testing.T) {
233212

234213
table.Render()
235214
}
215+
216+
/*
217+
TestSimulatedSOCKS5ProxyPerformance benchmarks SOCKS5 proxy performance under simulated network conditions.
218+
219+
It uses:
220+
1. simlibp2p for simulated QUIC transport between peers
221+
2. simnet for configurable network latency and bandwidth
222+
3. Real SOCKS5 client/server for actual proxy connections
223+
4. Real HTTP client/server for end-to-end measurements
224+
225+
The data flow is:
226+
HTTP Client → SOCKS5 Listener (real TCP) → p2p Stream (simnet QUIC) → SOCKS5 Server → HTTP Server (real TCP)
227+
*/
228+
func TestSimulatedSOCKS5ProxyPerformance(t *testing.T) {
229+
// TODO: we have test for socks5 client receiving. Add test for socks5 client sending
230+
231+
const Mbps = 1_000_000
232+
233+
if os.Getenv("CI") != "" {
234+
t.Skip("skip in CI because it's a benchmark")
235+
}
236+
237+
scenarios := []struct {
238+
name string
239+
latency time.Duration
240+
bandwidthMbps int
241+
}{
242+
{
243+
name: "WARM-UP",
244+
latency: 10 * time.Millisecond,
245+
bandwidthMbps: 50 * Mbps,
246+
},
247+
{
248+
name: "Fiber_100Mbps_1ms",
249+
latency: 1 * time.Millisecond,
250+
bandwidthMbps: 100 * Mbps,
251+
},
252+
{
253+
name: "LongDistFiber_100Mbps_200ms",
254+
latency: 200 * time.Millisecond,
255+
bandwidthMbps: 100 * Mbps,
256+
},
257+
258+
{
259+
name: "Cable_50Mbps_10ms",
260+
latency: 10 * time.Millisecond,
261+
bandwidthMbps: 50 * Mbps,
262+
},
263+
{
264+
name: "LongDistCable_50Mbps_300ms",
265+
latency: 300 * time.Millisecond,
266+
bandwidthMbps: 50 * Mbps,
267+
},
268+
269+
{
270+
name: "Cable_10Mbps_100ms",
271+
latency: 100 * time.Millisecond,
272+
bandwidthMbps: 10 * Mbps,
273+
},
274+
{
275+
name: "LongDistCable_10Mbps_300ms",
276+
latency: 300 * time.Millisecond,
277+
bandwidthMbps: 10 * Mbps,
278+
},
279+
}
280+
281+
const testDuration = 20 * time.Second
282+
const testLastDuration = 5 * time.Second
283+
284+
table := tablewriter.NewWriter(os.Stdout)
285+
table.SetHeader([]string{"Scenario", "Latency", "Bandwidth Limit", "Throughput\navg", "Throughput\nlast 5 sec", "Utilization", "TTFB"})
286+
287+
for _, sc := range scenarios {
288+
t.Run(sc.name, func(t *testing.T) {
289+
ts := NewSimnetTestSuite(t)
290+
291+
ctx := t.Context()
292+
293+
// Create two peers connected over simulated network
294+
// peer1: SOCKS5 client side (listener enabled)
295+
// peer2: SOCKS5 server side (proxying enabled)
296+
peer1, peer2 := ts.NewSimnetPeerPair(sc.latency, sc.bandwidthMbps,
297+
&SOCKS5PeerConfig{ListenerEnabled: true, ProxyingEnabled: false},
298+
&SOCKS5PeerConfig{ListenerEnabled: false, ProxyingEnabled: true},
299+
)
300+
301+
// Configure peer2 to allow peer1 to use as exit node
302+
peer1Config, err := peer2.api.KnownPeerConfig(peer1.PeerID())
303+
ts.NoError(err)
304+
305+
err = peer2.api.UpdatePeerSettings(entity.UpdatePeerSettingsRequest{
306+
PeerID: peer1.PeerID(),
307+
Alias: peer1Config.Alias,
308+
DomainName: peer1Config.DomainName,
309+
IPAddr: peer1Config.IPAddr,
310+
AllowUsingAsExitNode: true,
311+
})
312+
ts.NoError(err)
313+
314+
// Wait for status exchange to propagate AllowedUsingAsExitNode to peer1
315+
ts.Eventually(func() bool {
316+
peer2Config, err := peer1.api.KnownPeerConfig(peer2.PeerID())
317+
ts.NoError(err)
318+
return peer2Config.AllowedUsingAsExitNode
319+
}, 2*time.Second, 100*time.Millisecond)
320+
321+
// Set peer2 as proxy for peer1
322+
peer1.app.SOCKS5.SetProxyPeerID(peer2.PeerID())
323+
peer2.app.SOCKS5.SetProxyingLocalhostEnabled(true)
324+
325+
// Setup raw TCP server that sends unlimited data
326+
tcpAddr := startUnlimitedTCPServer(t)
327+
328+
dialer, err := proxy.SOCKS5("tcp", peer1.app.Conf.SOCKS5.ListenAddress, nil, nil)
329+
ts.NoError(err)
330+
331+
// Measure TTFB and throughput
332+
connectStart := time.Now()
333+
334+
// Connect through SOCKS5 proxy to TCP server
335+
conn, err := dialer.Dial("tcp", tcpAddr)
336+
ts.NoError(err)
337+
defer conn.Close()
338+
339+
// Read first byte to measure TTFB
340+
firstByte := make([]byte, 1)
341+
_, err = io.ReadFull(conn, firstByte)
342+
ts.NoError(err)
343+
ttfb := time.Since(connectStart)
344+
345+
// Measure throughput for testDuration
346+
const bufSize = 1 << 20
347+
buf := make([]byte, bufSize)
348+
349+
totalBytes := int64(1)
350+
startTime := time.Now()
351+
352+
startTimeLastSeconds := time.Time{}
353+
bytesLastSeconds := int64(0)
354+
355+
for time.Since(startTime) < testDuration || ctx.Err() != nil {
356+
if startTimeLastSeconds.IsZero() && time.Since(startTime) > testDuration-testLastDuration {
357+
startTimeLastSeconds = time.Now()
358+
}
359+
360+
n, err := conn.Read(buf)
361+
totalBytes += int64(n)
362+
if !startTimeLastSeconds.IsZero() {
363+
bytesLastSeconds += int64(n)
364+
}
365+
if err != nil {
366+
t.Errorf("Read error after %d bytes: %v", totalBytes, err)
367+
break
368+
}
369+
}
370+
371+
duration := time.Since(startTime)
372+
durationLast := time.Since(startTimeLastSeconds)
373+
374+
// Calculate metrics
375+
throughputMbps := (float64(totalBytes) * 8) / duration.Seconds() / float64(Mbps)
376+
throughputMbpsLastSeconds := (float64(bytesLastSeconds) * 8) / durationLast.Seconds() / float64(Mbps)
377+
expectedMbps := float64(sc.bandwidthMbps) / float64(Mbps)
378+
utilization := (throughputMbps / expectedMbps) * 100
379+
380+
table.Append([]string{
381+
sc.name,
382+
sc.latency.String(),
383+
fmt.Sprintf("%d Mbps", sc.bandwidthMbps/Mbps),
384+
fmt.Sprintf("%.2f Mbps", throughputMbps),
385+
fmt.Sprintf("%.2f Mbps", throughputMbpsLastSeconds),
386+
fmt.Sprintf("%.2f %%", utilization),
387+
ttfb.Round(100 * time.Microsecond).String(),
388+
})
389+
})
390+
391+
// Cool down between tests
392+
time.Sleep(time.Second)
393+
}
394+
395+
table.Render()
396+
}
397+
398+
// startUnlimitedTCPServer starts a TCP server that sends unlimited data to any client.
399+
// Returns the server address. Server is automatically closed when test ends.
400+
func startUnlimitedTCPServer(t *testing.T) string {
401+
listener, err := net.Listen("tcp", "127.0.0.1:0")
402+
if err != nil {
403+
t.Fatalf("Failed to start TCP server: %v", err)
404+
}
405+
406+
t.Cleanup(func() {
407+
listener.Close()
408+
})
409+
410+
const chunkSize = 1 << 20 // 1 MB
411+
chunk := bytes.Repeat([]byte("X"), chunkSize)
412+
413+
go func() {
414+
for {
415+
conn, err := listener.Accept()
416+
if err != nil {
417+
return // Listener closed
418+
}
419+
go func(c net.Conn) {
420+
defer c.Close()
421+
for {
422+
_, err2 := c.Write(chunk)
423+
if err2 != nil {
424+
return
425+
}
426+
}
427+
}(conn)
428+
}
429+
}()
430+
431+
return listener.Addr().String()
432+
}

cmd/awl-tray/go.mod

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ module awl-tray
22

33
go 1.25.0
44

5+
replace (
6+
github.com/anywherelan/awl => ../../
7+
github.com/haxii/socks5 => github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc
8+
github.com/ipfs/go-log/v2 => github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5
9+
github.com/ncruces/zenity => github.com/pymq/zenity v0.0.0-20230509161854-c117c448544d
10+
)
11+
512
require (
613
fyne.io/systray v1.11.1-0.20250812065214-4856ac3adc3c
714
github.com/GrigoryKrasnochub/updaterini v0.1.0
@@ -15,12 +22,6 @@ require (
1522
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
1623
)
1724

18-
replace (
19-
github.com/anywherelan/awl => ../../
20-
github.com/ipfs/go-log/v2 => github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5
21-
github.com/ncruces/zenity => github.com/pymq/zenity v0.0.0-20230509161854-c117c448544d
22-
)
23-
2425
require (
2526
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
2627
github.com/akavel/rsrc v0.10.2 // indirect

cmd/awl-tray/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
1111
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
1212
github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5 h1:uQsw+HnQo6Ru5eFgUdEQYKMwkiYkoNZDQmY2ob1E58Y=
1313
github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5/go.mod h1:r8UEDyeHO6bYVcP9R2/HnK2ZSZ5CJp89gubcHLKfRv0=
14+
github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc h1:BxUih+wVbxEfAAC4+vzXSoDXHNFLgnLR9AmeXRtJbuk=
15+
github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc/go.mod h1:KpbKhNH2RqojJGPVT9faDXnYWkqzfgpKeehC8xTKaeY=
1416
github.com/anywherelan/ts-dns v0.0.0-20240721135326-6d6b7b811853 h1:RVKWGnppAfxgD2wphkq+OYDOqqI8zgbymBLl2pxYKzY=
1517
github.com/anywherelan/ts-dns v0.0.0-20240721135326-6d6b7b811853/go.mod h1:ly7HpPle1G3D0jwrr12uolTGWKN3DPgxzBYNR086BLo=
1618
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
@@ -88,8 +90,6 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
8890
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
8991
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
9092
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
91-
github.com/haxii/socks5 v1.0.0 h1:78BIzd4lHibdRNOKdMwKCnnsgYLW9SeotqU+nMhWSSo=
92-
github.com/haxii/socks5 v1.0.0/go.mod h1:6O9Ba2yrLlvuSe/L1e84eZI8cPw6H+q1Ilr4hjgm4uY=
9393
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
9494
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
9595
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=

go.mod

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ module github.com/anywherelan/awl
22

33
go 1.25.0
44

5+
replace (
6+
github.com/haxii/socks5 => github.com/anywherelan/socks5 v0.0.0-20260112065346-bea9abfe9bdc
7+
github.com/ipfs/go-log/v2 => github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5
8+
)
9+
510
require (
611
github.com/GrigoryKrasnochub/updaterini v0.1.0
712
github.com/anywherelan/ts-dns v0.0.0-20240721135326-6d6b7b811853
@@ -11,7 +16,6 @@ require (
1116
github.com/ipfs/go-datastore v0.9.0
1217
github.com/ipfs/go-log/v2 v2.9.0
1318
github.com/labstack/echo/v4 v4.14.0
14-
github.com/libp2p/go-buffer-pool v0.1.0
1519
github.com/libp2p/go-libp2p v0.46.0
1620
github.com/libp2p/go-libp2p-kad-dht v0.36.0
1721
github.com/libp2p/go-libp2p-kbucket v0.8.0
@@ -35,8 +39,6 @@ require (
3539
golang.zx2c4.com/wireguard/windows v0.5.3
3640
)
3741

38-
replace github.com/ipfs/go-log/v2 => github.com/anywherelan/go-log/v2 v2.0.3-0.20221101180049-46e3967f6fe5
39-
4042
require (
4143
github.com/benbjohnson/clock v1.3.5 // indirect
4244
github.com/beorn7/perks v1.0.1 // indirect
@@ -72,6 +74,7 @@ require (
7274
github.com/koron/go-ssdp v0.0.6 // indirect
7375
github.com/labstack/gommon v0.4.2 // indirect
7476
github.com/leodido/go-urn v1.4.0 // indirect
77+
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
7578
github.com/libp2p/go-cidranger v1.1.0 // indirect
7679
github.com/libp2p/go-flow-metrics v0.3.0 // indirect
7780
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect

0 commit comments

Comments
 (0)