做这个程序的目的是在Windows PC上,批量添加路由条目到路由表。对于VPN用户来说,为了让国内访问不走VPN服务器,需要将国内ip段路由到第二连接(非VPN连接)。作为一个Linux程序员,理所当然选择MinGW开发Windows程序,它提供了类似Linux的编译环境。
使用典型的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的编译框架没有什么区别。
选择使用对话框框架,预览:
对于对话框程序来说,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; } |
创建对话框;消息主循环。
资源文件相当标准,所有的控件需要手动添加。如要添加一个按钮,可在resouce.rc对话框区域添加:
1 | PUSHBUTTON "Batch Del", IDC_BUTTON_BATCHDEL, 215, 115, 60, 12 |
IDC_BUTTON_BATCHDEL是定义在resouce.h中的宏,是按钮的id。在对话框消息处理中,消息来源的识别、控件窗口句柄获取等都依赖于该id。
相关消息包括初始化、通知、命令等。
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,对话框初始化消息,这里执行的操作包括:
创建图标。
初始化两个列表(设置表头)。
设置Database的路径为内置数据库名。
使用Shell_NotifyIcon()接口,在任务栏托盘创建图标。注册的自定义消息id为WM_USER_STATUS。
将对话框窗口移动到屏幕中央。
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,窗口销毁消息,关闭托盘图标。
设置为xp样式,并且设置运行等级为管理员运行。修改路由需要管理员权限,这里显示要求管理员权限,这样用户双击即可以管理员权限运行。
1 | <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> |
其它内容可以参考标准的manifest文件。
使用Win32 API实现,包括接口获取和路由管理两部分,代码位于route.c。
使用两个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); } |
使用WIN32 API,GetIpForwardTable()来获取路由表,具体从msdn参考。 获取到的路由表存储到全局变量
1 | PMIB_IPFORWARDTABLE g_pIpForwardTable = NULL; |
实际上,内存是在GetIpForwardTable()分配的,只是存储返回的指针。
使用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字段。
批量操作包括批量添加和批量删除,就是重复调用单次操作。每次操作只有dwForwardDest不同。dwForwardDest可以从数据库文件或者内置数组获取。当输入的数据库匹配:CHINA时,使用内置数组数据,否则当作为数据库文件。
内置数组每一个网段以usigned long存储,前24位存储ip段,后8位存储前缀,这样做可以节省内存,缺点是只能存储24位一下的网段。
从数据库文件批量添加时,就是读文本行,然后解析IP网段,每一行的格式为"ip段/前缀",如"192.168.0.1/32"。
批量操作时,失败即返回。
首先创建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