Create a Proxy Testing CLI Tool in Go

Create a Proxy Testing CLI Tool in Go

Go is ideal for proxy testing tools. Its goroutines make concurrent testing trivial, the compiled binary runs on any platform without dependencies, and its standard library includes everything needed for HTTP and SOCKS proxy support.

This tutorial builds a command-line proxy testing tool that checks proxy connectivity, measures latency, detects anonymity level, and outputs results in multiple formats.

Why Go for Proxy Tools

Python proxy checkers work fine for small lists. For testing thousands of proxies quickly, Go’s concurrency model gives you a 5-10x speed advantage. The resulting binary is a single file — no Python interpreter, no virtual environments, no dependency management. Deploy it on any machine by copying one file.

Project Setup

mkdir proxy-tester && cd proxy-tester
go mod init github.com/yourname/proxy-tester
go get golang.org/x/net/proxy

Data Types

// types.go
package main

import "time"

type ProxyProtocol string

const (
    HTTP   ProxyProtocol = "http"
    HTTPS  ProxyProtocol = "https"
    SOCKS4 ProxyProtocol = "socks4"
    SOCKS5 ProxyProtocol = "socks5"
)

type AnonymityLevel string

const (
    Transparent AnonymityLevel = "transparent"
    Anonymous   AnonymityLevel = "anonymous"
    Elite       AnonymityLevel = "elite"
    Unknown     AnonymityLevel = "unknown"
)

type ProxyResult struct {
    Proxy       string         `json:"proxy"`
    Protocol    ProxyProtocol  `json:"protocol"`
    Alive       bool           `json:"alive"`
    LatencyMs   int64          `json:"latency_ms"`
    Anonymity   AnonymityLevel `json:"anonymity"`
    ExternalIP  string         `json:"external_ip"`
    Country     string         `json:"country"`
    Error       string         `json:"error,omitempty"`
    SupportsSSL bool           `json:"supports_ssl"`
    CheckedAt   time.Time      `json:"checked_at"`
}

Proxy Tester Core

// tester.go
package main

import (
    "context"
    "crypto/tls"
    "encoding/json"
    "fmt"
    "net"
    "net/http"
    "net/url"
    "strings"
    "sync"
    "time"

    "golang.org/x/net/proxy"
)

type ProxyTester struct {
    TestURL     string
    Timeout     time.Duration
    Concurrency int
    MyIP        string
}

func NewProxyTester(timeout time.Duration, concurrency int) *ProxyTester {
    return &ProxyTester{
        TestURL:     "https://httpbin.org/ip",
        Timeout:     timeout,
        Concurrency: concurrency,
    }
}

func (t *ProxyTester) DetectMyIP() error {
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Get("https://httpbin.org/ip")
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    var data map[string]string
    json.NewDecoder(resp.Body).Decode(&data)
    t.MyIP = data["origin"]
    return nil
}

func (t *ProxyTester) TestProxy(proxyAddr string) ProxyResult {
    result := ProxyResult{
        Proxy:     proxyAddr,
        CheckedAt: time.Now(),
    }

    // Detect protocol
    result.Protocol = t.detectProtocol(proxyAddr)

    // Create HTTP client with proxy
    client, err := t.createClient(proxyAddr, result.Protocol)
    if err != nil {
        result.Error = err.Error()
        return result
    }

    // Test connectivity and measure latency
    start := time.Now()
    resp, err := client.Get(t.TestURL)
    if err != nil {
        result.Error = err.Error()
        return result
    }
    defer resp.Body.Close()

    result.LatencyMs = time.Since(start).Milliseconds()
    result.Alive = true

    // Parse response for anonymity check
    var data map[string]string
    json.NewDecoder(resp.Body).Decode(&data)
    result.ExternalIP = data["origin"]

    // Determine anonymity
    result.Anonymity = t.checkAnonymity(result.ExternalIP, resp.Header)

    // Test SSL support
    result.SupportsSSL = t.testSSL(proxyAddr, result.Protocol)

    // Get country (simplified)
    result.Country = t.lookupCountry(result.ExternalIP)

    return result
}

func (t *ProxyTester) TestBatch(proxies []string) []ProxyResult {
    results := make([]ProxyResult, len(proxies))
    sem := make(chan struct{}, t.Concurrency)
    var wg sync.WaitGroup

    for i, p := range proxies {
        wg.Add(1)
        sem <- struct{}{}

        go func(idx int, proxyAddr string) {
            defer wg.Done()
            defer func() { <-sem }()

            results[idx] = t.TestProxy(proxyAddr)

            status := "DEAD"
            if results[idx].Alive {
                status = "ALIVE"
            }
            fmt.Printf("  [%s] %s — %dms %s\n",
                status, proxyAddr,
                results[idx].LatencyMs,
                results[idx].Country,
            )
        }(i, p)
    }

    wg.Wait()
    return results
}

func (t *ProxyTester) detectProtocol(addr string) ProxyProtocol {
    lower := strings.ToLower(addr)
    switch {
    case strings.HasPrefix(lower, "socks5://"):
        return SOCKS5
    case strings.HasPrefix(lower, "socks4://"):
        return SOCKS4
    case strings.HasPrefix(lower, "https://"):
        return HTTPS
    default:
        return HTTP
    }
}

func (t *ProxyTester) createClient(proxyAddr string, protocol ProxyProtocol) (*http.Client, error) {
    switch protocol {
    case SOCKS5, SOCKS4:
        // Strip protocol prefix for SOCKS dialer
        addr := strings.TrimPrefix(proxyAddr, string(protocol)+"://")
        dialer, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
        if err != nil {
            return nil, err
        }

        transport := &http.Transport{
            DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
                return dialer.Dial(network, address)
            },
            TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
        }

        return &http.Client{
            Transport: transport,
            Timeout:   t.Timeout,
        }, nil

    default:
        proxyURL, err := url.Parse(proxyAddr)
        if err != nil {
            return nil, err
        }

        transport := &http.Transport{
            Proxy: http.ProxyURL(proxyURL),
        }

        return &http.Client{
            Transport: transport,
            Timeout:   t.Timeout,
        }, nil
    }
}

func (t *ProxyTester) checkAnonymity(visibleIP string, headers http.Header) AnonymityLevel {
    if t.MyIP != "" && strings.Contains(visibleIP, t.MyIP) {
        return Transparent
    }

    via := headers.Get("Via")
    xff := headers.Get("X-Forwarded-For")
    if via != "" || xff != "" {
        return Anonymous
    }

    return Elite
}

func (t *ProxyTester) testSSL(proxyAddr string, protocol ProxyProtocol) bool {
    client, err := t.createClient(proxyAddr, protocol)
    if err != nil {
        return false
    }
    client.Timeout = 5 * time.Second

    resp, err := client.Get("https://httpbin.org/ip")
    if err != nil {
        return false
    }
    resp.Body.Close()
    return true
}

func (t *ProxyTester) lookupCountry(ip string) string {
    cleanIP := strings.Split(ip, ",")[0]
    cleanIP = strings.TrimSpace(cleanIP)

    client := &http.Client{Timeout: 5 * time.Second}
    resp, err := client.Get(fmt.Sprintf("https://ipinfo.io/%s/json", cleanIP))
    if err != nil {
        return "unknown"
    }
    defer resp.Body.Close()

    var data map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&data)

    if country, ok := data["country"].(string); ok {
        return country
    }
    return "unknown"
}

CLI Interface

// main.go
package main

import (
    "bufio"
    "encoding/csv"
    "encoding/json"
    "flag"
    "fmt"
    "os"
    "strings"
    "time"
)

func main() {
    inputFile := flag.String("input", "", "File with proxy list (one per line)")
    outputFile := flag.String("output", "", "Output file (auto-detects format from extension)")
    format := flag.String("format", "table", "Output format: table, json, csv")
    concurrency := flag.Int("concurrency", 50, "Number of concurrent checks")
    timeout := flag.Int("timeout", 10, "Timeout in seconds per proxy")
    aliveOnly := flag.Bool("alive-only", false, "Show only alive proxies")
    proxy := flag.String("proxy", "", "Test a single proxy")

    flag.Parse()

    tester := NewProxyTester(
        time.Duration(*timeout)*time.Second,
        *concurrency,
    )

    fmt.Println("Detecting your IP...")
    if err := tester.DetectMyIP(); err != nil {
        fmt.Fprintf(os.Stderr, "Warning: could not detect IP: %v\n", err)
    } else {
        fmt.Printf("Your IP: %s\n", tester.MyIP)
    }

    var proxies []string

    if *proxy != "" {
        proxies = []string{*proxy}
    } else if *inputFile != "" {
        var err error
        proxies, err = readProxyFile(*inputFile)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
            os.Exit(1)
        }
    } else {
        fmt.Fprintln(os.Stderr, "Usage: proxy-tester -input proxies.txt")
        fmt.Fprintln(os.Stderr, "       proxy-tester -proxy http://host:port")
        flag.PrintDefaults()
        os.Exit(1)
    }

    fmt.Printf("Testing %d proxies with concurrency %d...\n\n", len(proxies), *concurrency)

    start := time.Now()
    results := tester.TestBatch(proxies)
    elapsed := time.Since(start)

    if *aliveOnly {
        var filtered []ProxyResult
        for _, r := range results {
            if r.Alive {
                filtered = append(filtered, r)
            }
        }
        results = filtered
    }

    // Summary
    alive := 0
    var totalLatency int64
    for _, r := range results {
        if r.Alive {
            alive++
            totalLatency += r.LatencyMs
        }
    }
    fmt.Printf("\n--- Results ---\n")
    fmt.Printf("Total: %d | Alive: %d | Dead: %d\n", len(proxies), alive, len(proxies)-alive)
    if alive > 0 {
        fmt.Printf("Avg Latency: %dms\n", totalLatency/int64(alive))
    }
    fmt.Printf("Completed in %.1fs\n\n", elapsed.Seconds())

    // Output
    switch *format {
    case "json":
        outputJSON(results, *outputFile)
    case "csv":
        outputCSV(results, *outputFile)
    default:
        outputTable(results)
    }
}

func readProxyFile(path string) ([]string, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    var proxies []string
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := strings.TrimSpace(scanner.Text())
        if line != "" && !strings.HasPrefix(line, "#") {
            proxies = append(proxies, line)
        }
    }
    return proxies, scanner.Err()
}

func outputTable(results []ProxyResult) {
    fmt.Printf("%-40s %-8s %-8s %-12s %-8s %-6s\n",
        "PROXY", "STATUS", "LATENCY", "ANONYMITY", "SSL", "COUNTRY")
    fmt.Println(strings.Repeat("-", 90))

    for _, r := range results {
        status := "DEAD"
        if r.Alive {
            status = "ALIVE"
        }
        ssl := "no"
        if r.SupportsSSL {
            ssl = "yes"
        }
        fmt.Printf("%-40s %-8s %-8d %-12s %-8s %-6s\n",
            r.Proxy, status, r.LatencyMs,
            r.Anonymity, ssl, r.Country,
        )
    }
}

func outputJSON(results []ProxyResult, filename string) {
    data, _ := json.MarshalIndent(results, "", "  ")
    if filename != "" {
        os.WriteFile(filename, data, 0644)
        fmt.Printf("Results written to %s\n", filename)
    } else {
        fmt.Println(string(data))
    }
}

func outputCSV(results []ProxyResult, filename string) {
    var writer *csv.Writer
    if filename != "" {
        file, _ := os.Create(filename)
        defer file.Close()
        writer = csv.NewWriter(file)
    } else {
        writer = csv.NewWriter(os.Stdout)
    }
    defer writer.Flush()

    writer.Write([]string{
        "proxy", "protocol", "alive", "latency_ms",
        "anonymity", "external_ip", "country", "ssl", "error",
    })

    for _, r := range results {
        writer.Write([]string{
            r.Proxy,
            string(r.Protocol),
            fmt.Sprintf("%t", r.Alive),
            fmt.Sprintf("%d", r.LatencyMs),
            string(r.Anonymity),
            r.ExternalIP,
            r.Country,
            fmt.Sprintf("%t", r.SupportsSSL),
            r.Error,
        })
    }

    if filename != "" {
        fmt.Printf("Results written to %s\n", filename)
    }
}

Building and Distribution

Build for multiple platforms:

# Build for current platform
go build -o proxy-tester .

# Cross-compile
GOOS=linux GOARCH=amd64 go build -o proxy-tester-linux-amd64 .
GOOS=darwin GOARCH=arm64 go build -o proxy-tester-darwin-arm64 .
GOOS=windows GOARCH=amd64 go build -o proxy-tester-windows-amd64.exe .

Usage Examples

# Test a single proxy
./proxy-tester -proxy http://proxy.example.com:8080

# Test from file
./proxy-tester -input proxies.txt -concurrency 100

# Export alive proxies to JSON
./proxy-tester -input proxies.txt -alive-only -format json -output alive.json

# Export to CSV
./proxy-tester -input proxies.txt -format csv -output results.csv

Internal Links

FAQ

How fast is the Go proxy tester compared to Python?

With 100 concurrent goroutines, Go tests 1,000 proxies in 15-20 seconds. An equivalent Python asyncio implementation takes 40-60 seconds. The difference comes from Go’s lightweight goroutines and compiled execution.

Can I test proxies that require authentication?

Yes. Include credentials in the proxy URL: http://user:pass@host:port. The Go HTTP client handles Basic authentication automatically. For SOCKS5 with auth, the golang.org/x/net/proxy package supports username/password authentication.

How do I distribute the binary to my team?

Go produces a single statically-linked binary. No runtime dependencies are required. Upload the binary to a shared drive, GitHub releases, or your package manager. Build for each target platform (Linux, macOS, Windows) using Go’s cross-compilation flags.

What proxy list format does the tool accept?

One proxy URL per line. Lines starting with # are treated as comments. The tool auto-detects the protocol from the URL scheme (http://, socks5://, etc). If no scheme is specified, HTTP is assumed.

How do I add proxy rotation to the tester itself?

The tester checks proxies against a test endpoint, not through another proxy. If you need to test through a chain (proxy through proxy), modify the createClient function to accept a secondary proxy for the outbound connection. This is useful for testing proxy chains.


Related Reading

Scroll to Top