Simple SOCKS5 Proxy Server By Golang
A simple SOCKS5 proxy server, implemented in Golang, and deployed using Docker Compose.
Prerequisites
- Golang development environment
- Docker
Directory Structure
your_project/
├── main.go
├── Dockerfile
└── docker-compose.yml
main.go Code
The default listening port is 1080. To change the port, modify the mapping in docker-compose.yml.
package main
import (
"encoding/binary"
"fmt"
"io"
"log"
"net"
"sync"
)
// Global variables for session management
var (
// allowedClients stores allowed client IPs and their active session counts
allowedClients sync.Map // map[string]interface{}, value is *int
// destToClient maps destination addresses to client UDP addresses
destToClient sync.Map // map[string]string
)
func main() {
// Start TCP listener
go func() {
listener, err := net.Listen("tcp", ":1080")
if err != nil {
log.Fatal("Failed to start TCP listener: ", err)
}
defer listener.Close()
log.Println("TCP serverK server started on :1080")
for {
conn, err := listener.Accept()
if err != nil {
log.Println("Failed to accept TCP connection: ", err)
continue
}
go handleConnection(conn)
}
}()
// Start UDP listener
udpAddr, err := net.ResolveUDPAddr("udp", ":1080")
if err != nil {
log.Fatal("Failed to resolve UDP address: ", err)
}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
log.Fatal("Failed to start UDP listener: ", err)
}
defer udpConn.Close()
log.Println("UDP server started on :1080")
buf := make([]byte, 65536)
for {
n, clientAddr, err := udpConn.ReadFromUDP(buf)
if err != nil {
log.Println("Failed to read UDP data: ", err)
continue
}
go handleUDPPacket(udpConn, clientAddr, buf[:n])
}
}
// handleConnection handles TCP connections
func handleConnection(conn net.Conn) {
defer conn.Close()
// Read client greeting
buf := make([]byte, 2)
_, err := io.ReadFull(conn, buf)
if err != nil {
log.Println("Failed to read greeting: ", err)
return
}
if buf[0] != 0x05 {
log.Println("Unsupported protocol version: ", buf[0])
return
}
nmethods := buf[1]
methods := make([]byte, nmethods)
_, err = io.ReadFull(conn, methods)
if err != nil {
log.Println("Failed to read authentication methods: ", err)
return
}
// Reply with no authentication required
_, err = conn.Write([]byte{0x05, 0x00})
if err != nil {
log.Println("Failed to send authentication reply: ", err)
return
}
// Read client request
buf = make([]byte, 4)
_, err = io.ReadFull(conn, buf)
if err != nil {
log.Println("Failed to read request:", err)
return
}
if buf[0] != 0x05 {
log.Println("Unsupported protocol version: ", buf[0])
return
}
cmd := buf[1]
if cmd != 0x01 && cmd != 0x03 {
log.Println("Unsupported command: ", cmd)
conn.Write([]byte{0x05, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
return
}
atyp := buf[3]
var addr string
switch atyp {
case 0x01: // IPv4
ip := make([]byte, 4)
_, err = io.ReadFull(conn, ip)
if err != nil {
log.Println("Failed to read IPv4 address: ", err)
return
}
addr = net.IP(ip).String()
case 0x03: // Domain name
lenByte := make([]byte, 1)
_, err = io.ReadFull(conn, lenByte)
if err != nil {
log.Println("Failed to read domain length: ", err)
return
}
domainLen := lenByte[0]
domain := make([]byte, domainLen)
_, err = io.ReadFull(conn, domain)
if err != nil {
log.Println("Failed to read domain: ", err)
return
}
addr = string(domain)
case 0x04: // IPv6
ip := make([]byte, 16)
_, err = io.ReadFull(conn, ip)
if err != nil {
log.Println("Failed to read IPv6 address: ", err)
return
}
addr = net.IP(ip).String()
default:
log.Println("Unsupported address type: ", atyp)
conn.Write([]byte{0x05, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
return
}
portBytes := make([]byte, 2)
_, err = io.ReadFull(conn, portBytes)
if err != nil {
log.Println("Failed to read port: ", err)
return
}
port := binary.BigEndian.Uint16(portBytes)
if cmd == 0x01 { // CONNECT
// Connect to the target server
destConn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port))
if err != nil {
log.Println("Failed to connect to target: ", err)
conn.Write([]byte{0x05, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
return
}
defer destConn.Close()
// Send success reply
reply := []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
_, err = conn.Write(reply)
if err != nil {
log.Println("Failed to send success reply: ", err)
return
}
// Relay data
go io.Copy(destConn, conn)
io.Copy(conn, destConn)
} else if cmd == 0x03 { // UDP ASSOCIATE
clientIP := conn.RemoteAddr().(*net.TCPAddr).IP.String()
val, _ := allowedClients.LoadOrStore(clientIP, new(int))
count := val.(*int)
*count++
defer func() {
*count--
if *count == 0 {
allowedClients.Delete(clientIP)
}
}()
// Send UDP address reply
localAddr := conn.LocalAddr().(*net.TCPAddr)
reply := []byte{0x05, 0x00, 0x00}
if localAddr.IP.To4() != nil {
reply = append(reply, 0x01)
reply = append(reply, localAddr.IP.To4()...)
} else {
reply = append(reply, 0x04)
reply = append(reply, localAddr.IP.To16()...)
}
reply = append(reply, byte(localAddr.Port>>8), byte(localAddr.Port&0xff))
_, err = conn.Write(reply)
if err != nil {
log.Println("Failed to send UDP ASSOCIATE reply: ", err)
return
}
// Keep TCP connection open
buf := make([]byte, 1)
_, err = conn.Read(buf)
if err != nil {
log.Println("TCP connection closed: ", err)
}
}
}
// handleUDPPacket handles UDP packets
func handleUDPPacket(udpConn *net.UDPConn, srcAddr *net.UDPAddr, data []byte) {
clientIP := srcAddr.IP.String()
if _, ok := allowedClients.Load(clientIP); !ok {
log.Println("UDP packet from unauthorized client: ", clientIP)
return
}
// Check if it's a SOCKS5 UDP packet from the client
if len(data) < 3 || binary.BigEndian.Uint16(data[0:2]) != 0 || data[2] != 0 {
// Response from the target server
destKey := srcAddr.String()
if val, ok := destToClient.Load(destKey); ok {
clientUDPAddrStr := val.(string)
clientUDPAddr, err := net.ResolveUDPAddr("udp", clientUDPAddrStr)
if err != nil {
log.Println("Failed to resolve client UDP address: ", err)
return
}
// Encapsulate SOCKS5 header
response := []byte{0x00, 0x00, 0x00} // RSV and FRAG
if ip := srcAddr.IP.To4(); ip != nil {
response = append(response, 0x01)
response = append(response, ip...)
} else if ip := srcAddr.IP.To16(); ip != nil {
response = append(response, 0x04)
response = append(response, ip...)
} else {
log.Println("Unsupported IP type")
return
}
response = append(response, byte(srcAddr.Port>>8), byte(srcAddr.Port&0xff))
response = append(response, data...)
_, err = udpConn.WriteToUDP(response, clientUDPAddr)
if err != nil {
log.Println("Failed to send response to client: ", err)
}
}
return
}
// Request from the client
atyp := data[3]
var destAddr string
var destPort uint16
var offset int
switch atyp {
case 0x01: // IPv4
if len(data) < 10 {
log.Println("Invalid UDP packet")
return
}
destAddr = net.IP(data[4:8]).String()
destPort = binary.BigEndian.Uint16(data[8:10])
offset = 10
case 0x03: // Domain name
if len(data) < 5 {
log.Println("Invalid UDP packet")
return
}
domainLen := data[4]
if len(data) < int(5+domainLen+2) {
log.Println("Invalid UDP packet")
return
}
domain := string(data[5 : 5+domainLen])
destAddr = domain
destPort = binary.BigEndian.Uint16(data[5+domainLen : 7+domainLen])
offset = int(7 + domainLen)
case 0x04: // IPv6
if len(data) < 22 {
log.Println("Invalid UDP packet")
return
}
destAddr = net.IP(data[4:20]).String()
destPort = binary.BigEndian.Uint16(data[20:22])
offset = 22
default:
log.Println("Unsupported address type: ", atyp)
return
}
payload := data[offset:]
// Send data to the target
destUDPAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", destAddr, destPort))
if err != nil {
log.Println("Failed to resolve target UDP address: ", err)
return
}
_, err = udpConn.WriteToUDP(payload, destUDPAddr)
if err != nil {
log.Println("Failed to send data to target: ", err)
return
}
// Record mapping from target to client
destKey := destUDPAddr.String()
clientKey := srcAddr.String()
destToClient.Store(destKey, clientKey)
}
Initialize Go Module and Download Dependencies
Run the following commands:
go mod init socks5-server
go mod tidy
Dockerfile
# Use the official Golang image as the base image
FROM golang:1.20-alpine
# Set the working directory
WORKDIR /app
# Copy all files from the current directory to /app in the container
COPY . .
# Build the Golang application
RUN go build -o socks5-server
# Run the compiled binary
CMD ["./socks5-server"]
docker-compose.yml
services:
socks5-server: # Service name
build:
context: . # Build context is the current directory
dockerfile: Dockerfile # Specify the Dockerfile to use
ports:
- "1080:1080" # Map host port 1080 to container port 1080
- "1080:1080/udp"
Starting the Server
docker compose up -d