go与go的实战案例sockts5 | 青训营笔记

112 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天.今天复习了go基础并进行了go的第一次实战案例.

一.go基础

1.slice的部分使用(append)

1.slice是go里面的可变化的数组,slice的append方法可以使sice长度增加.slice可以认为含有三个成分:cap(容量),len(长度),指针.

cap决定了在不修改底层数组的情况下,slice可以达到的最大长度.
len是当前slice的长度.
指针指向slice使用数组的第一个元素(slice的第一个元素不等于数组的第一个元素),slice通过数组的地址使用数组,对数组的元素是间接访问的.

2.对于slice的append,必须将返回的值赋给原本的slice变量,如果不处理返回值,可能会造成添加失败.

因为slice进行append时,如果没有足够的增长空间的话,append会先分配一个足够大的slice用于保存新的结果,先将原本的slice中的值复制到新的空间,然后添加新元素不处理返回值会将新的数组废弃,对于append我们不能保证他是否进行了slice的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间,所以最好将返回值赋予slice变量.

func appendInt(slice []int, x int) []int {
   var newSlice []int
   newLen := len(slice) + 1
   if newLen <= cap(slice) {
      newSlice = slice[:newLen]
   } else {
      zcap := newLen
      if zcap < 2*len(slice) {
         zcap = 2 * len(slice)
      }
      newSlice = make([]int, newLen, zcap)
      copy(newSlice, slice)
   }
   newSlice[len(slice)] = x

   return newSlice
}

这是append的一种简单实现,append的扩容方法类似于此,但是他使用了一套更加复杂的扩容方法.

2.导出

只有大写字母开头的变量称为被导出,导出的变量才可以被其他包使用,同样只有导出的结构体成员才会被Json化.

type Movie struct {
  Title  string
  Year   int  `json:"released"`
  Color  bool `json:"color"`
  Actors []string
}
var movies = []Movie{
  {Title: "Casablanca",
     Year:   1942,
     Color:  false,
     Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
}
func main() {
  Data, err := json.MarshalIndent(movies, "", "  ")
  if err != nil {
     log.Fatalf("JSON marshaling failed: %s", err)
  }
  fmt.Printf("%s\n", Data)
}

如果将结构体中的变量改为小写,那么他们将不会被josn化.

3.os/exec

在 Golang 中用于执行命令的库是 os/exec,exec.Command 函数返回一个*Cmd变量.

func main() {
   cmd := exec.Command("pwd")
   err := cmd.Run()
   if err != nil {
      log.Fatalln(err)
   }
}

如果你想要得到结果,你可以使用CombinedOutput(),他会返回你需要的输出

out, err := cmd.CombinedOutput()
fmt.Println(string(out))

如果你想要得到使用*适配符号,那么会很遗憾,因为他不能使用,比如

cmd := exec.Command("ls", "./*")

它将会报错ls: 无法访问 './*': 没有那个文件或目录

二.实战案例sockts5

今天刚刚学习的socks5的实战案例,尝试用"net/http"编写一个socks5代理.
1.socks5代理的交互流程:
image.png

2.客户端和代理服务器建立socket-tcp代理连接后,客户端向代理服务器发送请求来确认协议版本和认证方式,请求格式:(单位为字节)

    +----+----------+----------+
    |VER | NMETHODS | METHODS  |
    +----+----------+----------+
    | 1  |    1     | 1 to 255 |
    +----+----------+----------+

VER: 协议版本,socks5为0x05
NMETHODS: 支持认证的方法数量
METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
X’00’ 不需要认证
X’01’:GSSAPI
X’02’ 用户名、密码认证
X’80’-X’FE’:为私人方法保留
X’FF’:无可接受的方法

3.代理服务从客户端收到的协议中选择一个认证方法,会发送如下格式给客户端:(单位为字节)

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

VER:socks版本号
METHOD:认证方式,如果返回的是0XFF,客户端没有认证方式,需要与服务器关闭连接

4.客户端向代理服务器发送代理请求信息,请求格式:(单位为字节)

+----+-----+-------+------+----------+----------+
|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地址类型,DST.ADDR为16个字节
DST.ADDR 一个可变长度的值
DST.PORT 目标端口,固定2个字节

5.代理服务器收到客户端请求后,回复请求:(单位为字节)

+----+-----+-------+------+----------+----------+
|VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+

VER socks版本,这里为0x05
REP Relay field,内容取值如下 X’00’ succeeded
RSV 保留字段
ATYPE 地址类型
BND.ADDR 服务绑定的地址
BND.PORT 服务绑定的端口DST.PORT

6.完成以上步骤,连接就成功了,现在客户端通过与代理建立的socket发送消息,代理会将消息发送给目标服务器.

7.编写demo

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) {
   ver, err := reader.ReadByte()
   if err != nil {
      return fmt.Errorf("read ver failed:%w", err)
   }
   if ver != socks5Ver {
      return fmt.Errorf("not supported ver:%v", 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
}

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 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")
      host := make([]byte, 16)
      _, err = io.ReadFull(reader, host)
      if err != nil {
         return fmt.Errorf("read atyp failed:%w", err)
      }
      addr = fmt.Sprintf("%02x%02x.%02x%02x.%02x%02x.%02x%02x.%02x%02x.%02x%02x.%02x%02x.%02x%02x", host[0], host[1], host[2], host[3], host[4], host[5], host[6], host[7], host[8], host[9], host[10], host[11], host[12], host[13], host[14], host[15])
   default:
      return errors.New("invalid atyp")
   }
   _, err = io.ReadFull(reader, buf[:2])
   if err != nil {
      return fmt.Errorf("read : %w", err)
   }
   if err != nil {
      return fmt.Errorf("read port failed:%w", err)
   }
   port := binary.BigEndian.Uint16(buf[:2])

   log.Println("dial", addr, port)
   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()

   _, 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
}

这是今天课程上的小demo,不同的是我添加了ipv6的处理方法(似乎不太对劲).
auth()函数负责客户端与代理之间的协商协议版本和认证信息, connect()函数负责客户端与代理之间的认证连接和数据读取.

在connect()的最后使用通道(<-)等待goroutine的完成,防止程序运行完毕或进行其他工作,对foroutione进行影响或者影响foroutione.
读取时使用io.ReadFull(src,buf)函数,将读取src中最多能存储在buf的byte数,但如果读的数量不够就会阻塞住,最后使用两个goroutine并发进行copy.

但是目前对于www.qq.com 无法正确处理,调用www.qq.com 时会使用ipv6,并在最后爆出so such host的错误,但对于其他其他地址的解析正常,如: juejin.cn.

ps:如果有条件可以使用两台设备进行测试. image.png
image.png