Android Proxy实现分析(未完待续)
序章
最近在校招面试的时候顺手翻了一下TCP/IP详解卷一,然后就想起来VpnService刚出来的时候写过一个移动端抓包应用,写得非常累,还不少bug。其实proxy,sniffer之类的应用,在Android上实现都差不多,所以就趁着这个机会,看了一下圈内知名app shadowsocks Android端的实现。
proxy原理
proxy原理细节就不再赘述,简单地说,proxy其实就是起到了接收请求,返回响应的作用。Android上不是所有的应用都允许配置代理服务器的,所以我们必须实现一个透明代理(Transproxy)那么要在Android上实现一个Transproxy,我们需要做什么呢?首先我们需要实现一个client,负责和proxy server建立连接,转发流量,然后我们需要把整个系统的流量都拦截下来,转发到我们的client处理,最后再把处理完的响应交给系统,由系统分发到各个进程。
流量拦截
先说说流量转发,这一块的实现在Android平台上有一个艰辛的演进之路。在Android 4.0之前,Android SDK是没有提供接口让应用拦截系统流量的,因此需要实现全局透明代理,必须从更底层的Linux下手,Linux内核有一个netfilter模块,提供了包过滤,NAT等功能,上层的iptables就是基于netfilter实现的,4.0之前的流量拦截就是基于iptables实现的。Android 4.0之后,Android提供了VpnService类,允许用户实现自定义的Vpn服务,因此可以用这个类来拦截流量,很多proxy和sniffer都是基于这种方案实现,但是VpnService在这个时候API还不够完善,比如不拦截指定app流量依然无法实现,直到Android 5.0才实现了这个功能。
iptables实现
Android平台的透明代理早期都是基于NAT来实现的,netfilter模块内部有三张表,mangle, filter和nat。我们用的最多的可能是filter,如果要实现地址转换或者端口转发,就需要用到nat表。通俗一点来说,netfilter的存在,允许Linux被当作一个路由器使用,而NAT涉及到在IP包经过路由时,修改其源地址,端口或目标地址,端口。在Android 2.3时代,有一个很知名的代理软件,GAEProxy,全局代理功能就是通过iptables来实现的。shadowsocks早期为了实现分应用代理,也是使用iptables来实现全局代理的。这部分的代码都太过久远,现在很难找了,不过shadowsocks-libev提供了ss-redir的功能,里面也配置了iptables的规则,目的也是一样,为了实现在Linux上的透明代理,我们可以参考一下这部分规则,原理也是一样的。
# Create new chain
iptables -t nat -N SHADOWSOCKS
iptables -t mangle -N SHADOWSOCKS
# Ignore your shadowsocks server's addresses
# It's very IMPORTANT, just be careful.
iptables -t nat -A SHADOWSOCKS -d 123.123.123.123 -j RETURN
# Ignore LANs and any other addresses you'd like to bypass the proxy
# See Wikipedia and RFC5735 for full list of reserved networks.
# See ashi009/bestroutetb for a highly optimized CHN route list.
iptables -t nat -A SHADOWSOCKS -d 0.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 10.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 127.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 169.254.0.0/16 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 172.16.0.0/12 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 192.168.0.0/16 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 224.0.0.0/4 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 240.0.0.0/4 -j RETURN
# Anything else should be redirected to shadowsocks's local port
iptables -t nat -A SHADOWSOCKS -p tcp -j REDIRECT --to-ports 12345
# Add any UDP rules
ip route add local default dev lo table 100
ip rule add fwmark 1 lookup 100
iptables -t mangle -A SHADOWSOCKS -p udp --dport 53 -j TPROXY --on-port 12345 --tproxy-mark 0x01/0x01
# Apply the rules
iptables -t nat -A PREROUTING -p tcp -j SHADOWSOCKS
iptables -t mangle -A PREROUTING -j SHADOWSOCKS
首先创建了一个叫做SHADOWSOCKS的链,然后把服务端地址排除,否则会造成死循环,接下来一段过滤掉局域网地址,然后把其他所有TCP流量都重定向到shadowsocks本地监听的端口。这里要注意的是,iptables规则匹配是从前往后进行的,一旦匹配上一条就不再匹配后面的了,所以可以这么写。
接下来一段是关于UDP的规则,UDP是由TPROXY来处理的,这是因为通过NAT转发的包会修改目标地址,而代理软件是需要知道原始目标地址的,TCP包能获取到原始地址是因为netfilter的连接跟踪机制,而UDP包不行,所以需要由TPROXY来处理,TPROXY也是内核的一个模块,就是用来实现透明代理的,细节可以参考官方文档:https://www.kernel.org/doc/Documentation/networking/tproxy.txt
最后两行应用配置好的规则,综上,全局透明代理在Android早期版本就是这么实现的,因为配置iptables需要root权限,所以早期要实现全局透明代理必须root。
VpnService实现
早期的Android版本只支持内置的VPN连接,VPN服务器必须是标准的PPTP或者L2TP/IPSec实现,从Android 4.0开始,SDK提供了VpnService系列的接口,允许开发者实现自定义的VPN协议。这套API提供了拦截IP包的能力,给开发者提供了高度的可定制性。从官网摘了一张图,很清楚的描述了VpnService的实现原理。
可以看到,系统为VpnService实现了一个TUN设备,TUN设备可以理解为一个工作在OSI参考模型第三层的虚拟网卡,说得形象一点,物理网卡是通过网线收发数据包,而TUN设备通过/dev/tun这个文件来收发数据包。因为TUN设备工作在网络层,所以它没有MAC地址,其他方面与物理网卡没有区别。
从图中可以很清晰地看到,自定义的VpnService直接与TUN设备交互,当VpnService运行的时候,对于应用的出口流量,系统会通过TUN设备转发给VpnService,VpnService再通过一个被保护的连接(不再经过VpnService,否则会死循环)与VPN服务端交互,当产生回包时,VpnService把回包写入TUN设备,再由系统分发给应用程序。
在Android 4.0之后,全局透明代理基本都改为了这种方式实现,相比于iptables的实现,VpnService更稳定,而且不需要root,大大降低了使用门槛。
VpnService有很高的权限,可以获取系统的所有IP数据包,所以任何VpnService在启用前,都需要弹窗让用户确认,并且在建立连接后会在系统通知栏出现一条无法划掉的通知。任何时刻最多只能有一个VpnService被启用,后启动的VpnService会替代前一个。
分应用代理
上一节说到了VpnService是不需要root即可实现透明代理的,但是shadowsocks在VpnService出来之后很长一段时间还是保留了需要root的NAT方案,这显然不是作者忘了删除,而是因为VpnService早期API不全,分应用代理的功能无法实现。总不能更新版本后,原有的功能反而不支持了吧,所以NAT实现必须留着。直到Android 5.0,VpnService.Builder才加上了如下两个接口:
public VpnService.Builder addAllowedApplication (String packageName)
public VpnService.Builder addDisallowedApplication (String packageName)
这两个接口类似于黑名单和白名单,这两个接口是互斥的,调用addAllowedApplication会导致只有指定app通过VPN通信,其他的app是直连的,就像没有VPN一样,调用addDisallowedApplication会导致只有指定app直连,其他的app流量都通过VPN。
那么Android 5.0之前是通过什么方式实现分应用代理的呢?还是ipbtales,iptables有通过uid和gid匹配的扩展,可以筛选指定用户和用户组的流量。我们知道,Android上每个安装的app都有自己独立的uid,通过iptables很容易配置匹配规则,这里从redsocks里摘了两行样例:
# Any tcp connection made by `luser' should be redirected.
root# iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner luser -j REDSOCKS
# You can also control that in more precise way using `gid-owner` from
# iptables.
root# groupadd socksified
root# usermod --append --groups socksified luser
root# iptables -t nat -A OUTPUT -p tcp -m owner --gid-owner socksified -j REDSOCKS
这里的OUTPUT代表所有本机产生的流量,上面两段分别演示了按uid筛选和按gid筛选的方法。
shadowsocks-android源码分析
整体架构
要了解shadowsocks-android的整体架构,可以先从安装后的文件结构入手,这里可以看到,安装后的lib目录下有三个so文件。
:/data/app/com.github.shadowsocks-D1d-hk6w1vgWFxEyIY-e3g==/lib/arm64 #
ls -l
total 1228
-rwxr-xr-x 1 system system 274296 1979-11-30 00:00 libredsocks.so
-rwxr-xr-x 1 system system 642440 1979-11-30 00:00 libss-local.so
-rwxr-xr-x 1 system system 324152 1979-11-30 00:00 libtun2socks.so
翻一下编译脚本,会发现这三个so后缀的文件,其实全部都是ELF可执行文件。Android.mk里有这么一句:
BUILD_SHARED_EXECUTABLE := $(LOCAL_PATH)/build-shared-executable.mk
然后build-shared-executable.mk文件内容如下:
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
LOCAL_MAKEFILE := $(local-makefile)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
$(call check-LOCAL_MODULE_FILENAME)
# we are building target objects
my := TARGET_
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
$(call handle-module-built)
LOCAL_MODULE_CLASS := EXECUTABLE
include $(BUILD_SYSTEM)/build-module.mk
相比BUILD_EXECUTABLE,改动只有一行$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION)),把可执行文件按so来命名。其实这么改主要是为了省事,后缀改为so,后续直接被当成so打包进apk,实际上还是可执行文件。
这三个文件,libredsocks.so主要用来实现透明代理,目前已经废弃,我们先不看,libss-local.so既是一个SOCKS5服务器,也是用来和remote通信的客户端,是整个shadowsocks-android的核心,libtun2socks.so用来实现从tun设备读取数据包到SOCKS5协议的转换。我们先上一张整体的架构图表明各个部分的关系,后面详细讲各个部分的逻辑。
从图上可以看到,上层的VpnService只用来获取tun设备的fd,获取到fd之后通过sock_path这个LocalSocket传递给tun2socks,tun2socks获取到fd之后从tun读取数据包,然后转发给ss-local,ss-local负责与远程服务端建立连接,并把这个fd通过protect_path回传给VpnService,VpnService再把这个fd protect起来,防止死循环。
VpnService实现
VpnService的逻辑写得相当简单,逻辑都在startProcesses方法里。
override suspend fun startProcesses(hosts: HostsFile) {
worker = ProtectWorker().apply { start() }
super.startProcesses(hosts)
sendFd(startVpn())
}
这里一共做了四件事情:
- 启动libss-local.so,也就是本地的SOCKS5服务器
- 启动libtun2socks.so,用来做协议转换
- 启动一个线程,用于监听和remote建连成功的fd,并protect起来
- 将tun设备的fd发送给tun2socks
我这里debug了一下,前两步执行的命令是:
#/lib/arm64/libss-local.so -b 127.0.0.1 -l 1080 -t 600 -S /data/user_de/0/com.github.shadowsocks/no_backup/stat_main -c /data/user/0/com.github.shadowsocks/no_backup/shadowsocks.conf -V -u -D --fast-open
#/lib/arm64/libtun2socks.so --netif-ipaddr 172.19.0.2 --socks-server-addr 127.0.0.1:1080 --tunmtu 1500 --sock-path sock_path --dnsgw 127.0.0.1:5450 --loglevel warning --netif-ip6addr fdfe:dcba:9876::2 --enable-udprelay
命令具体的参数我们后面的章节慢慢分析,这里只需要注意,两条命令中127.0.0.1:1080是相同的。tun2socks和ss-local正是通过这个socket通信的。
这里需要特别注意的是第三和第四步,这两个fd的传递体现了VpnService的关键作用。
fd的传递
这里传递的有两个fd,分别通过sock_path和protect_path两个local socket传递,我们从Java到native code来分析一下两个fd的传递过程以及作用。
VpnService -> tun2socks
VpnService中通过sock_path发送出去了一个fd,这里传入的fd其实就是VpnService.Builder.establish()方法返回的fd,也就是tun设备的fd。
private suspend fun sendFd(fd: FileDescriptor) {
var tries = 0
val path = File(Core.deviceStorage.noBackupFilesDir, "sock_path").absolutePath
while (true) try {
delay(50L shl tries)
LocalSocket().use { localSocket ->
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
localSocket.setFileDescriptorsForSend(arrayOf(fd))
localSocket.outputStream.write(42)
}
return
} catch (e: IOException) {
if (tries > 5) throw e
tries += 1
}
}
启动tun2socks的命令中,也通过--sock-path sock_path参数指定了这个local socket,那么看一下native code中对sock_path的处理。
tun2socks的入口是tun2socks.c的main方法,解析命令行参数的方法是
int parse_arguments (int argc, char *argv[])
命令行参数最后会被解析到如下结构体:
// command-line options
struct {
int help;
int version;
int logger;
#ifndef BADVPN_USE_WINAPI
char *logger_syslog_facility;
char *logger_syslog_ident;
#endif
int loglevel;
int loglevels[BLOG_NUM_CHANNELS];
char *netif_ipaddr;
char *netif_netmask;
char *netif_ip6addr;
char *socks_server_addr;
char *username;
char *password;
char *password_file;
int append_source_to_username;
char *udpgw_remote_server_addr;
int udpgw_max_connections;
int udpgw_connection_buffer_size;
int udpgw_transparent_dns;
#ifdef __ANDROID__
int tun_mtu;
int fake_proc;
char *sock_path;
char *pid;
char *dnsgw;
#else
char *tundev;
#endif
} options;
那么再看一下对char *sock_path的引用,整个方法比较简短:
#ifdef __ANDROID__
int wait_for_fd()
{
int fd, sock;
struct sockaddr_un addr;
if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
BLog(BLOG_ERROR, "socket() failed: %s (socket sock = %d)\n", strerror(errno), sock);
return -1;
}
int flags;
if (-1 == (flags = fcntl(fd, F_GETFL, 0))) {
flags = 0;
}
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
char *path = "/data/data/com.github.shadowsocks/sock_path";
if (options.sock_path != NULL) {
path = options.sock_path;
}
unlink(path);
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, path, sizeof(addr.sun_path)-1);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
BLog(BLOG_ERROR, "bind() failed: %s (sock = %d)\n", strerror(errno), sock);
close(sock);
return -1;
}
if (listen(sock, 5) == -1) {
BLog(BLOG_ERROR, "listen() failed: %s (sock = %d)\n", strerror(errno), sock);
close(sock);
return -1;
}
fd_set set;
FD_ZERO (&set);
FD_SET (sock, &set);
struct timeval tv = {10, 0};
for (;;) {
if (select(sock + 1, &set, NULL, NULL, &tv) < 0) {
BLog(BLOG_ERROR, "select() failed: %s\n", strerror(errno));
break;
}
int sock2;
struct sockaddr_un remote;
int t = sizeof(remote);
if ((sock2 = accept(sock, (struct sockaddr *)&remote, &t)) == -1) {
BLog(BLOG_ERROR, "accept() failed: %s (sock = %d)\n", strerror(errno), sock);
break;
}
if (ancil_recv_fd(sock2, &fd)) {
BLog(BLOG_ERROR, "ancil_recv_fd: %s (sock = %d)\n", strerror(errno), sock2);
close(sock2);
break;
} else {
close(sock2);
BLog(BLOG_INFO, "received fd = %d", fd);
break;
}
}
close(sock);
return fd;
}
#endif
这一段做的事情就是创建了一个UNIX域socket,对应VpnService中的LocalSocket,通过ancil_recv_fd接收VpnService发送过来的tun fd。ancil_recv_fd方法是libancillary提供的,这个库的作用就是通过UNIX域socket跨进程传递fd,库的实现就不展开分析。这个方法最后返回了接收到的fd,这个fd如何使用,我们在tun2socks一节里分析。
ss-local -> VpnService
还有一个fd,是由ss-local发送给VpnService,同样是通过libancillary发送,实现如下:
int
protect_socket(int fd)
{
int sock;
struct sockaddr_un addr;
if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
LOGE("[android] socket() failed: %s (socket fd = %d)\n", strerror(errno), sock);
return -1;
}
// Set timeout to 3s
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(struct timeval));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(struct timeval));
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "protect_path", sizeof(addr.sun_path) - 1);
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
LOGE("[android] connect() failed for protect_path: %s (socket fd = %d)\n",
strerror(errno), sock);
close(sock);
return -1;
}
if (ancil_send_fd(sock, fd)) {
ERROR("[android] ancil_send_fd");
close(sock);
return -1;
}
char ret = 0;
if (recv(sock, &ret, 1, 0) == -1) {
ERROR("[android] recv");
close(sock);
return -1;
}
close(sock);
return ret;
}
查看调用发现在如下方法中。
static void
server_stream(EV_P_ ev_io *w, buffer_t *buf) {
// ...
#ifdef __ANDROID__
if (vpn) {
int not_protect = 0;
if (remote->addr.ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)&remote->addr;
if (s->sin_addr.s_addr == inet_addr("127.0.0.1"))
not_protect = 1;
}
if (!not_protect) {
if (protect_socket(remote->fd) == -1) {
ERROR("protect_socket");
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return;
}
}
}
#endif
// ...
}
这个方法太长,只贴了相关的一部分,这里涉及到shadowsocks基于libev实现的事件驱动,在ss-local一节会详细分析,这里我们只需要知道,通过libancillary发送给VpnService的,其实是ss-local和远程服务端建立连接的fd。我们看VpnService对fd的处理:
private inner class ProtectWorker : ConcurrentLocalSocketListener("ShadowsocksVpnThread",
File(Core.deviceStorage.noBackupFilesDir, "protect_path")) {
override fun acceptInternal(socket: LocalSocket) {
socket.inputStream.read()
val fd = socket.ancillaryFileDescriptors!!.single()!!
CloseableFd(fd).use {
socket.outputStream.write(if (underlyingNetwork.let { network ->
if (network != null && Build.VERSION.SDK_INT >= 23) try {
network.bindSocket(fd)
true
} catch (e: IOException) {
// suppress ENONET (Machine is not on the network)
if ((e.cause as? ErrnoException)?.errno != 64) printLog(e)
false
} else protect(getInt.invoke(fd) as Int)
}) 0 else 1)
}
}
}
上层通过LocalSocket接收了ss-local发过来的fd,并通过平台API给fd添加了例外,使得这个fd的数据读写不经过tun设备。
tun2socks实现
上一节说到,tun2socks在wait_for_fd方法中通过UNIX域socket获取到了fd,我们来看一下接下来对fd的处理。
首先是一堆初始化操作:
// initialize network
if (!BNetwork_GlobalInit()) {
BLog(BLOG_ERROR, "BNetwork_GlobalInit failed");
goto fail1;
}
// process arguments
if (!process_arguments()) {
BLog(BLOG_ERROR, "Failed to process arguments");
goto fail1;
}
// init time
BTime_Init();
// init reactor
if (!BReactor_Init(&ss)) {
BLog(BLOG_ERROR, "BReactor_Init failed");
goto fail1;
}
// set not quitting
quitting = 0;
// setup signal handler
if (!BSignal_Init(&ss, signal_handler, NULL)) {
BLog(BLOG_ERROR, "BSignal_Init failed");
goto fail2;
}
这一段里最关键的是BReactor_Init方法,它初始化了一个BReactor结构,我们看一下这个结构定义:
typedef struct {
int exiting;
int exit_code;
// jobs
BPendingGroup pending_jobs;
// timers
BReactor__TimersTree timers_tree;
LinkedList1 timers_expired_list;
// limits
LinkedList1 active_limits_list;
#ifdef BADVPN_USE_WINAPI
LinkedList1 iocp_list;
HANDLE iocp_handle;
LinkedList1 iocp_ready_list;
#endif
#ifdef BADVPN_USE_EPOLL
int efd; // epoll fd
struct epoll_event epoll_results[BSYSTEM_MAX_RESULTS]; // epoll returned events buffer
int epoll_results_num; // number of events in the array
int epoll_results_pos; // number of events processed so far
#endif
#ifdef BADVPN_USE_KEVENT
int kqueue_fd;
struct kevent kevent_results[BSYSTEM_MAX_RESULTS];
int kevent_prev_event[BSYSTEM_MAX_RESULTS];
int kevent_results_num;
int kevent_results_pos;
#endif
#ifdef BADVPN_USE_POLL
LinkedList1 poll_enabled_fds_list;
int poll_num_enabled_fds;
int poll_results_num;
int poll_results_pos;
struct pollfd *poll_results_pollfds;
BFileDescriptor **poll_results_bfds;
#endif
DebugObject d_obj;
#ifndef BADVPN_USE_WINAPI
DebugCounter d_fds_counter;
#endif
#ifdef BADVPN_USE_KEVENT
DebugCounter d_kevent_ctr;
#endif
DebugCounter d_limits_ctr;
} BReactor;
BReactor其实是一个封装了各平台差异的结构,上层可以直接通过BReactor结构来实现事件驱动,而不用关心各平台底层的实现,无论是epoll,poll还是kevent。shadowsocks-android在编译的时候传入了BADVPN_USE_EPOLL,因此我们只要关心epoll相关的实现。
efd在BReactor_Init中被初始化,就是epoll_create返回的fd,这样就简单了,我们只需要看一下epoll_ctl的调用,看看哪些fd被挂到了epoll上,就能理清整体的逻辑。
epoll_ctl一共在三个方法被调用,三个方法分别是:
int BReactor_AddFileDescriptor (BReactor *bsys, BFileDescriptor *bs)
void BReactor_RemoveFileDescriptor (BReactor *bsys, BFileDescriptor *bs)
void BReactor_SetFileDescriptorEvents (BReactor *bsys, BFileDescriptor *bs, int events)
分别对应EPOLL_CTL_ADD,EPOLL_CTL_DEL和EPOLL_CTL_MOD操作。这里我们重点关注一下EPOLL_CTL_ADD,查一下BReactor_AddFileDescriptor的调用,每个BReactor_AddFileDescriptor都是和BFileDescriptor_Init成对出现的,BFileDescriptor_Init的实现:
void BFileDescriptor_Init (BFileDescriptor *bs, int fd, BFileDescriptor_handler handler, void *user)
{
bs->fd = fd;
bs->handler = handler;
bs->user = user;
bs->active = 0;
}
结合BFileDescriptor的定义:
/**
* File descriptor object used with {@link BReactor}.
*/
typedef struct BFileDescriptor_t {
int fd;
BFileDescriptor_handler handler;
void *user;
int active;
int waitEvents;
#ifdef BADVPN_USE_EPOLL
struct BFileDescriptor_t **epoll_returned_ptr;
#endif
#ifdef BADVPN_USE_KEVENT
int kevent_tag;
int kevent_last_event;
#endif
#ifdef BADVPN_USE_POLL
LinkedList1Node poll_enabled_fds_list_node;
int poll_returned_index;
#endif
} BFileDescriptor;
不难看出,BFileDescriptor_Init把真正的fd和处理事件的callback绑定起来了,BFileDescriptor则是用于封装底层实现的结构体。所以只要找到各个fd对应的BFileDescriptor_handler实现,就能理清tun2socks的整体逻辑。
ss-local实现
libev
LocalDnsService实现
redsocks实现透明代理
UDP协议代理
加解密实现
gfwlist
郑重声明
本文所涉及到的所有内容仅用于技术学习研究,任何非法用途产生的后果自负,与本人无关