N_Netty 学习笔记

自改梯子 (自用)

参考项目 - https://github.com/ZhangJiupeng/AgentX/tree/master/src/main/java/cc/agentx

客户端

基于 Netty

入口

Configuration config = Configuration.INSTANCE;
InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE);
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();
try {
	ServerBootstrap bootstrap = new ServerBootstrap();
	bootstrap.group(bossGroup, workerGroup)
			.channel(NioServerSocketChannel.class)
			.childHandler(new ChannelInitializer<SocketChannel>() {
				@Override
				protected void initChannel(SocketChannel socketChannel) throws Exception {
					socketChannel.pipeline()
							.addLast("logging", new LoggingHandler(LogLevel.DEBUG))
							.addLast(new SocksInitRequestDecoder())
							.addLast(new SocksMessageEncoder())
							.addLast(new Socks5Handler())
							.addLast(Status.TRAFFIC_HANDLER);
				}
			});
	log.info("\tStartup {}-{}-client [{}{}]", Constants.APP_NAME, Constants.APP_VERSION, config.getMode(), config.getMode().equals("socks5") ? "": ":" + config.getProtocol());
	new Thread(() -> new UdpServer().start()).start();
	ChannelFuture future = bootstrap.bind(config.getLocalHost(), config.getLocalPort()).sync();
	future.addListener(future1 -> log.info("\tTCP listening at {}:{}...", config.getLocalHost(), config.getLocalPort()));
	future.channel().closeFuture().sync();
} catch (Exception e) {
	log.error("\tSocket bind failure ({})", e.getMessage());
} finally {
	log.info("\tShutting down");
	bossGroup.shutdownGracefully();
	workerGroup.shutdownGracefully();
}

启动代码

添加的几个Handler

netty 中已经内置了socks5协议的编解码, 只需要实现逻辑处理 Handler就行, 下面是几个主要的编解码codec:

1.SocksInitRequestDecoder SocksInitRequestDecoder 是一个 ChannelInboundHandlerAdapter, 即入站处理器, 用于对socks的请求进行解码.

2.SocksMessageEncoder SocksMessageEncoder 是一个 ChannelOutboundHandlerAdapter, 即出站处理器, 有了它, 我们可以很开心的在channel.write()里直接传入一个对象, 而无需自己去写buffer了.

简而言之, 在pipeline最后部加了’解码器’和’编码器’

protected void initChannel(SocketChannel socketChannel) throws Exception {
    pipeline = socketChannel.pipeline();
    pipeline.addLast("decoder", new MyProtocolDecoder());
    pipeline.addLast("encoder", new MyProtocolEncoder());
 

3.Socks5Handler

Socks5Handler 是处理S5的重点交互入口, netty 已经封装好s5的请求对象 SocksRequest

Socks5Handler

@Override
public void channelRead0(ChannelHandlerContext ctx, SocksRequest request) throws Exception {
	//一些其他CMD 请求 忽略之
 ...
    case SOCKS5:
        switch (request.requestType()) {
        case CONNECT:
            //其他都是处理标准socket5的  数据交换 关键这里 
            ctx.pipeline().addLast(new XClientLocalHandler()); // handover
            ctx.pipeline().remove(this);//移除自己(Socks5Handler) 交给 XClientLocalHandler
            ctx.fireChannelRead(request);
            break;
        default:
            ctx.close();
            log.warn("\tBad Handshake! (command not support: {})", ((SocksCmdRequest) request).cmdType());
 
    ....
    }

4.Status.TRAFFIC_HANDLER 流量统计

XClientLocalHandler

XClientLocalHandler

 
	@Override
	public void channelRead0(final ChannelHandlerContext ctx, final SocksCmdRequest request) throws Exception {
		/**
		 * 是否需要代理
		 * 目前没有用, 除了 localhost 等, 总是为 true 
		 */
		final boolean proxyMode = isAgentXNeeded(request.host());
		log.warn("客户端请求 {}:{} 处理方式 [{}]", request.host(), request.port(), 
				proxyMode ? "AGENTX": "DIRECT");
		/**
		 * 这个给下面连接的 XClientProxyHandler 回调, 然后双方搭桥, 数据加密 解密
            连接过了一手 XClientProxyHandler
		 */
		Promise<Channel> promise = ctx.executor().newPromise();
		promise.addListener((Future<Channel> future) -> {
			final Channel outboundChannel = future.getNow();
			if (future.isSuccess()) {
				//响应 CMD 成功
				ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.SUCCESS, request.addressType()))
						.addListener(channelFuture -> {
							if (!ctx.channel().isActive()) {
								//坑, 还要判断下本地
								outboundChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
								return;
							}
							/**
							 * 搭桥 (outboundChannel 是代理服务器(如果需要代理))
							 */
							outboundChannel.pipeline()
									.addLast(new XRelayHandler(ctx.channel(), proxyMode ? wrapper: rawWrapper, false));
							/**
							 * 搭桥 (ctx是本机回环 代理连接)
							 */
							ctx.pipeline().addLast(
									new XRelayHandler(outboundChannel, proxyMode ? wrapper: rawWrapper, true));
						});
			} else {
				//响应 CMD 失败, 本地关闭
				ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.FAILURE, request.addressType()));
				if (ctx.channel().isActive()) {
					ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
				}
			}
		});
        //提取到目标主机
		String host = request.host();
		int port = request.port();
		if (host.equals(config.getConsoleDomain())) {
			host = "localhost";
			port = config.getConsolePort();
		} else if (proxyMode) {
			//为代理主机, 真正数据包装在 xRequestBytes 给代理服务端
			host = config.getServerHost();
			port = config.getServerPort();
		}
		final String poxyServer = host;
		final int poxyPort = port;
		final  XClientProxyHandler  clientProxyHandler  = new XClientProxyHandler(ctx.channel(), promise, wrapper, proxyMode) ;
		//socket5 数据包 
		ByteBuf byteBuf = Unpooled.buffer();
		request.encodeAsByteBuf(byteBuf);
		final byte[] xRequestBytes = new byte[byteBuf.readableBytes()];
		byteBuf.getBytes(0, xRequestBytes);
        //netty 去连接 服务端
		//ReferenceCountUtil.retain(request); // 释放消息 auto-release? a trap?
		bootstrap.group(ctx.channel().eventLoop()).channel(NioSocketChannel.class)
				.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000).option(ChannelOption.SO_KEEPALIVE, true)
                //注意 这里过了一手  XClientProxyHandler
				.handler(clientProxyHandler ).connect(host, port)
				.addListener(new ChannelFutureListener() {
					@Override
					public void operationComplete(ChannelFuture future) throws Exception {
						if (future.isSuccess()) {
							log.info("连接代理or服务器 {}:{} (成功", poxyServer, poxyPort);
//							if (localRequest.cmdType() == SocksCmdType.UDP)//TODO UDP 实现
							if (proxyMode) {
								/*
								xRequestBytes是原始s5头部数据  握手包伪装, 伪装成http请求
								**/
								byte[] req = clientProxyHandler.getRequestResolver().wrap(xRequestBytes);
								future.channel().writeAndFlush(Unpooled.wrappedBuffer(req));
							}else {
								//wtf?
								ctx.writeAndFlush(Unpooled.EMPTY_BUFFER);
							}
						}else{
							log.warn("连接代理or服务器 {}:{} (失败", poxyServer, poxyPort);
							// 响应本地 s5 关闭回环
							ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.FAILURE, request.addressType()));
							if (ctx.channel().isActive()) {
								ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
							}
						}
					}
				});
	}

XClientLocalHandler

XClientLocalHandler

 
	@Override
	public void channelRead0(final ChannelHandlerContext ctx, final SocksCmdRequest request) throws Exception {
		/**
		 * 是否需要代理
		 * 目前没有用, 除了 localhost 等, 总是为 true 
		 */
		final boolean proxyMode = isAgentXNeeded(request.host());
		log.warn("客户端请求 {}:{} 处理方式 [{}]", request.host(), request.port(), 
				proxyMode ? "AGENTX": "DIRECT");
		/**
		 * 这个给下面连接的 XClientProxyHandler 回调, 然后双方搭桥, 数据加密 解密
            连接过了一手 XClientProxyHandler
		 */
		Promise<Channel> promise = ctx.executor().newPromise();
		promise.addListener((Future<Channel> future) -> {
			final Channel outboundChannel = future.getNow();
			if (future.isSuccess()) {
				ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.SUCCESS, request.addressType()))
						.addListener(channelFuture -> {
							if (!ctx.channel().isActive()) {
								//要判断本地
								outboundChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
								return;
							}
							/**
							 * 搭桥 (outboundChannel 是代理服务器(如果需要代理))
							 */
							outboundChannel.pipeline()
									.addLast(new XRelayHandler(ctx.channel(), proxyMode ? wrapper: rawWrapper, false));
							/**
							 * 搭桥 (ctx是本机回环 代理连接)
							 */
							ctx.pipeline().addLast(
									new XRelayHandler(outboundChannel, proxyMode ? wrapper: rawWrapper, true));
						});
			} else {
				// 告知本地 关闭
				ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.FAILURE, request.addressType()));
				if (ctx.channel().isActive()) {
					ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
				}
			}
		});
        //提取到目标主机
		String host = request.host();
		int port = request.port();
		if (host.equals(config.getConsoleDomain())) {
			host = "localhost";
			port = config.getConsolePort();
		} else if (proxyMode) {
			host = config.getServerHost();
			port = config.getServerPort();
		}
		final String poxyServer = host;
		final int poxyPort = port;
		final  XClientProxyHandler  clientProxyHandler  = new XClientProxyHandler(ctx.channel(), promise, wrapper, proxyMode) ;
		//socket5 数据 包起来
		ByteBuf byteBuf = Unpooled.buffer();
		request.encodeAsByteBuf(byteBuf);
		final byte[] xRequestBytes = new byte[byteBuf.readableBytes()];
		byteBuf.getBytes(0, xRequestBytes);
        //netty 去连接 服务端
		//ReferenceCountUtil.retain(request); // 释放消息 auto-release? a trap?
		bootstrap.group(ctx.channel().eventLoop()).channel(NioSocketChannel.class)
				.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000).option(ChannelOption.SO_KEEPALIVE, true)
                //注意 这里过了一手  XClientProxyHandler
				.handler(clientProxyHandler ).connect(host, port)
				.addListener(new ChannelFutureListener() {
					@Override
					public void operationComplete(ChannelFuture future) throws Exception {
						if (future.isSuccess()) {
							log.info("连接代理or服务器 {}:{} (成功", poxyServer, poxyPort);
//							if (localRequest.cmdType() == SocksCmdType.UDP)//TODO UDP 实现
							if (proxyMode) {
								/*
                                握手包伪装, 伪装成http请求
                               **/
								byte[] req = clientProxyHandler.getRequestResolver().wrap(xRequestBytes);
								future.channel().writeAndFlush(Unpooled.wrappedBuffer(req));
							}else {
								//wtf?
								ctx.writeAndFlush(Unpooled.EMPTY_BUFFER);
							}
						}else{
							log.warn("连接代理or服务器 {}:{} (失败", poxyServer, poxyPort);
							// 响应本地 s5 关闭回环
							ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.FAILURE, request.addressType()));
							if (ctx.channel().isActive()) {
								ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
							}
						}
					}
				});
	}

关于socket5 协议

RFC 1928 - SOCKS 5 协议中文文档「译」

Netty实现shadowsocks客户端

握手

客户端

客户端连接到 SOCKS 服务端,发送的协议版本与认证方法数据包格式(3字节):

版本方法数目方法列表
111 to 255

方法列表如下

  • X’00‘  无需认证
  • X’01‘  GSSAPI
  • X’02‘  用户名/密码
  • X’03‘ 到 X’7F’  IANA 指定
  • X’80‘ 到 X’FE’  为私有方法保留
  • X’FF‘  无可接受方法
服务端

服务端要从给定的方法列表中选择一个方法并返回选择报文(2字节)

版本方法
11
如果 METHOD (方法)字段为 X’FF‘, 表示方法列表中的所有方法均不可用,客户端收到此信息必须关闭连接。

目前已定义方法如下:

  • 0x00: 不需要认证
  • 0x01: GSSAPI认证
  • 0x02: 用户名和密码方式认证
  • 0x03: IANA认证
  • 0x80-0xfe: 保留的认证方式
  • 0xff: 不支持任何认证方式

请求

https://www.quarkay.com/code/383/socks5-protocol-rfc-chinese-traslation#content-header-6

客户端
版本命令保留字段地址类型目标地址目标端口
11X‘00’1Variable2

字段含义:

  • VER 协议版本: X‘05’

  • CMD 命令

    • CONNECT 连接, X‘01’
    • BIND 监听X‘02’
    • UDP ASSOCIATE UDP关联 X‘03’
  • RSV 保留字段

  • ATYP 地址类型

    • IPV4 X‘01’
    • 域名 X‘03’
    • IPV6 X‘04’
  • DST.ADDR 目标地址

  • DST.PORT 目标端口 (网络字节序)

SOCKS 服务端会根据请求类型和源、目标地址,执行对应操作,并且返回对应的一个或多个报文信息。

ATYP 地址字段具体含义由地址类型字段( ATYP )决定,具体对应关系如下:

  • X‘01’
    • 表明地址字段为一个 IPV4 地址,长度为 4 个字节
  • X‘03’
    • 表明地址字段为一个(合法的)域名,且第一个字节为域名长度标识,(显然)其不以 NULL 作为结束标识
  • X‘04’
    • 表明地址字段为一个 IPV6 地址,长度为 16 个字节
服务端

客户端与服务端建立连接并完成认证之后就会发送请求信息,服务端执行对应请求并返回如下格式的报文:

版本回复(类型)保留字段地址类型地址端口
11X‘00’1Variable2

字段细节:

  • VER协议版本: X‘05’

  • REP 回复字段(回复类型):

    • X‘00’ 成功
    • X‘01’ 常规 SOCKS 服务故障
    • X‘02’ 规则不允许的连接
    • X‘03’ 网络不可达
    • X‘04’ 主机无法访问
    • X‘05’ 拒绝连接
    • X‘06’ 连接超时
    • X‘07’ 不支持的命令
    • X‘08’ 不支持的地址类型
    • X‘09’ 到 X’FF’ 未定义
  • RSV 保留字段

  • ATYP 地址类型

    • IPV4 X‘01’
    • 域名 X‘03’
    • IPV6 X‘04’
  • BND.ADDR 服务端绑定地址

  • BND.PORT 服务端绑定端口 (网络字节序)

其中,标记为保留字段( RSV )的值必须设定为 X‘00’ 。

如果协商的方法为了完整性、可信性的校验需要封装数据包,则返回的数据包也会进行对应的封装。

CMD - 连接 (CONNECT)

https://www.quarkay.com/code/383/socks5-protocol-rfc-chinese-traslation#content-header-9

其连接请求抓包

  • CMD-连接 请求
版本命令保留字段地址类型目标地址目标端口
11X‘00’1Variable2
05 01 00 03 11626561636f6e73352e677674332e636f6d 01bb
  • CMD-连接 响应
版本回复(类型)保留字段地址类型地址端口
11X‘00’1Variable2
05 00 00 03 0100 0000

字段细节:

  • VER协议版本: X‘05’
  • REP 回复字段(回复类型):
    • X‘00’ 成功
    • X‘01’ 常规 SOCKS 服务故障
    • X‘02’ 规则不允许的连接
    • X‘03’ 网络不可达
    • X‘04’ 主机无法访问
    • X‘05’ 拒绝连接
    • X‘06’ 连接超时
    • X‘07’ 不支持的命令
    • X‘08’ 不支持的地址类型
    • X‘09’ 到 X’FF’ 未定义
  • RSV 保留字段
  • ATYP 地址类型
    • IPV4 X‘01’
    • 域名 X‘03’
    • IPV6 X‘04’
  • BND.ADDR 服务端绑定地址
  • BND.PORT 服务端绑定端口 (网络字节序)

其中,标记为保留字段( RSV )的值必须设定为 X‘00’ 。

如果协商的方法为了完整性、可信性的校验需要封装数据包,则返回的数据包也会进行对应的封装。

对于连接请求,返回的报文里面 BND.PORT 为服务端用来连接目标地址所使用的端口,而 BND.ADDR 则包含了对应的 IP 地址。返回的 BND.ADDR 的值( IP 地址)一般与客户端连接的 SOCKS 服务端地址不同,因为此类服务往往是多台机器提供服务。 SOCKS 服务端会使用目标地址、端口和客户端源地址、源端口信息来执行连接请求(建立对应关系)。

Socket5 for Netty

netty提供了三个解码器和一个编码器帮助开发人员实现socks5协议的服务端的绝大多数功能。

编码器作用
io.netty.handler.codec.socksx.v5.Socks5ServerEncodersocks5协议交互过程中编码服务端给客户端的响应
解码器作用
io.netty.handler.codec.socksx.v5.Socks5InitialRequestDecoder版本和认证方式交互阶段解码客户端请求
io.netty.handler.codec.socksx.v5.Socks5PasswordAuthRequestDecoder认证交互阶段解码客户端认证请求
io.netty.handler.codec.socksx.v5.Socks5CommandRequestDecoder数据交互阶段解码客户端连接请求

这几个解码器解决了从抽象的协议请求到对象的转换;而编码器解决了对象到抽象的协议转换。所以这些编解码器只是解决了这些问题还是不够的,剩下的逻辑需要自己实现才行。所以对应着三个解码器,有三个后续的自定义的入栈处理器与其一一对应

处理器作用
Socks5InitialRequestInboundHandler响应版本和认证方式交互阶段客户端请求
Socks5PasswordAuthRequestInboundHandler响应认证交互阶段客户端认证请求
Socks5CommandRequestInboundHandler响应数据交互阶段客户端连接请求

服务端

XServerClientHandler

 
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            ByteBuf byteBuf = (ByteBuf) msg;
            if (!byteBuf.hasArray()) {
                byte[] bytes = new byte[byteBuf.readableBytes()];
                byteBuf.getBytes(0, bytes);
                if (!requestParsed) {//没有成功解析请求(就是第一次握手)
                    XRequest xRequest = null;
                	try {
                    	byte[] xRequestBytes = requestResolver.parse(bytes);
                        xRequest = new XRequest(xRequestBytes);
					} catch (Exception e) {
						e.printStackTrace();
						log.error(e);
					}
                    if (xRequest == null || xRequest.getAtyp() == XRequest.Type.UNKNOWN) {
                    	//未知 当嗅探请求 cut 掉
                    	log.warn("未知请求, 当嗅探请求");
                        int delay = KeyHelper.generateRandomInteger(2000, 5000);
                        try {
                            Thread.sleep(delay);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //响应嗅探请求的数据
            			ctx.writeAndFlush(Unpooled.wrappedBuffer(requestResolver.sniffingReponse() ) ).addListener(ChannelFutureListener.CLOSE);
            			return;
                    }
 
                    if (xRequest.getChannel() == XRequest.Channel.TCP) {
                    	/**
                    	 * TCP 类型
                    	 *   拿到 host port 下面去连真正的目标主机
                    	 */
                        String host = xRequest.getHost();
                        int port = xRequest.getPort();
                        log.info("客户端=>请求代理>目标 {}:{}{}", host, port, DnsCache.isCached(host) ? " [Cached]": "");
                        if (xRequest.getAtyp() == XRequest.Type.DOMAIN) {
                        	//如果域名类型, DNS缓存处理
                            try {
                                host = DnsCache.get(host);
                                if (host == null) {
                                    host = xRequest.getHost();
                                }
                            } catch (UnknownHostException e) {
                                log.warn("客户端<=代理  Bad DNS! ({})", e.getMessage());
                                ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
                                return;
                            }
                        }
                        final String finalHost = host;
                        /**
                         * 这个给下面连接的 XServerTargetHandler 回调, 然后双方搭桥, 数据加密 解密
                         连接过了一手 XServerTargetHandler
                         */
                        Promise<Channel> promise = ctx.executor().newPromise();
                        promise.addListener(
                                new FutureListener<Channel>() {
                                    @Override
                                    public void operationComplete(final Future<Channel> future) throws Exception {
                                        final Channel outboundChannel = future.getNow();
                                        if (future.isSuccess()) {
                                            ctx.pipeline().remove(XServerClientHandler.this);
                                            String uuidString =UUID.fastUUID().toString(); 
                                            log.debug("{} , uuid={}", finalHost, uuidString);
                                        	byte[]  uuid = uuidString.getBytes();
                                          	/**
                                        	 * 响应伪装
                                        	 */
                                        	ctx.writeAndFlush( Unpooled.wrappedBuffer(requestResolver.wrap(uuid))).addListener(new ChannelFutureListener() {
												@Override
												public void operationComplete(ChannelFuture future) throws Exception {
												    /*
		                                             * 建立中继
		                                             */
		                                        	/**
                                                    (outboundChannel 是目标主机)
		                                        	 * 在目标主机 ChannelPipeline 注册 客户端中继
		                                        	 */
		                                            outboundChannel.pipeline().addLast(new XRelayHandler(ctx.channel(), wrapper, false)); 
		                                            /**
                                                    (ctx 客户端)
		                                        	 * 在客户端 ChannelPipeline 注册 目标主机中继
		                                        	 */
		                                            ctx.pipeline().addLast(new XRelayHandler(outboundChannel, wrapper, true));
												}
                                        	});
                                        } else {
                                            if (ctx.channel().isActive()) {
                                                ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
                                            }
                                        }
                                    }
                                }
                        );
                        /**
                        去连接目标主机
                         */
                        log.debug("Proxy 连接目标 {}:{} ", finalHost, port);
                        bootstrap.group(ctx.channel().eventLoop())
                                .channel(NioSocketChannel.class)
                                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000)
                                .option(ChannelOption.SO_KEEPALIVE, true)
                                .handler(new XServerTargetHandler(promise, System.currentTimeMillis()))
                                .connect(host, port).addListener(new ChannelFutureListener() {
                            @Override
                            public void operationComplete(ChannelFuture future) throws Exception {
                                /**连接回调
                                 */
                                if (!future.isSuccess()) {
                                	log.info("代理=>连接目标 {}:{} (失败", finalHost, port);
                                    if (ctx.channel().isActive()) {
                                        log.warn("代理与 客户端断开?  Bad Ping! ({}:{})", finalHost, port);
                                        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
                                    }
                                }else {
                                	InetSocketAddress insocket = (InetSocketAddress) future.channel().remoteAddress();
                            		String clientIP = insocket.getAddress().getHostAddress();
                                	log.info("代理=>连接目标 {} (成功, ip={}", finalHost, clientIP);
                                }
                            }
                        });
 
                    } else if (xRequest.getChannel() == XRequest.Channel.UDP) {
                        //UDP 类型
                        InetSocketAddress udpTarget = new InetSocketAddress(xRequest.getHost(), xRequest.getPort());
                        XChannelMapper.putTcpChannel(udpTarget, ctx.channel());
 
                        ctx.pipeline().addLast(new Tcp2UdpHandler(udpTarget, requestResolver, wrapper)); // handover
                        ctx.pipeline().remove(this);
                        ctx.fireChannelRead(msg);
                        return;
                    }
 
                    requestParsed = true;
                } else {
                	//请求已经解析成功了, 还有数据, 但是未建立中继, 后续数据需要缓存(貌似无需处理, 客户端正常逻辑不会到达这里) 
                    log.error("代理 数据错误  [{} bytes], 字节{} {} {} {} {}", bytes.length, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4]);
                }
            }
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
 
 

添加 HTTP代理支持

Privoxy 找了一堆 s5 转 http proxy 都没卵用, 自己写!

Netty实现 简单Http代理转发

简单测试代码

//main
final int port = 8848;
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup(2);
try {
	ServerBootstrap b = new ServerBootstrap();
	b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
		.option(ChannelOption.SO_BACKLOG, 100)
		.option(ChannelOption.TCP_NODELAY, true)
		.handler(new LoggingHandler(LogLevel.INFO))
		.childHandler(new ChannelInitializer<Channel>() {
 
		@Override
		protected void initChannel(Channel ch) throws Exception {
			ch.pipeline().addLast("httpCodec", new HttpServerCodec());
			ch.pipeline().addLast("httpObject", new HttpObjectAggregator(6553600));
			ch.pipeline().addLast("serverHandle", new Handle());
		}
	});
	ChannelFuture f = b.bind(port).sync();
	f.channel().closeFuture().sync();
} catch (Exception e) {
	e.printStackTrace();
} finally {
	bossGroup.shutdownGracefully();
	workerGroup.shutdownGracefully();
}
 
 
//Handler
/**
 * 
 * @author yangfh 2020年7月18日
 */
public class Handle extends ChannelInboundHandlerAdapter {
	private static final Logger log = LoggerFactory.getLogger(Handle.class);
	private ChannelFuture cf;
	private String host;
	private int port = 80;
 
	@Override
	public void channelRead(final ChannelHandlerContext localCtx, final Object climsg) throws Exception {
		// 只做转发
		if (climsg instanceof FullHttpRequest) {
			FullHttpRequest request = (FullHttpRequest) climsg;
			if (request.uri().startsWith("https")) {
				port = 443;
			}
			host = request.headers().get("host");
			String [] reqHs = host.split(":");
			if(reqHs.length > 1) {
				host = reqHs[0];
				port = Integer.valueOf(reqHs[1]);
			}
			//如果是https 可以以此判断
			if ("CONNECT".equalsIgnoreCase(request.method().name())) {// HTTPS建立代理握手
				log.info("代理 CONNECT 请求");
				HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
				localCtx.writeAndFlush(response);
				localCtx.pipeline().remove("httpCodec");
				localCtx.pipeline().remove("httpObject");
				return;
			}
		}
		if (cf == null) {
			// 连接至目标服务器
			Bootstrap bootstrap = new Bootstrap();
			bootstrap.group(localCtx.channel().eventLoop()) // 复用客户端连接线程池
					.channel(localCtx.channel().getClass()) // 使用NioSocketChannel来作为连接用的channel类
					.handler(new ChannelInitializer() {
						@Override
						protected void initChannel(Channel ch) throws Exception {
                            //http 需要添加 HttpClientCodec 解码器/ 作用是将客户端的 FullHttpRequest 解码 发送到 目标服务器
                            // https 不需要(要去掉) 应该是在 CONNECT 流程中处理了
							ch.pipeline().addLast( new HttpClientCodec());
							ch.pipeline().addLast(new HttpObjectAggregator(65536));
							
							ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
								@Override
								public void channelRead(ChannelHandlerContext ctx0, Object msg) throws Exception {
									log.info("{} <-目标消息", msg);
									localCtx.channel().writeAndFlush(msg);
								}
							});
						}
					});
			cf = bootstrap.connect(host, port);
			cf.addListener(new ChannelFutureListener() {
				public void operationComplete(ChannelFuture future) throws Exception {
					if (future.isSuccess()) {
//						FullHttpRequest request = (FullHttpRequest) climsg;
						//TODO http 没有发送/ 需要给'目标' Channel 添加 HttpClientCodec
						future.channel().writeAndFlush(climsg);
						log.info("连接目标 {}:{} 成功", host, port);
					} else {
						log.info("连接目标 {}:{} 失败", host, port);
						localCtx.channel().close();
					}
				}
			});
		} else {
			log.info("客户端消息->", climsg);
			cf.channel().writeAndFlush(climsg);
		}
	}
 
}

另外

HttpObjectAggregator

当我们用POST方式请求服务器的时候, 对应的参数信息是保存在message body中的, 如果只是单纯的用HttpServerCodec是无法完全的解析Http POST请求的, 因为HttpServerCodec只能获取uri中参数, 所以需要加上HttpObjectAggregator.

注意!!! 这里 handler 不能继承 SimpleChannelInboundHandler 如果在channelRead 中写了ctx.write(转发接收到的内容),由于write是异步的, 在channelRead0 返回之后, 仍然没有完成! 为此, 扩展了 ChannelInboundHandlerAdapter, 其在这个时间点上不会释放消息, 消息在 channelReadComplete(), 当writeAndFlush方法被调用时释放

请求转 socket5 cmd 头

/************************************************/
//http proxy 代理请求封装 s5 头
 
String host = "www.baidu.com";
int port = 80;
byte[] hostByte = host.getBytes();
byte hostByteLen= (byte) hostByte.length;
 
byte[] head = new byte[200];
// 第1字节类型 ATYP_DOMAIN , 域名: 第2字节是域名长度, 第3, 4字节 端口 
head[0] = 0x03;
head[1] = hostByteLen;
//左右两边
head[hostByteLen+2] =  (byte) ((port>>8)&0xFF);
head[hostByteLen+3] =  (byte) ((port<<24)&0xFF);
...
ByteBuf byteBuf = Unpooled.buffer();
    			

问题来了 HttpServerCodec 解码 FullHttpRequest 是没有办法拿到原始byte数据的.., 可以使用io.netty.handler.codec.http.HttpRequestDecoder 解码http请求; 但是响应需要自己序列化, 还要维护 ByteBuf 内存回收..

最终方案

还是用 httpCodec 和 httpObject, 可以最后中继的时候移除它们!

int fromPort = localSocketChannel.localAddress().getPort();
if(fromPort == config.getLocalHttpProxyPort() ) {
	log.info("HTTP代理请求.");
	localSocketChannel.pipeline()
			.addLast("logging", new LoggingHandler(LogLevel.DEBUG))
			//http 仅编码
//	        .addLast("httpCodec", new HttpResponseEncoder())
			//http 解编码
			.addLast("httpCodec", new HttpServerCodec())
			.addLast("httpObject", new HttpObjectAggregator(65536))
			.addLast("xClient", new XClientLocalHttpProxyHandler())
			.addLast(Status.TRAFFIC_HANDLER);
	return ; 
}

问题

IllegalReferenceCountException 异常

An exception was thrown by cc.agentx.client.net.nio.http.XClientLocalHttpProxyHandler1$$Lambda6/135166056.operationComplete() io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1

这是因为Netty的引用计数器原因, 自从Netty 4开始, 对象的生命周期由它们的引用计数(reference counts) 管理, 而不是由垃圾收集器(garbage collector) 管理了.ByteBuf是最值得注意的, 它使用了引用计数来改进分配内存和释放内存的性能.

在我们创建ByteBuf对象后, 它的引用计数是1, 当你释放(release) 引用计数对象时, 它的引用计数减1, 如果引用计数为0, 这个引用计数对象会被释放(deallocate) , 并返回对象池. 当尝试访问引用计数为0的引用计数对象会抛出IllegalReferenceCountException异常

每次调用ctx.write/writeAndFlush, pipeline.write/writeAndFlush , 等一系列方法时, 被封装的 ByteBuf 对象的引用计数会减一, so 不用自己释放..

与本地回环交互 cmd UNKNOWN 问题

原因s5 cmd CONNECT 请求之后(即 LoggingHandler 第三次响应) 应答的 REP 是 0x1 普通的SOCKS服务器请求失败

S5服务端应答协议格式: 原因是 REP返回 0x00 即失败?

VER	REP	RSV	ATYP	BND.ADDR	BND.PORT
1	1	X’00’	1	Variable	2

一旦建立了一个到SOCKS服务器的连接, 并且完成了认证方式的协商过程, 客户机将会发送一个SOCKS请求信息给服务器.服务器将会根据请求, 以如下格式 其中各字段的含义如下: VER 协议版本: X’05’ REP 应答字段: X’00’ 成功 X’01’ 普通的SOCKS服务器请求失败 X’02’ 现有的规则不允许的连接 X’03’ 网络不可达 X’04’ 主机不可达 X’05’ 连接被拒 X’06’ TTL超时 X’07’ 不支持的命令 X’08’ 不支持的地址类型 X’09’ – X’FF’ 未定义 RSV 保留 (此字段必须设置为X’00’) ATYP 后面的地址类型 IPV4: X’01’ 域名: X’03’ IPV6: X’04’ BND.ADDR 服务器绑定的地址 BND.PORT 以网络字节顺序表示的服务器绑定的端口

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 05 01 00 03 01 00 00 00                         |........        |
+--------+-------------------------------------------------+----------------+
 
 
-- ------- 正常应该是↓
     	+-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 05 00 00 03 01 00 00 00                         |........        |
+--------+-------------------------------------------------+----------------+

某些服务端主机会出现这种情况?

再原因是 XClientProxyHandler 因为 exceptionCaught An existing connection was forcibly closed by the remote host 回调了 promise 失败

Wireshark 抓包分析下 刚连上还没与代理握手就收到RST 包.. 八成是GFW问题… 不管了 6 TCP 60 3265 → 3668 [RST, ACK] Seq=1 Ack=434 Win=994816 Len=0

HTTP 代理响应卡死 问题

原因是XRelayHandler, writeAndFlush 的数据, 浏览器只接受的了第一行, 不完整 HTTP/1.1 200 OK(15字节, 原本是418字节), why?

/**
* 浏览器只收到
* HTTP/1.1 200 OK
 
*/
dstChannel.writeAndFlush(Unpooled.wrappedBuffer(bytes));
log.info("客户端<=代理<=目标 \tGet [{} bytes]", bytes.length);

而回调的结果是false, whf?

dstChannel.writeAndFlush(Unpooled.wrappedBuffer(bytes)).addListener(channelFuture ->{
	//写入结果: false
	System.out.println("写入结果: "+channelFuture.isSuccess());
});

原因是HTTP 也要移除在initChannel添加的Handler, 它们不能序列化ByteBuf数据对象

if(isHttps) {
	localCtx.pipeline().remove("httpCodec");
	localCtx.pipeline().remove("httpObject");
}else {
	localCtx.pipeline().remove("httpCodec");
	localCtx.pipeline().remove("httpObject");

然而浏览器又收到多一行? 什么鬼?

HTTP/1.1 200 OK

HTTP/1.1 200 
Server: nginx/1.16.1
Date: Fri, 25 Sep 2020 01:41:41 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY

33
{"code":500, "msg":"..................", "data":null}
0

代码逻辑问题, 把https CONNECT 的响应返回给http了, 上面序列化问题能收多一行, 也是这原因..

HTTPS 有CONNECT流程; 需要添加个响应给浏览器; HTTP 无CONNECT流程; 解析到Host 还需要给代理服务端, 发送原请求数据;

HTTP 如何判断响应结束?

如果 Content-Length=x 不存在, 那么头类型为Transfer-Encoding: chunked 说明响应的长度不固定, 则在响应头结束后标记第一段流的长度 然后接着读, 最后会用\r\n0\r\n\r\n表示结束.

客户端socket模拟http请求,如何判断http响应结束(http1.1)

添加 系统级代理

设置注册表? 计算机\HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings

参考 shadowsocks-windows 的源码 貌似是通过wininet.dll 设置代理的…

参考 shadowsocks

其实就是设置 windows的 internet选项 > 连接

PAC模式是配置使用自动脚本 配置文件指向 pac.txt 全局模式是配置为LAN使用代理服务器

代码归档

java 版

自改梯子 v2.2.0

android 版

自改梯子 v2.5.x

内存溢出错误?

服务接受客户端连接时, 抛出分配堆外内存失败!

io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16777216 byte(s) of direct memory (used: 335544320, max: 343932928)
	at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:776)
	at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:731)
	at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:645)
	at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:621)
	at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:204)
	at io.netty.buffer.PoolArena.tcacheAllocateNormal(PoolArena.java:188)
	at io.netty.buffer.PoolArena.allocate(PoolArena.java:138)
	at io.netty.buffer.PoolArena.allocate(PoolArena.java:128)
	at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:378)
	at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
	at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)
	at io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:139)
	at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:150)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLo

代理服务器 中继读写不平衡, 导致积压消息太多占用内存.

Netty 自带的’水流’ 标记选项, 当处于高位时会修改 Channel 的标记, 通过Channel.isWritable() 可判断

 
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.childOption(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 32 * 1024);
bootstrap.childOption(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024);
//OR
bootstrap.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, WriteBufferWaterMark.DEFAULT)
bootstrap.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(1024,4096))

Netty发送队列积压导致内存泄露 Netty | 设置高低水位线避免 OOM

游戏加速 (自用)

思路

  1. 与代理建立两个(or多个)连接通道, 一个通道断开了, 数据立即重发给另一个连接通道.
  2. 或者代理主机hold住连接, 客户端立即从连?
  • 去掉数据加密/伪装 稳定&速度第一
  • 直接客户端与服务端建立多个连接形成一个连接池, 在需要时, 随时拿一个可用的连接, 建立通讯; 有个好处, 这个池的连接可以被任意客户协议拿来用, 模块化; 但是麻烦逻辑.. 既要处理重连, 又要处理正常断连, 要维护连接池 双方连接的锁, 要维护连接池数量.. handler 客户端要注册在本地回环, 而服务端要注册在远程, 不然断了, 就没有上下文了.. 需要hook游戏程序网络..

一些软件方案

hook游戏客户端: 需要hook本地游戏的网络, 类似的软件 Netch(开源), ProxyCap(收费), NetLimiter,

服务端 & 客户端: Clash for Windows, V2Ray Clash for Windows

一个环形缓冲池的实现

CircularByteBufPool

 public static void main(String[] args) {
        //// 测试环形队列
//        CircularByteBufPool ttt = new CircularByteBufPool(4);
//        for (int i = 0; i < 12; i++) {
//            ttt.addCircularData(null);
//            System.out.println(Arrays.toString(ttt.getCircularIndexs()));
//            int s = ttt.getLastItemIndex();
//            int e = ttt.getFirstItemIndex();
//            System.out.println("e="+e+"    , s="+s);
//        }
        //// 测试环形取数据 getCircularBytesByPosition
        String test= "ifskgh_jhfuighaif_jfgnfalkgjfhfGOFSAHGfslghlkfdxfg";
        byte[] testBts = test.getBytes(StandardCharsets.UTF_8);
        System.out.println("length =" +testBts.length );//50
        System.out.println("testBts =" + new String( testBts,StandardCharsets.UTF_8));
        byte[] testBts1 = Arrays.copyOfRange(testBts,0,10);
        byte[] testBts2 = Arrays.copyOfRange(testBts,10,20);
        byte[] testBts3 = Arrays.copyOfRange(testBts,20,50);
        System.out.println("testBts1 =" + new String( testBts1,StandardCharsets.UTF_8));
        System.out.println("testBts2 =" + new String( testBts2,StandardCharsets.UTF_8));
        System.out.println("testBts3 =" + new String( testBts3,StandardCharsets.UTF_8));
        //////////
/**
 * 环形缓存池, 非线程安全; 如设置最大容量为4; 循环无限添加的数据块情况, 如下:
 * <pre>
 * [0, 1, 2, 3]
 * [1, 2, 3, 0]
 * [2, 3, 0, 1]
 * [3, 0, 1, 2]
 * [0, 1, 2, 3]
 * ...
 * </pre>
 *
**/
        CircularByteBufPool byteBufPool = new CircularByteBufPool(10);
        ByteBufItem t1 = new ByteBufItem(testBts1);
        t1.startPosition = 0;
        t1.endPosition = testBts1.length-1;
        byteBufPool.addCircularData(t1);
        //
        ByteBufItem t2 = new ByteBufItem(testBts2);
        t2.startPosition = t1.endPosition;
        t2.endPosition = t1.endPosition + testBts2.length;
        byteBufPool.addCircularData(t2);
        //
        ByteBufItem t3 = new ByteBufItem(testBts3);
        t3.startPosition = t2.endPosition;
        t3.endPosition = t2.endPosition + testBts3.length;
        byteBufPool.addCircularData(t3);
        //
        System.out.println(byteBufPool ); //{circularData=[{s=0, e=9}, {s=9, e=19}, {s=19, e=49}, null, null, null, null, null, null, null]}
        byte[] tbs = byteBufPool.getBytesByPosition(0,49);
        if (tbs == null){
            System.out.println("============越界!!!");
        }else{
            System.out.println("tbs len="+tbs.length+", data="+ new String( tbs,StandardCharsets.UTF_8));
        }
//        for (int i = 0; i < 5; i++) {
//            byte[] tbs2 = byteBufPool.getBytesByPosition(0+i,15+i);
//            if (tbs2 == null){
//                System.out.println("============越界!!!");
//            }else{
//                System.out.println("tbs len="+tbs2.length+", data="+ new String( tbs2,StandardCharsets.UTF_8));
//            }
//        }
    } 
 

WireGuard

wireguard是非常简单、现代化、快速的vpn,使用最新的加密技术,udp传输,支持ip漫游等。wireguard没有服务端、客户端的区分,每一台设备都是一个peer。 https://icloudnative.io/posts/wireguard-docs-practice/#interface

网络拓扑

一端位于 NAT 后面,另一端直接通过公网暴露 这种情况下,最简单的方案是:通过公网暴露的一端作为服务端,另一端指定服务端的公网地址和端口,然后通过 persistent-keepalive 选项维持长连接,让 NAT 记得对应的映射关系。

两端都位于 NAT 后面,通过中继服务器连接 大多数情况下,当通信双方都在 NAT 后面的时候,NAT 会做源端口随机化处理,直接连接可能比较困难。可以加一个中继服务器,通信双方都将中继服务器作为对端,然后维持长连接,流量就会通过中继服务器进行转发。

服务端与客户端 在 WireGuard 里,客户端和服务端基本是平等的,差别只是谁主动连接谁而已。双方都会监听一个 UDP 端口,谁主动连接,谁就是客户端。主动连接的客户端需要指定对端的公网地址和端口

WireGuard 安装

官页 quickstart WireGuard 教程:WireGuard 的搭建使用与配置详解

for CentOS 8

$ sudo yum install yum-utils epel-release
$ sudo yum-config-manager --setopt=centosplus.includepkgs="kernel-plus, kernel-plus-*" --setopt=centosplus.enabled=1 --save
$ sudo sed -e 's/^DEFAULTKERNEL=kernel-core$/DEFAULTKERNEL=kernel-plus-core/' -i /etc/sysconfig/kernel
$ sudo yum install kernel-plus wireguard-tools
$ sudo reboot

for Ubuntu

#Ubuntu ≥ 18.04
$ apt install wireguard
 
# Ubuntu ≤ 16.04
$ add-apt-repository ppa:wireguard/wireguard
$ apt-get update
$ apt-get install wireguard

配置说明

Interface

本地节点是客户端,只路由自身的流量,只暴露一个 IP。

[Interface]
# Name = phone.example-vpn.dev
Address = 192.0.2.5/32
PrivateKey = eJVUP7R+lNH21aTtZ/HmPVflFTcMODaxBiJ1jm9Hkks=

本地节点是中继服务器,它可以将流量转发到其他对等节点(peer),并公开整个 VPN 子网的路由。

[Interface]
# Name = public-server1.example-vpn.tld
Address = 192.0.2.1/24
ListenPort = 51820
PrivateKey = eJVUP7R+lNH21aTtZ/HmPVflFTcMODaxBiJ1jm9Hkks=
DNS = 8.8.8.8
  • Address 定义本地节点应该对哪个地址范围进行路由。如果是常规的客户端,则将其设置为节点本身的单个 IP(使用 CIDR 指定,例如 192.0.2.3/32);如果是中继服务器,则将其设置为可路由的子网范围。
  • ListenPort 当本地节点是中继服务器时,需要通过该参数指定端口来监听传入 VPN 连接,默认端口号是 51820。常规客户端不需要此选项。
  • PrivateKey 本地节点的私钥,所有节点(包括中继服务器)都必须设置。不可与其他服务器共用。
  • DNS 通过 DHCP 向客户端宣告 DNS 服务器。客户端将会使用这里指定的 DNS 服务器来处理 VPN 子网中的 DNS 请求,但也可以在系统中覆盖此选项。
  • PreUp 启动 VPN 接口之前运行的命令。这个选项可以指定多次,按顺序执行
  • PostUp 启动 VPN 接口之后运行的命令。这个选项可以指定多次,按顺序执行。

Peer

[Peer] 定义能够为一个或多个地址路由流量的对等节点(peer)的 VPN 设置。对等节点(peer)可以是将流量转发到其他对等节点(peer)的中继服务器,也可以是通过公网或内网直连的客户端。

中继服务器必须将所有的客户端定义为对等节点(peer),除了中继服务器之外,其他客户端都不能将位于 NAT 后面的节点定义为对等节点(peer),因为路由不可达。对于那些只为自己路由流量的客户端,只需将中继服务器作为对等节点(peer),以及其他需要直接访问的节点。 (简而言之 服务器节点必须定义所有节点)

对等节点(peer)是位于 NAT 后面的客户端,只为自己路由流量

[Peer]
# Name = home-server.example-vpn.dev
Endpoint = home-server.example-vpn.dev:51820
PublicKey = <public key for home-server.example-vpn.dev>
AllowedIPs = 192.0.2.3/32

对等节点(peer)是中继服务器,用来将流量转发到其他对等节点(peer)

[Peer]
# Name = public-server1.example-vpn.tld
Endpoint = public-server1.example-vpn.tld:51820
PublicKey = <public key for public-server1.example-vpn.tld>
# 路由整个 VPN 子网的流量
AllowedIPs = 192.0.2.1/24
PersistentKeepalive = 25
  • Endpoint 指定远端对等节点(peer)的公网地址。如果对等节点(peer)位于 NAT 后面或者没有稳定的公网访问地址,就忽略这个字段。通常只需要指定中继服务器的 Endpoint,当然有稳定公网 IP 的节点也可以指定。
  • AllowedIPs 允许该对等节点(peer)发送过来的 VPN 流量中的源地址范围。同时这个字段也会作为本机路由表中 wg0 绑定的 IP 地址范围。 如果对等节点(peer)是常规的客户端,则将其设置为节点本身的单个 IP; 如果对等节点(peer)是中继服务器,则将其设置为可路由的子网范围。可以使用 , 来指定多个 IP 或子网范围。该字段也可以指定多次。

对等节点(peer)是常规客户端,只路由自身的流量: AllowedIPs = 192.0.2.3/32

对等节点(peer)是中继服务器,可以将流量转发到其他对等节点(peer): AllowedIPs = 192.0.2.1/24

对等节点(peer)是中继服务器,可以路由其自身的流量和它所在的内网的流量: AllowedIPs = 192.0.2.3/32,192.168.1.1/24

对等节点(peer)是中继服务器,可以转发所有的流量,包括外网流量和 VPN 流量,可以用来干嘛你懂得: AllowedIPs = 0.0.0.0/0,::/0

  • PublicKey 对等节点(peer)的公钥,所有节点(包括中继服务器)都必须设置。可与其他对等节点(peer)共用同一个公钥。
  • PersistentKeepalive 如果连接是从一个位于 NAT 后面的对等节点(peer)到一个公网可达的对等节点(peer),那么 NAT 后面的对等节点(peer)必须定期发送一个出站 ping 包来检查连通性,如果 IP 有变化,就会自动更新Endpoint

启动与停止管理

对于 wg-quick@.service ,它会调用 /usr/bin/wg-quick 读取 /etc/wireguard/*.conf 后启动

# 启动(服务)
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
sudo systemctl restart wg-quick@wg0
sudo systemctl stop wg-quick@wg0
 
# 启动(脚本)
$ wg-quick up /full/path/to/wg0.conf
$ wg-quick down /full/path/to/wg0.conf
 
# 查看/测试 状态
$ sudo systemctl status wg-quick@wg0
$ sudo wg
$ sudo ip a show wg0
# 查看系统 VPN 接口信息
$ ip link show wg0
# 查看 VPN 接口详细信息
$ wg show all
$ wg show wg0
 
 
# 启动/停止 VPN 网络接口
$ ip link set wg0 up
$ ip link set wg0 down
 
# 注册/注销 VPN 网络接口
$ ip link add dev wg0 type wireguard
$ ip link delete dev wg0
 
# 注册/注销 本地 VPN 地址
$ ip address add dev wg0 192.0.2.3/32
$ ip address delete dev wg0 192.0.2.3/32
 
# 添加/删除 VPN 路由
$ ip route add 192.0.2.3/32 dev wg0
$ ip route delete 192.0.2.3/32 dev wg0

WireGuard 服务端

1. 生成服务器密钥对

$ wg genkey | tee privatekey | wg pubkey > publickey && cat privatekey && cat publickey eJVUP7R+lNH21aTtZ/HmPVflFTcMODaxBiJ1jm9Hkks= HfVbHLZ1VNaSKmPL1qtSNxfgandncNvjRZ5C1n7070Y= 其中: wg genkey为生成密钥串命令,“xxxx=”为生成的密钥串;privatekey 为密钥串保存的文件名; wg pubkey为根据密钥串生成公钥串命令,“xxxx=”为生成的公钥串;publickey 为公钥串保存的文件名。

2. 生成客户端密钥对

$ wg genkey | tee privatekey | wg pubkey > publickey && cat privatekey && cat publickey oFjW3hyRSZ+TzrWsesx4eBA679iEJ6nbRvkxfnPyz0I= upAWoK7/WLwyIowH5RjjaKSHpoxAU6x7TCysvw0beEo=

3. 配置文件

配置文件的命名形式必须为 ${WireGuard 接口的名称}.conf。通常情况下 WireGuard 接口名称以 wg 为前缀,并从 0 开始编号,但你也可以使用其他名称,只要符合正则表达式 ^[a-zA-Z0-9_=+.-]{1,15}$ 就行。

vim /etc/wireguard/wg0.conf

[Interface]
# 服务器配置
Address = 10.0.0.1/24
SaveConfig = true
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# 自定义端口
ListenPort = 5600
# 服务器私钥
PrivateKey = eJVUP7R+lNH21aTtZ/HmPVflFTcMODaxBiJ1jm9Hkks=
 
# 节点配置 
[Peer]
#客户端公钥
PublicKey = upAWoK7/WLwyIowH5RjjaKSHpoxAU6x7TCysvw0beEo=
AllowedIPs = 0.0.0.0/0,::/0
PersistentKeepalive = 25

在中继服务器上开启 IP 地址转发:

$ echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
$ echo "net.ipv4.conf.all.proxy_arp = 1" >> /etc/sysctl.conf
$ sysctl -p /etc/sysctl.conf

WireGuard

Trojan

Trojan 不使用自定义的加密协议来隐藏自身。相反,使用特征明显的TLS协议(TLS/SSL),使得流量看起来与正常的HTTPS网站相同。TLS是一个成熟的加密体系,HTTPS即使用TLS承载HTTP流量。使用正确配置的加密TLS隧道,可以保证传输的

trojan-go 服务端

git release 页面 x86 linux 选 [trojan-go-linux-386.zip] 文档

使用配置文件启动 ./trojan-go -config config.json

对于服务器 server,key 和cert为必填。

下面是一份比较安全的服务器配置server.json,需要你在本地80端口配置一个HTTP服务(必要,你也可以使用其他的网站HTTP服务器,如”remote_addr”: “example.com”),在1234端口配置一个HTTPS服务,或是一个展示”400 Bad Request”的静态HTTP网页服务。(可选,可以删除fallback_port字段,跳过这个步骤)

{
    "run_type": "server",
    "local_addr": "0.0.0.0",
    "local_port": 443,
    "remote_addr": "127.0.0.1",
    "remote_port": 80,
    "password": [
        "your_awesome_password"
    ],
    "ssl": {
        "cert": "server.crt",
        "key": "server.key",
        "fallback_port": 1234
    }
}

这个配置文件使Trojan-Go在服务器的所有IP地址上(0.0.0.0)监听443端口,分别使用server.crt和server.key作为证书和密钥进行TLS握手。你应该使用尽可能复杂的密码,同时确保客户端和服务端password是一致的。注意,Trojan-Go会检测你的HTTP服务器http://remote_addr:remote_port是否正常工作。如果你的HTTP服务器工作不正常,Trojan-Go将拒绝启动。

不要在remote_port和fallback_port搭建有高实时性需求的服务,Trojan-Go识别到非Trojan协议流量时会有意增加少许延迟以抵抗GFW基于时间的检测。

握手流程

当一个客户端试图连接Trojan-Go的监听端口时,会发生下面的事情:

  1. 如果TLS握手成功,检测到TLS的内容非Trojan协议(有可能是HTTP请求,或者来自GFW的主动探测)。Trojan-Go将TLS连接代理到本地127.0.0.1:80上的HTTP服务。这时在远端看来,Trojan-Go服务就是一个HTTPS网站。

  2. 如果TLS握手成功,并且被确认是Trojan协议头部,并且其中的密码正确,那么服务器将解析来自客户端的请求并进行代理,否则和上一步的处理方法相同。

  3. 如果TLS握手失败,说明对方使用的不是TLS协议进行连接。此时Trojan-Go将这个TCP连接代理到本地127.0.0.1:1234上运行的HTTPS服务(或者HTTP服务),返回一个展示400 Bad Reqeust的HTTP页面。fallback_port是一个可选选项,如果没有填写,Trojan-Go会直接终止连接。虽然是可选的,但是还是强烈建议填写。

搞一个免费域名

https://www.freenom.com/en/index.html?lang=en

搞一个免费证书

TODO

V2Ray

V2Ray 服务端

V2Ray

core 核心: Xray/SagerNet

V2Ray

V2Ray 目前支持以下协议(截止到2019年12月):

  • Blackhole:“黑洞”,是一个出站数据协议,它会阻碍所有数据的出站,配合路由(Routing)一起使用,可以达到禁止访问某些网站的效果。
  • Dokodemo-door:中文名称“任意门”,是一个入站数据协议,它可以监听一个本地端口,并把所有进入此端口的数据发送至指定服务器的一个端口,从而达到端口映射的效果。
  • Freedom:是一个出站协议,可以用来向任意网络发送(正常的) TCP 或 UDP 数据。
  • HTTP:超文本传输协议,是传统的代理协议
  • MTProto:Telegram 的开发团队开发的专用协议,是一个 Telegram 专用的代理协议。在 V2Ray 中可使用一组入站出站代理来完成 Telegram 数据的代理任务。目前只支持转发到 Telegram 的 IPv4 地址。
  • Shadowsocks:最早被个人开发的科学上网梯子协议,但 V2Ray 目前不支持 ShadowsocksR。
  • Socks:标准 Socks 协议实现,兼容 Socks 4、Socks 4a 和 Socks 5,也是传统的代理协议。
  • VMess:是V2Ray 专用的加密传输协议,它分为入站和出站两部分,通常作为 V2Ray 客户端和服务器之间的桥梁。因为增加了混淆和加密,据说比 Shadowsocks 更安全。现在的机场支持 V2Ray,一般是指支持 VMess 协议。VMess 依赖于系统时间,请确保使用 V2Ray 的系统 UTC 时间误差在 90 秒之内,时区无关。在 Linux 系统中可以安装ntp服务来自动同步系统时间。
Xray

Xray与V2Ray完全类同,Xray 是 Project X 项目的核心模块。因为Xray和XTLS黑科技的作者rprx曾经是V2fly社区的重要成员,所以Xray直接Fork全部V2Ray的功能,然后进行性能优化,并增加了新功能,使Xray在功能上成为了V2Ray的超集,且完全兼容V2Ray。

SagerNet

SagerNet 是一个基于 V2Ray 的 Android 通用代理应用。

V2Ray 客户端

sstap + V2Ray

SSTap 全称 SOCKSTap, 是一款利用虚拟网卡技术在网络层实现的代理工具。SSTap 能在网络层拦截所有连接并转发给 HTTP, SOCKS4/5, SHADOWSOCKS(R) 代理,而无需对被代理的应用程序做任何修改或设置。它能同时转发 TCP, UDP 数据包,非常适合于游戏玩家使用。

SStap项目方已经不再维护 SSTap 和 SocksCap64,SSTap 和 SocksCap64 只是一个加速器,旨在减少网络游戏的 ping,这不是绕过防火墙的工具,所以即使你使用这个工具,你也无法打开那些被中国屏蔽的网站,如谷歌、Youtube、Twitter 等。而且,他们是免费的!它从不提供任何代理节点。如果其他人销售代理节点并将其用作游戏加速器,这与我无关。最后,在使用本软件时,请遵守国家法律法规。用户将对因使用本软件而造成的任何损害和责任承担全部责任。

  • SSTap和SocksCap64已于2017年11月19日停止开发及维护。 SSTap-backup

Netch + V2Ray(最终)

V2Ray Netch 是一款 Windows 平台的开源游戏加速工具,不同于 SSTap 那样需要通过添加规则来实现黑名单代理, Netch 原理更类似 SocksCap64

Netch避免了由SSTap引起的受限NAT问题。您可以使用NATTypeTester来测试您的NAT类型。当使用SSTap加速某些P2P游戏连接或该开放NAT类型需要游戏时,您可能会遇到一些不好的情况,例如无法加入游戏。

  • 不同于SSTap那样需要通过添加规则来实现黑名单代理,Netch原理更类似Sockscap64,通过扫描游戏目录获得需要代理的进程名进行代理。 也可以实现 SSTap 那样的全局 TUN/TAP 代理,和 shadowsocks-windows 那样的本地 Socks5,HTTP 和系统代理。
  • 在日常网页浏览方面,可以进行分流设置。
  • 支持的代理协议:Socks5 / Shadowsocks / ShadowsocksR / Trojan / Vmess / VLess
  • UDP NAT FullCone
  • 指定进程加速

Github- Netch Windows - Netch 使用教程 5分钟了解游戏加速器的原理与搭建

netch 报 aioDNS start fialled: 设置自定义DNS就行了