diff --git a/.gitignore b/.gitignore index 3bcd02a..0e7fa8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ bin/ build/ +projects/* +!projects/*.sln +!projects/*.vcxproj +!projects/*.vcxproj.filters tmp/ zig-cache/ zig-out/ @@ -8,3 +12,4 @@ etc/clumsy.aps obj_vs obj_ninja obj_gmake +tools/genie.exe diff --git a/README.md b/README.md index f98fe02..dbe67b1 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,72 @@ -# clumsy +# clumsy 4.0 __clumsy makes your network condition on Windows significantly worse, but in a managed and interactive manner.__ -Leveraging the awesome [WinDivert](http://reqrypt.org/windivert.html), clumsy stops living network packets and capture them, lag/drop/tamper/.. the packets on demand, then send them away. Whether you want to track down weird bugs related to broken network, or evaluate your application on poor connections, clumsy will come in handy: +clumsy uses [WinDivert](https://reqrypt.org/windivert.html) to capture live packets, apply impairments such as lag, drop, throttle, duplicate, tamper, reset, and bandwidth limits, then reinject the packets. It is useful for testing applications under poor network conditions without changing application code or setting up a proxy. -* No installation. -* No need for proxy setup or code change in your application. -* System wide network capturing means it works on any application. -* Works even if you're offline (ie, connecting from localhost to localhost). -* Your application keeps running, while clumsy can start and stop anytime. -* Interactive control how bad the network can be, with enough visual feedback to tell you what's going on. +Version 4.0 adds native application-level filtering. You can now limit clumsy's impairments to traffic owned by a selected executable while unrelated traffic passes through normally. -See [this page](http://jagt.github.io/clumsy) for more info and build instructions. +## Download +- [Download clumsy 4.0 for Windows x64](https://github.com/foodak/clumsy/releases/download/v4.0/clumsy-4.0-win64.zip) +- [View all releases](https://github.com/foodak/clumsy/releases) + +clumsy must be run as Administrator because it uses WinDivert to intercept packets. + +## What's New In 4.0 + +- Native "Limit to application" controls in the Filtering panel. +- Process-name matching, for example `Game.exe` or `Speedtest.exe`. +- Full-path matching with a Browse button for exact executable selection. +- Multiple targets separated with commas or semicolons. +- Child-process matching for launchers and portable apps, such as `SpeedtestPortable.exe` launching `Speedtest.exe`. +- WinDivert FLOW tracking plus IP Helper seeding so existing and newly opened TCP/UDP flows can be attributed to process IDs. +- Target-aware packet capture that narrows the NETWORK-layer filter to known target TCP/UDP ports when the app filter is enabled. +- Live status counters for target PIDs, target flows, UDP endpoints, affected packets, passed packets, and unknown packets. + +## Application Filter Preview + +![clumsy 4.0 application filter](clumsy-4.0-application-filter.jpg) + +## Quick Start + +1. Run clumsy as Administrator. +2. Choose a packet filter, for example `outbound and (tcp or udp)`. +3. Enable the impairment modules you want, such as lag, drop, throttle, or bandwidth. +4. Optional: check "Limit to application". +5. Enter an executable name such as `Speedtest.exe`, or use Browse to select a full path. +6. Click Start. + +When the application filter is disabled, clumsy keeps its original system-wide behavior. + +## Application Filter + +The application filter is built for the common case where you want to test one app without disrupting debuggers, calls, browsers, or other unrelated network activity. + +The normal packet filter still controls which traffic clumsy is allowed to capture. When "Limit to application" is enabled, clumsy only applies impairments to TCP/UDP packets attributed to the selected executable name or full path. Non-target and unknown packets are passed through unchanged. + +Example: + +- Packet filter: `outbound and (tcp or udp)` +- Application: `Speedtest.exe` + +Notes: + +- Process-name matching is case-insensitive. +- In process-name mode, `game` also matches `game.exe`. +- Browse fills in a full executable path and switches matching to full-path mode. +- Multiple applications can be separated with commas or semicolons. +- Child-process matching is enabled, which helps launchers and portable apps. +- The target app does not need to be running before clumsy starts. +- Unknown or unattributed packets pass through unchanged by design. +- Loopback attribution has the same caveats as clumsy's existing loopback support. ## Details -Simulate network latency, delay, packet loss with clumsy on Windows 7/8/10: +clumsy supports latency, delay, packet loss, throttling, packet duplication, out-of-order delivery, tampering, connection reset, and bandwidth limiting on Windows. ![](clumsy-demo.gif) - ## License MIT diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..e72ca3e --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,34 @@ +# clumsy 4.0 + +clumsy 4.0 adds native application-level filtering for Windows. + +## Highlights + +- New "Limit to application" controls in the Filtering panel. +- Match target apps by process name, such as `Speedtest.exe`, or by full executable path. +- Browse button for selecting an executable and switching to full-path mode. +- Multiple application targets separated with commas or semicolons. +- Child-process matching for launchers and portable apps. +- WinDivert FLOW tracking plus IP Helper seeding for existing and new TCP/UDP flows. +- Target-aware packet capture so broad packet filters do not unnecessarily route unrelated apps through the impairment loop. +- Live status counters for target PIDs, target flows, UDP endpoints, affected packets, passed packets, and unknown packets. + +## Download + +Download `clumsy-4.0-win64.zip`, extract it, and run `clumsy.exe` as Administrator. + +The zip contains: + +- `clumsy.exe` +- `WinDivert.dll` +- `WinDivert64.sys` +- `iup.dll` +- `config.txt` +- `LICENSE.txt` +- `README.md` + +## Notes + +- Unknown or unattributed packets pass through unchanged. +- The application filter does not use `processId` in a NETWORK-layer WinDivert filter. +- Portable launchers can be targeted directly when child-process matching finds the real network-owning child process. diff --git a/build.zig b/build.zig index 99a2923..238d43f 100644 --- a/build.zig +++ b/build.zig @@ -96,6 +96,7 @@ pub fn build(b: *std.build.Builder) void { exe.step.dependOn(&cmd.step); exe.addObjectFile(res_obj_path); + exe.addCSourceFile("src/app_filter.c", &.{""}); exe.addCSourceFile("src/bandwidth.c", &.{""}); exe.addCSourceFile("src/divert.c", &.{""}); exe.addCSourceFile("src/drop.c", &.{""}); @@ -109,7 +110,6 @@ pub fn build(b: *std.build.Builder) void { exe.addCSourceFile("src/tamper.c", &.{""}); exe.addCSourceFile("src/throttle.c", &.{""}); exe.addCSourceFile("src/utils.c", &.{""}); - exe.addCSourceFile("src/utils.c", &.{""}); if (arch == .x86) exe.addCSourceFile("etc/chkstk.s", &.{""}); @@ -130,6 +130,7 @@ pub fn build(b: *std.build.Builder) void { exe.linkSystemLibrary("comctl32"); exe.linkSystemLibrary("Winmm"); exe.linkSystemLibrary("ws2_32"); + exe.linkSystemLibrary("iphlpapi"); exe.linkSystemLibrary("kernel32"); exe.linkSystemLibrary("gdi32"); exe.linkSystemLibrary("comdlg32"); @@ -174,4 +175,4 @@ pub const RemoveOutFile = struct { const out_dir = try std.fs.openDirAbsolute(self.builder.exe_dir, .{}); try out_dir.deleteFile(self.rel_path); } -}; \ No newline at end of file +}; diff --git a/clumsy-4.0-application-filter.jpg b/clumsy-4.0-application-filter.jpg new file mode 100644 index 0000000..be678ba Binary files /dev/null and b/clumsy-4.0-application-filter.jpg differ diff --git a/etc/clumsy.manifest b/etc/clumsy.manifest index 2d5a0e2..5756d68 100644 --- a/etc/clumsy.manifest +++ b/etc/clumsy.manifest @@ -2,7 +2,7 @@ for details +# Default: capture normal IPv4 and IPv6 traffic in both directions. +ipv4 + ipv6 all: ip or ipv6 + # loopback packets can only be filtered using 'outbound'. localhost ipv4 all: outbound and loopback localhost ipv4 tcp: tcp and outbound and loopback @@ -24,4 +27,4 @@ udp ipv4 against port: udp and (udp.DstPort == 12354 or udp.SrcPort == 12354) ipv6 all: ipv6 # you can add your usual filters here for your own use: -#http requests ONLY(data transmit on other ports): outbound and tcp.DstPort == 80 \ No newline at end of file +#http requests ONLY(data transmit on other ports): outbound and tcp.DstPort == 80 diff --git a/genie.lua b/genie.lua index f991124..d08c3d3 100644 --- a/genie.lua +++ b/genie.lua @@ -1,11 +1,18 @@ -- genie, https://github.com/bkaradzic/GENie --- known working version +-- known working version (for VS2019 and earlier): -- https://github.com/bkaradzic/bx/blob/51f25ba638b9cb35eb2ac078f842a4bed0746d56/tools/bin/windows/genie.exe +-- +-- For VS2022 support, use a recent GENie built from master: +-- https://github.com/bkaradzic/GENie +-- Pre-built genie.exe can also be found in the bx repo tools/bin/windows/. +-- +-- Supported actions: vs2022, vs2019, vs2017, vs2015, gmake +-- Usage: genie.exe vs2022 MINGW_ACTION = 'gmake' if _ACTION == 'clean' then - os.rmdir('./build') + os.rmdir('./projects') os.rmdir('./bin') os.rmdir('./obj_vs') os.rmdir('./obj_' .. MINGW_ACTION) @@ -30,14 +37,14 @@ local ROOT = os.getcwd() print(ROOT) solution('clumsy') - location("./build") + location("./projects") configurations({'Debug', 'Release'}) platforms({'x32', 'x64'}) project('clumsy') language("C") files({'src/**.c', 'src/**.h'}) - links({'WinDivert', 'iup', 'comctl32', 'Winmm', 'ws2_32'}) + links({'WinDivert', 'iup', 'comctl32', 'Winmm', 'ws2_32', 'Iphlpapi'}) if string.match(_ACTION, '^vs') then -- only vs can include rc file in solution files({'./etc/clumsy.rc'}) elseif _ACTION == MINGW_ACTION then @@ -56,7 +63,7 @@ solution('clumsy') kind("WindowedApp") configuration(MINGW_ACTION) - links({'kernel32', 'gdi32', 'comdlg32', 'uuid', 'ole32'}) -- additional libs + links({'kernel32', 'gdi32', 'comdlg32', 'uuid', 'ole32', 'iphlpapi'}) -- additional libs buildoptions({ '-Wno-missing-braces', '-Wno-missing-field-initializers', @@ -65,12 +72,10 @@ solution('clumsy') objdir('obj_'..MINGW_ACTION) configuration("vs*") - defines({"_CRT_SECURE_NO_WARNINGS"}) + defines({"_CRT_SECURE_NO_WARNINGS", "_CRT_NONSTDC_NO_DEPRECATE"}) flags({'NoManifest'}) - kind("WindowedApp") -- We don't need the console window in VS as we use OutputDebugString(). buildoptions({'/wd"4214"'}) - linkoptions({'/ENTRY:"mainCRTStartup" /SAFESEH:NO'}) - -- characterset("MBCS") + -- MBCS is the default in GENie when 'Unicode' flag is not set includedirs({LIB_DIVERT_VC11 .. '/include'}) objdir('obj_vs') @@ -78,6 +83,7 @@ solution('clumsy') -- defines would be passed to resource compiler for whatever reason -- and ONLY can be put here not under 'configuration('x32')' or it won't work defines({'X32'}) + linkoptions({'/ENTRY:"mainCRTStartup"', '/SAFESEH:NO'}) includedirs({LIB_IUP_WIN32_VC11 .. '/include'}) libdirs({ LIB_DIVERT_VC11 .. '/x86', @@ -86,6 +92,7 @@ solution('clumsy') configuration({'x64', 'vs*'}) defines({'X64'}) + linkoptions({'/ENTRY:"mainCRTStartup"'}) includedirs({LIB_IUP_WIN64_VC11 .. '/include'}) libdirs({ LIB_DIVERT_VC11 .. '/x64', @@ -141,10 +148,12 @@ solution('clumsy') targetdir(subdir) debugdir(subdir) if platform == 'vs*' then + -- robocopy returns 1 on success, which MSBuild treats as error. + -- All commands run in one batch; final exit /B 0 forces success. postbuildcommands({ - "robocopy " .. divert_lib .." " .. subdir .. ' *.dll *.sys >> robolog.txt', - "robocopy " .. iup_lib .. " " .. subdir .. ' iup.dll >> robolog.txt', - "robocopy " .. ROOT .. "/etc/ " .. subdir .. ' config.txt >> robolog.txt', + "robocopy " .. divert_lib .." " .. subdir .. ' *.dll *.sys > NUL', + "robocopy " .. iup_lib .. " " .. subdir .. ' iup.dll > NUL', + "robocopy " .. ROOT .. "/etc/ " .. subdir .. ' config.txt > NUL', "exit /B 0" }) elseif platform == MINGW_ACTION then diff --git a/projects/clumsy.sln b/projects/clumsy.sln new file mode 100644 index 0000000..435cce7 --- /dev/null +++ b/projects/clumsy.sln @@ -0,0 +1,29 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2022 +# Generated by GENie https://github.com/bkaradzic/GENie +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "clumsy", "clumsy.vcxproj", "{C218B2F6-AEBA-DCCC-9775-F02083B6631D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C218B2F6-AEBA-DCCC-9775-F02083B6631D}.Debug|Win32.ActiveCfg = Debug|Win32 + {C218B2F6-AEBA-DCCC-9775-F02083B6631D}.Debug|Win32.Build.0 = Debug|Win32 + {C218B2F6-AEBA-DCCC-9775-F02083B6631D}.Debug|x64.ActiveCfg = Debug|x64 + {C218B2F6-AEBA-DCCC-9775-F02083B6631D}.Debug|x64.Build.0 = Debug|x64 + {C218B2F6-AEBA-DCCC-9775-F02083B6631D}.Release|Win32.ActiveCfg = Release|Win32 + {C218B2F6-AEBA-DCCC-9775-F02083B6631D}.Release|Win32.Build.0 = Release|Win32 + {C218B2F6-AEBA-DCCC-9775-F02083B6631D}.Release|x64.ActiveCfg = Release|x64 + {C218B2F6-AEBA-DCCC-9775-F02083B6631D}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection +EndGlobal diff --git a/projects/clumsy.vcxproj b/projects/clumsy.vcxproj new file mode 100644 index 0000000..baf5606 --- /dev/null +++ b/projects/clumsy.vcxproj @@ -0,0 +1,298 @@ + + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {C218B2F6-AEBA-DCCC-9775-F02083B6631D} + clumsy + 10.0 + 10.0.10240.0 + Win32Proj + + + + 16.0 + Application + true + v143 + x64 + + + 16.0 + Application + true + v143 + x64 + + + 16.0 + Application + false + v143 + x64 + + + 16.0 + Application + false + v143 + x64 + + + + + + + + + + + + + + + + + + + ..\bin\vs\Debug\x32\ + ..\obj_vs\x32\Debug\ + clumsy + .exe + true + false + + + ..\bin\vs\Debug\x64\ + ..\obj_vs\x64\Debug\ + clumsy + .exe + true + false + + + ..\bin\vs\Release\x32\ + ..\obj_vs\x32\Release\ + clumsy + .exe + false + false + + + ..\bin\vs\Release\x64\ + ..\obj_vs\x64\Release\ + clumsy + .exe + false + false + + + + /wd"4214" %(AdditionalOptions) + Disabled + ..\external\WinDivert-2.2.0-A\include;..\external\iup-3.30_Win32_dll16_lib\include;%(AdditionalIncludeDirectories) + _DEBUG;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;X32;%(PreprocessorDefinitions) + false + true + EnableFastChecks + MultiThreadedDebugDLL + true + + Level4 + EditAndContinue + $(IntDir)clumsy.compile.pdb + CompileAsC + Caret + + + _DEBUG;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;X32;%(PreprocessorDefinitions) + ..\external\WinDivert-2.2.0-A\include;..\external\iup-3.30_Win32_dll16_lib\include;%(AdditionalIncludeDirectories) + + + Console + true + $(OutDir)clumsy.pdb + WinDivert.lib;iup.lib;comctl32.lib;Winmm.lib;ws2_32.lib;Iphlpapi.lib;;%(AdditionalDependencies) + ..\external\WinDivert-2.2.0-A\x86;..\external\iup-3.30_Win32_dll16_lib;%(AdditionalLibraryDirectories) + $(OutDir)clumsy.exe + mainCRTStartup + MachineX86 + /ENTRY:"mainCRTStartup" /SAFESEH:NO %(AdditionalOptions) + + + robocopy c:/Users/phudak/Desktop/clumsy/external/WinDivert-2.2.0-A/x86/ c:/Users/phudak/Desktop/clumsy/bin/vs/Debug/x32 *.dll *.sys > NUL robocopy c:/Users/phudak/Desktop/clumsy/external/iup-3.30_Win32_dll16_lib c:/Users/phudak/Desktop/clumsy/bin/vs/Debug/x32 iup.dll > NUL robocopy c:/Users/phudak/Desktop/clumsy/etc/ c:/Users/phudak/Desktop/clumsy/bin/vs/Debug/x32 config.txt > NUL exit /B 0 + + + + + /wd"4214" %(AdditionalOptions) + Disabled + ..\external\WinDivert-2.2.0-A\include;..\external\iup-3.30_Win64_dll16_lib\include;%(AdditionalIncludeDirectories) + _DEBUG;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;X64;%(PreprocessorDefinitions) + false + true + EnableFastChecks + MultiThreadedDebugDLL + true + + Level4 + EditAndContinue + $(IntDir)clumsy.compile.pdb + CompileAsC + Caret + + + _DEBUG;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;X64;%(PreprocessorDefinitions) + ..\external\WinDivert-2.2.0-A\include;..\external\iup-3.30_Win64_dll16_lib\include;%(AdditionalIncludeDirectories) + + + Console + true + $(OutDir)clumsy.pdb + WinDivert.lib;iup.lib;comctl32.lib;Winmm.lib;ws2_32.lib;Iphlpapi.lib;;%(AdditionalDependencies) + ..\external\WinDivert-2.2.0-A\x64;..\external\iup-3.30_Win64_dll16_lib;%(AdditionalLibraryDirectories) + $(OutDir)clumsy.exe + mainCRTStartup + MachineX64 + /ENTRY:"mainCRTStartup" %(AdditionalOptions) + + + robocopy c:/Users/phudak/Desktop/clumsy/external/WinDivert-2.2.0-A/x64/ c:/Users/phudak/Desktop/clumsy/bin/vs/Debug/x64 *.dll *.sys > NUL robocopy c:/Users/phudak/Desktop/clumsy/external/iup-3.30_Win64_dll16_lib c:/Users/phudak/Desktop/clumsy/bin/vs/Debug/x64 iup.dll > NUL robocopy c:/Users/phudak/Desktop/clumsy/etc/ c:/Users/phudak/Desktop/clumsy/bin/vs/Debug/x64 config.txt > NUL exit /B 0 + + + + + /wd"4214" %(AdditionalOptions) + Full + ..\external\WinDivert-2.2.0-A\include;..\external\iup-3.30_Win32_dll16_lib\include;%(AdditionalIncludeDirectories) + NDEBUG;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;X32;%(PreprocessorDefinitions) + false + true + true + MultiThreadedDLL + true + + Level3 + ProgramDatabase + $(IntDir)clumsy.compile.pdb + CompileAsC + Caret + + + NDEBUG;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;X32;%(PreprocessorDefinitions) + ..\external\WinDivert-2.2.0-A\include;..\external\iup-3.30_Win32_dll16_lib\include;%(AdditionalIncludeDirectories) + + + Windows + true + $(OutDir)clumsy.pdb + true + true + WinDivert.lib;iup.lib;comctl32.lib;Winmm.lib;ws2_32.lib;Iphlpapi.lib;;%(AdditionalDependencies) + ..\external\WinDivert-2.2.0-A\x86;..\external\iup-3.30_Win32_dll16_lib;%(AdditionalLibraryDirectories) + $(OutDir)clumsy.exe + mainCRTStartup + MachineX86 + /ENTRY:"mainCRTStartup" /SAFESEH:NO %(AdditionalOptions) + + + robocopy c:/Users/phudak/Desktop/clumsy/external/WinDivert-2.2.0-A/x86/ c:/Users/phudak/Desktop/clumsy/bin/vs/Release/x32 *.dll *.sys > NUL robocopy c:/Users/phudak/Desktop/clumsy/external/iup-3.30_Win32_dll16_lib c:/Users/phudak/Desktop/clumsy/bin/vs/Release/x32 iup.dll > NUL robocopy c:/Users/phudak/Desktop/clumsy/etc/ c:/Users/phudak/Desktop/clumsy/bin/vs/Release/x32 config.txt > NUL exit /B 0 + + + + + /wd"4214" %(AdditionalOptions) + Full + ..\external\WinDivert-2.2.0-A\include;..\external\iup-3.30_Win64_dll16_lib\include;%(AdditionalIncludeDirectories) + NDEBUG;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;X64;%(PreprocessorDefinitions) + false + true + true + MultiThreadedDLL + true + + Level3 + ProgramDatabase + $(IntDir)clumsy.compile.pdb + CompileAsC + Caret + + + NDEBUG;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;X64;%(PreprocessorDefinitions) + ..\external\WinDivert-2.2.0-A\include;..\external\iup-3.30_Win64_dll16_lib\include;%(AdditionalIncludeDirectories) + + + Windows + true + $(OutDir)clumsy.pdb + true + true + WinDivert.lib;iup.lib;comctl32.lib;Winmm.lib;ws2_32.lib;Iphlpapi.lib;;%(AdditionalDependencies) + ..\external\WinDivert-2.2.0-A\x64;..\external\iup-3.30_Win64_dll16_lib;%(AdditionalLibraryDirectories) + $(OutDir)clumsy.exe + mainCRTStartup + MachineX64 + /ENTRY:"mainCRTStartup" %(AdditionalOptions) + + + robocopy c:/Users/phudak/Desktop/clumsy/external/WinDivert-2.2.0-A/x64/ c:/Users/phudak/Desktop/clumsy/bin/vs/Release/x64 *.dll *.sys > NUL robocopy c:/Users/phudak/Desktop/clumsy/external/iup-3.30_Win64_dll16_lib c:/Users/phudak/Desktop/clumsy/bin/vs/Release/x64 iup.dll > NUL robocopy c:/Users/phudak/Desktop/clumsy/etc/ c:/Users/phudak/Desktop/clumsy/bin/vs/Release/x64 config.txt > NUL exit /B 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/clumsy.vcxproj.filters b/projects/clumsy.vcxproj.filters new file mode 100644 index 0000000..dd9d961 --- /dev/null +++ b/projects/clumsy.vcxproj.filters @@ -0,0 +1,69 @@ + + + + + + {2DAB880B-99B4-887C-2230-9F7C8E38947C} + + + {E16F880B-4D79-887C-D6F4-9E7C42FD937C} + + + + + src + + + src + + + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + src + + + + + etc + + + diff --git a/src/app_filter.c b/src/app_filter.c new file mode 100644 index 0000000..15766fc --- /dev/null +++ b/src/app_filter.c @@ -0,0 +1,1840 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "app_filter.h" +#include "common.h" + +#define APP_FILTER_MAX_TARGETS 32 +#define APP_FILTER_TARGET_LEN APP_FILTER_TARGETS_BUFSIZE +#define APP_FILTER_MAX_FLOWS 16384 +#define APP_FILTER_MAX_ENDPOINTS 8192 +#define APP_FILTER_MAX_PID_CACHE 2048 +#define APP_FILTER_MAX_TARGET_PORTS 256 +#define APP_FILTER_PID_CACHE_MS 3000 +#define APP_FILTER_REFRESH_MS 1000 +#define APP_FILTER_FLOW_LINGER_MS 15000 +#define APP_FILTER_PORT_LINGER_MS 30000 +#define APP_FILTER_FLOW_PRIORITY 0 + +#pragma pack(push, 1) +typedef struct FlowKey +{ + UINT8 protocol; + UINT8 localAddr[16]; + UINT8 remoteAddr[16]; + UINT16 localPort; + UINT16 remotePort; +} FlowKey; +#pragma pack(pop) + +typedef struct EndpointKey +{ + UINT8 protocol; + UINT8 localAddr[16]; + UINT16 localPort; +} EndpointKey; + +typedef struct FlowEntry +{ + BOOL active; + BOOL seeded; + FlowKey key; + UINT64 endpointId; + DWORD pid; + BOOL isTarget; + BOOL lingering; + DWORD lastSeenTick; +} FlowEntry; + +typedef struct EndpointEntry +{ + BOOL active; + EndpointKey key; + DWORD pid; + BOOL isTarget; + DWORD lastSeenTick; +} EndpointEntry; + +typedef struct PidCacheEntry +{ + BOOL active; + DWORD pid; + BOOL isTarget; + DWORD lastCheckedTick; +} PidCacheEntry; + +typedef struct TargetPortEntry +{ + BOOL active; + UINT8 protocol; + UINT16 localPort; + DWORD lastSeenTick; +} TargetPortEntry; + +typedef struct TargetEntry +{ + char text[APP_FILTER_TARGET_LEN]; + BOOL hasExeSuffix; +} TargetEntry; + +static CRITICAL_SECTION appFilterLock; +static BOOL lockInitialized = FALSE; +static volatile LONG appFilterEnabled = FALSE; +static volatile LONG stopFlowThread = FALSE; +static volatile LONG refreshInProgress = FALSE; +static HANDLE flowHandle = INVALID_HANDLE_VALUE; +static HANDLE flowThread = NULL; + +static AppFilterMode configuredMode = APP_FILTER_MODE_PROCESS_NAME; +static BOOL includeChildProcesses = TRUE; +static TargetEntry targets[APP_FILTER_MAX_TARGETS]; +static UINT targetCount = 0; +static FlowEntry flows[APP_FILTER_MAX_FLOWS]; +static EndpointEntry endpoints[APP_FILTER_MAX_ENDPOINTS]; +static PidCacheEntry pidCache[APP_FILTER_MAX_PID_CACHE]; +static TargetPortEntry targetPorts[APP_FILTER_MAX_TARGET_PORTS]; +static AppFilterStats stats; +static DWORD lastRefreshTick = 0; +static volatile LONG networkFilterVersion = 0; + +static DWORD WINAPI AppFilterFlowThread(LPVOID arg); +static BOOL ParseTargets(const char* targetText, AppFilterMode mode, + char* statusBuf, UINT statusBufLen); +static void SeedIpHelperTables(BOOL clearSeededFirst); +static void BumpNetworkFilterVersion(void); + +static void CopyString(char* dst, UINT dstLen, const char* src) +{ + UINT ix; + + if (dstLen == 0) + { + return; + } + if (src == NULL) + { + dst[0] = '\0'; + return; + } + for (ix = 0; ix + 1 < dstLen && src[ix] != '\0'; ++ix) + { + dst[ix] = src[ix]; + } + dst[ix] = '\0'; +} + +static void SetStatus(char* statusBuf, UINT statusBufLen, const char* status) +{ + CopyString(statusBuf, statusBufLen, status); +} + +static BOOL IsBlank(const char* text) +{ + const unsigned char* p = (const unsigned char*)text; + + if (p == NULL) + { + return TRUE; + } + while (*p != '\0') + { + if (!isspace(*p)) + { + return FALSE; + } + ++p; + } + return TRUE; +} + +static void NormalizePathText(char* text) +{ + unsigned char* p = (unsigned char*)text; + + while (*p != '\0') + { + if (*p == '/') + { + *p = '\\'; + } + else + { + *p = (unsigned char)tolower(*p); + } + ++p; + } +} + +static const char* BaseName(const char* path) +{ + const char* slash; + const char* backslash; + + if (path == NULL) + { + return ""; + } + slash = strrchr(path, '/'); + backslash = strrchr(path, '\\'); + if (slash == NULL || (backslash != NULL && backslash > slash)) + { + slash = backslash; + } + return slash == NULL ? path : slash + 1; +} + +static BOOL EndsWithExe(const char* text) +{ + size_t len; + + if (text == NULL) + { + return FALSE; + } + len = strlen(text); + if (len < 4) + { + return FALSE; + } + return text[len - 4] == '.' && + text[len - 3] == 'e' && + text[len - 2] == 'x' && + text[len - 1] == 'e'; +} + +static BOOL SameText(const char* left, const char* right) +{ + return strcmp(left, right) == 0; +} + +static void BumpNetworkFilterVersion(void) +{ + InterlockedIncrement(&networkFilterVersion); +} + +static BOOL MatchNormalizedProcessName(const char* processName) +{ + UINT ix; + char processNoExe[APP_FILTER_TARGET_LEN]; + + for (ix = 0; ix < targetCount; ++ix) + { + if (SameText(processName, targets[ix].text)) + { + return TRUE; + } + if (!targets[ix].hasExeSuffix && EndsWithExe(processName)) + { + CopyString(processNoExe, sizeof(processNoExe), processName); + processNoExe[strlen(processNoExe) - 4] = '\0'; + if (SameText(processNoExe, targets[ix].text)) + { + return TRUE; + } + } + } + return FALSE; +} + +static BOOL MatchNormalizedFullPath(const char* path) +{ + UINT ix; + + for (ix = 0; ix < targetCount; ++ix) + { + if (SameText(path, targets[ix].text)) + { + return TRUE; + } + } + return FALSE; +} + +static BOOL MatchProcessPath(const char* path) +{ + char normalized[APP_FILTER_TARGET_LEN]; + const char* name; + + if (path == NULL || path[0] == '\0') + { + return FALSE; + } + CopyString(normalized, sizeof(normalized), path); + NormalizePathText(normalized); + if (configuredMode == APP_FILTER_MODE_FULL_PATH) + { + return MatchNormalizedFullPath(normalized); + } + name = BaseName(normalized); + return MatchNormalizedProcessName(name); +} + +static BOOL SnapshotProcessInfo(DWORD pid, DWORD* parentPid, char* exeName, + UINT exeNameLen) +{ + HANDLE snapshot; + PROCESSENTRY32 entry; + BOOL found = FALSE; + + snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) + { + return FALSE; + } + + memset(&entry, 0, sizeof(entry)); + entry.dwSize = sizeof(entry); + if (Process32First(snapshot, &entry)) + { + do + { + if (entry.th32ProcessID == pid) + { + if (parentPid != NULL) + { + *parentPid = entry.th32ParentProcessID; + } + if (exeName != NULL && exeNameLen > 0) + { + CopyString(exeName, exeNameLen, entry.szExeFile); + } + found = TRUE; + break; + } + } + while (Process32Next(snapshot, &entry)); + } + + CloseHandle(snapshot); + return found; +} + +static BOOL SnapshotMatchPid(DWORD pid) +{ + char exeName[APP_FILTER_TARGET_LEN]; + char normalized[APP_FILTER_TARGET_LEN]; + + if (!SnapshotProcessInfo(pid, NULL, exeName, sizeof(exeName))) + { + return FALSE; + } + + CopyString(normalized, sizeof(normalized), exeName); + NormalizePathText(normalized); + return MatchNormalizedProcessName(BaseName(normalized)); +} + +static BOOL QueryPidDirectMatchesTarget(DWORD pid, char* detailBuf, UINT detailBufLen) +{ + HANDLE process; + DWORD pathLen; + char path[APP_FILTER_TARGET_LEN]; + BOOL matched = FALSE; + + if (pid == 0) + { + return FALSE; + } + + process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (process != NULL) + { + pathLen = sizeof(path); + if (QueryFullProcessImageNameA(process, 0, path, &pathLen)) + { + path[sizeof(path) - 1] = '\0'; + CopyString(detailBuf, detailBufLen, path); + matched = MatchProcessPath(path); + } + CloseHandle(process); + } + + if (!matched && configuredMode == APP_FILTER_MODE_PROCESS_NAME) + { + matched = SnapshotMatchPid(pid); + if (matched && (detailBuf == NULL || detailBuf[0] == '\0')) + { + CopyString(detailBuf, detailBufLen, "snapshot process name"); + } + } + + return matched; +} + +static BOOL QueryPidChildMatchesTarget(DWORD pid, DWORD* matchedParentPid) +{ + DWORD currentPid; + DWORD parentPid; + UINT depth; + + currentPid = pid; + for (depth = 0; depth < 32; ++depth) + { + if (!SnapshotProcessInfo(currentPid, &parentPid, NULL, 0)) + { + return FALSE; + } + if (parentPid == 0 || parentPid == currentPid) + { + return FALSE; + } + if (QueryPidDirectMatchesTarget(parentPid, NULL, 0)) + { + if (matchedParentPid != NULL) + { + *matchedParentPid = parentPid; + } + return TRUE; + } + currentPid = parentPid; + } + + return FALSE; +} + +static BOOL QueryPidMatchesTarget(DWORD pid) +{ + char detail[APP_FILTER_TARGET_LEN]; + DWORD matchedParentPid = 0; + BOOL matched; + + detail[0] = '\0'; + matched = QueryPidDirectMatchesTarget(pid, detail, sizeof(detail)); + if (!matched && includeChildProcesses) + { + matched = QueryPidChildMatchesTarget(pid, &matchedParentPid); + } + + if (matchedParentPid != 0) + { + LOG("App filter PID %lu is target (via parent %lu)", pid, matchedParentPid); + } + else if (matched) + { + LOG("App filter PID %lu is target (%s)", pid, detail[0] ? detail : "no path"); + } + return matched; +} + +static void RecountPidTargetsLocked(void) +{ + UINT ix; + LONG count = 0; + + for (ix = 0; ix < APP_FILTER_MAX_PID_CACHE; ++ix) + { + if (pidCache[ix].active && pidCache[ix].isTarget) + { + ++count; + } + } + InterlockedExchange(&stats.targetPidCount, count); +} + +static BOOL IsTargetPid(DWORD pid) +{ + UINT ix; + UINT freeIx = APP_FILTER_MAX_PID_CACHE; + UINT oldestIx = 0; + DWORD now = GetTickCount(); + DWORD oldestTick = now; + BOOL foundOldest = FALSE; + BOOL matched; + + if (pid == 0) + { + return FALSE; + } + + EnterCriticalSection(&appFilterLock); + for (ix = 0; ix < APP_FILTER_MAX_PID_CACHE; ++ix) + { + if (pidCache[ix].active && pidCache[ix].pid == pid) + { + if (now - pidCache[ix].lastCheckedTick <= APP_FILTER_PID_CACHE_MS) + { + matched = pidCache[ix].isTarget; + LeaveCriticalSection(&appFilterLock); + return matched; + } + freeIx = ix; + break; + } + if (!pidCache[ix].active && freeIx == APP_FILTER_MAX_PID_CACHE) + { + freeIx = ix; + } + if (pidCache[ix].active && + (!foundOldest || pidCache[ix].lastCheckedTick - oldestTick > 0x80000000UL)) + { + oldestIx = ix; + oldestTick = pidCache[ix].lastCheckedTick; + foundOldest = TRUE; + } + } + LeaveCriticalSection(&appFilterLock); + + matched = QueryPidMatchesTarget(pid); + + EnterCriticalSection(&appFilterLock); + if (freeIx == APP_FILTER_MAX_PID_CACHE) + { + freeIx = foundOldest ? oldestIx : 0; + } + pidCache[freeIx].active = TRUE; + pidCache[freeIx].pid = pid; + pidCache[freeIx].isTarget = matched; + pidCache[freeIx].lastCheckedTick = now; + RecountPidTargetsLocked(); + LeaveCriticalSection(&appFilterLock); + + return matched; +} + +static int CompareFlowKey(const FlowKey* left, const FlowKey* right) +{ + return memcmp(left, right, sizeof(*left)); +} + +static int CompareEndpointKey(const EndpointKey* left, const EndpointKey* right) +{ + return memcmp(left, right, sizeof(*left)); +} + +static BOOL IsPortFilterable(UINT8 protocol, UINT16 localPort) +{ + return localPort != 0 && (protocol == IPPROTO_TCP || protocol == IPPROTO_UDP); +} + +static void SetIpv4MappedAddress(UINT8 addr[16], UINT32 ipv4HostOrder) +{ + UINT32 words[4]; + + words[0] = 0; + words[1] = 0; + words[2] = 0x0000FFFF; + words[3] = ipv4HostOrder; + memcpy(addr, words, sizeof(words)); +} + +static void SetZeroAddress(UINT8 addr[16]) +{ + memset(addr, 0, 16); +} + +static void SetHostIpv6Address(UINT8 addr[16], const UINT32 words[4]) +{ + memcpy(addr, words, 16); +} + +static void SetNetworkIpv6Address(UINT8 addr[16], const UINT8 networkBytes[16]) +{ + UINT32 netWords[4]; + UINT32 hostWords[4]; + + memcpy(netWords, networkBytes, sizeof(netWords)); + WinDivertHelperNtohIPv6Address(netWords, hostWords); + SetHostIpv6Address(addr, hostWords); +} + +static void FlowKeyFromFlowAddress(FlowKey* key, const WINDIVERT_ADDRESS* addr) +{ + memset(key, 0, sizeof(*key)); + key->protocol = addr->Flow.Protocol; + memcpy(key->localAddr, addr->Flow.LocalAddr, sizeof(key->localAddr)); + memcpy(key->remoteAddr, addr->Flow.RemoteAddr, sizeof(key->remoteAddr)); + key->localPort = addr->Flow.LocalPort; + key->remotePort = addr->Flow.RemotePort; +} + +static void ReverseFlowKey(FlowKey* reversed, const FlowKey* key) +{ + memset(reversed, 0, sizeof(*reversed)); + reversed->protocol = key->protocol; + memcpy(reversed->localAddr, key->remoteAddr, sizeof(reversed->localAddr)); + memcpy(reversed->remoteAddr, key->localAddr, sizeof(reversed->remoteAddr)); + reversed->localPort = key->remotePort; + reversed->remotePort = key->localPort; +} + +static void EndpointKeyFromFlowKey(EndpointKey* endpoint, const FlowKey* key) +{ + memset(endpoint, 0, sizeof(*endpoint)); + endpoint->protocol = key->protocol; + memcpy(endpoint->localAddr, key->localAddr, sizeof(endpoint->localAddr)); + endpoint->localPort = key->localPort; +} + +static int FindFlowByKeyLocked(const FlowKey* key) +{ + UINT ix; + + for (ix = 0; ix < APP_FILTER_MAX_FLOWS; ++ix) + { + if (flows[ix].active && CompareFlowKey(&flows[ix].key, key) == 0) + { + return (int)ix; + } + } + return -1; +} + +static int FindFlowByEndpointLocked(UINT64 endpointId) +{ + UINT ix; + + if (endpointId == 0) + { + return -1; + } + for (ix = 0; ix < APP_FILTER_MAX_FLOWS; ++ix) + { + if (flows[ix].active && flows[ix].endpointId == endpointId) + { + return (int)ix; + } + } + return -1; +} + +static int AllocateFlowSlotLocked(void) +{ + UINT ix; + UINT oldestIx = 0; + DWORD oldestTick = 0; + BOOL foundOldest = FALSE; + + for (ix = 0; ix < APP_FILTER_MAX_FLOWS; ++ix) + { + if (!flows[ix].active) + { + return (int)ix; + } + if (!foundOldest || flows[ix].lastSeenTick - oldestTick > 0x80000000UL) + { + oldestIx = ix; + oldestTick = flows[ix].lastSeenTick; + foundOldest = TRUE; + } + } + return (int)oldestIx; +} + +static int FindTargetPortLocked(UINT8 protocol, UINT16 localPort) +{ + UINT ix; + + for (ix = 0; ix < APP_FILTER_MAX_TARGET_PORTS; ++ix) + { + if (targetPorts[ix].active && + targetPorts[ix].protocol == protocol && + targetPorts[ix].localPort == localPort) + { + return (int)ix; + } + } + return -1; +} + +static BOOL IsTargetPortRememberedLocked(UINT8 protocol, UINT16 localPort) +{ + return IsPortFilterable(protocol, localPort) && + FindTargetPortLocked(protocol, localPort) >= 0; +} + +static int AllocateTargetPortSlotLocked(void) +{ + UINT ix; + UINT oldestIx = 0; + DWORD oldestTick = 0; + BOOL foundOldest = FALSE; + + for (ix = 0; ix < APP_FILTER_MAX_TARGET_PORTS; ++ix) + { + if (!targetPorts[ix].active) + { + return (int)ix; + } + if (!foundOldest || + targetPorts[ix].lastSeenTick - oldestTick > 0x80000000UL) + { + oldestIx = ix; + oldestTick = targetPorts[ix].lastSeenTick; + foundOldest = TRUE; + } + } + return (int)oldestIx; +} + +static BOOL RememberTargetPortLocked(UINT8 protocol, UINT16 localPort) +{ + int slot; + BOOL wasActive; + BOOL changed; + + if (!IsPortFilterable(protocol, localPort)) + { + return FALSE; + } + + slot = FindTargetPortLocked(protocol, localPort); + if (slot < 0) + { + slot = AllocateTargetPortSlotLocked(); + } + + wasActive = targetPorts[slot].active; + changed = !wasActive || + targetPorts[slot].protocol != protocol || + targetPorts[slot].localPort != localPort; + targetPorts[slot].active = TRUE; + targetPorts[slot].protocol = protocol; + targetPorts[slot].localPort = localPort; + targetPorts[slot].lastSeenTick = GetTickCount(); + return changed; +} + +static BOOL PurgeExpiredTargetPortsLocked(DWORD now) +{ + UINT ix; + BOOL changed = FALSE; + + for (ix = 0; ix < APP_FILTER_MAX_TARGET_PORTS; ++ix) + { + if (targetPorts[ix].active && + now - targetPorts[ix].lastSeenTick >= APP_FILTER_PORT_LINGER_MS) + { + memset(&targetPorts[ix], 0, sizeof(targetPorts[ix])); + changed = TRUE; + } + } + return changed; +} + +static void AddOrUpdateFlow(const FlowKey* key, UINT64 endpointId, DWORD pid, + BOOL isTarget, BOOL seeded) +{ + int slot; + BOOL wasActive; + BOOL wasTarget; + BOOL newSeeded; + + EnterCriticalSection(&appFilterLock); + if (!isTarget && IsTargetPortRememberedLocked(key->protocol, key->localPort)) + { + isTarget = TRUE; + } + slot = FindFlowByKeyLocked(key); + if (slot < 0) + { + slot = AllocateFlowSlotLocked(); + } + + wasActive = flows[slot].active; + wasTarget = flows[slot].isTarget; + newSeeded = wasActive ? (flows[slot].seeded && seeded) : seeded; + + flows[slot].active = TRUE; + flows[slot].seeded = newSeeded; + flows[slot].key = *key; + flows[slot].endpointId = endpointId; + flows[slot].pid = pid; + flows[slot].isTarget = isTarget; + flows[slot].lingering = FALSE; + flows[slot].lastSeenTick = GetTickCount(); + + if (wasActive && wasTarget && !isTarget) + { + InterlockedDecrement(&stats.targetFlowCount); + } + else if ((!wasActive || !wasTarget) && isTarget) + { + InterlockedIncrement(&stats.targetFlowCount); + } + if (isTarget && RememberTargetPortLocked(key->protocol, key->localPort)) + { + BumpNetworkFilterVersion(); + } + LeaveCriticalSection(&appFilterLock); +} + +static void RemoveFlowByEndpointOrKey(const FlowKey* key, UINT64 endpointId) +{ + int slot; + DWORD now = GetTickCount(); + + EnterCriticalSection(&appFilterLock); + slot = key != NULL ? FindFlowByKeyLocked(key) : -1; + if (slot < 0) + { + slot = FindFlowByEndpointLocked(endpointId); + } + if (slot >= 0) + { + if (flows[slot].isTarget) + { + flows[slot].lingering = TRUE; + flows[slot].endpointId = 0; + flows[slot].lastSeenTick = now; + } + else + { + memset(&flows[slot], 0, sizeof(flows[slot])); + } + } + LeaveCriticalSection(&appFilterLock); +} + +static int FindEndpointByKeyLocked(const EndpointKey* key) +{ + UINT ix; + + for (ix = 0; ix < APP_FILTER_MAX_ENDPOINTS; ++ix) + { + if (endpoints[ix].active && CompareEndpointKey(&endpoints[ix].key, key) == 0) + { + return (int)ix; + } + } + return -1; +} + +static int AllocateEndpointSlotLocked(void) +{ + UINT ix; + UINT oldestIx = 0; + DWORD oldestTick = 0; + BOOL foundOldest = FALSE; + + for (ix = 0; ix < APP_FILTER_MAX_ENDPOINTS; ++ix) + { + if (!endpoints[ix].active) + { + return (int)ix; + } + if (!foundOldest || endpoints[ix].lastSeenTick - oldestTick > 0x80000000UL) + { + oldestIx = ix; + oldestTick = endpoints[ix].lastSeenTick; + foundOldest = TRUE; + } + } + return (int)oldestIx; +} + +static void AddOrUpdateEndpoint(const EndpointKey* key, DWORD pid, BOOL isTarget) +{ + int slot; + BOOL wasActive; + BOOL wasTarget; + + EnterCriticalSection(&appFilterLock); + if (!isTarget && IsTargetPortRememberedLocked(key->protocol, key->localPort)) + { + isTarget = TRUE; + } + slot = FindEndpointByKeyLocked(key); + if (slot < 0) + { + slot = AllocateEndpointSlotLocked(); + } + + wasActive = endpoints[slot].active; + wasTarget = endpoints[slot].isTarget; + + endpoints[slot].active = TRUE; + endpoints[slot].key = *key; + endpoints[slot].pid = pid; + endpoints[slot].isTarget = isTarget; + endpoints[slot].lastSeenTick = GetTickCount(); + + if (wasActive && wasTarget && !isTarget) + { + InterlockedDecrement(&stats.targetEndpointCount); + } + else if ((!wasActive || !wasTarget) && isTarget) + { + InterlockedIncrement(&stats.targetEndpointCount); + } + if (isTarget && RememberTargetPortLocked(key->protocol, key->localPort)) + { + BumpNetworkFilterVersion(); + } + LeaveCriticalSection(&appFilterLock); +} + +static void ClearSeededEntries(void) +{ + UINT ix; + + EnterCriticalSection(&appFilterLock); + for (ix = 0; ix < APP_FILTER_MAX_FLOWS; ++ix) + { + if (flows[ix].active && flows[ix].seeded) + { + if (flows[ix].isTarget) + { + InterlockedDecrement(&stats.targetFlowCount); + } + memset(&flows[ix], 0, sizeof(flows[ix])); + } + } + for (ix = 0; ix < APP_FILTER_MAX_ENDPOINTS; ++ix) + { + if (endpoints[ix].active) + { + if (endpoints[ix].isTarget) + { + InterlockedDecrement(&stats.targetEndpointCount); + } + memset(&endpoints[ix], 0, sizeof(endpoints[ix])); + } + } + LeaveCriticalSection(&appFilterLock); +} + +static void PurgeExpiredLingeringFlowsLocked(DWORD now) +{ + UINT ix; + + for (ix = 0; ix < APP_FILTER_MAX_FLOWS; ++ix) + { + if (flows[ix].active && flows[ix].isTarget && flows[ix].lingering && + now - flows[ix].lastSeenTick >= APP_FILTER_FLOW_LINGER_MS) + { + InterlockedDecrement(&stats.targetFlowCount); + memset(&flows[ix], 0, sizeof(flows[ix])); + } + } +} + +static BOOL LookupFlowLocked(const FlowKey* key, BOOL* isTarget) +{ + int slot; + + slot = FindFlowByKeyLocked(key); + if (slot < 0) + { + return FALSE; + } + flows[slot].lastSeenTick = GetTickCount(); + *isTarget = flows[slot].isTarget; + return TRUE; +} + +static BOOL LookupEndpointKeyLocked(const EndpointKey* key, BOOL* isTarget) +{ + int slot; + + slot = FindEndpointByKeyLocked(key); + if (slot < 0) + { + return FALSE; + } + endpoints[slot].lastSeenTick = GetTickCount(); + *isTarget = endpoints[slot].isTarget; + return TRUE; +} + +static BOOL LookupUdpEndpointLocked(const FlowKey* key, BOOL* isTarget) +{ + EndpointKey endpoint; + + EndpointKeyFromFlowKey(&endpoint, key); + if (LookupEndpointKeyLocked(&endpoint, isTarget)) + { + return TRUE; + } + + SetIpv4MappedAddress(endpoint.localAddr, 0); + if (LookupEndpointKeyLocked(&endpoint, isTarget)) + { + return TRUE; + } + + SetZeroAddress(endpoint.localAddr); + return LookupEndpointKeyLocked(&endpoint, isTarget); +} + +static BOOL LookupLocalPortTargetLocked(const FlowKey* key, BOOL* isTarget) +{ + UINT ix; + + if (!IsPortFilterable(key->protocol, key->localPort)) + { + return FALSE; + } + + for (ix = 0; ix < APP_FILTER_MAX_TARGET_PORTS; ++ix) + { + if (targetPorts[ix].active && + targetPorts[ix].protocol == key->protocol && + targetPorts[ix].localPort == key->localPort) + { + targetPorts[ix].lastSeenTick = GetTickCount(); + *isTarget = TRUE; + return TRUE; + } + } + + for (ix = 0; ix < APP_FILTER_MAX_FLOWS; ++ix) + { + if (flows[ix].active && + flows[ix].isTarget && + flows[ix].key.protocol == key->protocol && + flows[ix].key.localPort == key->localPort) + { + flows[ix].lastSeenTick = GetTickCount(); + *isTarget = TRUE; + return TRUE; + } + } + + for (ix = 0; ix < APP_FILTER_MAX_ENDPOINTS; ++ix) + { + if (endpoints[ix].active && + endpoints[ix].isTarget && + endpoints[ix].key.protocol == key->protocol && + endpoints[ix].key.localPort == key->localPort) + { + endpoints[ix].lastSeenTick = GetTickCount(); + *isTarget = TRUE; + return TRUE; + } + } + + return FALSE; +} + +static BOOL BuildPacketFlowKey(const char* packet, UINT packetLen, + const WINDIVERT_ADDRESS* addr, FlowKey* key) +{ + PWINDIVERT_IPHDR ipHeader; + PWINDIVERT_IPV6HDR ipv6Header; + PWINDIVERT_TCPHDR tcpHeader; + PWINDIVERT_UDPHDR udpHeader; + UINT32 srcAddr6[4]; + UINT32 dstAddr6[4]; + UINT8 srcAddr[16]; + UINT8 dstAddr[16]; + UINT16 srcPort; + UINT16 dstPort; + + ipHeader = NULL; + ipv6Header = NULL; + tcpHeader = NULL; + udpHeader = NULL; + memset(key, 0, sizeof(*key)); + + if (addr == NULL || + !WinDivertHelperParsePacket(packet, packetLen, &ipHeader, &ipv6Header, NULL, + NULL, NULL, &tcpHeader, &udpHeader, NULL, NULL, NULL, NULL)) + { + return FALSE; + } + + if (tcpHeader != NULL) + { + key->protocol = IPPROTO_TCP; + srcPort = WinDivertHelperNtohs(tcpHeader->SrcPort); + dstPort = WinDivertHelperNtohs(tcpHeader->DstPort); + } + else if (udpHeader != NULL) + { + key->protocol = IPPROTO_UDP; + srcPort = WinDivertHelperNtohs(udpHeader->SrcPort); + dstPort = WinDivertHelperNtohs(udpHeader->DstPort); + } + else + { + return FALSE; + } + + if (ipHeader != NULL) + { + SetIpv4MappedAddress(srcAddr, WinDivertHelperNtohl(ipHeader->SrcAddr)); + SetIpv4MappedAddress(dstAddr, WinDivertHelperNtohl(ipHeader->DstAddr)); + } + else if (ipv6Header != NULL) + { + WinDivertHelperNtohIPv6Address(ipv6Header->SrcAddr, srcAddr6); + WinDivertHelperNtohIPv6Address(ipv6Header->DstAddr, dstAddr6); + SetHostIpv6Address(srcAddr, srcAddr6); + SetHostIpv6Address(dstAddr, dstAddr6); + } + else + { + return FALSE; + } + + if (addr->Outbound) + { + memcpy(key->localAddr, srcAddr, sizeof(key->localAddr)); + memcpy(key->remoteAddr, dstAddr, sizeof(key->remoteAddr)); + key->localPort = srcPort; + key->remotePort = dstPort; + } + else + { + memcpy(key->localAddr, dstAddr, sizeof(key->localAddr)); + memcpy(key->remoteAddr, srcAddr, sizeof(key->remoteAddr)); + key->localPort = dstPort; + key->remotePort = srcPort; + } + + return TRUE; +} + +static BOOL LookupPacketTarget(const FlowKey* key, const WINDIVERT_ADDRESS* addr, + BOOL* isTarget) +{ + FlowKey reversed; + + EnterCriticalSection(&appFilterLock); + if (LookupLocalPortTargetLocked(key, isTarget)) + { + LeaveCriticalSection(&appFilterLock); + return TRUE; + } + + if (LookupFlowLocked(key, isTarget)) + { + LeaveCriticalSection(&appFilterLock); + return TRUE; + } + + if (addr != NULL && addr->Loopback) + { + ReverseFlowKey(&reversed, key); + if (LookupFlowLocked(&reversed, isTarget)) + { + LeaveCriticalSection(&appFilterLock); + return TRUE; + } + } + + if (key->protocol == IPPROTO_UDP) + { + if (LookupUdpEndpointLocked(key, isTarget)) + { + LeaveCriticalSection(&appFilterLock); + return TRUE; + } + if (addr != NULL && addr->Loopback) + { + ReverseFlowKey(&reversed, key); + if (LookupUdpEndpointLocked(&reversed, isTarget)) + { + LeaveCriticalSection(&appFilterLock); + return TRUE; + } + } + } + + LeaveCriticalSection(&appFilterLock); + return FALSE; +} + +static void MaybeRefreshSeededTables(void) +{ + DWORD now; + + if (!AppFilterIsEnabled()) + { + return; + } + now = GetTickCount(); + if (now - lastRefreshTick < APP_FILTER_REFRESH_MS) + { + return; + } + if (InterlockedCompareExchange(&refreshInProgress, TRUE, FALSE) != FALSE) + { + return; + } + if (now - lastRefreshTick >= APP_FILTER_REFRESH_MS) + { + SeedIpHelperTables(TRUE); + lastRefreshTick = GetTickCount(); + } + InterlockedExchange(&refreshInProgress, FALSE); +} + +static BOOL ParseTargets(const char* targetText, AppFilterMode mode, + char* statusBuf, UINT statusBufLen) +{ + char buf[APP_FILTER_TARGETS_BUFSIZE]; + char token[APP_FILTER_TARGET_LEN]; + char* start; + char* end; + char* p; + UINT len; + + targetCount = 0; + memset(targets, 0, sizeof(targets)); + + if (IsBlank(targetText)) + { + SetStatus(statusBuf, statusBufLen, + "Application filter target is empty. Enter a process name or path."); + return FALSE; + } + + CopyString(buf, sizeof(buf), targetText); + start = buf; + p = buf; + for (;;) + { + if (*p == ',' || *p == ';' || *p == '\0') + { + end = p; + while (start < end && isspace((unsigned char)*start)) + { + ++start; + } + while (end > start && isspace((unsigned char)*(end - 1))) + { + --end; + } + len = (UINT)(end - start); + if (len > 0) + { + if (targetCount >= APP_FILTER_MAX_TARGETS) + { + SetStatus(statusBuf, statusBufLen, + "Application filter has too many targets."); + return FALSE; + } + if (len >= sizeof(token)) + { + len = sizeof(token) - 1; + } + memcpy(token, start, len); + token[len] = '\0'; + NormalizePathText(token); + if (mode == APP_FILTER_MODE_PROCESS_NAME) + { + CopyString(targets[targetCount].text, + sizeof(targets[targetCount].text), BaseName(token)); + } + else + { + CopyString(targets[targetCount].text, + sizeof(targets[targetCount].text), token); + } + targets[targetCount].hasExeSuffix = EndsWithExe(targets[targetCount].text); + ++targetCount; + } + if (*p == '\0') + { + break; + } + start = p + 1; + } + ++p; + } + + if (targetCount == 0) + { + SetStatus(statusBuf, statusBufLen, + "Application filter target is empty. Enter a process name or path."); + return FALSE; + } + return TRUE; +} + +static void SeedTcp4Table(void) +{ + PMIB_TCPTABLE_OWNER_PID table; + DWORD size; + DWORD err; + DWORD ix; + FlowKey key; + PMIB_TCPROW_OWNER_PID row; + BOOL isTarget; + + size = 0; + err = GetExtendedTcpTable(NULL, &size, FALSE, AF_INET, + TCP_TABLE_OWNER_PID_ALL, 0); + if (err != ERROR_INSUFFICIENT_BUFFER) + { + LOG("GetExtendedTcpTable IPv4 sizing failed (%lu)", err); + return; + } + + table = (PMIB_TCPTABLE_OWNER_PID)malloc(size); + if (table == NULL) + { + LOG("Failed to allocate IPv4 TCP table (%lu bytes)", size); + return; + } + + err = GetExtendedTcpTable(table, &size, FALSE, AF_INET, + TCP_TABLE_OWNER_PID_ALL, 0); + if (err == NO_ERROR) + { + for (ix = 0; ix < table->dwNumEntries; ++ix) + { + row = &table->table[ix]; + memset(&key, 0, sizeof(key)); + key.protocol = IPPROTO_TCP; + SetIpv4MappedAddress(key.localAddr, WinDivertHelperNtohl(row->dwLocalAddr)); + SetIpv4MappedAddress(key.remoteAddr, WinDivertHelperNtohl(row->dwRemoteAddr)); + key.localPort = WinDivertHelperNtohs((UINT16)row->dwLocalPort); + key.remotePort = WinDivertHelperNtohs((UINT16)row->dwRemotePort); + isTarget = IsTargetPid(row->dwOwningPid); + AddOrUpdateFlow(&key, 0, row->dwOwningPid, isTarget, TRUE); + } + } + else + { + LOG("GetExtendedTcpTable IPv4 failed (%lu)", err); + } + + free(table); +} + +static void SeedTcp6Table(void) +{ + PMIB_TCP6TABLE_OWNER_PID table; + DWORD size; + DWORD err; + DWORD ix; + FlowKey key; + PMIB_TCP6ROW_OWNER_PID row; + BOOL isTarget; + + size = 0; + err = GetExtendedTcpTable(NULL, &size, FALSE, AF_INET6, + TCP_TABLE_OWNER_PID_ALL, 0); + if (err != ERROR_INSUFFICIENT_BUFFER) + { + LOG("GetExtendedTcpTable IPv6 sizing failed (%lu)", err); + return; + } + + table = (PMIB_TCP6TABLE_OWNER_PID)malloc(size); + if (table == NULL) + { + LOG("Failed to allocate IPv6 TCP table (%lu bytes)", size); + return; + } + + err = GetExtendedTcpTable(table, &size, FALSE, AF_INET6, + TCP_TABLE_OWNER_PID_ALL, 0); + if (err == NO_ERROR) + { + for (ix = 0; ix < table->dwNumEntries; ++ix) + { + row = &table->table[ix]; + memset(&key, 0, sizeof(key)); + key.protocol = IPPROTO_TCP; + SetNetworkIpv6Address(key.localAddr, row->ucLocalAddr); + SetNetworkIpv6Address(key.remoteAddr, row->ucRemoteAddr); + key.localPort = WinDivertHelperNtohs((UINT16)row->dwLocalPort); + key.remotePort = WinDivertHelperNtohs((UINT16)row->dwRemotePort); + isTarget = IsTargetPid(row->dwOwningPid); + AddOrUpdateFlow(&key, 0, row->dwOwningPid, isTarget, TRUE); + } + } + else + { + LOG("GetExtendedTcpTable IPv6 failed (%lu)", err); + } + + free(table); +} + +static void SeedUdp4Table(void) +{ + PMIB_UDPTABLE_OWNER_PID table; + DWORD size; + DWORD err; + DWORD ix; + EndpointKey key; + PMIB_UDPROW_OWNER_PID row; + BOOL isTarget; + + size = 0; + err = GetExtendedUdpTable(NULL, &size, FALSE, AF_INET, + UDP_TABLE_OWNER_PID, 0); + if (err != ERROR_INSUFFICIENT_BUFFER) + { + LOG("GetExtendedUdpTable IPv4 sizing failed (%lu)", err); + return; + } + + table = (PMIB_UDPTABLE_OWNER_PID)malloc(size); + if (table == NULL) + { + LOG("Failed to allocate IPv4 UDP table (%lu bytes)", size); + return; + } + + err = GetExtendedUdpTable(table, &size, FALSE, AF_INET, + UDP_TABLE_OWNER_PID, 0); + if (err == NO_ERROR) + { + for (ix = 0; ix < table->dwNumEntries; ++ix) + { + row = &table->table[ix]; + memset(&key, 0, sizeof(key)); + key.protocol = IPPROTO_UDP; + SetIpv4MappedAddress(key.localAddr, WinDivertHelperNtohl(row->dwLocalAddr)); + key.localPort = WinDivertHelperNtohs((UINT16)row->dwLocalPort); + isTarget = IsTargetPid(row->dwOwningPid); + AddOrUpdateEndpoint(&key, row->dwOwningPid, isTarget); + } + } + else + { + LOG("GetExtendedUdpTable IPv4 failed (%lu)", err); + } + + free(table); +} + +static void SeedUdp6Table(void) +{ + PMIB_UDP6TABLE_OWNER_PID table; + DWORD size; + DWORD err; + DWORD ix; + EndpointKey key; + PMIB_UDP6ROW_OWNER_PID row; + BOOL isTarget; + + size = 0; + err = GetExtendedUdpTable(NULL, &size, FALSE, AF_INET6, + UDP_TABLE_OWNER_PID, 0); + if (err != ERROR_INSUFFICIENT_BUFFER) + { + LOG("GetExtendedUdpTable IPv6 sizing failed (%lu)", err); + return; + } + + table = (PMIB_UDP6TABLE_OWNER_PID)malloc(size); + if (table == NULL) + { + LOG("Failed to allocate IPv6 UDP table (%lu bytes)", size); + return; + } + + err = GetExtendedUdpTable(table, &size, FALSE, AF_INET6, + UDP_TABLE_OWNER_PID, 0); + if (err == NO_ERROR) + { + for (ix = 0; ix < table->dwNumEntries; ++ix) + { + row = &table->table[ix]; + memset(&key, 0, sizeof(key)); + key.protocol = IPPROTO_UDP; + SetNetworkIpv6Address(key.localAddr, row->ucLocalAddr); + key.localPort = WinDivertHelperNtohs((UINT16)row->dwLocalPort); + isTarget = IsTargetPid(row->dwOwningPid); + AddOrUpdateEndpoint(&key, row->dwOwningPid, isTarget); + } + } + else + { + LOG("GetExtendedUdpTable IPv6 failed (%lu)", err); + } + + free(table); +} + +static void SeedIpHelperTables(BOOL clearSeededFirst) +{ + if (!AppFilterIsEnabled()) + { + return; + } + if (clearSeededFirst) + { + ClearSeededEntries(); + } + SeedTcp4Table(); + SeedTcp6Table(); + SeedUdp4Table(); + SeedUdp6Table(); + LOG("App filter seeded tables: target pids=%ld flows=%ld endpoints=%ld", + stats.targetPidCount, stats.targetFlowCount, stats.targetEndpointCount); +} + +static BOOL AppendText(char* dst, UINT dstLen, const char* src) +{ + size_t used; + size_t add; + + if (dstLen == 0 || src == NULL) + { + return FALSE; + } + used = strlen(dst); + add = strlen(src); + if (used + add + 1 > dstLen) + { + return FALSE; + } + memcpy(dst + used, src, add + 1); + return TRUE; +} + +static BOOL AppendFormat(char* dst, UINT dstLen, const char* fmt, ...) +{ + char buf[256]; + va_list args; + int written; + + va_start(args, fmt); + written = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + if (written < 0 || written >= (int)sizeof(buf)) + { + return FALSE; + } + return AppendText(dst, dstLen, buf); +} + +static BOOL AppendTargetPortClause(char* dst, UINT dstLen, + const TargetPortEntry* port, BOOL* hasClause) +{ + const char* proto; + + if (!IsPortFilterable(port->protocol, port->localPort)) + { + return TRUE; + } + + proto = port->protocol == IPPROTO_TCP ? "tcp" : "udp"; + if (*hasClause && !AppendText(dst, dstLen, " or ")) + { + return FALSE; + } + if (!AppendFormat(dst, dstLen, + "(%s and (%s.SrcPort == %u or %s.DstPort == %u))", + proto, proto, (UINT)port->localPort, proto, (UINT)port->localPort)) + { + return FALSE; + } + *hasClause = TRUE; + return TRUE; +} + +LONG AppFilterGetNetworkFilterVersion(void) +{ + if (AppFilterIsEnabled() && lockInitialized) + { + EnterCriticalSection(&appFilterLock); + if (PurgeExpiredTargetPortsLocked(GetTickCount())) + { + BumpNetworkFilterVersion(); + } + PurgeExpiredLingeringFlowsLocked(GetTickCount()); + LeaveCriticalSection(&appFilterLock); + } + return InterlockedCompareExchange(&networkFilterVersion, 0, 0); +} + +BOOL AppFilterBuildNetworkFilter(const char* baseFilter, char* filterBuf, + UINT filterBufLen, BOOL* hasTargetFilters) +{ + char targetFilter[FILTER_BUFSIZE]; + const char* base; + UINT ix; + BOOL hasClause = FALSE; + BOOL ok = TRUE; + + if (filterBuf == NULL || filterBufLen == 0) + { + return FALSE; + } + filterBuf[0] = '\0'; + if (hasTargetFilters != NULL) + { + *hasTargetFilters = FALSE; + } + + base = IsBlank(baseFilter) ? "true" : baseFilter; + targetFilter[0] = '\0'; + + if (!AppFilterIsEnabled()) + { + CopyString(filterBuf, filterBufLen, base); + return TRUE; + } + + EnterCriticalSection(&appFilterLock); + if (PurgeExpiredTargetPortsLocked(GetTickCount())) + { + BumpNetworkFilterVersion(); + } + PurgeExpiredLingeringFlowsLocked(GetTickCount()); + for (ix = 0; ok && ix < APP_FILTER_MAX_TARGET_PORTS; ++ix) + { + if (targetPorts[ix].active) + { + ok = AppendTargetPortClause(targetFilter, sizeof(targetFilter), + &targetPorts[ix], &hasClause); + } + } + LeaveCriticalSection(&appFilterLock); + + if (!ok) + { + LOG("Application target network filter exceeded %u bytes.", + (UINT)sizeof(targetFilter)); + if (!hasClause) + { + CopyString(targetFilter, sizeof(targetFilter), "false"); + } + } + else if (!hasClause) + { + CopyString(targetFilter, sizeof(targetFilter), "false"); + } + + if (!AppendText(filterBuf, filterBufLen, "(") || + !AppendText(filterBuf, filterBufLen, base) || + !AppendText(filterBuf, filterBufLen, ") and (") || + !AppendText(filterBuf, filterBufLen, targetFilter) || + !AppendText(filterBuf, filterBufLen, ")")) + { + CopyString(filterBuf, filterBufLen, "false"); + hasClause = FALSE; + LOG("Application network filter exceeded %u bytes; capture is paused.", + filterBufLen); + } + + if (hasTargetFilters != NULL) + { + *hasTargetFilters = hasClause; + } + return TRUE; +} + +void AppFilterDefaultConfig(AppFilterConfig* config) +{ + if (config == NULL) + { + return; + } + memset(config, 0, sizeof(*config)); + config->enabled = FALSE; + config->mode = APP_FILTER_MODE_PROCESS_NAME; + config->includeChildProcesses = TRUE; + config->targets[0] = '\0'; +} + +BOOL AppFilterStart(const AppFilterConfig* config, char* statusBuf, UINT statusBufLen) +{ + AppFilterConfig localConfig; + + if (config == NULL) + { + AppFilterDefaultConfig(&localConfig); + config = &localConfig; + } + + AppFilterStop(); + if (!config->enabled) + { + SetStatus(statusBuf, statusBufLen, ""); + return TRUE; + } + + configuredMode = config->mode; + includeChildProcesses = config->includeChildProcesses; + if (!ParseTargets(config->targets, configuredMode, statusBuf, statusBufLen)) + { + return FALSE; + } + memset(flows, 0, sizeof(flows)); + memset(endpoints, 0, sizeof(endpoints)); + memset(pidCache, 0, sizeof(pidCache)); + memset(targetPorts, 0, sizeof(targetPorts)); + memset(&stats, 0, sizeof(stats)); + InterlockedExchange(&networkFilterVersion, 1); + InitializeCriticalSection(&appFilterLock); + lockInitialized = TRUE; + InterlockedExchange(&stopFlowThread, FALSE); + InterlockedExchange(&refreshInProgress, FALSE); + InterlockedExchange(&appFilterEnabled, TRUE); + + lastRefreshTick = GetTickCount(); + SeedIpHelperTables(FALSE); + + flowHandle = WinDivertOpen("true", WINDIVERT_LAYER_FLOW, APP_FILTER_FLOW_PRIORITY, + WINDIVERT_FLAG_SNIFF | WINDIVERT_FLAG_RECV_ONLY); + if (flowHandle == INVALID_HANDLE_VALUE) + { + sprintf(statusBuf, "Failed to start application filter flow tracker (code:%lu).", + GetLastError()); + AppFilterStop(); + return FALSE; + } + + flowThread = CreateThread(NULL, 1, AppFilterFlowThread, NULL, 0, NULL); + if (flowThread == NULL) + { + sprintf(statusBuf, "Failed to create application filter flow thread (%lu).", + GetLastError()); + AppFilterStop(); + return FALSE; + } + + LOG("Application filter started with %u target(s).", targetCount); + SetStatus(statusBuf, statusBufLen, "Application filter started."); + return TRUE; +} + +void AppFilterStop(void) +{ + HANDLE thread; + HANDLE handle; + + InterlockedExchange(&appFilterEnabled, FALSE); + InterlockedExchange(&stopFlowThread, TRUE); + + thread = flowThread; + handle = flowHandle; + if (handle != INVALID_HANDLE_VALUE) + { + WinDivertShutdown(handle, WINDIVERT_SHUTDOWN_RECV); + } + if (thread != NULL) + { + WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); + flowThread = NULL; + } + if (handle != INVALID_HANDLE_VALUE) + { + WinDivertClose(handle); + flowHandle = INVALID_HANDLE_VALUE; + } + + if (lockInitialized) + { + EnterCriticalSection(&appFilterLock); + memset(flows, 0, sizeof(flows)); + memset(endpoints, 0, sizeof(endpoints)); + memset(pidCache, 0, sizeof(pidCache)); + memset(targetPorts, 0, sizeof(targetPorts)); + memset(&stats, 0, sizeof(stats)); + targetCount = 0; + LeaveCriticalSection(&appFilterLock); + DeleteCriticalSection(&appFilterLock); + lockInitialized = FALSE; + } + includeChildProcesses = TRUE; + InterlockedExchange(&networkFilterVersion, 0); +} + +BOOL AppFilterIsEnabled(void) +{ + return InterlockedCompareExchange(&appFilterEnabled, FALSE, FALSE) != FALSE; +} + +BOOL AppFilterShouldAffectPacket(const char* packet, UINT packetLen, + const WINDIVERT_ADDRESS* addr) +{ + FlowKey key; + BOOL isTarget; + + if (!AppFilterIsEnabled()) + { + return TRUE; + } + + if (!BuildPacketFlowKey(packet, packetLen, addr, &key)) + { + InterlockedIncrement(&stats.unknownPackets); + return FALSE; + } + + if (LookupPacketTarget(&key, addr, &isTarget)) + { + if (isTarget) + { + InterlockedIncrement(&stats.affectedPackets); + return TRUE; + } + InterlockedIncrement(&stats.passedUnmatchedPackets); + return FALSE; + } + + InterlockedIncrement(&stats.unknownPackets); + MaybeRefreshSeededTables(); + return FALSE; +} + +void AppFilterGetStats(AppFilterStats* outStats) +{ + if (outStats == NULL) + { + return; + } + *outStats = stats; +} + +static DWORD WINAPI AppFilterFlowThread(LPVOID arg) +{ + WINDIVERT_ADDRESS addr; + FlowKey key; + BOOL isTarget; + DWORD lastError; + + UNREFERENCED_PARAMETER(arg); + + for (;;) + { + memset(&addr, 0, sizeof(addr)); + if (!WinDivertRecv(flowHandle, NULL, 0, NULL, &addr)) + { + lastError = GetLastError(); + if (InterlockedCompareExchange(&stopFlowThread, FALSE, FALSE) || + lastError == ERROR_OPERATION_ABORTED || + lastError == ERROR_INVALID_HANDLE) + { + return 0; + } + LOG("Application filter flow recv failed (%lu)", lastError); + Sleep(10); + continue; + } + + if (!AppFilterIsEnabled()) + { + continue; + } + + if (addr.Event == WINDIVERT_EVENT_FLOW_ESTABLISHED) + { + FlowKeyFromFlowAddress(&key, &addr); + isTarget = IsTargetPid(addr.Flow.ProcessId); + AddOrUpdateFlow(&key, addr.Flow.EndpointId, addr.Flow.ProcessId, + isTarget, FALSE); + if (isTarget) + { + LOG("Application filter tracked target flow pid=%lu endpoint=%I64u", + addr.Flow.ProcessId, addr.Flow.EndpointId); + } + } + else if (addr.Event == WINDIVERT_EVENT_FLOW_DELETED) + { + FlowKeyFromFlowAddress(&key, &addr); + RemoveFlowByEndpointOrKey(&key, addr.Flow.EndpointId); + } + } +} diff --git a/src/app_filter.h b/src/app_filter.h new file mode 100644 index 0000000..e7b6ca7 --- /dev/null +++ b/src/app_filter.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include "windivert.h" + +#define APP_FILTER_TARGETS_BUFSIZE 512 + +typedef enum AppFilterMode +{ + APP_FILTER_MODE_PROCESS_NAME = 0, + APP_FILTER_MODE_FULL_PATH = 1 +} AppFilterMode; + +typedef struct AppFilterConfig +{ + BOOL enabled; + AppFilterMode mode; + BOOL includeChildProcesses; + char targets[APP_FILTER_TARGETS_BUFSIZE]; +} AppFilterConfig; + +typedef struct AppFilterStats +{ + LONG targetPidCount; + LONG targetFlowCount; + LONG targetEndpointCount; + LONG affectedPackets; + LONG passedUnmatchedPackets; + LONG unknownPackets; +} AppFilterStats; + +void AppFilterDefaultConfig(AppFilterConfig* config); +BOOL AppFilterStart(const AppFilterConfig* config, char* statusBuf, UINT statusBufLen); +void AppFilterStop(void); +BOOL AppFilterIsEnabled(void); +LONG AppFilterGetNetworkFilterVersion(void); +BOOL AppFilterBuildNetworkFilter(const char* baseFilter, char* filterBuf, + UINT filterBufLen, BOOL* hasTargetFilters); +BOOL AppFilterShouldAffectPacket(const char* packet, UINT packetLen, + const WINDIVERT_ADDRESS* addr); +void AppFilterGetStats(AppFilterStats* stats); diff --git a/src/bandwidth.c b/src/bandwidth.c index 5b6a472..97794f6 100644 --- a/src/bandwidth.c +++ b/src/bandwidth.c @@ -50,6 +50,8 @@ static volatile short bandwidthEnabled = 0, static volatile LONG bandwidthLimit = BANDWIDTH_DEFAULT; static CRateStats *rateStats = NULL; +static DWORD lastDropLogTick = 0; +static int pendingDropLogCount = 0; static Ihandle* bandwidthSetupUI() { @@ -88,6 +90,8 @@ static Ihandle* bandwidthSetupUI() { static void bandwidthStartUp() { if (rateStats) crate_stats_delete(rateStats); rateStats = crate_stats_new(1000, 1000); + lastDropLogTick = 0; + pendingDropLogCount = 0; LOG("bandwidth enabled"); } @@ -121,8 +125,6 @@ static short bandwidthProcess(PacketNode *head, PacketNode* tail) { int rate = crate_stats_calculate(rateStats, now_ts); int size = pac->packetLen; if (rate + size > limit) { - LOG("dropped with bandwidth %dKB/s, direction %s", - (int)bandwidthLimit, pac->addr.Outbound ? "OUTBOUND" : "INBOUND"); discard = 1; } else { @@ -137,6 +139,18 @@ static short bandwidthProcess(PacketNode *head, PacketNode* tail) { } } + if (dropped > 0) + { + pendingDropLogCount += dropped; + if (lastDropLogTick == 0 || now_ts - lastDropLogTick >= 1000) + { + LOG("dropped %d packets with bandwidth %dKB/s", + pendingDropLogCount, (int)bandwidthLimit); + pendingDropLogCount = 0; + lastDropLogTick = now_ts; + } + } + return dropped > 0; } diff --git a/src/common.h b/src/common.h index e140f91..505bd74 100644 --- a/src/common.h +++ b/src/common.h @@ -1,12 +1,14 @@ #pragma once #include +#include #include #include "iup.h" #include "windivert.h" +#include "app_filter.h" -#define CLUMSY_VERSION "0.3" +#define CLUMSY_VERSION "4.0" #define MSG_BUFSIZE 512 -#define FILTER_BUFSIZE 1024 +#define FILTER_BUFSIZE 32768 #define NAME_SIZE 16 #define MODULE_CNT 8 #define ICON_UPDATE_MS 200 @@ -70,10 +72,13 @@ static void VsLog(const char* pFmt, ...) va_list args; va_start(args, pFmt); - vsprintf_s(buf, 1024, pFmt, args); + vsnprintf_s(buf, sizeof(buf), _TRUNCATE, pFmt, args); va_end(args); + buf[sizeof(buf) - 1] = '\0'; OutputDebugString(buf); + fputs(buf, stderr); + fflush(stderr); } #define LOG(fmt, ...) (VsLog(__FUNCTION__ ": " fmt "\n", ##__VA_ARGS__)) @@ -159,7 +164,7 @@ extern volatile short sendState; void showStatus(const char* line); // WinDivert -int divertStart(const char * filter, char buf[]); +int divertStart(const char * filter, const AppFilterConfig *appConfig, char buf[]); void divertStop(); // utils diff --git a/src/divert.c b/src/divert.c index 0e1b5df..64f7c91 100644 --- a/src/divert.c +++ b/src/divert.c @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include "windivert.h" @@ -15,9 +15,17 @@ static HANDLE divertHandle; static volatile short stopLooping; static HANDLE loopThread, clockThread, mutex; +static char baseFilter[FILTER_BUFSIZE]; +static char activeFilter[FILTER_BUFSIZE]; +static volatile LONG appliedNetworkFilterVersion = 0; +static volatile LONG reopenNetworkFilter = FALSE; static DWORD divertReadLoop(LPVOID arg); static DWORD divertClockLoop(LPVOID arg); +static BOOL divertBuildEffectiveFilter(char *buf, UINT bufLen); +static BOOL divertOpenNetworkHandle(const char *filter, char buf[]); +static BOOL divertReopenNetworkHandle(void); +static void divertMaybeRequestNetworkReopen(void); // not to put these in common.h since modules shouldn't see these extern PacketNode * const head; @@ -77,9 +85,48 @@ void dumpPacket(char *buf, int len, PWINDIVERT_ADDRESS paddr) { #define dumpPacket(x, y, z) #endif -int divertStart(const char *filter, char buf[]) { - int ix; +static void copyFilterText(char* dst, UINT dstLen, const char* src) +{ + UINT ix; + + if (dstLen == 0) + { + return; + } + if (src == NULL) + { + dst[0] = '\0'; + return; + } + for (ix = 0; ix + 1 < dstLen && src[ix] != '\0'; ++ix) + { + dst[ix] = src[ix]; + } + dst[ix] = '\0'; +} + +static BOOL divertBuildEffectiveFilter(char* buf, UINT bufLen) +{ + BOOL hasTargetFilters = FALSE; + + if (AppFilterIsEnabled()) + { + if (!AppFilterBuildNetworkFilter(baseFilter, buf, bufLen, &hasTargetFilters)) + { + return FALSE; + } + if (!hasTargetFilters) + { + LOG("Application filter has no target flows; packet capture is paused."); + } + return TRUE; + } + + copyFilterText(buf, bufLen, baseFilter); + return TRUE; +} +static BOOL divertOpenNetworkHandle(const char *filter, char buf[]) { divertHandle = WinDivertOpen(filter, WINDIVERT_LAYER_NETWORK, DIVERT_PRIORITY, 0); if (divertHandle == INVALID_HANDLE_VALUE) { DWORD lastError = GetLastError(); @@ -91,11 +138,109 @@ int divertStart(const char *filter, char buf[]) { } return FALSE; } - LOG("Divert opened handle."); + LOG("Divert opened handle with filter length %u: %.800s%s", + (UINT)strlen(filter), filter, strlen(filter) > 800 ? "..." : ""); WinDivertSetParam(divertHandle, WINDIVERT_PARAM_QUEUE_LENGTH, QUEUE_LEN); WinDivertSetParam(divertHandle, WINDIVERT_PARAM_QUEUE_TIME, QUEUE_TIME); LOG("WinDivert internal queue Len: %d, queue time: %d", QUEUE_LEN, QUEUE_TIME); + return TRUE; +} + +static BOOL divertReopenNetworkHandle(void) +{ + char nextFilter[FILTER_BUFSIZE]; + char errBuf[MSG_BUFSIZE]; + HANDLE oldHandle; + DWORD waitResult; + + waitResult = WaitForSingleObject(mutex, INFINITE); + if (waitResult != WAIT_OBJECT_0 && waitResult != WAIT_ABANDONED) + { + LOG("Failed to acquire mutex for network filter reopen (%lu)", GetLastError()); + InterlockedIncrement16(&stopLooping); + return FALSE; + } + + if (!divertBuildEffectiveFilter(nextFilter, sizeof(nextFilter))) + { + LOG("Failed to build application network filter."); + InterlockedIncrement16(&stopLooping); + ReleaseMutex(mutex); + return FALSE; + } + + oldHandle = divertHandle; + if (!divertOpenNetworkHandle(nextFilter, errBuf)) + { + LOG("%s", errBuf); + divertHandle = oldHandle; + InterlockedIncrement16(&stopLooping); + ReleaseMutex(mutex); + return FALSE; + } + + if (oldHandle != INVALID_HANDLE_VALUE) + { + WinDivertClose(oldHandle); + } + copyFilterText(activeFilter, sizeof(activeFilter), nextFilter); + InterlockedExchange(&appliedNetworkFilterVersion, + AppFilterGetNetworkFilterVersion()); + InterlockedExchange(&reopenNetworkFilter, FALSE); + + if (!ReleaseMutex(mutex)) + { + LOG("Fatal: Failed to release mutex after network filter reopen (%lu)", + GetLastError()); + ABORT(); + } + return TRUE; +} + +static void divertMaybeRequestNetworkReopen(void) +{ + LONG version; + + if (stopLooping || !AppFilterIsEnabled()) + { + return; + } + + version = AppFilterGetNetworkFilterVersion(); + if (version == appliedNetworkFilterVersion) + { + return; + } + + if (InterlockedCompareExchange(&reopenNetworkFilter, TRUE, FALSE) == FALSE) + { + LOG("Application target flow set changed; reopening packet filter."); + WinDivertShutdown(divertHandle, WINDIVERT_SHUTDOWN_RECV); + } +} + +int divertStart(const char* filter, const AppFilterConfig* appConfig, char buf[]) +{ + int ix; + copyFilterText(baseFilter, sizeof(baseFilter), filter); + activeFilter[0] = '\0'; + InterlockedExchange(&appliedNetworkFilterVersion, 0); + InterlockedExchange(&reopenNetworkFilter, FALSE); + + if (!AppFilterStart(appConfig, buf, MSG_BUFSIZE)) + { + return FALSE; + } + + if (!divertBuildEffectiveFilter(activeFilter, sizeof(activeFilter)) || + !divertOpenNetworkHandle(activeFilter, buf)) + { + AppFilterStop(); + return FALSE; + } + InterlockedExchange(&appliedNetworkFilterVersion, + AppFilterGetNetworkFilterVersion()); // init package link list initPacketNodeList(); @@ -111,17 +256,31 @@ int divertStart(const char *filter, char buf[]) { mutex = CreateMutex(NULL, FALSE, NULL); if (mutex == NULL) { sprintf(buf, "Failed to create mutex (%lu)", GetLastError()); + WinDivertClose(divertHandle); + AppFilterStop(); return FALSE; } loopThread = CreateThread(NULL, 1, (LPTHREAD_START_ROUTINE)divertReadLoop, NULL, 0, NULL); if (loopThread == NULL) { sprintf(buf, "Failed to create recv loop thread (%lu)", GetLastError()); + CloseHandle(mutex); + mutex = NULL; + WinDivertClose(divertHandle); + AppFilterStop(); return FALSE; } clockThread = CreateThread(NULL, 1, (LPTHREAD_START_ROUTINE)divertClockLoop, NULL, 0, NULL); if (clockThread == NULL) { sprintf(buf, "Failed to create clock loop thread (%lu)", GetLastError()); + InterlockedIncrement16(&stopLooping); + WinDivertClose(divertHandle); + WaitForSingleObject(loopThread, INFINITE); + CloseHandle(loopThread); + loopThread = NULL; + CloseHandle(mutex); + mutex = NULL; + AppFilterStop(); return FALSE; } @@ -243,6 +402,8 @@ static DWORD divertClockLoop(LPVOID arg) { UNREFERENCED_PARAMETER(arg); for(;;) { + divertMaybeRequestNetworkReopen(); + // use acquire as wait for yielding thread startTick = GetTickCount(); waitResult = WaitForSingleObject(mutex, CLOCK_WAITMS); @@ -329,11 +490,36 @@ static DWORD divertReadLoop(LPVOID arg) { UNREFERENCED_PARAMETER(arg); for(;;) { - // each step must fully consume the list - assert(isListEmpty()); // FIXME has failed this assert before. don't know why + // The clock thread may have released packets from module buffers + // into the list after the last iteration finished. Drain them + // before blocking on WinDivertRecv so the list stays bounded. + // We must hold the mutex here because the clock thread also + // modifies the list inside divertConsumeStep(). + if (!isListEmpty()) { + DWORD drainResult = WaitForSingleObject(mutex, INFINITE); + if (drainResult == WAIT_OBJECT_0) { + if (!stopLooping) { + divertConsumeStep(); + } + if (!ReleaseMutex(mutex)) { + LOG("Fatal: Failed to release mutex after draining (%lu)", GetLastError()); + } + } + } if (!WinDivertRecv(divertHandle, packetBuf, MAX_PACKETSIZE, &readLen, &addrBuf)) { DWORD lastError = GetLastError(); - if (lastError == ERROR_INVALID_HANDLE || lastError == ERROR_OPERATION_ABORTED) { + if (!stopLooping && reopenNetworkFilter && + (lastError == ERROR_OPERATION_ABORTED || + lastError == ERROR_INVALID_HANDLE || + lastError == ERROR_NO_DATA)) { + if (divertReopenNetworkHandle()) { + continue; + } + return 0; + } + if (lastError == ERROR_INVALID_HANDLE || + lastError == ERROR_OPERATION_ABORTED || + (stopLooping && lastError == ERROR_NO_DATA)) { // treat closing handle as quit LOG("Handle died or operation aborted. Exit loop."); return 0; @@ -348,6 +534,24 @@ static DWORD divertReadLoop(LPVOID arg) { //dumpPacket(packetBuf, readLen, &addrBuf); + if (stopLooping) { + LOG("Lost last recved packet but user stopped. Stop read loop."); + return 0; + } + + if (AppFilterIsEnabled() && + !AppFilterShouldAffectPacket(packetBuf, readLen, &addrBuf)) { + UINT sendLen = 0; + if (!WinDivertSend(divertHandle, packetBuf, readLen, &sendLen, &addrBuf) || + sendLen < readLen) { + if (!stopLooping) { + LOG("Failed to pass through non-target packet (%lu)", GetLastError()); + InterlockedExchange16(&sendState, SEND_STATUS_FAIL); + } + } + continue; + } + waitResult = WaitForSingleObject(mutex, INFINITE); switch(waitResult) { case WAIT_OBJECT_0: @@ -393,5 +597,27 @@ void divertStop() { InterlockedIncrement16(&stopLooping); WaitForMultipleObjects(2, threads, TRUE, INFINITE); + if (loopThread != NULL) + { + CloseHandle(loopThread); + loopThread = NULL; + } + if (clockThread != NULL) + { + CloseHandle(clockThread); + clockThread = NULL; + } + if (mutex != NULL) + { + CloseHandle(mutex); + mutex = NULL; + } + + AppFilterStop(); + InterlockedExchange(&appliedNetworkFilterVersion, 0); + InterlockedExchange(&reopenNetworkFilter, FALSE); + baseFilter[0] = '\0'; + activeFilter[0] = '\0'; + LOG("Successfully waited threads and stopped."); } diff --git a/src/elevate.c b/src/elevate.c index dd73fc4..f3f3af6 100644 --- a/src/elevate.c +++ b/src/elevate.c @@ -117,8 +117,8 @@ BOOL tryElevate(HWND hWnd, BOOL silent) { // when not silent then trying to reinvoke to elevate if (!silent) { - wchar_t szPath[MAX_PATH]; - if (GetModuleFileName(NULL, (LPSTR)szPath, ARRAYSIZE(szPath))) + char szPath[MAX_PATH]; + if (GetModuleFileName(NULL, szPath, ARRAYSIZE(szPath))) { // Launch itself as administrator. SHELLEXECUTEINFO sei = { sizeof(sei) }; diff --git a/src/main.c b/src/main.c index c6bb41c..68983c4 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include "iup.h" #include "common.h" @@ -24,11 +25,17 @@ volatile short sendState = SEND_STATUS_NONE; static Ihandle *dialog, *topFrame, *bottomFrame; static Ihandle *statusLabel; static Ihandle *filterText, *filterButton; +static Ihandle *appFilterToggle; +static Ihandle *appFilterText; +static Ihandle *appFilterBrowseButton; +static Ihandle *appFilterModeList; +static Ihandle *appFilterControls; Ihandle *filterSelectList; // timer to update icons static Ihandle *stateIcon; static Ihandle *timer; static Ihandle *timeout = NULL; +static BOOL dialogWasForeground = FALSE; void showStatus(const char *line); static int uiOnDialogShow(Ihandle *ih, int state); @@ -38,6 +45,14 @@ static int uiTimerCb(Ihandle *ih); static int uiTimeoutCb(Ihandle *ih); static int uiListSelectCb(Ihandle *ih, char *text, int item, int state); static int uiFilterTextCb(Ihandle *ih); +static int uiDialogFocusCb(Ihandle *ih); +static BOOL uiIsBlankText(const char *text); +static BOOL uiDialogIsForeground(void); +static void uiRefreshDialog(void); +static void uiSetAppFilterInputsLocked(BOOL locked); +static int uiAppFilterToggleCb(Ihandle *ih, int state); +static int uiAppFilterBrowseCb(Ihandle *ih); +static void uiBuildAppFilterConfig(AppFilterConfig *config); static void uiSetupModule(Module *module, Ihandle *parent); // serializing config files using a stupid custom format @@ -113,8 +128,8 @@ EAT_SPACE: while (isspace(*current)) { ++current; } { LOG("Failed to load from config. Fill in a simple one."); // config is missing or ill-formed. fill in some simple ones - filters[filtersSize].filterName = "loopback packets"; - filters[filtersSize].filterValue = "outbound and ip.DstAddr >= 127.0.0.1 and ip.DstAddr <= 127.255.255.255"; + filters[filtersSize].filterName = "ipv4 + ipv6 all"; + filters[filtersSize].filterValue = "ip or ipv6"; filtersSize = 1; } } @@ -122,8 +137,11 @@ EAT_SPACE: while (isspace(*current)) { ++current; } void init(int argc, char* argv[]) { UINT ix; Ihandle *topVbox, *bottomVbox, *dialogVBox, *controlHbox; + Ihandle *appTargetHbox, *appModeHbox; Ihandle *noneIcon, *doingIcon, *errorIcon; char* arg_value = NULL; + char *appArg = NULL; + char *appModeArg = NULL; // fill in config loadConfig(); @@ -148,6 +166,21 @@ void init(int argc, char* argv[]) { filterSelectList = IupList(NULL), NULL ), + appFilterToggle = IupToggle("Limit to application", NULL), + appFilterControls = IupVbox( + appTargetHbox = IupHbox( + IupLabel("Application:"), + appFilterText = IupText(NULL), + appFilterBrowseButton = IupButton("Browse...", NULL), + NULL + ), + appModeHbox = IupHbox( + IupLabel("Match:"), + appFilterModeList = IupList(NULL), + NULL + ), + NULL + ), NULL ) ); @@ -166,13 +199,51 @@ void init(int argc, char* argv[]) { IupSetAttribute(topFrame, "TITLE", "Filtering"); IupSetAttribute(topFrame, "EXPAND", "HORIZONTAL"); + IupSetAttribute(topVbox, "EXPAND", "HORIZONTAL"); IupSetAttribute(filterText, "EXPAND", "HORIZONTAL"); IupSetCallback(filterText, "VALUECHANGED_CB", (Icallback)uiFilterTextCb); IupSetAttribute(filterButton, "PADDING", "8x"); IupSetCallback(filterButton, "ACTION", uiStartCb); IupSetAttribute(topVbox, "NCMARGIN", "4x4"); IupSetAttribute(topVbox, "NCGAP", "4x2"); + IupSetAttribute(controlHbox, "EXPAND", "HORIZONTAL"); IupSetAttribute(controlHbox, "ALIGNMENT", "ACENTER"); + IupSetCallback(appFilterToggle, "ACTION", (Icallback)uiAppFilterToggleCb); + IupSetAttribute(appFilterControls, "ACTIVE", "NO"); + IupSetAttribute(appFilterControls, "EXPAND", "HORIZONTAL"); + IupSetAttribute(appFilterControls, "NCGAP", "4x2"); + IupSetAttribute(appTargetHbox, "EXPAND", "HORIZONTAL"); + IupSetAttribute(appTargetHbox, "ALIGNMENT", "ACENTER"); + IupSetAttribute(appTargetHbox, "NCGAP", "4"); + IupSetAttribute(appModeHbox, "EXPAND", "HORIZONTAL"); + IupSetAttribute(appModeHbox, "ALIGNMENT", "ACENTER"); + IupSetAttribute(appModeHbox, "NCGAP", "4"); + IupSetAttribute(appFilterText, "EXPAND", "HORIZONTAL"); + IupSetAttribute(appFilterText, "VISIBLECOLUMNS", "36"); + IupSetAttribute(appFilterBrowseButton, "PADDING", "8x"); + IupSetCallback(appFilterBrowseButton, "ACTION", uiAppFilterBrowseCb); + IupSetAttribute(appFilterModeList, "DROPDOWN", "YES"); + IupSetAttribute(appFilterModeList, "VISIBLECOLUMNS", "14"); + IupStoreAttribute(appFilterModeList, "1", "Process name"); + IupStoreAttribute(appFilterModeList, "2", "Full path"); + IupSetAttribute(appFilterModeList, "VALUE", "1"); + + if (parameterized) { + appArg = IupGetGlobal("app"); + appModeArg = IupGetGlobal("app-mode"); + if (appArg != NULL) { + IupSetAttribute(appFilterToggle, "VALUE", "ON"); + IupSetAttribute(appFilterText, "VALUE", appArg); + uiAppFilterToggleCb(appFilterToggle, 1); + } + if (appModeArg != NULL && (appModeArg[0] == 'n' || appModeArg[0] == 'N')) { + IupSetAttribute(appFilterModeList, "VALUE", "1"); + } else if (appModeArg != NULL && + (appModeArg[0] == 'p' || appModeArg[0] == 'P' || + appModeArg[0] == 'f' || appModeArg[0] == 'F')) { + IupSetAttribute(appFilterModeList, "VALUE", "2"); + } + } // setup state icon IupSetAttribute(stateIcon, "IMAGE", "none_icon"); @@ -234,6 +305,7 @@ void init(int argc, char* argv[]) { IupSetAttribute(dialog, "SIZE", "480x"); // add padding manually to width IupSetAttribute(dialog, "RESIZE", "NO"); IupSetCallback(dialog, "SHOW_CB", (Icallback)uiOnDialogShow); + IupSetCallback(dialog, "GETFOCUS_CB", (Icallback)uiDialogFocusCb); // global layout settings to affect childrens @@ -286,6 +358,48 @@ void showStatus(const char *line) { IupStoreAttribute(statusLabel, "TITLE", line); } +static BOOL uiDialogIsForeground(void) +{ + HWND hWnd; + HWND foreground; + + if (dialog == NULL) + { + return FALSE; + } + + hWnd = (HWND)IupGetAttribute(dialog, "HWND"); + foreground = GetForegroundWindow(); + return hWnd != NULL && foreground != NULL && + (foreground == hWnd || IsChild(hWnd, foreground)); +} + +static void uiRefreshDialog(void) +{ + HWND hWnd; + + if (dialog == NULL) + { + return; + } + + IupRefresh(dialog); + hWnd = (HWND)IupGetAttribute(dialog, "HWND"); + if (hWnd != NULL) + { + RedrawWindow(hWnd, NULL, NULL, + RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW); + } +} + +static int uiDialogFocusCb(Ihandle* ih) +{ + UNREFERENCED_PARAMETER(ih); + uiRefreshDialog(); + dialogWasForeground = TRUE; + return IUP_DEFAULT; +} + // in fact only 32bit binary would run on 64 bit os // if this happens pop out message box and exit static BOOL check32RunningOn64(HWND hWnd) { @@ -359,18 +473,33 @@ static int uiOnDialogShow(Ihandle *ih, int state) { static int uiStartCb(Ihandle *ih) { char buf[MSG_BUFSIZE]; + AppFilterConfig appConfig; UNREFERENCED_PARAMETER(ih); - if (divertStart(IupGetAttribute(filterText, "VALUE"), buf) == 0) { + + uiBuildAppFilterConfig(&appConfig); + if (appConfig.enabled && appConfig.targets[0] == '\0') { + showStatus("Enter an application name or path before starting the application filter."); + return IUP_DEFAULT; + } + + if (divertStart(IupGetAttribute(filterText, "VALUE"), &appConfig, buf) == 0) { showStatus(buf); return IUP_DEFAULT; } // successfully started - showStatus("Started filtering. Enable functionalities to take effect."); - IupSetAttribute(filterText, "ACTIVE", "NO"); + if (appConfig.enabled) { + showStatus("Started filtering for the selected application. Enable functionalities to take effect."); + } else { + showStatus("Started filtering. Enable functionalities to take effect."); + } + IupSetAttribute(filterText, "READONLY", "YES"); + IupSetAttribute(appFilterToggle, "ACTIVE", "NO"); + uiSetAppFilterInputsLocked(TRUE); IupSetAttribute(filterButton, "TITLE", "Stop"); IupSetCallback(filterButton, "ACTION", uiStopCb); IupSetAttribute(timer, "RUN", "YES"); + uiRefreshDialog(); return IUP_DEFAULT; } @@ -384,7 +513,10 @@ static int uiStopCb(Ihandle *ih) { IupFlush(); // flush to show disabled state divertStop(); - IupSetAttribute(filterText, "ACTIVE", "YES"); + IupSetAttribute(filterText, "READONLY", "NO"); + IupSetAttribute(appFilterToggle, "ACTIVE", "YES"); + uiSetAppFilterInputsLocked(FALSE); + uiAppFilterToggleCb(appFilterToggle, IupGetInt(appFilterToggle, "VALUE")); IupSetAttribute(filterButton, "TITLE", "Start"); IupSetAttribute(filterButton, "ACTIVE", "YES"); IupSetCallback(filterButton, "ACTION", uiStartCb); @@ -399,9 +531,92 @@ static int uiStopCb(Ihandle *ih) { IupSetAttribute(stateIcon, "IMAGE", "none_icon"); showStatus("Stopped. To begin again, edit criteria and click Start."); + uiRefreshDialog(); + return IUP_DEFAULT; +} + +static BOOL uiIsBlankText(const char* text) +{ + const unsigned char* p = (const unsigned char*)text; + if (p == NULL) + { + return TRUE; + } + while (*p != '\0') + { + if (!isspace(*p)) + { + return FALSE; + } + ++p; + } + return TRUE; +} + +static void uiSetAppFilterInputsLocked(BOOL locked) +{ + IupSetAttribute(appFilterText, "READONLY", locked ? "YES" : "NO"); + IupSetAttribute(appFilterBrowseButton, "ACTIVE", locked ? "NO" : "YES"); + IupSetAttribute(appFilterModeList, "ACTIVE", locked ? "NO" : "YES"); +} + +static int uiAppFilterToggleCb(Ihandle* ih, int state) +{ + UNREFERENCED_PARAMETER(ih); + IupSetAttribute(appFilterControls, "ACTIVE", state ? "YES" : "NO"); + uiRefreshDialog(); return IUP_DEFAULT; } +static int uiAppFilterBrowseCb(Ihandle* ih) +{ + Ihandle* fileDlg; + const char* path; + int status; + + UNREFERENCED_PARAMETER(ih); + + fileDlg = IupFileDlg(); + IupSetAttribute(fileDlg, "DIALOGTYPE", "OPEN"); + IupSetAttribute(fileDlg, "TITLE", "Select application executable"); + IupSetAttribute(fileDlg, "EXTFILTER", "Executable files|*.exe|All files|*.*|"); + IupSetAttributeHandle(fileDlg, "PARENTDIALOG", dialog); + IupPopup(fileDlg, IUP_CENTERPARENT, IUP_CENTERPARENT); + + status = IupGetInt(fileDlg, "STATUS"); + if (status != -1) + { + path = IupGetAttribute(fileDlg, "VALUE"); + if (!uiIsBlankText(path)) + { + IupStoreAttribute(appFilterText, "VALUE", path); + IupSetAttribute(appFilterModeList, "VALUE", "2"); + } + } + + IupDestroy(fileDlg); + return IUP_DEFAULT; +} + +static void uiBuildAppFilterConfig(AppFilterConfig* config) +{ + const char* targetsText; + + AppFilterDefaultConfig(config); + config->enabled = IupGetInt(appFilterToggle, "VALUE") ? TRUE : FALSE; + config->mode = IupGetInt(appFilterModeList, "VALUE") == 2 + ? APP_FILTER_MODE_FULL_PATH + : APP_FILTER_MODE_PROCESS_NAME; + config->includeChildProcesses = TRUE; + + targetsText = IupGetAttribute(appFilterText, "VALUE"); + if (!uiIsBlankText(targetsText)) + { + strncpy(config->targets, targetsText, APP_FILTER_TARGETS_BUFSIZE - 1); + config->targets[APP_FILTER_TARGETS_BUFSIZE - 1] = '\0'; + } +} + static int uiToggleControls(Ihandle *ih, int state) { Ihandle *controls = (Ihandle*)IupGetAttribute(ih, CONTROLS_HANDLE); short *target = (short*)IupGetAttribute(ih, SYNCED_VALUE); @@ -419,7 +634,16 @@ static int uiToggleControls(Ihandle *ih, int state) { static int uiTimerCb(Ihandle *ih) { int ix; + BOOL dialogIsForeground; UNREFERENCED_PARAMETER(ih); + + dialogIsForeground = uiDialogIsForeground(); + if (dialogIsForeground && !dialogWasForeground) + { + uiRefreshDialog(); + } + dialogWasForeground = dialogIsForeground; + for (ix = 0; ix < MODULE_CNT; ++ix) { if (modules[ix]->processTriggered) { IupSetAttribute(modules[ix]->iconHandle, "IMAGE", "doing_icon"); @@ -445,6 +669,18 @@ static int uiTimerCb(Ihandle *ih) { break; } + if (AppFilterIsEnabled()) + { + AppFilterStats stats; + char statusBuf[MSG_BUFSIZE]; + AppFilterGetStats(&stats); + sprintf(statusBuf, + "Application filter: target PIDs %ld, target flows %ld, UDP endpoints %ld, affected %ld, passed %ld, unknown %ld.", + stats.targetPidCount, stats.targetFlowCount, stats.targetEndpointCount, + stats.affectedPackets, stats.passedUnmatchedPackets, stats.unknownPackets); + showStatus(statusBuf); + } + return IUP_DEFAULT; } diff --git a/tools/get_genie.ps1 b/tools/get_genie.ps1 new file mode 100644 index 0000000..e36005e --- /dev/null +++ b/tools/get_genie.ps1 @@ -0,0 +1,54 @@ +<# +.SYNOPSIS + Downloads the latest GENie project generator binary for Windows. +.DESCRIPTION + Downloads genie.exe from the bx repository (maintained by the same author as GENie). + This binary supports Visual Studio 2022 (vs2022), 2019, 2017, 2015, and MinGW (gmake). +.PARAMETER OutDir + Directory to save genie.exe. Defaults to the script's parent directory (tools/). +.PARAMETER Force + Overwrite genie.exe if it already exists. +.EXAMPLE + .\get_genie.ps1 + Downloads genie.exe to the tools/ directory. +.EXAMPLE + .\get_genie.ps1 -Force + Re-downloads even if genie.exe already exists. +#> +param( + [string]$OutDir = $PSScriptRoot, + [switch]$Force +) + +$genieUrl = "https://github.com/bkaradzic/bx/raw/master/tools/bin/windows/genie.exe" +$outFile = Join-Path $OutDir "genie.exe" + +if (Test-Path $outFile -PathType Leaf) { + if (-not $Force) { + Write-Host "genie.exe already exists at $outFile" -ForegroundColor Green + Write-Host "Use -Force to re-download." -ForegroundColor Yellow + exit 0 + } + Write-Host "Overwriting existing genie.exe..." -ForegroundColor Yellow +} + +Write-Host "Downloading GENie from $genieUrl ..." -ForegroundColor Cyan + +try { + # Try to use BITS if available (more reliable), fall back to Invoke-WebRequest + if (Get-Command Start-BitsTransfer -ErrorAction SilentlyContinue) { + Start-BitsTransfer -Source $genieUrl -Destination $outFile + } else { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri $genieUrl -OutFile $outFile -UseBasicParsing + } + Write-Host "genie.exe downloaded successfully to $outFile" -ForegroundColor Green +} catch { + Write-Host "ERROR: Failed to download genie.exe: $_" -ForegroundColor Red + Write-Host "" + Write-Host "Alternative options:" -ForegroundColor Yellow + Write-Host "1. Build GENie from source: https://github.com/bkaradzic/GENie" + Write-Host "2. Download manually from: $genieUrl" + Write-Host " and place it at: $outFile" + exit 1 +}