认证门户（Captive Portal）是生活中非常常见的上网准入方式。当我们连上一个开放式（不需要密码即可连接）的 Wi-Fi 接入点以后，几乎都不能直接访问互联网，而是需要在一个网页上进行一些认证后才可以访问，这就是认证门户。 从这种 Wi-Fi 接入点的准入方式来说，我们很容易就发现一个问题：需要访问网页才能让用户开始准入认证，但怎么才能让用户去访问呢？目前通行的一种做法是直接劫持 DNS 请求或者 HTTP 请求，重定向到认证页面。这种做法在 HTTPS 时代之前很容易做到，但是 HTTPS 时代之后就遇到了困难：由于认证门户并没有用户访问的网站的证书，要劫持用户就势必要用假的证书来进行，这样一来二去也会给用户带来信任不安全证书的坏习惯。 当然，现在的系统也都有自己的做法。比如 Windows 就会尝试用 http 连接 www.msftconnecttest.com，如果请求被重定向了，那么就会弹出页面让用户进行登录。这虽然解决了一部分问题，但是怎么看怎么不清真对吧，所以在 2015 年，RFC 7710 规定了一种方式：如果 DHCP 或 RA 在指定的 option 中发送了一个 URL，那么就引导用户前往该页面进行认证。 RFC 7710 看起来确实解决了一些问题，但是如果用户此前已经登录，其实是没必要再次登录的。这样系统还是绕不开一次互联网连通性的测试，当然 DHCP 服务器可以在通过认证的情况下不发送有关 option，然而 DHCP 服务器并不一定和计费系统是耦合的，做这种耦合也是复杂且不合适的。所以 capport 工作组设计了一套认证门户的标准，也就是 RFC 8908 和 RFC 8910（代替 RFC 7710）。 我了解到 capport 工作组的这些内容，是因为前两天名为菜色狼的群友去了澳门，在澳门线下赌场看完性感荷官在线发牌之后回到了他下榻的喜来登酒店，连上 Wi-Fi 后发现了名为“到期时间”的内容。 一开始我猜测是 DHCP 租约到期时间，但是菜色狼说之前在家里和别的地方都没见过，于是猜测是某种标准，我便顺藤摸瓜找到了上面提到的两个 RFC。 RFC 8908 规定了认证门户对客户端提供一些信息的 API 格式，API 返回格式为 JSON 对象，IANA 也维护了一个注册表来登记可用的字段。目前只有随着 RFC 发布的这几个，其中 captive 字段是必须包含的： captive: boolean 表示用户是否需要登录。 user-portal-url: string 描述了用户完成准入所需访问的页面，也是之前 RFC 7710 中规定的 URL。 venue-info-url: string 描述了网络运营者想向用户展示的内容（例如地图、航班信息等）。 can-extend-session: boolean 表示用户当前的会话是否可以被延长（时间、流量等），这个字段可以让系统在时间或流量将要耗尽的时候让用户访问 user-portal-url 重新认证。 seconds-remaining: number 描述了用户当前会话的剩余时间。 bytes-remaining: number 描述了用户当前会话的剩余流量（字节数）。 其中菜色狼看到的“到期时间”就是 seconds-remaining 带来的。 RFC 8910 则是替代了之前的 RFC 7710，将 URL 的定义改成了 RFC 8908 中提到的 API 端点，同样也有 3 种通告方式： DHCPv4 Option 114 DHCPv6 Option 103 IPv6 Router Advertisement Option 37 有了这两个 RFC，认证门户上网准入的流程就是这样的了： 连上网络、从 DHCP/RA 获得地址的同时知道认证门户 API 的地址。 系统请求 API，得知设备当前的准入状态，如果需要认证则让用户访问 user-portal-url。 设备已经准入，根据 API 返回的信息选择性地进行一些信息展示。 相比劫持用户的连接、系统自己探测这些做法之外，就清晰和明确很多，也安全很多了。 而且这样一来，只需要提供一个 API 地址并返回规定的内容，并不需要一个真实的认证门户就能复现，折腾的劲儿也就上来了。接着我创建了这样一个 PHP 文件： <?php header('Content-Type: application/captive+json'); echo json_encode([ 'captive' => false, 'venue-info-url' => 'https://jin.sh', 'seconds-remaining' => 4555739950 - time(), 'bytes-remaining' => 114514000, 'can-extend-session' => false ]); 按照 RFC 8908 所描述的，这样应该会在 Android 上显示出过期时间为 2114-05-14 19:19:10（即 4555739950 时间戳），并展示一个前往“信息页” https://jin.sh 的链接。接着只要把代码挂上线，在 DHCP 服务器上根据 RFC 8910 中所描述的 DHCP Option 指向线上的 URL。对于我的 Juniper 上只需要一句： set access address-assignment pool soha-home_4 family inet dhcp-attributes option 114 string "https://soha.moe/rfc8908.php" OpenWrt 也可以在 DHCP 服务器设置中的高级设置选项卡里用类似 114,https://soha.moe/rfc8908.php 的方式来设置这个 DHCP option，但我没有可以测试的设备，没法确定这里的写法是否正确。另外 Android 不支持 DHCPv6，我这里也就没设置 DHCPv6 对应的 DHCP option。 然而手机连接后并没有任何动静，服务器没看到任何请求。根据 commit 历史，这个功能早在 2020 年已经进入 Android 主线，Android 13 应当是支持的。但是考虑到我的设备是运行 Oxygen OS 的 1+ 10T，1+ 魔改的系统可能并没有合并这个功能。因此我斥资 1500 元在某宝购买了一个全新 Pixel 6a，在原生 Android 14 上进行体验。 两天后手机到货，连上 Wi-Fi 以后如愿看到了过期时间的信息、以及一个跳转到我的个人主页的按钮“Open site”和通知信息。 RFC 对于安全的考虑还是很充分的，比如强制要求 API 和认证页面是通过 https 请求的，还考虑了 DNS 安全等方面的问题，包括但不限于这些内容在 RFC 原文中都有提到。 我在最初测试的时候并不是将 API 放在 https://soha.moe/rfc8908.php，而是直接在本地用 php -S 开了一个服务器（所以没有 https）。配上 DHCP option 以后并没有看到有任何请求，我当时以为是 Oxygen OS 完全不支持。为了测试这个功能，以及之前用来收短信的 1+ 3T 的电池已经把屏幕都彻底顶开该换了，才直接购买了一部 Pixel 6a。但是到货以后还是不行，重新阅读 RFC 才发现了有强制 https 的内容，所以 Android 不请求我的 API。重新配置才发现其实 Oxygen OS 也是会请求 API 的，只是 OnePlus 的设置 UI 并没有 merge 主线上的这一部分，也就没有展示出来，打开 venue-info-url 的通知信息也没有弹出来。 当前这个 RFC 似乎也只有 Android 实现了，虽然 Apple 员工是 RFC 8908 的一作，但是 iOS 并不支持这个功能。Windows 也似乎没有相关的跟进。 iOS 18 已经支持显示 venue-info-url，但是没有理会 bytes-remaining 和 seconds-remaining：
TL;DR: IPv6 子网内第一个地址是任播地址，没有需求，最好别用。 在网上冲浪的时候，经常看到过类似“IPv6 取消了子网号和广播号，所有地址都能用”这样的言论。也经常看到有人给自己的主机或路由器配上形如 2001:db8:1:2:: 的地址。那么，IPv6 真的每个地址都能用吗？ 首先我们先限制一下讨论的范围，要去除各种特殊的地址，例如未指定地址 ::/128、本地回环地址 ::1/128、链路本地地址 fe80::/10 还有多播地址 ff00::/8 等，因为这些地址大家都知道有特定含义、不能乱用。这里说的都是一个普通的单播地址，即全球单播地址（Global Unicast Address，目前是 2000::/3）或者唯一本地地址（Unique Local Address，fc00::/7）范围内的子网的地址。 接着用最近流行的句式讲结论：能，但不是完全能。 诚然，IPv6 中没有 IPv4 中子网的第一个地址是网络号不能用、最后一个地址是广播号不能用的规定。但在规定 IPv6 地址结构的 RFC 4291 中，介绍了一种特别的任播地址——子网路由器任播地址（Subnet-Router Anycast Address） 。这个地址似乎一直被人忽略，也很少有人提，所以知道的人并不多。这个地址就是子网的第一个地址，例如 2001:db8:2:33::/64 这个子网（范围为 2001:db8:2:33:: 到 2001:db8:2:33:ffff:ffff:ffff:ffff）中的子网路由器任播地址就是 2001:db8:2:33::，2001:db8:3:4:5:6:7:8/125 这个子网（范围为 2001:db8:3:4:5:6:7:8 到 2001:db8:3:4:5:6:7:f）中的子网路由器任播地址就是 2001:db8:3:4:5:6:7:8。 如果了解任播（anycast）的概念应该知道，任播是一组主机共享一个地址。对于一个分组，只有这一组主机中的一台会收到，然后做出一定的响应。子网路由器任播地址就是给需要和子网内任意一台（不是指定的一个，也不是所有的）路由器通讯这种场景所保留的。因此这个地址也是有特殊含义的，应当在特定情况下使用，而不是随意使用。 目前很多系统都支持子网路由器任播地址，例如 Linux。Linux 会在 IPv6 转发处于开启状态的时候，自动绑定上机器目前所有子网的子网路由器任播地址。虽然只有路由器才会转发 IPv6 报文，但是有些时候（尤其是我们这些网络技术爱好者），子网内某些机子很可能会打开它，例如需要给 Docker 配上 IPv6 支持的时候。这样的情况下，如果你想通过子网内的第一个地址访问路由器，就有可能会变成访问了你这台 Docker 主机。当然，你可以在网络设备（如交换机）上设置规则来限制除了你指定的路由器之外的主机响应子网路由器任播地址的邻居请求（Neighbor Solicitation）信息，但是你不应该在完全不知道这个知识去使用子网内的第一个地址。 另外，因为/127 大小的子网只有 2 个、/128 大小的子网只有 1 个地址。因此子网路由器任播地址不会在这两个尺寸的子网内起作用。说到这个也是很有趣的：最初，RFC 3627 以这个理由不让大家在路由器之间使用/127 大小的子网，因为这样的子网只有两个地址，第一个地址同时属于两台路由器，那就只有一个地址提供给单独一个路由器了。显然/127 大小的子网做路由器间互联是最合适的，因为方便计算且只有两个地址。于是后来 RFC 6164 要求子网路由器任播地址不能作用在/127 大小的子网中，这样就可以在路由器之间用/127 大小的子网互联了。原来的 RFC 3627 也在第二年被 RFC 6547 废除了。
本文同步发布于 GitHub，如有问题、意见、建议，请移步 GitHub issue。 我本来是一直不想写这个这类文章的，因为我一直认为动态路由（包括但不限于 BGP）这玩意儿基本都是学习了计算机网络基础知识的人玩的。这些人大都有足够的阅读、学习经验与能力，根据 BIRD 的官方文档也都能写出基本正确、可用的配置。而且教程往往会导致更多的新手入坑，我并不是很希望什么都不会的人在没有前置知识储备的基础下来研究这个，所以一直没动笔。 但是在我对入坑的新手的行为观察下来，好像有很多人只是想拥有自己的 ASN 然后实名上网，并不想、或没时间对计算机网络、对 BIRD 的配置文件进行深一度的研究和学习，亦或者是因为语言问题，没有去阅读文档。配置文件只是拿别人放出来的进行拼凑（如某些个人博客，和 Vultr 的样例），知其然而不知其所以然，也造成了很多错误的理解。出于这些原因，我还是动笔写下了这篇文章，希望能为新手留下尽量正确的理解。 话虽如此，我还是希望新手能在学习一部分计算机网络的基础后再来，不然有些部分可能会理解困难。 以及建议学习的话选择开几台虚拟机，或者在 DN42（一个世界性的很大的实验性互联网）上进行试验。只有在不满足于在本地或 DN42 实验的情况下，想要真真正正的自己当 ISP，才申请、租用互联网上的 ASN 和 IP 地址资源。 BIRD 与 BGP 的新手开场 版本：1.0-20220806 本文以 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 License 进行授权。 本文中使用的 BIRD 版本为撰写此文时的最新版，BIRD 2.0.7。目前 BIRD 1.x 和 2.x 同时在维护，区别是 1.x 中 IPv4 和 IPv6 协议是分开的（bird 和 bird6），而 2.x 将两部分代码合并在了一起且引入了更多功能。但是 1.x 本身也是落后技术终将被淘汰，所以我推荐使用 BIRD 2.x。两个版本的语法并无差别，无非就是 2.x 在和路由表相关的操作的时候，需要指定特别的协议，如 ipv4; ipv6;。 本文追求的是为有心学习的人提供一个更容易达到的起点。如果只是为了最简单的装逼或者说这篇文章完全看不懂，建议还是别看下去了，可能蔡老师的 RouterOS 版本更适合你。 本文因为是 BIRD 与 BGP 的快速起步，因此不会介绍 BGP 协议，也只会介绍实践部分中用得到的 BIRD 配置，并稍微扩展介绍一些其他 BIRD 的功能，更多的内容则建议在阅读完本文后试着去阅读官方文档，以更深入地理解。 BIRD 官方也提供了一些样例，可以去 BIRD 的 GitLab Wiki 查看。 正文 什么是 BIRD BIRD 的基本语法 杂项 变量与常量 操作符 分支 过滤器和函数 过滤器 filter 函数 function 过滤器写什么呢 路由属性 协议实例 频道 channel 协议 protocol 协议模板 template 杂七杂八的东西 更多配置 控制台 实践中学习 结语 附录 BIRD 全局选路规则 BIRD BGP 协议选路规则 BIRD 的包含（~）操作符 常用的各种配置 公网上约定俗成的最小前缀 Bogon Prefixes and ASNs 与 Vultr 的 BGP session 与 HE TunnelBroker 的 BGP session 1 什么是 BIRD BIRD 是一个实现多种动态路由协议（如 OSPF、BGP、RIP 等）的软件，相似的软件有 FRR（Quagga）、MRT。在 BGP 玩家常用的软件中，BIRD 因其内存占用小、管理方便大受欢迎。 在 BIRD 中，有一个存储了从各个协议收到的路由（包括静态路由）的 RIB（Route Information Base，路由表），经过选路规则的挑选，产生最优的一条路由，这条最优的路由会被写入 FIB（Forward Information Base，转发表）中，导出给系统（如 Linux 内核）中的转发表，这样发到该路由器的流量就能被继续转发了。 注：在高阶玩法中可以使用多个 RIB，在协议中可以指定其使用哪个 RIB，在 BIRD 中也可以指定 RIB 对应的是内核中的哪个 FIB。为了简洁明白，本文只使用 BIRD 的默认 RIB，和内核的默认 FIB。 2 BIRD 的基本语法 和 Juniper、Cisco 等路由器，或 FRR（Quagga）等路由软件不同，写 BIRD 的配置文件就像是在写程序，如果你是个程序员，那么上手应该会很快。正因如此，它也有着和常见编程语言所类似的语法。下面则是一些基础语法。 杂项 用 /* */ 包起来的内容是注释，# 至其所在行行末的内容也是注释。 分号 ; 标示着一个选项或语句的结束，而花括号 { } 内则是多个选项或语句。 在 BIRD 的配置文件中，有协议实例（protocol <proto> <name> {}），过滤器（filter <name> [local_variables] {}），函数（function <name> [local_variables] {}）可以定义，这些将在下文各部分选择性挑重点介绍。 print 用来输出内容，这些会输出在 BIRD 的日志文件中，在使用 systemd 的系统中，可以使用 journalctl -xeu bird.service 查看。 变量与常量 变量名、常量名、协议实例名、过滤器名、函数名等，都遵循着这样的规则：必须以下划线或字母开头，名称内也只能有字母、数字、下划线。比如 Soha233、_my_filter、bgp_4842 都是合法的名字。当然在 BIRD 中有例外，如果一个名字用单引号括起来，那么我们还可以用冒号、横线、点，比如 '2.333:what-a-strange-name'，只不过不推荐这么用就是了。 使用 define 定义常量，如 define LOCAL_AS = 65550。 BIRD 中可以针对很多变量类型定义集合。集合用一对方括号定义，如 [1, 2, 3, 4]。集合可以用范围来快速生成，比如 [1, 2, 10..13] 就会生成为 [1, 2, 10, 11, 12, 13]。范围的写法还可以用在社区属性中，如 [(64512, 100..200)]，在社区属性中还可以用通配符，如 [(1, 2, *)]。 前缀的集合中的范围写法较为复杂，[prefix{low, high}]，prefix 是一个用于匹配的前缀，low 和 high 两个值限制了它的 CIDR 长度。[192.168.1.0/24{16,30}] 表示的是包含或被包含于 192.168.1.0/24 且 CIDR 在 16-30 之间的前缀，例如 192.168.0.0/20 和 192.168.1.0/29 均属于这个集合，而 192.168.233.0/24 不属于。这样子的写法可能过于麻烦，所以 BIRD 中也使用加号和减号提供了两种便捷的写法，如 [2001:db8:10::/44+, 2001:db8:2333::/48-] 则等价于 [2001:db8:10::/44{44,128}, 2001:db8:2333::/48{0,48}]。 在 BIRD 中还有一类特殊的变量类型，他们都是列表，bgppath（AS Path，路由的 bgp_path 属性）、clist（BGP Community 列表，路由的 bgp_community 属性）、eclist（BGP Extended Community 列表，路由的 bgp_ext_community 属性）、lclist（BGP Large Community 列表，路由的 bgp_large_community 属性）都是这类变量，他们的操作有非常特殊的用法。下面的代码展示了 bgppath 的用法。clist/eclist/lclist 与之类似，但是它们只能使用其中的 empty、len、add、delete、filter。 function foo() bgppath P; bgppath P2; { print "path 中第一个元素是", P.first, "，最后一个元素是", P.last; # 第一个元素可以认为是邻居的 ASN，最后一个元素是宣告这条路由的 ASN # 这两个在 P 中没有元素的时候是 0 print "path 的长度是", P.len; if P.empty then { print "path 为空"; } P.prepend(233); # 在 path 的第一个位置插入元素 P.delete(233); # 删除 path 中所有等于 233 的元素 P.delete([64512..65535]); # 删除 path 中所有属于集合 [64512..65535] 的元素 P.filter([64512..65535]); # 只在 path 中留下集合 [64512..65535] 中出现的元素 # 如果不想改变 P，可以使用下面这样的方法将操作后的结果存入 P2 P2 = delete(P, 233) P2 = filter(P, [64512..65535]) } 变量只能定义在函数或过滤器的最开头（左花括号外面），关于变量类型的更详细信息，请移步官方文档相关部分。 操作符 在 BIRD 中有很多常见的操作符。如 +、-、*、/、() 这些基本的算数操作符，有等于 a = b、不等于 a != b、大于 a > b、大于等于 a >= b、小于 a < b、小于等于 a <= b 这些比较符，有与 &&、或 ||、非 ! 这三种逻辑操作符。还有 ~ 和 !~ 这两种判断包含或者不包含的操作符。包含操作符的用法写在附录 3 中。 分支 过滤器和函数中的语句都是顺序执行的。同时支持 case 和 if 两种分支语句。在 BIRD 中是不支持循环的。 if 的写法如下： if 6939 ~ bgp_path then { # 只要 AS Path 中有 6939 bgp_local_pref = 233; # 就将这条路由的 Local Preference 调为 233 } else { bgp_local_pref = 2333; # 否则设为 2333 } case 的写法如下： case arg1 { 2: print "two"; print "I can do more commands without {}"; # ^ case 不需要花括号就能在一个分支中写下更多语句。 3..5: print "three to five"; else: print "something else"; } 在 BIRD 中，if 和 case 的写法均与常见编程语言略有不同。 3 过滤！过滤！ 为什么讲完基本语法就开始讲过滤，因为过滤是一个非常重要的内容！请记住，永远不要对过滤不上心，不漏路由（指把不应该发出去的路由发出去）是最重要的！ 首先要知道的是，当你敲下 export 的时候，操作的对象是整个路由表，只要是路由表里有的（不管是 static 协议、BGP 协议，还是 OSPF 协议）都是会被发出去的，所以过滤做的事情就是挑选路由表中应该发出去的表，把不应该发出去的表挡在自己的路由器内。import 同理，只不过是从外面挑选什么路由应该被导入到我们的路由器上。 过滤器 filter 我们先来看一个过滤器的例子。 filter sample_import int set reject_origin_asn; { # ^ 定义一个整数集 reject_origin_asn = [64512..64519, 65500]; # ^ 这里的变量定义没啥意义，只是给大家了解可以定义局部变量 if net_len_too_long() then reject; # 调用函数 net_len_too_long，如果为真就 reject if bgp_path.last ~ reject_origin_asn then { reject; # 不接收上面定义的 reject_origin_asn 里面任何一个 ASN 宣告的路由 } accept; } 过滤器是用在导入和导出路由的时候的。过滤器会对每一条路由执行一次，同时该路由的所有属性都可以直接在过滤器中被使用（如例子中的 bgp_path），当前路由的可编辑属性（如 bgp_local_pref、bgp_path）也可以在这里被编辑。对于可编辑属性，如果是导入时候的过滤，那么修改后的属性会随着路由存入 BIRD 内的路由表；如果是导出时的过滤，那么修改后的属性只会传播给导出对象，BIRD 路由表中的属性不会被修改。顺便一提，属性还需要是可传播的，这样才能传播给导出对象，导出时的修改才有意义。 reject 语句和 accept 语句决定了这条路由是被拒绝还是被接受。如果导入路由的时候拒绝或接受，那么路由会被丢弃、或存入路由表；如果导出的时候拒绝或接受，那么这条路由不会被导出或会被导出。只要 reject 或者 accept 被触发，过滤器就会立刻退出，不执行之后的任何代码。 过滤器可以在协议实例的 import filter 或 export filter 选项中使用，如果一个过滤器只在一个地方用，我们也可以使用“匿名过滤器”的形式，如下面的例子所示。 protocol bgp { /* 略去一些内容 */ ipv6 { import filter sample_import; # 使用名为 sample_import 的过滤器 export filter { if net ~ [2001:db8::/32{40,48}] then accept; reject; }; # 一个匿名的过滤器，只导出 2001:db8::/32 下面 CIDR 为 40 到 48 的前缀 }; } 函数 function 下面是一个函数的例子。 function net_len_too_long(int hello) # 这里定义的参数 hello 没啥意义，只是让大家知道可以定义参数，不定义括号内留空即可 int world; { # ^ 这里的变量定义没啥意义，只是让大家知道可以定义局部变量 case net.type { NET_IP4: return net.len > 24; # IPv4 CIDR 大于 /24 为太长 NET_IP6: return net.len > 48; # IPv6 CIDR 大于 /48 为太长 else: print "net_len_too_long: unexpected net.type ", net.type, " ", net; return false; } } 函数可以减少在过滤器之间的冗余代码，可以在过滤器中被调用。和过滤器中一样，当前路由的所有属性都可以直接在函数中被使用（如例子中的 net），可编辑属性可以在这里被编辑。return 可以返回任何值或不返回值，省略 return 也是可以的。return 后该函数即退出，不会执行之后的任何代码。这个值可以在过滤器中使用，如上面过滤器中的例子，就调用了 net_len_too_long() 来判断 CIDR 是否太长。 除了在过滤器中引用，在协议实例中也可以使用匿名过滤器来调用函数，例子是 export where net_len_too_long();。此时函数返回值必须为 bool 类型，即 true 和 false。 过滤器写什么呢 写过滤器的时候我们首先需要知道，我们要怎么发路由。在自己网络的内部，我们想怎么发都行，就算是直接写 export all; 也可以。但是和其他网络之间的 BGP session，我们就必须写好过滤器，只把正确的路由发送出去。 下面介绍几个常见的场景： 向上游（transit）导出 只导出自己和下游（如果有）的路由 向对等伙伴（peer）导出 只导出自己和下游（如果有）的路由 向下游（customer）导出 导出自己、上游、对等伙伴的路由 这种情况中，自己就是下游的上游（transit） 路由属性 上文提到，当前路由的所有属性都可以直接在过滤器和函数中被使用，那么有什么属性比较常用呢？ net，类型为 prefix，就是“当前路由” preference，类型为 int proto，类型为 string，是协议实例的名字 source，它的取值是这几个系统内置的常量 RTS_DUMMY, RTS_STATIC, RTS_INHERIT, RTS_DEVICE, RTS_STATIC_DEVICE, RTS_REDIRECT, RTS_RIP, RTS_OSPF, RTS_OSPF_IA, RTS_OSPF_EXT1, RTS_OSPF_EXT2, RTS_BGP, RTS_PIPE, RTS_BABEL，代表路由来源的协议类型 还有一些常用的属性（如 bgp_path）是协议定义的，请看协议相关的内容 如果想看更多的请看官方文档的相关内容。 4 协议实例 一个协议实例（protocol instance）定义了使用什么协议、什么参数进行路由交换。BIRD 所有支持的协议和相关的参数，都可以在官方文档相关内容中找到。 定义一个协议实例使用这样的语法： protocol <协议> [实例名] { /* 参数们 */ <频道> { /* 频道参数 */ } } 实例名是可以省略的，当然为了自己方便还是写上名字好。那么频道是什么呢？ 频道 channel 简单的理解的话，一个频道就是一个网络协议，常用的频道为 ipv4 和 ipv6。当然 BIRD 支持的远不止这些。我们通常像这样来写： protocol some_proto { /* 略 */ ipv6 { export filter some_filter1; import filter some_filter2; # 下面这两个是可选项，默认是不做限制的 export limit 100 action disable; # 导出的路由数量多于 100 后自动停止该协议，避免传出过多路由（往往这个时候是漏路由了） import limit 100 action restart; # 导入的路由数量多于 100 后自动重启该协议，限制对端传入的路由数 }; } 一般的，频道默认 export none; 和 import all;，所以当你用在 static 等协议中，可以不明确写出 filter。 频道的具体参数请参照官方文档。 协议 protocol 这里简单介绍 BGP 协议、static 协议（静态路由）、direct 协议的常用参数，很多用不到的内容都被省略了，如果需要了解更多请查阅官方文档的 BGP 协议部分、static 协议部分和 direct 协议部分。 static 协议 一个 static 协议中可以描述数条路由。常用的配置如下： protocol static { ipv6; # 启用 ipv6 channel，否则不会收集 IPv6 路由 route 2001:db8:100a::/48 reject; # 定义一条路由 2001:db8:100a::/48 为 reject/unreachable route 2001:db8:100b::/48 via "eth0"; # 定义一条路由 2001:db8:100b::/48 的下一跳为 eth0 route 2001:db8:100c::/48 via 2001:db8:eeee::1; # 定义一条路由 2001:db8:100c::/48 的下一跳为 2001:db8:eeee::1 } direct 协议 direct 协议用来直接从内核的网络设备上获取地址和路由，并将其导入到 BIRD 的路由表中。 protocol direct { ipv4; # 启用 ipv4 channel，否则不会收集 IPv4 路由 ipv6; # 启用 ipv6 channel，同上 interface "eth*", "tun*"; # 如果不写这个参数，那么 BIRD 会默认使用所有网络设备 # 参数后面用逗号分隔不同的匹配字符 # “*”表示通配符 # 更详细的参数参照官方文档 # https://bird.network.cz/?get_doc&v=20&f=bird-3.html#proto-iface } BGP 协议 protocol bgp { local as 65550; # 指定自己的 ASN 为 65550 source address 2001:db8:ffff::6:5550; # 指定 BIRD 发起 BGP 会话的源地址 neighbor 2001:db8:ffff::6:4501 as 64501; # 指定对端的 ASN 为 64501，IP 为 2001:db8:ffff::6:4501 # 如果 ASN 和 local as 相同，那么 BIRD 会自动认为这是一个 iBGP，否则是 eBGP # i 表示 internal（内部），e 表示 external（外部） direct; # eBGP 默认启用可以不写 # iBGP 如果是直接连接的可以写这个来避免 multihop 被指定 multihop 2; # 如果自己端到对端不是直接连接的，需要指定 multihop 的值为到达对端所需要经过的跃点数 # eBGP 默认不启用，而是 direct # iBGP 默认启用 # 例如： # 在 Vultr 上配置的时候，对端并不是直接连接的，而是需要经过一个网关，那么这时候需要指定 multihop 为 2 # 如果是 HE 或者别的服务商提供的隧道，一般都是直接连接的，那么这时候就不需要这个参数了 # 具体的跃点数使用 traceroute 追踪对端即可 password "Aa1"; # 如果和对端约定了密码，在这里配置约定好的密码，否则不用写 graceful restart; # 配置 BGP 的 graceful restart # 如果对端因为网络抖动或暂时崩溃而暂时下线，会导致所有传入路由瞬间消失 # 为了避免这种情况下数据转发中断，才有 graceful restart # 建议打开 # rr client; # 自己作为路由反射器（Route Reflector，RR），对端为被反射的路由器 # 此项请在阅读路由反射器相关的文献资料后使用 <频道> {}; } 另外提一笔 iBGP，iBGP 是一个自治系统内部的 BGP 会话。为了建设全球性的网络，我们可能会选择通过 iBGP 的方式交换各个路由器收到的路由，以获得最佳的上网体验。在 BGP 中，为了避免产生环路，从 iBGP 收到的路由表不会传播给别的 iBGP，这就要求网络内所有 iBGP 路由之间必须采用全连接的形式（即每两台路由器都要起 iBGP 会话）。 一般 BGP Player 的网络一般不会这么大，4 台路由器也就是 6 个会话，并不是多么麻烦的事情。但是在较大的网络，比如 10 台路由器就要配置 45 个会话，这样配置和维护起来就非常吃力了。为了避免产生额外的连接成本，可以使用路由反射器（Route Reflector，RR）。另外一种情况是，如果某些路由器不方便加入全连接，也可以使用 RR 的形式。因为大部分玩家并不会有如此大型的网络，此处只是提到 RR，具体内容还请有兴趣的自行检索资料学习。 在附录 4 中介绍了一些常见情况下的协议实例配置样例。 BGP 协议中的过滤器可以使用这些属性： bgp_path AS Path bgp_local_pref Local Preference bgp_med Multiple Exit Discriminator bgp_community BGP Community bgp_large_community BGP Large Community 等 这么看起来，只要配置多个实例就很容易产生很多重复的代码，例如 BGP 实例中的 local as 参数等，我们这时候就可以用协议模板减少重复。 协议模板 template 在协议模板中定义的参数将被协议实例继承，如果模板中的参数在实例中再一次被定义，那么模板中的参数将被覆盖。 在下面这个例子中我们就定义了这样一个协议模板 tpl_ibgp，在 ibgp_123 实例中用 from tpl_ibgp 继承模板。然后覆盖了 IPv6 channel 中的 import 参数。 template bgp tpl_ibgp { graceful restart; local as 65550; med metric; direct; # 如果 iBGP 是直接连接的（比如使用 GRE 隧道直接连接）就需要写这个，否则需要针对 session 指定 multihop ipv4 { # 对于初学者来说，路由器间两两打隧道、将 multihop 关掉并将 next hop 改为当前路由器的操作会使 iBGP 更方便，也更好理解 next hop self; gateway direct; import all; export all; }; ipv6 { next hop self; # 同上 gateway direct; import all; export all; }; } protocol bgp ibgp_123 from tpl_bgp { interface "tun1"; neighbor fe80::1 as 65550; ipv6 { import none; } } 5 杂七杂八的东西 BIRD 配置还有一些必要的内容，所以放在这里一并列出。 更多配置 # 启用日志并记录到 syslog，或者文件 log syslog all; # log "/var/log/bird.log" all; # 路由器识别号，32 位整数 # 一般是全球唯一的，所以建议使用自己的公网 IPv4 地址 router id 198.51.100.1; # device 协议必须有，否则 BIRD 不会自动从内核获取比如网络接口的信息，direct 协议和寻找下一跳的时候就挂了 protocol device {} # kernel 协议用于导出路由表到内核，这里列出了常见的配置，详细的请看官方文档 # https://bird.network.cz/?get_doc&v=20&f=bird.html#ss6.6 # IPv4 的 kernel 协议，用于导出 IPv4 路由表到内核用于数据转发 protocol kernel { ipv4 { export all; }; } # IPv6 的 kernel 协议，用于导出 IPv6 路由表到内核用于数据转发 protocol kernel { ipv6 { export all; }; } 控制台 BIRD 提供了一个控制台工具，它叫做 birdc。 # birdc BIRD 2.0.7 ready. bird> ? quit Quit the client exit Exit the client help Description of the help system show ... Show status information dump ... Dump debugging information eval <expr> Evaluate an expression echo ... Control echoing of log messages disable (<protocol> | "<pattern>" | all) [message] Disable protocol enable (<protocol> | "<pattern>" | all) [message] Enable protocol restart (<protocol> | "<pattern>" | all) [message] Restart protocol reload <protocol> | "<pattern>" | all Reload protocol debug ... Control protocol debugging via BIRD logs mrtdump ... Control protocol debugging via MRTdump files restrict Restrict current CLI session to safe commands configure ... Reload configuration down Shut the daemon down graceful restart Shut the daemon down for graceful restart bird> configure 可以在修改配置之后让 BIRD 热重载配置文件，避免重启整个 BIRD。 show protocols 可以用来查询协议实例，它默认会显示所有的协议实例，后面加上 all 则会显示出所有细节信息。如果后面再加上协议实例的名字或者用来匹配的模式，那么只会显示具体的一个或几个协议实例的信息，就像 show protocols all "bgp_*" 会显示出所有名字开头是“bgp_”的协议实例的详细信息。 show route 可以用来查询 BIRD 中的路由表，默认显示的都是简略信息，后面加上 all 就会显示出所有细节信息。它可以在后面加上 for <前缀或 IP> 来查询所有包含这个前缀或 IP 的路由，当然如果不加 for 就是完全匹配了。使用 export <协议实例名> 可以查看在指定的协议实例中导出的路由。还可以使用 where <语句> 来查找使 where 后面语句为真的路由。 birdc 的命令在不引起歧义的情况下可以不完整写出，比如 s p 等价于 show protocols。其他功能和命令大家可以自行摸索。 当然直接在 birdc 后面追加命令，也是可以执行的。像下面这样。 # birdc show proto BIRD 2.0.7 ready. Name Proto Table State Since Info device1 Device --- up 2020-06-18 kernel1 Kernel test4 up 2020-06-18 static1 Static test4 up 2020-06-18 ospf1 OSPF test4 up 2020-06-18 Alone 6 实践中学习 场景 我们维护一个自治系统 AS65550，它拥有一个 IPv6 前缀 2001:db8:1000::/44、三台路由器。 这个自治系统将要在一台路由器（Router ID 10.0.0.1）上与 AS64501、AS64502 互联。AS64501 是上游（transit），AS64502 是一个对等伙伴（peer）。 这台路由器上通过 GRE 隧道连接到了路由器 A，隧道的名字是 tun-a。当前路由的 IPv6 地址为 2001:db8:eeee:a::1/64，路由器 A 的为 2001:db8:eeee:a::a/64。 这台路由器上通过 GRE 隧道连接到了路由器 B，隧道的名字是 tun-b。当前路由的 IPv6 地址为 2001:db8:eeee:b::1/64，路由器 B 的为 2001:db8:eeee:b::b/64。 我们的自治系统和 AS64501 在同一子网，我们的地址是 2001:db8:ffff:1::6:5550/64，对端是 2001:db8:ffff:1::6:4501/64。 我们的自治系统和 AS64502 在同一子网，我们的地址是 2001:db8:ffff:2::6:5550/64，对端是 2001:db8:ffff:2::6:4502/64。 AS64501 有如下的 BGP Community 规则： 如果我们宣告的路由有 (0, a) 的 Community，那么不会被宣告到 ASN 为 a 的路由器 AS64501 有如下的 BGP Large Community 规则： AS64501 从 ASN=a 处收来的路由，会包含 (64501, 1, a) 的 Large Community 我们想要达到以下目标： eBGP 过滤器只宣告 2001:db8:1000::/44 下 CIDR 长度在 44-46 之间的 static 协议路由 宣告 2001:db8:1000::/44 配置上游 AS64501 的 BGP session 不让上游 AS64501 将我们的路由发到 AS65510（即添加 Community (0, 65510) 到导出的路由） 使用 community 不接受上游 AS64501 发来的 AS65511 的路由（即拒绝所有 Large Community 包括 (64501, 1, 65511) 的路由） 配置对等 AS64502 的 BGP session 将 AS64502 收到的路由表的 Local Preference 均设为 1000 使用静态路由将 2001:db8:100a::/48 指向路由器 A 与路由器 B 配置 iBGP session，并交换所有路由表 配置 下面的配置只是代表笔者的配置习惯。 log syslog all; router id 10.0.0.1; define LOCAL_ASN = 65550; protocol device {} protocol kernel { ipv6 { export all; }; } # 公网上约定俗成的最小前缀长度是 24（IPv4）和 48（IPv6），所以要在导出的时候过滤 function net_len_too_long(){ case net.type { NET_IP4: return net.len > 24; # IPv4 CIDR 大于 /24 为太长 NET_IP6: return net.len > 48; # IPv6 CIDR 大于 /48 为太长 else: print "net_len_too_long: unexpected net.type ", net.type, " ", net; return false; } } # 目标 1 function bgp_export() { if net_len_too_long() then return false; if source != RTS_STATIC then return false; if net !~ [2001:db8:1000::/44{44,46}] then return false; return true; } # 目标 2 protocol static { ipv6; route 2001:db8:1000::/44 reject; } # 目标 8 protocol static { ipv6; route 2001:db8:100a::/48 via "tun-a"; } # 使用模板减少重复代码 template bgp tpl_bgp { graceful restart; local as LOCAL_ASN; ipv6 { import where !net_len_too_long(); export where bgp_export(); }; } template bgp tpl_ibgp from tpl_bgp { med metric; direct; ipv6 { next hop self; gateway direct; import all; export all; }; } # 目标 3 protocol bgp bgp_as64501 from tpl_bgp { source address 2001:db8:ffff:1::6:5550; neighbor 2001:db8:ffff:1::6:4501 as 64501; ipv6 { import filter { if net_len_too_long() then reject; if (64501, 1, 65511) ~ bgp_large_community then reject; # 目标 5 accept; }; export filter { if !bgp_export() then reject; bgp_community.add((0, 65510)); # 目标 4 accept; } }; } # 目标 6 protocol bgp bgp_as64502 from tpl_bgp { source address 2001:db8:ffff:2::6:5550; neighbor 2001:db8:ffff:2::6:4502 as 64502; ipv6 { import filter { if net_len_too_long() then reject; bgp_local_pref = 1000; # 目标 7 accept; }; # export 使用了 template 中的默认值 }; } # 目标 9 protocol bgp ibgp_b from tpl_ibgp { source address 2001:db8:eeee:b::1; neighbor 2001:db8:eeee:b::b as LOCAL_ASN; } 7 结语 希望大家能在阅读之后，对如何使用 BIRD 配置 BGP 有了点概念，并能做出正确的配置。当然更希望大家能不止步于此，多学习计算机网络相关的基础知识，或阅读 BIRD 官方文档学到更全面的配置。 特别感谢 foobar 院的 twd2 对本文的审读与修改，感谢 foobar 院的 Martian、快乐 BGP 群的 alanyhq 试读本文并提供意见，感谢 ZX、Anillc、小黄蜂、pansz 对本文内容的完善与修正。 本文写成较快，虽然也有多人试读、审校，难免会有遗漏，相关英文术语的翻译也会有不合适的地方，如有问题、意见或者建议，请在 issue 中提出。 （如果有需要申请 ASN 资源的请联系我的邮箱 soha@lohu.info。） 附录 1 BIRD 全局选路规则 按照从上到下的顺序执行。如果某条的值相同，那么执行下一条。这里描述的是 BIRD 实现的选路规则，其他路由软件可能不同。 路由的 preference 高者优先 协议实例的 preference 高者优先 如果路由来源是相同协议，如 BGP 和 BGP 比较，参照该协议的选路规则（如附录 2 的 BIRD BGP 协议选路规则） 如果路由来源是不同协议，如 BGP 和 OSPF 比较，这个是未定义行为，最优的选择方式不一定 附录 2 BIRD BGP 协议选路规则 按照从上到下的顺序执行。如果某条的值相同，那么执行下一条。这里描述的是 BIRD 实现的选路规则，其他路由软件可能不同。 Local Preference（bgp_local_pref） 高者优先 AS Path（bgp_path） 短者优先 Origin 属性（bgp_origin）中，IGP（ORIGIN_IGP）优先于 EGP（ORIGIN_EGP）优先于 incomplete（ORIGIN_INCOMPLETE） MED（bgp_med，Multiple Exit Discriminator）值小者优先 从 eBGP 收到的路由优先于 iBGP 收到的路由 到边界路由器的内部距离小者优先 宣告该路由的 Router ID 小者优先 附录 3 BIRD 的包含操作符 BIRD 的包含 ~ 和不包含 !~ 操作符能用于下表中所示的类型。~ 或者 !~ 左边的东西叫做左操作数（是的，它不一定是数字，也可以是其它类型），右边的东西叫做右操作数。 左操作数类型 右操作数类型 样例 说明 bgppath bgpmask bgp_path ~ [= * 64512 64513 * =] bgp_path 符合右边描述的模式则为真 int bgppath 64512 ~ bgp_path 左边被包含于右边则为真 pair/quad/ip4 clist (123, 456) ~ bgp_community 左边被包含于右边则为真，pair、quad、ip4 都是 32 位的，所以它们等价 ec eclist (rt, 10, 3) ~ bgp_ext_community 左边被包含于右边则为真 lc lclist (4842, 0, 0) ~ bgp_large_community 左边被包含于右边则为真 string string proto ~ "bgp_*" 左边的字符串符合右边的模式（类 shell）就为真 ip prefix 1.1.1.1 ~ 1.0.0.0/8 IP 被包含于某前缀则为真 prefix prefix 1.1.1.0/24 ~ 1.0.0.0/8{16,24} 左边的前缀被包含于右边的前缀则为真 prefix prefix set 1.1.1.0/24 ~ [1.0.0.0/24, 1.1.0.0/22] 左边的前缀被包含于右边任意一个前缀则为真 path int set bgp_path ~ [233, 2333, 64512] 左右两边交集非空即为真 clist pair set bgp_community ~ [(0, 6939), (1, 2333)] 左右两边交集非空即为真 eclist ec set bgp_ext_community ~ [(rt, 1, 30), (ro, 2, *)] 左右两边交集非空即为真 lclist lc set bgp_large_community ~ [(1, 2, 100..233)] 左右两边交集非空即为真 附录 4 常用的各种配置 公网上约定俗成的最小前缀 公网上约定俗成的最小前缀长度是 24（IPv4）和 48（IPv6）。 function net_len_too_long(){ case net.type { NET_IP4: return net.len > 24; NET_IP6: return net.len > 48; else: print "net_len_too_long: unexpected net.type ", net.type, " ", net; return false; } } Bogon Prefixes and ASNs 有一些特殊用途的、私有的、被保留的 IP 地址块或者 ASN 是不适宜出现在公网的，所以这里列出了所有“bogon”的 IP 前缀和 ASN，供大家写过滤器的时候使用。 define BOGON_ASNS = [ 0, # RFC 7607 23456, # RFC 4893 AS_TRANS 64496..64511, # RFC 5398 and documentation/example ASNs 64512..65534, # RFC 6996 Private ASNs 65535, # RFC 7300 Last 16 bit ASN 65536..65551, # RFC 5398 and documentation/example ASNs 65552..131071, # RFC IANA reserved ASNs 4200000000..4294967294, # RFC 6996 Private ASNs 4294967295 # RFC 7300 Last 32 bit ASN ]; define BOGON_PREFIXES_V4 = [ 0.0.0.0/8+, # RFC 1122 'this' network 10.0.0.0/8+, # RFC 1918 private space 100.64.0.0/10+, # RFC 6598 Carrier grade nat space 127.0.0.0/8+, # RFC 1122 localhost 169.254.0.0/16+, # RFC 3927 link local 172.16.0.0/12+, # RFC 1918 private space 192.0.2.0/24+, # RFC 5737 TEST-NET-1 192.88.99.0/24+, # RFC 7526 deprecated 6to4 relay anycast. If you wish to allow this, change `24+` to `24{25,32}`(no more specific) 192.168.0.0/16+, # RFC 1918 private space 198.18.0.0/15+, # RFC 2544 benchmarking 198.51.100.0/24+, # RFC 5737 TEST-NET-2 203.0.113.0/24+, # RFC 5737 TEST-NET-3 224.0.0.0/4+, # multicast 240.0.0.0/4+ # reserved ]; define BOGON_PREFIXES_V6 = [ ::/8+, # RFC 4291 IPv4-compatible, loopback, et al 0064:ff9b::/96+, # RFC 6052 IPv4/IPv6 Translation 0064:ff9b:1::/48+, # RFC 8215 Local-Use IPv4/IPv6 Translation 0100::/64+, # RFC 6666 Discard-Only 2001::/32{33,128}, # RFC 4380 Teredo, no more specific 2001:2::/48+, # RFC 5180 BMWG 2001:10::/28+, # RFC 4843 ORCHID 2001:db8::/32+, # RFC 3849 documentation 2002::/16+, # RFC 7526 deprecated 6to4 relay anycast. If you wish to allow this, change `16+` to `16{17,128}`(no more specific) 3ffe::/16+, 5f00::/8+, # RFC 3701 old 6bone fc00::/7+, # RFC 4193 unique local unicast fe80::/10+, # RFC 4291 link local unicast fec0::/10+, # RFC 3879 old site local unicast ff00::/8+ # RFC 4291 multicast ]; function is_bogon_prefix() { case net.type { NET_IP4: return net ~ BOGON_PREFIXES_V4; NET_IP6: return net ~ BOGON_PREFIXES_V6; else: print "is_bogon_prefix: unexpected net.type ", net.type, " ", net; return false; } } function is_bogon_asn() { if bgp_path ~ BOGON_ASNS then return true; return false; } 与 Vultr 的 BGP session 与 Vultr 建立 BGP 会话较为麻烦，因为我们到 Vultr 的 BGP 路由器不是直接连接的，需要配置 multihop，以及告诉 BIRD 如何到达那个路由器，否则会找不到 next hop 在哪里。Vultr 内部也使用着大量的私有 ASN，我们也有必要将私有 ASN 给处理掉，不然会在一些时候出问题。 protocol bgp bgp_vultr_6 from tpl_bgp { source address xxx; # 请修改 neighbor 2001:19f0:ffff::1 as 64515; # 请核对 multihop 2; password "2333"; # 请修改 ipv6 { /* 请自行写好过滤器 */ }; } protocol static { ipv6; # 告诉 BIRD，Vultr 路由器的下一跳在哪里 route 2001:19f0:ffff::1/128 via fe80::xxxx%ens3; # 这个 via 自己找，一般看内核的 v6 路由表就能找到 # ens3 是 Vultr 的网卡，%ens3 的意思是 ens3 网卡上的 link-local 地址 } # 这个函数可以删除 Vultr 发来的表中各个路由 AS Path 前面的私有 ASN，并替换为 20473 function preprocess_vultr_bgp_path() { bgp_path.delete(BOGON_ASNS); # ^ 目的是删除 Vultr 传过来的私有 ASN，为了实现简单， # 这里删除 AS Path 里面所有私有 ASN # （BIRD 不能只删除特定位置的，不过私有 ASN 本身不应该出现在公网所以直接全删了也没关系） if bgp_path.first != 20473 then bgp_path.prepend(20473); } 与 HE TunnelBroker 的 BGP session protocol bgp bgp_he { graceful restart; local as 64512; # 请修改 source address 2001:470:aa:aa::2; # 请修改 neighbor 2001:470:aa:aa::1 as 6939; # 请修改 ipv6 { /* 请自行写好过滤器 */ }; }
“教育系统很多bug都来自业务逻辑丢前端，以及表单数据处理不严格。” —— Soha King 0x00 无关的背景介绍 又到了一学期一度的选修课选课时间！已经高二下半学期的Soha将迎来最后一期选修课。高一刚入学的时候选了金老师的某课程，然而之前那个学期并没有开课。这一次听说我们班大部分同学选了这节课，我心里也有点痒。（毕竟这是我们的信息技术任课老师，超可爱） 0x01 问题的重现过程 我尝试选择金老师的这门课程。 尴尬……都怪学校当年不开课。 我顺手在“选课”上右键审查元素。 很强，直接把这种东西丢在前面。然后我看了看Network……再点击选课后并没有任何的请求。不过想想也是，除了一个confirm后的doPostBack，没有其他的xhr请求了。 于是我把onclick里面的语句修改了一下（其实直接删掉也没毛病）。 然后再点一次选课。 轻松加愉快。 0x02 Soha的解决方案 虽然没人会处理这种bug 后端加个判断，是否选过这门课。
刚才抓了三段浙江电信插广告的脚本
有空研究一下
今天本来想调一下原来的bootstrap的ui的，结果被我调！炸！了！ 索性一火重新用Materialize写了一个看起来像是Material Design的UI（因为我懒得去看MD的规范） 顺便开启了安全的HTTPS访问。 博客改名了，也麻烦一下各位朋友把友链的名称改成潜入海底。 然后之前那个博客程序也是很老的了，是我在Typecho 0.x（几来着？我忘了）的基础上自己二次开发的。 现在我把博客程序换成了Typecho官方最新版本。 欢迎来访～
新年到了，身边的朋友都在发红包，几个it圈子里的朋友也做出了找红包的页面。我也花了十几分钟，做了一个找红包的页面。 http：//hb.lohu.info/redBag.php 由于红包有效期是24小时，所以将在2016年2月8日晚上六点(CST)左右公布答案 下面公布答案（解出来的人不多，早知道红包发多一点了） 第一个 点击我要红包。咦？怎么没有红包？好的，我们来看看源码。 （略） 我要红包 （中略） function getHongBaoKouLing(){ $.ajax({ type: 'POST', url: '/hongBao.php', data: { 'year': '2015' }, success: function(data){ console.log(data); alert(data.return); }, dataType: "json" }); } $("#getButton").bind('click',function(){ getHongBaoKouLing(); }); （略） 看起来点击按钮就会调用getHongBaoKouLing()这个函数，这个函数显然是一个POST请求……一眼戳中'year':'2015'，今年明明是2016年了呀！ 到console里重新定义这个函数 function getHongBaoKouLing(){ $.ajax({ type: 'POST', url: '/hongBao.php', data: { 'year': '2016' }, success: function(data){ console.log(data); alert(data.return); }, dataType: "json" }); } 现在再点击按钮，就可以得到8位口令啦！ 0.10元1个，共50个，领走16个（有人说我这么没诚意orz） 第二个 第二个可能稍微有点难找。很多人想到了看cookies。的确有一个名为hongBao的cookie。 但是只有4位数，和我们想要的8位数对不上啊。会藏在哪里呢？这时候回到elements看源码，css里面原来也藏着一点东西！有几个注释…… 分别是8,1,5。还少一位……在最后一个注释的position前面还有一个2！按顺序拼凑加上cookies里面的4位。就是我们要的口令！ 0.66元，共16个，领走3个 第三个 第三个要找到真的是很需要细心的，也很简单。只要使用我提供的入口页面http：//hb.lohu.info/redBag.php再盯牢Network就行了。（没找到的同志们，不会觉得我给的地址是redBag.php却跳转到index.php很奇怪吗？） redBag.php这个回应header里有一个Set-C00kie，第一个=后面显然是base64加密的一段东西。解密，得到8位口令。 0.88元，共10个，领走2个 再次祝大家新年快乐！
-1 保持更新 请注意，本文中自行编译的方法已经不再被建议使用。第一点，自己编译可能会遇到一些奇怪的事情，同时也会浪费时间；二则是现在已经有了更好的解决方案。 OpenWRT 的官方 x86 版本现在自带了 Hyper-V 的支持，在官方页面直接下载 x86/64 版本的磁盘映像就可以直接使用第一代 Hyper-V 虚拟机挂磁盘启动了。因为官方给的磁盘映像是仅 MBR 的，如果想要使用第二代虚拟机（UEFI 模式）启动，请参考《构建 UEFI 启动的 OpenWrt 磁盘映像》来构建一个 UEFI 启动的磁盘映像。 Project LEDE他们说自己是Reboot了OpenWRT社区。而且很多用OpenWRT的也都升级到了LEDE。LEDE的x86_64版本是直接支持Hyper-V的，可以直接下载官网预编译版本而不用自己编译。因此建议大家使用LEDE的x86_64版本，本文中的方法仅用于学习借鉴。谢谢。 附上一张LEDE的x86_64的官方预编译版本在Hyper-V上的表现。 0 为什么我会折腾这个 自己在国内放了台服务器，线路什么的都是自己拾掇的。因为丢在自己家里，那条线路也是肩负着我家出网的任务。一时兴起乘着续费线路的机会，打算试试软路由，就搞了个软路由。 首先我根据一篇文章，Hyper-v 安装 OpenWrt_x86 版教程(03.14更新) 在Hyper-V上装好了OpenWRT。 正当我兴奋的时候，我发现测速结果不太对劲。上传下载都只有20Mbps左右。和我的100Mbps相差甚远。看起来像是OpenWRT不支持Hyper-V的网卡，我一直搞不懂这是为什么，后来我闲逛的时候看到了一个贴子：Hyper-V安装Open-WRT的问题。于是我大概知道了为什么我的Hyper-V上的OpenWRT速度会如此坑爹。（看起来我的情况还比这个发帖人好一些哈哈哈） 我马上开始着手倒腾一个适合Hyper-V的OpenWRT。 1 准备工作 首先先get一个Linux Server，然后 git clone git://git.openwrt.org/15.05/openwrt.git 注意一些依赖包，我准备安装了以下包，如果还有别的依赖没安装，make前是会进行依赖检查的 sudo apt-get install build-essential openssl unzip libncurses5-dev zlib1g-dev libssl-dev subversion make过程中经常会从网上下载需要的东西，大部分来自aws，情况大家都是知道的，所以自己准备好proxychains或者vpn等避免下载失败所导致的make中止。 10 修改 修改/package/kernel/linux/modules/virtual.mk，在最后加入下面的内容，添加让Linux内核支持Hyper-V的驱动。 # # Hyper-V Drivers. # define KernelPackage/hyperv-balloon SUBMENU:=$(VIRTUAL_MENU) DEPENDS:=@(TARGET_x86||TARGET_x86_64) TITLE:=Microsoft Hyper-V Balloon Driver KCONFIG:= \ CONFIG_HYPERV_BALLOON \ CONFIG_HYPERVISOR_GUEST=y \ CONFIG_PARAVIRT=n \ CONFIG_HYPERV=y FILES:=$(LINUX_DIR)/drivers/hv/hv_balloon.ko AUTOLOAD:=$(call AutoLoad,06,hv_balloon) endef define KernelPackage/hyperv-balloon/description Microsofot Hyper-V balloon driver. endef $(eval $(call KernelPackage,hyperv-balloon)) define KernelPackage/hyperv-net-vsc SUBMENU:=$(VIRTUAL_MENU) DEPENDS:=@(TARGET_x86||TARGET_x86_64) TITLE:=Microsoft Hyper-V Network Driver KCONFIG:= \ CONFIG_HYPERV_NET \ CONFIG_HYPERVISOR_GUEST=y \ CONFIG_PARAVIRT=n \ CONFIG_HYPERV=y FILES:=$(LINUX_DIR)/drivers/net/hyperv/hv_netvsc.ko AUTOLOAD:=$(call AutoLoad,35,hv_netvsc) endef define KernelPackage/hyperv-net-vsc/description Microsoft Hyper-V Network Driver endef $(eval $(call KernelPackage,hyperv-net-vsc)) define KernelPackage/hyperv-util SUBMENU:=$(VIRTUAL_MENU) DEPENDS:=@(TARGET_x86||TARGET_x86_64) TITLE:=Microsoft Hyper-V Utility Driver KCONFIG:= \ CONFIG_HYPERV_UTILS \ CONFIG_HYPERVISOR_GUEST=y \ CONFIG_PARAVIRT=n \ CONFIG_HYPERV=y FILES:=$(LINUX_DIR)/drivers/hv/hv_util.ko AUTOLOAD:=$(call AutoLoad,10,hv_util) endef define KernelPackage/hyperv-util/description Microsoft Hyper-V Utility Driver endef $(eval $(call KernelPackage,hyperv-util)) # # Hyper-V Storage Drive needs to be in kernel rather than module to load the root fs. # define KernelPackage/hyperv-storage SUBMENU:=$(VIRTUAL_MENU) DEPENDS:=@(TARGET_x86||TARGET_x86_64) +kmod-scsi-core TITLE:=Microsoft Hyper-V Storage Driver KCONFIG:= \ CONFIG_HYPERV_STORAGE=y \ CONFIG_HYPERVISOR_GUEST=y \ CONFIG_PARAVIRT=n \ CONFIG_HYPERV=y FILES:=$(LINUX_DIR)/drivers/scsi/hv_storvsc.ko AUTOLOAD:=$(call AutoLoad,40,hv_storvsc) endef define KernelPackage/hyperv-storage/description Microsoft Hyper-V Storage Driver endef $(eval $(call KernelPackage,hyperv-storage)) 修改/target/linux/x86/64/config-default和/target/linux/x86/config-3.18，均添加以下内容 # CONFIG_HYPERV is not set # CONFIG_HYPERV_BALLOON is not set # CONFIG_HYPERV_NET is not set # CONFIG_HYPERV_STORAGE is not set # CONFIG_HYPERV_UTILS is not set # CONFIG_FB_HYPERV is not set # CONFIG_HID_HYPERV_MOUSE is not set # CONFIG_HYPERV_KEYBOARD is not set 内核部分修改完成。 接下来修改make部分。 P.S. 这一部分不改也没事，你也可以用 qemu-img convert -f raw -O vpc openwrt-x86-generic-combined-ext4.img openwrt-x86-generic-combined-ext4.vhd 把生成的img转换成微软的VHD 修改/config/Config-images.in，在config VDI_IMAGE代码块的下面添加生成vhd的选项。 config VHD_IMAGES bool "Build Hyper-V image files (VHD)" depends on TARGET_x86 || TARGET_x86_64 select GRUB_IMAGES select TARGET_IMAGES_PAD select PACKAGE_kmod-hyperv-balloon select PACKAGE_kmod-hyperv-net-vsc select PACKAGE_kmod-hyperv-util select PACKAGE_kmod-hyperv-storage 修改/target/linux/x86/image/Makefile，添加img转vhd的操作 在ifneq ($(CONFIG_VDI_IMAGES),)代码块下面添加以下内容 ifneq ($(CONFIG_VHD_IMAGES),) define Image/Build/vhd rm $(BIN_DIR)/$(IMG_PREFIX)-combined-$(1).vhd || true qemu-img convert -f raw -O vpc \ $(BIN_DIR)/$(IMG_PREFIX)-combined-$(1).img \ $(BIN_DIR)/$(IMG_PREFIX)-combined-$(1).vhd endef endif 在define Image/Build代码块中$(call Image/Build/vdi,$(1))下面加入 $(call Image/Build/vhd,$(1)) 至此，修改完成。 11 配置 回到OpenWRT源码主目录，执行make menuconfig 选择Target System为x86，Subtarget选择Generic或者x86_64。自己看着办。 然后在Target Images里选中Build Hyper-V image files (VHD)。（如果之前修改源码的时候，没有添加生成vhd的选项，这里不会有这个选项，你只要生成后把img用工具转换成vhd就可以了，比如qemu-img convert -f raw -O vpc openwrt-x86-generic-combined-ext4.img openwrt-x86-generic-combined-ext4.vhd） 然后在Kernel modules里的Virtualization Support里勾选上4个Hyper-V相关的。如果有选过Build Hyper-V image files (VHD)，这4个是会默认选中不可修改的。 Kernel modules里的Network Devices中其他网卡可以都取消选择了。 配置至此结束。其他配置你自己开心就好。 100 编译 使用命令make可以进行编译，make V=s可以让它输出调试信息，带上-j1参数表示用1个核心编译。 101 完成 编译完成后，镜像文件在/bin目录下。vhd可以直接装上Hyper-V使用了。 110 附上我编译的OpenWRT Chaos Calmer版本。http://pan.baidu.com/s/1kUtLaWR 2016-06-15补充： 因为我编译时的内核版本是3.18.23，而官方源里面的内核版本是3.18.20，因此有些涉及内核依赖的包不能安装。虽然说可以ignore，但还是用匹配内核的会好一些。现在我已经把我1月份编译这个Chaos Calmer时一起编译出来的包上传。还是上面那个地址，请自行取用。 111 感谢评论区neil补充如下： 要用opkg install 安装luci，需要先在 /etc/opkg/customfeeds.conf中加上luci的package url。https://downloads.openwrt.org/chaos_calmer/15.05/x86/generic/packages/luci/
内网是个多么奇怪的东西，所以为了穿透内网，有很多软件，像花生壳内网版，nat123之类。但他们大多不免费。现在好了有这么一个开源的玩意儿——ngrok，一切都变得简单。 首先去ngrok官网(https://ngrok.com/，貌似被GFW认证，大家可以去https://www.tunnel.org.cn/)下载ngrok。平台应该是没有区别的，我是Windows x64版本。（Linux配置方式与本文相同） 准备好以后只要解压到一个地方，没错就这么简单。 接下来就直接用命令行就行啦！让我们来试试转发80端口 打开命令提示符，输入 ngrok http 80 接下来你可以得到一个地址，如上，我是 http://9621c03a.ngrok.io/ 访问就可以看见啦 你也可以试试转发tcp，但是需要在官网注册账号。命令是 ngrok tcp port 有一个问题就是ngrok的服务器的访问速度不够快，接下来，我就告诉大家一个国内的ngrok服务器，就是Tunnel (https://www.tunnel.org.cn/，要求ngrok为1.7版本) ngrok已经准备好了，接下来在https://www.tunnel.org.cn上下载配置文件，你可以按照自己的需求下载不同地方节点。 我选择了美国节点，接下来就把这个配置文件放在和ngrok同根的目录下。然后在命令行里输入 ngrok -config ngrok.cfg -subdomain domain port 执行就可以啦 接下来访问这个地址试试，成功√，速度显然比官方的快 你也可以试试tcp，我就不演示了，命令如下 ngrok -config ngrok.cfg -proto=tcp port 享受你的ngrok吧！
加了SSL证书以及把服务器迁回了自己家
所以https://lohu.info也是可以访问我的博客的（其实主要是为了绕开被电信ban的80端口）感谢StartSSL的服务！ 今天还是cyy同学生日，祝他生日快乐！