From bcb0943eaeda2e71cd22f7102e106ea2f672533d Mon Sep 17 00:00:00 2001 From: Mehrdad Amini Date: Sun, 22 Dec 2024 20:05:48 +0300 Subject: [PATCH] init project --- .env.example | 3 + .github/workflows/build.yml | 51 +++++ .gitignore | 26 +++ Dockerfile | 27 +++ README.md | 241 ++++++++++++++++++++++ docker-compose.yml | 20 ++ go.mod | 7 + go.sum | 4 + main.go | 390 ++++++++++++++++++++++++++++++++++++ main.go.single-user.bk | 352 ++++++++++++++++++++++++++++++++ proxies.conf.example | 25 +++ users.conf.example | 2 + 12 files changed, 1148 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 main.go.single-user.bk create mode 100644 proxies.conf.example create mode 100644 users.conf.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..80690a1 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +COMPOSE_PROJECT_NAME=go-proxy-rotator +DC_SOCKS_PROXY_PORT=60255 +ENABLE_EDGE_MODE=true \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e1b4d05 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,51 @@ +name: Build and Push Docker image to GHCR + +on: + push: + branches: + - main + +jobs: + build-and-push: + runs-on: + group: self-hosted + #labels: + # - Linux + permissions: + packages: write + contents: read + + steps: + # Step 1: Checkout the repository + - name: Checkout repository + uses: actions/checkout@v3 + + # Step 2: Log in to GitHub Container Registry (GHCR) + - name: Log in to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Step 3: Define dynamic image name and tag based on repo name + - name: Set dynamic image name and tag + id: vars + run: | + REPO_NAME=$(basename "${{ github.repository }}") # Extract the repository name + IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/$REPO_NAME" + GIT_SHA="${{ github.sha }}" + TAG="main-${GIT_SHA:0:7}" # Example: Use first 7 characters of commit SHA + echo "IMAGE_NAME=$IMAGE_NAME" >> $GITHUB_ENV + echo "IMAGE_TAG=$TAG" >> $GITHUB_ENV + + # Step 4: Build the Docker image with both tags + - name: Build Docker image + run: | + docker build -t $IMAGE_NAME:$IMAGE_TAG -t $IMAGE_NAME:latest . + + # Step 5: Push both Docker image tags to GHCR + - name: Push Docker image to GHCR + run: | + docker push $IMAGE_NAME:$IMAGE_TAG + docker push $IMAGE_NAME:latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2ae795 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Binary +/proxy-server + +# Config files with sensitive data +.env +#proxies.conf +#users.conf + +# Go specific +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# IDE +.idea/ +.vscode/ +*.swp + +# MacOS +.DS_Store + +# Temp files +*.log +*.tmp \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..12dba4f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1.22.5-alpine AS builder +# Install required system packages +RUN apk update && \ + apk upgrade && \ + apk add --no-cache ca-certificates && \ + update-ca-certificates + +WORKDIR /build + +# Copy go mod and source files +COPY go.mod go.sum ./ +COPY *.go ./ + +# Download dependencies +RUN go mod download + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o proxy-server . + +# Final stage +FROM scratch +WORKDIR /app +COPY --from=builder /build/proxy-server . +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +EXPOSE 1080 +CMD ["./proxy-server"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..af5725e --- /dev/null +++ b/README.md @@ -0,0 +1,241 @@ +# Go Proxy Rotator + +A high-performance SOCKS5 proxy server written in Go that rotates through multiple upstream proxies. Perfect for distributed scraping, API access, and general proxy needs. + +## Features + +- SOCKS5 proxy server with username/password authentication +- Support for multiple upstream proxy protocols: + - HTTP proxies + - HTTPS proxies (encrypted) + - SOCKS5 proxies + - SOCKS5H proxies (proxy performs DNS resolution) +- Round-robin proxy rotation +- Edge mode for fallback to direct connections +- Multi-user support via configuration file +- Docker and docker-compose support +- Configurable port +- Zero runtime dependencies +- Comments support in configuration files +- Automatic proxy failover +- IPv6 support + +## Quick Start with Docker Compose (Recommended) + +1. Clone the repository: +```bash +git clone https://github.com/ariadata/go-proxy-rotator.git +cd go-proxy-rotator +``` + +2. Set up configuration files: +```bash +# Copy environment example +cp .env.example .env + +# Create users file +echo "user1:password1" > users.conf +echo "user2:password2" >> users.conf + +# Create proxies file (add your proxies) +touch proxies.conf +``` + +3. Create `docker-compose.yml`: +```yaml +version: '3.8' + +services: + proxy-rotator: + image: 'ghcr.io/ariadata/go-proxy-rotator:latest' + ports: + - "${DC_SOCKS_PROXY_PORT}:1080" + volumes: + - ./proxies.conf:/app/proxies.conf:ro + - ./users.conf:/app/users.conf:ro + env_file: + - .env + restart: unless-stopped + healthcheck: + test: ["CMD", "nc", "-z", "localhost", "1080"] + interval: 30s + timeout: 10s + retries: 3 +``` + +4. Start the service: +```bash +docker-compose up -d +``` + +5. Test your connection: +```bash +curl --proxy socks5h://user1:password1@localhost:60255 https://api.ipify.org?format=json +``` + +## Installation with Go + +1. Clone and enter the repository: +```bash +git clone https://github.com/ariadata/go-proxy-rotator.git +cd go-proxy-rotator +``` + +2. Install dependencies: +```bash +go mod download +``` + +3. Set up configuration files: +```bash +cp .env.example .env +# Edit users.conf and proxies.conf +``` + +4. Build and run: +```bash +go build -o proxy-server +./proxy-server +``` + +## Configuration + +### Environment Variables (.env) + +```env +# Project name for docker-compose +COMPOSE_PROJECT_NAME=go-proxy-rotator + +# Port for the SOCKS5 server +DC_SOCKS_PROXY_PORT=60255 + +# Enable direct connections when proxies fail +ENABLE_EDGE_MODE=true +``` + +### User Configuration (users.conf) + +Format: +``` +username1:password1 +username2:password2 +# Comments are supported +``` + +### Proxy Configuration (proxies.conf) + +The proxy configuration file supports various proxy formats: + +``` +# HTTP proxies +http://proxy1.example.com:8080 +http://user:password@proxy2.example.com:8080 + +# HTTPS proxies (encrypted connection to proxy) +https://secure-proxy.example.com:8443 +https://user:password@secure-proxy2.example.com:8443 + +# SOCKS5 proxies (standard) +socks5://socks-proxy.example.com:1080 +socks5://user:password@socks-proxy2.example.com:1080 + +# SOCKS5H proxies (proxy performs DNS resolution) +socks5h://socks-proxy3.example.com:1080 +socks5h://user:password@socks-proxy4.example.com:1080 + +# IPv6 support +http://[2001:db8::1]:8080 +socks5://user:password@[2001:db8::2]:1080 + +# Real-world format examples +http://proxy-user:Abcd1234@103.1.2.3:8080 +https://proxy-user:Abcd1234@103.1.2.4:8443 +socks5://socks-user:Abcd1234@103.1.2.5:1080 +``` + +## Edge Mode + +When edge mode is enabled (`ENABLE_EDGE_MODE=true`), the server will: + +1. First attempt a direct connection +2. If direct connection fails, rotate through available proxies +3. If all proxies fail, return an error + +This is useful for: +- Accessing both internal and external resources +- Reducing latency for local/fast connections +- Automatic failover to direct connection + +## Usage Examples + +### With cURL +```bash +# Basic usage +curl --proxy socks5h://user:pass@localhost:60255 https://api.ipify.org?format=json + +# With specific DNS resolution +curl --proxy socks5h://user:pass@localhost:60255 https://example.com + +# With insecure mode (skip SSL verification) +curl --proxy socks5h://user:pass@localhost:60255 -k https://example.com +``` + +### With Python Requests +```python +import requests + +proxies = { + 'http': 'socks5h://user:pass@localhost:60255', + 'https': 'socks5h://user:pass@localhost:60255' +} + +response = requests.get('https://api.ipify.org?format=json', proxies=proxies) +print(response.json()) +``` + +### With Node.js +```javascript +const SocksProxyAgent = require('socks-proxy-agent'); + +const proxyOptions = { + hostname: 'localhost', + port: 60255, + userId: 'user', + password: 'pass', + protocol: 'socks5:' +}; + +const agent = new SocksProxyAgent(proxyOptions); + +fetch('https://api.ipify.org?format=json', { agent }) + .then(res => res.json()) + .then(data => console.log(data)); +``` + +## Building for Production + +For production builds, use: + +```bash +CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o proxy-server . +``` + +## Security Notes + +- Always use strong passwords in `users.conf` +- Consider using HTTPS/SOCKS5 proxies for sensitive traffic +- The server logs minimal information for privacy + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +MIT License + +## Acknowledgments + +Built using: +- [go-socks5](https://github.com/armon/go-socks5) - SOCKS5 server implementation +- Go's standard library for proxy and networking features \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c6c578b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +services: + go-proxy-rotator: + image: 'ghcr.io/ariadata/go-proxy-rotator:latest' + build: + context: . + dockerfile: Dockerfile + container_name: go-proxy-rotator + restart: unless-stopped + env_file: + - .env + ports: + - '${DC_SOCKS_PROXY_PORT:-1080}:1080' + volumes: + - ./proxies.conf:/app/proxies.conf + - ./users.conf:/app/users.conf + healthcheck: + test: [ "CMD", "nc", "-z", "localhost", "1080" ] + interval: 30s + timeout: 10s + retries: 3 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7379f3b --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module go-proxy-rotator + +go 1.22.5 + +require github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 + +require golang.org/x/net v0.33.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..24de917 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..77a8472 --- /dev/null +++ b/main.go @@ -0,0 +1,390 @@ +package main + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "github.com/armon/go-socks5" + "io" + "log" + "net" + "net/http" + "net/url" + "os" + "strings" + "sync" +) + +type ProxyConfig struct { + proxyURL *url.URL +} + +type ProxyManager struct { + proxies []*ProxyConfig + currentIdx int + mu sync.Mutex + enableEdge bool +} + +func NewProxyManager(enableEdge bool) *ProxyManager { + return &ProxyManager{ + proxies: make([]*ProxyConfig, 0), + enableEdge: enableEdge, + } +} + +func (pm *ProxyManager) LoadProxies(filename string) error { + file, err := os.Open(filename) + if err != nil { + if pm.enableEdge { + // If edge mode is enabled and no proxy file, that's okay + return nil + } + return err + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + proxyStr := line + proxyURL, err := url.Parse(proxyStr) + if err != nil { + return fmt.Errorf("invalid proxy URL: %s", err) + } + + proxy := &ProxyConfig{ + proxyURL: proxyURL, + } + + pm.proxies = append(pm.proxies, proxy) + } + + if err := scanner.Err(); err != nil { + return err + } + + // Only return error if no proxies AND edge mode is disabled + if len(pm.proxies) == 0 && !pm.enableEdge { + return fmt.Errorf("no proxies loaded from configuration and edge mode is disabled") + } + + return nil +} + +func (pm *ProxyManager) GetNextProxy() (*ProxyConfig, error) { + pm.mu.Lock() + defer pm.mu.Unlock() + + if len(pm.proxies) == 0 { + return nil, fmt.Errorf("no proxies available") + } + + proxy := pm.proxies[pm.currentIdx] + pm.currentIdx = (pm.currentIdx + 1) % len(pm.proxies) + + return proxy, nil +} + +type ProxyDialer struct { + manager *ProxyManager +} + +func (d *ProxyDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) { + if d.manager.enableEdge { + // If edge mode is enabled, try direct connection first + conn, err := net.Dial(network, addr) + if err == nil { + return conn, nil + } + // Only continue to proxies if we have any + if len(d.manager.proxies) == 0 { + return nil, fmt.Errorf("direct connection failed: %v", err) + } + } + + // If we get here and have no proxies, return error + if len(d.manager.proxies) == 0 { + return nil, fmt.Errorf("no proxies available and edge mode is disabled") + } + + // Try each proxy until one succeeds + var lastError error + for i := 0; i < len(d.manager.proxies); i++ { + proxy, err := d.manager.GetNextProxy() + if err != nil { + return nil, err + } + + conn, err := d.dialWithProxy(proxy, network, addr) + if err != nil { + lastError = err + continue + } + return conn, nil + } + + return nil, fmt.Errorf("all proxies failed, last error: %v", lastError) +} + +func (d *ProxyDialer) dialWithProxy(proxy *ProxyConfig, network, addr string) (net.Conn, error) { + switch proxy.proxyURL.Scheme { + case "socks5", "socks5h": + return d.dialSocks5(proxy, addr) + case "http", "https": + return d.dialHTTP(proxy, network, addr) + default: + return nil, fmt.Errorf("unsupported proxy scheme: %s", proxy.proxyURL.Scheme) + } +} + +func (d *ProxyDialer) dialSocks5(proxy *ProxyConfig, addr string) (net.Conn, error) { + conn, err := net.Dial("tcp", proxy.proxyURL.Host) + if err != nil { + return nil, err + } + + if proxy.proxyURL.User != nil { + err = performSocks5Handshake(conn, proxy.proxyURL) + if err != nil { + _ = conn.Close() + return nil, err + } + } + + if err := sendSocks5Connect(conn, addr); err != nil { + _ = conn.Close() + return nil, err + } + + return conn, nil +} + +func (d *ProxyDialer) dialHTTP(proxy *ProxyConfig, network, addr string) (net.Conn, error) { + conn, err := net.Dial("tcp", proxy.proxyURL.Host) + if err != nil { + return nil, err + } + + if proxy.proxyURL.Scheme == "https" { + tlsConn := tls.Client(conn, &tls.Config{ + InsecureSkipVerify: true, + }) + if err := tlsConn.Handshake(); err != nil { + _ = conn.Close() + return nil, err + } + conn = tlsConn + } + + req := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: make(http.Header), + } + + if proxy.proxyURL.User != nil { + basicAuth := base64.StdEncoding.EncodeToString([]byte(proxy.proxyURL.User.String())) + req.Header.Set("Proxy-Authorization", "Basic "+basicAuth) + } + + if err := req.Write(conn); err != nil { + _ = conn.Close() + return nil, err + } + + resp, err := http.ReadResponse(bufio.NewReader(conn), req) + if err != nil { + _ = conn.Close() + return nil, err + } + if resp.StatusCode != 200 { + _ = conn.Close() + return nil, fmt.Errorf("proxy error: %s", resp.Status) + } + + return conn, nil +} + +func loadUserCredentials(filename string) (socks5.StaticCredentials, error) { + credentials := make(socks5.StaticCredentials) + + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.Split(line, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid credentials format in users.conf: %s", line) + } + credentials[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + if len(credentials) == 0 { + return nil, fmt.Errorf("no valid credentials found in users.conf") + } + + return credentials, nil +} + +func main() { + // Load user credentials from file + credentials, err := loadUserCredentials("users.conf") + if err != nil { + log.Fatal(err) + } + + // Check if edge mode is enabled + enableEdge := os.Getenv("ENABLE_EDGE_MODE") == "true" + + // Initialize proxy manager + proxyManager := NewProxyManager(enableEdge) + if err := proxyManager.LoadProxies("proxies.conf"); err != nil { + log.Fatal(err) + } + + dialer := &ProxyDialer{manager: proxyManager} + + // Create SOCKS5 server configuration with authentication + conf := &socks5.Config{ + Dial: dialer.Dial, + Credentials: credentials, + AuthMethods: []socks5.Authenticator{socks5.UserPassAuthenticator{ + Credentials: credentials, + }}, + } + + server, err := socks5.New(conf) + if err != nil { + log.Fatal(err) + } + + port := os.Getenv("DC_SOCKS_PROXY_PORT") + if port == "" { + port = "1080" + } + + log.Printf("SOCKS5 server running on :%s (Edge Mode: %v, Users: %d, Proxies: %d)\n", + port, enableEdge, len(credentials), len(proxyManager.proxies)) + if err := server.ListenAndServe("tcp", ":"+port); err != nil { + log.Fatal(err) + } +} + +// Helper functions for SOCKS5 +func performSocks5Handshake(conn net.Conn, proxyURL *url.URL) error { + _, err := conn.Write([]byte{0x05, 0x01, 0x02}) + if err != nil { + return err + } + + resp := make([]byte, 2) + if _, err := io.ReadFull(conn, resp); err != nil { + return err + } + + if resp[0] != 0x05 || resp[1] != 0x02 { + return fmt.Errorf("unsupported auth method") + } + + username := proxyURL.User.Username() + password, _ := proxyURL.User.Password() + + auth := []byte{0x01} + auth = append(auth, byte(len(username))) + auth = append(auth, []byte(username)...) + auth = append(auth, byte(len(password))) + auth = append(auth, []byte(password)...) + + if _, err := conn.Write(auth); err != nil { + return err + } + + authResp := make([]byte, 2) + if _, err := io.ReadFull(conn, authResp); err != nil { + return err + } + + if authResp[1] != 0x00 { + return fmt.Errorf("authentication failed") + } + + return nil +} + +func sendSocks5Connect(conn net.Conn, addr string) error { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return err + } + + req := []byte{0x05, 0x01, 0x00} + ip := net.ParseIP(host) + + if ip == nil { + req = append(req, 0x03, byte(len(host))) + req = append(req, []byte(host)...) + } else if ip4 := ip.To4(); ip4 != nil { + req = append(req, 0x01) + req = append(req, ip4...) + } else { + req = append(req, 0x04) + req = append(req, ip.To16()...) + } + + portNum := uint16(0) + _, _ = fmt.Sscanf(port, "%d", &portNum) + req = append(req, byte(portNum>>8), byte(portNum&0xff)) + + if _, err := conn.Write(req); err != nil { + return err + } + + resp := make([]byte, 4) + if _, err := io.ReadFull(conn, resp); err != nil { + return err + } + + if resp[1] != 0x00 { + return fmt.Errorf("connect failed: %d", resp[1]) + } + + switch resp[3] { + case 0x01: + _, err = io.ReadFull(conn, make([]byte, 4+2)) + case 0x03: + size := make([]byte, 1) + _, err = io.ReadFull(conn, size) + if err == nil { + _, err = io.ReadFull(conn, make([]byte, int(size[0])+2)) + } + case 0x04: + _, err = io.ReadFull(conn, make([]byte, 16+2)) + } + + return err +} diff --git a/main.go.single-user.bk b/main.go.single-user.bk new file mode 100644 index 0000000..4b70855 --- /dev/null +++ b/main.go.single-user.bk @@ -0,0 +1,352 @@ +package main + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "github.com/armon/go-socks5" + "io" + "log" + "net" + "net/http" + "net/url" + "os" + "sync" +) + +type ProxyConfig struct { + proxyURL *url.URL +} + +type ProxyManager struct { + proxies []*ProxyConfig + currentIdx int + mu sync.Mutex + enableEdge bool +} + +func NewProxyManager(enableEdge bool) *ProxyManager { + return &ProxyManager{ + proxies: make([]*ProxyConfig, 0), + enableEdge: enableEdge, + } +} + +func (pm *ProxyManager) LoadProxies(filename string) error { + file, err := os.Open(filename) + if err != nil { + if pm.enableEdge { + // If edge mode is enabled and no proxy file, that's okay + return nil + } + return err + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + proxyStr := scanner.Text() + proxyURL, err := url.Parse(proxyStr) + if err != nil { + return fmt.Errorf("invalid proxy URL: %s", err) + } + + proxy := &ProxyConfig{ + proxyURL: proxyURL, + } + + pm.proxies = append(pm.proxies, proxy) + } + + if err := scanner.Err(); err != nil { + return err + } + + // Only return error if no proxies AND edge mode is disabled + if len(pm.proxies) == 0 && !pm.enableEdge { + return fmt.Errorf("no proxies loaded from configuration and edge mode is disabled") + } + + return nil +} + +func (pm *ProxyManager) GetNextProxy() (*ProxyConfig, error) { + pm.mu.Lock() + defer pm.mu.Unlock() + + if len(pm.proxies) == 0 { + return nil, fmt.Errorf("no proxies available") + } + + proxy := pm.proxies[pm.currentIdx] + pm.currentIdx = (pm.currentIdx + 1) % len(pm.proxies) + + return proxy, nil +} + +type ProxyDialer struct { + manager *ProxyManager +} + +func (d *ProxyDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) { + if d.manager.enableEdge { + // If edge mode is enabled, try direct connection first + conn, err := net.Dial(network, addr) + if err == nil { + return conn, nil + } + // Only continue to proxies if we have any + if len(d.manager.proxies) == 0 { + return nil, fmt.Errorf("direct connection failed: %v", err) + } + } + + // If we get here and have no proxies, return error + if len(d.manager.proxies) == 0 { + return nil, fmt.Errorf("no proxies available and edge mode is disabled") + } + + // Try each proxy until one succeeds + var lastError error + for i := 0; i < len(d.manager.proxies); i++ { + proxy, err := d.manager.GetNextProxy() + if err != nil { + return nil, err + } + + conn, err := d.dialWithProxy(proxy, network, addr) + if err != nil { + lastError = err + continue + } + return conn, nil + } + + return nil, fmt.Errorf("all proxies failed, last error: %v", lastError) +} + +func (d *ProxyDialer) dialWithProxy(proxy *ProxyConfig, network, addr string) (net.Conn, error) { + switch proxy.proxyURL.Scheme { + case "socks5", "socks5h": + return d.dialSocks5(proxy, addr) + case "http", "https": + return d.dialHTTP(proxy, network, addr) + default: + return nil, fmt.Errorf("unsupported proxy scheme: %s", proxy.proxyURL.Scheme) + } +} + +func (d *ProxyDialer) dialSocks5(proxy *ProxyConfig, addr string) (net.Conn, error) { + conn, err := net.Dial("tcp", proxy.proxyURL.Host) + if err != nil { + return nil, err + } + + if proxy.proxyURL.User != nil { + err = performSocks5Handshake(conn, proxy.proxyURL) + if err != nil { + _ = conn.Close() + return nil, err + } + } + + if err := sendSocks5Connect(conn, addr); err != nil { + _ = conn.Close() + return nil, err + } + + return conn, nil +} + +func (d *ProxyDialer) dialHTTP(proxy *ProxyConfig, network, addr string) (net.Conn, error) { + conn, err := net.Dial("tcp", proxy.proxyURL.Host) + if err != nil { + return nil, err + } + + if proxy.proxyURL.Scheme == "https" { + tlsConn := tls.Client(conn, &tls.Config{ + InsecureSkipVerify: true, + }) + if err := tlsConn.Handshake(); err != nil { + _ = conn.Close() + return nil, err + } + conn = tlsConn + } + + req := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: make(http.Header), + } + + if proxy.proxyURL.User != nil { + basicAuth := base64.StdEncoding.EncodeToString([]byte(proxy.proxyURL.User.String())) + req.Header.Set("Proxy-Authorization", "Basic "+basicAuth) + } + + if err := req.Write(conn); err != nil { + _ = conn.Close() + return nil, err + } + + resp, err := http.ReadResponse(bufio.NewReader(conn), req) + if err != nil { + _ = conn.Close() + return nil, err + } + if resp.StatusCode != 200 { + _ = conn.Close() + return nil, fmt.Errorf("proxy error: %s", resp.Status) + } + + return conn, nil +} + +func main() { + // Get authentication credentials from environment + authUser := os.Getenv("PROXY_AUTH_USER") + authPass := os.Getenv("PROXY_AUTH_PASS") + if authUser == "" || authPass == "" { + log.Fatal("PROXY_AUTH_USER and PROXY_AUTH_PASS must be set") + } + + // Create credentials + credentials := make(socks5.StaticCredentials) + credentials[authUser] = authPass + + // Check if edge mode is enabled + enableEdge := os.Getenv("ENABLE_EDGE_MODE") == "true" + + // Initialize proxy manager + proxyManager := NewProxyManager(enableEdge) + if err := proxyManager.LoadProxies("proxies.conf"); err != nil { + log.Fatal(err) + } + + dialer := &ProxyDialer{manager: proxyManager} + + // Create SOCKS5 server configuration with authentication + conf := &socks5.Config{ + Dial: dialer.Dial, + Credentials: credentials, + AuthMethods: []socks5.Authenticator{socks5.UserPassAuthenticator{ + Credentials: credentials, + }}, + } + + server, err := socks5.New(conf) + if err != nil { + log.Fatal(err) + } + + port := os.Getenv("DC_SOCKS_PROXY_PORT") + if port == "" { + port = "1080" + } + + log.Printf("SOCKS5 server running on :%s (Edge Mode: %v)\n", port, enableEdge) + if err := server.ListenAndServe("tcp", ":"+port); err != nil { + log.Fatal(err) + } +} + +// Helper functions for SOCKS5 +func performSocks5Handshake(conn net.Conn, proxyURL *url.URL) error { + _, err := conn.Write([]byte{0x05, 0x01, 0x02}) + if err != nil { + return err + } + + resp := make([]byte, 2) + if _, err := io.ReadFull(conn, resp); err != nil { + return err + } + + if resp[0] != 0x05 || resp[1] != 0x02 { + return fmt.Errorf("unsupported auth method") + } + + username := proxyURL.User.Username() + password, _ := proxyURL.User.Password() + + auth := []byte{0x01} + auth = append(auth, byte(len(username))) + auth = append(auth, []byte(username)...) + auth = append(auth, byte(len(password))) + auth = append(auth, []byte(password)...) + + if _, err := conn.Write(auth); err != nil { + return err + } + + authResp := make([]byte, 2) + if _, err := io.ReadFull(conn, authResp); err != nil { + return err + } + + if authResp[1] != 0x00 { + return fmt.Errorf("authentication failed") + } + + return nil +} + +func sendSocks5Connect(conn net.Conn, addr string) error { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return err + } + + req := []byte{0x05, 0x01, 0x00} + ip := net.ParseIP(host) + + if ip == nil { + req = append(req, 0x03, byte(len(host))) + req = append(req, []byte(host)...) + } else if ip4 := ip.To4(); ip4 != nil { + req = append(req, 0x01) + req = append(req, ip4...) + } else { + req = append(req, 0x04) + req = append(req, ip.To16()...) + } + + portNum := uint16(0) + _, _ = fmt.Sscanf(port, "%d", &portNum) + req = append(req, byte(portNum>>8), byte(portNum&0xff)) + + if _, err := conn.Write(req); err != nil { + return err + } + + resp := make([]byte, 4) + if _, err := io.ReadFull(conn, resp); err != nil { + return err + } + + if resp[1] != 0x00 { + return fmt.Errorf("connect failed: %d", resp[1]) + } + + switch resp[3] { + case 0x01: + _, err = io.ReadFull(conn, make([]byte, 4+2)) + case 0x03: + size := make([]byte, 1) + _, err = io.ReadFull(conn, size) + if err == nil { + _, err = io.ReadFull(conn, make([]byte, int(size[0])+2)) + } + case 0x04: + _, err = io.ReadFull(conn, make([]byte, 16+2)) + } + + return err +} diff --git a/proxies.conf.example b/proxies.conf.example new file mode 100644 index 0000000..f58060d --- /dev/null +++ b/proxies.conf.example @@ -0,0 +1,25 @@ +# HTTP proxies +http://proxy1.example.com:8080 +http://user:password@proxy2.example.com:8080 + +# HTTPS proxies (encrypted connection to proxy) +https://secure-proxy.example.com:8443 +https://user:password@secure-proxy2.example.com:8443 + +# SOCKS5 proxies (standard) +socks5://socks-proxy.example.com:1080 +socks5://user:password@socks-proxy2.example.com:1080 + +# SOCKS5H proxies (proxy performs DNS resolution) +socks5h://socks-proxy3.example.com:1080 +socks5h://user:password@socks-proxy4.example.com:1080 + +# IPv6 examples +http://[2001:db8::1]:8080 +socks5://user:password@[2001:db8::2]:1080 + +# Real-world format examples +http://proxy-user:Abcd1234@103.1.2.3:8080 +https://proxy-user:Abcd1234@103.1.2.4:8443 +socks5://socks-user:Abcd1234@103.1.2.5:1080 +socks5h://socks-user:Abcd1234@103.1.2.6:1080 \ No newline at end of file diff --git a/users.conf.example b/users.conf.example new file mode 100644 index 0000000..78e5625 --- /dev/null +++ b/users.conf.example @@ -0,0 +1,2 @@ +user:pass +user2:pass2 \ No newline at end of file