tun2socks 技术人员大多数应该知晓的一个代理转发工具,其作用则是处理 tun/tap 点对点虚拟字符以太网设备输入输出的 “二层或三层” 数据报文转换成 socks 协议(几乎都为 socks5 代理协议)

       本人不鼓励使用 “tun2socks” 进行一切 “科学、魔法、黑科技上网” 的违法行为,对于绝大部分人而言只需要 “百度一下,你就知道” ,回到本文正文本人将带领 “各位有志之士” 一起深入研究探讨 “tun2socks” 的技术核心原理

       “tun2socks” 技术的核心难点只有一个,如何解决 TCP-LINK 转发或者说是程式如何理解并处理 TCP-LINK 的问题,同时必须兼顾 “高效、稳定、低CPU/RAM指标、低耗电量” 

        UDP 承载协议本人不推荐基于 Socks5 协议,因其协议定义对 UDP 承载协议支持并不好,虚端口复用率很低;这对于一个专业代理而言是很严峻的问题,更别提其协议不兼容 ICMP(互联网控制协议)报文(ICMP_ER、ICMP_TE、ICMP_EC...)

       “tun2socks” 实现方法只有两种,就真的再也找不到其它的办法(或许是我很菜的缘故~~~笑~~~!若是还有其它的方法,希望知晓的各位可以拿出来分享探讨一下)。

        1、TAP中继程式运行一个独立TCP/IP协议栈(tun2socks-cli 原理)[主流]

              中继程式建立运行一个侦听 “TCP_SYN” 任何地址与端口的虚拟TCP服务器(正统TCP服务器只可以处理“监听”端口与                  Netif-IP地址的数据);然后处理 Socks5 与虚拟TCP服务器之间的TCP流数据相互交换(类似虚拟数据交换机);

              PS:LWIP协议栈需要修改监听部分代码,以支持监听任何的IP地址与端口的TCP链接

              RX:此方法实现的人太多虽然真正可以用的没几个,已不在我喜欢,应该走一走非主流无人实现过的办法~笑~~!

                       等下用C#基于内核协议栈桥接的方法,跑跑娱乐的图图来个本文结尾镇下楼 (无任何调用C/C++或非.NET库的代

                       码,不过调TAP虚拟网卡驱动这个就不能算了哈~笑~~!)

        2、内核协议栈桥接 ( SupersocksR-Astar、tun2socks-.netcore 原理)[非主流]

             1、NDIS-5.x/6.x 内核层虚拟链路重叠 (必须为驱动层)

             2、基于PCAP物理网卡虚拟链路重叠  (应用层可处理)

             3、基于虚拟网卡设备层虚拟链路重叠 (应用层可处理)

             PS:虚拟一个或多个IP主机,对链路进行往返重叠让内核处理这些数据报文转换为系统 bsd/socket 故而处理链路转发

                     类似于NAT,你或许可以参考 “SoftEther 2 Layer-VPN” 对于TAP-NAT之间的处理,对你或许会有一定的启发。

       “tun2socks” 实现流派目前几乎是清一色的,TAP中继程式内部运行一个小型轻量级的TCP/IP协议栈(C/C++ LWIP[魔改]、Go NetStack)把从TAP设备输入的TCP/IP-FRAME输入到TCP/IP应用层协议栈(lwip: tcp_input)为每个TCP_PCB(TCP协议控制块)上添加 recv 的事件回调函数,把由TCP/IP协议栈处理过后的有序流数据转发到 “Socks5” 代理服务器;反之从 “Socks5” 收到的TCP流数据将其插入到对应TCP_PCB的发送窗口队列(snd_win)之中(tcp_write) 同时要求协议栈尽量立即发送TCP数据报文(tcp_output)

       基于LWIP协议栈实现 “tun2socks” 本人已经实现其工具并在 GitHub 上托管了开放源代码)或许我有足够的理由评价这种方法的缺陷性) :: https://github.com/liulilittle/tun2socks (Microsoft Visual C/C++ 2015-x86 [msvc2015]) .......R*: https://blog.csdn.net/liulilittle/article/details/104603258 为其编译版本与使用说明。

       当然你可以选择不使用 “Lwip” TCP/IP协议栈,可以从0开始手写一个TCP/IP协议栈;本人在 SkylakeNAT 实现了一个精简到不能在精简的 “TCP/IP” 协议栈,它仅供TCP/IP协议基础测试,问题有很多但是只要不PSH报文速度过快一般不会出现太大的问题,它仅用于让人对 TCP/IP 协议是如何ACK、PSH、SYN、FIN/RST有一些基本了解,它并没有一个完整的TCP/IP要求需要实现的相应组件与功能,例:“快速重传、快速确认、滑块窗口、流速控制”,还有TCP链路状态机(例如:RECVD、TIME_WAIT_1、TIME_WAIT_2...)

       有很多时间的同学们可以尝试去实现一个符合RFC标准的完整TCP/IP协议栈,TCP/IP协议栈实现上去会很复杂与困难,实现时间也是按照多少个季度时间来算的,但这也意味着实现一个稳定可靠TCP/IP协议栈的你会变得更加的强大!(PS:纸上谈兵的话,只需要一秒一分钟一天一个月就可以实现~~~笑~~!)

       似乎听上去仅仅只需要使用 “Lwip” 则可以实现 “tun2socks” 用接口似乎并不难,我或许应该笑而不语,当你仔细的阅读分析过 “Lwip” 与要解决的问题时,你会发现你自己是多么的天真多么的异想天开和傻白甜~~,这样的天真的你或许比较适合 Java、PHP;(问题越是容易被解决的东西,那么任何人都可以解决,那么对应的开源项目就多,而 “tun2socks” 并不是一个容易被轻易解决的问题。

      仅仅是解决利用 “Lwip” 协议栈与 “socks5” 套接字之间数据相互传输的问题,就非常困难,即便你认为你的代码完全没有一点问题;但是各种 “野指针”、无效内存;就可以轻松教你做上几个星期的人;相信没有任何人可以接受你的程序坚持不到十秒或者一分钟就各种 “crash” 掉吧(十秒都坚持不到的男淫~~笑!)

       Lwip + Socks5 协议转发之间主要会遇见的问题为

                  1、资源泄露(管理的、非管理的)

                  2、无效引用(资源被释放)

                  3、CPU空转(多为试用一段时间后)

                  4、TCP-PCB句柄泄露

                  5、LWIP协议栈必须运行在STA下,意味着不允许并发式

                  6、LWIP必须魔改可以支持监听TCP_SYN“任何地址与端口”建立TCP_PCB

       上述几个问题是应用 “Lwip + Socks5” 的根本性问题,我们应当如何解决这些问题?其实这并没有一个成明文体系的解决方法,你只能使用最笨的办法,逐步阅读 LWIP 协议栈的源代码(TCP-IP协议栈部分,若你仅仅使用TCP/IP协议栈的前提下) 按照你遇见的问题与需求,逐步的修改 “LWIP” 协议栈的源代码,以确保修改后的代码可以与协议栈内部代码还有你的 “Socks5” 转发代码之间建立联系,当然这会改很多而且也很麻烦,不过这个是在你决定使用 LWIP 协议栈来实现 “tun2socks” 代理转发工具的时候你必须要做好一个心理准备,否则你还是选择放弃,不可耻今天的放弃明天的不放弃)。

       前辈心得,这也是我为什么在上文提到 “LWIP[魔改]” 的字样,因为这个在基于这样形式的解决方案里面,它是一个很核心的部分,外部接口转发那其仅仅是一个小儿科的问题,不值得单独拿出来就此单独的讨论,如果你连一个很明白很直接的接口都不知道怎么用,那么几乎你就已经彻底废掉了。

        “LWIP” 协议栈里面最让人难以理解的部分仅仅只有 TCP/IP 协议栈的实现,因为 TCP/IP 协议栈实现是真的很困难与消磨时间,但 UDP/IP/、ICMP 这样的 “分层承载协议” 处理反倒是最傻瓜式的。

      上述这些问题,本人实现的 “tun2socks” 的工具代码里面都已经解决,一个高效的 “tun2socks” 工具“处理器、内存资源、耗电量” 这些指标都很低才对,本人提供一个推荐指标,你可以用以辅助评估基于 “应用协议栈” 的 “tun2socks”工具的资源消耗合理区间。

       1、CPU(<= 1逻辑核心)

       2、耗电量(非常低~低)

       3、内存(< 5mb,多为2.Xmb) [非托管程序]

       4、内核句柄数( < 200)

        超出上述提到的指标的 “tun2socks” 实现都是有问题的,一般I/O处理不应该有这么高资源消耗,一般操作发出一TCP分段帧数据报文最大载荷多为1400字节,而不是TCP_MSS=536;“tun2socks” 大多数情况都是处理1400个字节分段数据,那么我们算一下达到10Mbps,一秒钟最小需要处理TCP_PSH多少次 

       (10 << 10 << 10) >> 3 = ‭10,485,760‬‬ / 8 = ‭1,310,720‬‬ / 1,400 = ‭936‬ (CP_ACP), 10Mbps 只需要协议栈处理PSH一秒钟至少936次实际次数不固定(大多数时只有几百此INPUT),但是几乎最大秒帧数量都不会超出1.5倍(936*.15=1404) 这个其实很容易进行测试,那么现在我们请问现代处理平台跑一个 “Thread” 一秒钟1500次TCP_INPUT运算都跑不了?居然还需要很高的CPU处理器占用?如果是那就是这个 “程序或者应用的协议栈” 本身就是存在非常严重的性能故障。

        解决:Socks5 域名代理的问题,解决这个问题真的很容易就是采用 “DNS污染(劫持)” 的办法,你只需要劫持全部的DNS请求返回你的程序伪造的IP/NS,但是这个IP与这个域名是一一对应不就可以了吗?这只是基操!

            struct pbuf sp;
            memset(&sp, 0, sizeof(sp));

            sp.payload = sz;
            sp.len = sizeof(sz);
            sp.tot_len = sizeof(sz);

            // 设当前收取到的UDP帧长度不足DNS协议头的长度则返回假。
            if (sp.len < sizeof(DNSHeader)) {
                continue;
            }

            auto request = (DNSHeader*)sp.payload;
            request->usTransID = htons(request->usTransID);
            request->usFlags = htons(request->usFlags);
            request->usQuestionCount = htons(request->usQuestionCount);
            request->usAnswerCount = htons(request->usAnswerCount);
            request->usAuthorityCount = htons(request->usAuthorityCount);
            request->usAdditionalCount = htons(request->usAdditionalCount);

            // 不支持除A4地址解析以外的任何DNS协议(不过按照INETv4以太网卡也不可能出现A6地址析请求)
            // A6根本不需要虚拟网卡链路层网络远程桥接,先天的scope机制就足以抵御外部入侵的防火长城。
            if (0 == (request->usFlags & 0x0100)) {
                continue;
            }

            // 若客户端查询问题是空直接不给客户端应答就让它卡在那里用户态(RING3)通过系统DNS服务进行解析不太可能是请求空答案。
            // 虽然这会造成系统内核使用处于等待数据包应答的状态;句柄资源无法释放但是已经不太重要了;底层也不太好操作把上层
            // 搞崩溃,搞太猛系统就蓝屏了;当然倒是可以强制把目标进程的内存全部设置为WPOFF让它死的难看至极。
            // 不过这么搞了就必须要在RING0做防护了;万一逗逼跑来强制从内核卸载怎么办,一定要让这些人付出代价必须蓝屏死机。
            // 虽然这并不是没有办法。对付小小的用户态程式方法真的太多,搞死它只要你想轻而易举;毕竟应用层都是最低贱的程式。
            if (0 == request->usQuestionCount) {
                continue;
            }

            // 应答客户端查询DNS的请求,DNS地址污染并且强制劫持到分配的保留地址段假IP。
            auto payload = (char*)(request + 1);

            // 从DNS协议流中获取需要解析的域名。
            std::string hostname = "";
            while (*payload) {
                u8_t len = (u8_t)*payload++;
                if (!hostname.empty()) {
                    hostname += ".";
                }
                hostname += std::string(payload, len);
                payload += len;
            }
            payload++; // 查询字符串的最后一个字节是\x0中止符号。

                       // 问题所需求的查询类型。
            u16_t usQType = ntohs(*(u16_t*)payload);
            payload += sizeof(u16_t);

            // 问题所需求的查询类别。
            u16_t usQClass = ntohs(*(u16_t*)payload);
            payload += sizeof(u16_t);

            // 构建DNS应答数据报文。
            std::shared_ptr<pbuf> p(
                pbuf_alloc(pbuf_layer::PBUF_TRANSPORT, 1500, pbuf_type::PBUF_RAM),
                [](pbuf* _p) {
                pbuf_free(_p);
            });

            payload = (char*)p->payload;
            p->tot_len = 0;
            p->len = 0;

            // 构建虚假DNS服务响应头。
            auto response = (DNSHeader*)payload;
            response->usTransID = htons(request->usTransID); // usFlags & 0xfb7f -- RFC1035 4.1.1(Header section format)
            response->usFlags = htons(0x8180);
            response->usAuthorityCount = 0;
            response->usAdditionalCount = 0;
            response->usAnswerCount = 0;
            response->usQuestionCount = htons(1);

            payload += sizeof(DNSHeader);
            tun2socks_dns_fill_hostname(hostname.data(), hostname.length(), payload);

            *(u16_t*)payload = ntohs(usQType);
            payload += sizeof(u16_t);
            *(u16_t*)payload = ntohs(usQClass);
            payload += sizeof(u16_t);

            if (usQClass & 1) {
#pragma pack(push, 1)
                tun2socks_dns_fill_hostname(hostname.data(), hostname.length(), payload);

                struct Answer
                {
                    u16_t usQType;
                    u16_t usQClass;
                    u32_t uTTL;
                    u16_t usRDLength;
                };

                Answer* answer = (Answer*)payload;
                answer->usQType = ntohs(usQType);
                answer->usQClass = ntohs(usQClass);
                answer->uTTL = ntohl(0x7f);
                answer->usRDLength = 0;

                if (usQType & 1) {
                    answer->usQType = ntohs(1);

                    struct AnswerAddress {
                        Answer stAnswer;
                        u32_t dwAddress;
                    };

                    AnswerAddress* rrA = (AnswerAddress*)answer;
                    answer->usRDLength = ntohs(4);
                    rrA->dwAddress = ntohl(tun2socks_dns_alloc(hostname));

                    payload += sizeof(AnswerAddress);
                    response->usAnswerCount = ntohs(1);

                    printf("NS Lookup[A, IN]: %s hijacked -> %s\n", hostname.data(), get_address_string(rrA->dwAddress).data());
                }
                else if (usQType & 5) {
                    answer->usQType = ntohs(5);

                    payload += sizeof(Answer);

                    int resouces_data_length = tun2socks_dns_fill_hostname(hostname.data(), hostname.length(), payload);
                    answer->usRDLength = ntohs(resouces_data_length);

                    response->usAnswerCount = ntohs(1);
                }
#pragma pack(pop)
            }

        好了本文逼逼到这里,已经逼逼了太多的内容,在这里放一张完全用 “C#” 语言实现 “tun2socks” 的图图图,这只是基操而已~~笑~~!,C# 可以做到的事情超乎大部分人贫穷而匮乏的想象,不要把 C# (C四个+)用成 Java、PHP 还不如的东西,那简直是在埋没糟蹋 C# 这个优雅而强大的语言。(~笑)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐