Skip to content

Commit d71a729

Browse files
committed
app: add vpn tunneling benchmark with simulated network with different bandwidth/latency
1 parent 2811a9b commit d71a729

7 files changed

Lines changed: 653 additions & 363 deletions

File tree

application.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ type Application struct {
7171
Conf *config.Config
7272
Eventbus awlevent.Bus
7373

74+
// For tests only:
75+
ExtraLibp2pOpts []libp2p.Option
76+
AllowEmptyBootstrapPeers bool
77+
7478
ctx context.Context
7579
ctxCancel context.CancelFunc
7680
vpnDevice *vpn.Device
@@ -265,18 +269,19 @@ func (a *Application) makeP2pHostConfig() p2p.HostConfig {
265269
}
266270

267271
return p2p.HostConfig{
268-
PrivKeyBytes: a.Conf.PrivKey(),
269-
ListenAddrs: a.Conf.GetListenAddresses(),
270-
UserAgent: config.UserAgent,
271-
BootstrapPeers: a.Conf.GetBootstrapPeers(),
272-
EnableAutoRelay: true,
273-
Libp2pOpts: []libp2p.Option{
272+
PrivKeyBytes: a.Conf.PrivKey(),
273+
ListenAddrs: a.Conf.GetListenAddresses(),
274+
UserAgent: config.UserAgent,
275+
BootstrapPeers: a.Conf.GetBootstrapPeers(),
276+
AllowEmptyBootstrapPeers: a.AllowEmptyBootstrapPeers,
277+
EnableAutoRelay: true,
278+
Libp2pOpts: append([]libp2p.Option{
274279
libp2p.EnableRelay(),
275280
libp2p.EnableAutoNATv2(),
276281
libp2p.ResourceManager(mgr),
277282
libp2p.EnableHolePunching(),
278283
libp2p.NATPortMap(),
279-
},
284+
}, a.ExtraLibp2pOpts...),
280285
ConnManager: struct {
281286
LowWater int
282287
HighWater int

application_simnet_test.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package awl
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"sync/atomic"
7+
"testing"
8+
"time"
9+
10+
"github.com/libp2p/go-libp2p"
11+
simlibp2p "github.com/libp2p/go-libp2p/x/simlibp2p"
12+
"github.com/marcopolo/simnet"
13+
"github.com/multiformats/go-multiaddr"
14+
"github.com/olekukonko/tablewriter"
15+
)
16+
17+
/*
18+
TestSimulatedTunnelPerformance performs a benchmark of the AWL VPN tunnel under simulated network conditions.
19+
20+
It uses:
21+
1. simlibp2p (github.com/libp2p/go-libp2p/x/simlibp2p): A simulated network transport for libp2p.
22+
This allows us to run libp2p hosts that communicate over a simulated network rather than
23+
actual OS sockets. This is faster and more deterministic.
24+
25+
2. simnet (github.com/marcopolo/simnet): A network simulator that simlibp2p uses underneath.
26+
Simnet allows defining network topologies with specific link properties like latency and bandwidth_mbps.
27+
This enables testing how the VPN protocol behaves under "Fiber", "Satellite", or "DSL" conditions.
28+
*/
29+
30+
func TestSimulatedTunnelPerformance(t *testing.T) {
31+
const Mbps = 1_000_000
32+
33+
if os.Getenv("CI") != "" {
34+
t.Skip("skip in CI because it's a benchmark")
35+
}
36+
37+
scenarios := []struct {
38+
name string
39+
latency time.Duration
40+
bandwidthMbps int
41+
}{
42+
{
43+
name: "Fiber_300Mbps_1ms",
44+
latency: 1 * time.Millisecond,
45+
bandwidthMbps: 300 * Mbps,
46+
},
47+
{
48+
name: "LongDistFiber_300Mbps_200ms",
49+
latency: 200 * time.Millisecond,
50+
bandwidthMbps: 300 * Mbps,
51+
},
52+
{
53+
name: "Cable_10Mbps_1ms",
54+
latency: 1 * time.Millisecond,
55+
bandwidthMbps: 10 * Mbps,
56+
},
57+
{
58+
name: "Cable_10Mbps_10ms",
59+
latency: 10 * time.Millisecond,
60+
bandwidthMbps: 10 * Mbps,
61+
},
62+
{
63+
name: "Cable_10Mbps_100ms",
64+
latency: 100 * time.Millisecond,
65+
bandwidthMbps: 10 * Mbps,
66+
},
67+
{
68+
name: "Cable_10Mbps_200ms",
69+
latency: 200 * time.Millisecond,
70+
bandwidthMbps: 10 * Mbps,
71+
},
72+
{
73+
name: "LongDistCable_10Mbps_300ms",
74+
latency: 300 * time.Millisecond,
75+
bandwidthMbps: 10 * Mbps,
76+
},
77+
{
78+
name: "Cable_50Mbps_1ms",
79+
latency: 1 * time.Millisecond,
80+
bandwidthMbps: 50 * Mbps,
81+
},
82+
{
83+
name: "Cable_50Mbps_10ms",
84+
latency: 10 * time.Millisecond,
85+
bandwidthMbps: 50 * Mbps,
86+
},
87+
{
88+
name: "Cable_50Mbps_100ms",
89+
latency: 100 * time.Millisecond,
90+
bandwidthMbps: 50 * Mbps,
91+
},
92+
{
93+
name: "Cable_50Mbps_200ms",
94+
latency: 200 * time.Millisecond,
95+
bandwidthMbps: 50 * Mbps,
96+
},
97+
{
98+
name: "LongDistCable_50Mbps_300ms",
99+
latency: 300 * time.Millisecond,
100+
bandwidthMbps: 50 * Mbps,
101+
},
102+
{
103+
name: "DSL_20Mbps_25ms",
104+
latency: 25 * time.Millisecond,
105+
bandwidthMbps: 20 * Mbps,
106+
},
107+
{
108+
name: "LTE_30Mbps_40ms",
109+
latency: 40 * time.Millisecond,
110+
bandwidthMbps: 30 * Mbps,
111+
},
112+
}
113+
114+
table := tablewriter.NewWriter(os.Stdout)
115+
table.SetHeader([]string{"Scenario", "Latency", "Bandwidth Limit", "Actual Throughput", "Utilization", "Packet Loss"})
116+
117+
for _, sc := range scenarios {
118+
t.Run(sc.name, func(t *testing.T) {
119+
ts := NewSimnetTestSuite(t)
120+
121+
net := &simnet.Simnet{}
122+
net.LatencyFunc = simnet.StaticLatency(sc.latency)
123+
net.Start()
124+
defer net.Close()
125+
126+
// TODO: try with different packet sizes
127+
// we probably should aim for one packet per UDP datagram
128+
const packetSize = 3500 // Typical VPN packet size
129+
const testDuration = 10 * time.Second
130+
131+
// Setup link properties for the simulation
132+
// We simulate a symmetric link between the two peers
133+
linkSettings := simnet.NodeBiDiLinkSettings{
134+
Downlink: simnet.LinkSettings{BitsPerSecond: sc.bandwidthMbps},
135+
Uplink: simnet.LinkSettings{BitsPerSecond: sc.bandwidthMbps},
136+
}
137+
138+
// Create two peers
139+
ctx := t.Context()
140+
extraLibp2pOpts := []libp2p.Option{
141+
simlibp2p.QUICSimnet(net, linkSettings),
142+
}
143+
144+
listenAddrs1 := []multiaddr.Multiaddr{
145+
multiaddr.StringCast("/ip4/1.2.3.1/udp/1234/quic-v1"),
146+
}
147+
peer1 := ts.newTestPeer(true, listenAddrs1, extraLibp2pOpts)
148+
listenAddrs2 := []multiaddr.Multiaddr{
149+
multiaddr.StringCast("/ip4/1.2.3.2/udp/1234/quic-v1"),
150+
}
151+
peer2 := ts.newTestPeer(true, listenAddrs2, extraLibp2pOpts)
152+
ts.makeFriendsSimnet(peer1, peer2)
153+
154+
packet := testPacket(packetSize)
155+
peer2.tun.ReferenceInboundPacketLen = packetSize
156+
peer2.tun.ClearInboundCount()
157+
158+
// Send packets
159+
var packetsSent int64
160+
done := make(chan struct{})
161+
startTime := time.Now()
162+
163+
go func() {
164+
defer close(done)
165+
timer := time.NewTimer(testDuration)
166+
defer timer.Stop()
167+
168+
for i := 0; ; i++ {
169+
select {
170+
case <-timer.C:
171+
return
172+
case <-ctx.Done():
173+
return
174+
default:
175+
// ok
176+
}
177+
178+
// Send with "batches" to minimize runtime overhead for select
179+
for range 10 {
180+
peer1.tun.Outbound <- packet
181+
atomic.AddInt64(&packetsSent, 1)
182+
}
183+
184+
// TODO:
185+
// Slight throttle to keep drops lower
186+
//const sleepEvery = 200
187+
//if newCount != 0 && newCount%sleepEvery == 0 {
188+
// time.Sleep(1 * time.Millisecond)
189+
//}
190+
}
191+
}()
192+
193+
// Wait for sender to finish
194+
<-done
195+
duration := time.Since(startTime)
196+
197+
// Allow some time for packets to arrive
198+
time.Sleep(sc.latency * 2)
199+
200+
// Collect metrics
201+
received := peer2.tun.InboundCount()
202+
sent := atomic.LoadInt64(&packetsSent)
203+
204+
packetLoss := (float64(1) - float64(received)/float64(sent)) * 100
205+
206+
totalBits := float64(received) * float64(packetSize) * 8
207+
actualMbps := (totalBits / duration.Seconds()) / Mbps
208+
expectedMbps := sc.bandwidthMbps / Mbps
209+
210+
utilization := (actualMbps / float64(expectedMbps)) * 100
211+
212+
table.Append([]string{
213+
sc.name,
214+
sc.latency.String(),
215+
fmt.Sprintf("%d Mbps", expectedMbps),
216+
fmt.Sprintf("%.2f Mbps", actualMbps),
217+
fmt.Sprintf("%.2f %%", utilization),
218+
fmt.Sprintf("%.2f %%", packetLoss),
219+
// TODO: calculate p50/p95/p99 latency, jitter
220+
})
221+
})
222+
}
223+
224+
table.Render()
225+
}

0 commit comments

Comments
 (0)