Add post: Replacing RDPGuard With IPBan (EN + ZH, og+banner)
Deploy / build (push) Successful in 22s

Case study of migrating from paid RDPGuard 7.8.7 to open-source IPBan
4.1.0, covering the three undocumented traps: the uninstaller that would
have silently unbanned 12 active attackers, the non-existent
--install-service flag in v4.1.0, and ExpireTime vs BanTime.

Also fixes an og-gen defect: the tag chip hardcoded KIND='DevOps', so
every non-devops post carried a wrong label (e.g. "case-studies · DevOps"
on the STT card). KIND now derives from the post category, and all 29
existing OG images are regenerated with correct badges.
This commit is contained in:
2026-09-19 19:50:27 +08:00
parent 8cebe20981
commit daca86dc43
35 changed files with 528 additions and 3 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 40 KiB

+20
View File
@@ -565,6 +565,26 @@ BANNERS['self-hosted-speech-to-text-api'] = {
],
};
BANNERS['replacing-rdpguard-with-ipban'] = {
titlebar: 'root@win11 — ipban service',
lines: [
{ t: 'cmd', text: 'sc.exe qc IPBAN' },
{ t: 'ok', text: 'START_TYPE : 2 AUTO_START ✓' },
{ t: 'dim', text: 'RdpGuard 7.8.7 — paid, closed, 3 versions behind' },
{ t: 'err', text: 'uninstall → rule rdpguard-… deleted → 12 bans LOST' },
{ t: 'cmd', text: 'ban.txt → IPBan import (12 ipv4)' },
{ t: 'ok', text: 'Updating firewall with 12 entries...' },
{ t: 'err', text: "ipban --install-service → 'Unrecognized command'" },
{ t: 'hl', text: '16 attackers blocked · zero protection gap' },
],
flow: [
{ n: '1', label: 'export bans' },
{ n: '2', label: 'sc.exe install' },
{ n: '3', label: 'whitelist LAN' },
{ n: '4', label: '16 blocked ✓' },
],
};
// ---------- read frontmatter ----------
const postPath = join(ROOT, 'src', 'content', 'posts', `${slug}.md`);
let category = 'devops';
+20 -2
View File
@@ -176,6 +176,11 @@ TERMINALS['self-hosted-speech-to-text-api'] = `
<div class="line"><span class="prompt">&nbsp;</span><span class="err">connect ETIMEDOUT 192.168.1.123:20129 — bound to 127.0.0.1 only</span></div>
<div class="line"><span class="prompt">$</span><span class="cmd">bind 0.0.0.0 · firewall LocalSubnet · n8n key gate</span><span class="fix">→ 130 wpm ✓</span></div>`;
TERMINALS['replacing-rdpguard-with-ipban'] = `
<div class="line"><span class="prompt">$</span><span class="cmd">ipban --install-service</span></div>
<div class="line"><span class="prompt">&nbsp;</span><span class="err">Unrecognized command or argument '--install-service'</span></div>
<div class="line"><span class="prompt">$</span><span class="cmd">sc.exe create IPBAN type= own start= auto binPath= ...</span><span class="fix">→ AUTO_START ✓</span></div>`;
const DEFAULT_TERMINAL = `
<div class="line"><span class="prompt">$</span><span class="cmd">engineering · devops · self-hosting</span></div>
<div class="line"><span class="prompt">&nbsp;</span><span class="fix">read the full post →</span></div>`;
@@ -227,12 +232,25 @@ const tagsHtml = tags.map((t) => `<span>#${esc(t)}</span>`).join('');
const logoPath = 'file:///' + join(ROOT, 'public', 'logo-square.png').replace(/\\/g, '/');
// the tag chip shows "<category> · <kind>"; kind is a human label for the section.
// Were KIND to be hardcoded, every non-devops post would carry a wrong label.
const KIND_BY_CATEGORY = {
engineering: 'Engineering',
devops: 'DevOps',
ai: 'AI',
web3: 'Web3',
tutorials: 'Tutorials',
'case-studies': 'Case Study',
notes: 'Notes',
};
const kind = KIND_BY_CATEGORY[category] || category;
let tpl = readFileSync(join(__dirname, 'template.html'), 'utf8');
tpl = tpl
.replace('{{TITLE_SIZE}}', String(titleSize))
.replace('{{LOGO_PATH}}', logoPath)
.replace('{{CATEGORY}}', esc(category))
.replace('{{KIND}}', 'DevOps')
.replace('{{CATEGORY}}', esc(category === kind.toLowerCase() ? category : kind))
.replace('{{KIND}}', '')
.replace('{{TITLE_HTML}}', titleHtml)
.replace('{{TERMINAL_HTML}}', terminalHtml)
.replace('{{TAGS_HTML}}', tagsHtml);
+1 -1
View File
@@ -78,7 +78,7 @@
<div class="brand">Mr Hoelee <span class="domain">· blog.hoelee.com</span></div>
</div>
<div class="tag"><span class="dot"></span>{{CATEGORY}} · {{KIND}}</div>
<div class="tag"><span class="dot"></span>{{CATEGORY}}</div>
<div class="title">{{TITLE_HTML}}</div>
@@ -0,0 +1,244 @@
---
title: "Replacing RDPGuard With IPBan: The Traps Nobody Documents"
description: "I migrated from paid RDPGuard 7.8.7 to open-source IPBan 4.1.0 — and hit three undocumented traps, including an uninstaller that would have silently unbanned 12 attackers."
pubDate: 2026-09-19
category: devops
tags: [windows, security, rdp, ipban, rdpguard, brute-force, firewall, self-hosting]
ogImage: /og/replacing-rdpguard-with-ipban.png
banner: /banners/replacing-rdpguard-with-ipban.png
draft: false
---
Anyone who has put a Windows machine on the public internet with Remote Desktop enabled knows the drill. Within minutes of the port being reachable, the log fills with failed logins from rented servers all over the world. Something has to block them automatically.
I ran **RDPGuard 7.8.7** for years. It worked. But it's paid, closed-source, and I was three major versions behind. So I replaced it with **IPBan** — MIT-licensed, maintained since 2011, about 2,200 stars on GitHub, and genuinely the closest thing to a drop-in replacement.
The migration took an evening. Three of the steps were undocumented and one of them would have quietly **removed protection for twelve active attackers**. This is what actually happened.
## Why this matters
RDP exposed to the internet is not a "maybe someone will try" risk. On this machine, IPBan detected and banned **16 distinct attacker IPs in its first hour** — including one that walked a dictionary of administrator usernames and two that guessed the machine owner's actual name as the login. That's a targeted attack, not ambient noise.
If you run RDPGuard, IPBan, or anything similar, two things in this post will matter to you even if you never migrate:
1. **Uninstalling a blocker can unban your attackers.** Most tools own a firewall rule. Remove the tool, lose the rule, lose the bans.
2. **The config key you think controls ban duration probably doesn't.** Both tools in this story have a key that *sounds* right and does something else.
## What I was running
RDPGuard installed as two services plus three processes:
```
RdpGuardService C:\Program Files (x86)\RdpGuard\rdpguard-svc.exe
RdpGuardProxy ...\RDPGuardProxyServer\RDPGuardProxyServer.exe
```
It was doing its job. Its firewall rule held **12 blocked IPs**, each with a per-ban XML record in `C:\ProgramData\RdpGuard\blocked_ips\` capturing the IP, the attempted username, and the protocol.
That last part turned out to be the most important thing I checked.
## Trap 1: The uninstaller would have unbanned everyone
RDPGuard owns a Windows Firewall rule named `rdpguard-e1e259c5-local`. All twelve bans live in that one rule.
Here's the problem: when you uninstall RDPGuard, that rule goes with it. The uninstaller has no reason to distinguish "unsafe tool" from "useful ban list" — it just removes what it created. Uninstall first, and you'd have handed twelve known attackers a clean slate.
So before touching the uninstaller, I exported the bans and fed them into IPBan. IPBan reads a plain `ban.txt` file in its service folder — one IP per line — and consumes it on the next cycle:
```bash
# C:\app\IPBan\ban.txt — one IP per line
118.70.185.179
121.162.129.47
163.47.35.89
183.80.60.33
20.57.167.231
202.165.14.28
45.131.194.223
45.141.233.12
45.146.54.21
5.181.86.179
5.181.86.60
61.216.137.152
```
IPBan picked it up within seconds, deleted the file itself, and rewrote its own rule:
```
19:22:00 Updating firewall with 12 entries...
19:22:00 Firewall entries updated: 118.70.185.179:add, 121.162.129.47:add,
163.47.35.89:add, 183.80.60.33:add, 20.57.167.231:add, 202.165.14.28:add,
45.131.194.223:add, 45.141.233.12:add, 45.146.54.21:add, 5.181.86.179:add,
5.181.86.60:add, 61.216.137.152:add
```
The ban records also turned out to be worth reading rather than deleting. Two of the twelve had tried the usernames `HOELEE01` and `HOELEE1` — the machine owner's actual handle, guessed. One had cycled `Administrator``Administrador``Admin`. That's reconnaissance aimed at a specific person, and it's the kind of detail you'd never know you'd lost if you let the uninstaller run first.
## Trap 2: `--install-service` does not exist
The official install path for IPBan is a PowerShell script that pulls the latest release into `C:\Program Files\IPBan` and registers the service. I deliberately skipped it, because I'd already unzipped a specific version to `C:\app\IPBan` and wanted to keep that config and database.
So I tried the obvious thing:
```
DigitalRuby.IPBan.exe --install-service
```
IPBan 4.1.0's CLI is minimal. Here is the entire command list:
```
Commands:
version Get ipban software version
info Get information about hosting OS
migrate Migrate other provider to ipban.override.config
logfiletest <file> Test a log file with regexes for failures and successes
list List currently banned IPs (State=Active/in firewall)
unban <ip> Request UNBAN for an IP or for all currently banned IPs
ban <ip> Request BAN for an IP
```
No `--install-service`. No `--service`. Nothing service-related at all. IPBan is designed to be installed by the script, and when you don't use the script, service registration is on you.
The answer is `sc.exe`, which the project's own uninstall script hints at (`sc.exe stop IPBAN` / `sc.exe delete IPBAN`):
```bash
sc.exe create IPBAN type= own start= auto ^
binPath= "C:\app\IPBan\DigitalRuby.IPBan.exe" DisplayName= "IPBan"
```
**The spacing in `sc.exe` is load-bearing.** `type= own`, `start= auto`, and `binPath= "..."` each require **a space after the `=`**. Omit it and `sc` parses your argument as a single token and fails. This is a genuine footgun: it looks like a typo, and therefore looks optional.
Two deliberate differences from the official script:
| | Official installer | What I used |
|---|---|---|
| Path | `C:\Program Files\IPBan` | `C:\app\IPBan` (existing dir, config preserved) |
| Startup | `delayed-auto` | `auto` — starts earlier at boot, better for an exposed RDP box |
Verifying it landed, four ways:
```bash
$ sc.exe qc IPBAN
SERVICE_NAME: IPBAN
TYPE : 10 WIN32_OWN_PROCESS
START_TYPE : 2 AUTO_START
BINARY_PATH_NAME: C:\app\IPBan\DigitalRuby.IPBan.exe
SERVICE_START_NAME : LocalSystem
```
And the Windows Event Log confirms the registration independently — event ID 7045:
```
A service was installed in the system.
Service Name: IPBAN
Service Start Type: auto start
Service Account: LocalSystem
```
One cosmetic wart: because `sc create` got no `DisplayName=`, the service's display name is the bare `IPBAN` rather than a friendly string. Harmless, but it looks unfinished in `services.msc`.
## Trap 3: `ExpireTime` is not the ban duration
This is the one I got wrong first, and it's the one most likely to bite anyone tuning either tool.
I wanted a 24-hour ban. I searched the config, found `ExpireTime`, and nearly changed it. The actual key is **`BanTime`**:
```xml
<!-- The duration of time to ban an ip address (DD:HH:MM:SS) -->
<add key="BanTime" value="01:00:00:00"/>
```
`ExpireTime` does something else entirely — it's how long a *failed-login count* is remembered before it resets to zero:
```xml
<!-- The duration after the last failed login attempt that the ip is forgotten
(count reset back to 0). Set to 00:00:00:00 to use max duration. -->
<add key="ExpireTime" value="01:00:00:00"/>
```
Both default to `01:00:00:00`, which makes them look interchangeable. They are not. Set `ExpireTime` when you meant `BanTime` and your ban durations never change — you've just quietly changed how long attackers accumulate strikes.
Format is `DD:HH:MM:SS`. So 24 hours is `01:00:00:00`, and `00:00:00:00` means "9999 days," i.e. effectively permanent.
There's a second red herring. Every `<LogFile>` block in `ipban.config` carries its own `<FailedLoginThreshold>` element — and **all 25 of them are `0`**, meaning "use the global default." The global key that actually governs is:
```xml
<!-- Number of failed logins before banning the ip address -->
<add key="FailedLoginAttemptsBeforeBan" value="5"/>
```
Changing the per-log elements would have done nothing at all.
## How to tune IPBan without losing your settings
IPBan splits configuration in two:
| File | Role |
|---|---|
| `ipban.config` | Shipped defaults. **Overwritten on update — never edit.** |
| `ipban.override.config` | Your settings. Merges over the top. Edit this one. |
My working `ipban.override.config`:
```xml
<appSettings>
<!-- Never ban these. Whitelist beats blacklist. -->
<add key="Whitelist"
value="192.168.1.0/24,100.64.0.0/10,127.0.0.1,:?::1"/>
<!-- Ban after 3 failed logins (default is 5) -->
<add key="FailedLoginAttemptsBeforeBan" value="3"/>
<!-- Ban duration, DD:HH:MM:SS. 01:00:00:00 = 24 hours -->
<add key="BanTime" value="01:00:00:00"/>
</appSettings>
```
The whitelist deserves emphasis. IPBan watches **20 event-log queries** plus a pile of log files — not just RDP, but OpenSSH, IIS, Exchange, MSSQL, MySQL, FTP, SMTP/IMAP/POP3, VoIP/SIP, VNC, Tomcat. Any of those can trigger a ban.
If your own address isn't whitelisted, three mistyped passwords lock you out of your own machine for 24 hours. If RDP is your only remote access and the port is public, that's a real recovery problem. Whitelisted IPs are also placed in a separate **Allow** firewall rule (`IPBan_GlobalWhitelist_0`), which takes precedence.
Note that in a container or NAT'd network you can't always self-test — with your LAN and Tailscale ranges whitelisted, there's no easy way to deliberately trigger a ban from your own machine.
## Useful commands
```bash
# Watch the log live
Get-Content C:\app\IPBan\logfile.txt -Wait -Tail 20
# List currently banned IPs
C:\app\IPBan\DigitalRuby.IPBan.exe list
# Manually ban / unban
C:\app\IPBan\DigitalRuby.IPBan.exe ban 203.0.113.9
C:\app\IPBan\DigitalRuby.IPBan.exe unban 203.0.113.9
C:\app\IPBan\DigitalRuby.IPBan.exe unban all
```
Bans all live in one firewall rule that gets rewritten each cycle, so the GUI is the wrong place to look — `list` is the source of truth.
## The result
Sixteen attacker IPs blocked within the first hour, with no protection gap during the migration:
- **12 ported** from RDPGuard, so uninstalling it unblocked nothing
- **4 caught** by IPBan itself, including one that arrived mid-migration
IPBan logs `Remote ip address:` on startup, which is a neat way to confirm which of your own addresses the service considers the client. Verifying config is genuinely applied is harder than it looks — IPBan **hot-reloads** on file change and logs `Config file changed`, but it does not re-print your settings. A full restart (`Restart-Service IPBAN -Force`) requires elevation; from a non-elevated shell you get `System error 5 / Access is denied` and may wrongly conclude the restart worked when nothing happened.
## What I'd do differently
- **Export bans before uninstalling anything.** Make this step one, not an afterthought. Any tool that owns a firewall rule will take its ban list with it.
- **Read the ban records, not just the IPs.** The attempted usernames told me the attack was targeted.
- **Don't grep for a plausible key name — read the comment above it.** `BanTime` and `ExpireTime` both default to the same value, which is exactly why the wrong one survives testing.
- **Prefer `sc.exe` when the official installer would orphan your config.** If you've already unzipped a version somewhere, installing "properly" into `Program Files` leaves your database and settings behind.
- **Fix the exposure, not just the symptom.** Every tool here reacts *after* failed logins. Each attacker still gets its full threshold of guesses per ban window, from unlimited IPs. The durable fix is to not publish 3389 — restrict it to your LAN and VPN ranges and let the blocker handle the residue.
A blocker is a mitigation. If a port doesn't need to face the internet, closing it beats defending it.
## Want this for your business?
If you're running Windows servers with RDP, SQL, or mail services reachable from the internet, I set up brute-force protection like this — automated IP banning wired into the Windows Firewall, sensible thresholds, and a whitelist that keeps you from locking yourself out.
**WhatsApp: [+60 12-797 2969](https://wa.me/60127972969)** · **Email: [[email protected]](mailto:[email protected]?subject=RDP%20brute-force%20protection)** · **[hoelee.com](https://hoelee.com)**
Website design and development is my main line of work; server hardening and self-hosted infrastructure is the other half of it.
@@ -0,0 +1,243 @@
---
title: "用 IPBan 替换 RDPGuard:没人写出来的三个坑"
description: "我把付费的 RDPGuard 7.8.7 换成了开源的 IPBan 4.1.0,踩到三个没有文档的坑——其中一个卸载程序会悄悄放走 12 个正在攻击的 IP。"
pubDate: 2026-09-19
category: devops
tags: [windows, security, rdp, ipban, rdpguard, brute-force, firewall, self-hosting]
ogImage: /og/replacing-rdpguard-with-ipban.png
banner: /banners/replacing-rdpguard-with-ipban.png
draft: false
---
只要把一台开了远程桌面的 Windows 机器放到公网上,你就知道接下来会发生什么:几分钟内,日志里就塞满了来自世界各地租用服务器的登录失败记录。你必须有东西自动把它们挡掉。
我用了好几年 **RDPGuard 7.8.7**。它是能用的,但它是付费闭源的,而且我已经落后了三个大版本。所以我换成了 **IPBan**——MIT 许可、从 2011 年维护至今、GitHub 上大约 2200 星,确实是目前最接近即插即用的替代品。
整个迁移花了一个晚上。其中三步没有文档,而其中一步会悄悄**让 12 个正在攻击的 IP 重新获得访问权**。以下是真实发生的过程。
## 为什么这件事值得看
把 RDP 暴露在公网上不是「也许会有人来试试」的风险。在我这台机器上,IPBan 上线的**第一个小时内就检测并封禁了 16 个不同的攻击 IP**——其中一个遍历了管理员用户名词典,还有两个直接猜机器主人的真实名字作为登录名。这是有针对性的攻击,不是环境噪音。
如果你在跑 RDPGuard、IPBan 或任何类似的东西,这篇文章里有两件事跟你有关系,哪怕你永远不迁移:
1. **卸载一个拦截工具可能会把攻击者放出来。** 这类工具通常独占一条防火墙规则。工具没了,规则就没了,封禁也就没了。
2. **你以为控制封禁时长的那个配置键,多半不是。** 这个故事里两个工具都有这样「听起来没错、其实干别的事」的键。
## 我原本在跑什么
RDPGuard 装成了两个服务加三个进程:
```
RdpGuardService C:\Program Files (x86)\RdpGuard\rdpguard-svc.exe
RdpGuardProxy ...\RDPGuardProxyServer\RDPGuardProxyServer.exe
```
它一直在干活。它的防火墙规则里躺着 **12 个被封的 IP**,每个在 `C:\ProgramData\RdpGuard\blocked_ips\` 都有一份 XML 记录,里面存着 IP、尝试的用户名和协议。
最后一项,恰恰是最该检查的东西。
## 坑一:卸载程序会把所有人放出来
RDPGuard 独占了一条叫 `rdpguard-e1e259c5-local` 的 Windows 防火墙规则。12 个封禁全在这条规则里。
问题就在这里:卸载 RDPGuard 时,这条规则会跟着一起走。卸载程序没有任何理由去区分「不安全的工具」和「有用的封禁列表」——它只管删掉自己创建的东西。如果先卸载,你就等于给 12 个已知攻击者发了白纸一张。
所以在动卸载程序之前,我先把封禁导出来,喂给了 IPBan。IPBan 会读取服务目录里的纯文本 `ban.txt`——一行一个 IP——并在下一个周期消费掉:
```bash
# C:\app\IPBan\ban.txt — 一行一个 IP
118.70.185.179
121.162.129.47
163.47.35.89
183.80.60.33
20.57.167.231
202.165.14.28
45.131.194.223
45.141.233.12
45.146.54.21
5.181.86.179
5.181.86.60
61.216.137.152
```
IPBan 几秒内就读到了,自己把文件删掉,然后重写了它自己的规则:
```
19:22:00 Updating firewall with 12 entries...
19:22:00 Firewall entries updated: 118.70.185.179:add, 121.162.129.47:add,
163.47.35.89:add, 183.80.60.33:add, 20.57.167.231:add, 202.165.14.28:add,
45.131.194.223:add, 45.141.233.12:add, 45.146.54.21:add, 5.181.86.179:add,
5.181.86.60:add, 61.216.137.152:add
```
这些封禁记录也值得读一读,而不是直接删。12 个里有 2 个试用的用户名是 `HOELEE01``HOELEE1`——正好是机器主人的账号名,被猜出来了。另一个把 `Administrator``Administrador``Admin` 轮了一遍。这是针对具体个人的侦察。如果先让卸载程序跑,这类细节你根本不会知道自己丢了。
## 坑二:`--install-service` 根本不存在
IPBan 官方安装方式是一个 PowerShell 脚本,把最新版拉到 `C:\Program Files\IPBan` 并注册服务。我故意没用它,因为我已经把指定版本解压到 `C:\app\IPBan`,想保留那份配置和数据库。
于是我很自然地试了:
```
DigitalRuby.IPBan.exe --install-service
```
IPBan 4.1.0 的命令行非常精简。这是它的全部命令:
```
Commands:
version Get ipban software version
info Get information about hosting OS
migrate Migrate other provider to ipban.override.config
logfiletest <file> Test a log file with regexes for failures and successes
list List currently banned IPs (State=Active/in firewall)
unban <ip> Request UNBAN for an IP or for all currently banned IPs
ban <ip> Request BAN for an IP
```
没有 `--install-service`,没有 `--service`,跟服务相关的一个都没有。IPBan 的设计是「用脚本装」,而你不用脚本时,注册服务就是你自己的事。
答案是 `sc.exe`——项目自己的卸载脚本就暗示了这一点(`sc.exe stop IPBAN` / `sc.exe delete IPBAN`):
```
sc.exe create IPBAN type= own start= auto binPath= "C:\app\IPBan\DigitalRuby.IPBan.exe" DisplayName= "IPBan"
```
**`sc.exe` 里的空格是承重的。** `type= own``start= auto``binPath= "..."` 每一处的 `=` 后面都**必须有一个空格**。省略了,`sc` 会把参数当成一整个 token 解析然后报错。这是个真实的陷阱:它看起来像打错字,所以看起来也可以不打。
跟官方脚本相比,我有两个刻意的差异:
| | 官方安装脚本 | 我用的 |
|---|---|---|
| 路径 | `C:\Program Files\IPBan` | `C:\app\IPBan`(沿用已有目录,配置得以保留) |
| 启动方式 | `delayed-auto` | `auto`——开机更早启动,对暴露 RDP 的机器更合适 |
验证是否注册成功,有四种方式:
```bash
$ sc.exe qc IPBAN
SERVICE_NAME: IPBAN
TYPE : 10 WIN32_OWN_PROCESS
START_TYPE : 2 AUTO_START
BINARY_PATH_NAME: C:\app\IPBan\DigitalRuby.IPBan.exe
SERVICE_START_NAME : LocalSystem
```
Windows 事件日志可以独立佐证——事件 ID 7045:
```
A service was installed in the system.
Service Name: IPBAN
Service Start Type: auto start
Service Account: LocalSystem
```
一个小小的瑕疵:因为 `sc create` 没给 `DisplayName=`,服务的显示名就是干巴巴的 `IPBAN`,而不是一个友好的名字。无害,但在 `services.msc` 里看起来像没装完。
## 坑三:`ExpireTime` 不是封禁时长
这个是我一开始搞错的,也是最容易坑到人的一个。
我想要 24 小时封禁。我在配置里搜到 `ExpireTime`,差点就改了。真正的键是 **`BanTime`**
```xml
<!-- The duration of time to ban an ip address (DD:HH:MM:SS) -->
<add key="BanTime" value="01:00:00:00"/>
```
`ExpireTime` 干的是完全另一件事——它是**失败登录计数**在被清零之前会被记住多久:
```xml
<!-- The duration after the last failed login attempt that the ip is forgotten
(count reset back to 0). Set to 00:00:00:00 to use max duration. -->
<add key="ExpireTime" value="01:00:00:00"/>
```
两者的默认值都是 `01:00:00:00`,所以看起来像是可以互换的。它们不是。当你本想设 `BanTime` 却设了 `ExpireTime`,你的封禁时长根本没变——你只是悄悄改掉了攻击者累积次数的窗口。
格式是 `DD:HH:MM:SS`。所以 24 小时是 `01:00:00:00`,而 `00:00:00:00` 表示「9999 天」,也就是实际上永久。
还有第二个障眼法。`ipban.config` 里每个 `<LogFile>` 块都带一个自己的 `<FailedLoginThreshold>` 元素——而且**全部 25 个都是 `0`**,意思是「用全局默认值」。真正起作用的全局键是:
```xml
<!-- Number of failed logins before banning the ip address -->
<add key="FailedLoginAttemptsBeforeBan" value="5"/>
```
改那些 per-log 元素完全不会有任何效果。
## 怎样调 IPBan 又不搞丢配置
IPBan 把配置拆成两份:
| 文件 | 作用 |
|---|---|
| `ipban.config` | 官方默认值。**升级时会被覆盖——永远不要改。** |
| `ipban.override.config` | 你的设置。叠加在上面。改这个。 |
我实际在用的 `ipban.override.config`
```xml
<appSettings>
<!-- 不要封禁这些。白名单优先级高于黑名单。 -->
<add key="Whitelist"
value="192.168.1.0/24,100.64.0.0/10,127.0.0.1,:?::1"/>
<!-- 3 次失败登录即封禁(默认 5 次) -->
<add key="FailedLoginAttemptsBeforeBan" value="3"/>
<!-- 封禁时长,DD:HH:MM:SS。01:00:00:00 = 24 小时 -->
<add key="BanTime" value="01:00:00:00"/>
</appSettings>
```
白名单这一项必须强调。IPBan 会监听 **20 个事件日志查询**,外加一堆日志文件——不只是 RDP,还有 OpenSSH、IIS、Exchange、MSSQL、MySQL、FTP、SMTP/IMAP/POP3、VoIP/SIP、VNC、Tomcat。上面任何一个都可能触发封禁。
如果你自己的地址不在白名单里,三次输错密码就能把你锁在自己的机器外面 24 小时。如果 RDP 是你唯一的远程通道、端口又是公开的,这就是个真实的恢复难题。白名单里的 IP 还会被单独放进一条 **Allow** 防火墙规则(`IPBan_GlobalWhitelist_0`),优先级更高。
注意在容器或 NAT 网络里你没法自己测试——当你的局域网和 Tailscale 段都进了白名单,就没有什么方便的办法从本机故意触发一次封禁。
## 常用命令
```bash
# 实时看日志
Get-Content C:\app\IPBan\logfile.txt -Wait -Tail 20
# 列出当前被封的 IP
C:\app\IPBan\DigitalRuby.IPBan.exe list
# 手动封禁 / 解封
C:\app\IPBan\DigitalRuby.IPBan.exe ban 203.0.113.9
C:\app\IPBan\DigitalRuby.IPBan.exe unban 203.0.113.9
C:\app\IPBan\DigitalRuby.IPBan.exe unban all
```
所有封禁都在同一条防火墙规则里,每个周期重写一遍,所以 GUI 是找错地方了——`list` 才是真相来源。
## 结果
第一个小时内封掉 16 个攻击 IP,迁移全程没有防护空窗:
- **12 个是导入**自 RDPGuard,所以卸载它没有放走任何东西
- **4 个是 IPBan** 自己抓的,其中一个还是在迁移过程中来的
IPBan 启动时会记录 `Remote ip address:`,这是确认服务把你哪个地址当作客户端的好办法。要验证配置是否真的生效,比看起来要难——IPBan 在文件变动时会**热重载**并记录 `Config file changed`,但它不会重新打印你的设置。完整重启(`Restart-Service IPBAN -Force`)需要管理员权限;在非提权 shell 里你会得到 `System error 5 / Access is denied`,然后可能误以为重启成功了,其实什么都没发生。
## 我会怎么改
- **卸载任何东西之前,先导出封禁列表。** 这一步要放到第一位,而不是事后补。任何独占防火墙规则的工具,都会连封禁列表一起带走。
- **读封禁记录,不只是看 IP。** 那些被尝试的用户名告诉我,这是有针对性的攻击。
- **不要只 grep 一个听起来像的键名——读它上面的注释。** `BanTime``ExpireTime` 默认值相同,这正是设错了还能通过测试的原因。
- **当官方安装脚本会让你的配置成为孤儿时,宁可自己用 `sc.exe`。** 如果你已经把某个版本解压到别处,再「正规地」装到 `Program Files`,等于把数据库和设置丢在原地。
- **治根,不要只治症。** 这类工具全都是在失败登录**之后**才反应。每个攻击者在每个封禁窗口内依然能试满它那个阈值次,而且 IP 无限多。真正耐久的做法是不发布 3389——限制为局域网和 VPN 段可达,让拦截工具只处理剩下的部分。
拦截工具只是缓解措施。如果一个端口本来不需要面向互联网,关掉它比防守它划算。
## 需要为你的业务做这个吗?
如果你有 Windows 服务器把 RDP、SQL 或邮件服务暴露在公网上,我可以帮你搭这类暴力破解防护——自动封禁接入 Windows 防火墙、合理的阈值,以及一份不会把你自己锁在外面的白名单。
**WhatsApp[+60 12-797 2969](https://wa.me/60127972969)** · **邮箱:[[email protected]](mailto:[email protected]?subject=RDP%20brute-force%20protection)** · **[hoelee.com](https://hoelee.com)**
网站设计与开发是我的主业;服务器加固与自托管基础设施是它的另一半。