将stunnel修改成一个透明代理--附:setjmp/longjmp

晨曦之光 发布于 2012/04/10 14:59
阅读 1K+
收藏 0

stunnel的代码很简单,简单得没法形容,其执行过程分为下面几个部分:
1.main-loop:
while(1) {
    fd = accept
    do_client--可应用一些mpm
}
2.do_client:
if(server) {
    init_ssl
    init_remote
} else if (client) {
    init_remote
    init_ssl
}
transfer
3.init_ssl:
SSL_new
if(server) {
    SSL_accept
} else if (client) {
    SSL_connect
}
4.init_remote:
if (server) {
    connect-real-server
    连接真实的服务器
} else if (client) {
    fd作为和用户通信的套接字
}
5.transfer:
while (没有关闭连接){
    调用poll或者selece或者epoll等循环将数据从socket中读取然后写入SSL,以及从SSL中读取然后写入socket,实现透明数据转发
}
以上就是stunnel的工作流程,stunnel总是将自己作为client,在stunnel的客户端,它将自己作为stunnel客户端,在stuunel服务器,它将自己作为真实服务器的客户端,这样是很合理的,因为作为代理,它的意义将数据发送给最终的服务,并且向用户转发服务的响应,代理和转发过程中,通过修改stuunel的代码可以实现一个简单的七层过滤功能,不管是stunnel客户端还是stunnel服务器,在代理意义上,前面总是有stunnel的目的地,stunnel始终都是主动地去连接别的主机,即使stunnel服务器会接受stunnel客户端的连接,但是这无非只是为了建立一条安全的应用层隧道,和真正的业务数据无关。
     按照stunnel的手册做,很容易使用stunnel配置一个反向代理服务器,诸如下面的配置文件:
server:
...
[https]
accept  = 443
connect = 80
client:
...
[https]
accept  = 448
connect = 443
这样在用户浏览器中配置stunnel客户端的448端口为代理服务器,所有的80端口的访问全部由stunnel来代理,这就是不透明的隧道式反向代理,所谓不透明是因为需要对用户浏览器进行配置,所谓反向代理是因为真正的目的地址完全由代理服务器决定,所谓隧道式指的是代理服务器并不是直接转发请求,而是分为了c-s模式,c-s之间建立一条安全隧道,这样比较适用于远程代理或者vpn等环境,同时也便于卸载用户环境的代理压力,可以将代理请求本身发往处理能力很强的远端stunnel服务器,本地的代理只需要接收代理请求并转发即可。
     可是stunnel无法实现正向代理,因为对于http的正向代理需要解析http请求的头,而这个协议头是应用层数据,stunnel中并没有解析数据,只是透明地转发(详细情况参考transfer函数),因此如果想实现正向http代理则必须对stunnel代码进行一些修改,也就是解析stunnel服务器接收到的第一个请求包,从GET中解析出真实的地址信息后,然后再决定如何转发,这就涉及到了http协议,本文不谈。即使对http的正向代理实现了,那么对其它基于tcp协议的应用的正向代理如何实现呢?这就需要stunnel知道客户端需要访问的真实地址信息,因此这种代理很大程度上也就是透明代理,通过设置路由或者在windows上通过spi的方式将客户端对tcp应用的访问以stunnel客户端为下一跳,当请求过来后,通过下面的配置将请求重定向到stunnel客户端:
iptables -t nat -A PREROUTING --protocol tcp -m tcp -j REDIRECT --to-port xxx
xxx为stunnel客户端监听的端口。如此一来所有的请求都被定向到了stunnel,下面的问题是如何能得到这些请求原始的目的地信息,如果你很精通nat原理以及linux的实现,那么这个问题很简单,直接通过下面的调用就能得到:
getsockopt (fd, SOL_IP, SO_ORIGINAL_DST, &addr, &size);
得到了这个原始地址信息之后,需要想办法将之传给stunnel服务器,这样stunnel服务器才能根据这个地址信息来决定如何做,这时stunnel并不需要进行类似反向代理那样的配置。我们可以自定义一个通道将这个信息传过去,可是那样实现很松散,必须考虑连接和这个原始地址信息的对应关系,因此不用。由于stunnel客户端和stunnel服务器之间是一个ssl连接,在RFC5246中对ssl协议进行了更新,client-hello消息如下:
struct {
    ProtocolVersion client_version;   
    Random random;
    SessionID session_id;
    CipherSuite cipher_suites<2..2^16-2>;
    CompressionMethod compression_methods<1..2^8-1>;
    select (extensions_present) {
    case false:
        struct {};
    case true:  //多了一个extension字段
        Extension extensions<0..2^16-1>;
    };
} ClientHello;
因此完全可以将这个地址信息通过这个client-hello消息的extension传给stunnel服务器,由于OpenSSL本身并没有将特性导出接口,故而需要对OpenSSL代码进行修改,本文省略。在stunnel服务器端,可以从这个hello消息中将该原始地址信息解析出来,然后以此信息进行连接决策。到此为止,整套的解决方案已经有了,下面就是如何修改代码的问题了。过程如下:
第一步:配置上述的nat规则;
第二步:其次修改prototypes.h中的CLI结构体,添加几个字段,并且添加相应的结构体定义:
typedef struct {
    union {
        struct sockaddr_in addr;
        char szAddr[16];
        char szPort[6];
    } addr;  //传输hello消息时,最好将信息编码为字符串,但是使用它进行connect的时候还是需要转化为sockaddr_in
    char ext[0];
}ORIG_ADDR;
typedef struct {
...
} REAL_ADDR_CTX;
typedef struct {
    ...
    ORIG_ADDR    orig;  //原始地址信息
    REAL_ADDR_CTX orig_ctx; //ssl中和hello-extension相关的结构体
} CLI;
第三步:修改alloc_client_session,增加:
if (opt->option.client) {
    int err = 1;
    socklen_t sin_size = sizeof(struct sockaddr_in);
        err = getsockopt (rfd, SOL_IP, SO_ORIGINAL_DST, &c->orig.addr, &sin_size);
    ...
    //为c->orig.addr赋值,转化为字符串格式   
}
第四步:修改init_ssl函数,服务器和客户端添加的代码不同:
1.SSL_new之后:
if (c->opt->option.client){
    //对于client,用用户的真实目的地址信息初始化CLI的orig_ctx字段,也就是将之填入ssl相关的结构体,SSL_connect的时候会通过client-hello发送
    } else {
    //对于server,准备好CLI的orig_ctx字段,准备在SSL_accept之后接收用户的原始地址信息   
}
2.SSL_accept(c->ssl)之后(肯定是server的情况):
取出client-hello-extension的消息,初始化CLI的orig_ctx结构体
第五步:修改connect_remote函数,初始化连接地址结构体前添加:
if(!c->opt->option.client){  //仅仅在server端添加
    ret = callback(...);//在callback中可以根据策略修改最终的地址,也就是修改c->orig.addr.addr
    memcpy(&addr.sa, &c->orig.addr.addr, sizeof addr);   
}
以上五个步骤做完,stunnel也就成了一个透明代理了,用户无需在本地做任何配置(当然需要将网关设置为stunnel客户端,在windows上还可以利用spi),stunnel作为透明代理就转发了你的tcp请求(可能还会冲定向或者拦截),当然还可以进一步修改,使一切可配置化,不过这就和框架原理没有关系了。
附:setjmp和longjmp
stunnel使用setjmp来安装错误处理代码,这一点感觉很好,比如在一个连接刚建立并且处理业务之前,即run_client的开头,有如下调用:
static void run_client(CLI *c) {
    ...
    error=setjmp(c->err);
    if(!error) //直接调用setjmp,其返回为0,如果是从longjmp中返回的setjmp,其返回为1
        do_client(c); //直接调用setjmp相当于仅仅安装错误处理代码,do_client完整的实现了业务逻辑,调用层次很深。
//如果do_client中以及其调用过程中的某处出现错误,就会调用longjmp(c->err),然后执行绪到达上面setjmp处,返回1,此时就要真正处理错误了,下面的代码全部是错误处理
    s_log(...); //日志记录
...//资源清理后返回,结束此次连接
}
需要注意的是,虽然longjmp可以使得执行绪跳转到setjmp处,但是由于jmp_buf是基于当前栈帧的,因此必须在比setjmp调用层次更深的地方调用longjmp,也就是说setjmp的调用函数不能返回,返回了,longjmp就失效了,之所以有的情况下setjmp返回了longjmp还可用,那是因为栈虽然清了,为了效率只是改写了esp,ebp等寄存器的值,栈上的数据依然存在,因此longjmp恢复setjmp的jmp_buf时,数据依然是存在的,但是已经不可用了。因此一般的做法是,在处理的开始处调用setjmp安装错误处理代码,然后在该栈帧不返回前提下,后续的代码出错后调用longjmp。


原文链接:http://blog.csdn.net/dog250/article/details/5738218
加载中
返回顶部
顶部