Created
May 14, 2024 08:21
-
-
Save jan-bar/b856c271712a6481260131dd66dd7ffe to your computer and use it in GitHub Desktop.
proxy server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
Author
jan-bar
commented
May 14, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment