Skip to content

Commit 8f1acaf

Browse files
committed
feat: migrate from logf to Go's standard slog and fix tunnel shutdown race condition
- Replace zerodha/logf with log/slog throughout codebase for modern structured logging - Add slog initialization with configurable log levels in cmd/server/init.go - Update all logger interfaces and calls to use *slog.Logger in api, auth, middleware, registry - Fix tunnel shutdown race condition with mutex protection and proper resource cleanup - Remove explicit TUN close as WireGuard device handles it automatically - Update prod config to use port 55555 to reduce scanning attacks - Fix Makefile lint command to use $(PWD) instead of $(pwd)
1 parent 63b0aa2 commit 8f1acaf

File tree

12 files changed

+401
-201
lines changed

12 files changed

+401
-201
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ fresh: build run
1818

1919
.PHONY: lint
2020
lint:
21-
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.43.0 golangci-lint run -v
21+
docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.43.0 golangci-lint run -v

cmd/server/init.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,48 @@ package main
22

33
import (
44
"fmt"
5+
"log/slog"
56
"os"
67
"strings"
78

8-
"github.com/zerodha/logf"
9-
109
"github.com/knadh/koanf"
1110
"github.com/knadh/koanf/parsers/toml"
1211
"github.com/knadh/koanf/providers/env"
1312
"github.com/knadh/koanf/providers/file"
1413
flag "github.com/spf13/pflag"
1514
)
1615

17-
// initLogger initializes logger instance.
18-
func initLogger(ko *koanf.Koanf) logf.Logger {
19-
opts := logf.Opts{EnableColor: true, EnableCaller: true}
20-
if ko.String("app.log_level") == "debug" {
21-
opts.Level = logf.DebugLevel
16+
// initLogger initializes slog logger instance.
17+
func initLogger(ko *koanf.Koanf) *slog.Logger {
18+
// Parse log level from config
19+
var level slog.Level
20+
switch strings.ToLower(ko.String("app.log_level")) {
21+
case "debug":
22+
level = slog.LevelDebug
23+
case "info":
24+
level = slog.LevelInfo
25+
case "warn", "warning":
26+
level = slog.LevelWarn
27+
case "error":
28+
level = slog.LevelError
29+
default:
30+
level = slog.LevelInfo
31+
}
32+
33+
// Configure handler options
34+
opts := &slog.HandlerOptions{
35+
Level: level,
36+
AddSource: true, // Enable caller information
2237
}
23-
return logf.New(opts)
38+
39+
// Create text handler for console output
40+
handler := slog.NewTextHandler(os.Stdout, opts)
41+
logger := slog.New(handler)
42+
43+
// Set as default logger
44+
slog.SetDefault(logger)
45+
46+
return logger
2447
}
2548

2649
// initConfig loads config to `ko` object.

cmd/server/main.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"log/slog"
67
"os"
78
"os/signal"
89
"sync"
@@ -30,12 +31,13 @@ func main() {
3031
ko := initConfig("config.sample.toml", "ARBOK_SERVER")
3132
logger := initLogger(ko)
3233

33-
logger.Info("starting arbok server", "version", buildString)
34+
logger.Info("starting arbok server", slog.String("version", buildString))
3435

3536
// Parse configuration
3637
cfg, err := parseConfig(ko)
3738
if err != nil {
38-
logger.Fatal("config error", "error", err)
39+
logger.Error("config error", slog.Any("error", err))
40+
os.Exit(1)
3941
}
4042

4143
// Initialize WireGuard tunnel
@@ -47,7 +49,8 @@ func main() {
4749
PrivateKey: cfg.Server.PrivateKey,
4850
})
4951
if err != nil {
50-
logger.Fatal("failed to initialize tunnel", "error", err)
52+
logger.Error("failed to initialize tunnel", slog.Any("error", err))
53+
os.Exit(1)
5154
}
5255

5356
// Initialize registry
@@ -57,7 +60,8 @@ func main() {
5760
CleanupInterval: cfg.Tunnel.CleanupInterval,
5861
}, logger)
5962
if err != nil {
60-
logger.Fatal("failed to initialize registry", "error", err)
63+
logger.Error("failed to initialize registry", slog.Any("error", err))
64+
os.Exit(1)
6165
}
6266

6367
// Initialize authenticator
@@ -86,7 +90,7 @@ func main() {
8690
go func() {
8791
defer wg.Done()
8892
if err := tun.Up(ctx); err != nil {
89-
logger.Error("tunnel error", "error", err)
93+
logger.Error("tunnel error", slog.Any("error", err))
9094
}
9195
}()
9296

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,22 @@ require (
1010
github.com/gorilla/mux v1.8.1
1111
github.com/knadh/koanf v1.5.0
1212
github.com/spf13/pflag v1.0.7
13-
github.com/vishvananda/netlink v1.3.1
14-
github.com/zerodha/logf v0.5.5
1513
golang.org/x/crypto v0.40.0
1614
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
1715
)
1816

1917
require (
2018
github.com/fsnotify/fsnotify v1.9.0 // indirect
19+
github.com/google/btree v1.1.3 // indirect
2120
github.com/mitchellh/copystructure v1.2.0 // indirect
2221
github.com/mitchellh/mapstructure v1.5.0 // indirect
2322
github.com/mitchellh/reflectwalk v1.0.2 // indirect
2423
github.com/pelletier/go-toml v1.9.5 // indirect
2524
github.com/valyala/fastrand v1.1.0 // indirect
2625
github.com/valyala/histogram v1.2.0 // indirect
27-
github.com/vishvananda/netns v0.0.5 // indirect
2826
golang.org/x/net v0.42.0 // indirect
2927
golang.org/x/sys v0.34.0 // indirect
28+
golang.org/x/time v0.12.0 // indirect
3029
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
30+
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
3131
)

go.sum

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
8080
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
8181
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
8282
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
83-
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
84-
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
83+
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
84+
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
8585
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
8686
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
8787
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -237,22 +237,15 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
237237
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
238238
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
239239
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
240+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
240241
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
241-
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
242-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
243242
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
244243
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
245244
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
246245
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
247-
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
248-
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
249-
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
250-
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
251246
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
252247
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
253248
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
254-
github.com/zerodha/logf v0.5.5 h1:AhxHlixHNYwhFjvlgTv6uO4VBKYKxx2I6SbHoHtWLBk=
255-
github.com/zerodha/logf v0.5.5/go.mod h1:HWpfKsie+WFFpnUnUxelT6Z0FC6xu9+qt+oXNMPg6y8=
256249
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
257250
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
258251
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
@@ -336,8 +329,6 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
336329
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
337330
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
338331
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
339-
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
340-
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
341332
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
342333
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
343334
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -348,8 +339,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
348339
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
349340
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
350341
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
351-
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
352-
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
342+
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
343+
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
353344
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
354345
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
355346
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

internal/api/handlers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ func (s *Server) handleDeleteTunnel(w http.ResponseWriter, r *http.Request) {
131131
}
132132

133133
// Remove peer from WireGuard
134-
if err := s.tun.RemovePeer(t.PublicKey); err != nil {
134+
if err := s.tun.RemovePeer(t.PublicKey, t.AllowedIP); err != nil {
135135
s.logger.Error("failed to remove peer", "error", err, "tunnel_id", t.ID)
136136
}
137137

@@ -240,7 +240,7 @@ PostUp = echo "🐍 Arbok tunnel active! Local port %d → %s"
240240
241241
[Peer]
242242
PublicKey = %s
243-
AllowedIPs = 10.100.0.1/32
243+
AllowedIPs = 10.100.0.0/24
244244
Endpoint = %s
245245
PersistentKeepalive = 25`,
246246
t.AllowedIP,

internal/api/proxy.go

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"bufio"
5+
"context"
56
"fmt"
67
"io"
78
"net"
@@ -12,7 +13,7 @@ import (
1213
"time"
1314
)
1415

15-
// createReverseProxy creates a reverse proxy for a tunnel
16+
// createReverseProxy creates a reverse proxy for a tunnel using netstack
1617
func (s *Server) createReverseProxy(targetIP string, port uint16) *httputil.ReverseProxy {
1718
target := &url.URL{
1819
Scheme: "http",
@@ -21,13 +22,12 @@ func (s *Server) createReverseProxy(targetIP string, port uint16) *httputil.Reve
2122

2223
proxy := httputil.NewSingleHostReverseProxy(target)
2324

24-
// Customize the transport for better performance
25+
// Get netstack from tunnel for userspace networking
26+
tnet := s.tun.GetNetstack()
27+
28+
// Customize the transport to use netstack (userspace WireGuard networking)
2529
proxy.Transport = &http.Transport{
26-
Proxy: http.ProxyFromEnvironment,
27-
DialContext: (&net.Dialer{
28-
Timeout: 30 * time.Second,
29-
KeepAlive: 30 * time.Second,
30-
}).DialContext,
30+
DialContext: tnet.DialContext, // Use netstack instead of kernel networking
3131
ForceAttemptHTTP2: true,
3232
MaxIdleConns: 100,
3333
IdleConnTimeout: 90 * time.Second,
@@ -75,21 +75,29 @@ func (s *Server) createReverseProxy(targetIP string, port uint16) *httputil.Reve
7575
return proxy
7676
}
7777

78-
// handleTunnelTrafficWithProxy handles incoming traffic and proxies it to the tunnel
79-
func (s *Server) handleTunnelTrafficWithProxy(w http.ResponseWriter, r *http.Request) {
80-
// Extract subdomain from host
81-
host := r.Host
82-
if idx := strings.Index(host, ":"); idx != -1 {
78+
// extractSubdomain extracts the subdomain from a host header value.
79+
// It handles port stripping and returns just the subdomain portion.
80+
func extractSubdomain(host string) string {
81+
// Remove port if present
82+
if idx := strings.IndexByte(host, ':'); idx != -1 {
8383
host = host[:idx]
8484
}
8585

86-
parts := strings.Split(host, ".")
87-
if len(parts) < 2 {
88-
http.Error(w, "Invalid host", http.StatusBadRequest)
86+
// Extract subdomain (first part before first dot)
87+
if idx := strings.IndexByte(host, '.'); idx != -1 {
88+
return host[:idx]
89+
}
90+
return host
91+
}
92+
93+
// handleTunnelTrafficWithProxy handles incoming traffic and proxies it to the tunnel
94+
func (s *Server) handleTunnelTrafficWithProxy(w http.ResponseWriter, r *http.Request) {
95+
// Extract subdomain from host
96+
subdomain := extractSubdomain(r.Host)
97+
if subdomain == "" {
98+
http.Error(w, "Invalid host header", http.StatusBadRequest)
8999
return
90100
}
91-
92-
subdomain := parts[0]
93101
tunnel := s.registry.GetTunnelBySubdomain(subdomain)
94102
if tunnel == nil {
95103
http.Error(w, "Tunnel not found", http.StatusNotFound)
@@ -121,7 +129,7 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request, targetI
121129
targetURL += "?" + r.URL.RawQuery
122130
}
123131

124-
targetConn, resp, err := websocketDial(targetURL, r.Header)
132+
targetConn, resp, err := s.websocketDial(targetURL, r.Header)
125133
if err != nil {
126134
s.logger.Error("websocket dial error", "error", err, "target", targetURL)
127135
http.Error(w, "Bad Gateway", http.StatusBadGateway)
@@ -150,31 +158,47 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request, targetI
150158
return
151159
}
152160

153-
// Proxy data between connections
161+
// Use context for proper cancellation
162+
ctx, cancel := context.WithCancel(r.Context())
163+
defer cancel()
164+
165+
// Proxy data between connections with proper cleanup
154166
errc := make(chan error, 2)
155167
go func() {
168+
defer cancel() // Cancel context when one direction completes
156169
_, err := io.Copy(targetConn, clientConn)
157170
errc <- err
158171
}()
159172
go func() {
173+
defer cancel() // Cancel context when one direction completes
160174
_, err := io.Copy(clientConn, targetConn)
161175
errc <- err
162176
}()
163177

164-
// Wait for either copy to complete
165-
<-errc
178+
// Wait for either copy to complete or context cancellation
179+
select {
180+
case <-ctx.Done():
181+
return
182+
case <-errc:
183+
return
184+
}
166185
}
167186

168-
// websocketDial dials a WebSocket connection
169-
func websocketDial(targetURL string, headers http.Header) (net.Conn, *http.Response, error) {
187+
// websocketDial dials a WebSocket connection using the tunnel's netstack
188+
func (s *Server) websocketDial(targetURL string, headers http.Header) (net.Conn, *http.Response, error) {
170189
// Parse the URL
171190
u, err := url.Parse(targetURL)
172191
if err != nil {
173192
return nil, nil, err
174193
}
175194

176-
// Dial TCP connection
177-
conn, err := net.DialTimeout("tcp", u.Host, 10*time.Second)
195+
// Get netstack from tunnel for userspace networking
196+
tnet := s.tun.GetNetstack()
197+
198+
// Dial TCP connection using netstack (userspace WireGuard networking)
199+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
200+
defer cancel()
201+
conn, err := tnet.DialContext(ctx, "tcp", u.Host)
178202
if err != nil {
179203
return nil, nil, err
180204
}

0 commit comments

Comments
 (0)