From d5ef7bd84ea677616dd8b916764a5c765aa9bdcc Mon Sep 17 00:00:00 2001 From: Mehrdad Amini Date: Mon, 23 Dec 2024 23:09:52 +0300 Subject: [PATCH] struct updated --- .dockerignore | 7 + Dockerfile | 25 +- README.md | 212 ++------- cmd/main.go | 53 +++ internal/config/config.go | 61 +++ .../proxy_dialer/proxy_dialer.go | 186 ++------ internal/proxy_manager/proxy_manager.go | 112 +++++ main.go | 404 ------------------ makefile | 62 +++ proxies.conf.example => proxies.conf | 48 +-- users.conf | 2 + users.conf.example | 2 - 12 files changed, 407 insertions(+), 767 deletions(-) create mode 100644 .dockerignore create mode 100644 cmd/main.go create mode 100644 internal/config/config.go rename main.go.single-user.bk => internal/proxy_dialer/proxy_dialer.go (51%) create mode 100644 internal/proxy_manager/proxy_manager.go delete mode 100644 main.go create mode 100644 makefile rename proxies.conf.example => proxies.conf (97%) create mode 100644 users.conf delete mode 100644 users.conf.example diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e8c347b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +.env* +README.md +Dockerfile +docker-compose.yml +build/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 12dba4f..eeb4c6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,24 @@ 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 +# Install required packages and set up workspace in a single layer +RUN 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 +# Copy all necessary files in a single layer +COPY . . # Build the application -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o proxy-server . +RUN go mod download && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o proxy-server ./cmd/main.go -# Final stage +# Final stage - minimal image FROM scratch WORKDIR /app -COPY --from=builder /build/proxy-server . + +# Copy only the necessary files from builder COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /build/proxy-server . +COPY --from=builder /build/proxies.conf ./proxies.conf +COPY --from=builder /build/users.conf ./users.conf EXPOSE 1080 CMD ["./proxy-server"] \ No newline at end of file diff --git a/README.md b/README.md index af5725e..725be23 100644 --- a/README.md +++ b/README.md @@ -5,237 +5,99 @@ A high-performance SOCKS5 proxy server written in Go that rotates through multip ## 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) +- Multiple proxy protocol support (HTTP, HTTPS, SOCKS5, SOCKS5H) - Round-robin proxy rotation - Edge mode for fallback to direct connections -- Multi-user support via configuration file -- Docker and docker-compose support -- Configurable port +- Multi-user support +- Docker support - Zero runtime dependencies -- Comments support in configuration files -- Automatic proxy failover - IPv6 support -## Quick Start with Docker Compose (Recommended) +## Quick Start 1. Clone the repository: ```bash -git clone https://github.com/ariadata/go-proxy-rotator.git +git clone https://github.com/yourusername/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 +cp users.conf.example users.conf +cp proxies.conf.example 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 -``` +3. Edit the configuration files: + - `users.conf`: Add your username:password pairs + - `proxies.conf`: Add your proxy servers + - `.env`: Adjust settings if needed -4. Start the service: +4. Run with Docker: ```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 +docker compose up -d ``` ## 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/HTTPS 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 +https://user:pass@proxy2.example.com:8443 -# 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 +# SOCKS5 proxies +socks5://proxy3.example.com:1080 +socks5h://user:pass@proxy4.example.com:1080 ``` -## Edge Mode - -When edge mode is enabled (`ENABLE_EDGE_MODE=true`), the server will: +## Testing -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 +Test your connection: ```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 +curl --proxy socks5h://username1:password1@localhost:60255 https://api.ipify.org?format=json ``` -### With Python Requests -```python -import requests - -proxies = { - 'http': 'socks5h://user:pass@localhost:60255', - 'https': 'socks5h://user:pass@localhost:60255' -} +## Building from Source -response = requests.get('https://api.ipify.org?format=json', proxies=proxies) -print(response.json()) +```bash +make build ``` -### 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); +## Docker Commands -fetch('https://api.ipify.org?format=json', { agent }) - .then(res => res.json()) - .then(data => console.log(data)); +Build image: +```bash +docker build -t go-proxy-rotator . ``` -## Building for Production - -For production builds, use: - +Run container: ```bash -CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o proxy-server . +docker run -d \ + -p 60255:1080 \ + -v $(pwd)/proxies.conf:/app/proxies.conf:ro \ + -v $(pwd)/users.conf:/app/users.conf:ro \ + -e ENABLE_EDGE_MODE=true \ + go-proxy-rotator ``` -## 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 +## Contributing -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 +Contributions are welcome! Please feel free to submit a Pull Request. \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..eb25b86 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "github.com/armon/go-socks5" + "go-proxy-rotator/internal/config" + "go-proxy-rotator/internal/proxy_dialer" + "go-proxy-rotator/internal/proxy_manager" + "log" +) + +func main() { + // Load configuration + cfg := config.NewConfig() + + // Load user credentials + credentials, err := config.LoadUserCredentials(cfg.UsersFile) + if err != nil { + log.Fatal(err) + } + + // Initialize proxy manager + proxyManager := proxy_manager.NewManager(cfg.EnableEdgeMode) + if err := proxyManager.LoadProxies(cfg.ProxiesFile); err != nil { + log.Fatal(err) + } + + // Initialize proxy dialer + dialer := proxy_dialer.NewProxyDialer(proxyManager) + + // Create SOCKS5 server configuration with authentication + serverConfig := &socks5.Config{ + Dial: dialer.Dial, + Credentials: credentials, + AuthMethods: []socks5.Authenticator{socks5.UserPassAuthenticator{ + Credentials: credentials, + }}, + } + + server, err := socks5.New(serverConfig) + if err != nil { + log.Fatal(err) + } + + log.Printf("SOCKS5 server running on %s (Edge Mode: %v, Users: %d)\n", + cfg.ListenAddr, + cfg.EnableEdgeMode, + len(credentials)) + + // Start server + if err := server.ListenAndServe("tcp", cfg.ListenAddr); err != nil { + log.Fatal(err) + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..78bc725 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,61 @@ +package config + +import ( + "bufio" + "fmt" + "github.com/armon/go-socks5" + "os" + "strings" +) + +type Config struct { + EnableEdgeMode bool + ProxiesFile string + UsersFile string + ListenAddr string +} + +func NewConfig() *Config { + return &Config{ + EnableEdgeMode: os.Getenv("ENABLE_EDGE_MODE") == "true", + ProxiesFile: "proxies.conf", + UsersFile: "users.conf", + ListenAddr: ":1080", + } +} + +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 +} diff --git a/main.go.single-user.bk b/internal/proxy_dialer/proxy_dialer.go similarity index 51% rename from main.go.single-user.bk rename to internal/proxy_dialer/proxy_dialer.go index 4b70855..f54fd5c 100644 --- a/main.go.single-user.bk +++ b/internal/proxy_dialer/proxy_dialer.go @@ -1,4 +1,4 @@ -package main +package proxy_dialer import ( "bufio" @@ -6,148 +6,87 @@ import ( "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 + ProxyURL *url.URL } -type ProxyManager struct { - proxies []*ProxyConfig - currentIdx int - mu sync.Mutex - enableEdge bool +type ProxyManager interface { + GetNextProxy() (*ProxyConfig, error) + ShouldUseDirect() bool + HasProxies() bool + IsEdgeEnabled() 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 +type ProxyDialer struct { + manager ProxyManager } -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") +func NewProxyDialer(manager ProxyManager) *ProxyDialer { + return &ProxyDialer{ + manager: manager, } - - 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 + var lastError error + + if d.manager.ShouldUseDirect() { 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) - } + lastError = 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++ { + if d.manager.HasProxies() { proxy, err := d.manager.GetNextProxy() if err != nil { + if lastError != nil { + return nil, fmt.Errorf("direct connection failed: %v, proxy error: %v", lastError, err) + } return nil, err } conn, err := d.dialWithProxy(proxy, network, addr) - if err != nil { - lastError = err - continue + if err == nil { + return conn, nil } - return conn, nil + lastError = err + } else if !d.manager.IsEdgeEnabled() { + return nil, fmt.Errorf("no proxies available and edge mode is disabled") } - return nil, fmt.Errorf("all proxies failed, last error: %v", lastError) + if lastError != nil { + return nil, fmt.Errorf("all connection attempts failed, last error: %v", lastError) + } + return nil, fmt.Errorf("no connection methods available") } func (d *ProxyDialer) dialWithProxy(proxy *ProxyConfig, network, addr string) (net.Conn, error) { - switch proxy.proxyURL.Scheme { + 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) + 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) + 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 proxy.ProxyURL.User != nil { + err = performSocks5Handshake(conn, proxy.ProxyURL) if err != nil { _ = conn.Close() return nil, err @@ -163,12 +102,12 @@ func (d *ProxyDialer) dialSocks5(proxy *ProxyConfig, addr string) (net.Conn, err } func (d *ProxyDialer) dialHTTP(proxy *ProxyConfig, network, addr string) (net.Conn, error) { - conn, err := net.Dial("tcp", proxy.proxyURL.Host) + conn, err := net.Dial("tcp", proxy.ProxyURL.Host) if err != nil { return nil, err } - if proxy.proxyURL.Scheme == "https" { + if proxy.ProxyURL.Scheme == "https" { tlsConn := tls.Client(conn, &tls.Config{ InsecureSkipVerify: true, }) @@ -186,8 +125,8 @@ func (d *ProxyDialer) dialHTTP(proxy *ProxyConfig, network, addr string) (net.Co Header: make(http.Header), } - if proxy.proxyURL.User != nil { - basicAuth := base64.StdEncoding.EncodeToString([]byte(proxy.proxyURL.User.String())) + if proxy.ProxyURL.User != nil { + basicAuth := base64.StdEncoding.EncodeToString([]byte(proxy.ProxyURL.User.String())) req.Header.Set("Proxy-Authorization", "Basic "+basicAuth) } @@ -209,55 +148,6 @@ func (d *ProxyDialer) dialHTTP(proxy *ProxyConfig, network, addr string) (net.Co 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 { diff --git a/internal/proxy_manager/proxy_manager.go b/internal/proxy_manager/proxy_manager.go new file mode 100644 index 0000000..7eac516 --- /dev/null +++ b/internal/proxy_manager/proxy_manager.go @@ -0,0 +1,112 @@ +package proxy_manager + +import ( + "bufio" + "fmt" + "go-proxy-rotator/internal/proxy_dialer" + "net/url" + "os" + "strings" + "sync" + "time" +) + +type Manager struct { + proxies []*proxy_dialer.ProxyConfig + currentIdx int + mu sync.Mutex + enableEdge bool + lastUsed time.Time +} + +func NewManager(enableEdge bool) *Manager { + return &Manager{ + proxies: make([]*proxy_dialer.ProxyConfig, 0), + enableEdge: enableEdge, + lastUsed: time.Now(), + } +} + +func (pm *Manager) LoadProxies(filename string) error { + file, err := os.Open(filename) + if err != nil { + if pm.enableEdge { + 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 + } + + proxyURL, err := url.Parse(line) + if err != nil { + return fmt.Errorf("invalid proxy URL: %s", err) + } + + pm.proxies = append(pm.proxies, &proxy_dialer.ProxyConfig{ProxyURL: proxyURL}) + } + + if err := scanner.Err(); err != nil { + return err + } + + if len(pm.proxies) == 0 && !pm.enableEdge { + return fmt.Errorf("no proxies loaded from configuration and edge mode is disabled") + } + + return nil +} + +func (pm *Manager) GetNextProxy() (*proxy_dialer.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) + pm.lastUsed = time.Now() + + return proxy, nil +} + +func (pm *Manager) ShouldUseDirect() bool { + pm.mu.Lock() + defer pm.mu.Unlock() + + if !pm.enableEdge { + return false + } + + if len(pm.proxies) == 0 { + return true + } + + return time.Since(pm.lastUsed) > 5*time.Second +} + +func (pm *Manager) HasProxies() bool { + pm.mu.Lock() + defer pm.mu.Unlock() + return len(pm.proxies) > 0 +} + +func (pm *Manager) IsEdgeEnabled() bool { + return pm.enableEdge +} + +func (pm *Manager) ProxyCount() int { + pm.mu.Lock() + defer pm.mu.Unlock() + return len(pm.proxies) +} diff --git a/main.go b/main.go deleted file mode 100644 index 7304e6e..0000000 --- a/main.go +++ /dev/null @@ -1,404 +0,0 @@ -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" - "time" -) - -type ProxyConfig struct { - proxyURL *url.URL -} - -type ProxyManager struct { - proxies []*ProxyConfig - currentIdx int - mu sync.Mutex - enableEdge bool - lastUsed time.Time -} - -func NewProxyManager(enableEdge bool) *ProxyManager { - return &ProxyManager{ - proxies: make([]*ProxyConfig, 0), - enableEdge: enableEdge, - lastUsed: time.Now(), - } -} - -func (pm *ProxyManager) LoadProxies(filename string) error { - file, err := os.Open(filename) - if err != nil { - if pm.enableEdge { - 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 - } - - proxyURL, err := url.Parse(line) - if err != nil { - return fmt.Errorf("invalid proxy URL: %s", err) - } - - pm.proxies = append(pm.proxies, &ProxyConfig{proxyURL: proxyURL}) - } - - if err := scanner.Err(); err != nil { - return err - } - - 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") - } - - // Always increment the index - proxy := pm.proxies[pm.currentIdx] - pm.currentIdx = (pm.currentIdx + 1) % len(pm.proxies) - - // Update last used time - pm.lastUsed = time.Now() - - return proxy, nil -} - -func (pm *ProxyManager) ShouldUseDirect() bool { - pm.mu.Lock() - defer pm.mu.Unlock() - - if !pm.enableEdge { - return false - } - - // If we have no proxies, always use direct - if len(pm.proxies) == 0 { - return true - } - - // In edge mode with proxies, use direct connection periodically - // This ensures we rotate through direct connection as well - return time.Since(pm.lastUsed) > 5*time.Second -} - -type ProxyDialer struct { - manager *ProxyManager -} - -func (d *ProxyDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) { - var lastError error - - // Check if we should try direct connection first - if d.manager.ShouldUseDirect() { - conn, err := net.Dial(network, addr) - if err == nil { - return conn, nil - } - lastError = err - } - - // If we have proxies, try them - if len(d.manager.proxies) > 0 { - proxy, err := d.manager.GetNextProxy() - if err != nil { - if lastError != nil { - return nil, fmt.Errorf("direct connection failed: %v, proxy error: %v", lastError, err) - } - return nil, err - } - - conn, err := d.dialWithProxy(proxy, network, addr) - if err == nil { - return conn, nil - } - lastError = err - } else if !d.manager.enableEdge { - return nil, fmt.Errorf("no proxies available and edge mode is disabled") - } - - if lastError != nil { - return nil, fmt.Errorf("all connection attempts failed, last error: %v", lastError) - } - return nil, fmt.Errorf("no connection methods available") -} - -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) - } - - log.Printf("SOCKS5 server running on :1080 (Edge Mode: %v, Users: %d, Proxies: %d)\n", - enableEdge, len(credentials), len(proxyManager.proxies)) - - // Always listen on port 1080 inside container - if err := server.ListenAndServe("tcp", ":1080"); err != nil { - log.Fatal(err) - } -} - -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/makefile b/makefile new file mode 100644 index 0000000..6fe9729 --- /dev/null +++ b/makefile @@ -0,0 +1,62 @@ +.PHONY: run build test clean git-update docker-build docker-run + +# Binary name +BINARY_NAME=proxy-server +BUILD_DIR=build + +# Go parameters +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get +GOMOD=$(GOCMD) mod + +# Main package path +MAIN_PACKAGE=./cmd/main.go + +# Build flags +BUILD_FLAGS=-ldflags="-s -w" -trimpath + +all: test build + +run: + $(GOCMD) run $(MAIN_PACKAGE) + +build: + mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 $(GOBUILD) $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PACKAGE) + +build-linux: + mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PACKAGE) + +build-windows: + mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PACKAGE) + +build-mac: + mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PACKAGE) + +test: + $(GOTEST) -v ./... + +clean: + $(GOCLEAN) + rm -rf $(BUILD_DIR) + +deps: + $(GOGET) -v ./... + $(GOMOD) tidy + +docker-build: + docker build -t $(BINARY_NAME) . + +docker-run: + docker run -p 1080:1080 $(BINARY_NAME) + +git-update: + git add . + git commit -am "update" + git push \ No newline at end of file diff --git a/proxies.conf.example b/proxies.conf similarity index 97% rename from proxies.conf.example rename to proxies.conf index f58060d..0808b19 100644 --- a/proxies.conf.example +++ b/proxies.conf @@ -1,25 +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 +# 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 b/users.conf new file mode 100644 index 0000000..b6a133b --- /dev/null +++ b/users.conf @@ -0,0 +1,2 @@ +user:pass +user2:password2 \ No newline at end of file diff --git a/users.conf.example b/users.conf.example deleted file mode 100644 index 78e5625..0000000 --- a/users.conf.example +++ /dev/null @@ -1,2 +0,0 @@ -user:pass -user2:pass2 \ No newline at end of file