I recently needed to build a proxy server, and Go was a natural choice for the job — its standard library and lightweight goroutines make network services a joy to write. In this post, I'll walk through the whole process step by step: from the first prototype to a Dockerized setup with logging and Prometheus metrics.
What We're Building
The proxy accepts two types of connections:
- HTTP/HTTPS forward proxy — built on
net/httpand httputil.ReverseProxy (for regular requests), plus manual handling of the CONNECT method (for HTTPS tunnels). - SOCKS5 proxy — using the popular
github.com/armon/go-socks5library (golang.org/x/net/proxyis client-side only, so a server implementation would have to be written from scratch).
By the end, we'll have a single binary that runs both proxies, logs every request, exposes Prometheus metrics, and ships with a docker-compose.yml that brings up Prometheus and Grafana alongside it.
Prerequisites: a recent version of Go (1.25 was used here), curl for testing, and Docker if you want the containerized setup.
Step 1: The HTTP/HTTPS Forward Proxy
First, I sketched out the code for the HTTP/HTTPS forward proxy:
package main
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
// HTTPProxy holds the proxy configuration (can be extended later)
type HTTPProxy struct {
transport *http.Transport
}
// NewHTTPProxy creates a new proxy instance with a custom transport
func NewHTTPProxy() *HTTPProxy {
return &HTTPProxy{
transport: &http.Transport{
// InsecureSkipVerify is for testing only — remove in production
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
},
}
}
// ServeHTTP implements http.Handler
func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// If this is a CONNECT request, handle the HTTPS tunnel
if r.Method == http.MethodConnect {
p.handleTunnel(w, r)
return
}
// For regular HTTP requests, use ReverseProxy
p.handleHTTP(w, r)
}
// handleTunnel establishes a tunnel for CONNECT (HTTPS) requests
func (p *HTTPProxy) handleTunnel(w http.ResponseWriter, r *http.Request) {
// Target host from the request (e.g., "example.com:443")
target := r.Host
if !strings.Contains(target, ":") {
target += ":443" // default to 443 if no port is specified
}
// Connect to the target server
destConn, err := net.Dial("tcp", target)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer destConn.Close()
// Reply to the client with "200 Connection Established"
w.WriteHeader(http.StatusOK)
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer clientConn.Close()
// Copy data in both directions (client <-> target server)
go io.Copy(destConn, clientConn)
io.Copy(clientConn, destConn)
}
// handleHTTP uses ReverseProxy for regular HTTP requests
func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) {
// Build the target URL from the request.
// For a forward proxy, the request URL contains the full address
//(e.g., http://example.com/foo)
targetURL, err := url.Parse(r.URL.String())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// If no scheme is specified, infer it from the port (usually 80)
if targetURL.Scheme == "" {
if r.URL.Port() == "443" {
targetURL.Scheme = "https"
} else {
targetURL.Scheme = "http"
}
}
// Create a ReverseProxy with a custom Director that rewrites the request
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
// Forward to the target host
req.URL.Scheme = targetURL.Scheme
req.URL.Host = targetURL.Host
req.URL.Path = targetURL.Path
req.URL.RawQuery = targetURL.RawQuery
req.Host = targetURL.Host // important for virtual hosts
// Drop proxy-specific headers (if needed)
req.Header.Del("Proxy-Connection")
},
Transport: p.transport,
}
// Serve the request
proxy.ServeHTTP(w, r)
}
func main() {
proxy := NewHTTPProxy()
server := &http.Server{
Addr: ":8080",
Handler: proxy,
}
fmt.Println("HTTP/HTTPS forward proxy is running on :8080")
if err := server.ListenAndServe(); err != nil {
fmt.Println("Error:", err)
}
}
For CONNECT requests, the proxy hijacks the connection and pipes raw bytes in both directions, so the encrypted TLS session passes through untouched.
Step 2: The SOCKS5 Proxy
Next, the SOCKS5 proxy (using github.com/armon/go-socks5):
package main
import (
"fmt"
"log"
"net"
"github.com/armon/go-socks5"
)
func main() {
// Create the SOCKS5 configuration (authentication, logging, etc. can be added here)
conf := &socks5.Config{
// You can plug in a custom Dialer here, e.g., to enforce restrictions
// Logger: log.New(os.Stdout, "socks5: ", log.LstdFlags),
}
// Create the server
server, err := socks5.New(conf)
if err != nil {
log.Fatalf("Failed to create SOCKS5 server: %v", err)
}
// Start listening on port 1080
addr := ":1080"
listener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("Failed to start listener: %v", err)
}
fmt.Printf("SOCKS5 proxy is running on %s\n", addr)
if err := server.Serve(listener); err != nil {
log.Fatalf("Server error: %v", err)
}
}
Running and testing
Everything works nicely. Launch it with:
// The proxy will listen on http://localhost:8080
go run http_proxy.go
// The proxy will listen on socks5://localhost:1080
go run socks5_proxy.go
Testing it is straightforward:
# HTTP request
curl -x http://localhost:8080 http://example.com
# HTTPS request (via CONNECT)
curl -x http://localhost:8080 https://example.com
# SOCKS5 proxy
curl -x socks5://localhost:1080 https://example.com
Putting it all together
Now let's run both proxies from a single process. The project structure:
main.go
http_proxy.go
socks5_proxy.go
The entry point uses a cancellable context for graceful shutdown:
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return runSocks5(ctx) })
g.Go(func() error { return runHTTPProxy(ctx, ":8080") })
if err := g.Wait(); err != nil && err != context.Canceled {
log.Fatalf("Proxies stopped with an error: %v", err)
}
log.Println("Proxies shut down")
}
errgroup runs both proxies concurrently and tears everything down cleanly on SIGINT/SIGTERM.
Adding Logging
Great — now let's add logging for the SOCKS5 proxy. The go-socks5 library has built-in support for this: the Logger field in socks5.Config. You can pass in any logger that satisfies the expected interface — in practice, anything with a Printf method, such as *log.Logger (if you're on slog, wrap it with slog.NewLogLogger).
func runSocks5(ctx context.Context) error {
// Create a logger with the desired prefix and flags
logger := log.New(os.Stdout, "[SOCKS5] ", log.LstdFlags|log.Lshortfile)
conf := &socks5.Config{
Logger: logger, // Pass in the logger
}
server, err := socks5.New(conf)
if err != nil {
return fmt.Errorf("creating SOCKS5: %w", err)
}
// ... the rest of the code as before
}
And the same for the HTTP proxy:
func runHTTPProxy(ctx context.Context, addr string) error {
proxy := NewHTTPProxy()
logger := log.New(os.Stdout, "[HTTP] ", log.LstdFlags|log.Lshortfile)
server := &http.Server{
Addr: addr,
Handler: proxy,
ErrorLog: logger,
}
// ... the rest of the code as before
}
http.Server.ErrorLog covers server-level errors (TLS handshakes, Accept failures, panics), while socks5.Config.Logger logs protocol errors, authentication issues, and read/write failures.
Now let's log a bit more detail: the method, URL, and client IP. Add this to ServeHTTP
func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Log every request
log.Printf("[HTTP] %s %s from %s", r.Method, r.URL.String(), r.RemoteAddr)
if r.Method == http.MethodConnect {
p.handleTunnel(w, r)
return
}
p.handleHTTP(w, r)
}
One caveat: if your proxy runs behind a load balancer or another proxy, r.RemoteAddr will show the address of the previous hop. In that case, use the X-Forwarded-For header instead:
clientIP := r.Header.Get("X-Forwarded-For")
if clientIP == "" {
clientIP = r.RemoteAddr
}
log.Printf("[HTTP] %s %s from %s", r.Method, r.URL.String(), clientIP)
Next, let's add proper middleware that logs the response status, processing time, and response size. For this we need a couple of wrapper types:
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
size int
}
func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
return &loggingResponseWriter{
ResponseWriter: w,
statusCode: http.StatusOK, // default to 200 unless explicitly set
}
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
n, err := lrw.ResponseWriter.Write(b)
lrw.size += n
return n, err
}
// Hijack is required to support CONNECT tunnels
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hijacker, ok := lrw.ResponseWriter.(http.Hijacker); ok {
return hijacker.Hijack()
}
return nil, nil, fmt.Errorf("ResponseWriter does not support Hijack")
}
The final changes:
func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap the ResponseWriter for logging
lrw := newLoggingResponseWriter(w)
// Handle the request through the existing methods
if r.Method == http.MethodConnect {
p.handleTunnel(lrw, r)
} else {
p.handleHTTP(lrw, r)
}
// Measure the duration and log it
duration := time.Since(start)
clientIP := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
clientIP = forwarded
}
// Use the standard log or your own logger
log.Printf("[HTTP] %s %s from %s — status: %d, size: %d bytes, duration: %v",
r.Method, r.URL.String(), clientIP, lrw.statusCode, lrw.size, duration)
}
Sample output:
[HTTP] GET http://detectportal.firefox.com/canonical.html from 127.0.0.1:12495 — status: 200, size: 90 bytes, duration: 157.5029ms
[HTTP] GET http://detectportal.firefox.com/success.txt?ipv4 from 127.0.0.1:12497 — status: 200, size: 8 bytes, duration: 143.6892ms
[HTTP] GET http://detectportal.firefox.com/success.txt?ipv6 from 127.0.0.1:12498 — status: 200, size: 8 bytes, duration: 151.2498ms
Let's add similar logging for SOCKS5, so we can see the command (CONNECT, BIND, UDP), the target address, the client IP, the session duration, and the status (success/failure).
Here's what we add:
type loggingListener struct {
net.Listener
logger *log.Logger
}
func (l loggingListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
clientAddr := conn.RemoteAddr().String()
l.logger.Printf("Accepted connection from %s", clientAddr)
// Store the start time in the wrapper
return &loggingConn{
Conn: conn,
logger: l.logger,
clientAddr: clientAddr,
startTime: time.Now(),
}, nil
}
type loggingConn struct {
net.Conn
logger *log.Logger
clientAddr string
startTime time.Time
}
func (c *loggingConn) Close() error {
err := c.Conn.Close()
duration := time.Since(c.startTime)
c.logger.Printf("Connection from %s closed, total duration: %v", c.clientAddr, duration)
return err
}
And the full runSocks5 with logging wired in:
func runSocks5(ctx context.Context) error {
logger := log.New(os.Stdout, "[SOCKS5] ", log.LstdFlags)
rawListener, err := net.Listen("tcp", ":1080")
if err != nil {
return fmt.Errorf("SOCKS5 listener: %w", err)
}
// Logging listener
listener := loggingListener{
Listener: rawListener,
logger: logger,
}
conf := &socks5.Config{
Logger: logger,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
// Log the target address and connection setup time
start := time.Now()
conn, err := net.Dial(network, addr)
duration := time.Since(start)
if err != nil {
logger.Printf("Dial to %s failed: %v (duration: %v)", addr, err, duration)
return nil, err
}
logger.Printf("Dial to %s succeeded (duration: %v)", addr, duration)
return conn, nil
},
}
server, err := socks5.New(conf)
if err != nil {
return fmt.Errorf("creating SOCKS5: %w", err)
}
errCh := make(chan error, 1)
go func() {
errCh <- server.Serve(listener)
}()
logger.Printf("SOCKS5 is running on :1080")
select {
case <-ctx.Done():
rawListener.Close()
<-errCh
logger.Printf("SOCKS5 stopped")
return ctx.Err()
case err := <-errCh:
return err
}
}
Sample output:
[SOCKS5] 2025/03/15 12:34:56 Accepted connection from 192.168.1.100:54321
[SOCKS5] 2025/03/15 12:34:56 Dial to 2.23.167.41:443 succeeded (duration: 40.6345ms)
[SOCKS5] 2025/03/15 12:35:10 Connection from 192.168.1.100:54321 closed, total duration: 14s
Adding metrics
Let's add a /metrics endpoint with a single metric — the number of active TCP connections (covering both HTTP and SOCKS5). That gives us basic load monitoring. We'll create a metrics.go file and add github.com/prometheus/client_golang to the project.
In metrics.go:
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net"
"sync/atomic"
)
var (
activeConnections = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "proxy_active_connections",
Help: "Current number of active TCP connections",
})
)
func init() {
prometheus.MustRegister(activeConnections)
}
// metricsListener is a wrapper that counts active connections
type metricsListener struct {
net.Listener
}
func (ml metricsListener) Accept() (net.Conn, error) {
conn, err := ml.Listener.Accept()
if err != nil {
return nil, err
}
activeConnections.Inc()
return &metricsConn{Conn: conn}, nil
}
type metricsConn struct {
net.Conn
}
func (mc *metricsConn) Close() error {
err := mc.Conn.Close()
activeConnections.Dec()
return err
}
func runMetrics(ctx context.Context) error {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
server := &http.Server{
Addr: ":9090",
Handler: mux,
}
errCh := make(chan error, 1)
go func() {
errCh <- server.ListenAndServe()
}()
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
return ctx.Err()
case err := <-errCh:
return err
}
}
Integrating into runHTTPProxy:
func runHTTPProxy(ctx context.Context, addr string) error {
logger := log.New(os.Stdout, "[HTTP] ", log.LstdFlags)
rawListener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
listener := metricsListener{Listener: rawListener}
proxy := NewHTTPProxy()
server := &http.Server{
Handler: proxy,
ErrorLog: logger,
}
errCh := make(chan error, 1)
go func() {
errCh <- server.Serve(listener)
}()
select {
case <-ctx.Done():
// graceful shutdown
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
return ctx.Err()
case err := <-errCh:
return err
}
}
Integrating into runSocks5 — here we also use metricsListener instead of loggingListener:
func runSocks5(ctx context.Context) error {
logger := log.New(os.Stdout, "[SOCKS5] ", log.LstdFlags)
rawListener, err := net.Listen("tcp", ":1080")
if err != nil {
return err
}
listener := metricsListener{Listener: rawListener}
conf := &socks5.Config{
Logger: logger,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
// Log the target address and connection setup time
start := time.Now()
conn, err := net.Dial(network, addr)
duration := time.Since(start)
if err != nil {
logger.Printf("Dial to %s failed: %v (duration: %v)", addr, err, duration)
return nil, err
}
logger.Printf("Dial to %s succeeded (duration: %v)", addr, duration)
return conn, nil
},
}
server, err := socks5.New(conf)
if err != nil {
return err
}
errCh := make(chan error, 1)
go func() {
errCh <- server.Serve(listener)
}()
select {
case <-ctx.Done():
rawListener.Close()
<-errCh
logger.Printf("SOCKS5 stopped")
return ctx.Err()
case err := <-errCh:
return err
}
}
And the /metrics server itself:
func runMetrics(ctx context.Context) error {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
server := &http.Server{
Addr: ":9090",
Handler: mux,
}
errCh := make(chan error, 1)
go func() {
errCh <- server.ListenAndServe()
}()
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
return ctx.Err()
case err := <-errCh:
return err
}
}
We add a third goroutine in main:
g.Go(func() error { return runMetrics(ctx) })
Containerizing with Docker
The last piece is the Dockerfile:
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o proxy .
FROM alpine:latest
RUN apk --no-cache add ca-certificates && \
addgroup -S appgroup && \
adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/proxy .
RUN chown appuser:appgroup /app/proxy && chmod 755 /app/proxy
EXPOSE 8080 1080 9090
USER appuser
CMD ["./proxy"]
And a docker-compose.yml that brings up the proxy alongside Prometheus and Grafana:
version: '3.8'
services:
proxy:
build: .
container_name: go-proxy
ports:
- "8080:8080" # HTTP proxy
- "1080:1080" # SOCKS5
- "9090:9090" # metrics
restart: unless-stopped
networks:
- monitoring
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
ports:
- "9091:9090" # We forward it to a different port to avoid conflicts with proxy metrics.
restart: unless-stopped
networks:
- monitoring
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
grafana:
image: grafana/grafana:latest
container_name: grafana
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
ports:
- "3000:3000"
restart: unless-stopped
networks:
- monitoring
networks:
monitoring:
volumes:
prometheus_data:
grafana_data:
Final files:
main.go:
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return runSocks5(ctx) })
g.Go(func() error { return runHTTPProxy(ctx, ":8080") })
g.Go(func() error { return runMetrics(ctx) })
if err := g.Wait(); err != nil && err != context.Canceled {
log.Fatalf("Proxies stopped with an error: %v", err)
}
log.Println("Proxies shut down")
}
http_proxy.go:
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
size int
}
func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
return &loggingResponseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
n, err := lrw.ResponseWriter.Write(b)
lrw.size += n
return n, err
}
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hijacker, ok := lrw.ResponseWriter.(http.Hijacker); ok {
return hijacker.Hijack()
}
return nil, nil, fmt.Errorf("ResponseWriter does not support Hijack")
}
type HTTPProxy struct {
transport *http.Transport
}
func NewHTTPProxy() *HTTPProxy {
return &HTTPProxy{
transport: &http.Transport{
TLSClientConfig: &tls.Config{},
Proxy: http.ProxyFromEnvironment,
},
}
}
func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lrw := newLoggingResponseWriter(w)
if r.Method == http.MethodConnect {
p.handleTunnel(lrw, r)
} else {
p.handleHTTP(lrw, r)
}
duration := time.Since(start)
clientIP := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
clientIP = forwarded
}
log.Printf("[HTTP] %s %s from %s — status: %d, size: %d bytes, duration: %v",
r.Method, r.URL.String(), clientIP, lrw.statusCode, lrw.size, duration)
}
func (p *HTTPProxy) handleTunnel(w http.ResponseWriter, r *http.Request) {
target := r.Host
if !strings.Contains(target, ":") {
target += ":443" // default to 443 if no port is specified
}
destConn, err := net.Dial("tcp", target)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer destConn.Close()
w.WriteHeader(http.StatusOK)
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer clientConn.Close()
go io.Copy(destConn, clientConn)
io.Copy(clientConn, destConn)
}
func (p *HTTPProxy) handleHTTP(w http.ResponseWriter, r *http.Request) {
targetURL, err := url.Parse(r.URL.String())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if targetURL.Scheme == "" {
if r.URL.Port() == "443" {
targetURL.Scheme = "https"
} else {
targetURL.Scheme = "http"
}
}
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = targetURL.Scheme
req.URL.Host = targetURL.Host
req.URL.Path = targetURL.Path
req.URL.RawQuery = targetURL.RawQuery
req.Host = targetURL.Host
req.Header.Del("Proxy-Connection")
},
Transport: p.transport,
}
proxy.ServeHTTP(w, r)
}
func runHTTPProxy(ctx context.Context, addr string) error {
logger := log.New(os.Stdout, "[HTTP] ", log.LstdFlags)
rawListener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
listener := metricsListener{Listener: rawListener}
proxy := NewHTTPProxy()
server := &http.Server{
Handler: proxy,
ErrorLog: logger,
}
errCh := make(chan error, 1)
go func() {
errCh <- server.Serve(listener)
}()
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
return ctx.Err()
case err := <-errCh:
return err
}
}
socks5_proxy.go:
type contextKey string
const (
startTimeKey contextKey = "start_time"
clientAddrKey contextKey = "client_addr"
cmdKey contextKey = "command"
)
type loggingListener struct {
net.Listener
logger *log.Logger
}
func (l loggingListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
clientAddr := conn.RemoteAddr().String()
l.logger.Printf("Accepted connection from %s", clientAddr)
ctx := context.WithValue(context.Background(), startTimeKey, time.Now())
ctx = context.WithValue(ctx, clientAddrKey, clientAddr)
return &loggingConn{
Conn: conn,
ctx: ctx,
logger: l.logger,
}, nil
}
type loggingConn struct {
net.Conn
ctx context.Context
logger *log.Logger
}
func (c *loggingConn) Close() error {
err := c.Conn.Close()
start, ok := c.ctx.Value(startTimeKey).(time.Time)
if ok {
duration := time.Since(start)
clientAddr, _ := c.ctx.Value(clientAddrKey).(string)
c.logger.Printf("Connection closed from %s, duration: %v", clientAddr, duration)
}
return err
}
func runSocks5(ctx context.Context) error {
logger := log.New(os.Stdout, "[SOCKS5] ", log.LstdFlags)
rawListener, err := net.Listen("tcp", ":1080")
if err != nil {
return err
}
listener := metricsListener{Listener: rawListener}
conf := &socks5.Config{
Logger: logger,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
start := time.Now()
conn, err := net.Dial(network, addr)
duration := time.Since(start)
if err != nil {
logger.Printf("Dial to %s failed: %v (duration: %v)", addr, err, duration)
return nil, err
}
logger.Printf("Dial to %s succeeded (duration: %v)", addr, duration)
return conn, nil
},
}
server, err := socks5.New(conf)
if err != nil {
return err
}
errCh := make(chan error, 1)
go func() {
errCh <- server.Serve(listener)
}()
select {
case <-ctx.Done():
rawListener.Close()
<-errCh
logger.Printf("SOCKS5 stopped")
return ctx.Err()
case err := <-errCh:
return err
}
}
metrics.go:
var (
activeConnections = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "proxy_active_connections",
Help: "Current number of active TCP connections",
})
)
func init() {
prometheus.MustRegister(activeConnections)
}
type metricsListener struct {
net.Listener
}
func (ml metricsListener) Accept() (net.Conn, error) {
conn, err := ml.Listener.Accept()
if err != nil {
return nil, err
}
activeConnections.Inc()
return &metricsConn{Conn: conn}, nil
}
type metricsConn struct {
net.Conn
}
func (mc *metricsConn) Close() error {
err := mc.Conn.Close()
activeConnections.Dec()
return err
}
func runMetrics(ctx context.Context) error {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
server := &http.Server{
Addr: ":9090",
Handler: mux,
}
errCh := make(chan error, 1)
go func() {
errCh <- server.ListenAndServe()
}()
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
return ctx.Err()
case err := <-errCh:
return err
}
}
Top comments (1)
Worth knowing before you ship this to a VPS: reputation APIs mislabel whole cloud ranges. proxycheck and ip-api flag AWS and Hetzner blocks wholesale. That's why our scoring needs two independent specialized sources agreeing before it calls a datacenter IP a proxy, while one is enough for residential. ASN matches on M247 or Datacamp stand alone.