跳转至

博客

在 LEDE(OpenWrt)上启用 wpad

WPAD(Web Proxy Auto-Discovery Protocol)是一个可以利用 dhcp 分发 pac 配置的协议。方法如下:

$ # ssh to router first
$ vim /etc/dnsmasq.conf
dhcp-option=252,"http://router_ip/wpad.dat"
$ vim /www/wpad.dat # put pac here
$ service dnsmasq restart
$ # ensure proxy is available to lan
$ # enable wpad on devices

参考文档:

  1. Web Proxy Auto-Discovery Protocol
  2. Automatic Proxy Configuration with WPAD
  3. Deployment Options
  4. Example PAC File

在 Xcode 9 上启用 Vim 模拟(XVim 2)

作为一个不用 vim 编辑会死星人,用 Xcode 总是止不住自己想 Escape 的心。于是找到了 XVimProject/XVim2 进行配置。

大致方法如下:

  1. 按照 Signing Xcode 对 Xcode 进行重签名。套路和对 GDB 进行签名一样。不过这次,签名完成的时间可长多了,毕竟 Xcode 这么大。
  2. 接着按照项目的 README,首先 git clone 然后 make ,第一次打开 Xcode 的时候选择 Load Bundle 即可。

终于可以满足我 Escape Xcode 的欲望了。

在 macOS 上读取移动硬盘的 S.M.A.R.T. 信息

之前想看看自己各个盘的情况,但是发现只能看电脑内置的 SSD 的 S.M.A.R.T 信息,而移动硬盘的都显示:

$ smartctl -a /dev/disk2
smartctl 6.6 2017-11-05 r4594 [Darwin 17.7.0 x86_64] (local build)
Copyright (C) 2002-17, Bruce Allen, Christian Franke, www.smartmontools.org

/dev/disk2: Unable to detect device type
Please specify device type with the -d option.

Use smartctl -h to get a usage summary

一开始我怀疑是个别盘不支持,但换了几个盘都不能工作,问题应该出现在了 USB 上。查了下资料,果然如此。根据 USB devices and smartmontools ,获取 S.M.A.R.T 信息需要直接发送 ATA 命令,但是由于经过了 USB,于是需要进行一个转换,导致无法直接发送 ATA 命令。这个问题自然是有解决方案,大概就是直接把 ATA 命令发送过去(pass-through)。上面这个地址里写到,如果需要在 macOS 上使用,需要安装一个内核驱动。可以找到,源码在 kasbert/OS-X-SAT-SMART-Driver 并且有一个带签名的安装包在 External USB / FireWire drive diagnostics support 中可以下载。丢到 VirusTotal 上没查出问题,用 v0.8 版本安装好后就成功地读取到了移动硬盘的 S.M.A.R.T 信息了。

然后我又简单研究了一下各个 S.M.A.R.T 各个值的含义是什么。 VALUE 代表当前的值, WORST 代表目前检测到的最差的值, THRESH 代表损坏阈值。这些值都是从 RAW_VALUE 进行计算后归一化而来。然后 TYPE 分为两种,一是 Pre-fail ,代表如果这一项的值小于阈值,代表这个机器很危险了,赶紧拷数据丢掉吧。二是 Old_age ,代表如果这一项小于阈值,代表这个机器比较老了,但还没坏。真正要看是否坏了,可以看 When_Failed 一栏。

通过 Ipfilter Extension 实现 RFC8367

前几天无聊闲逛看到了一个很有趣的 RFC8367 - Wrongful Termination of Internet Protocol (IP) Packets ,看到日期大家应该都懂了,这是个粥客,不过里面还是反映了一些事情,咳。

之前看到闪客实现了 shankerwangmiao/xt_PROTO ,想到自己也可以做一个 iptables 扩展,于是就写了 jiegec/xt_EQUALIZE 。它是这样使用的:

$ git clone git@github.com:jiegec/xt_EQUALIZE.git
$ make
$ sudo make install
$ sudo iptables -t filter -A INPUT -j EQUALIZE
$ sudo dmesg -w &
$ # Make some random network requests to see the effect!
$ ping 1.1.1.1
$ ping 8.8.8.8
$ ping ::1

目前还没有把参数都变成可以配置的。如果真的有人需要这个模块的话,我再改吧(逃

在 macOS 上 TAP Interface 上启用 IPv6 自动配置

由于 macOS 对 TAP Interface 不会自动出现一个设置中对应的服务,所以需要手动进行配置。一番测试后,发现可以通过:

$ sudo ipconfig set [tap_if] automatic-v6
$ sudo ipconfig set [tap_if] dhcp

启用系统自带的 dhcp 和 ra 功能。也许有方法可以把这些 tap 搬到系统的设置中去。

UPDATE:

可以把 TAP Interface 加到系统的设置中去。方法参考Virtual network interface in Mac OS X。完成以后可以直接通过系统设置界面进行配置。

在 macOS 下实现 GRETAP

由于没有找到 macOS 下现成的 GRETAP 实现,我就想到自己实现一个。由于tuntaposx提供了一个和 Linux 下基本一样的 TAP Interface,于是自己利用 raw socket 和 TAP Interface 实现了一下,主要方法:

  1. 打开 raw socket,读取收到的 proto 47 的包,判断是否为 GRETAP 包,是,则写入内层包到打开的 TAP Interface 中。
  2. 从 TAP Interface 中读入包,自己加上 GRE 头和 IP 头,然后发送。

主要的难度是在 raw socket 部分,macOS 继承了 BSD,与 Linux 不大一样。于是参考了SOCK_RAW Demystified,成功地实现了这个功能。

代码放在jiegec/gretapmac。写得并不高效,仅仅可用,用了一百多行。

UPDATE: 之后又随手实现了一个类似的协议,L2TPv3 over UDP。代码在jiegc/l2tpv3udptap

在 WireGuard 构建的 Overlay Network 上跑 babel 路由协议

Run Babeld over Wireguard - Fugoes's BlogRoute-based VPN on Linux with WireGuard 启发,自己也想尝试一下,在一个有多个结点的网络中,如何通过 WireGuard 构建一个 overlay network,并通过 babel 自动进行结点发现和路径选择。

首先建立点对点的 WireGuard Tunnel。由于我们用 babel 进行路由,所以我们不能采用 Wiregurad 本身基于目的地址的端口复用,所以每一个 WireGuard interface 都只有一个 Peer。

配置一个点对点的 WireGuard Tunnel:

$ # for wg-quick
$ cat wg0.conf
[Interface]
Address = IPV4/32, fe80::ID/64
PrivateKey = REDACTED
ListenPort = PORT1
Table = off # ask wg-quick not to insert peer address into routing table

[Peer]
PublicKey = REDACTED
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = REDACTED:PORT2

这里的 IPV4 和 ID 在同一设备上的不同 WireGuard Tunnel 上相同。只是通过 wg interface 编号来区分。

接着配置 babeld:

$ cat babeld.conf

router-id ID
local-port 33123 # for babelweb2

# one line for each wg interface
interface wg0 type tunnel rtt-max 512

redistribute ip PREFIX/LEN ge LEN le 32 local allow # tunnel neighbors
redistribute proto 42 # routes installed by babeld
redistribute local deny
# consult babeld man page for more

然后通过 BabelWeb2(很难用)进行可视化,然后通过手动触发一些网络波动即可达到效果。

更改 macOS 屏幕亮度的按键

由于我打开了「Invert Fn」功能,所以需要调亮度的时候,是采用 Fn+F1/F2 的方法。但是,我的机械键盘则是,不按 Fn 时为 1-9,按着 Fn 时为对应的 F1-F9,但是就无法调整亮度和声音了。

然后捣腾了一下,发现可以用 ScLk 和 Pa/Br(名称在各个键盘上不大一样)调整亮度。不过,还没发现如何更改音量。。。

向 Lenovo y1s 刷入 OpenWRT 17.01.5 固件,并把 IPv6 bridge 到内网中和配置认证脚本

首先参照OpenWRT Wiki - Lenovo Y1 v1找到刷固件教程:

  1. 下载Lenovo y1s 的固件备用
  2. 断开电源,等待一段时间,插入电源同时快速按下重置按钮,如果面板双闪,则说明进入了恢复模式
  3. 电脑连接到四个 LAN 口中任意一个,配置静态地址在 192.168.1.0/24 网段
  4. 打开 192.168.1.1 可以看到刷固件的页面
  5. 上传固件,等待路由器重启
  6. 配置 IP 地址为 DHCP 模式,打开 192.168.1.1 进行配置

然后就是常规的密码设置,opkg 源设置为 tuna 的源,配置 ssh 和 公钥。

接下来,我们为了使用学校的 SLAAC,采用 ebtables 直接把学校的 IPv6 bridge 进来,而 IPv4 由于准入系统,需要 NAT。

参考 Bridge IPv6 connections to WAN,下载 v6brouter_openwrt.sh 到某个地方,然后修改一下里面的一些参数:

# For Lenovo y1s
WAN_DEV=eth0.2
BRIDGE=br-lan
# the rest remain unchanged

然后跑起来之后,自己的电脑可以成功拿到原生的 IPv6 地址了,不需要用难用的 NAT66 技术。

下一步是采用z4yx/GoAuthing

$ go get -u -v github.com/z4yx/GoAuthing
$ cd $GOPATH/src/github.com/z4yx/GoAuthing/cli
$ env GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build main.go
$ mipsel-linux-gnu-strip main
$ scp main root@192.168.1.1:~/GoAuthing
$ ssh root@192.168.1.1
$ opkg install ca-certificates
$ ./GoAuthing

这里参考了解决 GO 语言编译程序在 openwrt(mipsle 架构) 上运行提示 Illegal instruction 问题,配置了 GOMIPS 环境变量。为了访问 HTTPS 网站,参考了OpenWRT Wiki - SSL and Certificates in wget。有毒的是,这个环境变量,在 macOS 上不能正常工作,而在 Linux 机子上是没有问题的。

然后就可以成功地跑起来 GoAuthing,解决了上校园网认证的问题。

感谢宇翔编写的 GoAuthing 小工具。

更新:简化了一下 v6brouter 脚本:

#!/bin/sh
BRIDGE=br-lan
WAN_DEV=$(/sbin/uci get network.wan.ifname)
WHITELIST1="00:11:22:33:44:55"
WHITELIST2="55:44:33:22:11:00"

brctl addbr $BRIDGE 2> /dev/null
brctl addif $BRIDGE $WAN_DEV
ip link set $BRIDGE down
ip link set $BRIDGE up
brctl show

ebtables -F
ebtables -P FORWARD ACCEPT
ebtables -L

uci set dhcp.lan.ra='disabled'
uci set dhcp.lan.dhcpv6='disabled'
uci commit
/etc/init.d/odhcpd restart

echo 2 > /proc/sys/net/ipv6/conf/$BRIDGE/accept_ra
ebtables -t broute -F
ebtables -t broute -A BROUTING -i $WAN_DEV -p ! ipv6 -j DROP
ebtables -t broute -A BROUTING -s $WHITELIST1 -p ipv6 -j ACCEPT
ebtables -t broute -A BROUTING -d $WHITELIST1 -p ipv6 -j ACCEPT
ebtables -t broute -A BROUTING -s $WHITELIST2 -p ipv6 -j ACCEPT
ebtables -t broute -A BROUTING -d $WHITELIST2 -p ipv6 -j ACCEPT
ebtables -t broute -A BROUTING -p ipv6 -j DROP
ebtables -t broute -L

注意,这里添加了两个 WHITELIST 的 MAC 地址,表示只让这两个 MAC 地址的设备访问 v6。一般来说,外面网关的 MAC 地址也要放进来,不然可能接收不到 RA。如果不需要白名单的话,可以去掉 ebtables 的后几行规则。

构建简易的 initramfs

一直对 Linux 的启动很感兴趣,但对 initrd 和 initramfs 等概念不大了解,于是上网找了资料,自己成功地看到了现象。

参考资料:

具体步骤:

$ cat hello.c
#include <stdio.h>
#include <unistd.h>

int main() {
    for (;;) {
        printf("Hello, world!\n");
    }
}
$ gcc -static hello.c -o init
$ echo init | cpio -o -H newc | gzip > initrd
$ qemu-system-x86_64 -kernel /boot/vmlinuz-linux -initrd initrd -nographic -append 'console=ttyS0'
# Use C-a c q u i t <Enter> to exit

可以看到过一会(三四秒?),可以看到满屏的 Hello world 在输出。

用 MuSSH 快速对多台机器进行软件包升级

Debian Stretch 9.5 刚刚更新,自己手上有不少 stretch 的机器,于是顺手把他们都升级了。不过,这个过程比较繁琐,于是我采用了 MuSSH 的方法,让这个效率可以提高,即自动同时 SSH 到多台机器上进行更新。

首先编写 hostlist 文件,一行一个地址,分别对应每台机器。

然后采用 MuSSH 对每台机器执行同样的命令

$ mussh -H hostlist -c 'apt update && apt upgrade -y'

此时要求,ssh 上去以后有相应的权限。这个有许多方法,不再赘述。然后就可以看到一台台机器升级,打上安全补丁,爽啊。

配置 fcitx-fbterm 实现在终端下显示和输入中文

参考网站:

Ubuntu 使用 fbterm 无法打开 fb 设备的解决及 fcitx-fbterm 安装 Fcitx - ArchWiki 完美中文 tty, fbterm+yong(小小输入法) 让 linux console 支持中文显示和 fcitx 输入法

考虑到 lemote yeeloong 机器的 cpu 运算性能,跑一个图形界面会非常卡,于是选择直接用 framebuffer。但是,显示中文又成了问题。于是,采用了 fbterm 和 fcitx 配合,加上 gdm 的方法,完成了终端下的中文输入。

首先,安装相关的包:

$ sudo apt install gpm fcitx-fronend-fbterm dbus-x11 fbterm fonts-wqy-zenhei

接着,基于以上参考网站第一个,编写 zhterm 文件:

$ echo zhterm
#!/bin/bash
eval `dbus-launch --auto-syntax`
fcitx >/dev/null 2>&1
fbterm -i fcitx-fbterm
kill $DBUS_SESSION_BUS_PID
fcitx-remote -e
$ chmod +x zhterm
$ zhterm
# Use C-SPC to switch input source

另:找到一个映射 Caps Lock 到 Escape 的方案

$ sudo bash -c "dumpkeys | sed 's/CtrlL_Lock/Escape/' | loadkeys"

在 Lemote Yeeloong 上安装 Debian jessie

参考网站:

gNewSense To MIPS Run a TFTP server on macOS Debian on Yeeloong Debian MIPS port wiki Debian MIPS port

首先,进入设备的 PMON:

Press Del to enter PMON

然后,下载 Debian Jessie 的 netboot 文件:

$ wget https://mirrors.tuna.tsinghua.edu.cn/debian/dists/jessie/main/installer-mipsel/current/images/loongson-2f/netboot/vmlinux-3.16.0-6-loongson-2f
$ wget https://mirrors.tuna.tsinghua.edu.cn/debian/dists/jessie/main/installer-mipsel/current/images/loongson-2f/netboot/initrd.gz

以 macOS 为例,起一个 tftp 服务器以供远程下载:

# ln -s these files to /private/tftpboot:
# initrd.gz
# vmlinux-4.16.0-6-loongson-2f
$ sudo launchctl load -F /System/Library/LaunchDaemons/tftp.plist
# set addr manually to 192.168.2.1

回到 PMON,配置远程启动:

> ifaddr rtl0 192.168.2.2
> load tftp://192.168.2.1/vmlinux-3.16.0-6-loongson-2f
> initrd tftp://192.168.2.1/initrd.gz
> g

之后就是熟悉的 Debian Installer 界面。起来之后,就可以顺手把 tftp 服务器关了:

$ sudo launchctl unload -F /System/Library/LaunchDaemons/tftp.plist

实测滚到 stretch 会挂。因为 stretch 虽然也有 mipsel 架构,但是它的 revision 与 Loongson-2f 不大一样,会到处出现 SIGILL 的问题,不可用。靠 jessie 和 jessie-backports 已经有不少的软件可以使用了。

通过 systemd-run 直接在容器中执行命令

之前使用 systemd-nspawn 开了容器,然后通过 machinectl shell 进去,想要起一个服务然后丢到后台继续执行,但是发现离开这个 session 后这个进程总是会被杀掉,于是找了 systemd-run 的方案,即:

systemd-run --machine machine_name_here absolute_path_to_executable args_here

这样可以直接在容器中跑服务,而且用这个命令输出的临时 server 名称,还可以看到日志:

journalctl --machine machine_name_here -u unit_name_above

通过 iptables 在同一个端口根据源地址解复用(demux)

现在遇到一个场景,原来的一个服务只给一个客户端用,但现在增加了一个客户端,由于客户端配置相同,但是服务端需要区别对待两个客户端的服务端配置,所以利用 iptables 根据源地址做了一个端口转发,实现了 demux。

假设:服务器在 192.168.0.1,客户端分别在 192.168.0.2 和 192.168.0.3。客户端配置的服务端地址是 192.168.0.1:8000。之前,在服务器上只跑了一个服务,监听着 8000 端口。

现在,在服务器上再跑一个服务,监听 8001 端口,同时根据需求进行相应的配置。然后,加上如下 iptables 规则:

$ sudo iptables -t nat -A PREROUTING -s 192.168.0.3 -d 192.168.0.1 -p tcp -m tcp --dport 8000 -j REDIRECT --to-ports 8001

这样,在不需要更改客户端的情况下,完成了需要的效果。

升级 MongoDB 到 4.0

MongoDB 4.0 刚刚发布,加入了我很想要的 Transaction 功能。不过,我一更新就发现 MongoDB 起不来了。研究了一下日志,发现由于我创建数据库时,MongoDB 版本是 3.4,虽然后来升级到了 3.6,但还是用着 3.4 的兼容模式。这个可以这样来检测:

$ mongo
> db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

如果不是 3.6,升级到 4.0 之前,需要先执行如下操作:

$ # MongoDB version 3.6
$ mongo
> db.adminCommand( { setFeatureCompatibilityVersion: "3.6" } )

然后再升级到 MongoDB 4.0,才能正常地启动 MongoDB 4.0。之后可以考虑尝试使用 MongoDB 4.0 的 Transaction 了。不知道什么时候进入 Debian 的 stretch-backports 源中。

为了使用 MongoDB 4.0 的新特性,输入以下命令:

$ mongo
> db.adminCommand( { setFeatureCompatibilityVersion: "4.0" } )

之后会尝试一下 MongoDB 4.0 的 Transaction 功能。

Wireguard 隧道搭建

随着 Wireguard Go 版本的开发,在 macOS 上起 WireGuard Tunnel 成为现实。于是,搭建了一个 macOS 和 Linux 之间的 WireGuard Tunnel。假设 Linux 端为服务端,macOS 端为客户端。

macOS 端:

$ brew install wireguard-tools
$ cd /usr/local/etc/wireguard
$ wg genkey > privatekey
$ wg pubkey < privatekey > publickey
$ vim tunnel.conf
[Interface]
PrivateKey = MACOS_PRIVATE_KEY

[Peer]
PublicKey = LINUX_PUBLIC_KEY # Generated below
AllowedIPs = 192.168.0.0/24
Endpoint = LINUX_PUBLIC_IP:12345
$ vim up.sh
#!/bin/bash
# change interface name when necessary
sudo wireguard-go utun0
sudo wg setconf utun0 tunnel.conf
sudo ifconfig utun0 192.168.0.2 192.168.0.1
$ chmod +x up.sh
$ ./up.sh

配置 Linux 端:

$ git clone https://git.zx2c4.com/WireGuard
$ make
$ sudo make install
$ sudo fish
$ cd /etc/wireguard
$ wg genkey > privatekey
$ wg pubkey < privatekey > publickey
$ vim wg0.conf
[Interface]
Address = 192.168.0.1/24
PrivateKey = LINUX_PRIVATE_KEY
ListenPort = 12345

[Peer]
PublicKey = MACOS_PUBLIC_KEY
AllowedIPs = 192.168.0.2/24
$ wg-quick up wg0

经过测试,两边可以互相 ping 通。

后续尝试在 Android 上跑通 WireGuard。

UPDATE 2018-07-11:

成功在 Android 上跑通 WireGuard。在 Google Play 上下载官方的 App 即可。麻烦在于,将 Android 上生成的 Public Key 和服务器的 Public Key 进行交换。

然后又看到WireGuard 在 systemd-networkd上的配置方案,自己也实践了一下。首先,如果用的是 stretch,请首先打开 stretch-backports 源并把 systemd 升级到 237 版本。

然后,根据上面这个连接进行配置,由于都是 ini 格式,基本就是复制粘贴就可以配置了。有一点要注意,就是,要保护 PrivateKey 的安全,注意配置 .netdev 文件的权限。

Verilog 初体验

自己以前一直对硬件方面没有接触,但是大二大三很快就要接触相关知识,所以自己就先预习一下 Verilog HDL,以便以后造计算机。听学长们推荐了一本书叫《自己动手写 CPU》,由于自己手中只有很老的 Spartan-3 板子,手上没有可以用来试验的 FPGA,所以选择用 Verilog + Verilator 进行模拟。既然是模拟,自然是会有一定的问题,不过这个以后再说。

然后就是模仿着这本书的例子,写了指令的获取和指令的解码两部分很少很少的代码,只能解码 ori (or with immidiate) 这一个指令。然后,通过 verilator 跑模拟,输出 vcd 文件,再用 gtkwave 显示波形,终于能够看到我想要的结果了。能够看到,前一个时钟周期获取指令,下一个时钟周期进行解码,出现了流水线的结果。这让我十分开心。

接下来就是实现一些基本的算术指令,然后讲计算的结果写入到相应的寄存器中。这样做完之后,就可以做一个基于 verilator 的简易 A+B 程序了。

我的代码发布在jiegec/learn_verilog中。最近马上到考试周,可能到暑假会更频繁地更新吧。

在 ArchLinux 上编译 LineageOS for Huawei Angler

实践了一下如何在 ArchLinux 上编译自己的 LineageOS。本文主要根据官方文档 进行编写。

$ # for py2 virtualenv and running x86 prebuilt binaries(e.g. bison)
$ sudo pacman -Sy python2-virtualenv lib32-gcc-libs 
$ mkdir -p ~/bin
$ mkdir -p ~/virtualenv
$ # build script is written in python 2
$ cd ~/virtualenv
$ virtualenv2 -p /usr/bin/python2 py2
$ mkdir -p ~/android/lineage
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod a+x ~/bin/repo
$ vim ~/.config/fish/config.fish
set -x PATH ~/bin $PATH
set -x USE_CCACHE=1
$ exec fish -l
$ cd ~/android/lineage
$ repo init -u https://github.com/LineageOS/android.git -b lineage-15.1
$ # alternatively, follow https://mirrors.tuna.tsinghua.edu.cn/help/lineageOS/
$ repo sync
$ source ~/virtualenv/py2/bin/activate
$ source build/envsetup.sh
$ breakfast angler
$ vim ~/.config/fish/config.fish
$ ccache -M 50G
$ cd ~/android/lineage/device/huawei/angler
$ ./extract-files.sh
# Plug in Nexus 6P, maybe over ssh, see my another post
$ cd ~/android/lineage
$ croot
$ brunch angler
$ # Endless waiting... (for me, more than 2 hrs)

编写 eBPF 程序和利用 HyperLogLog 统计包的信息

前段时间在写概率论与数理统计的期末论文,讨论的主题是如何对一个十分巨大的多重集合(或者是流)中相异元素个数进行估计,写的是 HyperLogLog 等算法。联想到前段时间 LWN 上多次提到的 eBPF 和 BCC 的文章,我准备自己用 eBPF 实现一个高效的估计 inbound packet 中来相异源地址的个数和 outbound packet 中相异目的地址的个数。经过了许多的尝试和努力,最终是写成了 jiegec/hll_ebpf ,大致原理如下:

由于 eBPF 是一个采用专用的 bytecode 并且跑在内核中的语言,虽然我们可以用 clang 写 C 语言然后交给 LLVM 生成相应地 eBPF bytecode,但仍然收到许多的限制。而且,我很少接触 Linux 内核开发,于是在找内核头文件时费了一番功夫。首先是核心代码:

struct bpf_map_def SEC("maps") hll_ebpf_out_daddr = {
    .type = BPF_MAP_TYPE_PERCPU_ARRAY,
    .key_size = sizeof(u32),
    .value_size = sizeof(u32),
    .max_entries = 256,
    .pinning = 2 // PIN_GLOBAL_NS
};

SEC("out_daddr")
int bpf_out_daddr(struct __sk_buff *skb) {
  u32 daddr = get_daddr(skb);
  u32 hash = Murmur3(daddr, 0);
  update_hll(&hll_ebpf_out_daddr, hash);
  return 0;
}

首先是声名一个类型为 PERCPU_ARRAY 的 eBPF MAP 类型。这里的 MAP 不是字典,Array 才是真是的数据结构,只不过提供的 API 是类似于字典的。SEC 宏则是指定这个东西要放在哪一个段,这个在后面会提到。这个函数的作用就是,获取 IP 包的目的地址(其实应该判断一下是否是 IPv4 的),然后根据 HyperLogLog 的要求,进行哈希(这里采用的是 Murmur3),然后对得到的哈希值分段,前一部分用于索引,后一部分的 nlz(clz, whatever)用于估计。具体算法详情可以参考 HyperLogLog 的论文。

接着,我们可以把这个 eBPF 函数进行编译,并且应用起来:

$ export KERN=4.16.0-2 # or use uname -r with awk, see Makefile
$ clang -O2 -I /usr/src/linux-headers-${KERN}-common/include -I /usr/src/linux-headers-${KERN}-common/arch/x86/include -emit-llvm -c bpf.c -o - | llc -march=bpf -filetype=obj -o bpf.o
$ export IFACE=en0 
$ sudo tc qdisc add dev ${IFACE} clsact || true
$ sudo tc filter del dev ${IFACE} egress
$ sudo tc filter add dev ${IFACE} egress bpf obj bpf.o sec out_daddr
$ sudo tc filter del dev ${IFACE} ingress
$ sudo tc filter add dev ${IFACE} ingress bpf obj bpf.o sec in_saddr

我们需要在用户态读出上面这个 MAP 中的内容。由于它是全局的,我们可以在 /sys/fs/bpf/tc/globals 中找到他们。然后,把统计得到的数据进行综合,得到结果:

void read_file(const char *file) {
  int fd = bpf_obj_get(file);
  const static int b = 6;
  const static int m = 1 << b;
  int M[m] = {0};
  int V = 0;
  double sum = 0;
  for (unsigned long i = 0; i <m; i++) {
    unsigned long value[2] = {0};
    bpf_map_lookup_elem(fd, &i, &value);
    M[i] = value[0] > value[1] ? value[0] : value[1]; // assuming 2 CPUs, will change later
    if (M[i] == 0)
      V++;
    sum += pow(2, -M[i]);
  }
  double E = 0.709 * m * m / sum;
  if (E <= 5 * m / 2) {
    if (V != 0) {
      E = m * log(1.0 * m / V);
    }
  } else if (E> pow(2, 32) / 30) {
    E = -pow(2, 32) * log(1 - E / pow(2, 32));
  }
  printf("%ld\n", lround(E));
}

可以手动通过 nmap 测试,例如扫描一个段,可以看到数据会增长许多。如果扫描相同的段,则数字不会变化,但如果扫描新的段,数字会有变化。这是一个 利用了 eBPF 的 HyperLogLog 的实现。

调整 Nginx 和 PHP 的上传文件大小限制

之前迁移的 MediaWiki,有人提出说无法上传一个 1.4M 的文件。我去看了一下网站,上面写的是限制在 2M,但是一上传就说 Entity Too Large,无法上传。后来经过研究,是 Nginx 对 POST 的大小进行了限制,同时 PHP 也有限制。

Nginx 的话,可以在 nginx.conf 的 http 中添加,也可以在 server 或者 location 中加入这么一行:

client_max_body_size 100m;

我的建议是,尽量缩小范围到需要的地方,即 location > server > http。

在 PHP 中,则修改 /etc/php/7.0/fpm/php.ini:

post_max_size = 100M

回到 MediaWiki 的上传页面,可以看到显示的大小限制自动变成了 100M,这个是从 PHP 的配置中直接获得的。

最近写 Node.js 遇到的若干坑

最近在做前后端分离,前端在用 Vue.js 逐步重写,后端则变为 api 的形式。同时,我尝试了用 autocannon 和 clinic 工具测试自己的 api endpoint 的性能,一开始发现有几个延迟会特别高,即使是一个很简单的 api 也有不正常的高延迟。

于是,我用 clinic 生成了 flamegraph,发现了一些问题:

  1. 我在 session 里保存了一些缓存的信息,这部分内容比较大,express-session 在保存到数据库前会先 JSON.stringify 再 crc 判断是否有改变,如果有改变则保存下来。但是由于我的这个对象嵌套层数多,所以时间花得很多。我调整了这个对象的结构,缩小了很多以后,果然这部分快了很多
  2. 有一个 API 需要大量的数据库查询,原本是 O(结点总数)次查询,我考虑到我们数据的结构,改成了 O(深度),果然快了许多
  3. 之前遇到一个小问题,就是即使我没有登录,服务器也会记录 session 并且返回一个 cookie。检查以后发现,是 connect-flash 即使在没有使用的时候,也会往 cookie 中写入一个空的对象,这就导致 express-session 认为需要保存,所以出现了问题。解决方案就是,换成了它的一个 fork:connect-flash-plus,它解决了这个问题

在 Nginx 将某个子路径反代

现在遇到这么一个需求,访问根下面是提供一个服务,访问某个子路径(/abc),则需要提供另一个服务。这两个服务处于不同的机器上,我们现在通过反代把他们合在一起。在配置这个的时候,遇到了一些问题,最后得以解决。

upstream root {
    server 1.2.3.4:1234;
}
upstream subpath {
    server 4.3.2.1:4321;
}

server {
    listen 443 ssl;
    server_name test.example.com;

    # the last slash is useful, see below
    location /abc/ {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        # the last slash is useful too, see below
        proxy_pass http://subpath/;
    }

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://root;
    }
}

由于并不想 subpath 他看到路径中 /abc/ 这一层,导致路径和原来在根下不同,通过这样配置以后,特别是两个末尾的斜杠,可以让 nginx 把 GET /abc/index.html 改写为 GET /index.html,这样我们就可以减少许多配置。当然,我们还是需要修改一下配置,现在是 host 在一个新的域名的一个新的子路径下,这主要是为了在返回的页面中,连接写的是正确的。

向 Nexus 6P 中刷入 LineageOS 实践

Nexus 6P 自带的系统没有允许 Root,所以需要自己解锁 bootloader 并且刷上别的系统。我选择了 LineageOS。Nexus 6P 的代号为 angler,首先可以找到官方的安装教程

我们需要下载的东西:

$ wget https://mirrorbits.lineageos.org/full/angler/20180521/lineage-15.1-20180521-nightly-angler-signed.zip
$ wget https://mirrorbits.lineageos.org/full/angler/20180521/lineage-15.1-20180521-nightly-angler-signed.zip?sha256 -O lineage-15.1-20180521-nightly-angler-signed.zip.sha256
$ wget https://mirrorbits.lineageos.org/su/addonsu-15.1-arm64-signed.zip
$ wget https://mirrorbits.lineageos.org/su/addonsu-15.1-arm64-signed.zip?sha256 -O addonsu-15.1-arm64-signed.zip
$ wget https://github.com/opengapps/arm64/releases/download/20180527/open_gapps-arm64-8.1-full-20180527.zip
$ wget https://github.com/opengapps/arm64/releases/download/20180527/open_gapps-arm64-8.1-full-20180527.zip.md5
$ wget https://dl.twrp.me/angler/twrp-3.2.1-0-angler.img
$ wget https://dl.twrp.me/angler/twrp-3.2.1-0-angler.img.asc
$ wget https://dl.twrp.me/angler/twrp-3.2.1-0-angler.img.md5
$ gpg --verify *.asc
$ md5sum -c *.md5
$ sha256sum -c *.sha256

其中 Open GApps 可以自己考虑选择 full 还是其它的选择。

接下来,按照教程,先解锁 bootloader。连接手机,进入 USB Debugging Mode,重启进入 bootloader 并且解锁:

$ adb reboot bootloader
$ fastboot flashing unlock
# Confirm unlocking, and then the data should be wiped

接下来刷入 TWRP。还是进入 bootloader,然后刷入。

$ fastboot flash recovery twrp-3.2.1-0-angler.img
# Select recovery, and enter it

进入 TWRP 后,把我们刚刚下载的 zip 文件都 push 到手机上,并用 TWRP 安装:

# Select Wipe -> Advanced Wipe, Select Cache, System and Data and wipe then
# Install lineageos, opengapps, addonsu and follow on-screen instructions
# Reboot into system

经过一段时间的等待,LineageOS 就安装成功了。但是遇到了一些问题:

  1. 开机时提示 vendor image 版本与打包 LineagesOS 时采用的版本不同。 于是我下载了官方的 factory image,找到其中的 vendor.img,用 TWRP 刷到了 vendor 分区中。并且执行了 flash-bash.sh 更新 bootloader 和 radio。重启的时候这个错误就解决了。2018-06-12 更新 注意:不要下载 Driver Binaries 里面的 vendor, 刷上去系统还是提示版本 mismatch,建议还是下载完整的 factory 镜像。
  2. 检测不到 SIM 卡。 回到 bootloader 看 Barcode, 是有 IMEI 等信息的,说明分区没有被写坏。在网上搜索一段时间以后,发现禁用登录密码重启一次后即可使用,之后把密码加回来即可。