一、Socks5协议简介

socks5是基于传输层的协议,客户端和服务器经过两次握手协商之后服务端为客户端建立一条到目标服务器的通道,在传输层转发TCP/UDP流量。

关于socks5协议规范,到处都可以找到,我再重复一遍也没啥意思,因此不再赘述,可以参见rfc1928(英文),或者查阅维基百科SOCKS5 - 维基百科(中文)。

二、代码实现

基于socks5进行了一个代理服务器的简单实现,认证方式没有做,客户端和服务器只是简单的进行两次握手即开始转发数据。

package cc11001100.proxyServerDev.socks5;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; /**
* socks5代理服务器简单实现
*
* <a>https://www.ietf.org/rfc/rfc1928.txt</a>
* <p>
* <p>
* 使用socks5代理的坑,域名在本地解析还是在代理服务器端解析,有些比如google.com就必须在代理服务器端解析
* <a>https://blog.emacsos.com/use-socks5-proxy-in-curl.html</a>
*
* @author CC11001100
*/
public class Socks5ProxyServer { // 服务监听在哪个端口上
private static final Integer SERVICE_LISTENER_PORT = 10086; // 能够允许的最大客户端数量
private static final Integer MAX_CLIENT_NUM = 100; // 用于统计客户端的数量
private static AtomicInteger clientNumCount = new AtomicInteger(); // socks协议的版本,固定为5
private static final byte VERSION = 0X05;
// RSV,必须为0
private static final byte RSV = 0X00; private static String SERVER_IP_ADDRESS; static {
try {
SERVER_IP_ADDRESS = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
} public static class ClientHandler implements Runnable { private Socket clientSocket;
private String clientIp;
private int clientPort; public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
this.clientIp = clientSocket.getInetAddress().getHostAddress();
this.clientPort = clientSocket.getPort();
} @Override
public void run() {
try { // 协商认证方法
negotiationCertificationMethod(); // 开始处理客户端的命令
handleClientCommand(); } catch (Exception e) {
handleLog("exception, " + e.getMessage());
} finally {
close(clientSocket);
handleLog("client dead, current client count=%s", clientNumCount.decrementAndGet());
}
} // 协商与客户端的认证方法
private void negotiationCertificationMethod() throws IOException {
InputStream is = clientSocket.getInputStream();
OutputStream os = clientSocket.getOutputStream();
byte[] buff = new byte[255];
// 接收客户端的支持的方法
is.read(buff, 0, 2);
int version = buff[0];
int methodNum = buff[1]; if (version != VERSION) {
throw new RuntimeException("version must 0X05");
} else if (methodNum < 1) {
throw new RuntimeException("method num must gt 0");
} is.read(buff, 0, methodNum);
List<METHOD> clientSupportMethodList = METHOD.convertToMethod(Arrays.copyOfRange(buff, 0, methodNum));
handleLog("version=%s, methodNum=%s, clientSupportMethodList=%s", version, methodNum, clientSupportMethodList); // 向客户端发送回应,这里不进行认证
buff[0] = VERSION;
buff[1] = METHOD.NO_AUTHENTICATION_REQUIRED.rangeStart;
os.write(buff, 0, 2);
os.flush();
} // 认证通过,开始处理客户端发送过来的指令
private void handleClientCommand() throws IOException {
InputStream is = clientSocket.getInputStream();
OutputStream os = clientSocket.getOutputStream();
byte[] buff = new byte[255];
// 接收客户端命令
is.read(buff, 0, 4);
int version = buff[0];
COMMAND command = COMMAND.convertToCmd(buff[1]);
int rsv = buff[2];
ADDRESS_TYPE addressType = ADDRESS_TYPE.convertToAddressType(buff[3]);
if (rsv != RSV) {
throw new RuntimeException("RSV must 0X05");
} else if (version != VERSION) {
throw new RuntimeException("VERSION must 0X05");
} else if (command == null) {
// 不支持的命令
sendCommandResponse(COMMAND_STATUS.COMMAND_NOT_SUPPORTED);
handleLog("not supported command");
return;
} else if (addressType == null) {
// 不支持的地址类型
sendCommandResponse(COMMAND_STATUS.ADDRESS_TYPE_NOT_SUPPORTED);
handleLog("address type not supported");
return;
} String targetAddress = "";
switch (addressType) {
case DOMAIN:
// 如果是域名的话第一个字节表示域名的长度为n,紧接着n个字节表示域名
is.read(buff, 0, 1);
int domainLength = buff[0];
is.read(buff, 0, domainLength);
targetAddress = new String(Arrays.copyOfRange(buff, 0, domainLength));
break;
case IPV4:
// 如果是ipv4的话使用固定的4个字节表示地址
is.read(buff, 0, 4);
targetAddress = ipAddressBytesToString(buff);
break;
case IPV6:
throw new RuntimeException("not support ipv6.");
} is.read(buff, 0, 2);
int targetPort = ((buff[0] & 0XFF) << 8) | (buff[1] & 0XFF); StringBuilder msg = new StringBuilder();
msg.append("version=").append(version).append(", cmd=").append(command.name())
.append(", addressType=").append(addressType.name())
.append(", domain=").append(targetAddress).append(", port=").append(targetPort);
handleLog(msg.toString()); // 响应客户端发送的命令,暂时只实现CONNECT命令
switch (command) {
case CONNECT:
handleConnectCommand(targetAddress, targetPort);
case BIND:
throw new RuntimeException("not support command BIND");
case UDP_ASSOCIATE:
throw new RuntimeException("not support command UDP_ASSOCIATE");
} } // convert ip address from 4 byte to string
private String ipAddressBytesToString(byte[] ipAddressBytes) {
// first convert to int avoid negative
return (ipAddressBytes[0] & 0XFF) + "." + (ipAddressBytes[1] & 0XFF) + "." + (ipAddressBytes[2] & 0XFF) + "." + (ipAddressBytes[3] & 0XFF);
} // 处理CONNECT命令
private void handleConnectCommand(String targetAddress, int targetPort) throws IOException {
Socket targetSocket = null;
try {
targetSocket = new Socket(targetAddress, targetPort);
} catch (IOException e) {
sendCommandResponse(COMMAND_STATUS.GENERAL_SOCKS_SERVER_FAILURE);
return;
}
sendCommandResponse(COMMAND_STATUS.SUCCEEDED);
new SocketForwarding(clientSocket, targetSocket).start();
} private void sendCommandResponse(COMMAND_STATUS commandStatus) throws IOException {
OutputStream os = clientSocket.getOutputStream();
os.write(buildCommandResponse(commandStatus.rangeStart));
os.flush();
} private byte[] buildCommandResponse(byte commandStatusCode) {
ByteBuffer payload = ByteBuffer.allocate(100);
payload.put(VERSION);
payload.put(commandStatusCode);
payload.put(RSV);
// payload.put(ADDRESS_TYPE.IPV4.value);
// payload.put(SERVER_IP_ADDRESS.getBytes());
payload.put(ADDRESS_TYPE.DOMAIN.value);
byte[] addressBytes = SERVER_IP_ADDRESS.getBytes();
payload.put((byte) addressBytes.length);
payload.put(addressBytes);
payload.put((byte) (((SERVICE_LISTENER_PORT & 0XFF00) >> 8)));
payload.put((byte) (SERVICE_LISTENER_PORT & 0XFF));
byte[] payloadBytes = new byte[payload.position()];
payload.flip();
payload.get(payloadBytes);
return payloadBytes;
} private void handleLog(String format, Object... args) {
log("handle, clientIp=" + clientIp + ", port=" + clientPort + ", " + format, args);
} } // 用来连接客户端和目标服务器转发流量
public static class SocketForwarding { // 客户端socket
private Socket clientSocket;
private String clientIp;
// 目标地址socket
private Socket targetSocket;
private String targetAddress;
private int targetPort; public SocketForwarding(Socket clientSocket, Socket targetSocket) {
this.clientSocket = clientSocket;
this.clientIp = clientSocket.getInetAddress().getHostAddress();
this.targetSocket = targetSocket;
this.targetAddress = targetSocket.getInetAddress().getHostAddress();
this.targetPort = targetSocket.getPort();
} public void start() {
OutputStream clientOs = null;
InputStream clientIs = null;
InputStream targetIs = null;
OutputStream targetOs = null;
long start = System.currentTimeMillis();
try { clientOs = clientSocket.getOutputStream();
clientIs = clientSocket.getInputStream();
targetOs = targetSocket.getOutputStream();
targetIs = targetSocket.getInputStream(); // 512K,因为会有很多个线程同时申请buff空间,所以不要太大以以防OOM
byte[] buff = new byte[1024 * 512];
while (true) { boolean needSleep = true;
while (clientIs.available() != 0) {
int n = clientIs.read(buff);
targetOs.write(buff, 0, n);
transientLog("client to remote, bytes=%d", n);
needSleep = false;
} while (targetIs.available() != 0) {
int n = targetIs.read(buff);
clientOs.write(buff, 0, n);
transientLog("remote to client, bytes=%d", n);
needSleep = false;
} if (clientSocket.isClosed()) {
transientLog("client closed");
break;
} // 会话最多30秒超时,防止有人占着线程老不释放
if (System.currentTimeMillis() - start > 30_000) {
transientLog("time out");
break;
} // 如果本次循环没有数据传输,说明管道现在不繁忙,应该休息一下把资源让给别的线程
if (needSleep) {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
} catch (IOException e) {
transientLog("conn exception" + e.getMessage());
} finally {
close(clientIs);
close(clientOs);
close(targetIs);
close(targetOs);
close(clientSocket);
close(targetSocket);
}
transientLog("done.");
} private void transientLog(String format, Object... args) {
log("forwarding, clientIp=" + clientIp + ", targetAddress=" + targetAddress + ", port=" + targetPort + ", " + format, args);
} } // 客户端认证方法
public static enum METHOD { NO_AUTHENTICATION_REQUIRED((byte) 0X00, (byte) 0X00, "NO AUTHENTICATION REQUIRED"),
GSSAPI((byte) 0X01, (byte) 0X01, "GSSAPI"),
USERNAME_PASSWORD((byte) 0X02, (byte) 0X02, " USERNAME/PASSWORD"),
IANA_ASSIGNED((byte) 0X03, (byte) 0X07, "IANA ASSIGNED"),
RESERVED_FOR_PRIVATE_METHODS((byte) 0X80, (byte) 0XFE, "RESERVED FOR PRIVATE METHODS"),
NO_ACCEPTABLE_METHODS((byte) 0XFF, (byte) 0XFF, "NO ACCEPTABLE METHODS"); private byte rangeStart;
private byte rangeEnd;
private String description; METHOD(byte rangeStart, byte rangeEnd, String description) {
this.rangeStart = rangeStart;
this.rangeEnd = rangeEnd;
this.description = description;
} public boolean isMe(byte value) {
return value >= rangeStart && value <= rangeEnd;
} public static List<METHOD> convertToMethod(byte[] methodValues) {
List<METHOD> methodList = new ArrayList<>();
for (byte b : methodValues) {
for (METHOD method : METHOD.values()) {
if (method.isMe(b)) {
methodList.add(method);
break;
}
}
}
return methodList;
} } // 客户端命令
public static enum COMMAND {
CONNECT((byte) 0X01, "CONNECT"),
BIND((byte) 0X02, "BIND"),
UDP_ASSOCIATE((byte) 0X03, "UDP ASSOCIATE"); byte value;
String description; COMMAND(byte value, String description) {
this.value = value;
this.description = description;
} public static COMMAND convertToCmd(byte value) {
for (COMMAND cmd : COMMAND.values()) {
if (cmd.value == value) {
return cmd;
}
}
return null;
} } // 要请求的地址类型
public static enum ADDRESS_TYPE {
IPV4((byte) 0X01, "the address is a version-4 IP address, with a length of 4 octets"),
DOMAIN((byte) 0X03, "the address field contains a fully-qualified domain name. The first\n" +
" octet of the address field contains the number of octets of name that\n" +
" follow, there is no terminating NUL octet."),
IPV6((byte) 0X04, "the address is a version-6 IP address, with a length of 16 octets.");
byte value;
String description; ADDRESS_TYPE(byte value, String description) {
this.value = value;
this.description = description;
} public static ADDRESS_TYPE convertToAddressType(byte value) {
for (ADDRESS_TYPE addressType : ADDRESS_TYPE.values()) {
if (addressType.value == value) {
return addressType;
}
}
return null;
} } // 对于命令的处理结果
public static enum COMMAND_STATUS {
SUCCEEDED((byte) 0X00, (byte) 0X00, "succeeded"),
GENERAL_SOCKS_SERVER_FAILURE((byte) 0X01, (byte) 0X01, "general SOCKS server failure"),
CONNECTION_NOT_ALLOWED_BY_RULESET((byte) 0X02, (byte) 0X02, "connection not allowed by ruleset"),
NETWORK_UNREACHABLE((byte) 0X03, (byte) 0X03, "Network unreachable"),
HOST_UNREACHABLE((byte) 0X04, (byte) 0X04, "Host unreachable"),
CONNECTION_REFUSED((byte) 0X05, (byte) 0X05, "Connection refused"),
TTL_EXPIRED((byte) 0X06, (byte) 0X06, "TTL expired"),
COMMAND_NOT_SUPPORTED((byte) 0X07, (byte) 0X07, "Command not supported"),
ADDRESS_TYPE_NOT_SUPPORTED((byte) 0X08, (byte) 0X08, "Address type not supported"),
UNASSIGNED((byte) 0X09, (byte) 0XFF, "unassigned"); private byte rangeStart;
private byte rangeEnd;
private String description; COMMAND_STATUS(byte rangeStart, byte rangeEnd, String description) {
this.rangeStart = rangeStart;
this.rangeEnd = rangeEnd;
this.description = description;
} } private synchronized static void log(String format, Object... args) {
System.out.println(String.format(format, args));
} private static void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(SERVICE_LISTENER_PORT);
while (true) {
Socket socket = serverSocket.accept();
if (clientNumCount.get() >= MAX_CLIENT_NUM) {
log("client num run out.");
continue;
}
log("new client, ip=%s:%d, current client count=%s", socket.getInetAddress(), socket.getPort(), clientNumCount.get());
clientNumCount.incrementAndGet();
new Thread(new ClientHandler(socket), "client-handler-" + UUID.randomUUID().toString()).start();
} } }

验证一下开发的这个代理服务器究竟能不能用呢?

这里使用一台国外拨号主机做实验,将自己写的代理服务器部署上去,新建一个Java文件将上面的代码粘贴进去删除第一行的package信息然后编译运行:

javac Socks5ProxyServer.java
java Socks5ProxyServer

在Chrome浏览器的SwitchyOmega中新增一个情景模式,配置刚刚启动的socks5代理:

然后使用这个情景模式打开百度,查看自己当前的ip:

查看启动的服务器控制台打印信息:

三、使用socks5需要注意的坑

在客户端访问域名的时候,涉及到一个问题,这个域名是应该是客户端解析完告诉代理服务器ip还是应该把域名交给代理服务器去解析?

一般客户端的默认行为是域名在客户端解析,然后再将解析出来的ip拿给代理服务器去处理,但是对于一些网站来说通过ip访问是不成功的,比如google的dns解析,国内机器解析到的ip可能已经被污染,笔者实验发现对于在阿里云服务器访问google解析到的ip 74.86.151.162:443,通过ip访问是不能成功的,而在微软云机器上解析到的ip 172.217.161.164:443,通过ip可以访问成功,将域名交给代理服务器,代理服务器在美国,解析到的ip是能够访问成功的,所以在使用socks5的时候最好能够指明域名是在本地解析还是在代理服务器解析:

默认是在本地解析完将ip传给代理服务器:

curl --socks5 "23.225.xxx.xxx:10086" https://www.google.com

通过socks5-hostname指定域名交给代理服务器解析

curl --socks5-hostname "23.225.xxx.xxx:10086" "https://www.google.com" 

curl中关于这部分的说明:

--socks5-hostname <host[:port]>
Use the specified SOCKS5 proxy (and let the proxy resolve the host name). If the port number is not specified, it is assumed at port 1080. (Added in 7.18.0) This option overrides any previous use of -x, --proxy, as they are mutually exclusive. Since 7.21.7, this option is superfluous since you can specify a socks5 hostname proxy with -x, --proxy using a socks5h:// protocol prefix. If this option is used several times, the last one will be used. (This option was previously wrongly documented and used as --socks without the number appended.) --socks5 <host[:port]>
Use the specified SOCKS5 proxy - but resolve the host name locally. If the port number is not specified, it is assumed at port 1080. This option overrides any previous use of -x, --proxy, as they are mutually exclusive. Since 7.21.7, this option is superfluous since you can specify a socks5 proxy with -x, --proxy using a socks5:// protocol prefix. If this option is used several times, the last one will be used. (This option was previously wrongly documented and used as --socks without the number appended.) This option (as well as --socks4) does not work with IPV6, FTPS or LDAP.

Chrome浏览器的SwitchyOmega的socks5代理是会将域名传给代理服务器解析。

相关资料:

1. SOCKS5 - 维基百科

2. rfc1928

.

自己动手开发Socks5代理服务器的更多相关文章

  1. 在Centos7下搭建Socks5代理服务器

    在Centos7下搭建Socks5代理服务器 http://blog.51cto.com/quliren/2052776   采用socks协议的代理服务器就是SOCKS服务器,是一种通用的代理服务器 ...

  2. CentOS下配置SS5(SOCKS5)代理服务器

    方案:使用开源的SS5( Socks Server 5 ) 官网:http://ss5.sourceforge.net/ (点击左侧的Software在右侧的Server处进入下载地址) CentOs ...

  3. atitit.自己动手开发编译器and解释器(2) ------语法分析,语义分析,代码生成--attilax总结

    atitit.自己动手开发编译器and解释器(2) ------语法分析,语义分析,代码生成--attilax总结 1. 建立AST 抽象语法树 Abstract Syntax Tree,AST) 1 ...

  4. atitit.自己动手开发编译器and解释器(1) ------词法分析--attilax总结

    atitit.自己动手开发编译器and解释器(1) ------词法分析--attilax总结 1.   应用场景:::DSL 大大提升开发效率 1 2. 2. 流程如下::: 词法分析(生成toke ...

  5. 【nodejs代理服务器一】nodejs http-proxy 开发反向代理服务器,防火墙,过滤常见的web渗透

    事出有因 最近web系统引来了黑客的攻击,经常被扫描,各种漏洞尝试. 分析攻击日志,有几种常见的攻击手段: 上传webshell 远程执行命令漏洞 sql注入 xxs 攻击 试探各种开源框架爆出来的漏 ...

  6. 自己动手开发手机APP控制西门子200smart 教程(原创干货)

    自己动手开发手机APP控制西门子200smart 教程(原创干货)   自己动手开发手机APP控制西门子200smart 教程(原创干货) 2020-02-09 19:06:45 自己动手开发手机AP ...

  7. 自己动手开发jQuery插件

    因为工作需要,所以这几天琢磨了一下关于jQuery插件开发的问题,经过一天鏖战,终于完成自己动手做的第一个jQuery插件,对于俺这种见了css就蛋疼菊紧的人来说,一天时间8小时,保守估计有5个小时在 ...

  8. 2015英特尔® 实感™ (Intel® RealSense™) 动手开发实验课

    2015年英特尔® 全球实感技术动手实验课路演来到中国, 这次在中国将有北京和广州两站,包括一天的动手实验室活动 - 面向对感知计算.3D 开发和虚拟现实兴趣浓厚的开发人员.英特尔专家将会指导您如何借 ...

  9. Ubuntu 12.04 安装socks5代理服务器dante-server

    dante-server是一个很好的socks4/5代理服务器软件. 使用apt-get安装   1 apt-getinstall dante-server 添加一个用户   1 2 useradd ...

随机推荐

  1. Daily Scrumming* 2015.12.12(Day 4)

    一.团队scrum meeting照片 二.今日总结 姓名 WorkItem ID 工作内容 签入链接以及备注说明  江昊 任务1036 进行界面开发,明日准备开发第一个界面,社团展示界面 工作暂未完 ...

  2. Fibbing以让虚结点的设置更简单为目的优化网络需求

  3. Daily Scrum - 11/25

    今天是Sprint 2的最后一天,我们在下午的课上对之前两个Sprint作了比较详尽的Review,并在课后Daily Scrum上讨论制订了Sprint 3的任务安排.具体Task会在明天更新在TF ...

  4. RYU 灭龙战 fourth day (1)

    RYU 灭龙战 fourth day (1) 前言 对于流量的监控,对于一个网络管理人员来说是非常重要的,可以从可视化的角度,方便检测出哪里的设备出了问题:而在传统网络中,如果是哪里的设备出了问题的话 ...

  5. Alpha版本冲刺(十)

    目录 组员情况 组员1(组长):胡绪佩 组员2:胡青元 组员3:庄卉 组员4:家灿 组员5:凯琳 组员6:翟丹丹 组员7:何家伟 组员8:政演 组员9:黄鸿杰 组员10:刘一好 组员11:何宇恒 展示 ...

  6. win8以上windows系统eclipse环境下图片显示乱码问题解决

    相信升级了win10系统的诸多安卓开发者在用eclipse时会发现一个很不爽的地方,就是原本win7环境下能正常打开的图片文件现在成了一页乱码,我曾多次碰到这个问题,在网上也很难找到行之有效的具体解决 ...

  7. 作业四 任务分解(WBS)

    近日忙于实验,未来得及完成任务分解昨晚召开了紧急会议,才确定了任务划分.主体分配如下:三名编程人员,一个主编两个辅编,一人做需求分析,一人做程序测试,一人专司文档. 具体细节如下:在剩余的三周左右的时 ...

  8. C# winform打开文件夹并选中指定文件

    例如:打开“E:\Training”文件夹并选中“20131250.html”文件 System.Diagnostics.Process.Start("Explorer.exe", ...

  9. ElasticSearch 2 (13) - 深入搜索系列之结构化搜索

    ElasticSearch 2 (13) - 深入搜索系列之结构化搜索 摘要 结构化查询指的是查询那些具有内在结构的数据,比如日期.时间.数字都是结构化的.它们都有精确的格式,我们可以对这些数据进行逻 ...

  10. Solr查询语法

    基于solr版本:6.0.0 当配置好本地的环境之后,就访问http://localhost:8080/solr/index.html.或者是访问已经放在服务器上的solr环境,例如http://10 ...