青训营 第一课实战案例

71 阅读3分钟

抓包

工具

  • 把请求复制为Curl后,使用curlconverter就可以生成发送http请求的代码
  • 序列化json数据,需要定义结构体。json数据如果庞大,自己定义结构体会很麻烦,可以使用oktools

SOCKS5

历史

某些公司内部,十分注重数据安全,因此建立了十分严密的防火墙。但是因此呢,公司内部的内网络也很难访问外网络的信息。SOCKS5就是在这个墙中开了小口,让内网也可以访问外网。

原理:

Alt text转存失败,建议直接上传图片文件

    1. 首先 Client和代理服务器建立链接,浏览器向代理服务器发送报文(协议版本号,鉴权方法的数量,健全的方法)
    1. 代理服务器收到Client请求后,向目标服务器建立TCP链接
    1. 代理服务器正常工作

第二步,代理服务器发送的报文: // +----+-----+-------+------+----------+----------+ // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ // VER 版本号,socks5的值为0x05 // CMD 0x01表示CONNECT请求 // RSV 保留字段,值为0x00 // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。 // 0x01表示IPv4地址,DST.ADDR为4个字节 // 0x03表示域名,DST.ADDR是一个可变长度的域名 // DST.ADDR 一个可变长度的值 // DST.PORT 目标端口,固定2个字节

实现

package main

import (
	"bufio"
	"context"
	"encoding/binary"
	"fmt"
	"io"
	"log"
	"net"

	"github.com/pkg/errors"
)
const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main(){
	server,err := net.Listen("tcp","127.0.0.1:1080")
	if err != nil {
		panic(err)
	}

	for{
		conn,err := server.Accept()
		if err != nil {
			fmt.Printf("Accept failed!!!!")
			continue
		}

		go process(conn)
	}
}

func process(conn net.Conn){
	defer conn.Close()
	reader := bufio.NewReader(conn)
	
    err := auth(reader,conn)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("auth success!!!")

	err = connect(reader,conn)
	if err != nil {
		log.Fatal(err)
	}
}

// 1. 客户端向代理服务器发送认证请求
func auth(reader *bufio.Reader,conn net.Conn)error{
	ver,err := reader.ReadByte()
	if err != nil {
		return err
	}
	if ver != socks5Ver {
		return errors.New("unsupport ver")
	}

	methodsize,err := reader.ReadByte()
	if err !=nil{
		return err
	}

	methods := make([]byte,methodsize)
	_,err = io.ReadFull(reader,methods)
	if err!=nil{
		return err
	}

	log.Println("version: ",ver,"method:",methods)
	_,err = conn.Write([]byte{socks5Ver,0x00})
	if err != nil {
		return errors.New("write failed")
	}

	return nil
}

// 2. 代理服务器向服务器发送链接请求
// 3. 代理服务器向服务器发送响应,从服务器读取数据,发送给客户端

func connect(reader *bufio.Reader,conn net.Conn) (err error){
	// +----+-----+-------+------+----------+----------+
	// |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X'00' |  1   | Variable |    2     |
	// +----+-----+-------+------+----------+----------+
	// VER 版本号,socks5的值为0x05
	// CMD 0x01表示CONNECT请求
	// RSV 保留字段,值为0x00
	// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
	//   0x01表示IPv4地址,DST.ADDR为4个字节
	//   0x03表示域名,DST.ADDR是一个可变长度的域名
	// DST.ADDR 一个可变长度的值
	// DST.PORT 目标端口,固定2个字节
	cache := make([]byte,4)
	_,err = io.ReadFull(reader,cache)
	if err != nil {
		return errors.New("read failed function io.readfull")
	}

	// 判断每一个协议的值
	if cache[0]!= socks5Ver || cache[1]!= cmdBind {
		return errors.New("unsupport ver or cmd")
	}

	atyp := cache[3]
	addr := ""
	
	switch atyp {
	case atypeIPV4:
	      _,err := io.ReadFull(reader,cache)
	      if err!= nil {
	      	return errors.New("read failed function io.readfull at astypeIPV4")
	      }
		  addr = fmt.Sprintf("%d.%d.%d.%d",cache[0],cache[1],cache[2],cache[3])

	case atypeIPV6:
		return errors.New("unsupport atypeIPV6")

	case atypeHOST:
	      hostsize,err := reader.ReadByte()
	      if err!= nil {
			return errors.New("read failed function readbyte at astypeHOST")	
		  }

		  newcache := make([]byte,hostsize)
		  _,err = io.ReadFull(reader,newcache)
		  if err!= nil {
			return errors.New("read failed function io.readfull at astypeHOST")
		  }
		  addr = string(newcache)
	default:
		return errors.New("invaild atyp")
	}
	
	_,err = io.ReadFull(reader,cache[:2])
	if err!= nil {
		return errors.New("read failed function io.readfull at port")
	}
	port := binary.BigEndian.Uint16(cache[:2])

	log.Printf("addr: %s,port: %d",addr,port)

	// 建立和服务器的链接
	dest,err := net.Dial("tcp",fmt.Sprintf("%s:%d",addr,port))
	
	if err!= nil {
		return errors.New("dial failed")
	}
	defer dest.Close()
	log.Println("dial", addr, port)

	
	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	if err!= nil {
		return errors.New("write failed")
	}

	// 实现数据交换!
	ctx,cancel := context.WithCancel(context.Background())
	defer cancel()

	go func(){
		_,_ = io.Copy(dest,reader)
		cancel()
	}()

	go func(){
		_,_ = io.Copy(conn,dest)
		cancel()
	}()
	
	// 任意一个协程失败,就把两个goroutine都结束,就结束
	<-ctx.Done()
	return nil
}


运行这个代码就需要run之后,新建终端输入nc命令建立链接。

总结

实现SOCKES5的编程用到了 socket编程的相关知识和Context控制goroutine声明周期的相关知识。如若看不懂,可以参考我的socket编程Context的相关笔记。