日期:2014-05-17  浏览次数:21112 次

Windows上的OpenVPN如何封装真实IP作为源地址
OpenVPN引入了一个虚拟的网段,进而每个节点都有一个虚拟的IP地址。为了在加入OpenVPN之后保证原有的网络拓扑不更改,必要的做法就是不让虚拟IP跑出VPN节点之外,也就是说虚拟IP地址不能在任何非VPN网段出现,因为在加入OpenVPN之前,根本就没有这些IP地址,现在加入了,也不应该有。或许很多的防火墙会把这种奇怪的地址直接屏蔽掉。
        然而当OpenVPN用于Windows平台的时候,问题就来了。由于Windows在发出数据包的时候,其源地址完全是根据路由选择而来的,人们无法干预源地址的选择,一旦数据包需要经由虚拟网卡发出,那么其源地址自然也就成了虚拟IP地址,这样为了保证网络拓扑的无改动,需要在服务端数据包从真实网卡路由出去的口子上作一个SNAT将源地址改回Windows的真实IP地址。这一次的SNAT导致了通信变成了单向的,也就是说只允许Windows客户端通过VPN发起访问,而其它网段无法通过VPN对Windows进行访问。要想将通信变为双向的,就需要再做一次DNAT,一个Windows客户端接入导致了服务端两条NAT规则的添加,这明显是一种修补策略,太撇脚了。
        如果能让通信从Windows客户端发起的时候就使用真实IP地址作为源地址,任何问题就解决了。也就是说,如果Windows的路由命令可以支持Linux的iproute2的src参数就好了:
ip route add 1.2.3.4/32 via $VPN服务端虚拟地址 dev tap0 src $客户端真实地址
遗憾的是,Windows不支持这个,即便是Windows 2008也不支持用户手工指定源IP地址。搞技术的碰到这个问题,最显然的想法就是解决它而不是避开它,目标只有一个,那就是不让虚拟IP地址流到VPN网络之外,并且不通过地址转换来做,因为我知道Linux的地址转换是基于流的,要配置就要配两条,很不爽,非Linux的也没有实验环境,最关键的,我就是Linux程序员,术业有专攻。解决这个问题的方法最直接的就是编写NDIS驱动,然而那样动作太大了,并且它只和OpenVPN(目前)有关,因此就想到使用OpenVPN的plugin,注册up事件来做,这是合理的。然而问题是怎么做,由于不熟悉Windows开发环境,也不能恋战,不知怎么突然就想到了LSP,那就顺着这个路子走下去吧,幸亏能走通,否则就又和去年折腾虚拟文件系统时那样一场悲剧了。我只知道LSP可以在标准的Winsock上添加自己的逻辑,就是个钩子链,具体的代码还是需要请教公司的Windows高手以及互联网上查找现有的代码,虽然有时我也浏览MSDN,还是没有百度来的快,对于这种代码,百度比google快。
        大概花了5个小时时间,代码调通了,并且很好的和OpenVPN联动,也就是实现了OpenVPN的plugin,如果一个去往1.2.3.4的包需要走虚拟网卡,本机的物理网卡IP地址11.22.33.44,那么可以完美实现数据包从虚拟网卡发出时,封装11.22.33.44作为源地址,而不是封装虚拟地址128.129.0.3。代码如下:
#include <ws2spi.h>
#include <errno.h>
#include <fstream>
#include <list>
#include <Sporder.h>        // 定义了WSCWriteProviderOrder函数
#include <windows.h>
#include <stdio.h>

#pragma comment(lib, "Rpcrt4.lib")
#pragma   comment(lib,"Ws2_32.lib")
GUID filterguid = {0xd3c21122, 0x85e1, 0x48f3,
{0x9a,0xb6,0x23,0xd9,0x0c,0x73,0x07,0xef}};
LPWSAPROTOCOL_INFOW  ProtoInfo=NULL;
WSPPROC_TABLE        NextProcTable;
DWORD                ProtoInfoSize=0;
int                  TotalProtos=0;

using namespace std;

//将socket与“是否bind源IP地址”进行关联
typedef struct  {
    SOCKET s;
    BOOL bd;
}sockets;
list<sockets*> li;

/*************************************************************************/
/*                            以下安装LSP                                */
/* 代码来自:http://hi.baidu.com/hiliqirun/item/194805be5695c6462bebe3f0 */
/*************************************************************************/
GUID ProviderGuid = {0xd3c21122, 0x85e1, 0x48f3,
{0x9a,0xb6,0x23,0xd9,0x0c,0x73,0x07,0xef}};
LPWSAPROTOCOL_INFOW GetProvider(LPINT lpnTotalProtocols)
{
    DWORD dwSize = 0;
    int nError;
    LPWSAPROTOCOL_INFOW pProtoInfo = NULL;

    // 取得需要的长度
    if(::WSCEnumProtocols(NULL, pProtoInfo, &dwSize, &nError) == SOCKET_ERROR){
        if(nError != WSAENOBUFS)
            return NULL;
    }

    pProtoInfo = (LPWSAPROTOCOL_INFOW)::GlobalAlloc(GPTR, dwSize);
    *lpnTotalProtocols = ::WSCEnumProtocols(NULL, pProtoInfo, &dwSize, &nError);
    return pProtoInfo;
}
void FreeProvider(LPWSAPROTOCOL_INFOW pProtoInfo)
{
    ::GlobalFree(pProtoInfo);
}
BOOL InstallProvider(WCHAR *pwszPathName)
{
    WCHAR wszLSPName[] = L"ZetsinLSP";
    LPWSAPROTOCOL_INFOW pProtoInfo;
    int nProtocols;
    WSAPROTOCOL_INFOW OriginalProtocolInfo[3];
    DWORD             dwOrigCatalogId[3];
    int nArrayCount = 0;
    DWORD dwLayeredCatalogId;        // 我们分层协议的目录ID号
    int nError;

    // 找到我们的下层协议,将信息放入数组中
    // 枚举所有服务程序提供者
    pProtoInfo = GetProvider(&nProtocols);
    BOOL bFindUdp = FALSE;
    BOOL bFindTcp = FALSE;
    BOOL bFindRaw = FALSE;
    for(int i=0; i<nProtocols; i++){
        if(pProtoInfo[i].iAddressFamily == AF_INET){
            if(!bFindUdp && pProtoInfo[i].iProtocol == IPPROTO_UDP){
                memcpy(&OriginalProtocolInfo[nArrayCount], &pProtoInfo[i], sizeof(WSAPROTOCOL_INFOW));
                OriginalProtocolInfo[nArrayCount].dwServ