这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
代码生成工具
curl转go
以cURL(bash)格式复制请求,将请求粘贴到一下网站,就能够转换成go语言的请求代码。 curlconverter.com/#go
Json转Golang Struct
将json数据粘贴到该网站就可以获得对应的Struct结构体。有展开和嵌套两种形式 oktools.net/json2go
防御式编程
在正确处理之前错误的判断,如下
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代理服务器当作是自己真正要通讯的服务器一样对待,而代理服务器则会转发真正的通讯信息。
协商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
}