使用 DNSTap 和 BGP 绕过 ILV 阻塞

使用 DNSTap 和 BGP 绕过 ILV 阻塞

我知道这个话题很糟糕。 例如,有一个很棒的 文章, 但只有黑名单的 IP 部分才被考虑在内。 我们还将添加域。

由于法院和 RKN 左右屏蔽所有内容,并且提供商正在努力避免受到 Revizorro 开出的罚款,因此屏蔽带来的相关损失相当大。 在“合法”阻止的网站中,有很多有用的网站(你好,rutracker)

我住在 RKN 管辖范围之外,但我的父母、亲戚和朋友留在家里。 因此决定为远离 IT 的人想出一个简单的方法来绕过封锁,最好是完全没有他们的参与。

在这篇笔记中,我不会按步骤描述基本的网络事物,而是描述如何实现这个方案的一般原则。 因此,必须了解网络的一般工作原理,尤其是在 Linux 中的工作原理。

锁的种类

首先,让我们回顾一下被阻止的内容。

从 RKN 卸载的 XML 中有几种类型的锁:

  • IP
  • 域名
  • 网址

为简单起见,我们将它们减少为两个:IP 和域,我们将简单地通过 URL 将域从阻止中拉出来(更准确地说,他们已经为我们做了这件事)。

好人来自 俄罗斯联邦 实现了精彩 API,通过它我们可以得到我们需要的东西:

访问被封锁的网站

为此,我们需要一些小型的外国 VPS,最好是无限流量 - 有很多这样的 VPS 只需 3-5 美元。 您需要在国外附近使用它,这样 ping 就不会很大,但同样要考虑到 Internet 和地理位置并不总是重合。 而且由于没有 5 美元的 SLA,最好从不同的供应商处获取 2 个以上的容错。

接下来,我们需要建立一条从客户端路由器到 VPS 的加密隧道。 我使用 Wireguard 作为最快和最容易设置的。 我也有基于 Linux 的客户端路由器(APU2 或者 OpenWRT 中的东西)。 对于某些 Mikrotik / Cisco,您可以使用它们提供的协议,如 OpenVPN 和 GRE-over-IPSEC。

识别和重定向感兴趣的流量

当然,您可以关闭所有通过国外的互联网流量。 但是,最有可能的是,处理本地内容的速度会因此受到很大影响。 另外,VPS 的带宽要求会更高。

因此,我们需要以某种方式将流量分配给被阻止的站点,并有选择地将其定向到隧道。 即使有一些“额外”的交通到达那里,也比让所有东西都通过隧道要好得多。

为了管理流量,我们将使用 BGP 协议并宣布从我们的 VPS 到客户端的必要网络的路由。 让我们将 BIRD 作为功能最强大、最方便的 BGP 守护进程之一。

IP

通过 IP 阻止,一切都很清楚:我们只需使用 VPS 公布所有被阻止的 IP。 问题是 API 返回的列表中大约有 600 万个子网,其中绝大多数是 /32 主机。 如此多的路由会使弱客户端路由器感到困惑。

因此,在处理列表时,如果有 24 个或更多主机,则决定汇总到网络 / 2。 因此,路线数量减少到约 100 万条。 将遵循此脚本。

它比较复杂,有几种方法。 例如,您可以在每个客户端路由器上安装一个透明的 Squid,并在那里进行 HTTP 拦截并窥视 TLS 握手,以便在第一种情况下获取请求的 URL,在第二种情况下从 SNI 获取域。

但是由于各种新奇的 TLS1.3 + eSNI,HTTPS 分析每天都变得越来越不真实。 是的,客户端的基础设施变得越来越复杂——你将不得不至少使用 OpenWRT。

因此,我决定采用拦截 DNS 请求响应的方式。 在这里,任何 DNS-over-TLS / HTTPS 也开始悬停在你的头上,但我们(现在)可以在客户端控制这部分 - 要么禁用它,要么使用你自己的服务器进行 DoT / DoH。

如何拦截DNS?

这里也可能有几种方法。

  • 通过 PCAP 或 NFLOG 拦截 DNS 流量
    这两种拦截方法都在实用程序中实现 西达. 但是支持时间不长,功能也很原始,所以还是需要自己写一个harness。
  • 解析DNS服务器日志
    不幸的是,我所知道的递归器无法记录响应,而只能记录请求。 原则上,这是合乎逻辑的,因为与请求不同,答案具有复杂的结构并且很难以文本形式编写。
  • DNSTap
    幸运的是,他们中的许多人已经为此目的支持 DNSTap。

什么是 DNSTap?

使用 DNSTap 和 BGP 绕过 ILV 阻塞

它是一种基于协议缓冲区和帧流的客户端-服务器协议,用于从 DNS 服务器传输到结构化 DNS 查询和响应的收集器。 本质上,DNS 服务器传输查询和响应元数据(消息类型、客户端/服务器 IP 等)以及(二进制)形式的完整 DNS 消息,并通过网络与它们一起工作。

了解在 DNSTap 范例中,DNS 服务器充当客户端,收集器充当服务器,这一点很重要。 也就是说,DNS 服务器连接到收集器,而不是相反。

今天,所有流行的 DNS 服务器都支持 DNSTap。 但是,例如,许多发行版(如 Ubuntu LTS)中的 BIND 通常出于某种原因在没有其支持的情况下构建。 所以我们不要为重新组装而烦恼,而是采用更轻更快的递归 - Unbound。

如何捕捉 DNSTap?

一些 号码 用于处理 DNSTap 事件流的 CLI 实用程序,但它们不适合解决我们的问题。 因此,我决定发明自己的自行车,它可以完成所有必要的事情: dnstap-bgp

工作算法:

  • 启动时,它会从文本文件加载域列表,将它们反转 (habr.com -> com.habr),排除断线、重复和子域(即如果列表包含 habr.com 和 www.habr.com,它只会加载第一个)并构建一个前缀树以快速搜索此列表
  • 作为 DNSTap 服务器,它等待来自 DNS 服务器的连接。 原则上它同时支持UNIX和TCP套接字,但我所知道的DNS服务器只能使用UNIX套接字
  • 传入的 DNSTap 数据包首先被反序列化为 Protobuf 结构,然后位于 Protobuf 字段之一的二进制 DNS 消息本身被解析为 DNS RR 记录级别
  • 检查请求的主机(或其父域)是否在加载列表中,如果不在,则忽略响应
  • 仅从响应中选择A/AAAA/CNAME RR,并从中提取相应的IPv4/IPv6地址
  • IP 地址使用可配置的 TTL 缓存并通告给所有已配置的 BGP 对等体
  • 当收到指向已缓存 IP 的响应时,更新其 TTL
  • TTL 过期后,条目将从缓存和 BGP 公告中删除

附加功能:

  • 通过 SIGHUP 重读域列表
  • 保持缓存与其他实例同步 dnstap-bgp 通过 HTTP/JSON
  • 复制磁盘上的缓存(在 BoltDB 数据库中)以在重启后恢复其内容
  • 支持切换到不同的网络命名空间(为什么需要这个将在下面描述)
  • 支持 IPv6

限制:

  • 尚不支持 IDN 域
  • 很少的 BGP 设置

我收集了 RPM 和 DEB 易于安装的软件包。 应该可以在所有带有 systemd 的相对较新的操作系统上工作。 他们没有任何依赖关系。

方案

那么,让我们开始将所有组件组装在一起。 结果,我们应该得到类似这样的网络拓扑:
使用 DNSTap 和 BGP 绕过 ILV 阻塞

工作的逻辑,我认为,从图中可以清楚地看出:

  • 客户端将我们的服务器配置为 DNS,DNS 查询也必须通过 VPN。 这是必要的,这样提供商就无法使用 DNS 拦截来阻止。
  • 打开站点时,客户端会发送一个 DNS 查询,例如“xxx.org 的 IP 是什么”
  • 不作承诺 解析 xxx.org(或从缓存中获取)并向客户端发送响应“xxx.org 具有某某 IP”,通过 DNSTap 并行复制它
  • dnstap-bgp 公布这些地址 如果域在阻止列表中,则通过 BGP
  • 通告到这些 IP 的路由 next-hop self 客户端路由器
  • 从客户端到这些 IP 的后续数据包通过隧道

在服务器上,对于到被阻止站点的路由,我在 BIRD 中使用了一个单独的表,它不会以任何方式与操作系统相交。

这个方案有一个缺点:来自客户端的第一个 SYN 数据包很可能有时间通过​​国内提供商离开。 路线不会立即公布。 这里的选项可能取决于提供商如何进行阻止。 如果他只是降低流量,那么就没有问题。 如果他将它重定向到某个 DPI,那么(理论上)特效是可能的。

客户端也可能不尊重 DNS TTL 奇迹,这可能导致客户端使用其腐烂缓存中的一些陈旧条目而不是询问 Unbound。

在实践中,第一个和第二个都没有给我带来问题,但你的里程可能会有所不同。

服务器调整

为了便于滚动,我写了 Ansible 的角色. 它可以配置基于 Linux 的服务器和客户端(专为基于 deb 的发行版而设计)。 所有设置都非常明显并设置在 库存.yml. 这个角色是从我的大剧本中删减的,所以它可能包含错误 - 拉请求 欢迎🙂

让我们来看看主要的组成部分。

BGP

在同一台主机上运行两个 BGP 守护进程有一个根本问题:BIRD 不想与本地主机(或任何本地接口)建立 BGP 对等。 从字皆。 谷歌搜索和阅读邮件列表没有帮助,他们声称这是设计使然。 也许有某种方法,但我没有找到。

你可以尝试另一个 BGP 守护进程,但我喜欢 BIRD,我到处都在使用它,我不想生产实体。

因此,我将 dnstap-bgp 隐藏在网络命名空间中,它通过 veth 接口连接到根:它就像一个管道,其末端伸出到不同的命名空间中。 在每一端,我们都挂有不超出主机范围的私有 p2p IP 地址,因此它们可以是任何东西。 这与用于访问内部进程的机制相同 受到所有人的喜爱 Docker 和其他容器。

为此,它被写成 脚本 并且上面已经描述的将你自己拖到另一个命名空间的功能被添加到 dnstap-bgp。 因此,它必须以 root 身份运行或通过 setcap 命令发布到 CAP_SYS_ADMIN 二进制文件。

创建命名空间的示例脚本

#!/bin/bash

NS="dtap"

IP="/sbin/ip"
IPNS="$IP netns exec $NS $IP"

IF_R="veth-$NS-r"
IF_NS="veth-$NS-ns"

IP_R="192.168.149.1"
IP_NS="192.168.149.2"

/bin/systemctl stop dnstap-bgp || true

$IP netns del $NS > /dev/null 2>&1
$IP netns add $NS

$IP link add $IF_R type veth peer name $IF_NS
$IP link set $IF_NS netns $NS

$IP addr add $IP_R remote $IP_NS dev $IF_R
$IP link set $IF_R up

$IPNS addr add $IP_NS remote $IP_R dev $IF_NS
$IPNS link set $IF_NS up

/bin/systemctl start dnstap-bgp

dnstap-bgp.conf

namespace = "dtap"
domains = "/var/cache/rkn_domains.txt"
ttl = "168h"

[dnstap]
listen = "/tmp/dnstap.sock"
perm = "0666"

[bgp]
as = 65000
routerid = "192.168.149.2"

peers = [
    "192.168.149.1",
]

鸟.conf

router id 192.168.1.1;

table rkn;

# Clients
protocol bgp bgp_client1 {
    table rkn;
    local as 65000;
    neighbor 192.168.1.2 as 65000;
    direct;
    bfd on;
    next hop self;
    graceful restart;
    graceful restart time 60;
    export all;
    import none;
}

# DNSTap-BGP
protocol bgp bgp_dnstap {
    table rkn;
    local as 65000;
    neighbor 192.168.149.2 as 65000;
    direct;
    passive on;
    rr client;
    import all;
    export none;
}

# Static routes list
protocol static static_rkn {
    table rkn;
    include "rkn_routes.list";
    import all;
    export none;
}

rkn_routes.list

route 3.226.79.85/32 via "ens3";
route 18.236.189.0/24 via "ens3";
route 3.224.21.0/24 via "ens3";
...

DNS

默认情况下,在 Ubuntu 中,Unbound 二进制文件被 AppArmor 配置文件限制,禁止它连接到各种 DNSTap 套接字。 您可以删除或禁用此配置文件:

# cd /etc/apparmor.d/disable && ln -s ../usr.sbin.unbound .
# apparmor_parser -R /etc/apparmor.d/usr.sbin.unbound

这可能应该添加到剧本中。 当然,更正配置文件并颁发必要的权限是理想的,但我太懒了。

未绑定.conf

server:
    chroot: ""
    port: 53
    interface: 0.0.0.0
    root-hints: "/var/lib/unbound/named.root"
    auto-trust-anchor-file: "/var/lib/unbound/root.key"
    access-control: 192.168.0.0/16 allow

remote-control:
    control-enable: yes
    control-use-cert: no

dnstap:
    dnstap-enable: yes
    dnstap-socket-path: "/tmp/dnstap.sock"
    dnstap-send-identity: no
    dnstap-send-version: no

    dnstap-log-client-response-messages: yes

下载和处理列表

用于下载和处理 IP 地址列表的脚本
它下载列表,汇总到前缀 x。 在 不要添加 и 不要总结 您可以告诉 IP 和网络跳过或不汇总。 我需要它。 我的 VPS 的子网在黑名单中 🙂

有趣的是,RosKomSvoboda API 会阻止使用默认 Python 用户代理的请求。 看起来脚本小子明白了。 因此,我们将其更改为 Ognelis。

到目前为止,它只适用于 IPv4。 IPv6 的份额很小,但很容易修复。 除非你也必须使用 bird6。

rkn.py

#!/usr/bin/python3

import json, urllib.request, ipaddress as ipa

url = 'https://api.reserve-rbl.ru/api/v2/ips/json'
pfx = '24'

dont_summarize = {
    # ipa.IPv4Network('1.1.1.0/24'),
}

dont_add = {
    # ipa.IPv4Address('1.1.1.1'),
}

req = urllib.request.Request(
    url,
    data=None, 
    headers={
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36'
    }
)

f = urllib.request.urlopen(req)
ips = json.loads(f.read().decode('utf-8'))

prefix32 = ipa.IPv4Address('255.255.255.255')

r = {}
for i in ips:
    ip = ipa.ip_network(i)
    if not isinstance(ip, ipa.IPv4Network):
        continue

    addr = ip.network_address

    if addr in dont_add:
        continue

    m = ip.netmask
    if m != prefix32:
        r[m] = [addr, 1]
        continue

    sn = ipa.IPv4Network(str(addr) + '/' + pfx, strict=False)

    if sn in dont_summarize:
        tgt = addr
    else:
        tgt = sn

    if not sn in r:
        r[tgt] = [addr, 1]
    else:
        r[tgt][1] += 1

o = []
for n, v in r.items():
    if v[1] == 1:
        o.append(str(v[0]) + '/32')
    else:
        o.append(n)

for k in o:
    print(k)

要更新的脚本
我每天在表冠上运行一次,也许值得每 4 小时拉动一次。 在我看来,这是 RKN 要求供应商提供的续订期限。 另外,他们还有其他一些超级紧急的封锁,可能会更快到达。

执行以下操作:

  • 运行第一个脚本并更新路由列表(rkn_routes.list) 为鸟
  • 重新加载鸟
  • 更新并清理 dnstap-bgp 的域列表
  • 重新加载 dnstap-bgp

rkn_update.sh

#!/bin/bash

ROUTES="/etc/bird/rkn_routes.list"
DOMAINS="/var/cache/rkn_domains.txt"

# Get & summarize routes
/opt/rkn.py | sed 's/(.*)/route 1 via "ens3";/' > $ROUTES.new

if [ $? -ne 0 ]; then
    rm -f $ROUTES.new
    echo "Unable to download RKN routes"
    exit 1
fi

if [ -e $ROUTES ]; then
    mv $ROUTES $ROUTES.old
fi

mv $ROUTES.new $ROUTES

/bin/systemctl try-reload-or-restart bird

# Get domains
curl -s https://api.reserve-rbl.ru/api/v2/domains/json -o - | jq -r '.[]' | sed 's/^*.//' | sort | uniq > $DOMAINS.new

if [ $? -ne 0 ]; then
    rm -f $DOMAINS.new
    echo "Unable to download RKN domains"
    exit 1
fi

if [ -e $DOMAINS ]; then
    mv $DOMAINS $DOMAINS.old
fi

mv $DOMAINS.new $DOMAINS

/bin/systemctl try-reload-or-restart dnstap-bgp

它们是在没有经过深思熟虑的情况下编写的,所以如果您看到可以改进的地方 - 那就去做吧。

客户端设置

在这里,我将给出 Linux 路由器的示例,但在 Mikrotik / Cisco 的情况下,它应该更容易。

首先,我们设置 BIRD:

鸟.conf

router id 192.168.1.2;
table rkn;

protocol device {
    scan time 10;
};

# Servers
protocol bgp bgp_server1 {
    table rkn;
    local as 65000;
    neighbor 192.168.1.1 as 65000;
    direct;
    bfd on;
    next hop self;
    graceful restart;
    graceful restart time 60;
    rr client;
    export none;
    import all;
}

protocol kernel {
    table rkn;
    kernel table 222;
    scan time 10;
    export all;
    import none;
}

因此,我们将从 BGP 接收的路由与内核路由表编号 222 同步。

之后,要求内核在查看默认板之前先查看此板就足够了:

# ip rule add from all pref 256 lookup 222
# ip rule
0:  from all lookup local
256:    from all lookup 222
32766:  from all lookup main
32767:  from all lookup default

一切,剩下的就是在路由器上配置DHCP,将服务器的隧道IP地址分配为DNS,方案就准备好了。

限制

使用当前生成和处理域列表的算法,它包括,除其他外, youtube.com 及其 CDN。

这导致所有视频都将通过 VPN,这会阻塞整个频道。 也许值得编制一份流行域排除列表,暂时阻止 RKN,胆量很小。 并在解析时跳过它们。

结论

所描述的方法允许您绕过提供商当前实施的几乎所有阻止。

原则上, dnstap-bgp 可用于任何其他需要基于域名进行某种级别的流量控制的目的。 请记住,在我们这个时代,一千个站点可以挂在同一个 IP 地址上(例如,在某些 Cloudflare 后面),因此这种方法的准确性相当低。

但是对于绕过锁的需求,这已经足够了。

添加、编辑、拉取请求 - 欢迎!

来源: habr.com

添加评论