From c5a5bf4e0cc7cdf1823d9a13c13b46297f6fce1f Mon Sep 17 00:00:00 2001 From: Nikita Lipatov <6stringsohei@gmail.com> Date: Sun, 15 Mar 2026 18:14:15 +0300 Subject: [PATCH 01/15] test(bench): add dataplane full-cycle benchmarks --- ReadMe.md | 24 +++ .../tcp_chacha20/full_cycle_benchmark_test.go | 157 ++++++++++++++++++ .../udp_chacha20/full_cycle_benchmark_test.go | 140 ++++++++++++++++ .../tcp_chacha20/full_cycle_benchmark_test.go | 148 +++++++++++++++++ .../udp_chacha20/full_cycle_benchmark_test.go | 132 +++++++++++++++ 5 files changed, 601 insertions(+) create mode 100644 src/infrastructure/tunnel/dataplane/client/tcp_chacha20/full_cycle_benchmark_test.go create mode 100644 src/infrastructure/tunnel/dataplane/client/udp_chacha20/full_cycle_benchmark_test.go create mode 100644 src/infrastructure/tunnel/dataplane/server/tcp_chacha20/full_cycle_benchmark_test.go create mode 100644 src/infrastructure/tunnel/dataplane/server/udp_chacha20/full_cycle_benchmark_test.go diff --git a/ReadMe.md b/ReadMe.md index d316a92c..bc67d429 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -24,6 +24,30 @@ --- +## 📈 Performance + +TunGo includes in-memory full-cycle dataplane benchmarks for both UDP and TCP. These benchmarks measure userspace packet-processing throughput only: encryption, routing/lookup, validation, decryption, and handoff to an in-memory sink. + +Example single-run results for **1400-byte packets** on **Apple M4 Pro**: + +| Path | ns/op | Throughput | Allocs/op | +|---|---:|---:|---:| +| UDP client -> server | ~2.6 us | ~551 MB/s | 0 | +| UDP server -> client | ~2.9 us | ~494 MB/s | 0 | +| TCP client -> server | ~2.5 us | ~565 MB/s | 0 | +| TCP server -> client | ~2.6 us | ~554 MB/s | 0 | + +These numbers do **not** include TUN device, socket, kernel, firewall/NAT, or real network overhead. Treat them as dataplane-core benchmarks, not end-to-end VPN throughput claims. + +To reproduce: + +```bash +cd src +GOCACHE=/tmp/go-build-cache go test ./infrastructure/tunnel/dataplane/server/udp_chacha20 ./infrastructure/tunnel/dataplane/client/udp_chacha20 ./infrastructure/tunnel/dataplane/server/tcp_chacha20 ./infrastructure/tunnel/dataplane/client/tcp_chacha20 -run ^$ -bench FullCycle -benchmem +``` + +--- + ## 🚀 QuickStart Refer to: [QuickStart](https://tungo.ethacore.com/docs/QuickStart) diff --git a/src/infrastructure/tunnel/dataplane/client/tcp_chacha20/full_cycle_benchmark_test.go b/src/infrastructure/tunnel/dataplane/client/tcp_chacha20/full_cycle_benchmark_test.go new file mode 100644 index 00000000..88e92081 --- /dev/null +++ b/src/infrastructure/tunnel/dataplane/client/tcp_chacha20/full_cycle_benchmark_test.go @@ -0,0 +1,157 @@ +package tcp_chacha20 + +import ( + "bytes" + "fmt" + "io" + "net/netip" + "testing" + "tungo/application/network/connection" + chacha "tungo/infrastructure/cryptography/chacha20" + appip "tungo/infrastructure/network/ip" + "tungo/infrastructure/network/service_packet" + "tungo/infrastructure/settings" + "tungo/infrastructure/tunnel/session" + + "golang.org/x/crypto/chacha20poly1305" +) + +type benchmarkHandshake struct { + id [32]byte + c2s []byte + s2c []byte +} + +func (h *benchmarkHandshake) Id() [32]byte { return h.id } +func (h *benchmarkHandshake) KeyClientToServer() []byte { return append([]byte(nil), h.c2s...) } +func (h *benchmarkHandshake) KeyServerToClient() []byte { return append([]byte(nil), h.s2c...) } +func (h *benchmarkHandshake) ServerSideHandshake(connection.Transport) (int, error) { + return 0, nil +} +func (h *benchmarkHandshake) ClientSideHandshake(connection.Transport) error { return nil } + +type benchmarkPacketSink struct { + buf []byte +} + +func (s *benchmarkPacketSink) Write(p []byte) (int, error) { + s.buf = append(s.buf[:0], p...) + return len(p), nil +} + +func benchmarkTCPCryptoPair(b *testing.B) (connection.Crypto, connection.Crypto) { + b.Helper() + + hs := &benchmarkHandshake{ + id: [32]byte{ + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + }, + c2s: bytes.Repeat([]byte{0x11}, chacha20poly1305.KeySize), + s2c: bytes.Repeat([]byte{0x22}, chacha20poly1305.KeySize), + } + + builder := chacha.NewTcpSessionBuilder(chacha.NewDefaultAEADBuilder()) + clientCrypto, _, err := builder.FromHandshake(hs, false) + if err != nil { + b.Fatalf("build client crypto: %v", err) + } + serverCrypto, _, err := builder.FromHandshake(hs, true) + if err != nil { + b.Fatalf("build server crypto: %v", err) + } + return clientCrypto, serverCrypto +} + +func benchmarkIPv4Packet(src, dst netip.Addr, payloadLen int) []byte { + packet := make([]byte, 20+payloadLen) + packet[0] = 0x45 + totalLen := len(packet) + packet[2] = byte(totalLen >> 8) + packet[3] = byte(totalLen) + packet[8] = 64 + packet[9] = 17 + src4 := src.As4() + dst4 := dst.As4() + copy(packet[12:16], src4[:]) + copy(packet[16:20], dst4[:]) + for i := 20; i < len(packet); i++ { + packet[i] = byte(i) + } + return packet +} + +func benchmarkHandleTCPClientIngress(crypto connection.Crypto, tunWriter io.Writer, packet []byte) (int, error) { + if len(packet) < chacha20poly1305.Overhead || len(packet) > settings.DefaultEthernetMTU+settings.TCPChacha20Overhead { + return 0, nil + } + payload, err := crypto.Decrypt(packet) + if err != nil { + return 0, err + } + if spType, spOk := service_packet.TryParseHeader(payload); spOk { + switch spType { + case service_packet.EpochExhausted, service_packet.RekeyAck, service_packet.Pong: + return 0, nil + } + } + _, err = tunWriter.Write(payload) + if err != nil { + return 0, err + } + return len(payload), nil +} + +func BenchmarkFullCycleServerToClientTCP(b *testing.B) { + clientInner := netip.MustParseAddr("10.0.0.2") + clientOuter := netip.MustParseAddrPort("203.0.113.10:51820") + remoteSrc := netip.MustParseAddr("1.1.1.1") + parser := appip.NewHeaderParser() + payloadSizes := []int{64, 512, 1400} + + for _, payloadSize := range payloadSizes { + b.Run(fmt.Sprintf("%dB", payloadSize), func(b *testing.B) { + clientCrypto, serverCrypto := benchmarkTCPCryptoPair(b) + cipherSink := &benchmarkPacketSink{} + tunSink := &benchmarkPacketSink{} + + repo := session.NewDefaultRepository() + peer := session.NewPeer( + session.NewSession(serverCrypto, nil, clientInner, clientOuter), + connection.NewDefaultEgress(cipherSink, serverCrypto), + ) + repo.Add(peer) + + packet := benchmarkIPv4Packet(remoteSrc, clientInner, payloadSize) + sendBuf := make([]byte, epochPrefixSize+len(packet), settings.DefaultEthernetMTU+settings.TCPChacha20Overhead) + + b.ReportAllocs() + b.SetBytes(int64(len(packet))) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + addr, err := parser.DestinationAddress(packet) + if err != nil { + b.Fatalf("parse destination: %v", err) + } + peer, err := repo.FindByDestinationIP(addr) + if err != nil { + b.Fatalf("find peer: %v", err) + } + copy(sendBuf[epochPrefixSize:], packet) + if err := peer.Egress().SendDataIP(sendBuf[:epochPrefixSize+len(packet)]); err != nil { + b.Fatalf("server encrypt/send: %v", err) + } + written, err := benchmarkHandleTCPClientIngress(clientCrypto, tunSink, cipherSink.buf) + if err != nil { + b.Fatalf("client handle packet: %v", err) + } + if written != len(packet) { + b.Fatalf("unexpected payload length: got %d want %d", written, len(packet)) + } + } + }) + } +} diff --git a/src/infrastructure/tunnel/dataplane/client/udp_chacha20/full_cycle_benchmark_test.go b/src/infrastructure/tunnel/dataplane/client/udp_chacha20/full_cycle_benchmark_test.go new file mode 100644 index 00000000..62970b94 --- /dev/null +++ b/src/infrastructure/tunnel/dataplane/client/udp_chacha20/full_cycle_benchmark_test.go @@ -0,0 +1,140 @@ +package udp_chacha20 + +import ( + "bytes" + "fmt" + "io" + "net/netip" + "testing" + "tungo/application/network/connection" + chacha "tungo/infrastructure/cryptography/chacha20" + appip "tungo/infrastructure/network/ip" + "tungo/infrastructure/settings" + "tungo/infrastructure/tunnel/session" + + "golang.org/x/crypto/chacha20poly1305" +) + +type benchmarkHandshake struct { + id [32]byte + c2s []byte + s2c []byte +} + +func (h *benchmarkHandshake) Id() [32]byte { return h.id } +func (h *benchmarkHandshake) KeyClientToServer() []byte { return append([]byte(nil), h.c2s...) } +func (h *benchmarkHandshake) KeyServerToClient() []byte { return append([]byte(nil), h.s2c...) } +func (h *benchmarkHandshake) ServerSideHandshake(connection.Transport) (int, error) { + return 0, nil +} +func (h *benchmarkHandshake) ClientSideHandshake(connection.Transport) error { return nil } + +type benchmarkPacketSink struct { + buf []byte +} + +func (s *benchmarkPacketSink) Write(p []byte) (int, error) { + s.buf = append(s.buf[:0], p...) + return len(p), nil +} + +func benchmarkUDPCryptoPair(b *testing.B) (connection.Crypto, connection.Crypto) { + b.Helper() + + hs := &benchmarkHandshake{ + id: [32]byte{ + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + }, + c2s: bytes.Repeat([]byte{0x11}, chacha20poly1305.KeySize), + s2c: bytes.Repeat([]byte{0x22}, chacha20poly1305.KeySize), + } + + builder := chacha.NewUdpSessionBuilder(chacha.NewDefaultAEADBuilder()) + clientCrypto, _, err := builder.FromHandshake(hs, false) + if err != nil { + b.Fatalf("build client crypto: %v", err) + } + serverCrypto, _, err := builder.FromHandshake(hs, true) + if err != nil { + b.Fatalf("build server crypto: %v", err) + } + return clientCrypto, serverCrypto +} + +func benchmarkIPv4Packet(src, dst netip.Addr, payloadLen int) []byte { + packet := make([]byte, 20+payloadLen) + packet[0] = 0x45 + totalLen := len(packet) + packet[2] = byte(totalLen >> 8) + packet[3] = byte(totalLen) + packet[8] = 64 + packet[9] = 17 + src4 := src.As4() + dst4 := dst.As4() + copy(packet[12:16], src4[:]) + copy(packet[16:20], dst4[:]) + for i := 20; i < len(packet); i++ { + packet[i] = byte(i) + } + return packet +} + +func BenchmarkFullCycleServerToClientUDP(b *testing.B) { + clientInner := netip.MustParseAddr("10.0.0.2") + clientOuter := netip.MustParseAddrPort("203.0.113.10:51820") + remoteSrc := netip.MustParseAddr("1.1.1.1") + parser := appip.NewHeaderParser() + payloadSizes := []int{64, 512, 1400} + + for _, payloadSize := range payloadSizes { + b.Run(fmt.Sprintf("%dB", payloadSize), func(b *testing.B) { + clientCrypto, serverCrypto := benchmarkUDPCryptoPair(b) + cipherSink := &benchmarkPacketSink{} + + repo := session.NewDefaultRepository() + peer := session.NewPeer( + session.NewSessionWithAuth(serverCrypto, nil, clientInner, clientOuter, nil, nil), + connection.NewDefaultEgress(cipherSink, serverCrypto), + ) + repo.Add(peer) + + handler := &TransportHandler{ + writer: io.Discard, + cryptographyService: clientCrypto, + } + + packet := benchmarkIPv4Packet(remoteSrc, clientInner, payloadSize) + prefixLen := chacha.UDPRouteIDLength + chacha20poly1305.NonceSize + sendBuf := make([]byte, prefixLen+len(packet), settings.DefaultEthernetMTU+settings.UDPChacha20Overhead) + + b.ReportAllocs() + b.SetBytes(int64(len(packet))) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + addr, err := parser.DestinationAddress(packet) + if err != nil { + b.Fatalf("parse destination: %v", err) + } + peer, err := repo.FindByDestinationIP(addr) + if err != nil { + b.Fatalf("find peer: %v", err) + } + copy(sendBuf[prefixLen:], packet) + if err := peer.Egress().SendDataIP(sendBuf[:prefixLen+len(packet)]); err != nil { + b.Fatalf("server encrypt/send: %v", err) + } + written, err := handler.handleDatagram(cipherSink.buf) + if err != nil { + b.Fatalf("client handle datagram: %v", err) + } + if written != len(packet) { + b.Fatalf("unexpected payload length: got %d want %d", written, len(packet)) + } + } + }) + } +} diff --git a/src/infrastructure/tunnel/dataplane/server/tcp_chacha20/full_cycle_benchmark_test.go b/src/infrastructure/tunnel/dataplane/server/tcp_chacha20/full_cycle_benchmark_test.go new file mode 100644 index 00000000..47738017 --- /dev/null +++ b/src/infrastructure/tunnel/dataplane/server/tcp_chacha20/full_cycle_benchmark_test.go @@ -0,0 +1,148 @@ +package tcp_chacha20 + +import ( + "bytes" + "fmt" + "io" + "net/netip" + "testing" + "tungo/application/network/connection" + chacha "tungo/infrastructure/cryptography/chacha20" + "tungo/infrastructure/network/ip" + "tungo/infrastructure/network/service_packet" + "tungo/infrastructure/settings" + "tungo/infrastructure/tunnel/session" + + "golang.org/x/crypto/chacha20poly1305" +) + +type benchmarkHandshake struct { + id [32]byte + c2s []byte + s2c []byte +} + +func (h *benchmarkHandshake) Id() [32]byte { return h.id } +func (h *benchmarkHandshake) KeyClientToServer() []byte { return append([]byte(nil), h.c2s...) } +func (h *benchmarkHandshake) KeyServerToClient() []byte { return append([]byte(nil), h.s2c...) } +func (h *benchmarkHandshake) ServerSideHandshake(connection.Transport) (int, error) { + return 0, nil +} +func (h *benchmarkHandshake) ClientSideHandshake(connection.Transport) error { return nil } + +type benchmarkPacketSink struct { + buf []byte +} + +func (s *benchmarkPacketSink) Write(p []byte) (int, error) { + s.buf = append(s.buf[:0], p...) + return len(p), nil +} + +func benchmarkTCPCryptoPair(b *testing.B) (connection.Crypto, connection.Crypto) { + b.Helper() + + hs := &benchmarkHandshake{ + id: [32]byte{ + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + }, + c2s: bytes.Repeat([]byte{0x11}, chacha20poly1305.KeySize), + s2c: bytes.Repeat([]byte{0x22}, chacha20poly1305.KeySize), + } + + builder := chacha.NewTcpSessionBuilder(chacha.NewDefaultAEADBuilder()) + clientCrypto, _, err := builder.FromHandshake(hs, false) + if err != nil { + b.Fatalf("build client crypto: %v", err) + } + serverCrypto, _, err := builder.FromHandshake(hs, true) + if err != nil { + b.Fatalf("build server crypto: %v", err) + } + return clientCrypto, serverCrypto +} + +func benchmarkIPv4Packet(src, dst netip.Addr, payloadLen int) []byte { + packet := make([]byte, 20+payloadLen) + packet[0] = 0x45 + totalLen := len(packet) + packet[2] = byte(totalLen >> 8) + packet[3] = byte(totalLen) + packet[8] = 64 + packet[9] = 17 + src4 := src.As4() + dst4 := dst.As4() + copy(packet[12:16], src4[:]) + copy(packet[16:20], dst4[:]) + for i := 20; i < len(packet); i++ { + packet[i] = byte(i) + } + return packet +} + +func benchmarkHandleTCPServerIngress(peer *session.Peer, tunWriter io.Writer, packet []byte) error { + if len(packet) < chacha20poly1305.Overhead || len(packet) > settings.DefaultEthernetMTU+settings.TCPChacha20Overhead { + return nil + } + if !peer.CryptoRLock() { + return nil + } + pt, err := peer.Crypto().Decrypt(packet) + peer.CryptoRUnlock() + if err != nil { + return err + } + if spType, spOk := service_packet.TryParseHeader(pt); spOk { + switch spType { + case service_packet.RekeyInit, service_packet.Ping: + return nil + } + } + srcIP, srcOk := ip.ExtractSourceIP(pt) + if !srcOk || !peer.Session.IsSourceAllowed(srcIP) { + return nil + } + _, err = tunWriter.Write(pt) + return err +} + +func BenchmarkFullCycleClientToServerTCP(b *testing.B) { + clientInner := netip.MustParseAddr("10.0.0.2") + clientOuter := netip.MustParseAddrPort("203.0.113.10:51820") + remoteDst := netip.MustParseAddr("1.1.1.1") + payloadSizes := []int{64, 512, 1400} + + for _, payloadSize := range payloadSizes { + b.Run(fmt.Sprintf("%dB", payloadSize), func(b *testing.B) { + clientCrypto, serverCrypto := benchmarkTCPCryptoPair(b) + cipherSink := &benchmarkPacketSink{} + clientEgress := connection.NewDefaultEgress(cipherSink, clientCrypto) + tunSink := &benchmarkPacketSink{} + + peer := session.NewPeer( + session.NewSession(serverCrypto, nil, clientInner, clientOuter), + nil, + ) + + packet := benchmarkIPv4Packet(clientInner, remoteDst, payloadSize) + sendBuf := make([]byte, epochPrefixSize+len(packet), settings.DefaultEthernetMTU+settings.TCPChacha20Overhead) + + b.ReportAllocs() + b.SetBytes(int64(len(packet))) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + copy(sendBuf[epochPrefixSize:], packet) + if err := clientEgress.SendDataIP(sendBuf[:epochPrefixSize+len(packet)]); err != nil { + b.Fatalf("client encrypt/send: %v", err) + } + if err := benchmarkHandleTCPServerIngress(peer, tunSink, cipherSink.buf); err != nil { + b.Fatalf("server handle packet: %v", err) + } + } + }) + } +} diff --git a/src/infrastructure/tunnel/dataplane/server/udp_chacha20/full_cycle_benchmark_test.go b/src/infrastructure/tunnel/dataplane/server/udp_chacha20/full_cycle_benchmark_test.go new file mode 100644 index 00000000..62f3c099 --- /dev/null +++ b/src/infrastructure/tunnel/dataplane/server/udp_chacha20/full_cycle_benchmark_test.go @@ -0,0 +1,132 @@ +package udp_chacha20 + +import ( + "bytes" + "fmt" + "io" + "net/netip" + "testing" + "tungo/application/network/connection" + chacha "tungo/infrastructure/cryptography/chacha20" + "tungo/infrastructure/settings" + "tungo/infrastructure/tunnel/session" + + "golang.org/x/crypto/chacha20poly1305" +) + +type benchmarkHandshake struct { + id [32]byte + c2s []byte + s2c []byte +} + +func (h *benchmarkHandshake) Id() [32]byte { return h.id } +func (h *benchmarkHandshake) KeyClientToServer() []byte { return append([]byte(nil), h.c2s...) } +func (h *benchmarkHandshake) KeyServerToClient() []byte { return append([]byte(nil), h.s2c...) } +func (h *benchmarkHandshake) ServerSideHandshake(connection.Transport) (int, error) { + return 0, nil +} +func (h *benchmarkHandshake) ClientSideHandshake(connection.Transport) error { return nil } + +type benchmarkPacketSink struct { + buf []byte +} + +func (s *benchmarkPacketSink) Write(p []byte) (int, error) { + s.buf = append(s.buf[:0], p...) + return len(p), nil +} + +func benchmarkUDPCryptoPair(b *testing.B) (connection.Crypto, connection.Crypto) { + b.Helper() + + hs := &benchmarkHandshake{ + id: [32]byte{ + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + }, + c2s: bytes.Repeat([]byte{0x11}, chacha20poly1305.KeySize), + s2c: bytes.Repeat([]byte{0x22}, chacha20poly1305.KeySize), + } + + builder := chacha.NewUdpSessionBuilder(chacha.NewDefaultAEADBuilder()) + clientCrypto, _, err := builder.FromHandshake(hs, false) + if err != nil { + b.Fatalf("build client crypto: %v", err) + } + serverCrypto, _, err := builder.FromHandshake(hs, true) + if err != nil { + b.Fatalf("build server crypto: %v", err) + } + return clientCrypto, serverCrypto +} + +func benchmarkIPv4Packet(src, dst netip.Addr, payloadLen int) []byte { + packet := make([]byte, 20+payloadLen) + packet[0] = 0x45 + totalLen := len(packet) + packet[2] = byte(totalLen >> 8) + packet[3] = byte(totalLen) + packet[8] = 64 + packet[9] = 17 + src4 := src.As4() + dst4 := dst.As4() + copy(packet[12:16], src4[:]) + copy(packet[16:20], dst4[:]) + for i := 20; i < len(packet); i++ { + packet[i] = byte(i) + } + return packet +} + +func BenchmarkFullCycleClientToServerUDP(b *testing.B) { + clientInner := netip.MustParseAddr("10.0.0.2") + clientOuter := netip.MustParseAddrPort("203.0.113.10:51820") + remoteDst := netip.MustParseAddr("1.1.1.1") + payloadSizes := []int{64, 512, 1400} + + for _, payloadSize := range payloadSizes { + b.Run(benchmarkSizeName(payloadSize), func(b *testing.B) { + clientCrypto, serverCrypto := benchmarkUDPCryptoPair(b) + cipherSink := &benchmarkPacketSink{} + clientEgress := connection.NewDefaultEgress(cipherSink, clientCrypto) + + repo := session.NewDefaultRepository() + peer := session.NewPeer( + session.NewSessionWithAuth(serverCrypto, nil, clientInner, clientOuter, nil, nil), + nil, + ) + repo.Add(peer) + + handler := &TransportHandler{ + routeLookup: repo, + addrUpdater: repo, + dp: newUdpDataplaneWorker(io.Discard, controlPlaneHandler{}), + } + + packet := benchmarkIPv4Packet(clientInner, remoteDst, payloadSize) + prefixLen := chacha.UDPRouteIDLength + chacha20poly1305.NonceSize + sendBuf := make([]byte, prefixLen+len(packet), settings.DefaultEthernetMTU+settings.UDPChacha20Overhead) + + b.ReportAllocs() + b.SetBytes(int64(len(packet))) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + copy(sendBuf[prefixLen:], packet) + if err := clientEgress.SendDataIP(sendBuf[:prefixLen+len(packet)]); err != nil { + b.Fatalf("client encrypt/send: %v", err) + } + if err := handler.handlePacket(clientOuter, cipherSink.buf); err != nil { + b.Fatalf("server handle packet: %v", err) + } + } + }) + } +} + +func benchmarkSizeName(payloadSize int) string { + return fmt.Sprintf("%dB", payloadSize) +} From a345e004fcbde37329a6fc539a739d3c144ec7cb Mon Sep 17 00:00:00 2001 From: Nikita Lipatov <6stringsohei@gmail.com> Date: Sun, 15 Mar 2026 19:24:52 +0300 Subject: [PATCH 02/15] docs(readme): simplify benchmark command --- ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReadMe.md b/ReadMe.md index bc67d429..77bf7ba1 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -43,7 +43,7 @@ To reproduce: ```bash cd src -GOCACHE=/tmp/go-build-cache go test ./infrastructure/tunnel/dataplane/server/udp_chacha20 ./infrastructure/tunnel/dataplane/client/udp_chacha20 ./infrastructure/tunnel/dataplane/server/tcp_chacha20 ./infrastructure/tunnel/dataplane/client/tcp_chacha20 -run ^$ -bench FullCycle -benchmem +go test ./infrastructure/tunnel/dataplane/server/udp_chacha20 ./infrastructure/tunnel/dataplane/client/udp_chacha20 ./infrastructure/tunnel/dataplane/server/tcp_chacha20 ./infrastructure/tunnel/dataplane/client/tcp_chacha20 -run ^$ -bench FullCycle -benchmem ``` --- From c644141e10a3346fdcd0034c191ce22c87087769 Mon Sep 17 00:00:00 2001 From: Nikita Lipatov <6stringsohei@gmail.com> Date: Sun, 15 Mar 2026 20:01:03 +0300 Subject: [PATCH 03/15] test(bench): add scaling and contention benchmarks --- .../connection/egress_benchmark_test.go | 55 +++++++ .../udp_chacha20/full_cycle_benchmark_test.go | 111 ++++++++++++- .../udp_chacha20/full_cycle_benchmark_test.go | 95 +++++++++++- .../session/repository_benchmark_test.go | 146 ++++++++++++++++++ 4 files changed, 391 insertions(+), 16 deletions(-) create mode 100644 src/application/network/connection/egress_benchmark_test.go create mode 100644 src/infrastructure/tunnel/session/repository_benchmark_test.go diff --git a/src/application/network/connection/egress_benchmark_test.go b/src/application/network/connection/egress_benchmark_test.go new file mode 100644 index 00000000..15ff41df --- /dev/null +++ b/src/application/network/connection/egress_benchmark_test.go @@ -0,0 +1,55 @@ +package connection + +import ( + "fmt" + "testing" +) + +type egressBenchCrypto struct{} + +func (egressBenchCrypto) Encrypt(b []byte) ([]byte, error) { return b, nil } +func (egressBenchCrypto) Decrypt(b []byte) ([]byte, error) { return b, nil } + +type egressBenchWriter struct{} + +func (egressBenchWriter) Write(p []byte) (int, error) { return len(p), nil } + +func BenchmarkDefaultEgress_SendDataIP(b *testing.B) { + sizes := []int{64, 512, 1400} + + for _, size := range sizes { + b.Run(fmt.Sprintf("%dB", size), func(b *testing.B) { + egress := NewDefaultEgress(egressBenchWriter{}, egressBenchCrypto{}) + payload := make([]byte, size) + + b.ReportAllocs() + b.SetBytes(int64(size)) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = egress.SendDataIP(payload) + } + }) + } +} + +func BenchmarkDefaultEgress_SendDataIPParallel(b *testing.B) { + sizes := []int{64, 512, 1400} + + for _, size := range sizes { + b.Run(fmt.Sprintf("%dB", size), func(b *testing.B) { + egress := NewDefaultEgress(egressBenchWriter{}, egressBenchCrypto{}) + payload := make([]byte, size) + + b.ReportAllocs() + b.SetBytes(int64(size)) + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = egress.SendDataIP(payload) + } + }) + }) + } +} diff --git a/src/infrastructure/tunnel/dataplane/client/udp_chacha20/full_cycle_benchmark_test.go b/src/infrastructure/tunnel/dataplane/client/udp_chacha20/full_cycle_benchmark_test.go index 62970b94..414a9c42 100644 --- a/src/infrastructure/tunnel/dataplane/client/udp_chacha20/full_cycle_benchmark_test.go +++ b/src/infrastructure/tunnel/dataplane/client/udp_chacha20/full_cycle_benchmark_test.go @@ -2,9 +2,12 @@ package udp_chacha20 import ( "bytes" + "encoding/binary" "fmt" "io" "net/netip" + "sync" + "sync/atomic" "testing" "tungo/application/network/connection" chacha "tungo/infrastructure/cryptography/chacha20" @@ -38,16 +41,14 @@ func (s *benchmarkPacketSink) Write(p []byte) (int, error) { return len(p), nil } -func benchmarkUDPCryptoPair(b *testing.B) (connection.Crypto, connection.Crypto) { +func benchmarkUDPCryptoPair(b *testing.B, idSeed uint64) (connection.Crypto, connection.Crypto) { b.Helper() + var id [32]byte + binary.BigEndian.PutUint64(id[24:], idSeed) + hs := &benchmarkHandshake{ - id: [32]byte{ - 1, 2, 3, 4, 5, 6, 7, 8, - 9, 10, 11, 12, 13, 14, 15, 16, - 17, 18, 19, 20, 21, 22, 23, 24, - 25, 26, 27, 28, 29, 30, 31, 32, - }, + id: id, c2s: bytes.Repeat([]byte{0x11}, chacha20poly1305.KeySize), s2c: bytes.Repeat([]byte{0x22}, chacha20poly1305.KeySize), } @@ -82,6 +83,14 @@ func benchmarkIPv4Packet(src, dst netip.Addr, payloadLen int) []byte { return packet } +func benchmarkIPv4Addr(a, b, c, d byte) netip.Addr { + return netip.AddrFrom4([4]byte{a, b, c, d}) +} + +func benchmarkIPv4AddrPort(a, b, c, d byte, port uint16) netip.AddrPort { + return netip.AddrPortFrom(benchmarkIPv4Addr(a, b, c, d), port) +} + func BenchmarkFullCycleServerToClientUDP(b *testing.B) { clientInner := netip.MustParseAddr("10.0.0.2") clientOuter := netip.MustParseAddrPort("203.0.113.10:51820") @@ -91,7 +100,7 @@ func BenchmarkFullCycleServerToClientUDP(b *testing.B) { for _, payloadSize := range payloadSizes { b.Run(fmt.Sprintf("%dB", payloadSize), func(b *testing.B) { - clientCrypto, serverCrypto := benchmarkUDPCryptoPair(b) + clientCrypto, serverCrypto := benchmarkUDPCryptoPair(b, 1) cipherSink := &benchmarkPacketSink{} repo := session.NewDefaultRepository() @@ -138,3 +147,89 @@ func BenchmarkFullCycleServerToClientUDP(b *testing.B) { }) } } + +func BenchmarkFullCycleServerToClientUDPParallelPeers(b *testing.B) { + type flow struct { + mu sync.Mutex + packet []byte + sink *benchmarkPacketSink + egress connection.Egress + crypto connection.Crypto + } + + const payloadSize = 1400 + peerCounts := []int{1, 64, 1024} + remoteSrc := netip.MustParseAddr("1.1.1.1") + parser := appip.NewHeaderParser() + + for _, peerCount := range peerCounts { + b.Run(fmt.Sprintf("%dpeers", peerCount), func(b *testing.B) { + repo := session.NewDefaultRepository() + flows := make([]flow, peerCount) + + for i := 0; i < peerCount; i++ { + clientInner := benchmarkIPv4Addr(10, byte(i/256), byte(i%256), 10) + clientOuter := benchmarkIPv4AddrPort(203, 0, byte(i/256), byte(i%256), uint16(20000+i)) + clientCrypto, serverCrypto := benchmarkUDPCryptoPair(b, uint64(i+1)) + sink := &benchmarkPacketSink{} + egress := connection.NewDefaultEgress(sink, serverCrypto) + repo.Add(session.NewPeer( + session.NewSessionWithAuth(serverCrypto, nil, clientInner, clientOuter, nil, nil), + egress, + )) + flows[i] = flow{ + packet: benchmarkIPv4Packet(remoteSrc, clientInner, payloadSize), + sink: sink, + egress: egress, + crypto: clientCrypto, + } + } + + var workerSeq atomic.Uint64 + prefixLen := chacha.UDPRouteIDLength + chacha20poly1305.NonceSize + + b.ReportAllocs() + b.SetBytes(int64(20 + payloadSize)) + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + flowIdx := int(workerSeq.Add(1)-1) % len(flows) + f := &flows[flowIdx] + sendBuf := make([]byte, prefixLen+len(f.packet), settings.DefaultEthernetMTU+settings.UDPChacha20Overhead) + handler := &TransportHandler{ + writer: io.Discard, + cryptographyService: f.crypto, + } + + for pb.Next() { + f.mu.Lock() + addr, err := parser.DestinationAddress(f.packet) + if err != nil { + f.mu.Unlock() + b.Fatalf("parse destination: %v", err) + } + peer, err := repo.FindByDestinationIP(addr) + if err != nil { + f.mu.Unlock() + b.Fatalf("find peer: %v", err) + } + copy(sendBuf[prefixLen:], f.packet) + if err := peer.Egress().SendDataIP(sendBuf[:prefixLen+len(f.packet)]); err != nil { + f.mu.Unlock() + b.Fatalf("server encrypt/send: %v", err) + } + written, err := handler.handleDatagram(f.sink.buf) + if err != nil { + f.mu.Unlock() + b.Fatalf("client handle datagram: %v", err) + } + if written != len(f.packet) { + f.mu.Unlock() + b.Fatalf("unexpected payload length: got %d want %d", written, len(f.packet)) + } + f.mu.Unlock() + } + }) + }) + } +} diff --git a/src/infrastructure/tunnel/dataplane/server/udp_chacha20/full_cycle_benchmark_test.go b/src/infrastructure/tunnel/dataplane/server/udp_chacha20/full_cycle_benchmark_test.go index 62f3c099..278168b3 100644 --- a/src/infrastructure/tunnel/dataplane/server/udp_chacha20/full_cycle_benchmark_test.go +++ b/src/infrastructure/tunnel/dataplane/server/udp_chacha20/full_cycle_benchmark_test.go @@ -2,9 +2,12 @@ package udp_chacha20 import ( "bytes" + "encoding/binary" "fmt" "io" "net/netip" + "sync" + "sync/atomic" "testing" "tungo/application/network/connection" chacha "tungo/infrastructure/cryptography/chacha20" @@ -37,16 +40,14 @@ func (s *benchmarkPacketSink) Write(p []byte) (int, error) { return len(p), nil } -func benchmarkUDPCryptoPair(b *testing.B) (connection.Crypto, connection.Crypto) { +func benchmarkUDPCryptoPair(b *testing.B, idSeed uint64) (connection.Crypto, connection.Crypto) { b.Helper() + var id [32]byte + binary.BigEndian.PutUint64(id[24:], idSeed) + hs := &benchmarkHandshake{ - id: [32]byte{ - 1, 2, 3, 4, 5, 6, 7, 8, - 9, 10, 11, 12, 13, 14, 15, 16, - 17, 18, 19, 20, 21, 22, 23, 24, - 25, 26, 27, 28, 29, 30, 31, 32, - }, + id: id, c2s: bytes.Repeat([]byte{0x11}, chacha20poly1305.KeySize), s2c: bytes.Repeat([]byte{0x22}, chacha20poly1305.KeySize), } @@ -81,6 +82,14 @@ func benchmarkIPv4Packet(src, dst netip.Addr, payloadLen int) []byte { return packet } +func benchmarkIPv4Addr(a, b, c, d byte) netip.Addr { + return netip.AddrFrom4([4]byte{a, b, c, d}) +} + +func benchmarkIPv4AddrPort(a, b, c, d byte, port uint16) netip.AddrPort { + return netip.AddrPortFrom(benchmarkIPv4Addr(a, b, c, d), port) +} + func BenchmarkFullCycleClientToServerUDP(b *testing.B) { clientInner := netip.MustParseAddr("10.0.0.2") clientOuter := netip.MustParseAddrPort("203.0.113.10:51820") @@ -89,7 +98,7 @@ func BenchmarkFullCycleClientToServerUDP(b *testing.B) { for _, payloadSize := range payloadSizes { b.Run(benchmarkSizeName(payloadSize), func(b *testing.B) { - clientCrypto, serverCrypto := benchmarkUDPCryptoPair(b) + clientCrypto, serverCrypto := benchmarkUDPCryptoPair(b, 1) cipherSink := &benchmarkPacketSink{} clientEgress := connection.NewDefaultEgress(cipherSink, clientCrypto) @@ -130,3 +139,73 @@ func BenchmarkFullCycleClientToServerUDP(b *testing.B) { func benchmarkSizeName(payloadSize int) string { return fmt.Sprintf("%dB", payloadSize) } + +func BenchmarkFullCycleClientToServerUDPParallelPeers(b *testing.B) { + type flow struct { + mu sync.Mutex + outer netip.AddrPort + packet []byte + egress connection.Egress + sink *benchmarkPacketSink + } + + const payloadSize = 1400 + peerCounts := []int{1, 64, 1024} + remoteDst := netip.MustParseAddr("1.1.1.1") + + for _, peerCount := range peerCounts { + b.Run(fmt.Sprintf("%dpeers", peerCount), func(b *testing.B) { + repo := session.NewDefaultRepository() + handler := &TransportHandler{ + routeLookup: repo, + addrUpdater: repo, + dp: newUdpDataplaneWorker(io.Discard, controlPlaneHandler{}), + } + + flows := make([]flow, peerCount) + for i := 0; i < peerCount; i++ { + clientInner := benchmarkIPv4Addr(10, byte(i/256), byte(i%256), 10) + clientOuter := benchmarkIPv4AddrPort(203, 0, byte(i/256), byte(i%256), uint16(20000+i)) + clientCrypto, serverCrypto := benchmarkUDPCryptoPair(b, uint64(i+1)) + sink := &benchmarkPacketSink{} + flows[i] = flow{ + outer: clientOuter, + packet: benchmarkIPv4Packet(clientInner, remoteDst, payloadSize), + egress: connection.NewDefaultEgress(sink, clientCrypto), + sink: sink, + } + repo.Add(session.NewPeer( + session.NewSessionWithAuth(serverCrypto, nil, clientInner, clientOuter, nil, nil), + nil, + )) + } + + var workerSeq atomic.Uint64 + prefixLen := chacha.UDPRouteIDLength + chacha20poly1305.NonceSize + + b.ReportAllocs() + b.SetBytes(int64(20 + payloadSize)) + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + flowIdx := int(workerSeq.Add(1)-1) % len(flows) + f := &flows[flowIdx] + sendBuf := make([]byte, prefixLen+len(f.packet), settings.DefaultEthernetMTU+settings.UDPChacha20Overhead) + + for pb.Next() { + f.mu.Lock() + copy(sendBuf[prefixLen:], f.packet) + if err := f.egress.SendDataIP(sendBuf[:prefixLen+len(f.packet)]); err != nil { + f.mu.Unlock() + b.Fatalf("client encrypt/send: %v", err) + } + if err := handler.handlePacket(f.outer, f.sink.buf); err != nil { + f.mu.Unlock() + b.Fatalf("server handle packet: %v", err) + } + f.mu.Unlock() + } + }) + }) + } +} diff --git a/src/infrastructure/tunnel/session/repository_benchmark_test.go b/src/infrastructure/tunnel/session/repository_benchmark_test.go new file mode 100644 index 00000000..e6818799 --- /dev/null +++ b/src/infrastructure/tunnel/session/repository_benchmark_test.go @@ -0,0 +1,146 @@ +package session + +import ( + "fmt" + "net/netip" + "testing" +) + +func benchmarkIPv4Addr(a, b, c, d byte) netip.Addr { + return netip.AddrFrom4([4]byte{a, b, c, d}) +} + +func benchmarkIPv4AddrPort(a, b, c, d byte, port uint16) netip.AddrPort { + return netip.AddrPortFrom(benchmarkIPv4Addr(a, b, c, d), port) +} + +func benchmarkRepositoryExactInternal(size int) *DefaultRepository { + repo := NewDefaultRepository().(*DefaultRepository) + for i := 0; i < size; i++ { + peer := NewPeer(NewSession( + nil, + nil, + benchmarkIPv4Addr(10, byte(i/256), byte(i%256), 10), + benchmarkIPv4AddrPort(203, 0, byte(i/256), byte(i%256), uint16(10000+i)), + ), nil) + repo.Add(peer) + } + return repo +} + +func benchmarkRepositoryAllowedHost(size int) *DefaultRepository { + repo := NewDefaultRepository().(*DefaultRepository) + for i := 0; i < size; i++ { + internal := benchmarkIPv4Addr(10, byte(i/256), byte(i%256), 10) + allowed := benchmarkIPv4Addr(172, 16, byte(i/256), byte(i%256)) + peer := NewPeer(NewSessionWithAuth( + nil, + nil, + internal, + benchmarkIPv4AddrPort(203, 0, byte(i/256), byte(i%256), uint16(10000+i)), + nil, + []netip.Prefix{netip.PrefixFrom(allowed, allowed.BitLen())}, + ), nil) + repo.Add(peer) + } + return repo +} + +func benchmarkRepositoryRouteIDs(size int) *DefaultRepository { + repo := NewDefaultRepository().(*DefaultRepository) + for i := 0; i < size; i++ { + peer := NewPeer(&fakeSessionWithCrypto{ + fakeSession: fakeSession{ + internal: benchmarkIPv4Addr(10, byte(i/256), byte(i%256), 10), + external: benchmarkIPv4AddrPort(203, 0, byte(i/256), byte(i%256), uint16(10000+i)), + }, + crypto: &fakeRouteCrypto{id: uint64(i + 1)}, + }, nil) + repo.Add(peer) + } + return repo +} + +func BenchmarkDefaultRepository_FindByDestinationIP_ExactInternal(b *testing.B) { + sizes := []int{1, 100, 1000, 10000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("%dpeers", size), func(b *testing.B) { + repo := benchmarkRepositoryExactInternal(size) + target := benchmarkIPv4Addr(10, byte((size-1)/256), byte((size-1)%256), 10) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + peer, err := repo.FindByDestinationIP(target) + if err != nil || peer == nil { + b.Fatalf("lookup failed: peer=%v err=%v", peer, err) + } + } + }) + } +} + +func BenchmarkDefaultRepository_FindByDestinationIP_AllowedHost(b *testing.B) { + sizes := []int{1, 100, 1000, 10000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("%dpeers", size), func(b *testing.B) { + repo := benchmarkRepositoryAllowedHost(size) + target := benchmarkIPv4Addr(172, 16, byte((size-1)/256), byte((size-1)%256)) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + peer, err := repo.FindByDestinationIP(target) + if err != nil || peer == nil { + b.Fatalf("lookup failed: peer=%v err=%v", peer, err) + } + } + }) + } +} + +func BenchmarkDefaultRepository_FindByDestinationIP_Miss(b *testing.B) { + sizes := []int{1, 100, 1000, 10000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("%dpeers", size), func(b *testing.B) { + repo := benchmarkRepositoryExactInternal(size) + target := benchmarkIPv4Addr(192, 0, 2, 123) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + peer, err := repo.FindByDestinationIP(target) + if err != ErrNotFound || peer != nil { + b.Fatalf("expected miss, got peer=%v err=%v", peer, err) + } + } + }) + } +} + +func BenchmarkDefaultRepository_GetByRouteID(b *testing.B) { + sizes := []int{1, 100, 1000, 10000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("%dpeers", size), func(b *testing.B) { + repo := benchmarkRepositoryRouteIDs(size) + target := uint64(size) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + peer, err := repo.GetByRouteID(target) + if err != nil || peer == nil { + b.Fatalf("route lookup failed: peer=%v err=%v", peer, err) + } + } + }) + } +} From aafa6420d1c889d8fdf503dc25b427847e9ecb50 Mon Sep 17 00:00:00 2001 From: Nikita Lipatov <6stringsohei@gmail.com> Date: Sun, 15 Mar 2026 20:10:34 +0300 Subject: [PATCH 04/15] docs(benchmarks): add performance dashboard page --- docs/TunGo/docusaurus.config.js | 5 + docs/TunGo/src/data/benchmarks.js | 77 +++++ docs/TunGo/src/pages/benchmarks.js | 344 ++++++++++++++++++++ docs/TunGo/src/pages/benchmarks.module.css | 346 +++++++++++++++++++++ 4 files changed, 772 insertions(+) create mode 100644 docs/TunGo/src/data/benchmarks.js create mode 100644 docs/TunGo/src/pages/benchmarks.js create mode 100644 docs/TunGo/src/pages/benchmarks.module.css diff --git a/docs/TunGo/docusaurus.config.js b/docs/TunGo/docusaurus.config.js index 50e4df0d..b33850aa 100644 --- a/docs/TunGo/docusaurus.config.js +++ b/docs/TunGo/docusaurus.config.js @@ -85,6 +85,11 @@ const config = { position: 'left', label: 'Tutorial', }, + { + to: '/benchmarks', + label: 'Benchmarks', + position: 'left', + }, { type: 'localeDropdown', position: 'right', diff --git a/docs/TunGo/src/data/benchmarks.js b/docs/TunGo/src/data/benchmarks.js new file mode 100644 index 00000000..f60a5122 --- /dev/null +++ b/docs/TunGo/src/data/benchmarks.js @@ -0,0 +1,77 @@ +export const benchmarkSnapshot = { + generatedAt: '2026-03-15', + machine: 'Apple M4 Pro', + goVersion: 'Go 1.26', + scope: + 'In-memory userspace dataplane benchmarks. These numbers exclude TUN, socket, kernel, NAT, firewall, and real network overhead.', + fullCycle1400: [ + {id: 'udp-c2s', transport: 'UDP', direction: 'Client -> Server', ns: 2579, throughput: 550.56, allocs: 0}, + {id: 'udp-s2c', transport: 'UDP', direction: 'Server -> Client', ns: 2874, throughput: 494.10, allocs: 0}, + {id: 'tcp-c2s', transport: 'TCP', direction: 'Client -> Server', ns: 2515, throughput: 564.56, allocs: 0}, + {id: 'tcp-s2c', transport: 'TCP', direction: 'Server -> Client', ns: 2564, throughput: 553.73, allocs: 0}, + ], + parallelPeers1400: [ + { + id: 'udp-c2s-parallel', + label: 'UDP Client -> Server', + series: [ + {peers: 1, ns: 4245, throughput: 334.55}, + {peers: 64, ns: 532.6, throughput: 2666.21}, + {peers: 1024, ns: 404.9, throughput: 3506.79}, + ], + }, + { + id: 'udp-s2c-parallel', + label: 'UDP Server -> Client', + series: [ + {peers: 1, ns: 3182, throughput: 446.21}, + {peers: 64, ns: 511.3, throughput: 2777.08}, + {peers: 1024, ns: 480.6, throughput: 2954.59}, + ], + }, + ], + repository: { + fastPath: [ + { + id: 'exact-internal', + label: 'Exact internal lookup', + series: [ + {peers: 1, ns: 8.704}, + {peers: 100, ns: 9.235}, + {peers: 1000, ns: 9.413}, + {peers: 10000, ns: 9.505}, + ], + }, + { + id: 'allowed-host', + label: 'Allowed host lookup', + series: [ + {peers: 1, ns: 13.66}, + {peers: 100, ns: 13.89}, + {peers: 1000, ns: 13.55}, + {peers: 10000, ns: 15.47}, + ], + }, + { + id: 'route-id', + label: 'Route ID lookup', + series: [ + {peers: 1, ns: 3.894}, + {peers: 100, ns: 5.760}, + {peers: 1000, ns: 6.270}, + {peers: 10000, ns: 6.371}, + ], + }, + ], + missPath: [ + {peers: 1, ns: 35.96}, + {peers: 100, ns: 704.0}, + {peers: 1000, ns: 9001}, + {peers: 10000, ns: 92342}, + ], + }, + egress: { + uncontendedNs: 4.666, + contendedNs: 82.36, + }, +}; diff --git a/docs/TunGo/src/pages/benchmarks.js b/docs/TunGo/src/pages/benchmarks.js new file mode 100644 index 00000000..4377ab6c --- /dev/null +++ b/docs/TunGo/src/pages/benchmarks.js @@ -0,0 +1,344 @@ +import React from 'react'; +import Layout from '@theme/Layout'; +import Heading from '@theme/Heading'; +import Link from '@docusaurus/Link'; +import Styles from './benchmarks.module.css'; +import {benchmarkSnapshot} from '@site/src/data/benchmarks'; + +function formatNs(ns) { + if (ns >= 1000) { + return `~${(ns / 1000).toFixed(1)} us`; + } + if (ns >= 100) { + return `~${Math.round(ns)} ns`; + } + return `~${ns.toFixed(1)} ns`; +} + +function formatThroughput(value) { + if (value >= 1000) { + return `~${(value / 1000).toFixed(1)} GB/s`; + } + return `~${value.toFixed(0)} MB/s`; +} + +function buildPolyline(series, valueKey, width, height, padding) { + const values = series.map((point) => point[valueKey]); + const min = Math.min(...values); + const max = Math.max(...values); + const innerWidth = width - padding * 2; + const innerHeight = height - padding * 2; + + return series + .map((point, index) => { + const x = padding + (innerWidth * index) / Math.max(series.length - 1, 1); + const y = + max === min + ? padding + innerHeight / 2 + : padding + innerHeight - ((point[valueKey] - min) / (max - min)) * innerHeight; + return `${x},${y}`; + }) + .join(' '); +} + +function buildDots(series, valueKey, width, height, padding) { + const values = series.map((point) => point[valueKey]); + const min = Math.min(...values); + const max = Math.max(...values); + const innerWidth = width - padding * 2; + const innerHeight = height - padding * 2; + + return series.map((point, index) => { + const x = padding + (innerWidth * index) / Math.max(series.length - 1, 1); + const y = + max === min + ? padding + innerHeight / 2 + : padding + innerHeight - ((point[valueKey] - min) / (max - min)) * innerHeight; + return {x, y, label: point.peers}; + }); +} + +function Sparkline({series, valueKey, color, formatter}) { + const width = 520; + const height = 180; + const padding = 18; + const polyline = buildPolyline(series, valueKey, width, height, padding); + const dots = buildDots(series, valueKey, width, height, padding); + + return ( +
+ +
+ {series.map((point) => ( +
+ {point.peers} peers + {formatter(point[valueKey])} +
+ ))} +
+
+ ); +} + +function MetricCard({label, value, note}) { + return ( +
+
{label}
+
{value}
+
{note}
+
+ ); +} + +function FullCycleTable() { + return ( +
+ + + + + + + + + + + {benchmarkSnapshot.fullCycle1400.map((entry) => ( + + + + + + + ))} + +
PathLatencyThroughputAllocs/op
+ {entry.transport} + {entry.direction} + {formatNs(entry.ns)}{formatThroughput(entry.throughput)}{entry.allocs}
+
+ ); +} + +function FastPathTable() { + const peerCounts = benchmarkSnapshot.repository.fastPath[0].series.map((entry) => entry.peers); + return ( +
+ + + + + {peerCounts.map((count) => ( + + ))} + + + + {benchmarkSnapshot.repository.fastPath.map((row) => ( + + + {row.series.map((point) => ( + + ))} + + ))} + + + {benchmarkSnapshot.repository.missPath.map((point) => ( + + ))} + + +
Lookup{count} peers
{row.label}{formatNs(point.ns)}
Miss path{formatNs(point.ns)}
+
+ ); +} + +export default function BenchmarksPage() { + const bestFullCycle = [...benchmarkSnapshot.fullCycle1400].sort((a, b) => b.throughput - a.throughput)[0]; + const lowestLatency = [...benchmarkSnapshot.fullCycle1400].sort((a, b) => a.ns - b.ns)[0]; + + return ( + +
+
+
+

Performance Benchmarks

+ + Userspace dataplane numbers, without the marketing fog + +

+ TunGo's benchmark suite currently covers full-cycle UDP/TCP packet processing, repository lookups, + egress serialization, and many-peer scaling. The numbers below are benchmark snapshots from{' '} + {benchmarkSnapshot.machine} on {benchmarkSnapshot.goVersion}. +

+

{benchmarkSnapshot.scope}

+
+
+
+ Snapshot date + {benchmarkSnapshot.generatedAt} +
+
+ Benchmark scope + Dataplane core and scaling paths +
+
+ Reproduce + go test -run ^$ -bench FullCycle -benchmem +
+
+
+ +
+ + + + +
+ +
+
+
+

Full-cycle dataplane

+ + 1400-byte snapshot + +
+

+ These numbers cover encrypt, route/peer lookup, validation, decrypt, and handoff to an in-memory sink. + They are useful as an upper-bound for the dataplane core, not as end-to-end VPN throughput claims. +

+
+ +
+ +
+
+
+

Parallel UDP scaling

+ + One peer is not the same as many peers + +
+

+ The parallel-peer benchmarks highlight the difference between single-flow serialization and multi-peer + aggregate throughput. This is the clearest signal that TunGo scales better across peers than within one + peer's send lane. +

+
+ +
+ {benchmarkSnapshot.parallelPeers1400.map((entry) => ( +
+
+ + {entry.label} + + Throughput +
+ formatThroughput(value)} + /> +
+ ))} + + {benchmarkSnapshot.parallelPeers1400.map((entry) => ( +
+
+ + {entry.label} + + Latency +
+ formatNs(value)} + /> +
+ ))} +
+
+ +
+
+
+

Repository behavior

+ + Fast paths are flat. Misses are not. + +
+

+ Internal-IP, allowed-host, and route-ID lookups stay effectively constant out to 10k peers. The miss path + is intentionally shown beside them because it is the part that already behaves like a real scalability + constraint. +

+
+ +
+ +
+
+ + What these benchmarks already tell us + +
+
+ Strong dataplane core +

+ Full-cycle UDP and TCP packet processing stays in the low-microsecond range for 1400-byte packets and + remains allocation-free on the hot path. +

+
+
+ Real architectural pressure points +

+ The relevant bottlenecks are no longer basic crypto or packet plumbing. They are repository misses and + per-peer serialization under contention. +

+
+
+ + Back to docs + +
+
+
+
+ ); +} diff --git a/docs/TunGo/src/pages/benchmarks.module.css b/docs/TunGo/src/pages/benchmarks.module.css new file mode 100644 index 00000000..3c2f38f8 --- /dev/null +++ b/docs/TunGo/src/pages/benchmarks.module.css @@ -0,0 +1,346 @@ +.page { + background: + radial-gradient(circle at top left, rgba(0, 159, 201, 0.12), transparent 30%), + linear-gradient(180deg, #f6fbfd 0%, #ffffff 32%, #f8fafc 100%); + min-height: 100vh; + padding: 3rem 0 4rem; +} + +[data-theme='dark'] .page { + background: + radial-gradient(circle at top left, rgba(79, 195, 227, 0.15), transparent 28%), + linear-gradient(180deg, #06131a 0%, #0b1721 35%, #0f172a 100%); +} + +.hero, +.section { + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem; +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 1.9fr) minmax(280px, 1fr); + gap: 1.5rem; + align-items: stretch; + margin-bottom: 2rem; +} + +.eyebrow, +.sectionEyebrow { + text-transform: uppercase; + letter-spacing: 0.16em; + font-size: 0.76rem; + font-weight: 700; + color: var(--ifm-color-primary); + margin: 0 0 0.75rem; +} + +.title { + font-size: clamp(2.3rem, 4vw, 4.5rem); + line-height: 0.95; + margin-bottom: 1rem; + max-width: 12ch; +} + +.lead { + font-size: 1.1rem; + line-height: 1.7; + max-width: 68ch; + margin-bottom: 1rem; +} + +.scope { + max-width: 65ch; + color: var(--ifm-color-emphasis-700); + margin: 0; +} + +.metaPanel { + border: 1px solid rgba(0, 159, 201, 0.18); + border-radius: 24px; + padding: 1.25rem; + background: rgba(255, 255, 255, 0.82); + backdrop-filter: blur(14px); + box-shadow: 0 28px 64px rgba(15, 23, 42, 0.08); + display: grid; + gap: 1rem; +} + +[data-theme='dark'] .metaPanel { + background: rgba(9, 18, 25, 0.88); + border-color: rgba(79, 195, 227, 0.16); + box-shadow: none; +} + +.metaLabel, +.metricLabel, +.chartMetric, +.calloutLabel { + display: block; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--ifm-color-emphasis-600); + margin-bottom: 0.35rem; +} + +.metaValue { + display: block; + font-weight: 700; +} + +.metaCode { + display: inline-block; + font-size: 0.82rem; + line-height: 1.45; + background: rgba(15, 23, 42, 0.06); + padding: 0.55rem 0.65rem; + border-radius: 12px; +} + +[data-theme='dark'] .metaCode { + background: rgba(255, 255, 255, 0.08); +} + +.metrics { + max-width: 1200px; + margin: 0 auto 2.25rem; + padding: 0 1.5rem; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1rem; +} + +.metricCard, +.chartCard, +.callout { + border-radius: 22px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.86); + box-shadow: 0 20px 40px rgba(15, 23, 42, 0.05); +} + +[data-theme='dark'] .metricCard, +[data-theme='dark'] .chartCard, +[data-theme='dark'] .callout { + background: rgba(15, 23, 42, 0.76); + border-color: rgba(255, 255, 255, 0.08); + box-shadow: none; +} + +.metricCard { + padding: 1.1rem 1.15rem 1.15rem; +} + +.metricValue { + font-size: 1.9rem; + font-weight: 800; + line-height: 1; + margin-bottom: 0.45rem; +} + +.metricNote { + color: var(--ifm-color-emphasis-700); + line-height: 1.55; +} + +.section { + margin-bottom: 2rem; +} + +.sectionHeader { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.9fr); + gap: 1.25rem; + align-items: end; + margin-bottom: 1rem; +} + +.sectionTitle { + margin: 0; +} + +.sectionText { + margin: 0; + color: var(--ifm-color-emphasis-700); + line-height: 1.65; +} + +.tableWrap { + overflow-x: auto; + border-radius: 22px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.84); + box-shadow: 0 20px 40px rgba(15, 23, 42, 0.05); +} + +[data-theme='dark'] .tableWrap { + background: rgba(15, 23, 42, 0.76); + border-color: rgba(255, 255, 255, 0.08); + box-shadow: none; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, +.table td { + padding: 1rem 1.1rem; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + text-align: left; + vertical-align: top; +} + +[data-theme='dark'] .table th, +[data-theme='dark'] .table td { + border-bottom-color: rgba(255, 255, 255, 0.08); +} + +.table tr:last-child td { + border-bottom: 0; +} + +.tablePrimary { + display: block; + font-weight: 700; +} + +.tableSecondary { + display: block; + color: var(--ifm-color-emphasis-600); + font-size: 0.9rem; + margin-top: 0.2rem; +} + +.chartGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.chartCard { + padding: 1rem; +} + +.chartHeader { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.85rem; +} + +.chartTitle { + margin: 0; + font-size: 1.05rem; +} + +.sparklineShell { + display: grid; + gap: 0.75rem; +} + +.sparkline { + width: 100%; + height: auto; +} + +.chartFrame { + fill: rgba(15, 23, 42, 0.02); + stroke: rgba(15, 23, 42, 0.06); + stroke-width: 1; +} + +[data-theme='dark'] .chartFrame { + fill: rgba(255, 255, 255, 0.02); + stroke: rgba(255, 255, 255, 0.08); +} + +.chartLine { + fill: none; + stroke-width: 4; + stroke-linecap: round; + stroke-linejoin: round; +} + +.chartDot { + stroke: #fff; + stroke-width: 3; +} + +[data-theme='dark'] .chartDot { + stroke: #08131a; +} + +.chartLabel { + fill: var(--ifm-color-emphasis-600); + font-size: 12px; + font-weight: 700; +} + +.sparklineLegend { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.55rem; +} + +.legendEntry { + border-radius: 14px; + padding: 0.65rem 0.8rem; + background: rgba(15, 23, 42, 0.04); +} + +[data-theme='dark'] .legendEntry { + background: rgba(255, 255, 255, 0.05); +} + +.legendPeers { + display: block; + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--ifm-color-emphasis-600); + margin-bottom: 0.3rem; +} + +.legendValue { + display: block; + font-weight: 700; +} + +.callout { + padding: 1.5rem; +} + +.calloutTitle { + margin-bottom: 1rem; +} + +.calloutGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin-bottom: 1rem; +} + +@media screen and (max-width: 996px) { + .hero, + .sectionHeader, + .metrics, + .chartGrid, + .calloutGrid { + grid-template-columns: 1fr; + } + + .title { + max-width: none; + } + + .sparklineLegend { + grid-template-columns: 1fr; + } +} From b60212666c74b099d2cd5cc5b535cbd7471819a1 Mon Sep 17 00:00:00 2001 From: Nikita Lipatov <6stringsohei@gmail.com> Date: Sun, 15 Mar 2026 20:36:55 +0300 Subject: [PATCH 05/15] docs(site): align landing and benchmarks visual style --- docs/TunGo/src/components/features/index.js | 34 ++- .../src/components/features/styles.module.css | 84 ++++++- .../src/components/footer/footer.module.css | 15 +- docs/TunGo/src/css/custom.css | 29 +++ docs/TunGo/src/pages/benchmarks.module.css | 50 ++--- docs/TunGo/src/pages/index.js | 100 +++++++-- docs/TunGo/src/pages/index.module.css | 207 +++++++++++++++++- 7 files changed, 448 insertions(+), 71 deletions(-) diff --git a/docs/TunGo/src/components/features/index.js b/docs/TunGo/src/components/features/index.js index b62411e9..30075584 100644 --- a/docs/TunGo/src/components/features/index.js +++ b/docs/TunGo/src/components/features/index.js @@ -3,15 +3,17 @@ import Heading from '@theme/Heading'; import Translate from '@docusaurus/Translate'; import Styles from './styles.module.css'; -function Feature({Svg, title, description}) { +function Feature({Svg, title, description, accent}) { return ( -
-
- -
-
- {title} -
{description}
+
+
+
+ +
+
+ {title} +
{description}
+
); @@ -21,34 +23,48 @@ export default function Features() { return (
-
+
+ Core characteristics + Built to stay small, explicit, and measurable +

+ TunGo is opinionated about what belongs on the hot path and what should stay outside it. That shows up in + the codebase, the transport design, and the benchmark results. +

+
+
CPU-Fast} description={No runtime allocations. Negligible CPU usage under load.} /> RAM-Efficient} description={≈5–15 MB RSS under load, ≈5–8 MB idle} /> Secure} description={Noise IK handshake, X25519 key agreement, ChaCha20-Poly1305 AEAD} /> Multi-Transport Support} description={UDP — high performance, TCP — reliable fallback, WebSocket/WSS — stealth mode} /> Supported Platforms} description={Linux (client and server), macOS (client), Windows (client)} /> Open Source} description={License: AGPLv3} /> diff --git a/docs/TunGo/src/components/features/styles.module.css b/docs/TunGo/src/components/features/styles.module.css index 211c1571..330ac3fd 100644 --- a/docs/TunGo/src/components/features/styles.module.css +++ b/docs/TunGo/src/components/features/styles.module.css @@ -1,19 +1,83 @@ .features { - display: grid; - padding: 2rem 0; - justify-items: center; + padding: 1rem 0 3rem; } .featureSvg { - height: 12rem; - width: 12rem; + height: 6.5rem; + width: 6.5rem; } -.featureDescriptionList { - display: flex; - flex-direction: column; +.sectionIntro { + max-width: 780px; + margin: 0 auto 1.4rem; + text-align: center; +} + +.eyebrow { + display: inline-block; + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 0.76rem; + font-weight: 800; + color: var(--ifm-color-primary); + margin-bottom: 0.8rem; +} + +.sectionTitle { + margin-bottom: 0.8rem; +} + +.sectionText { + color: var(--ifm-color-emphasis-700); + line-height: 1.7; + margin: 0; +} + +.featureColumn { + margin-bottom: 1.2rem; } -.jcCenter { +.featureCard { + height: 100%; + border-radius: 24px; + border: 1px solid var(--site-border); + background: var(--site-surface); + box-shadow: var(--site-shadow); + overflow: hidden; +} + +.featureVisual { + display: flex; justify-content: center; -} \ No newline at end of file + align-items: center; + min-height: 220px; + background: + radial-gradient(circle at top left, color-mix(in srgb, var(--feature-accent) 22%, transparent), transparent 44%), + linear-gradient(180deg, var(--site-surface-strong) 0%, rgba(255, 255, 255, 0.5) 100%); + border-bottom: 1px solid var(--site-border); +} + +[data-theme='dark'] .featureVisual { + background: + radial-gradient(circle at top left, color-mix(in srgb, var(--feature-accent) 28%, transparent), transparent 44%), + linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.02) 100%); +} + +.featureBody { + padding: 1.15rem 1.2rem 1.3rem; +} + +.featureTitle { + margin-bottom: 0.55rem; +} + +.featureDescription { + color: var(--ifm-color-emphasis-700); + line-height: 1.65; +} + +@media screen and (max-width: 996px) { + .features { + padding: 0 0 3rem; + } +} diff --git a/docs/TunGo/src/components/footer/footer.module.css b/docs/TunGo/src/components/footer/footer.module.css index c4df006c..bf7d547c 100644 --- a/docs/TunGo/src/components/footer/footer.module.css +++ b/docs/TunGo/src/components/footer/footer.module.css @@ -3,5 +3,18 @@ flex-direction: column; justify-content: center; align-items: center; - padding: 1rem; + gap: 0.15rem; + max-width: 680px; + margin: 0 auto 2rem; + padding: 1rem 1.2rem; + border: 1px solid var(--site-border); + border-radius: 22px; + background: var(--site-surface); + box-shadow: var(--site-shadow); +} + +.footer p { + margin: 0; + color: var(--ifm-color-emphasis-700); + text-align: center; } diff --git a/docs/TunGo/src/css/custom.css b/docs/TunGo/src/css/custom.css index 50c47631..5dabde45 100644 --- a/docs/TunGo/src/css/custom.css +++ b/docs/TunGo/src/css/custom.css @@ -19,6 +19,18 @@ /* Code highlight — tied to primary for harmony */ --docusaurus-highlighted-code-line-bg: rgba(0, 159, 201, 0.08); + + --site-hero-ink: #07131a; + --site-surface: rgba(255, 255, 255, 0.86); + --site-surface-strong: rgba(255, 255, 255, 0.94); + --site-surface-muted: rgba(6, 19, 26, 0.045); + --site-border: rgba(6, 19, 26, 0.08); + --site-shadow: 0 24px 60px rgba(6, 19, 26, 0.08); + --site-grid-line: rgba(0, 159, 201, 0.1); + --site-gradient: + radial-gradient(circle at top left, rgba(0, 159, 201, 0.16), transparent 28%), + radial-gradient(circle at 85% 10%, rgba(0, 159, 201, 0.08), transparent 24%), + linear-gradient(180deg, #f4fbfd 0%, #ffffff 35%, #f7fafc 100%); } /* Dark mode — same palette, softer contrast */ @@ -31,6 +43,18 @@ --ifm-color-primary-lightest: #c7f4fc; --docusaurus-highlighted-code-line-bg: rgba(0, 159, 201, 0.12); + + --site-hero-ink: #ecf7fb; + --site-surface: rgba(12, 23, 34, 0.82); + --site-surface-strong: rgba(13, 24, 35, 0.94); + --site-surface-muted: rgba(255, 255, 255, 0.05); + --site-border: rgba(255, 255, 255, 0.08); + --site-shadow: none; + --site-grid-line: rgba(79, 195, 227, 0.14); + --site-gradient: + radial-gradient(circle at top left, rgba(79, 195, 227, 0.16), transparent 28%), + radial-gradient(circle at 85% 10%, rgba(79, 195, 227, 0.08), transparent 24%), + linear-gradient(180deg, #06131a 0%, #0b1721 35%, #0f172a 100%); } /* Footer — explicit, neutral background */ @@ -46,3 +70,8 @@ footer.footer { a:hover { text-decoration-thickness: 0.125em; } + +.theme-doc-markdown, +.container { + position: relative; +} diff --git a/docs/TunGo/src/pages/benchmarks.module.css b/docs/TunGo/src/pages/benchmarks.module.css index 3c2f38f8..496d5428 100644 --- a/docs/TunGo/src/pages/benchmarks.module.css +++ b/docs/TunGo/src/pages/benchmarks.module.css @@ -1,15 +1,11 @@ .page { - background: - radial-gradient(circle at top left, rgba(0, 159, 201, 0.12), transparent 30%), - linear-gradient(180deg, #f6fbfd 0%, #ffffff 32%, #f8fafc 100%); + background: var(--site-gradient); min-height: 100vh; padding: 3rem 0 4rem; } [data-theme='dark'] .page { - background: - radial-gradient(circle at top left, rgba(79, 195, 227, 0.15), transparent 28%), - linear-gradient(180deg, #06131a 0%, #0b1721 35%, #0f172a 100%); + background: var(--site-gradient); } .hero, @@ -58,20 +54,18 @@ } .metaPanel { - border: 1px solid rgba(0, 159, 201, 0.18); + border: 1px solid var(--site-border); border-radius: 24px; padding: 1.25rem; - background: rgba(255, 255, 255, 0.82); + background: var(--site-surface); backdrop-filter: blur(14px); - box-shadow: 0 28px 64px rgba(15, 23, 42, 0.08); + box-shadow: var(--site-shadow); display: grid; gap: 1rem; } [data-theme='dark'] .metaPanel { - background: rgba(9, 18, 25, 0.88); - border-color: rgba(79, 195, 227, 0.16); - box-shadow: none; + background: var(--site-surface); } .metaLabel, @@ -117,17 +111,15 @@ .chartCard, .callout { border-radius: 22px; - border: 1px solid rgba(15, 23, 42, 0.08); - background: rgba(255, 255, 255, 0.86); - box-shadow: 0 20px 40px rgba(15, 23, 42, 0.05); + border: 1px solid var(--site-border); + background: var(--site-surface); + box-shadow: var(--site-shadow); } [data-theme='dark'] .metricCard, [data-theme='dark'] .chartCard, [data-theme='dark'] .callout { - background: rgba(15, 23, 42, 0.76); - border-color: rgba(255, 255, 255, 0.08); - box-shadow: none; + background: var(--site-surface); } .metricCard { @@ -171,15 +163,13 @@ .tableWrap { overflow-x: auto; border-radius: 22px; - border: 1px solid rgba(15, 23, 42, 0.08); - background: rgba(255, 255, 255, 0.84); - box-shadow: 0 20px 40px rgba(15, 23, 42, 0.05); + border: 1px solid var(--site-border); + background: var(--site-surface); + box-shadow: var(--site-shadow); } [data-theme='dark'] .tableWrap { - background: rgba(15, 23, 42, 0.76); - border-color: rgba(255, 255, 255, 0.08); - box-shadow: none; + background: var(--site-surface); } .table { @@ -250,14 +240,14 @@ } .chartFrame { - fill: rgba(15, 23, 42, 0.02); - stroke: rgba(15, 23, 42, 0.06); + fill: var(--site-surface-muted); + stroke: var(--site-border); stroke-width: 1; } [data-theme='dark'] .chartFrame { - fill: rgba(255, 255, 255, 0.02); - stroke: rgba(255, 255, 255, 0.08); + fill: var(--site-surface-muted); + stroke: var(--site-border); } .chartLine { @@ -291,11 +281,11 @@ .legendEntry { border-radius: 14px; padding: 0.65rem 0.8rem; - background: rgba(15, 23, 42, 0.04); + background: var(--site-surface-muted); } [data-theme='dark'] .legendEntry { - background: rgba(255, 255, 255, 0.05); + background: var(--site-surface-muted); } .legendPeers { diff --git a/docs/TunGo/src/pages/index.js b/docs/TunGo/src/pages/index.js index d351fa47..6f39a17c 100644 --- a/docs/TunGo/src/pages/index.js +++ b/docs/TunGo/src/pages/index.js @@ -6,23 +6,64 @@ import Layout from '@theme/Layout'; import Features from '@site/src/components/features'; import Heading from '@theme/Heading'; import Styles from './index.module.css'; -import Footer from "../components/footer/footer"; +import Footer from '../components/footer/footer'; + +const highlights = [ + { + label: 'Dataplane', + value: '0 allocs/op', + note: 'Full-cycle UDP/TCP packet path stays allocation-free in current benchmark set.', + }, + { + label: 'Memory', + value: '5-15 MB RSS', + note: 'Lean enough for small VPS instances and constrained edge deployments.', + }, + { + label: 'Transports', + value: 'UDP, TCP, WS', + note: 'High-performance default path plus fallback and stealth-oriented options.', + }, +]; function HomepageHeader() { const {siteConfig} = UseDocusaurusContext(); return ( -
-
- - {siteConfig.title} - -

{siteConfig.tagline}

-
- - Get started in minutes ⏱️ - +
+
+
+ TunGo VPN + + Lean userspace VPN for fast, readable, modern networking + +

+ {siteConfig.tagline}. TunGo focuses on a small hot path, explicit control plane, and performance that is + easy to reason about from the code up. +

+
+ + Get started in minutes + + + View benchmarks + +
+
+ +
+
+ Why it feels light + Go, not baggage +
+
+ {highlights.map((item) => ( +
+ {item.label} + {item.value} +

{item.note}

+
+ ))} +
@@ -39,8 +80,39 @@ export default function Home() {
+
+
+
+

Performance transparency

+ + Benchmark numbers are part of the product story + +

+ The docs site now includes a dedicated benchmark dashboard with dataplane throughput, latency, repository + lookup costs, and scaling behaviour. That makes performance claims auditable instead of decorative. +

+
+
+
+ Full-cycle UDP/TCP + ~2.5-2.9 us +
+
+ 1400B dataplane throughput + ~0.49-0.56 GB/s +
+
+ Repository fast paths + ~4-15 ns +
+ + Open benchmark dashboard + +
+
+
-