feat: первая версия Just a Blocker
WPF приложение для блокировки входящих/исходящих соединений через Windows Filtering Platform. Работает в фоне (трей), автозапуск через реестр. Фильтры персистентные — остаются активными даже после выхода из приложения.
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
## .NET
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
*.suo
|
||||
*.sln.docstates
|
||||
.vs/
|
||||
|
||||
## Build
|
||||
packages/
|
||||
*.nupkg
|
||||
|
||||
## IDE
|
||||
.idea/
|
||||
*.swp
|
||||
*~
|
||||
|
||||
## OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JustABlocker", "src\JustABlocker\JustABlocker.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,37 @@
|
||||
# Just a Blocker
|
||||
|
||||
Блокирует входящие/исходящие соединения на указанные домены и IP-адреса.
|
||||
|
||||
Работает через **Windows Filtering Platform (WFP)** — фильтрует трафик на уровне ядра, блокирует любые протоколы (TCP, UDP, ICMP и т.д.).
|
||||
|
||||
## Как это работает
|
||||
|
||||
1. Добавляешь домен или IP в список
|
||||
2. Приложение резолвит домен в IP-адреса (IPv4 + IPv6)
|
||||
3. Создаёт WFP-фильтры на всех сетевых интерфейсах
|
||||
4. Все пакеты к этим IP дропаются на уровне ядра
|
||||
|
||||
Фильтры **персистентные** — переживают перезагрузку и продолжают работать, даже если приложение не запущено.
|
||||
|
||||
## Использование
|
||||
|
||||
- Добавить в список: `example.com` или `1.2.3.4`
|
||||
- Удалить из списка — блокировка прекращается
|
||||
- Pause — временно отключает все фильтры
|
||||
- Приложение сворачивается в трей
|
||||
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
dotnet publish src/JustABlocker/JustABlocker.csproj \
|
||||
-c Release -r win-x64 --self-contained true \
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true
|
||||
|
||||
# EXE: src/JustABlocker/bin/Release/net8.0-windows/win-x64/publish/JustABlocker.exe
|
||||
```
|
||||
|
||||
## Системные требования
|
||||
|
||||
- Windows 7+
|
||||
- .NET 8 (self-contained — не нужен)
|
||||
- Права администратора (WFP требует Admin)
|
||||
@@ -0,0 +1,5 @@
|
||||
<Application x:Class="JustABlocker.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
ShutdownMode="OnExplicitShutdown">
|
||||
</Application>
|
||||
@@ -0,0 +1,176 @@
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
using JustABlocker.Services;
|
||||
|
||||
namespace JustABlocker;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private Mutex? _mutex;
|
||||
private WfpFilterService? _wfpService;
|
||||
private BlacklistManager? _blacklistManager;
|
||||
private NotifyIcon? _trayIcon;
|
||||
private MainWindow? _mainWindow;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
_mutex = new Mutex(true, "JustABlocker-Mutex", out var createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
Current.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
_blacklistManager = new BlacklistManager();
|
||||
_blacklistManager.Load();
|
||||
|
||||
try
|
||||
{
|
||||
_wfpService = new WfpFilterService();
|
||||
_wfpService.Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"Failed to initialize WFP: {ex.Message}\n\nRun as Administrator.",
|
||||
"Just a Blocker",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning);
|
||||
}
|
||||
|
||||
SetupTray();
|
||||
SyncFilters();
|
||||
|
||||
if (e.Args.Contains("--minimized") || e.Args.Contains("-m"))
|
||||
{
|
||||
_mainWindow = CreateMainWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowMainWindow();
|
||||
}
|
||||
|
||||
if (_wfpService != null)
|
||||
{
|
||||
var appPath = AutoStartup.GetExecutablePath();
|
||||
if (!string.IsNullOrEmpty(appPath) && !AutoStartup.IsEnabled())
|
||||
{
|
||||
AutoStartup.Enable(appPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTray()
|
||||
{
|
||||
_trayIcon = new NotifyIcon
|
||||
{
|
||||
Icon = CreateTrayIcon(),
|
||||
Text = "Just a Blocker",
|
||||
Visible = true,
|
||||
};
|
||||
|
||||
var menu = new ContextMenuStrip();
|
||||
menu.Items.Add("Show / Hide", null, (_, _) => ToggleWindow());
|
||||
menu.Items.Add(new ToolStripSeparator());
|
||||
menu.Items.Add("Pause", null, (_, _) => TogglePause());
|
||||
menu.Items.Add(new ToolStripSeparator());
|
||||
menu.Items.Add("Exit", null, (_, _) => ExitApp());
|
||||
|
||||
_trayIcon.ContextMenuStrip = menu;
|
||||
_trayIcon.DoubleClick += (_, _) => ToggleWindow();
|
||||
}
|
||||
|
||||
private static Icon CreateTrayIcon()
|
||||
{
|
||||
using var bitmap = new Bitmap(16, 16);
|
||||
using var g = Graphics.FromImage(bitmap);
|
||||
g.Clear(Color.Black);
|
||||
using var brush = new SolidBrush(Color.Red);
|
||||
g.FillRectangle(brush, 3, 3, 10, 10);
|
||||
using var whiteBrush = new SolidBrush(Color.White);
|
||||
var font = new Font("Segoe UI", 8, FontStyle.Bold);
|
||||
g.DrawString("!", font, whiteBrush, 4, 2);
|
||||
var hicon = bitmap.GetHicon();
|
||||
return Icon.FromHandle(hicon);
|
||||
}
|
||||
|
||||
private void ShowMainWindow()
|
||||
{
|
||||
if (_mainWindow == null)
|
||||
{
|
||||
_mainWindow = CreateMainWindow();
|
||||
}
|
||||
_mainWindow.Show();
|
||||
_mainWindow.Activate();
|
||||
}
|
||||
|
||||
private MainWindow CreateMainWindow()
|
||||
{
|
||||
var w = new MainWindow(_blacklistManager!, _wfpService!);
|
||||
w.Closing += (_, args) =>
|
||||
{
|
||||
args.Cancel = true;
|
||||
w.Hide();
|
||||
};
|
||||
return w;
|
||||
}
|
||||
|
||||
private void ToggleWindow()
|
||||
{
|
||||
if (_mainWindow == null || !_mainWindow.IsVisible)
|
||||
{
|
||||
ShowMainWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
_mainWindow.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void TogglePause()
|
||||
{
|
||||
if (_mainWindow != null && _mainWindow.IsVisible)
|
||||
{
|
||||
_mainWindow.TogglePause();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_wfpService != null)
|
||||
{
|
||||
_wfpService.RemoveAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncFilters()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_blacklistManager == null || _wfpService == null) return;
|
||||
var resolver = new DnsResolver();
|
||||
var enabled = _blacklistManager.GetEnabledEntries();
|
||||
var ips = resolver.ResolveAll(enabled);
|
||||
_wfpService.SetBlockedIps(ips);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.TraceWarning($"SyncFilters failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitApp()
|
||||
{
|
||||
_trayIcon?.Visible = false;
|
||||
_trayIcon?.Dispose();
|
||||
Current.Shutdown();
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
_trayIcon?.Dispose();
|
||||
_mutex?.Dispose();
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>JustABlocker</AssemblyName>
|
||||
<RootNamespace>JustABlocker</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,79 @@
|
||||
<Window x:Class="JustABlocker.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Just a Blocker"
|
||||
Width="560" Height="420"
|
||||
MinWidth="400" MinHeight="300"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<Window.Resources>
|
||||
<Style TargetType="Button" x:Key="ActionButton">
|
||||
<Setter Property="Padding" Value="8,4"/>
|
||||
<Setter Property="Margin" Value="2"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<DockPanel Margin="8">
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<TextBox x:Name="InputBox"
|
||||
Width="300"
|
||||
ToolTip="Domain or IP address"
|
||||
KeyDown="InputBox_OnKeyDown"/>
|
||||
<Button x:Name="AddButton"
|
||||
Style="{StaticResource ActionButton}"
|
||||
Click="AddButton_OnClick">+ Add</Button>
|
||||
<Separator Width="8" Opacity="0"/>
|
||||
<ToggleButton x:Name="PauseToggle"
|
||||
Style="{StaticResource ActionButton}"
|
||||
Checked="PauseToggle_OnChecked"
|
||||
Unchecked="PauseToggle_OnUnchecked"
|
||||
IsChecked="False">⏸ Pause</ToggleButton>
|
||||
<Separator Width="8" Opacity="0"/>
|
||||
<Button x:Name="ClearButton"
|
||||
Style="{StaticResource ActionButton}"
|
||||
Click="ClearButton_OnClick">✕ Clear</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Blacklist counter -->
|
||||
<TextBlock x:Name="StatusText"
|
||||
DockPanel.Dock="Bottom"
|
||||
Margin="0,4,0,0"
|
||||
FontSize="11"
|
||||
Foreground="Gray"/>
|
||||
|
||||
<DataGrid x:Name="EntriesGrid"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Single"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HeadersVisibility="Column"
|
||||
MouseDoubleClick="EntriesGrid_OnMouseDoubleClick">
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridCheckBoxColumn Header="On"
|
||||
Binding="{Binding IsEnabled}"
|
||||
Width="40"/>
|
||||
<DataGridTextColumn Header="Target"
|
||||
Binding="{Binding Target}"
|
||||
Width="*"/>
|
||||
<DataGridTextColumn Header="Added"
|
||||
Binding="{Binding AddedAt, StringFormat={}{0:yyyy-MM-dd HH:mm}}"
|
||||
Width="130"/>
|
||||
<DataGridTemplateColumn Width="50">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button Content="✕"
|
||||
FontSize="12"
|
||||
Padding="4,1"
|
||||
Cursor="Hand"
|
||||
Click="RemoveButton_OnClick"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using JustABlocker.Models;
|
||||
using JustABlocker.Services;
|
||||
|
||||
namespace JustABlocker;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly BlacklistManager _blacklistManager;
|
||||
private readonly WfpFilterService _wfpService;
|
||||
private readonly DnsResolver _resolver = new();
|
||||
private readonly System.Timers.Timer _refreshTimer;
|
||||
private bool _isPaused;
|
||||
|
||||
public MainWindow(BlacklistManager blacklistManager, WfpFilterService wfpService)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_blacklistManager = blacklistManager;
|
||||
_wfpService = wfpService;
|
||||
|
||||
EntriesGrid.ItemsSource = _blacklistManager.Entries;
|
||||
UpdateStatus();
|
||||
|
||||
_blacklistManager.EntriesChanged += OnEntriesChanged;
|
||||
|
||||
_refreshTimer = new System.Timers.Timer(TimeSpan.FromMinutes(5))
|
||||
{
|
||||
AutoReset = true,
|
||||
Enabled = true,
|
||||
};
|
||||
_refreshTimer.Elapsed += (_, _) =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (!_isPaused)
|
||||
SyncFilters();
|
||||
});
|
||||
};
|
||||
|
||||
Closed += (_, _) => _refreshTimer.Dispose();
|
||||
}
|
||||
|
||||
private void OnEntriesChanged()
|
||||
{
|
||||
UpdateStatus();
|
||||
if (!_isPaused)
|
||||
SyncFilters();
|
||||
}
|
||||
|
||||
private void SyncFilters()
|
||||
{
|
||||
var enabled = _blacklistManager.GetEnabledEntries();
|
||||
var ips = _resolver.ResolveAll(enabled);
|
||||
_wfpService.SetBlockedIps(ips);
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
var total = _blacklistManager.Entries.Count;
|
||||
var enabled = _blacklistManager.Entries.Count(e => e.IsEnabled);
|
||||
var status = _isPaused
|
||||
? $"⏸ Paused — {total} entries ({enabled} enabled)"
|
||||
: $"● Active — {total} entries ({enabled} enabled)";
|
||||
StatusText.Text = status;
|
||||
}
|
||||
|
||||
private void AddEntry(string target)
|
||||
{
|
||||
target = target.Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(target)) return;
|
||||
|
||||
if (_blacklistManager.Entries.Any(e =>
|
||||
e.Target.Equals(target, StringComparison.OrdinalIgnoreCase)))
|
||||
return;
|
||||
|
||||
var entry = new BlacklistEntry
|
||||
{
|
||||
Target = target,
|
||||
IsEnabled = true,
|
||||
AddedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
_blacklistManager.Add(entry);
|
||||
}
|
||||
|
||||
private void AddButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AddEntry(InputBox.Text);
|
||||
InputBox.Clear();
|
||||
InputBox.Focus();
|
||||
}
|
||||
|
||||
private void InputBox_OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
AddEntry(InputBox.Text);
|
||||
InputBox.Clear();
|
||||
InputBox.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button { DataContext: BlacklistEntry entry })
|
||||
{
|
||||
_blacklistManager.Remove(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void EntriesGrid_OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (EntriesGrid.SelectedItem is BlacklistEntry entry)
|
||||
{
|
||||
_blacklistManager.Toggle(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (MessageBox.Show(this,
|
||||
"Remove all entries and unblock everything?",
|
||||
"Clear All", MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question) != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
foreach (var entry in _blacklistManager.GetAllEntries().ToList())
|
||||
{
|
||||
_blacklistManager.Remove(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void PauseToggle_OnChecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_isPaused = true;
|
||||
_wfpService.RemoveAll();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private void PauseToggle_OnUnchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_isPaused = false;
|
||||
SyncFilters();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
public void TogglePause()
|
||||
{
|
||||
_isPaused = !_isPaused;
|
||||
PauseToggle.IsChecked = _isPaused;
|
||||
|
||||
if (_isPaused)
|
||||
_wfpService.RemoveAll();
|
||||
else
|
||||
SyncFilters();
|
||||
|
||||
UpdateStatus();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace JustABlocker.Models;
|
||||
|
||||
public class BlacklistEntry
|
||||
{
|
||||
[JsonPropertyName("target")]
|
||||
public string Target { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("addedAt")]
|
||||
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace JustABlocker.Native;
|
||||
|
||||
public static class WfpConstants
|
||||
{
|
||||
public const uint FWP_ACTION_BLOCK = 0x00000001;
|
||||
public const uint FWP_ACTION_PERMIT = 0x00000002;
|
||||
public const uint FWP_EMPTY = 0x00000000;
|
||||
public const uint FWP_UINT8 = 0x00000003;
|
||||
public const uint FWP_UINT32 = 0x00000006;
|
||||
public const uint FWP_BYTE_ARRAY16 = 0x0000000D;
|
||||
public const uint FWP_V4_ADDR_MASK = 0x00000012;
|
||||
public const uint FWP_MATCH_EQUAL = 0x00000001;
|
||||
public const uint FWPM_FILTER_FLAG_PERSISTENT = 0x00000001;
|
||||
public const uint RPC_C_AUTHN_WINNT = 10;
|
||||
|
||||
public static readonly Guid FWPM_LAYER_ALE_AUTH_CONNECT_V4 = new("0ACD93F8-7C6C-4349-A5B4-5360B41D6DBD");
|
||||
public static readonly Guid FWPM_LAYER_ALE_AUTH_CONNECT_V6 = new("49994238-ACA6-4A4E-9A78-9C0A7E39FDA2");
|
||||
public static readonly Guid FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 = new("E1CDD904-C728-47A4-9F2B-6BE2F47B1B20");
|
||||
public static readonly Guid FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 = new("B0E48247-90B6-4C46-B2B5-6F6B4A1E469E");
|
||||
|
||||
public static readonly Guid FWPM_CONDITION_IP_REMOTE_ADDRESS = new("D4153353-2C4D-4CA9-9D8A-8D8D0B5D8E8F");
|
||||
|
||||
public static readonly Guid SUBLAYER_GUID = new("8A4F2E1C-3D5B-4C6E-9F7A-1B2C3D4E5F6A");
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FWPM_DISPLAY_DATA0
|
||||
{
|
||||
public IntPtr name;
|
||||
public IntPtr description;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FWPM_SUBLAYER0
|
||||
{
|
||||
public Guid subLayerKey;
|
||||
public IntPtr displayData;
|
||||
public uint flags;
|
||||
|
||||
public IntPtr providerKey;
|
||||
|
||||
public Guid providerContextKey;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct FWP_VALUE0
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public uint type;
|
||||
|
||||
[FieldOffset(8)]
|
||||
public byte uint8;
|
||||
|
||||
[FieldOffset(8)]
|
||||
public ushort uint16;
|
||||
|
||||
[FieldOffset(8)]
|
||||
public uint uint32;
|
||||
|
||||
[FieldOffset(8)]
|
||||
public IntPtr pointer;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FWP_V4_ADDR_MASK
|
||||
{
|
||||
public uint addr;
|
||||
public uint mask;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FWP_BYTE_ARRAY16
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
|
||||
public byte[] byteArray16;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FWPM_FILTER_CONDITION0
|
||||
{
|
||||
public Guid fieldKey;
|
||||
public uint matchType;
|
||||
public FWP_VALUE0 conditionValue;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FWPM_ACTION0
|
||||
{
|
||||
public uint type;
|
||||
public Guid filterType;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FWPM_FILTER0
|
||||
{
|
||||
public Guid filterKey;
|
||||
public IntPtr displayData;
|
||||
public uint flags;
|
||||
public IntPtr providerKey;
|
||||
public IntPtr providerData;
|
||||
public Guid layerKey;
|
||||
public Guid subLayerKey;
|
||||
public FWP_VALUE0 weight;
|
||||
public IntPtr filterCondition;
|
||||
public uint numFilterConditions;
|
||||
public FWPM_ACTION0 action;
|
||||
public IntPtr rawContext;
|
||||
public ulong reserved1;
|
||||
public Guid reserved2;
|
||||
public Guid providerContextKey;
|
||||
}
|
||||
|
||||
public static class WfpNative
|
||||
{
|
||||
private const string DllName = "fwpmu.dll";
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||
public static extern uint FwpmEngineOpen0(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? serverName,
|
||||
uint authnService,
|
||||
IntPtr authIdentity,
|
||||
IntPtr session,
|
||||
out IntPtr engineHandle);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||
public static extern uint FwpmEngineClose0(IntPtr engineHandle);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||
public static extern uint FwpmSublayerAdd0(
|
||||
IntPtr engineHandle,
|
||||
ref FWPM_SUBLAYER0 subLayer,
|
||||
IntPtr sd);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||
public static extern uint FwpmSublayerDeleteByKey0(
|
||||
IntPtr engineHandle,
|
||||
ref Guid subLayerKey);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||
public static extern uint FwpmFilterAdd0(
|
||||
IntPtr engineHandle,
|
||||
ref FWPM_FILTER0 filter,
|
||||
IntPtr sd,
|
||||
out ulong id);
|
||||
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||
public static extern uint FwpmFilterDeleteByKey0(
|
||||
IntPtr engineHandle,
|
||||
ref Guid key);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern IntPtr LocalFree(IntPtr hMem);
|
||||
|
||||
public static Guid DeriveFilterKey(string ip, Guid layerGuid)
|
||||
{
|
||||
var ipBytes = Encoding.UTF8.GetBytes(ip.ToLowerInvariant());
|
||||
var layerBytes = layerGuid.ToByteArray();
|
||||
var combined = new byte[ipBytes.Length + layerBytes.Length];
|
||||
Buffer.BlockCopy(ipBytes, 0, combined, 0, ipBytes.Length);
|
||||
Buffer.BlockCopy(layerBytes, 0, combined, ipBytes.Length, layerBytes.Length);
|
||||
using var md5 = System.Security.Cryptography.MD5.Create();
|
||||
var hash = md5.ComputeHash(combined);
|
||||
return new Guid(hash);
|
||||
}
|
||||
|
||||
public static uint IpToNetworkOrder(string ipStr)
|
||||
{
|
||||
var ip = IPAddress.Parse(ipStr);
|
||||
var bytes = ip.GetAddressBytes();
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(bytes);
|
||||
return BitConverter.ToUInt32(bytes, 0);
|
||||
}
|
||||
|
||||
public static byte[] IpToBytes16(string ipStr)
|
||||
{
|
||||
var ip = IPAddress.Parse(ipStr);
|
||||
var bytes = ip.GetAddressBytes();
|
||||
if (bytes.Length != 16)
|
||||
return new byte[16];
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static IntPtr AllocString(string s)
|
||||
{
|
||||
return Marshal.StringToCoTaskMemUni(s);
|
||||
}
|
||||
|
||||
public static void FreeString(IntPtr ptr)
|
||||
{
|
||||
if (ptr != IntPtr.Zero)
|
||||
Marshal.FreeCoTaskMem(ptr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace JustABlocker.Services;
|
||||
|
||||
public static class AutoStartup
|
||||
{
|
||||
private const string KeyName = "JustABlocker";
|
||||
|
||||
public static void Enable(string executablePath)
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Run", true);
|
||||
key?.SetValue(KeyName, executablePath);
|
||||
}
|
||||
|
||||
public static void Disable()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Run", true);
|
||||
key?.DeleteValue(KeyName, false);
|
||||
}
|
||||
|
||||
public static bool IsEnabled()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Run", false);
|
||||
return key?.GetValue(KeyName) != null;
|
||||
}
|
||||
|
||||
public static string GetExecutablePath()
|
||||
{
|
||||
var path = Environment.ProcessPath;
|
||||
if (path != null && path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||||
path = path[..^4] + ".exe";
|
||||
return path ?? "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json;
|
||||
using JustABlocker.Models;
|
||||
|
||||
namespace JustABlocker.Services;
|
||||
|
||||
public class BlacklistManager
|
||||
{
|
||||
private readonly string _filePath;
|
||||
private readonly ObservableCollection<BlacklistEntry> _entries = [];
|
||||
|
||||
public ObservableCollection<BlacklistEntry> Entries => _entries;
|
||||
|
||||
public event Action? EntriesChanged;
|
||||
|
||||
public BlacklistManager()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var dir = Path.Combine(appData, "JustABlocker");
|
||||
Directory.CreateDirectory(dir);
|
||||
_filePath = Path.Combine(dir, "blacklist.json");
|
||||
}
|
||||
|
||||
public void Load()
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_filePath);
|
||||
var list = JsonSerializer.Deserialize<List<BlacklistEntry>>(json);
|
||||
if (list != null)
|
||||
{
|
||||
_entries.Clear();
|
||||
foreach (var entry in list)
|
||||
_entries.Add(entry);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore corrupted file
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var list = _entries.ToList();
|
||||
var json = JsonSerializer.Serialize(list, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(_filePath, json);
|
||||
}
|
||||
|
||||
public void Add(BlacklistEntry entry)
|
||||
{
|
||||
_entries.Add(entry);
|
||||
Save();
|
||||
EntriesChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Remove(BlacklistEntry entry)
|
||||
{
|
||||
_entries.Remove(entry);
|
||||
Save();
|
||||
EntriesChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Toggle(BlacklistEntry entry)
|
||||
{
|
||||
entry.IsEnabled = !entry.IsEnabled;
|
||||
Save();
|
||||
EntriesChanged?.Invoke();
|
||||
}
|
||||
|
||||
public List<BlacklistEntry> GetEnabledEntries()
|
||||
{
|
||||
return _entries.Where(e => e.IsEnabled).ToList();
|
||||
}
|
||||
|
||||
public List<BlacklistEntry> GetAllEntries()
|
||||
{
|
||||
return [.. _entries];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Net;
|
||||
using JustABlocker.Models;
|
||||
|
||||
namespace JustABlocker.Services;
|
||||
|
||||
public class DnsResolver
|
||||
{
|
||||
public List<(IPAddress ip, bool isV6)> ResolveAll(IEnumerable<BlacklistEntry> entries)
|
||||
{
|
||||
var results = new List<(IPAddress, bool)>();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var target = entry.Target.Trim();
|
||||
|
||||
if (IPAddress.TryParse(target, out var ip))
|
||||
{
|
||||
results.Add((ip, ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6));
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var addresses = Dns.GetHostAddresses(target);
|
||||
foreach (var addr in addresses)
|
||||
{
|
||||
results.Add((addr, addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// domain resolution failed, skip
|
||||
}
|
||||
}
|
||||
|
||||
return results.Distinct().ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using JustABlocker.Native;
|
||||
|
||||
namespace JustABlocker.Services;
|
||||
|
||||
public class WfpFilterService : IDisposable
|
||||
{
|
||||
private IntPtr _engineHandle = IntPtr.Zero;
|
||||
private readonly HashSet<string> _blockedIps = [];
|
||||
private bool _initialized;
|
||||
|
||||
private static readonly Guid[] OutboundLayers =
|
||||
[
|
||||
WfpConstants.FWPM_LAYER_ALE_AUTH_CONNECT_V4,
|
||||
WfpConstants.FWPM_LAYER_ALE_AUTH_CONNECT_V6,
|
||||
];
|
||||
|
||||
private static readonly Guid[] InboundLayers =
|
||||
[
|
||||
WfpConstants.FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4,
|
||||
WfpConstants.FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6,
|
||||
];
|
||||
|
||||
private static readonly Guid[] AllLayers = [.. OutboundLayers, .. InboundLayers];
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
var result = WfpNative.FwpmEngineOpen0(
|
||||
null, WfpConstants.RPC_C_AUTHN_WINNT,
|
||||
IntPtr.Zero, IntPtr.Zero, out _engineHandle);
|
||||
|
||||
if (result != 0)
|
||||
throw new InvalidOperationException(
|
||||
$"FwpmEngineOpen0 failed: 0x{result:X8}. Run as Administrator.");
|
||||
|
||||
CreateSublayer();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private void CreateSublayer()
|
||||
{
|
||||
var namePtr = WfpNative.AllocString("Just a Blocker");
|
||||
var descPtr = WfpNative.AllocString("Filters for Just a Blocker");
|
||||
|
||||
var displayData = new FWPM_DISPLAY_DATA0
|
||||
{
|
||||
name = namePtr,
|
||||
description = descPtr,
|
||||
};
|
||||
|
||||
var sublayer = new FWPM_SUBLAYER0
|
||||
{
|
||||
subLayerKey = WfpConstants.SUBLAYER_GUID,
|
||||
displayData = Marshal.AllocHGlobal(Marshal.SizeOf<FWPM_DISPLAY_DATA0>()),
|
||||
flags = 0,
|
||||
providerKey = IntPtr.Zero,
|
||||
providerContextKey = Guid.Empty,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(displayData, sublayer.displayData, false);
|
||||
var result = WfpNative.FwpmSublayerAdd0(_engineHandle, ref sublayer, IntPtr.Zero);
|
||||
if (result != 0 && result != 0x80320004) // FWP_E_ALREADY_EXISTS
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"FwpmSublayerAdd0 failed: 0x{result:X8}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
WfpNative.FreeString(namePtr);
|
||||
WfpNative.FreeString(descPtr);
|
||||
if (sublayer.displayData != IntPtr.Zero)
|
||||
Marshal.FreeHGlobal(sublayer.displayData);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBlockedIps(List<(IPAddress ip, bool isV6)> ips)
|
||||
{
|
||||
if (!_initialized) return;
|
||||
|
||||
var newIpSet = new HashSet<string>(ips.Select(i => i.ip.ToString()));
|
||||
|
||||
var toRemove = _blockedIps.Except(newIpSet).ToList();
|
||||
var toAdd = newIpSet.Except(_blockedIps).ToList();
|
||||
|
||||
foreach (var ip in toRemove)
|
||||
{
|
||||
RemoveFiltersForIp(ip);
|
||||
_blockedIps.Remove(ip);
|
||||
}
|
||||
|
||||
foreach (var ipStr in toAdd)
|
||||
{
|
||||
var entry = ips.First(i => i.ip.ToString() == ipStr);
|
||||
AddFiltersForIp(entry.ip, entry.isV6);
|
||||
_blockedIps.Add(ipStr);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAll()
|
||||
{
|
||||
if (!_initialized) return;
|
||||
|
||||
foreach (var ip in _blockedIps.ToList())
|
||||
{
|
||||
RemoveFiltersForIp(ip);
|
||||
_blockedIps.Remove(ip);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFiltersForIp(IPAddress ip, bool isV6)
|
||||
{
|
||||
var layers = isV6
|
||||
? new[] { WfpConstants.FWPM_LAYER_ALE_AUTH_CONNECT_V6, WfpConstants.FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6 }
|
||||
: new[] { WfpConstants.FWPM_LAYER_ALE_AUTH_CONNECT_V4, WfpConstants.FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 };
|
||||
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
AddSingleFilter(ip, isV6, layer);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSingleFilter(IPAddress ip, bool isV6, Guid layerKey)
|
||||
{
|
||||
var ipStr = ip.ToString();
|
||||
var filterKey = WfpNative.DeriveFilterKey(ipStr, layerKey);
|
||||
|
||||
var value = CreateIpValue(ip, isV6);
|
||||
|
||||
var condition = new FWPM_FILTER_CONDITION0
|
||||
{
|
||||
fieldKey = WfpConstants.FWPM_CONDITION_IP_REMOTE_ADDRESS,
|
||||
matchType = WfpConstants.FWP_MATCH_EQUAL,
|
||||
conditionValue = value,
|
||||
};
|
||||
|
||||
var conditionPtr = Marshal.AllocHGlobal(Marshal.SizeOf<FWPM_FILTER_CONDITION0>());
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(condition, conditionPtr, false);
|
||||
|
||||
var filter = new FWPM_FILTER0
|
||||
{
|
||||
filterKey = filterKey,
|
||||
displayData = IntPtr.Zero,
|
||||
flags = WfpConstants.FWPM_FILTER_FLAG_PERSISTENT,
|
||||
providerKey = IntPtr.Zero,
|
||||
providerData = IntPtr.Zero,
|
||||
layerKey = layerKey,
|
||||
subLayerKey = WfpConstants.SUBLAYER_GUID,
|
||||
weight = new FWP_VALUE0 { type = WfpConstants.FWP_EMPTY },
|
||||
filterCondition = conditionPtr,
|
||||
numFilterConditions = 1,
|
||||
action = new FWPM_ACTION0
|
||||
{
|
||||
type = WfpConstants.FWP_ACTION_BLOCK,
|
||||
filterType = Guid.Empty,
|
||||
},
|
||||
rawContext = IntPtr.Zero,
|
||||
reserved1 = 0,
|
||||
reserved2 = Guid.Empty,
|
||||
providerContextKey = Guid.Empty,
|
||||
};
|
||||
|
||||
var result = WfpNative.FwpmFilterAdd0(_engineHandle, ref filter, IntPtr.Zero, out _);
|
||||
if (result != 0 && result != 0x80320004)
|
||||
{
|
||||
Trace.TraceWarning($"FwpmFilterAdd0 failed for {ipStr}: 0x{result:X8}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(conditionPtr);
|
||||
if (value.pointer != IntPtr.Zero)
|
||||
Marshal.FreeHGlobal(value.pointer);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFiltersForIp(string ipStr)
|
||||
{
|
||||
foreach (var layer in AllLayers)
|
||||
{
|
||||
var filterKey = WfpNative.DeriveFilterKey(ipStr, layer);
|
||||
var result = WfpNative.FwpmFilterDeleteByKey0(_engineHandle, ref filterKey);
|
||||
if (result != 0 && result != 0x80320002) // FWP_E_NOT_FOUND
|
||||
{
|
||||
Trace.TraceWarning($"FwpmFilterDeleteByKey0 failed for {ipStr}: 0x{result:X8}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static FWP_VALUE0 CreateIpValue(IPAddress ip, bool isV6)
|
||||
{
|
||||
if (isV6)
|
||||
{
|
||||
var bytes = ip.GetAddressBytes();
|
||||
var byteArray = new FWP_BYTE_ARRAY16
|
||||
{
|
||||
byteArray16 = bytes,
|
||||
};
|
||||
var ptr = Marshal.AllocHGlobal(Marshal.SizeOf<FWP_BYTE_ARRAY16>());
|
||||
Marshal.StructureToPtr(byteArray, ptr, false);
|
||||
return new FWP_VALUE0
|
||||
{
|
||||
type = WfpConstants.FWP_BYTE_ARRAY16,
|
||||
pointer = ptr,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var addrBytes = ip.GetAddressBytes();
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(addrBytes);
|
||||
var addrUint = BitConverter.ToUInt32(addrBytes, 0);
|
||||
|
||||
var addrMask = new FWP_V4_ADDR_MASK
|
||||
{
|
||||
addr = addrUint,
|
||||
mask = 0xFFFFFFFF,
|
||||
};
|
||||
var ptr = Marshal.AllocHGlobal(Marshal.SizeOf<FWP_V4_ADDR_MASK>());
|
||||
Marshal.StructureToPtr(addrMask, ptr, false);
|
||||
return new FWP_VALUE0
|
||||
{
|
||||
type = WfpConstants.FWP_V4_ADDR_MASK,
|
||||
pointer = ptr,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_engineHandle != IntPtr.Zero)
|
||||
{
|
||||
WfpNative.FwpmEngineClose0(_engineHandle);
|
||||
_engineHandle = IntPtr.Zero;
|
||||
}
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="JustABlocker"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user