使用DNSTap和BGP绕过ILV锁定


我知道,这个主题很漂亮。 例如,有一篇很棒的文章 ,但其中仅考虑了阻止列表的IP部分。 我们还将添加域。


由于法院和ILV封锁了左右两侧的所有事实,提供者正在竭力不落入“ Revizorro”发出的罚款之列-锁造成的相关损失相当大。 是的,在“合法”阻止的站点中,有很多有用的站点(您好,rutracker)


我住在ILV管辖范围之外,但我的父母,亲戚和朋友留在家里。 因此,决定想出一种绕开锁的方法,这种方法对于远离IT部门的人员来说很容易,最好是无需他们的参与。


在本文中,我不会逐步描述基本的网络事物,而是描述如何实现此方案的一般原理。 因此,必须具备有关网络一般(特别是Linux)工作方式的知识。


锁的类型


首先,让我们刷新被阻止的内容。


ILV的分页XML中有几种类型的锁:


  • 知识产权
  • 网址

为简单起见,我们将它们减少为两个:IP和域,并且我们将域简单地从URL阻止中拉出(更准确地说,我们已经为我们完成了此操作)。


来自Roskomsvoboda的好人已经实现了出色的API,通过它我们可以获取所需的信息:



访问被阻止的网站


为此,我们需要一些小型的外国VPS,最好是无限制的流量-有很多这样的3-5美元。 您需要在国外使用它,以便ping不会很大,但再次考虑到Internet和地理位置并不总是一致。 而且,由于没有5美元的SLA,因此最好从不同的提供商处购买2件以上,以获得容错能力。


接下来,我们需要配置从客户端路由器到VPS的加密隧道。 我将Wireguard用作最快,最容易配置的工具,因为 我也有基于Linux的客户端路由器( APU2或OpenWRT上的某些产品)。 对于某些Mikrotik / Cisco,您可以使用在它们上可用的协议,例如OpenVPN和GRE-over-IPSEC。


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


当然,您可以完全打包出国的所有Internet流量。 但是,最有可能的是,处理本地内容的速度将因此而受到极大的影响。 另外,VPS上的带宽要求会更高。


因此,我们将需要以某种方式将流量分配给被阻止的站点,然后有选择地将其发送到隧道。 即使“额外”流量的一部分到达了那里,它仍然比通过隧道运输所有东西要好得多。


为了管理流量,我们将使用BGP协议并宣布从VPS到客户端的必要网络的路由。 作为BGP守护程序,让我们将BIRD视为最实用,最方便的工具之一。


知识产权


使用IP阻止,一切都变得很清楚:我们只需使用VPS宣布所有被阻止的IP。 问题在于,API提供的列表中大约有60万个子网,其中绝大多数是/ 32主机。 如此多的路由可能会使弱小的客户端路由器感到困惑。


因此,在处理列表时,如果其中有2个或更多主机,则决定汇总到网络/ 24。 因此,路线数量减少到约10万。 接下来的脚本。



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


但是由于所有新的TLS1.3 + eSNI,HTTPS分析每天都变得越来越不真实。 而且客户端的基础架构变得越来越复杂-您将至少必须使用OpenWRT。


因此,我决定采用拦截DNS查询响应的方法。 在这里,任何基于TLS / HTTPS的DNS都开始突飞猛进,但是我们可以(现在)在客户端上控制此部分-禁用它或将我们自己的服务器用于DoT / DoH。


如何拦截DNS?


可能还有几种方法。


  • 通过PCAP或NFLOG拦截DNS流量
    这两种拦截方法均在sidmat实用程序中实现。 但是很长一段时间以来一直不支持它,并且该功能非常原始,因此无论如何您都需要为其编写一个绑定。
  • DNS服务器日志分析
    不幸的是,我知道的递归不知道如何记录答案,而只是请求。 原则上,这是合乎逻辑的,因为与请求不同,答案具有复杂的结构,并且很难以文本形式编写。
  • 域名解析
    幸运的是,出于这些目的,其中许多已经支持DNSTap。

什么是DNSTap?



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


重要的是要理解,在DNSTap范例中,DNS服务器充当客户端,而收集器充当服务器。 即,DNS服务器连接到收集器,反之亦然。


今天,所有流行的DNS服务器都支持DNSTap。 但是,例如,由于某些原因,许多发行版(例如Ubuntu LTS)中的BIND经常由于没有支持而被构建。 因此,我们不会费心重建,而是采用更轻,更快的递归-Unbound。


如何捕获DNSTap?


许多 CLI实用程序可用于处理DNSTap事件流,但它们不能很好地完成我们的任务。 因此,我决定发明自己的自行车来完成所有工作: 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重新读取域列表
  • 通过HTTP / JSON将缓存与其他dnstap-bgp实例同步
  • 复制磁盘上的缓存(在BoltDB数据库中)以在重新启动后恢复其内容
  • 支持切换到其他网络名称空间(为什么将在下面进行描述)
  • IPv6支持

局限性:


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

我已经编译了RPM和DEB软件包以便于安装。 应该可以在所有使用systemd的相对较新的操作系统上使用,例如 他们没有依赖性。


方案


因此,让我们开始将所有组件组装在一起。 结果,我们应该得到如下网络拓扑:


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


  • 客户端将我们的服务器配置为DNS,并且DNS查询也必须通过VPN。 这是必需的,以便提供程序不能使用DNS拦截进行阻止。
  • 打开站点后,客户端将以“ xxx.org具有哪些IP?”的形式发送DNS查询。
  • Unbound解析xxx.org(或从缓存中获取它)并将响应发送到客户端“ xxx.org具有此类IP”,并通过DNSTap并行复制它
  • 如果域在阻止列表中,则dnstap-bgp通过BGP在BIRD中宣布这些地址
  • BIRD通过next-hop self宣布到这些IP的路由next-hop self客户端路由器
  • 从客户端到这些IP的后续数据包通过隧道

在服务器上,对于通往被阻止站点的路由,我在BIRD内有一个单独的表,它与操作系统不相交。


此方案有一个缺点:来自客户端的第一个SYN数据包很可能有时间通过​​国内提供商离开 该路线不会立即宣布。 根据提供者如何进行锁定,这里的选项是可能的。 如果他只是丢掉流量,那就没有问题。 而且,如果他将其重定向到某些DPI,则(理论上)可能会产生特殊效果。


如果客户端不遵守DNS TTL,也可能会产生奇迹,这可能导致客户端使用其腐烂缓存中一些过时的条目,而不是询问“未绑定”。


实际上,第一个和第二个都不会引起我问题,但是您的行驶里程可能会有所不同。


服务器设置


为了便于滚动,我为Ansible编写了一个角色 。 它可以配置服务器和基于Linux的客户端(专为基于deb的发行版而设计)。 所有设置都非常明显,并在stock.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", ] 

bird.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"; ... 

域名解析


默认情况下,在Ubuntu中,UnArbined二进制文件由AppArmor配置文件钳位,这可防止它连接到那里的任何DNSTap套接字。 您可以删除此配置文件或将其禁用:


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

这可能应该添加到剧本中。 当然,更正个人资料并颁发必要的权利是理想的选择,但是我实在太懒了。


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地址列表的脚本
它下载列表,在pfx前缀之前汇总。 在dont_adddont_summarize中,您可以告诉IP和网络跳过或不进行总结。 我需要它,因为 我的VPS子网位于阻止列表中:)


有趣的是,RosKomSvoboda API会使用默认的Python用户代理阻止请求。 看起来像一个脚本小子得到了它。 因此,我们将其更改为Firelis。


到目前为止,它仅适用于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小时拉一次,因为 我认为,这是ILV要求供应商提供的更新期限。 另外,还有一些其他的紧急锁可以使它们更快地飞行。


请执行以下操作:


  • 运行第一个脚本并更新BIRD的路由列表( rkn_routes.list
  • 重新加载BIRD
  • 更新并清理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:


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,从而阻塞整个频道。 也许值得列出暂时被ILV阻止的流行例外域列表。 并在解析时跳过它们。


结论


所描述的方法使您可以绕过提供程序当前实现的几乎所有锁。


原则上, dnstap-bgp可用于任何其他需要基于域名的流量控制级别的目的。 只需记住,如今一千个站点可以挂在同一个IP地址上(例如,对于某些Cloudflare),因此此方法的准确性较低。


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


欢迎添加,编辑,拉取任务!

Source: https://habr.com/ru/post/zh-CN467547/


All Articles