fwd55/fwd55.go
2023-11-07 03:23:23 -08:00

634 lines
12 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}