大多数有网络意识的程序都应该使用密码术保护数据,以免数据被偷窥,但许多程序都没有这样做,可能是因为程序本身是旧应用程序,或者因为安全套接字层(Secure Sockets Layer,SSL)很难加入到应用程序中。Stunnel 是一种程序,使程序员和系统管理员可以很轻松地对任意 TCP 会话加密。您可以很轻松地在客户机和服务器上启用 SSL — 而且这样做不会影响程序源代码。
SSL 挑战
在大多数开发环境中,并不是在产品的整个生命周期中都考虑安全性而是在出现安全性问题后才采取补救措施。从传统的观点来看,这是坏事 — 事后亡羊补牢很困难,就象向一个写得马马虎虎的软件添加一个可靠性模块来除去错误一样困难。
象这些亡羊补牢技术从安全性立场来看好像是可行的,但实际上不太可靠。例如,您可以写一段代码,建立网络连接,然后连接到一个库 — 该库用执行加密和认证的版本代替所有的传统网络调用。
实际上,这种集成的容易性是安全套接字层,或称为 SSL(用于 secure HTTP 连接的协议)的最初目的之一。SSL 的几种实现已经在试图顺便替代标准 Berkeley UNIX socket API,或者带有尽可能相似接口的库。
OpenSSL 库(请参阅参考资料)是尝试后一种方法的一个很好的示例。在 OpenSSL 库中,相似的 API 模仿传统的套接字调用,使用 SSL 上下文对象代替文件描述符。例如,传统的写入套接字的调用具有以下特征:
/* Returns the number of characters successfully written */
size_t write(int file_descriptor, void *buf, size_t len)
OpenSSL 更改每个参数的类型,但每个参数的语义不变:
/* Returns the number of characters successfully written */
int SSL_write(SSL *socket_info, char *buf, int len)
实际上,除 SSL 对象之外,其它所有的类型与初始调用都是兼容的。理想情况下,开发者能够对程序进行较小的修改,只需添加一些代码从文件描述符初始化 SSL 上下文即可。
而事实上,SSL 库都不容易使用。例如,开发者要写许多附加代码才能使 OpenSSL 在多线程环境下工作。实际上,为将这个库集成到代码中去,大多数开发组织花费的精力都比他们预计的要多的多,而结果还常常是一片混乱。
Stunnel
幸运的是,有一种方法可以将加密功能无缝添加到网络连接中,而不会将您原来的代码段基址搞乱。Stunnel 是一个程序,可以使用 OpenSSL 库对任意 TCP 会话进行加密。它作为服务器运行在程序外部。Stunnel 服务器主要执行两个功能:一,首先,接收未加密的数据流,进行 SSL 加密,然后将其通过网络发送;二,对已进行 SSL 加密的数据流进行解密,并将其通过网络发送给另一个程序(该程序通常驻留在同一机器上,以避免本地网络上的窥探攻击)。
这样,在必要时,您就可以很容易地运行未加密的程序,当您想要“嗅探”网络,看看到底有什么东西正在通过网络时,这一点很有用。
即使您是一个系统管理员,而不是一个开发者,Stunnel 对您来说也是一个强大的武器,因为它可以向不启用 SSL 的服务器端软件添加 SSL。例如,我已经使用 Stunnel 来保护 POP、SMTP 和 IMAP 服务器。唯一不太尽人意的地方是要使用这些服务器的安全版本,客户机必须是可识别 SSL 的。
Stunnel 要求已经安装了 OpenSSL。它已被移植到了 Windows,以及大多数 UNIX 平台。
一旦安装了 Stunnel,用它来保护服务器就很轻松。例如,您可以通过将常规服务绑定到本地主机使 IMAP 服务器启用 SSL,然后在外部 IP 地址(假设 IMAP 服务器已经在运行,且外部地址为 192.168.100.1)运行 Stunnel:
stunnel -d 192.168.100.1:imap2 -r 127.0.0.1:imap2
-d 标志指定我们希望用来运行自己的安全服务的端口。imap2 字符串指定使用标准 IMAP 端口;我们也可以将其设为 143。Stunnel 检查 "/etc/services" 文件以便将符号名映射到端口号。并非所有的机器都拥有这个文件(有些机器并不列出所有的标准服务),所以使用数字比使用服务名更方便。
-r 标志指定未加密的 IMAP 服务器运行所在的端口。
这个解决方案要求您的 IMAP 服务器只在回送(loopback)接口上侦听。如果 IMAP 服务器绑定到 IP 地址“0.0.0.0”,那么它将侦听机器上每个 IP 地址上的信息,包括 192.168.100.1;这会导致出现一条出错消息,指出我们的安全服务端口已在使用中。大多数服务都可以配置为只绑定到一个接口。不然的话,可能要更改一行代码。
另外,您可以将一个未加密的服务器设在一个非标准端口上。例如,您可以在端口 1143 上运行 IMAP,然后将安全的 IMAP 数据流转发到该端口。一般情况下,您不希望其它机器上的用户访问您未加密的服务。运行服务的机器上的个人防火墙可以加强这种策略。
使用 Stunnel 来保护如 IMAP 等服务面临的一个问题是服务器只接收来自我们提供的 Stunnel 服务器的连接。因此,所有的连接都看起来好象是来自本地机器。在 Linux 上,可以通过传递 -T 标志避开这个问题,传递 -T 标志可以使 Stunnel 服务器透明地代理信息包,这样真正的服务器就可以得到接收到的所有信息包中的正确的源地址。
用于客户机的 Stunnel
还可以使用客户机的 Stunnel 与服务器连接,不过要多做一些工作。首先,必须生成 Stunnel 作为外部进程。在基于 UNIX 的系统上,执行这个操作的最好方法是 fork() 客户机,并让子进程 execv() stunnel。父进程必须准备两套文件描述符用来与子进程通信 — 一对用于从 Stunnel 进程读取数据,另一对用于通过网络发送数据。这个工作量不小。实现这项功能的示例代码,提供一个简单的函数调用 run_cmd,掩盖潜在的复杂性;run_cmd 使用一个字符串指出要运行的命令,并返回一个 PIPE 对象,该对象有一个文件描述符,套接字使用该文件描述符进行读写操作:
pipe.h:
#ifndef POPEN_H__
#define POPEN_H__
#include
#include
#define EXITVAL 127
typedef struct pipe_st {
FILE *read_ptr;
FILE *write_ptr;
pid_t pid;
} PIPE;
PIPE *run_cmd(char *cmd);
int pipe_close(PIPE *p);
#endif POPEN_H__
pipe.c:
#include
#include
#include
#include
#include
#include
#include "pipe.h"
/* We allow double quotes and \ to escape spaces.
* All backslashes are "processed", despite the value
* of the next character. (Though \\ -> \).
* We don't care if there's a missing trailing quote,
* even if it should really be a syntax error.
*/
static char **
to_words(char *arg) {
char **arr;
char *p = arg;
int nw = 1;
int slc = 0;
int slm = 0;
char c;
short quote = 0;
char *cur;
/* Build a rough approximation of the number of words,
* simply so we don't malloc too low.
*/
while((c = *p++)) {
if(c == '"' || c == ' ') {
nw++;
if(slm < slc) slm = slc;
slc = 0;
}
}
arr = (char **)malloc(sizeof(char *)*(nw+1));
quote = nw = slc = 0;
p = arg;
cur = (char *)malloc(sizeof(char)*(slm+1));
arr[nw++] = cur;
while((c = *p++)) {
switch(c) {
case '"':
quote = !quote;
continue;
case ' ':
if(quote) {
*cur++ = c;
slc++;
continue;
} else {
if(!slc) continue;
*cur = 0;
cur = (char *)malloc(sizeof(char)*(slm+1));
arr[nw++] = cur;
slc = 0;
continue;
}
case '\\':
if(*p) {
*cur++ = *p++;
slc++;
continue;
}
default:
*cur++ = c;
slc++;
continue;
}
}
*cur = 0;
arr[nw] = 0;
return arr;
}
PIPE *
run_cmd(char *cmd) {
int prpd[2];
int pwpd[2];
pid_t pid;
char **args;
PIPE *ret;
args = to_words(cmd);
if(pipe(prpd) < 0 || pipe(pwpd) < 0) {
return 0; /* Pipe failed. */
}
pid = fork();
switch(pid) {
case -1:
close(prpd[STDIN_FILENO]);
close(prpd[STDOUT_FILENO]);
close(pwpd[STDIN_FILENO]);
close(pwpd[STDOUT_FILENO]);
return 0; /* Fork failed. */
/* Here we can only exit on error. */
case 0:
/* Child... */
if(dup2(pwpd[STDIN_FILENO], STDIN_FILENO) < 0) {
exit(EXITVAL);
}
if(dup2(prpd[STDOUT_FILENO], STDOUT_FILENO) < 0) {
阅读(1263) | 评论(0) | 转发(0) |