feat: первая версия Just a Blocker

WPF приложение для блокировки входящих/исходящих соединений
через Windows Filtering Platform. Работает в фоне (трей),
автозапуск через реестр. Фильтры персистентные — остаются
активными даже после выхода из приложения.
This commit is contained in:
SashegDev
2026-06-17 18:39:59 +00:00
commit bada4c5a25
15 changed files with 1138 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
## .NET
bin/
obj/
*.user
*.suo
*.sln.docstates
.vs/
## Build
packages/
*.nupkg
## IDE
.idea/
*.swp
*~
## OS
.DS_Store
Thumbs.db
+19
View File
@@ -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
+37
View File
@@ -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)
+5
View File
@@ -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>
+176
View File
@@ -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);
}
}
+12
View File
@@ -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>
+79
View File
@@ -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>
+162
View File
@@ -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();
}
}
+15
View File
@@ -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;
}
+199
View File
@@ -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);
}
}
+37
View File
@@ -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];
}
}
+38
View File
@@ -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;
}
}
+11
View File
@@ -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>