Last active
January 6, 2024 03:56
-
-
Save yunginnanet/c84f831a4ac39eada5609ce0319f8d54 to your computer and use it in GitHub Desktop.
fwd55.go
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 | |
// ▄─▄ ▄ ▄ ▄ ──▄ ▄─▄ ▄─▄ | |
// ▓─ ▓ ▓ ▓ ▓ ▓ ▀─▄ ▀─▄ | |
// ▀ ▀─▀─▀ ──▀ ▀─▀ ▀─▀ | |
// f w d --> 5 5 | |
// | |
// simple rfc1928 proxy server | |
// | |
// | |
// valid usecases: | |
// - learning | |
// - debugging | |
// - hoodrat stuff | |
// | |
// invalid usecases: | |
// - anything important | |
// why? | |
// - no signficant testing | |
// - no authentication | |
// - not fully RFC compliant (yet) | |
// - only supports CONNECT | |
// | |
// author: | |
// twitter.com/yunginnanet | |
// github.com/yunginnanet | |
// git.tcp.direct/kayos | |
// ircs://ircd.chat/tcpdirect | |
// | |
// | |
// --------------------------------- | |
// | |
// usage: ./fwd55 <listen> | |
// e.g: ./fwd55 127.0.0.1:1080 | |
// | |
// --------------------------------- | |
import ( | |
"context" | |
"encoding/binary" | |
"encoding/hex" | |
"errors" | |
"io" | |
"net" | |
"net/netip" | |
"os" | |
"slices" | |
"strconv" | |
"strings" | |
"time" | |
) | |
func main() { | |
serve() | |
} | |
type writer struct { | |
write func(p []byte) (n int, err error) | |
} | |
func (w writer) Write(p []byte) (n int, err error) { | |
return w.write(p) | |
} | |
func fmtHex(b []byte) string { | |
s := strings.Builder{} | |
s.WriteString("{") | |
for i := range b { | |
s.WriteString("0x") | |
s.WriteString(hex.EncodeToString([]byte{b[i]})) | |
if i < len(b)-1 { | |
s.WriteString(", ") | |
} | |
} | |
s.WriteString("}") | |
return s.String() | |
} | |
const ( | |
red = "\033[31m" | |
yellow = "\033[33m" | |
orange = "\033[38;5;208m" | |
purple = "\033[35m" | |
blue = "\033[34m" | |
green = "\033[32m" | |
gray = "\033[90m" | |
reset = "\033[0m" | |
errPrefix = red + "(FATAL) " | |
warnPrefix = orange + "(WARN!) " + reset | |
startPrefix = green + "(START) " + reset | |
closePrefix = red + "(CLOSE) " + reset | |
infoPrefix = gray + "(DEBUG) " + reset | |
writePrefix = yellow + "(W--->)" + reset | |
readPrefix = purple + "(<---R)" + reset | |
finPrefix = green + "(FINAL) " + reset | |
) | |
func handle(c net.Conn) { | |
_ = c.SetDeadline(time.Now().Add(time.Duration(5) * time.Second)) | |
log := func(s string, isErr ...string) { | |
dest := os.Stderr | |
prefix := "[" + c.RemoteAddr().String() + "]\t" | |
if len(isErr) > 0 && len(isErr[0]) > 0 { | |
prefix = prefix[:len(prefix)-1] | |
prefix += isErr[0] | |
} | |
_, _ = dest.Write([]byte(prefix)) | |
_, _ = dest.Write([]byte{byte('\t')}) | |
_, _ = dest.Write([]byte(s)) | |
if len(isErr) != 1 || isErr[0] != "" { | |
_, _ = dest.Write([]byte{byte('\n')}) | |
} | |
_, _ = dest.Write([]byte(reset)) | |
} | |
var finished = false | |
log0 := func(s string) { | |
log(s, infoPrefix) | |
} | |
logRead := func(s string) { | |
log(s, readPrefix) | |
} | |
logWrite := func(s string) { | |
log(s, writePrefix) | |
} | |
log1 := func(s string) { | |
log(s, errPrefix) | |
} | |
log2 := func(s string) { | |
log(s, warnPrefix) | |
} | |
logFin := func(s string) { | |
if finished { | |
log(s, finPrefix) | |
} else { | |
log(s, closePrefix) | |
} | |
} | |
logWriter := writer{write: func(p []byte) (n int, err error) { | |
log(string(p)) | |
return len(p), nil | |
}} | |
log(gray + "-----------------------------------------------") | |
log("connection established", startPrefix) | |
log("") | |
defer logFin("connection closed") | |
log(gray + "reading 2 bytes of protocol negotiation data...") | |
buf := make([]byte, 2) | |
var r int | |
var e error | |
r, e = c.Read(buf) | |
if e != nil { | |
log1(e.Error()) | |
return | |
} | |
enc := hex.Dumper(logWriter) | |
dump := func(buf []byte) { | |
log0("dumping buffer:") | |
_, _ = enc.Write(buf) | |
} | |
if r < 2 { | |
log1("short read:") | |
dump(buf[:r]) | |
_ = c.Close() | |
return | |
} | |
head := 0 | |
if buf[head] != 0x05 { | |
log1("bad version") | |
dump(buf) | |
_ = c.Close() | |
return | |
} | |
logRead("\tproto version:\t" + | |
blue + "0x" + hex.EncodeToString([]byte{buf[head]}) + | |
reset + gray + "\t(int: " + strconv.Itoa(int(buf[head])) + ")", | |
) | |
head++ | |
numMethods := int(buf[head]) | |
if numMethods < 1 { | |
log1("no methods") | |
dump(buf) | |
_ = c.Close() | |
return | |
} | |
if numMethods > 255 { | |
log1("too many methods (>255)") | |
dump(buf) | |
_ = c.Close() | |
return | |
} | |
logRead("\tauth offer ct:\t" + | |
blue + "0x" + hex.EncodeToString([]byte{buf[head]}) + | |
reset + gray + "\t(int: " + strconv.Itoa(int(buf[head])) + ")", | |
) | |
log("") | |
if buf[head] >= 1 { | |
log(gray + "expanding buffer for auth methods...") | |
buf = append(buf, make([]byte, numMethods)...) | |
} | |
var authMethods = map[string]bool{ | |
"anonymous": false, | |
"gss-api": false, | |
"user/pass": false, | |
} | |
updateAuthMethods := func(b byte) { | |
logRead("\tauth methods +=\t" + blue + "0x" + hex.EncodeToString([]byte{b}) + | |
reset + gray + "\t(int: " + strconv.Itoa(int(b)) + ")" + reset) | |
switch { | |
case b > 0x02: | |
switch { | |
case b > 0x02 && b < 0x7f: | |
log2("iana assigned auth method used by client: " + strconv.Itoa(int(b))) | |
case b > 0x7f && b < 0xfe: | |
log2("reserved auth method used by client: " + strconv.Itoa(int(b))) | |
case b == 0xff: | |
log2("no acceptable auth methods (0xff) sent by client") | |
default: | |
log2("unknown auth method: " + strconv.Itoa(int(b))) | |
} | |
default: | |
switch b { | |
case 0x00: | |
authMethods["anonymous"] = true | |
case 0x01: | |
authMethods["gss-api"] = true | |
case 0x02: | |
authMethods["user/pass"] = true | |
default: | |
log2("unknown auth method: " + strconv.Itoa(int(b))) | |
} | |
} | |
} | |
printAuthMethods := func() { | |
log("") | |
hdr := gray + "----- client auth methods -----" | |
log(hdr) | |
var res = "N/A" | |
for k, v := range authMethods { | |
res = red + strconv.FormatBool(v) + reset | |
if v { | |
res = green + strconv.FormatBool(v) + reset | |
} | |
log0(" " + k + ":\t\t" + res) | |
} | |
log(gray + strings.Repeat("-", len(hdr)-5)) | |
log("") | |
} | |
miniBuf := make([]byte, 1) | |
oldHead := head + 1 | |
for head++; head-oldHead < numMethods; head++ { | |
var e error | |
if r, e = c.Read(miniBuf); e != nil { | |
println(e.Error()) | |
_ = c.Close() | |
return | |
} | |
if r < 1 { | |
println("short read") | |
dump(buf) | |
_ = c.Close() | |
return | |
} | |
updateAuthMethods(miniBuf[0]) | |
copy(buf[head:], miniBuf) | |
miniBuf = slices.Delete(miniBuf, 0, 0) | |
} | |
printAuthMethods() | |
if !authMethods["anonymous"] { | |
log1("does not support anonymous auth") | |
// 0xff no acceptable auth methods | |
resp := []byte{0x05, 0xff} | |
logWrite(red + fmtHex(resp) + gray + " (no acceptable auth methods)") | |
_, _ = c.Write(resp) | |
_ = c.Close() | |
return | |
} | |
resp := []byte{0x05, 0x00} | |
logWrite(green + fmtHex(resp) + gray + "\t(successful auth)") | |
log("") | |
written, e := c.Write(resp) | |
if e != nil { | |
log1(e.Error()) | |
_ = c.Close() | |
return | |
} | |
if written != 2 { | |
log1("short write") | |
_ = c.Close() | |
return | |
} | |
log(gray + "reading 10 bytes of request data...") | |
buf = append(buf, make([]byte, 10)...) | |
r, e = c.Read(buf[head:]) | |
if e != nil { | |
log1(e.Error()) | |
_ = c.Close() | |
return | |
} | |
if r < 10 { | |
log1("short read") | |
dump(buf[:head+r]) | |
_ = c.Close() | |
return | |
} | |
if buf[head] != 0x05 { | |
log1("bad version") | |
dump(buf[head:]) | |
_ = c.Close() | |
return | |
} | |
logRead("\tproto version:\t" + | |
blue + "0x" + hex.EncodeToString([]byte{buf[head]}) + | |
reset + gray + "\t(int: " + strconv.Itoa(int(buf[head])) + ")", | |
) | |
head++ | |
if buf[head] != 0x01 { | |
log1("bad command") | |
dump(buf[head:]) | |
_ = c.Close() | |
return | |
} | |
logRead("\tcommand:\t" + | |
blue + "0x" + hex.EncodeToString([]byte{buf[head]}) + | |
reset + gray + "\t(int: " + strconv.Itoa(int(buf[head])) + ") (connect)", | |
) | |
head++ | |
if buf[head] != 0x00 { | |
log1("reserved header not zero") | |
dump(buf[head:]) | |
_ = c.Close() | |
return | |
} | |
logRead(gray + "\treserved:\t" + blue + "0x" + hex.EncodeToString([]byte{buf[head]})) | |
log("") | |
head++ | |
if buf[head] != 0x01 { | |
log1("bad address type, only ipv4 address supported") | |
dump(buf[head:]) | |
resp := []byte{0x05, 0x08} | |
logWrite(red + fmtHex(resp) + reset + gray + " (bad address type)") | |
_, _ = c.Write(resp) | |
_ = c.Close() | |
return | |
} | |
target := net.IP{ | |
buf[head+1], buf[head+2], buf[head+3], buf[head+4], | |
} | |
logRead("\ttarget addr:\t" + | |
blue + fmtHex([]byte(target)), | |
) | |
log("\t\t" + gray + " \\--> (int: " + | |
strconv.Itoa(int(binary.BigEndian.Uint32(target))) + | |
") (" + target.String() + ")", | |
) | |
head += 5 | |
portSlice := []byte{buf[head], buf[head+1]} | |
var port uint16 | |
for i := range portSlice { | |
if portSlice[i] < 0 || portSlice[i] > 255 { | |
log1("bad port") | |
dump(buf[head:]) | |
resp := []byte{0x05, 0x01} | |
logWrite(red + fmtHex(resp) + reset + gray + " (bad port)") | |
_, _ = c.Write(resp) | |
_ = c.Close() | |
return | |
} | |
if i == 0 { | |
port = uint16(portSlice[i]) | |
} else { | |
port = port<<8 | uint16(portSlice[i]) | |
} | |
} | |
logRead("\ttarget port:\t" + blue + fmtHex(portSlice)) | |
log("\t\t" + gray + " \\--> (int: " + strconv.Itoa(int(port)) + ")") | |
targetStr := target.String() + ":" + strconv.Itoa(int(port)) | |
ap, err := netip.ParseAddrPort(targetStr) | |
if err != nil { | |
log1(err.Error()) | |
dump(buf[head:]) | |
resp := []byte{0x05, 0x01} | |
logWrite(red + fmtHex(resp) + reset + gray + " (general failure)") | |
_, _ = c.Write(resp) | |
_ = c.Close() | |
return | |
} | |
targetHost := ap.String() | |
log("") | |
log(gray + "beginning connection to target host...") | |
log("\tconnecting to "+targetHost+"... ", "") | |
var conn net.Conn | |
if conn, e = net.DialTimeout("tcp", targetHost, time.Duration(5)*time.Second); e != nil { | |
_, _ = os.Stderr.Write([]byte(red + "failed" + reset + "\n")) | |
log("") | |
log1(e.Error()) | |
errResp := []byte{0x05, 0x01} | |
logWrite(red + fmtHex(errResp) + reset + gray + " (general failure)") | |
_, _ = c.Write(errResp) | |
_ = c.Close() | |
return | |
} | |
_, _ = os.Stderr.Write([]byte(green + "success!" + reset + "\n")) | |
log("") | |
localAddr := c.LocalAddr().(*net.TCPAddr).IP.To4() | |
localPortUint16 := uint16(c.LocalAddr().(*net.TCPAddr).Port) | |
localPortBytes := []byte{byte(localPortUint16 >> 8), byte(localPortUint16)} | |
resp = []byte{0x05, 0x00, 0x00, 0x01} | |
logWrite(green + fmtHex(resp) + reset + gray + " (success)") | |
written, e = c.Write(resp) | |
if e != nil { | |
log1(e.Error()) | |
return | |
} | |
if written != 4 { | |
log1("short write") | |
return | |
} | |
logWrite(blue + fmtHex(localAddr) + reset + gray + " (my addr: " + localAddr.String() + ")") | |
written, e = c.Write(localAddr) | |
if e != nil { | |
log1(e.Error()) | |
return | |
} | |
if written != 4 { | |
log1("short write") | |
dump(localAddr[:written]) | |
return | |
} | |
logWrite(blue + fmtHex(localPortBytes) + reset + gray + "\t\t (my port: " + strconv.Itoa(int(localPortUint16)) + ")") | |
log("") | |
written, e = c.Write(localPortBytes) | |
if e != nil { | |
log1(e.Error()) | |
return | |
} | |
if written != 2 { | |
log1("short write") | |
dump(localPortBytes[:written]) | |
return | |
} | |
log(gray+"beginning proxy i/o goroutines...", "") | |
defer func() { _ = conn.Close() }() | |
var totalRead, totalWritten int | |
totalRead, totalWritten, e = pipe(c, conn) | |
switch { | |
case errors.Is(e, io.EOF): | |
finished = true | |
_, _ = os.Stderr.WriteString(green + " EOF " + reset + "\n") | |
case e == nil: | |
finished = true | |
_, _ = os.Stderr.WriteString(green + " FIN " + reset + "\n") | |
default: | |
_, _ = os.Stderr.WriteString(red + " ERR " + reset + "\n") | |
log1(e.Error()) | |
} | |
log0(gray + "bytes read: " + strconv.Itoa(totalRead) + "\tbytes written: " + strconv.Itoa(totalWritten)) | |
log("") | |
} | |
func pipe(socksClient net.Conn, target net.Conn) (totalRead int, totalWritten int, err error) { | |
defer func() { _ = target.Close() }() | |
// caller closes socksClient | |
// defer func() { _ = socksClient.Close() }() | |
outBuf := make([]byte, 1024) | |
inBuf := make([]byte, 1024) | |
eChan := make(chan error, 2) | |
ctx, cancel := context.WithCancel(context.Background()) | |
totalRead = 0 | |
totalWritten = 0 | |
go func() { | |
defer cancel() | |
for { | |
_ = target.SetDeadline(time.Now().Add(time.Duration(5) * time.Second)) | |
select { | |
case <-ctx.Done(): | |
return | |
default: | |
} | |
n, e := target.Read(inBuf) | |
if e != nil { | |
eChan <- e | |
return | |
} | |
if n == 0 { | |
eChan <- io.EOF | |
return | |
} | |
totalRead += n | |
_, e = socksClient.Write(inBuf[:n]) | |
if e != nil { | |
eChan <- e | |
return | |
} | |
} | |
}() | |
go func() { | |
defer cancel() | |
for { | |
_ = socksClient.SetDeadline(time.Now().Add(time.Duration(5) * time.Second)) | |
select { | |
case <-ctx.Done(): | |
return | |
default: | |
} | |
n, e := socksClient.Read(outBuf) | |
if e != nil { | |
eChan <- e | |
return | |
} | |
if n == 0 { | |
eChan <- io.EOF | |
return | |
} | |
n, e = target.Write(outBuf[:n]) | |
totalWritten += n | |
if e != nil { | |
eChan <- e | |
return | |
} | |
} | |
}() | |
select { | |
case e := <-eChan: | |
return totalRead, totalWritten, e | |
case <-ctx.Done(): | |
return totalRead, totalWritten, nil | |
} | |
} | |
func serve() { | |
if len(os.Args) < 2 { | |
return | |
} | |
var l net.Listener | |
var e error | |
go func() { | |
l, e = net.Listen("tcp", os.Args[1]) | |
if e != nil { | |
println(e.Error()) | |
os.Exit(1) | |
} | |
}() | |
time.Sleep(time.Duration(5) * time.Millisecond) | |
println("listening on " + os.Args[1]) | |
for { | |
c, e := l.Accept() | |
if e != nil { | |
println(e.Error()) | |
continue | |
} | |
go handle(c) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment