git » 5medias » master » tree

[master] / 5medias.go

// 5medias is a simple SOCKS5 proxy.
//
// https://www.ietf.org/rfc/rfc1928.txt
//
// It only supports TCP CONNECT commands, and static username/password
// authentication.
//
// This authentication is only useful to prevent the most basic unauthorized
// access, but it is not really secure and it's extremely easy to sniff. Don't
// rely on it for real security.
package main

import (
	"bytes"
	"encoding/binary"
	"flag"
	"fmt"
	"io"
	"net"
	"time"

	"blitiri.com.ar/go/log"
)

// Command-line flags.
var (
	addr        = flag.String("addr", ":1080", "address to listen on")
	username    = flag.String("username", "", "username to expect")
	password    = flag.String("password", "", "password to expect")
	dialTimeout = flag.Duration("dial_timeout", 2*time.Second,
		"timeout for establishing outgoing connections")

	allowLoopback = flag.Bool("allow_loopback", false,
		"allow loopback connections")
)

func main() {
	flag.Parse()
	log.Init()

	ln, err := net.Listen("tcp", *addr)
	if err != nil {
		log.Fatalf("error listening: %v", err)
	}

	log.Infof("listening on %s", *addr)

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Errorf("error accepting: %v", err)
			continue
		}

		c := &Conn{conn: conn}
		go c.Handle()
	}
}

type Conn struct {
	conn net.Conn
}

func (c *Conn) Logf(f string, a ...interface{}) {
	log.Log(log.Info, 1, "%v: %s",
		c.conn.RemoteAddr(), fmt.Sprintf(f, a...))
}

func (c *Conn) Handle() {
	defer c.conn.Close()

	// Connetions should attempt to dial within 15s (this deadline is
	// cancelled before dial below).
	c.conn.SetDeadline(time.Now().Add(15 * time.Second))

	c.Logf("connected")
	if err := c.handshake(); err != nil {
		c.Logf("handshake error: %v", err)
		return
	}

	dstAddr, err := c.getRequest()
	if err != nil {
		c.Logf("request error: %v", err)
		return
	}

	// Reset the deadline, now it'll be up to the dial.
	c.conn.SetDeadline(time.Time{})

	c.Logf("dial %q", dstAddr)
	dstConn, err := net.DialTimeout("tcp", dstAddr, *dialTimeout)
	if err != nil {
		c.Logf("outgoing connection error: %v", err)
		c.reply(5) // Connection refused.
		return
	}
	defer dstConn.Close()

	if dstConn.LocalAddr().(*net.TCPAddr).IP.IsLoopback() && !*allowLoopback {
		c.Logf("loopback connection denied")
		c.reply(2) // Connection not allowed by ruleset.
		return
	}

	c.reply(0) // Success.

	c.Logf("proxying begin")
	bidirCopy(c.conn, dstConn)
	c.Logf("proxying end")
}

func (c *Conn) handshake() error {
	hdr := struct {
		Version  byte
		NMethods byte
	}{}
	if err := binary.Read(c.conn, binary.BigEndian, &hdr); err != nil {
		return err
	}

	if hdr.Version != 5 {
		return fmt.Errorf("invalid version")
	}

	methods := make([]byte, hdr.NMethods)
	if err := binary.Read(c.conn, binary.BigEndian, methods); err != nil {
		return err
	}

	if *username == "" {
		// Reply saying that we are ok with no authentication (#0).
		c.conn.Write([]byte{5, 0})
	} else {
		// Check if username/password (method 2) was offered.
		if !bytes.Contains(methods, []byte{2}) {
			c.conn.Write([]byte{5, 0xff})
			return fmt.Errorf("username/password method not offered")
		}

		// Reply saying that we accept user/password method (#2).
		c.conn.Write([]byte{5, 2})

		if err := c.auth(); err != nil {
			return err
		}
	}

	return nil
}

func (c *Conn) auth() error {
	ver, err := c.readByte()
	if err != nil {
		return err
	}
	if ver != 1 {
		return fmt.Errorf("unknown username/password version")
	}

	cUser, err := c.readBuf()
	if err != nil {
		return err
	}

	cPassword, err := c.readBuf()
	if err != nil {
		return err
	}

	if string(cUser) != *username || string(cPassword) != *password {
		// Introduce a small delay to mitigate blatant brute force attempts.
		time.Sleep(200 * time.Millisecond)
		c.conn.Write([]byte{1, 0xff})
		return fmt.Errorf("invalid username/password")
	}

	c.conn.Write([]byte{1, 0})

	return nil
}

func (c *Conn) getRequest() (string, error) {
	hdr := struct {
		Version  byte
		Command  byte
		Reserved byte
		AddrType byte
	}{}
	if err := binary.Read(c.conn, binary.BigEndian, &hdr); err != nil {
		return "", err
	}

	if hdr.Version != 5 {
		return "", fmt.Errorf("invalid version")
	}
	if hdr.Command != 1 {
		return "", fmt.Errorf("unsupported command %v", hdr.Command)
	}

	addrLen := 0
	switch hdr.AddrType {
	case 1: // ipv4
		addrLen = net.IPv4len
	case 3: // domain name
		addrLen = 0
	case 4: // ipv6
		addrLen = net.IPv6len
	default:
		return "", fmt.Errorf("unsupported address type %v", hdr.AddrType)
	}

	addr := ""
	if addrLen == 0 {
		domain, err := c.readBuf()
		if err != nil {
			return "", err
		}
		addr = string(domain)
	} else {
		raw := make([]byte, addrLen)
		if _, err := c.conn.Read(raw); err != nil {
			return "", err
		}
		ip := net.IP(raw)

		addr = ip.String()
	}

	port := uint16(0)
	if err := binary.Read(c.conn, binary.BigEndian, &port); err != nil {
		return "", err
	}

	return net.JoinHostPort(addr, fmt.Sprintf("%v", port)), nil
}

func (c *Conn) reply(result byte) {
	c.conn.Write([]byte{
		5, // version
		result,
		0,          // reserved
		1,          // addr type (ipv4, for convenience)
		0, 0, 0, 0, // ipv4 (0.0.0.0)
		0, 0, // port 0 (2 bytes)
	})
}

func (c *Conn) readByte() (byte, error) {
	b := make([]byte, 1)
	_, err := c.conn.Read(b)
	return b[0], err
}

func (c *Conn) readBuf() ([]byte, error) {
	bufLen, err := c.readByte()
	if err != nil {
		return nil, err
	}

	buf := make([]byte, bufLen)
	_, err = c.conn.Read(buf)
	return buf, err
}

func bidirCopy(src, dst io.ReadWriter) {
	done := make(chan bool, 2)

	go func() {
		io.Copy(src, dst)
		done <- true
	}()

	go func() {
		io.Copy(dst, src)
		done <- true
	}()

	// Return when one of the two completes.
	// The other goroutine will remain alive, it is up to the caller to create
	// the conditions to complete it (e.g. by closing one of the sides).
	<-done
}