意图
SOCKS5 是一个代理协议,旨在为位于 Intranet 防火墙后的用户提供访问 Internet 的代理服务(Intranet,你没听错,这是个有一定年头的协议,其 RFC 提案的时间比 HTTP 1.0 还要早两个月)。
代理
根据 HTTP 1.1 的定义,proxy 是:
An intermediary program which acts as both a server and a client for the purpose of making requests on behalf of other clients. Requests are serviced internally or by passing them on, with possible translation, to other servers.
代理就是中间人,一人分饰两角:客户端眼中的目标服务器,目标服务器眼中的客户端——这意味着他必须同时满足C/S 双方的规范。再细分,如果只是简单的 pipe C/S 两端数据,那他就是个“透明代理”;一旦他对请求或响应进行了修改,那就是“非透明代理”。
但其实,SOCKS5 协议并不负责代理服务器的数据传输环节,此协议只是在C/S两端真实交互之间,建立起一条从客户端到代理服务器的授信连接。来看看细节:
协议流程
从流程上来说,SOCKS5 是一个C/S 交互的协议,交互大概分为这么几步:
- 客户端发送认证协商
- 代理服务器就认证协商进行回复(如拒绝则本次会话结束)
- 如需GSSAPI或用户名/密码认证,客户端发送认证信息
- 代理服务器就对应项进行鉴权,并进行回复或拒绝
- 客户端发送希望连接的目标信息
- 代理服务器就连接信息进行确认或拒绝
- 【非协议内容】:代理服务器连接目标并 pipe 到客户端
协议细节
1. 认证
认证方法:
- 0x00: NO AUTHENTICATION REQUIRED
- 0x01: GSSAPI
- 0x02: USERNAME/PASSWORD
- 0x03: to X’7F’ IANA ASSIGNED
- 0x80: to X’FE’ RESERVED FOR PRIVATE METHODS
- 0xFF: NO ACCEPTABLE METHODS
1.1 客户端 -> 代理服务器,请求认证:
版本号(1字节) | 可供选认证方法(1字节) | 选择的方法(1~255字节) |
固定为5 | 选了多少种 | 都有上表中哪些方法 |
1.2 代理服务器 -> 客户端,响应认证:
版本号(1字节) | 确认认证的方法 |
固定为5 | 认证方法列表的某项: 0x00,则无需客户端发送进一步认证的信息 0x01,则需要客户端进行进一步认证,细节见 RFC1929 0x01,则需要客户端进行进一步认证,细节见RFC2743 0xFF,则相当于拒绝请求,客户端只能关闭连接 |
2. 请求信息
2.1 客户端 -> 代理服务器,发送目标信息:
版本号(1字节) | 命令(1字节) | 保留(1字节) | 请求类型(1字节) | 地址(不定长) | 端口(2字节) |
固定为5 | 0x01: CONNECT 0x02: BIND 0x03: UDP ASSOCIATE |
固定为 0x00 | 0x01: IP V4 地址 0x03: 域名 0x04: IP V6 地址 |
如果请求 类型是域名, 第个1字节为 域名的长度 |
命令字段说明:
- CONNECT: 用于客户端请求服务器进行代理
- BIND: 用于客户端向服务器上报自己的反向连接监听地址(应用场景如 FTP 下载,客户端需要接受来自服务器的连接
- UDP ASSOCIATE:用于请求建立到 UDP 数据报中继的连接
2.2 代理服务器 -> 客户端,确认连接:
版本号(1字节) | 确认回应(1字节) | 保留(1字节) | 响应类型(1字节) | 地址(不定长) | 端口(2字节) |
固定为5 | 0x00: succeeded 0x01: general SOCKS server failure 0x02: connection not allowed by ruleset 0x03: Network unreachable 0x04: Host unreachable 0x05: Connection refused 0x06: TTL expired 0x07: Command not supported 0x08: Address type not supported 0x09: to X’FF’ unassigned |
固定为 0x00 | 仅用于响应客 户端BIND命令: 0x01: IP V4 地址 0x03: 域名 0x04: IP V6 地址 |
仅用于响应客 户端BIND命令: 如果请求 类型是域名, 第个1字节为 域名的长度 |
仅用于响应客 户端BIND命令 |
可以看出,在代理服务器确认回应为 0x00 时,此次 SOCKS5 协议协商部分顺利完成,宣告进入到数据传输阶段(也可以说,这之后发生的事已经与SOCKS5协议无关)。
代码示例
协议就是这样严谨,还是代码比较轻松,解释也更为直观:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
import ( "encoding/binary" "errors" "flag" "fmt" "io" "log" "math/rand" "net" "os" "strconv" "time" ) const ( socksVersion = 5 layoutVer = 0 layoutNofMethods = 1 layoutMethods = 2 layoutCommand = 1 layoutRSV = 2 layoutATYP = 3 layoutAddr = 4 typeConnect = 1 typeIPv4 = uint8(1) typeDomain = uint8(3) typeIPv6 = uint8(4) lenIPv4 = 3 + 1 + net.IPv4len + 2 lenIPv6 = 3 + 1 + net.IPv6len + 2 lenDmBase = 3 + 1 + 1 + 2 ) type ( Socks5Negotiation struct { Version uint8 NumOfMethods uint8 Methods []uint8 } Socks5Request struct { Version uint8 Command uint8 RSV uint8 AddressType uint8 Address string Port uint16 AddressWithPort string RawAddr []byte } ) func reply(conn net.Conn, bytes []byte) (err error) { if _, err := conn.Write(bytes); err != nil { log.Println("[RESP Error]", err) } return } // extract method negotiation header // |VER | NMETHODS | METHODS | // +----+----------+----------+ // | 1 | 1 | 1 to 255 | func extractNegotiation(conn net.Conn) (socks *Socks5Negotiation, err error) { buf := make([]byte, requestBuf) if _, err = io.ReadFull(conn, buf[:layoutNofMethods+1]); err != nil { return } version := uint8(buf[layoutVer]) if version != 5 { err = errors.New("NOT a Socks5 request") return } nOfMethods := uint8(buf[layoutNofMethods]) if nOfMethods == 0 { // do nothing } else if _, err = io.ReadFull(conn, buf[layoutMethods:layoutMethods+nOfMethods]); err != nil { return } socks = new(tnt.Socks5Negotiation) socks.Version = version socks.NumOfMethods = nOfMethods for i := uint8(0); i < nOfMethods; i++ { method := uint8(buf[layoutMethods+i]) socks.Methods = append(socks.Methods, method) } return } // extract socks5 request // +----+-----+-------+------+----------+----------+ // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ func extractRequest(conn net.Conn) (socksReq *Socks5Request, err error) { buf := make([]byte, requestBuf) if _, err = io.ReadFull(conn, buf[:layoutATYP+1]); err != nil { return } version := uint8(buf[layoutVer]) if version != 5 { err = errors.New("NOT a socks5 request") return } command := uint8(buf[layoutCommand]) if command != typeConnect { err = errors.New("only CONNECT be able to accept") return } RSV := uint8(buf[layoutRSV]) ATYP := uint8(buf[layoutATYP]) var address string var addrEnd int var reqLen int switch ATYP { case typeIPv4: if _, err = io.ReadFull(conn, buf[layoutAddr:layoutAddr+lenIPv4]); err != nil { return } addrEnd = layoutAddr + lenIPv4 address = net.IP(buf[layoutAddr:addrEnd]).String() reqLen = lenIPv4 case typeIPv6: if _, err = io.ReadFull(conn, buf[layoutAddr:layoutAddr+lenIPv6]); err != nil { return } addrEnd = layoutAddr + lenIPv6 address = net.IP(buf[layoutAddr:addrEnd]).String() reqLen = lenIPv6 case typeDomain: if _, err = io.ReadFull(conn, buf[layoutAddr:layoutAddr+1]); err != nil { return } addrLen := int(buf[layoutAddr]) addrEnd = layoutAddr + 1 + addrLen reqLen = addrLen + lenDmBase if _, err = io.ReadFull(conn, buf[layoutAddr+1:addrEnd]); err != nil { return } address = string(buf[layoutAddr+1 : addrEnd]) default: err = fmt.Errorf("address type is Unknown: %d", ATYP) return } if _, err = io.ReadFull(conn, buf[addrEnd:addrEnd+2]); err != nil { return } socksReq = new(tnt.Socks5Request) socksReq.Version = version socksReq.Command = command socksReq.RSV = RSV socksReq.AddressType = ATYP socksReq.Address = address socksReq.Port = binary.BigEndian.Uint16(buf[addrEnd : addrEnd+2]) socksReq.AddressWithPort = net.JoinHostPort(address, strconv.Itoa(int(socksReq.Port))) socksReq.RawAddr = buf[layoutATYP:reqLen] return } // reply the method selection // |VER | METHOD | // +----+--------+ // | 1 | 1 | func replyNegotiation(conn net.Conn, socks *tnt.Socks5Negotiation) { // no authentication required reply(conn, []byte{socksVersion, 0x00}) } // reply the request // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | func replyRequest(conn net.Conn, socksRequest *tnt.Socks5Request) { reply(conn, []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x88}) } // rountine of per connection // https://www.ietf.org/rfc/rfc1928.txt func handleConn(conn net.Conn) { defer conn.Close() // 1. extract info about negotiation socks, err := extractNegotiation(conn) if err != nil { log.Println("[Negotiate Request Error]", err) return } log.Println(socks) // 2. confirm negotiation replyNegotiation(conn, socks) // 3. extract info about request socksRequest, err := extractRequest(conn) if err != nil { log.Println("[Extract Request Error]", err) return } log.Println(socksRequest) // 4. confirm the connection was established replyRequest(conn, socksRequest) // 5. pipe data ... ) |
引用
- SOCKS 5: https://www.ietf.org/rfc/rfc1928.txt
- SOCKS 5 Username/Password AUTH: https://www.ietf.org/rfc/rfc1929.txt
- HTTP 1.0: https://tools.ietf.org/rfc/rfc1945.txt
- HTTP 1.1: https://www.ietf.org/rfc/rfc2616.txt
- GSSAPI: https://tools.ietf.org/rfc/rfc2743.txt
打赏作者
您的支持将激励我继续创作!
cool