0x0 Pre
这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记.
SOCKS是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。SOCKS是"SOCKet Secure"的缩写。 当防火墙后的客户端要访问外部的服务器时,就跟SOCKS代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器。这个协议最初由David Koblas开发,而后由NEC的Ying-Da Lee将其扩展到SOCKS4。最新协议是SOCKS5,与前一版本相比,增加支持UDP、验证,以及IPv6。 根据OSI模型,SOCKS是会话层的协议,位于表示层与传输层之间。SOCKS协定不提供加密(来自维基百科[2])。
在本章中会掌握以下几点:
- golang的基础使用
- 字节流读取
- context 包的使用
- socks5服务器的搭建
0x1 流的读取
golang中基础的读取流大致有三种:os,bufio,ioutil,以下分别举例读取文件的方式:
- OS 读取方式:
file, err := os.Open("F:/a.txt") if err != nil { fmt.Println("打开文件失败") return } var data [1024]byte n, err := file.Read(data[:]) if err != nil { fmt.Println("Read Error") return } if err == io.EOF{ // 读取完毕 fmt.Println("Read Over") return } fmt.Println(n, string(data[:n])) file.Close() // 重要 - bufio 读取
通常情况下,func bufioRead(){ file, err := os.Open("F:/win10应用.txt") if err != nil { fmt.Println("文件打开失败", err) return } defer file.Close() // 创建读取内容的对象 reader := bufio.NewReader(file) line, err := reader.ReadString('\n') if err != nil{ fmt.Printf("err:%v\n", err) return } if err == io.EOF{ fmt.Printf("%v\n", "Read Over") return } fmt.Println(line) }bufio用于一行一行的读取流,使用reader.ReadString('\n')方法,以\n为分隔符进行读取到变量中。对于字节流数据想要读取一个 byte 可以使用方法reader.ReadByte()方法。 - ioutil 读取
func ioutilRead() { content, err := ioutil.ReadFile("F:/win10应用.txt") if err != nil { fmt.Printf("\"File Read Err, err: %v\n", err) return } fmt.Println(string(content)) }ioutil会自动帮忙进行打开和关闭文件,适用于读取整个文件。
0x2 context包
上下文 context.Context 在 Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。
- 使用 context 实现倒计时通信事件
输出:func main() { log.SetFlags(log.LstdFlags | log.Lmicroseconds) log.Printf("Start") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() go handle(ctx, 3*time.Second) select { case <-ctx.Done(): log.Println("main", ctx.Err()) } time.Sleep(1 * time.Second) } func handle(ctx context.Context, duration time.Duration) { select { case <-ctx.Done(): log.Println("handle", ctx.Err()) case <-time.After(duration): log.Println("process request with", duration) } }可见五秒后输出定时结果。2022/05/07 21:01:41.536615 Start 2022/05/07 21:01:44.567277 process request with 3s 2022/05/07 21:01:46.574656 main context deadline exceeded - 使用 context 实现父子协程之间的同步中断
输出:func main() { fatherCtx, cancel := context.WithCancel(context.Background()) // 在父context下创建子context sonCtx, _ := context.WithCancel(fatherCtx) go fatherWatch(fatherCtx) go sonWatch(sonCtx) time.Sleep(3 * time.Second) log.Printf("Cancel All.") cancel() time.Sleep(time.Second) } func fatherWatch(ctx context.Context) { for true { select { case <-ctx.Done(): log.Printf("Father exit") return default: log.Printf("Father is working") time.Sleep(time.Second) } } } func sonWatch(ctx context.Context) { for true { select { case <-ctx.Done(): log.Printf("Son exit") return default: log.Printf("Son is working") time.Sleep(time.Second) } } }2022/05/07 21:17:05 Father is working 2022/05/07 21:17:05 Son is working 2022/05/07 21:17:06 Father is working 2022/05/07 21:17:06 Son is working 2022/05/07 21:17:07 Son is working 2022/05/07 21:17:07 Father is working 2022/05/07 21:17:08 Cancel All. 2022/05/07 21:17:08 Father exit 2022/05/07 21:17:08 Son exit
0x3 搭建 socks5 服务器
一个sock5服务器需要进行两个阶段,验证(auth)和连接(connect):
Socks5服务器的验证
建立与SOCKS5服务器的TCP连接后客户端需要先发送请求来确认协议版本及认证方式,格式为(以字节为单位):
| VER | NMETHODS | METHODS |
|---|---|---|
| 1 | 1 | 1-255 |
-
VER是SOCKS版本,这里应该是0x05;
-
NMETHODS是METHODS部分的长度;
-
METHODS是客户端支持的认证方式列表,每个方法占1字节。(我们选择不需要验证)
-
0x00 不需要认证
-
0x02 用户名、密码认证
-
之后服务器需要返回客户端选择的认定的认证方式:
| VER | METHOD |
|---|---|
| 1 | 1 |
- VER是SOCKS版本,这里应该是0x05;
- METHOD是服务端选中的方法。如果返回0xFF表示没有一个认证方法被选中,客户端需要关闭连接。
编写代码:
func auth(reader *bufio.Reader, conn net.Conn) error {
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read version number failed: %v\n", err)
}
if ver != socks5Ver {
return fmt.Errorf("Not supported version: %v, only socks5\n", ver)
}
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
Socks5服务器的连接
当验证通过之后,就可以建立两者之间的TCP连接了。
客户端发送给服务器端的SOCKS5请求格式(以字节为单位):
| VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|---|---|---|---|---|---|
| 1 | 1 | 0x00 | 1 | 动态 | 2 |
-
VER是SOCKS版本,这里应该是0x05;
-
CMD是SOCK的命令码
- 0x01表示CONNECT请求
- 0x02表示BIND请求
- 0x03表示UDP转发
-
RSV 0x00,保留
-
ATYP DST.ADDR类型
- 0x01 IPv4地址,DST.ADDR部分4字节长度
- 0x03 域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。
- 0x04 IPv6地址,16个字节长度。
-
DST.ADDR 目的地址
-
DST.PORT 网络字节序表示的目的端口
服务器按以下格式回应客户端的请求(以字节为单位):
| VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
|---|---|---|---|---|---|
| 1 | 1 | 0x00 | 1 | 动态 | 2 |
-
VER是SOCKS版本,这里应该是0x05;
-
REP应答字段
- 0x00表示成功
- 0x01普通SOCKS服务器连接失败
- 0x02现有规则不允许连接
- 0x03网络不可达
- 0x04主机不可达
- 0x05连接被拒
- 0x06 TTL超时
- 0x07不支持的命令
- 0x08不支持的地址类型
- 0x09 - 0xFF未定义
-
RSV 0x00,保留
-
ATYP BND.ADDR类型
- 0x01 IPv4地址,DST.ADDR部分4字节长度
- 0x03域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。
- 0x04 IPv6地址,16个字节长度。
-
BND.ADDR 服务器绑定的地址
-
BND.PORT 网络字节序表示的服务器绑定的端口
按照以上编程可得:
const (
cmdBind = 0x1
atypIPV4 = 0x1
atypHOST = 0x3
atypIPV6 = 0x4
socks5Ver = 0x5
)
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
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)
}
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 atypHOST:
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 atypIPV6:
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)
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
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
}
Socks5服务器启动
func main() {
server, err := net.Listen("tcp", "localhost:1080") // 监听端口
if err != nil {
panic(err)
}
for {
client, err := server.Accept() // 阻塞方法,等待客户端连接
if err != nil {
log.Printf("Accept client err: %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
}
log.Printf("Client(%v) auth pass.", conn.RemoteAddr())
err = connect(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
}