ILD

使用MinGW开发windows路由管理程序
作者:Herbert Yuan 邮箱:yuanjp89@163.com
发布时间:2017-8-12 站点:Inside Linux Development

做这个程序的目的是在Windows PC上,批量添加路由条目到路由表。对于VPN用户来说,为了让国内访问不走VPN服务器,需要将国内ip段路由到第二连接(非VPN连接)。作为一个Linux程序员,理所当然选择MinGW开发Windows程序,它提供了类似Linux的编译环境。


1 源码组织与Makefile

使用典型的Linux源码组织结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
D:\WORK\PROJECT\WINROUTE
│  chinaip.dat
│  help.txt
│  Makefile
│  winroute.exe
├─inc
│      resource.h
│      route.h
├─obj
├─res
│      Application.manifest
│      ico.ico
│      resource.rc
└─src
        route.c
        winmain.c


各个文件:

主目录文件

Makefile:make文件。

help.txt:帮助文件。

chinaip.dat:中国网段数据库文件。

winroute.exe:生成的可执行文件。

src目录

源码.c目录

winmain.c:UI框架、main入口等。

route.c:路由操作接口。

res目录

资源文件目录

resource.rc:资源文件。

Application.manifest:应用程序配置文件。

ico.ico:图标文件。

inc目录

头文件目录

resource.h :资源头文件

route.h:路由接口头文件

obj目录编译中间文件。


Makefile文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# This Makefile will build the MinGW Win32 application.
 
HEADERS = inc/route.h inc/resource.h
 
OBJ_C = obj/winmain.o obj/route.o
OBJ_RC = obj/resource.o
 
CFLAGS = -O3 -std=c99 -D _WIN32_IE=0x0600 -D WINVER=0x0501 -Wall -I.\inc
 
ifeq (${CHARSET}, UNICODE)
    CFLAGS += -D UNICODE -D _UNICODE
endif
 
LDFLAGS = -s -lcomctl32 -lComdlg32 -lIphlpapi -lWs2_32 -Wl,--subsystem,windows
 
all: winroute
    .\winroute.exe
 
winroute: ${OBJ_C} ${OBJ_RC}
    gcc -o winroute.exe ${OBJ_C} ${OBJ_RC} ${LDFLAGS}
 
obj/winmain.o: src/winmain.c ${HEADERS}
    gcc ${CFLAGS} -c -o obj/winmain.o src/winmain.c
 
obj/route.o: src/route.c ${HEADERS}
    gcc ${CFLAGS} -c -o obj/route.o src/route.c
 
obj/resource.o: res/resource.rc res/Application.manifest res/ico.ico inc/resource.h
    windres -I.\inc -i $< -o $@
 
clean:
    del obj\*.o "winroute.exe"

CFLAGS的定义主要定义windows的版本。LDFLAGS主要是链接相关的动态库,需要根据使用到的Windows API接口来添加对应的动态库。另外-Wl,--subsystem,windows指定编译为窗口程序。使用windres命令来编译资源文件。其它的目标和gcc的使用与Linux的编译框架没有什么区别。


2 GUI框架搭建

选择使用对话框框架,预览:


2.1 WinMain

对于对话框程序来说,WinMain的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int APIENTRY WinMain(HINSTANCE hInstance,
                       HINSTANCE hPrevInstance,
                       LPTSTR    lpCmdLine,
                       int       nCmdShow)
{
    hInst = hInstance;
     
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);
 
    InitCommonControls();
     
    hDialog = CreateDialog (hInst, MAKEINTRESOURCE (DLG_MAIN), 0, (DLGPROC)DialogProc);
    if (!hDialog)
        return 1;
 
    MSG  msg;
    int status;
    while ((status = GetMessage (&msg, 0, 0, 0)) != 0)
    {
        if (status == -1)
            return -1;
        if (!IsDialogMessage (hDialog, &msg))
        {
            TranslateMessage ( &msg );
            DispatchMessage ( &msg );
        }
    }
 
    return msg.wParam;
}

创建对话框;消息主循环。


2.2 资源文件

资源文件相当标准,所有的控件需要手动添加。如要添加一个按钮,可在resouce.rc对话框区域添加:

1
PUSHBUTTON      "Batch Del",        IDC_BUTTON_BATCHDEL,          215,  115,   60,    12

IDC_BUTTON_BATCHDEL是定义在resouce.h中的宏,是按钮的id。在对话框消息处理中,消息来源的识别、控件窗口句柄获取等都依赖于该id。


2.3 对话框消息处理

相关消息包括初始化、通知、命令等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
BOOL CALLBACK DialogProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_INITDIALOG:
        {  
            HICON hIcon = LoadIcon (hInst, MAKEINTRESOURCE(DLG_ICON));        
            SendMessage(hwnd,WM_SETICON, (WPARAM)TRUE, (LPARAM)hIcon);
             
            hIcon = LoadIcon (hInst, MAKEINTRESOURCE(DLG_ICON_S));
            SendMessage(hwnd, WM_SETICON, (WPARAM)FALSE, (LPARAM)hIcon);
        }
         
        init_list_view(hwnd, IDC_LIST_IFS, ifs_header, ifs_header_w, 
            sizeof(ifs_header)/sizeof(ifs_header[0]));         
        init_list_view(hwnd, IDC_LIST_RTS, rts_header, rts_header_w, 
            sizeof(rts_header)/sizeof(rts_header[0]));
         
        SetWindowText( GetDlgItem(hwnd, IDC_EDITTEXT_FILE), DEFAULT_DB_NAME);
         
        refreshData(hwnd);
         
        {
            NOTIFYICONDATA nid;
            memset(&nid, 0, sizeof(nid));
            nid.cbSize = (DWORD)sizeof(NOTIFYICONDATA);
            nid.hWnd = hwnd;
            nid.uID = DLG_ICON;
            nid.uFlags = NIF_ICON|NIF_MESSAGE |NIF_TIP;
            nid.uCallbackMessage = WM_USER_STATUS;
            nid.hIcon = LoadIcon(hInst, MAKEINTRESOURCE(DLG_ICON)); 
            strcpy(nid.szTip, "winroute");
            Shell_NotifyIcon(NIM_ADD, &nid);
        }
         
        {
            RECT rectWin, rectDesk;
            HWND destWnd = GetDesktopWindow();
            GetWindowRect(hwnd, &rectWin);
            GetWindowRect(destWnd, &rectDesk);
            MoveWindow(
                hwnd, 
                (rectDesk.right-rectDesk.left)/2 - (rectWin.right-rectWin.left)/2,               
                (rectDesk.bottom-rectDesk.top)/2 - (rectWin.bottom-rectWin.top)/2,
                rectWin.right-rectWin.left, 
                rectWin.bottom-rectWin.top, 1);
        }
         
        return TRUE;
     
    case WM_NOTIFY:
        switch ( ((NMHDR *)lParam)->code)
        {
        case NM_CLICK:
            {
                LPNMITEMACTIVATE p = (LPNMITEMACTIVATE)lParam;
                 
                if (((NMHDR *)lParam)->idFrom == IDC_LIST_IFS)
                    click_interface(p->iItem, p->iSubItem);
                 
                if (((NMHDR *)lParam)->idFrom == IDC_LIST_RTS)
                    click_route(p->iItem, p->iSubItem);
            }
            return TRUE;
        }
        break;
         
    case WM_COMMAND:
        switch(LOWORD(wParam))
        {
        case MENU_ID_CLOSE:
            PostMessage(hwnd, WM_CLOSE, 0, 0);
            return TRUE;
        case IDC_BUTTON_LOAD:
            refreshData(hwnd);
            return TRUE;
        case IDC_BUTTON_DELALL:
            ui_modRoute(RTOP_DELALL);
            return TRUE;
        case IDC_BUTTON_SET:
            ui_modRoute(RTOP_SET);
            return TRUE;
        case IDC_BUTTON_ADD:
            ui_modRoute(RTOP_ADD);
            return TRUE;
        case IDC_BUTTON_DEL:
            ui_modRoute(RTOP_DEL);
            return TRUE;
        case IDC_BUTTON_SELECTFILE:
            get_db_file();
            return TRUE;
        case IDC_BUTTON_BATCHADD:
            ui_modRoute(RTOP_BATCH_ADD);
            return TRUE;
        case IDC_BUTTON_BATCHDEL:
            ui_modRoute(RTOP_BATCH_DEL);
            return TRUE;
        }
        break;
     
    case WM_USER_STATUS:  
        if (lParam == WM_LBUTTONUP) 
        {
            ShowWindow(hwnd, SW_SHOW);
            SetForegroundWindow(hwnd);
        }      
        if (lParam == WM_RBUTTONUP)
        {
            POINT pt;
            HMENU closeM = CreatePopupMenu();
            GetCursorPos(&pt);
            AppendMenu(closeM, MF_STRING, MENU_ID_CLOSE, "Close");
            SetForegroundWindow(hwnd);
            TrackPopupMenu(closeM, TPM_LEFTALIGN | TPM_BOTTOMALIGN, pt.x, pt.y, 0, hwnd, NULL);        
        }
        return TRUE;
         
    case WM_SYSCOMMAND:
        if (wParam == SC_MINIMIZE)
        {
            ShowWindow(hwnd, SW_HIDE);
            return TRUE;
        }
        break;
         
    case WM_DESTROY:
        {
            NOTIFYICONDATA nid;   
            memset(&nid, 0, sizeof(nid));
            nid.cbSize = (DWORD)sizeof(NOTIFYICONDATA);
            nid.hWnd = hwnd;
            nid.uID = DLG_ICON;  
            Shell_NotifyIcon(NIM_DELETE, &nid);
        }
        PostQuitMessage(0);
        return TRUE;
         
    case WM_CLOSE:
        DestroyWindow (hwnd);
        return TRUE;
    }
    return FALSE;
}


WM_INITDIALOG,对话框初始化消息,这里执行的操作包括:

  1. 创建图标。

  2. 初始化两个列表(设置表头)。

  3. 设置Database的路径为内置数据库名。

  4. 使用Shell_NotifyIcon()接口,在任务栏托盘创建图标。注册的自定义消息id为WM_USER_STATUS。

  5. 将对话框窗口移动到屏幕中央。


WM_NOTIFY,通知消息,处理两个列表的单击事件:

单击列表中的行时,会发送WM_NOTIFY消息给父窗口,lParam参数携带MITEMACTIVATE结构体数据、通过该数据可以获取NOFITY的类型、来源list id以及点击的item。


WM_COMMAND,命令消息,处理按钮单击事件和托盘菜单选择事件:

通过LOWORD(wParam)可以获取按钮id和菜单id。对于按钮id,处理单击事件;对于菜单id,处理菜单事件。


WM_USER_STATUS,托盘通知消息,处理托盘单击和右击事件:

通过lParam参数可以获取消息类型,单击消息,显示对话框并设置为前台。右击消息创建一个菜单,添加一个close菜单项,用于在托盘退出程序。菜单项的消息在如上WM_COMMAND消息中处理。


WM_SYSCOMMAND,系统命令消息,实现最小化时,隐藏窗口,只在托盘显示。


WM_DESTROY,窗口销毁消息,关闭托盘图标。


2.4 应用程序配置文件

设置为xp样式,并且设置运行等级为管理员运行。修改路由需要管理员权限,这里显示要求管理员权限,这样用户双击即可以管理员权限运行。

1
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>

其它内容可以参考标准的manifest文件。


3 路由管理

使用Win32 API实现,包括接口获取和路由管理两部分,代码位于route.c。


3.1 获取接口列表

使用两个API来获取接口的信息:GetAdaptersInfo()和getAdaptersAddresses()。这两个接口的使用可直接参考msdn的页面。

获取的数据存储到两个全局指针:

1
2
PIP_ADAPTER_INFO g_pAdapterInfo = NULL;
PIP_ADAPTER_ADDRESSES g_pAddresses = NULL;

前者包含接口的ip、掩码、网关等,后者包含接口的Metric信息等。


由于在Makefile中声明Windows版本为XP,而Metric字段在Vista以后的版本才会携带,所以这里有几个诡计:

首先、需要获取运行的操作系统版本,通过以下接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL IsWindowsVersionOrGreater(WORD major, WORD minor, WORD servpack)
{
    OSVERSIONINFOEX vi = {sizeof(vi),major,minor,0,0,{0},servpack};
    return VerifyVersionInfo(&vi, VER_MAJORVERSION|VER_MINORVERSION|VER_SERVICEPACKMAJOR,
        VerSetConditionMask(VerSetConditionMask(VerSetConditionMask(0,
            VER_MAJORVERSION,VER_GREATER_EQUAL),
            VER_MINORVERSION,VER_GREATER_EQUAL),
            VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL));
}
 
BOOL IsWindowsVistaOrGreater(void) {
    return IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_VISTA), LOBYTE(_WIN32_WINNT_VISTA), 0);
}

在VC中,这个接口在头文件VersionHelpers.h中定义,但是MinGW32并没有添加这个头文件,所以在winmain.c添加上述接口。

然后,MinGW32中IP_ADAPTER_ADDRESSES的定义是XP上的定义,并没有Metric字段,这也需要我们手动添加一个Vista上的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
typedef struct _IP_ADAPTER_ADDRESSES_VISTA {
  union {
    ULONGLONG Alignment;
    struct {
      ULONG Length;
      DWORD IfIndex;
    };
  };
  struct _IP_ADAPTER_ADDRESSES  *Next;
  PCHAR                              AdapterName;
  PIP_ADAPTER_UNICAST_ADDRESS        FirstUnicastAddress;
  PIP_ADAPTER_ANYCAST_ADDRESS        FirstAnycastAddress;
  PIP_ADAPTER_MULTICAST_ADDRESS      FirstMulticastAddress;
  PIP_ADAPTER_DNS_SERVER_ADDRESS     FirstDnsServerAddress;
  PWCHAR                             DnsSuffix;
  PWCHAR                             Description;
  PWCHAR                             FriendlyName;
  BYTE                               PhysicalAddress[MAX_ADAPTER_ADDRESS_LENGTH];
  DWORD                              PhysicalAddressLength;
  DWORD                              Flags;
  DWORD                              Mtu;
  DWORD                              IfType;
  IF_OPER_STATUS                     OperStatus;
  DWORD                              Ipv6IfIndex;
  DWORD                              ZoneIndices[16];
  PIP_ADAPTER_PREFIX                 FirstPrefix;
  ULONG64                            TransmitLinkSpeed;
  ULONG64                            ReceiveLinkSpeed;
  PVOID                          FirstWinsServerAddress;
  PVOID                                FirstGatewayAddress;
  ULONG                              Ipv4Metric;
  ULONG                              Ipv6Metric;
#if 0
  IF_LUID                            Luid;
  SOCKET_ADDRESS                     Dhcpv4Server;
  NET_IF_COMPARTMENT_ID              CompartmentId;
  NET_IF_NETWORK_GUID                NetworkGuid;
  NET_IF_CONNECTION_TYPE             ConnectionType;
  TUNNEL_TYPE                        TunnelType;
  SOCKET_ADDRESS                     Dhcpv6Server;
  BYTE                               Dhcpv6ClientDuid[MAX_DHCPV6_DUID_LENGTH];
  ULONG                              Dhcpv6ClientDuidLength;
  ULONG                              Dhcpv6Iaid;
  PIP_ADAPTER_DNS_SUFFIX             FirstDnsSuffix;
#endif
} IP_ADAPTER_ADDRESSES_VISTA, *PIP_ADAPTER_ADDRESSES_VISTA;

#if 0中的内容是我们并不需要的,可以关闭,因为有些字段的类型在MinGw中未定义,直接关闭这些字段即可。

最后,在使用时先判断是否为Vista,是的话直接强制转换类型获取Metric。

1
2
3
4
5
6
7
8
if (IsWindowsVistaOrGreater()) { 
    // vista and later 
    char met[32];
    pCurrAddressesVista = (PIP_ADAPTER_ADDRESSES_VISTA) pCurrAddresses;
    PEND_FMT("Ipv4Metric: %lu\n", pCurrAddressesVista->Ipv4Metric);
    sprintf(met, "%lu", pCurrAddressesVista->Ipv4Metric);
    set_editor_text(IDC_EDITTEXT_METRIC, met);
}



3.2 获取路由列表

使用WIN32 API,GetIpForwardTable()来获取路由表,具体从msdn参考。 获取到的路由表存储到全局变量

1
PMIB_IPFORWARDTABLE g_pIpForwardTable = NULL;

实际上,内存是在GetIpForwardTable()分配的,只是存储返回的指针。


3.3 操作路由条目

使用3个WIN32 API来操作路由,SetIpForwardEntry()修改已存在的路由、CreateIpForwardEntry()创建新的路由、DeleteIpForwardEntry()删除已存在路由。

路由信息由UI输入框设置。

上述3个函数的参数均为PMIB_IPFORWARDROW。根据msdn,需要设置PMIB_IPFORWARDROW的下述信息。

首先,必须清0。

其次、dwForwardProto字段必须设置为MIB_IPPROTO_NETMGMT,MinGw并没有定义MIB_IPPROTO_NETMGMT,在route.h手动定义其为3。

最后、必须设置dwForwardDest、dwForwardMask、dwForwardNextHop、dwForwardMetric1字段。


3.4 批量操作路由条目

批量操作包括批量添加和批量删除,就是重复调用单次操作。每次操作只有dwForwardDest不同。dwForwardDest可以从数据库文件或者内置数组获取。当输入的数据库匹配:CHINA时,使用内置数组数据,否则当作为数据库文件。

内置数组每一个网段以usigned long存储,前24位存储ip段,后8位存储前缀,这样做可以节省内存,缺点是只能存储24位一下的网段。

从数据库文件批量添加时,就是读文本行,然后解析IP网段,每一行的格式为"ip段/前缀",如"192.168.0.1/32"。

批量操作时,失败即返回。


4 体验

首先创建VPN连接,开启winroute程序。

两步操作,即可添加国内IP段走非VPN连接。首先点击网卡接口,会自动将网卡的网关、index、Metric设置到对应的输入框。确保Database为:CHINA,启动时自动设置为:CHINA。点击Batch Add按钮添加。


使用域名访问时有两个问题。

走VPN连接解析域名,可能导致某些国内域名解析到国外ip,从而路由到VPN连接,导致访问速度慢,如v.youku.com。

第二连接解析域名,域名可能被污染或被劫持,导致解析到错误的外国域名。

目前还没想到好的解决方法。

参考

[1] Building Win32 GUI Applications with MinGW. http://www.transmissionzero.co.uk/computing/win32-apps-with-mingw/

[2] Windows API Tutorial: Dialog-based App. https://www.relisoft.com/win32/windlg.html


附件

Copyright © insidelinuxdev.net 2017-2021. Some Rights Reserved.