Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Yuki-J1/8b4552f0b611760f0eabefc7fb7956ef to your computer and use it in GitHub Desktop.
Save Yuki-J1/8b4552f0b611760f0eabefc7fb7956ef to your computer and use it in GitHub Desktop.
Programming the Windows firewall—官方教程翻译

应用程序可以通过系统库提供的API访问WFP。Microsoft有广泛的参考文档,因此我将通过fwpuclnt.dllinet.af/wf的视角提供一个简介,这是我们开发的一个用于从Go与WFP交互的包。
要与WFP交互,您首先需要创建一个会话,通过该会话您将发送进一步的指令:

session, err := wf.New(&wf.SessionOptions{
    Name:    "my WFP session",
    Dynamic: true,
})

WFP会话有几个选项,但最有趣的是会话是否应该是动态的。在动态会话中创建的过滤规则在创建会话的进程退出时会自动删除。如果您只想让规则在服务运行时存在,这非常方便。另一种选择是静态会话,允许您创建长期存在的规则,甚至可以在重新启动后无需进一步干预而保持存在。
随着会话的打开,我们可以列出和创建防火墙规则。但在这之前,我们应该多看看包过滤框架是如何组织的。
WFP通过一组层插入到Windows网络堆栈中。层是数据包处理流程中的检查点,是WFP可以查看数据包并对其作出判断的地方。每一层都有不同的信息提供,因此过滤规则需要根据特定层中的信息进行调整。
例如,来自网络的 IPv4 UDP 数据包经过以下层:
image.png
INBOUND_IPPACKET_V4仅具有 IP 级信息(大致上包括 OSI 层 3),主要是源/目标地址和到达接口。
INBOUND_TRANSPORT_V4具有 UDP 级信息(大致上包括 OSI 层 4):源 IP 和目标 IP 和端口,IP 协议(在我们的情况下是 UDP)以及一些元数据,例如 IPSec 领域 ID。
ALE_AUTH_RECV_ACCEPT_V4仅对 UDP 会话的第一个数据包。这是主要事件,过滤器可以访问所有 OSI 层次,包括允许其继续的进程的身份。
ALE_FLOW_ESTABLISHED_V4一个仅提供信息的层,允许应用程序收到 ALE 过滤层(如 ALE_AUTH_RECV_ACCEPT_V4)允许的每个新会话的通知。
DATAGRAM_DATA_V4ALE_AUTH_RECV_ACCEPT_V4的详细程度类似,但它能够看到来往流量的两个方向,而不仅仅是传入方向的流量。INBOUND_TRANSPORT_V4
虽然您可以在任何一个层创建规则,但大多数应用程序会希望专注于 (ALE) 层。这些层仅在每个连接中被调用一次,以决定是否应该允许整个连接。在此之后,所有后续数据包都可以避免这些繁重的层,从而提高性能。
(你可能想知道在谈论 UDP 时什么是“连接”。WFP 像许多其他类似的系统一样,认为 UDP “连接” 是一个源和目的地之间的 UDP 数据包流,空闲超时为几分钟。)

规则优先级和子层

现在我们已经知道了层是什么以及应该查看哪些层,我们需要在添加规则之前查看规则权重和子层。
WFP中的每个规则都有一个权重,它决定了规则与其他规则的相对顺序。为了评估一个数据包,您需要获取所有条件与数据包匹配的规则,按照从最高到最低权重的顺序对它们进行排序,并按顺序运行它们,直到找到返回Permit或Block的规则。那就是您的结论。
但这还不是全部!在层内,WFP有子层。子层是规则的容器,它本身具有权重。当数据包到达一层时,所有子层都会查看它,并根据其加权规则返回一个结论。
一旦所有子层都返回了一个结论,WFP就会运行“仲裁”并将所有这些单独的结论合并为最终的Permit或Block。确切的规则相当复杂,但基本逻辑是Block优先于Permit,因此所有子层都必须返回Permit或Continue(又称“无意见”),才能允许数据包通过。
(完整的规则更加复杂的原因是,子层可以返回“软件”或“硬件”Permit,后者将覆盖不同层中的Block。然后还有另一个单独的操作称为否决,它是一个“更硬”的Block,即使是硬件Permit也会被覆盖。你可以想象导致这种规则一场会议的继任关系!)
子层允许我们构建不互相干扰但都对所允许的流量有发言权的不同过滤规则集。例如,除了Windows Defender在其自己的子层中执行的操作之外,我们还可以添加一个子层,声明无论Windows Defender如何,都不允许在特定接口上进行任何入站连接。这些子层将组成这样的结构:对于该接口,我们的Block“选票”将优先于Windows Defender想要授予的任何权限。

添加过滤规则

现在我们了解了一些有关 WFP 的架构知识,我们可以开始添加规则。作为一个相当复杂的示例,让我们看一下在隐私 VPN 中广受欢迎的“默认路由杀开关”。隐私杀开关是一组规则,它阻止了除 VPN 接口之外的所有接口上的所有出站流量。虽然隐私 VPN 倾向于将其宣传为一种额外的安全措施,可防止流量意外地执行错误操作,但这种杀开关程序实际上是使默认路由 VPN 在 Windows 上正常工作所必需的!当 VPN 添加指向 VPN 接口的默认路由时,这只会影响到未来的新连接。您可能认为这是一个边缘情况,但是现代浏览器维护一个套接字池,以改善浏览网站时的性能。这意味着如果在启用 VPN 之前加载网站,然后继续浏览...你很可能仍在没有 VPN 的情况下浏览该网站。
幸运的是,WFP 可以帮助我们解决这个问题:每当更改 ALE 层中的规则时,这将触发 ALE 再授权:已打开的连接将被重新提交到过滤引擎,规则(包括您添加的新规则)有机会强制终止它们,即使它们以前已经被授权。
首先,让我们在WFP中注册一个新的子层:

guid, _ := windows.GenerateGUID()
sublayerID := wf.SublayerID(guid)

session.AddSublayer(&wf.Sublayer{
  ID:     sublayerID,
  Name:   "Default route killswitch",
  Weight: 0xffff, // the highest possible weight
})

(为了易读,我们在本篇文章中省略了错误检查。当然,在生产代码中,您需要在每个步骤中正确地检查错误。)
我们将我们的子层放在子层序列的顶部(Windows Defender在0x1000 0xD000注册自己,没有核心的Windows子层排名比它更高),因此,如果我们发出一个阻止决策,没有其他人可以否决它。
现在是添加一些规则的时候了。首先,我们要允许所有流量进出我们的VPN接口:

layers := []wf.LayerID{
  wf.LayerALEAuthRecvAcceptV4,
  wf.LayerALEAuthRecvAcceptV6,
  wf.LayerALEAuthConnectV4,
  wf.LayerALEAuthConnectV6,
}

for _, layer := range layers {
  guid, _ := windows.GenerateGUID()
  session.AddRule(&wf.Rule{
    ID:         wf.RuleID(guid),
    Name:       "Allow on VPN interface",
    Layer:      layer,
    Sublayer:   sublayerID,
    Weight:     1000,
    Conditions: []*wf.Match{
    &wf.Match{
      Field: wf.FieldIPLocalInterface,
      Op:    wf.MatchTypeEqual,
      Value: uint64(5), // interface ID
    },
    Action:     wf.ActionPermit,
  },
})

总的来说,对于我们要添加的每个规则,我们需要将其添加四次,分别为:入站 IPv4、出站 IPv4、入站 IPv6 和出站 IPv6。上面的循环说明了这一点,但是为了简洁起见,对于以后的规则,我们将只显示其中一个层。为了进一步简化这些代码示例,我将接口号5硬编码为 VPN 接口 ID。在实际代码中,您需要查找接口 ID(例如使用 winipcfg 包),然后使用该接口 ID。
接下来,我们需要允许一些流量继续使用非VPN接口。让我们允许DHCP使用任何接口:

guid, _ := windows.GenerateGUID()
session.AddRule(&wf.Rule{
  ID:         wf.RuleID(guid),
  Name:       "Allow DHCP",
  Layer:      wf.LayerALEAuthRecvAcceptV4,
  Sublayer:   sublayerID,
  Weight:     900,
  Conditions: []*wf.Match{
    &wf.Match{
      Field: wf.FieldIPProtocol,
      Op:    wf.MatchTypeEqual,
      Value: wf.IPProtoUDP,
    },
    &wf.Match{
      Field: wf.FieldIPLocalPort,
      Op:    wg.MatchTypeEqual,
      Value: uint16(68), // DHCP client port
    },
  },
  Action: wf.ActionPermit,
})

这里我们看到了一个带有多个匹配条件的规则,所有条件都必须匹配才能允许连接。我们指定本地端口为68的任何UDP会话都是允许的。在实际代码中,您可能希望将其限制得更加严格,例如通过限制远程端口或列出预期存在DHCP流量的特定接口集来进行约束。
我们还需要允许VPN进程本身通过普通接口发送加密数据包。幸运的是,与Linux不同,Windows防火墙可以有基于特定程序身份的规则:

guid, _ := windows.GenerateGUID()
// Get the absolute path of the current program
execPath, _ := os.Executable()
// Ask windows for the corresponding application ID
appID, _ := wf.AppID(execPath)
// And let it through the firewall
session.AddRule(&wf.Rule{
  ID:         wf.RuleID(guid),
  Name:       "Allow VPN program",
  Layer:      wf.LayerALEAuthRecvAcceptV4,
  Sublayer:   sublayerID,
  Weight:     800,
  Conditions: []*wf.Match{
    &wf.Match{
      Field: wf.FieldALEAppID,
      Op:    wg.MatchTypeEqual,
      Value: appID,
    },
  },
  Action: wf.ActionPermit,
})

最后,既然我们已经允许了VPN流量和一些例外,我们可以添加一个低权重的规则来阻止所有其他流量:

session.AddRule(&wf.Rule{
  Name:       "Block everything",
  Layer:      wf.LayerALEAuthRecvAcceptV4,
  Weight:     100,
  Conditions: nil,
  Action:     wf.ActionBlock,
})

被阻止的数据包的生命周期

有了这些规则,我们就可以看到传入连接的结果如何。假设您在 TCP 端口 8080 上运行开发 Web 服务器,当 Windows Defender 提示您时,您指示它允许传入流量到端口 8080。
连接通过您的 VPN 进入。让我们跳过我们没有接触过的层,看看 ALE_AUTH_RECV_ACCEPT_V4 层发生了什么。
我们的子层和 Windows Defender 的子层都有机会查看连接。在我们的子层中,连接符合 1000 权重规则,因为此连接的本地接口是 VPN,因此我们发出 Permit 判决。同样,Windows Defender 会根据协议 (TCP) 和端口 (8080) 发出一个 Permit。该层从其子层获得了两个许可,因此允许连接。
现在,有人试图通过 LAN 连接到您的网络服务器。 Windows Defender 的子层也允许这样做(它仍然是 TCP 端口 8080),但是在我们的子层中,没有任何允许规则再与连接匹配,因此我们进入最终的阻止规则,并发出阻止裁决。阻止的级别高于允许,因此即使 Windows Defender 对传入连接没有问题,我们还是否决并阻止了它。

与 Linux 的比较

如果像我一样,你来自 Linux 世界,可能需要一些调整才能像 WFP 期望你那样思考,但这里有一些东西的解码器环。这也很有启发性,因为我们可以看到每个系统的亮点或不足之处。

Feature WFP iptables
Filter organization Layers Tables and chains
Rule coexistence Sublayers No direct analog. Sub-chains, sort-of
Rule ordering Relative weight Explicit ordering
Connection-oriented processing ALE layers -m state --state NEW
Allowing traffic Permit action -j ACCEPT
Blocking traffic Block action -j REJECT
Custom actions WFP callouts Packet mangling
Advanced processing WFP callouts Send to userspace process
Advanced matching Built-in Extension match modules

在Linux世界中没有类似的东西,主要是子层,以及判决如何从子层流出来。在这方面,在Windows中,我们得到了许多改变防火墙的Linux程序试图通过添加子链来模仿的功能,偶尔也会通过争夺子链中的第一位置来确保没有其他东西可以扰乱他们的流量。
另一方面,Linux通过iptables扩展提供了更多的数据包处理和高级处理能力,开箱即用。WFP有一个叫做 "呼出 "的机制,它让过滤规则调用一个已经在过滤框架中注册的功能。
然而,默认情况下,可用的调用要少得多,而且实现新的调用需要写一个内核驱动。再加上缺乏将数据包踢到用户空间进行处理的机制,这意味着Windows防火墙更难进行创新。
尽管如此,开箱即用的WFP的ALE层提供了比iptables多得多的连接信息。最有用的是,你可以在接收或进行连接的应用程序的ID上进行匹配,使得表达 "tailscale.exe可以拨出 "这样的规则变得微不足道,而Linux仍然不能很好地做到这一点(尽管你可以通过用户和组匹配来模拟这一点)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment