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