Go语言快速上手 | 青训营笔记

282 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

代码生成工具

curl转go

以cURL(bash)格式复制请求,将请求粘贴到一下网站,就能够转换成go语言的请求代码。 curlconverter.com/#go

image.png

Json转Golang Struct

将json数据粘贴到该网站就可以获得对应的Struct结构体。有展开和嵌套两种形式 oktools.net/json2go

image.png

防御式编程

在正确处理之前错误的判断,如下

    if resp.StatusCode != 200{
        log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
    }

Socks5代理

明文传输,不能用来翻墙。

采用socks协议的代理服务器,是一种通用的代理服务器。

Socks5代理工作在会话层,不要求应用程序遵循特定的操作系统平台,Socks5代理只是简单地传递数据包,而不必关心是何种应用协议(FTP/HTTP...)。

Socks5是一个代理协议,扮演中介的角色,能使得内网中的机器能够访问Internet网中的服务器,或者使通信更加安全。(通讯需要使用TCP/IP)

内网中的机器将本来发给真正服务器的请求发送给Socks5服务器,然后Socks5服务器将请求原封不动的转发给真正的服务器。

什么场景会使用到Socks5?

Socks5设计初衷是保证网络隔离的情况下,提高部分人员的网络访问权限,访问一些访问不到的资源。 举个例子:在学校,学生们只要连接内网,就可以方便地使用学校的很多服务资源,但是如果人在家中,就无法连接学校的内网访问到学校的资源了。这个时候就可以通过在公网上面的一个VPS(虚拟专用服务器)搭建一个Socks代理服务器,并且在内网搭建一台服务器和VPS建立Socks通道。这样学生们就可以通过连接到VPS提供给的某个代理端口来访问学校的内部资源了。

Socks5协议的过程

socks5一般使用1080端口来提供服务。 socks5协议使用四次握手来建议一个连接:

  • client协商method
  • socks5 server选用method
  • client发起请求
  • socks5 server处理请求并回复

四次握手结束之后,client会socks5代理服务器当作是自己真正要通讯的服务器一样对待,而代理服务器则会转发真正的通讯信息。

image.png

协商method

客户端想要建立一个防火墙外的连接时,首先向socks5代理服务器的1080端口发起一个tcp连接。 随后,客户端向服务器提供自己支持的的method列表,数据的payload如下:

  +----+----------+----------+
  |VER | NMETHODS | METHODS  |
  +----+----------+----------+
  | 1  | 1        | 1 to 255 |
  +----+----------+----------+
  • VER: 版本号,对于socks5来说,始终为5 0x05
  • NMETHODS: method个数
  • METHODS: method列表,个数需要和第二个字段相同,不能超过255
选择method

socks5 代理服务器接到请求之后,应当选用一种method返回给客户端:

  +----+--------+
  |VER | METHOD |
  +----+--------+
  | 1  | 1      |
  +----+--------+

method的选项有如下几项,最常用的是第一个。

  • 0x00 无需授权
  • 0x01 GSSAPI
  • 0x02 用户名/密码授权
  • 0x03-0x7f IANA ASSIGNED
  • 0x80-0xfe 用户服务器向客户端返回不支持method的错误代码
请求

然后客户端将自己想要连接的信息封成一个请求发给socks5代理服务器。

  +----+-----+-------+------+----------+----------+
  |VER | CMD | RSV   | ATYP | DST.ADDR | DST.PORT |
  +----+-----+-------+------+----------+----------+
  | 1 | 1    | X'00' | 1    | Variable | 2        |
  +----+-----+-------+------+----------+----------+
  • VER 版本号 0x05

  • CMD代表命令,表示建立连接还是监听连接

    • 0x01 connect 连接其他服务器
    • 0x02 bind 监听端口
    • 0x03 建立udp连接
  • 保留

  • 地址类型

    • 0x01 IPV4
    • 0x03 域名
    • 0x04 IPV6
  • 目的地址

  • 目的端口

值得一提的是,udp是没有建立连接这一步的,所以监听和连接是一样的。

地址类型不同,目的地址的长度也是不一样的。

  • IPV4:4个字节

  • 域名:目的地址的第一个字节代表域名长度

  • IPV6:16字节

回复

socks5代理服务器收到请求之后,需要向目的地址建立连接,如果失败,需要告诉客户端原因,如果成功了,也需要告诉客户端代理服务器使用的地址和端口信息

   +----+-----+-------+------+----------+----------+
   |VER | REP | RSV   | ATYP | BND.ADDR | BND.PORT |
   +----+-----+-------+------+----------+----------+
   | 1  | 1   | X'00' | 1    | Variable | 2        |
   +----+-----+-------+------+----------+----------+
  • VER 代表版本,0x05

  • REP 返回值

    • 0x00: 成功
    • 0x01: socks 服务器错误
    • 0x02: 不允许访问
    • 0x03: Network unreachable
    • 0x04: Host unreachable
    • 0x05: Connection refused
    • 0x06: TTL expired
    • 0x07: Command not supported
    • 0x08: Address type not supported
  • 保留

  • 地址类型

代码示例

package main

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

const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 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 {
		client, err := server.Accept()
		if err != nil {
			log.Printf("Accept failed %v", err)
			continue
		}
		go process(client)
	}
}

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	err := auth(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
	err = connect(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {

//------------------------------协商Method阶段------------------------------------
//代理服务器解析客户端发的method
	// +----+----------+----------+
	// |VER | NMETHODS | METHODS  |
	// +----+----------+----------+
	// | 1  |    1     | 1 to 255 |
	// +----+----------+----------+
	// VER: 协议版本,socks5为0x05
	// NMETHODS: 支持认证的方法数量
	// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
	// X’00’ NO AUTHENTICATION REQUIRED
	// X’02’ USERNAME/PASSWORD

//一个字节一个字节读取
	ver, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read ver failed:%w", err)
	}
	//检验是否为socks5协议
	if ver != socks5Ver {
		return fmt.Errorf("not supported ver:%v", ver)
	}
	//获取method的个数
	methodSize, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read methodSize failed:%w", err)
	}
	//获取所有的method方法
	method := make([]byte, methodSize)
	_, err = io.ReadFull(reader, method)
	if err != nil {
		return fmt.Errorf("read method failed:%w", err)
	}
//------------------------------代理服务器  选择method阶段------------------------
	// +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+
	
	//0x00代表 无需授权
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}

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是一个可变长度的域名 第一个字节代表域名的长度
	// 0x04表示IPv6地址  16个字节
	// DST.ADDR 一个可变长度的值
	// DST.PORT 目标端口,固定2个字节

	buf := make([]byte, 4)
	//读取前四个字节
	_, err = io.ReadFull(reader, buf)
	if err != nil {
		return fmt.Errorf("read header failed:%w", err)
	}
	ver, cmd, atyp := buf[0], buf[1], buf[3]
	if ver != socks5Ver {
		return fmt.Errorf("not supported ver:%v", ver)
	}
	if cmd != cmdBind {
		return fmt.Errorf("not supported cmd:%v", ver)
	}
	//获取目的IP地址
	addr := ""
	switch atyp {
	case atypIPV4:
		_, err = io.ReadFull(reader, buf)
		if err != nil {
			return fmt.Errorf("read atyp failed:%w", err)
		}
		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
	case atypeHOST:
		hostSize, err := reader.ReadByte()
		if err != nil {
			return fmt.Errorf("read hostSize failed:%w", err)
		}
		host := make([]byte, hostSize)
		_, err = io.ReadFull(reader, host)
		if err != nil {
			return fmt.Errorf("read host failed:%w", err)
		}
		addr = string(host)
	case atypeIPV6:
		return errors.New("IPv6: no supported yet")
	default:
		return errors.New("invalid atyp")
	}
	//获取端口号
	_, err = io.ReadFull(reader, buf[:2])
	if err != nil {
		return fmt.Errorf("read port failed:%w", err)
	}
	port := binary.BigEndian.Uint16(buf[:2])
//向真正的服务器拨号
	dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
	if err != nil {
		return fmt.Errorf("dial dst failed:%w", err)
	}
	defer dest.Close()
	log.Println("dial", addr, port)
//------------------------------代理服务器收到请求,向目的地址建立连接,转发报文,并向客户端做出回复------------------------
	// +----+-----+-------+------+----------+----------+
	// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X'00' |  1   | Variable |    2     |
	// +----+-----+-------+------+----------+----------+
	// VER socks版本,这里为0x05
	// REP  succeeded 0x00
	// RSV 保留字段
	// ATYPE 地址类型
	// BND.ADDR 服务绑定的地址
	// BND.PORT 服务绑定的端口DST.PORT

//conn 代理服务器与客户端之间的连接
//dest 代理服务器与目的服务器之间的连接
//填写给客户端的响应报文
	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	if err != nil {
		return fmt.Errorf("write failed: %w", err)
	}
//cancel 下面这两个携程 只要有一个执行到了cancel就结束 其实两个协程都执行完成最好
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go func() {
		//将客户端发送的请求转发给目的服务器
		_, _ = io.Copy(dest, reader)
		cancel()
	}()
	go func() {
		//将目的服务器的响应,返回给客户端
		_, _ = io.Copy(conn, dest)
		cancel()
	}()

	<-ctx.Done()
	return nil
}