视频教程
视频教程已上传B站,本篇将视频教程中的资料和代码进行了整理,可通过以下链接观看,注意结合本文档食用才更配哦
B站链接:https://www.bilibili.com/video/BV1954y1k7nc/
SOCKS5协议
百度百科
SOCKS5 是一个代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全。SOCKS5 服务器通过将前端发来的请求转发给真正的目标服务器, 模拟了一个前端的行为。在这里,前端和SOCKS5之间也是通过TCP/IP协议进行通讯,前端将原本要发送给真正服务器的请求发送给SOCKS5服务器,然后SOCKS5服务器将请求转发给真正的服务器。
维基百科
SOCKS是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。SOCKS是"SOCKetS"的缩写。
当防火墙后的客户端要访问外部的服务器时,就跟SOCKS代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器。
这个协议最初由David Koblas开发,而后由NEC的Ying-Da Lee将其扩展到SOCKS4。最新协议是SOCKS5,与前一版本相比,增加支持UDP、验证,以及IPv6。
根据OSI模型,SOCKS是会话层的协议,位于表示层与传输层之间。
SOCKS协议不提供加密。
SOCKS5比SOCKS4a多了验证、IPv6、UDP支持。创建与SOCKS5服务器的TCP连接后客户端需要先发送请求来确认协议版本及认证方式,格式为(以字节为单位):
VER | NMETHODS | METHODS |
---|---|---|
1 | 1 | 1-255 |
- VER是SOCKS版本,这里应该是0x05;
- NMETHODS是METHODS部分的长度;
- METHODS是客户端支持的认证方式列表,每个方法占1字节。当前的定义是:
- 0x00 不需要认证
- 0x01 GSSAPI[1]
- 0x02 用户名、密码认证
- 0x03 - 0x7F由IANA[2]分配(保留)
- 0x80 - 0xFE为私人方法保留
- 0xFF 无可接受的方法
服务器从客户端提供的方法中选择一个并通过以下消息通知客户端(以字节为单位):
VER | METHOD |
---|---|
1 | 1 |
- VER是SOCKS版本,这里应该是0x05;
- METHOD是服务端选中的方法。如果返回0xFF表示没有一个认证方法被选中,客户端需要关闭连接。
之后客户端和服务端根据选定的认证方式执行对应的认证。
认证结束后客户端就可以发送请求信息。如果认证方法有特殊封装要求,请求必须按照方法所定义的方式进行封装。
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剩余的内容为域名,没有 结尾。
- 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剩余的内容为域名,没有 结尾。
- 0x04 IPv6地址,16个字节长度。
- BND.ADDR 服务器绑定的地址
- BND.PORT 网络字节序表示的服务器绑定的端口
示意图
Dart实现源码
这里我们使用Dart语言来实现简单的SOCKS5服务器。
需要注意,这里使用的Dart SDK 版本为2.10,请尽量升级你本地Dart到最新版本,因为这里使用到的RawSocket
相关的某些API 是SDK 2.8之后的版本才提供的。
视频课程中是在Windows10系统上进行的本地测试,如果有远程主机,可在部署在远程主机进行测试
main.dart
import 'dart:io';
import '../lib/socks5.dart';
const server_port = 8082;
void main() {
print('socks5 service run on $server_port');
startSocks5Server();
}
void startSocks5Server() async {
RawServerSocket serverSocket =
await RawServerSocket.bind(InternetAddress.anyIPv4, server_port);
// 等待客户端socket连接
await for (RawSocket socket in serverSocket) {
Socks5Helper(socket);
}
serverSocket.close();
}
socks5.dart
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'dart:typed_data';
class Socks5Helper {
RawSocket clientSocket;
RawSocket targetSocket;
// 是否完成了握手
bool handshakeCompleted = false;
// 是否建立了连接
bool connCompleted = false;
Socks5Helper(this.clientSocket) {
// 监听Socks客户端的数据
clientSocket.listen(
(event) async {
if (event == RawSocketEvent.read) {
if (!handshakeCompleted) {
// 没有完成握手
handshakeCompleted = handshake();
} else if (!connCompleted) {
// 没有建立连接通道
connCompleted = await createConnection();
} else {
if (targetSocket != null) {
// 转发客户端的数据
forward(clientSocket, targetSocket);
}
}
}
},
onError: (e) => print("Socks5Helper onError:$e"),
onDone: () {
// 连接断开回调
clientSocket?.close();
targetSocket?.close();
});
}
//1. socks5握手
bool handshake() {
// 从客户端socket中读取字节数据
Uint8List bytes = clientSocket.read(clientSocket.available());
ByteData bd = ByteData.sublistView(bytes);
var req = HandShakeRequest.from(bd);
if (req.verify()) {
clientSocket.write(HandShakeResponse.from(0x00).toByte());
return true;
}
return false;
}
//2. 建立连接
Future<bool> createConnection() async {
// 从客户端socket中读取字节数据
Uint8List bytes = clientSocket.read(clientSocket.available());
ByteData bd = ByteData.sublistView(bytes);
var req = Socks5Request.from(bd);
// 仅支持CONNECT方式
if (req.cmd != 0x01) {
print("only support CONNECT CMD=${req.cmd}");
var res = Socks5Response.from(
0x07, IPv4(InternetAddress.tryParse('127.0.0.1').rawAddress, 8082));
clientSocket.write(res.toByte());
clientSocket.close();
return false;
}
try {
// 获取目标服务器IP地址
var addr = await req.getAddress();
// 访问目标服务器,建立Sockets连接
targetSocket = await RawSocket.connect(addr, req.dst.port,
timeout: Duration(seconds: 15));
print('${addr.host} => ${addr.address}');
var res = Socks5Response.from(
0x00, IPv4(targetSocket.address.rawAddress, targetSocket.port));
clientSocket.write(res.toByte());
// 第二阶段完成,设置目标服务器的监听
targetSocket.listen(
(event) {
if (event == RawSocketEvent.read) {
// 数据转发
forward(targetSocket, clientSocket);
}
},
onError: (e) => print("target socket error:$e"),
onDone: () {
clientSocket.close();
targetSocket.close();
});
return true;
} catch (e) {
var res = Socks5Response.from(
0x06, IPv4(InternetAddress.tryParse("127.0.0.1").rawAddress, 8082));
clientSocket.write(res.toByte());
clientSocket.close();
var errorMsg = (await req.getAddress()).host " => $e";
print(errorMsg);
}
return false;
}
//3. 转发请求
void forward(RawSocket input, RawSocket output) {
// 从输入源读取
var bytes = input.read(input.available());
// 发送给目标
output.write(bytes);
}
}
/// 握手请求
class HandShakeRequest {
int ver;
int nmethods;
Uint8List methods;
HandShakeRequest.from(ByteData blob) {
ver = blob.getUint8(0);
nmethods = blob.getUint8(1);
if (nmethods > 0) {
methods = blob.buffer.asUint8List(2, nmethods);
}
}
// 验证(暂未实现,空架子)
bool verify() {
if (ver != 0x05) {
print("version is not supported");
return false;
}
for (var it in methods) {
if (it == 0x00) {
return true;
}
}
print("verification method is not supported");
return false;
}
}
/// 握手响应
class HandShakeResponse {
int ver = 0x05;
int method;
HandShakeResponse.from(this.method);
Uint8List toByte() {
ByteData bd = ByteData(2);
bd.setUint8(0, ver);
bd.setUint8(1, method);
return bd.buffer.asUint8List();
}
}
/// 协商请求
class Socks5Request {
int ver;
int cmd;
int rsv;
int atyp;
Dst dst;
Socks5Request.from(ByteData blob) {
ver = blob.getUint8(0);
cmd = blob.getUint8(1);
rsv = blob.getUint8(2);
atyp = blob.getUint8(3);
switch (atyp) {
case 0x01: // IPv4地址
dst = IPv4(blob.buffer.asUint8List(4, 4), blob.getUint16(8));
break;
case 0x03: // 域名地址
var len = blob.getUint8(4);
var hostBytes = blob.buffer.asUint8List(5, len);
dst = Domain(ascii.decode(hostBytes), blob.getUint16(len 5));
break;
case 0x04:
break;
}
}
Future<InternetAddress> getAddress() async {
switch (atyp) {
case 0x01:
return (dst as IPv4).address;
case 0x03:
List<InternetAddress> addressList =
await InternetAddress.lookup((dst as Domain).host);
return addressList.first;
default:
return null;
}
}
}
class Dst {
int port;
}
class IPv4 implements Dst, Bnd {
int port;
InternetAddress address;
IPv4(Uint8List addr, this.port) {
address =
InternetAddress.fromRawAddress(addr, type: InternetAddressType.IPv4);
}
}
class IPv6 implements Dst, Bnd {
int port;
}
class Domain implements Dst, Bnd {
int port;
String host;
Domain(this.host, this.port);
}
class Bnd {
int port;
}
/// 协商响应
class Socks5Response {
int ver = 0x05;
int rep;
int rsv = 0x00;
int atyp = 0x01;
Bnd bnd;
Socks5Response.from(this.rep, this.bnd);
Uint8List toByte() {
var bd = ByteData(10);
bd.setUint8(0, ver);
bd.setUint8(1, rep);
bd.setUint8(2, rsv);
bd.setUint8(3, atyp);
Uint8List rawAddr = (bnd as IPv4).address.rawAddress;
for (var i = 0; i < 4; i ) {
bd.setUint8(4 i, rawAddr[i]);
}
bd.setUint16(8, bnd.port);
return bd.buffer.asUint8List();
}
}
课程制作不易,需要您的鼓励支持,请点个赞再走哦!
参考资料
[1]
GSSAPI: /w/index.php?title=GSSAPI&action=edit&redlink=1
[2]
IANA: /wiki/IANA