Skip to content

Instantly share code, notes, and snippets.

@jan-bar
Created May 14, 2024 08:21
Show Gist options
  • Save jan-bar/b856c271712a6481260131dd66dd7ffe to your computer and use it in GitHub Desktop.
Save jan-bar/b856c271712a6481260131dd66dd7ffe to your computer and use it in GitHub Desktop.
proxy server
package main
import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/url"
"strconv"
"sync"
)
func main() {
addr := flag.String("s", ":1080", "proxy server address")
flag.Parse()
err := proxy(*addr)
if err != nil {
panic(err)
}
}
const netTCP = "tcp"
func proxy(addr string) error {
lc, err := net.Listen(netTCP, addr)
if err != nil {
return err
}
//goland:noinspection GoUnhandledErrorResult
defer lc.Close()
for {
ac, ea := lc.Accept()
if ea == nil {
go func() {
if ea = handleProxy(ac); ea != nil {
log.Printf("%+v", ea)
}
}()
}
}
}
type poolBuf struct {
buf0, buf1 []byte
buf *bufio.Reader
}
func (pb *poolBuf) ReadLine() ([]byte, error) {
line := pb.buf0[:0]
for {
l, more, err := pb.buf.ReadLine()
if err != nil {
return nil, err
}
if len(line) == 0 && !more {
return l, nil
}
if line = append(line, l...); !more {
return line, nil
}
}
}
var bytePool = &sync.Pool{New: func() any {
const leakyBufSize = 64 << 10
return &poolBuf{
buf0: make([]byte, leakyBufSize),
buf1: make([]byte, leakyBufSize),
buf: new(bufio.Reader),
}
}}
//goland:noinspection GoUnhandledErrorResult
func handleProxy(c net.Conn) error {
defer c.Close()
bp := bytePool.Get().(*poolBuf)
defer bytePool.Put(bp)
bp.buf.Reset(c)
tp, err := bp.buf.Peek(2)
if err != nil {
return err
}
var rc net.Conn
switch {
case tp[0] == socksVer5:
rc, err = dialSocks5(c, bp.buf, bp.buf0, int(tp[1]))
if err != nil {
return err
}
defer rc.Close()
_, err = c.Write(socks5Established)
if err != nil {
return err
}
case tp[0] >= 'A' && tp[0] <= 'Z':
line, err := bp.ReadLine()
if err != nil {
return err
}
head := bytes.SplitN(line, httpSep, 3)
if len(head) != 3 {
return errors.New("head error")
}
address := string(head[1])
if bytes.Equal(head[0], httpConnect) {
for {
line, err = bp.ReadLine()
if err != nil {
return err
}
if len(line) == 0 {
break // read all heads
}
}
rc, err = net.Dial(netTCP, address)
if err != nil {
return err
}
defer rc.Close()
_, err = c.Write(httpEstablished)
if err != nil {
return err
}
} else {
u, err := url.Parse(address)
if err != nil {
return err
}
if port := u.Port(); port == "" {
if u.Scheme == "https" {
port = "443"
} else {
port = "80"
}
u.Host = net.JoinHostPort(u.Host, port)
}
rc, err = net.Dial(netTCP, u.Host)
if err != nil {
return err
}
defer rc.Close()
// forward first line
_, err = rc.Write(append(line, '\r', '\n'))
if err != nil {
return err
}
}
default:
return errors.New("unsupported protocol version")
}
ret := make(chan struct{})
go func() {
copyBuffer(c, rc, bp.buf0)
ret <- struct{}{}
}()
copyBuffer(rc, bp.buf, bp.buf1)
<-ret
return nil
}
func copyBuffer(dst io.Writer, src io.Reader, buf []byte) {
var (
nr, nw int
er, ew error
)
for er == nil {
nr, er = src.Read(buf)
if nr > 0 {
nw, ew = dst.Write(buf[:nr])
if nw < 0 || nr < nw || ew != nil || nr != nw {
return // Refer io.CopyBuffer
}
}
}
}
var (
httpConnect = []byte("CONNECT")
httpSep = []byte(" ")
httpEstablished = []byte("HTTP/1.1 200 Connection Established\r\n\r\n")
socks5Established = []byte{socksVer5, 0, 0, 1, 0, 0, 0, 0, 0, 0}
)
const (
socksVer5 = 5
aTypIPV4 = 1
aTypDomain = 3
aTypIPV6 = 4
)
func dialSocks5(w io.Writer, r io.Reader, buf []byte, n int) (net.Conn, error) {
_, err := io.ReadFull(r, buf[:n+2])
if err != nil {
return nil, err
}
_, err = w.Write(socks5Established[:2])
if err != nil {
return nil, err
}
_, err = io.ReadFull(r, buf[:5])
if err != nil {
return nil, err
}
if buf[0] != socksVer5 {
return nil, fmt.Errorf("invalid ver %d", buf[0])
}
if buf[1] != 1 { // cmd connect = 1
return nil, fmt.Errorf("invalid cmd %d", buf[1])
}
switch buf[3] {
case aTypDomain:
// VER CMD RSV TYPE LEN DST.ADDR DST.PORT
// 1 1 1 1 1 LEN 2
n = int(buf[4]) + 7
case aTypIPV4:
// VER CMD RSV TYPE DST.ADDR DST.PORT
// 1 1 1 1 ipv4Len 2
n = net.IPv4len + 6
case aTypIPV6:
// VER CMD RSV TYPE DST.ADDR DST.PORT
// 1 1 1 1 ipv6Len 2
n = net.IPv6len + 6
default:
return nil, errors.New("socks addr type not supported")
}
_, err = io.ReadFull(r, buf[5:n])
if err != nil {
return nil, err
}
var addr string
switch buf[3] {
case aTypDomain:
n = 5 + int(buf[4])
addr = string(buf[5:n])
case aTypIPV4:
n = 4 + net.IPv4len
addr = net.IP(buf[4:n]).String()
case aTypIPV6:
n = 4 + net.IPv6len
addr = net.IP(buf[4:n]).String()
}
addr = net.JoinHostPort(addr, strconv.FormatUint(uint64(buf[n])<<8|uint64(buf[n+1]), 10))
return net.Dial(netTCP, addr)
}
@jan-bar
Copy link
Author

jan-bar commented May 14, 2024

# http CONNECT proxy method
curl -v -x http://127.0.0.1:1080 https://cn.bing.com
# http direct proxy method
curl -v -x http://127.0.0.1:1080 http://cn.bing.com
# socks5 proxy method
curl -v -x socks5://127.0.0.1:1080 https://cn.bing.com
# socks5h proxy method
curl -v -x socks5h://127.0.0.1:1080 https://cn.bing.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment