跳转至

2018

调整 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 等信息的,说明分区没有被写坏。在网上搜索一段时间以后,发现禁用登录密码重启一次后即可使用,之后把密码加回来即可。

在 WSL 上开启一个 getty 到串口的方法

为了测试一个硬件的 terminal,想在 Windows 上向串口开一个 tty,跑各种软件来测试。这件事情在 Linux 上和 macOS 上都有实践,但一直不知道 Windows 上怎么搞。经过了一番搜索,找到了 https://blogs.msdn.microsoft.com/wsl/2017/04/14/serial-support-on-the-windows-subsystem-for-linux/ 和 https://unix.stackexchange.com/a/123559 的方案。

以 COM5 为例:

$ sudo chmod 666 /dev/ttyS5
$ sudo agetty -s 115200 ttyS5 linux

这样就可以看到一个登录的界面了。

在 macOS 上 (https://superuser.com/questions/1059744/serial-console-login-on-osx):

$ screen /dev/tty.SLAB_USBtoUART 115200
# type C-b : exec ::: /usr/libexec/getty std.115200

体验 Fedora on RISCV

看到 RISCV 很久了,但一直没能体验。最近工具链不断更新,QEMU 在 2.12.0 也正式加入了 riscv 的模拟。但是自己编译一个内核又太麻烦,就找到了 Fedora 做的 RISCV port,下载下来试用了一下。之前试过一次,但是遇到了一些问题,刚才总算是成功地搞出来了。

官方文档地址:https://fedorapeople.org/groups/risc-v/disk-images/readme.txt 首先下载 https://fedorapeople.org/groups/risc-v/disk-images/ 下的 bbl vmlinux 和 stage4-disk.img.xz 三个文件,然后解压 stage4-disk.img.xz,大约有 5G 的样子。之前作者在脚本里作死开得特别大,导致我以前光是解压这一步就成功不了。现在终于解决了。

然后启动 qemu 命令打开虚拟机:

qemu-system-riscv64 \
  -nographic \
  -machine virt \
  -m 2G \
  -kernel bbl \
  -object rng-random,filename=/dev/urandom,id=rng0 \
  -device virtio-rng-device,rng=rng0 \
  -append "console=ttyS0 ro root=/dev/vda" \
  -device virtio-blk-device,drive=hd0 \
  -drive file=stage4-disk.img,format=raw,id=hd0 \
  -device virtio-net-device,netdev=usernet \
  -netdev user,id=usernet,hostfwd=tcp::10000-:22

这段命令摘自 readme.txt,区别只在于把 -smp 4 去掉了。不知道为什么不能正常工作,可能和作者提到的 FPU patch 有关。然后系统就可以正常起来了(firewalld 和 systemd-logind 不止为啥起不来,但是不用管)。

可以验证一下我们的系统:

$ uname -a
Linux stage4.fedoraproject.org 4.15.0-00046-g48fb45691946 #27 SMP Mon May 14 08:25:14 UTC 2018 riscv64 riscv64 riscv64 GNU/Linux

在 VMware ESXi 上部署 vCSA 实践

首先获取 vCSA 的 ISO 镜像,挂载到 Linux 下(如 /mnt),然后找到 /mnt/vcsa-cli-installer/templates/install 下的 embedded_vCSA_on_ESXi.json,复制到其它目录并且修改必要的字段,第一个 password 为 ESXi 的登录密码,一会在安装的过程中再输入。下面有个 deployment_option,根据你的集群大小来选择,我则是用的 small。下面配置这台机器的 IP 地址,用内网地址即可。下面的 system_name 如果要写 fqdn,记得要让这个域名可以解析到正确的地址,不然会安装失败,我因此重装了一次。下面的密码都可以留空,在命令行中输入即可。SSO 为 vSphere Client 登录时用的密码和域名,默认用户名为 Administrator@domain_name (默认的话,则是 Administrator@vsphere.local) 这个用户名在安装结束的时候也会提示。下面的 CEIP 我选择关闭,设置为 false。

接下来进行安装。

$ /mnt/vcsa-cli-installer/lin64/vcsa-deploy install /path/to/embedded_vCSA_on_ESXi.json --accept-eula

一路输入密码,等待安装完毕即可。然后通过 443 端口进入 vSphere Client, 通过 5480 端口访问 vCSA 的管理页面。两个的密码可以不一样。

2018-05-21 Update: 想要设置 VMKernel 的 IPv6 网关的话,ESXi 中没找到配置的地方,但是在 vSphere Client 中可以进行相关配置。

在脚本中寻找 X11 的 DISPLAY 和 XAUTHORITY

之前在搞一个小工具,在里面需要访问 X11 server,但是访问 X11 server 我们需要两个东西:DISPLAY 和 XAUTHORITY 两个环境变量。但是,由于它们在不同的发型版和 Display Manager 下都有些不同,所以花了不少功夫才写了一些。

为了验证我们是否可以连上 X11 server,我们使用这一句:

DIMENSIONS=$(xdpyinfo | grep 'dimensions:' | awk '{print $2;exit}')

它尝试打开当前的 DISPLAY,并且输出它的分辨率。接下来,我对不同的一些发型版,综合网上的方法,尝试去找到正确的环境变量。

对于 Debian:

DISPLAY=$(w -hs | awk -v tty="$(cat /sys/class/tty/tty0/active)" '$2 == tty && $3 != "-" {print $3; exit}')
USER=$(w -hs | awk -v tty="$(cat /sys/class/tty/tty0/active)" '$2 == tty && $3 != "-" {print $1; exit}')
eval XAUTHORITY=~$USER/.Xauthority
export DISPLAY
export XAUTHORITY
DIMENSIONS=$(xdpyinfo | grep 'dimensions:' | awk '{print $2;exit}')

对于 Archlinux:

DISPLAY=$(w -hs | awk 'match($2, /:[0-9]+/) {print $2; exit}')
USER=$(w -hs | awk 'match($2, /:[0-9]+/) {print $1; exit}')
eval XAUTHORITY=/run/user/$(id -u $USER)/gdm/Xauthority
export DISPLAY
export XAUTHORITY
DIMENSIONS=$(xdpyinfo | grep 'dimensions:' | awk '{print $2;exit}')

最后一种情况很粗暴的,直接找进程拿:

XAUTHORITY=$(ps a | awk 'match($0, /Xorg/) {print $0; exit}' | perl -n -e '/Xorg.*\s-auth\s([^\s]+)\s/ && print $1')
PID=$(ps a | awk 'match($0, /Xorg/) {print $1; exit}')
DISPLAY=$(lsof -p $PID | awk 'match($9, /^\/tmp\/\.X11-unix\/X[0-9]+$/) {sub("/tmp/.X11-unix/X",":",$9); print $9; exit}')
export DISPLAY
export XAUTHORITY
DIMENSIONS=$(xdpyinfo | grep 'dimensions:' | awk '{print $2;exit}')

中间混用了大量的 awk perl 代码,就差 sed 了。牺牲了一点可读性,但是开发起来比较轻松。

在 macOS 和 Linux 之间搭建 tinc 网络

一直听说 tinc 比较科学,所以尝试自己用 tinc 搭建一个网络。这里,macOS 这段没有固定 IP 地址,Linux 机器有固定 IP 地址 linux_ip。假设网络名称为 example , macOS 端名为 macos 地址为 192.168.0.2, linux 端名为 linux 地址为 192.168.0.1。

2018-11-11 注:本文用的 tinc 版本为 1.0.x,而不是 1.1-pre,两个分支命令不同,但协议可以兼容。

在 macOS 上配置:

brew install tinc
mkdir -p /usr/local/etc/tinc/example

新建 /usr/local/etc/tinc/example/tinc.conf:

Name = macos
Device = utun0 # use an unused number
ConnectTo = linux

编辑 /usr/local/etc/tinc/example/tinc-up:

#!/bin/sh
ifconfig $INTERFACE 192.168.0.2 192.168.0.1 mtu 1500 netmask 255.255.255.255

和 /usr/local/etc/tinc/example/tinc-down:

#!/bin/sh
ifconfig $INTERFACE down

还有 /usr/local/etc/tinc/example/subnet-up:

#!/bin/sh
[ "$NAME" = "$NODE" ] && exit 0
/usr/local/opt/iproute2mac/bin/ip route add $SUBNET dev $INTERFACE

以及 /usr/local/etc/tinc/example/subnet-down:

#!/bin/sh
[ "$NAME" = "$NODE" ] && exit 0
/usr/local/opt/iproute2mac/bin/ip route del $SUBNET dev $INTERFACE

然后将它们都设为可执行的:

chmod +x tinc-up
chmod +x tinc-down
chmod +x subnet-down
chmod +x subnet-down

编辑 /usr/local/etc/tinc/example/macos:

Port = 655
Subnet = 192.168.0.1/24

执行 tincd -n example -K 生成密钥。

到 Linux 机器上: 编辑以下文件:

$ mkdir -p /etc/tinc/example/hosts
$ cat /etc/tinc/example/tinc.conf
Name = linux
$ cat /etc/tinc/example/tinc-up
$!/bin/sh
ip link set $INTERFACE up
ip addr add 192.168.0.1/24 dev $INTERFACE
$ cat /etc/tinc/example/tinc-down
$!/bin/sh
ip addr del 192.168.0.1/24 dev $INTERFACE
ip link set $INTERFACE down
$ cat /etc/tinc/example/hosts/linux
Address = linux_ip
Port = 655
Subnet = 192.168.0.1/24
$ tincd -n example -K

接着,把 linux 上 /etc/tinc/example/hosts/linux 拷贝到 macos 的 /usr/local/etc/tinc/example/hosts/linux,然后把 macos 上 /usr/local/etc/tinc/example/hosts/macos 拷贝到 /etc/tinc/example/hosts/macos。在两台机器上都 tinc -n example -D -d3 即可看到连接的建立,通过 ping 即可验证网络建立成功。

2018-05-29 Update: Android 上,利用 Tinc GUI 也可以把 Tinc 运行起来,只是配置不大一样:

$ cat tinc.conf
Name = example
Device = /dev/tun
Mode = switch
ConnectTo = remote
ScriptsInterpreter = /system/bin/sh
$ cat tinc-up
#!/bin/sh
ip link set $INTERFACE up
ip addr add local_ip/24 dev $INTERFACE
$ cat tinc-down
#!/bin/sh
ip addr del local_ip/24 dev $INTERFACE
ip link set $INTERFACE down
$ cat subnet-up
$!/bin/bash
[ "$NAME" = "$NODE" ] && exit 0
ip route add $SUBNET dev $INTERFACE metric $WEIGHT table local
$ cat subnet-down
#!/bin/bash
[ "$NAME" = "$NODE" ] && exit 0
ip route del $SUBNET dev $INTERFACE table local

注意 table local 的使用。需要 Root。

搭建 FTP server behind NAT

我们出现新的需求,要把以前的 FTP 服务器迁移到 NAT 之后的一台机器上。但是,FTP 不仅用到 20 21 端口,PASV 还会用到高端口,这给端口转发带来了一些麻烦。我们一开始测试,直接在 Router 上转发 20 和 21 端口到 Server 上。但是很快发现,Filezilla 通过 PASV 获取到地址为(内网地址,端口高 8 位,端口低 8 位),然后,Filezilla 检测出这个地址是内网地址,于是转而向 router_ip:port 发包,这自然是不会得到结果的。

此时我们去网上找了找资料,找到了一个很粗暴的方法:

iptables -A PREROUTING -i external_interface -p tcp -m tcp --dport 20 -j DNAT --to-destination internal_ip:20
iptables -A PREROUTING -i external_interface -p tcp -m tcp --dport 21 -j DNAT --to-destination internal_ip:21
iptables -A PREROUTING -i external_interface -p tcp -m tcp --dport 1024:65535 -j DNAT --to-destination internal_ip:1024-65535

有趣地是,macOS 自带的 ftp 命令(High Sierra 似乎已经删去)可以正常使用。研究发现,它用 EPSV(Extended Passive Mode)代替 PASV,这里并没有写内网地址,因而可以正常使用。

这么做,Filezilla 可以成功访问了。但是,用其它客户端的时候,它会直连那个内网地址而不是 Router 的地址,于是还是连不上。而且,使用了 1024-65535 的所有端口,这个太浪费而且会影响我们其它的服务。

我们开始研究我们 FTP 服务器 (pyftpdlib) 的配置。果然,找到了适用于 FTP behind NAT 的相关配置:

     - (str) masquerade_address:
        the "masqueraded" IP address to provide along PASV reply when
        pyftpdlib is running behind a NAT or other types of gateways.
        When configured pyftpdlib will hide its local address and
        instead use the public address of your NAT (default None).
     - (dict) masquerade_address_map:
        in case the server has multiple IP addresses which are all
        behind a NAT router, you may wish to specify individual
        masquerade_addresses for each of them. The map expects a
        dictionary containing private IP addresses as keys, and their
        corresponding public (masquerade) addresses as values.
     - (list) passive_ports:
        what ports the ftpd will use for its passive data transfers.
        Value expected is a list of integers (e.g. range(60000, 65535)).
        When configured pyftpdlib will no longer use kernel-assigned
        random ports (default None).

于是,我们配置了 masquerade_address 使得 FTP 服务器会在 PASV 中返回 Router 的地址,并且在 passive_ports 中缩小了 pyftpdlib 使用的端口范围。

进行配置以后,我们在前述的 iptables 命令中相应修改了端口范围,现在工作一切正常。

使用 Nginx 转发 VMware ESXi

我们的 VMware ESXi 在一台 NAT Router 之后,但是我们希望通过域名可以直接访问 VMware ESXi。我们首先的尝试是,把 8443 转发到它的 443 端口,比如:

socat TCP-LISTEN:8443,reuseaddr,fork TCP:esxi_addr:443

它能工作地很好(假的,如果你把 8443 换成 9443 它就不工作了),但是,我们想要的是,直接通过 esxi.example.org 就可以访问它。于是,我们需要 Nginx 在其中做一个转发的功能。在这个过程中遇到了很多的坑,最后终于是做好了(VMware Remote Console 等功能还不行,需要继续研究)。

首先讲讲为啥把 8443 换成 9443 不能工作吧 -- 很简单,ESXi 的网页界面会请求 8443 端口。只是恰好我用 8443 转发到 443,所以可以正常工作。这个很迷,但是测试的结果确实如此。VMware Remote Console 还用到了别的端口,我还在研究之中。

来谈谈怎么配置这个 Nginx 转发吧。首先是 80 跳转 443:

server {
        listen 80;
        listen 8080;
        server_name esxi.example.org;

        return 301 https://$host$request_uri;
}

这个很简单,接下来是转发 443 端口:

server {
        listen 443 ssl;
        server_name esxi.example.org;
        ssl_certificate /path/to/ssl/cert.pem;
        ssl_certificate_key /path/to/ssl/key.pem;

        location / {
                proxy_pass https://esxi_addr;
                proxy_ssl_verify off;
                proxy_ssl_session_reuse on;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}

此时,打开 https://esxi.example.org 就能看到登录界面了。但是仍然无法登录。从 DevTools 看错误,发现它请求了 8443 端口。于是进行转发:

server {
        listen 8443 ssl;
        server_name esxi.example.org;
        ssl_certificate /path/to/ssl/cert.pem;
        ssl_certificate_key /path/to/ssl/key.pem;


        location / {
                if ($request_method = 'OPTIONS') {
                        add_header 'Access-Control-Allow-Origin' 'https://esxi.example.org';
                        add_header 'Access-Control-Allow-Credentials' 'true';
                        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
                        add_header 'Access-Control-Max-Age' 1728000;
                        add_header 'Access-Control-Allow-Headers' 'VMware-CSRF-Token,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Cookie,SOAPAction';
                        add_header 'Content-Type' 'text/plain; charset=utf-8';
                        add_header 'Content-Length' 0;
                        return 204;
                }

                add_header 'Access-Control-Allow-Origin' 'https://esxi.example.org';
                add_header 'Access-Control-Allow-Credentials' 'true';
                proxy_pass https://esxi_addr:443;
                proxy_ssl_verify off;
                proxy_ssl_session_reuse on;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}

主要麻烦的是配置 CORS 的相关策略。我也是看了 DevTools 的错误提示半天才慢慢写出来的。这样配置以后,就可以成功登录 VMware ESXi 了。

20:02 更新:现在做了 WebSocket 转发,目前可以在浏览器中打开 Web Console 了。但是,在访问 https://esxi.example.org/ 的时候还是会出现一些问题,然而 https://esxi.example.org:8443/ 是好的。

转发 WebSocket:

map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
}

server {
        listen 8443 ssl;
        server_name esxi.example.org;
        ssl_certificate /path/to/ssl/cert.pem;
        ssl_certificate_key /path/to/ssl/key.pem;


        location / {

                if ($request_method = 'OPTIONS') {
                        add_header 'Access-Control-Allow-Origin' 'https://esxi.example.org';
                        add_header 'Access-Control-Allow-Credentials' 'true';
                        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
                        add_header 'Access-Control-Max-Age' 1728000;
                        add_header 'Access-Control-Allow-Headers' 'VMware-CSRF-Token,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Cookie,SOAPAction';
                        add_header 'Content-Type' 'text/plain; charset=utf-8';
                        add_header 'Content-Length' 0;
                        return 204;
                }

                add_header 'Access-Control-Allow-Origin' 'https://esxi.example.org' always;
                add_header 'Access-Control-Allow-Credentials' 'true' always;

                proxy_pass https://esxi_addr:443;
                proxy_ssl_verify off;
                proxy_ssl_session_reuse on;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection $connection_upgrade;
        }
}

20:29 更新:找到了 VMware Remote Console 的端口:902,用 iptables 进行 DNAT 即可:

iptables -A PREROUTING -i wan_interface -p tcp -m tcp --dport 902 -j DNAT --to-destination esxi_addr:902

2018-05-09 08:07 更新:最后发现,还是直接隧道到内网访问 ESXi 最科学。或者,让 443 重定向到 8443:

server {
        listen 443 ssl;
        server_name esxi.example.org;
        ssl_certificate /path/to/ssl/cert.pem;
        ssl_certificate_key /path/to/ssl/key.pem;

        return 301 https://$host:8443$request_uri;
}
这样,前面也不用写那么多 CORS 的东西了。

使用 iptables 和策略路由进行带源地址的 forwarding

陈老师打开他的服务器,突然发现 CPU 莫名高负载,然后发现是有一个用户被远程登录拿来挖矿了。但是这台机器在 NAT 后,所以登录的源地址全是 NAT 路由,所以不知道对方的地址是什么。我们为了能使用 fail2ban 来禁用多次尝试失败的 IP,但又不想因为别人把 NAT 路由的地址给禁了,这样我们自己也用不了了。所以必须要让这台机器能够知道 ssh 的源地址,我们现在简单的 socat 方案不能满足这个需求。

需求:

  1. 可以在外网连 NAT 路由的高端口(如 2222)来访问这台机器。
  2. 在内网中,既可以直接连它的内网地址,也可以连 NAT 路由的高端口来访问这台服务器。此时,由于连 ssh 的机器就在同一个子网中,如果保留了源地址,服务器发的包会直接回来不经过 NAT。所以我们还是保留了 socat 的方案。

实现方法:

在 NAT Router 上配置 DNAT,这样发到 NAT Router 上的包就可以转发到服务器上:

iptables -t nat -A PREROUTING -i external_interface -p tcp -m tcp --dport 2222 -j DNAT --to-destination internal_server_ip:22

但是,从服务器回来的包到了 NAT Router 上后,由于路由表的配置问题,默认的路由并不能把包送达对方。

方法 1: 我们首先给包打上 mark:

iptables -t mangle -A PREROUTING -i internal_interface -p tcp -m tcp --sport 22 -j MARK --set-mark 0x2222

然后配置策略路由:

ip rule add fwmark 0x2222 table 2222
ip route add table 2222 default via gateway_address

方法 2: (UPD 2018-07-07) 利用 ip rule 直接达成同样的效果

ip rule add from internal_ip/prefix table 2222
# or
ip rule add iif internal_interface table 2222
ip route add table 2222 default via gateway_address

这样就可以保证 ssh 的回包可以原路返回了。

由于前面提到的原因,上面我们配置的 DNAT 规则只对外网过来的包有效。为了内网的访问,我们仍然采用了 socat 的方式:

socat TCP-LISTEN:2222,reuseaddr,fork TCP:internal_server_ip:22

从不同的机器测试,都可以在 who 看到,地址确实是我们想看到的源地址。接下来配置 fail2ban即可。

利用 UPnP 协议进行 mosh NAT 穿透的研究

由于经常要从宿舍、教室等不同的 Wi-Fi 之间切换,但是 ssh 连接又总是断,所以想用 mosh 代替 ssh。但是 mosh 也有它的问题:

  1. 不能滚动。这个可以在 mosh 中嵌套一层 tmux 解决。我目前写了一些自动 mosh 后打开 tmux 并且开启鼠标支持的脚本,但还是有缺陷。
  2. 在高端口 60000+ 监听 UDP,这使得 NAT 后的服务器难以直接通过端口转发。如果直接转发到 NAT 后的机器,那么 NAT 后面如果有多台机器,这又失效了。

于是找了找网上的 NAT 穿透的一些文章,看到了 UPnP 的方法。大致就是,用户可以向路由器注册一个临时的转发规则,路由会自动在 iptables 上配置转发。但是,这样也会遇到一个问题:路由上的 mosh-server 不知道这个转发的存在,所以它可能会尝试监听同样的端口。解决方案下面会提到。

需求:

Server <---> NAT Router <---> My Laptop
On NAT Router, port 8022 is forwarded to Server:22
1. mosh router # works
2. mosh --ssh="ssh -p 8022" router # works

首先在 NAT Router 上配置 miniupnpd(以 Debian 为例)

sudo apt install miniupnpd
# you will get a dialog upon installation
# input your wan interface and listening ip accordingly
sudo vim /etc/default/miniupnpd
# edit START_DAEMON=0 to START_DAEMON=1
sudo vim /etc/miniupnpd/miniupnpd.conf
# edit ext_ifname, listening_ip accordingly
# set secure_mode=yes
# add 'allow 60000-60023 internal_ip/prefix 60000-60023'
# before the last line 'deny 0-65535 0.0.0.0/0 0-65535'
sudo systemctl enable --now miniupnpd

现在,复制 我修改的 mosh-wrapper.js 到用户的 home 目录下,在 Server 安装 miniupnpc 然后通过:

mosh --ssh="ssh -p 8022" --server=~/mosh-wrapper.js user@router

这样,mosh 首先会通过 ssh 和 Server 协商一个 AES 的密钥和 UDP 端口(如 60001),之后的通信都通过 UDP 端口走加密后的流量。我的 mosh-wrapper.js 通过 miniupnpc 向路由器请求把该 UDP 端口转发到 Server 上,这样, mosh-server 就能通过 NAT 路由穿透到后面的 Server 上。

等会!问题来了:

mosh 默认的 IP 范围是 60000-61000 ,根据我的观察,它会从 60001 开始尝试监听本机地址,如果已经被占用,则 60002, 60003, ... 但是!Router 和 Server 实际上占用了相同的端口空间,并且 mosh 只知道本机哪些端口被占用了,而不知道 Router 和 Server 共同占用了多少端口。

我想到了一些可能的解决方案:

  1. 在 Router 上让 miniupnpd 监听对应的端口,占住这个坑。这样,Router 上的 mosh-server 就不会用和 Server 相同的端口
  2. 如果有多个 Server,则会出现抢夺相同端口的情况。我目前的想法是,让 upnpc 去询问 Router 找空闲的端口,然后再传给 mosh-server 使用。另一种方法则是,给不同的 Server 划分不同的端口范围,比如 Router 用 60001-60005, 然后 Server1 用 60006-60010, Server2 用 60011-60015 如此下去。

然后,新的问题又发现了:

当我在和 Server 同一个子网的时候,由于 miniupnpd 配置的 iptables 规则中来源只有 WAN interface,所以我在内网发的包是不会被转发的。当然,既然在内网了,为啥不直接用内网 IP 呢,不知道 mosh 有没有提供设置备用 IP 的功能。

在 Archlinux 上用 winbind 配合 pam 配置 Windows AD 认证登录

作为不清真的网络管理员,为了配置一套完整的统一认证系统,陈老师采用了 Windows AD 的方法给这里配置统一认证。重装了系统,自然要把之前的统一认证再配到新装的 Archlinux 上。

参考资料: Active Directory Integration

首先安装相应的包:

pacman -S samba

我们还没有配好 Kerberos,所以跳过。

然后配置 /etc/samba/smb.conf,以下是一个例子。可以根据文档微调。

[global]
        security = ads
        realm = YOUR-AD-HERE
        workgroup = YOUR-GROUP-HERE
        idmap uid = 10000-20000
        idmap gid = 10000-20000
        winbind enum users = yes
        winbind enum groups = yes
        template homedir = /home/%D/%U
        template shell = /bin/bash
        client use spnego = yes
        client ntlmv2 auth = yes
        encrypt passwords = yes
        winbind use default domain = yes
        restrict anonymous = 2

这样,域上的用户 user 会拿到 home 目录为 /home/YOUR-DOMAIN-HERE/user,uid 在 10000-2000 范围内的用户。在一会经过配置之后,可以通过 getent passwd 验证。

接下来,需要把本机的 samba 登入到域的管理员,并且启动服务。

net ads join -U your-user-name
systemctl enable --now smb
systemctl enable --now nmb
systemctl enable --now winbind

更改 /etc/nsswitch.conf,在 passwd, shadow 和 group 都增添 winbind:

passwd: files mymachines systemd winbind
group: files mymachines systemd winbind
shadow: files winbind

接下来,可以进一步验证配置是否正确:

wbinfo -u
wbinfo -g
getent passwd
getent group
net ads info
net ads lookup

接下来可以配置 PAM 了。这一部分踩到了一些坑,现在终于做得差不多了。

需求:

  1. 如果一个用户名既有本地用户也有域上的用户,选择前者
  2. 用户要修改密码的话,如果是域用户,则要求走 Windows AD 那套方法改密码;否则仅修改本地用户密码。

实现:

修改 /etc/pam.d/system-auth:

第一部分:auth

auth [success=1 default=ignore]         pam_localuser.so
auth [success=2 default=die]            pam_winbind.so krb5_auth krb5_ccache_type=FILE cached_login try_first_pass
auth [success=1 default=die]            pam_unix.so nullok_secure
auth requisite                          pam_deny.so
auth optional                           pam_permit.so
auth required                           pam_env.so

首先利用 pam_localuser.so 匹配用户名和 /etc/passwd ,如果有, success=1 代表跳过下面一条规则,故会跳到 pam_unix.so 这一行。如果失败,default=ignore 表示忽略它的结果。如果是本地用户,匹配 pam_localuser.so 成功后跳到 pam_unix.so,如果成功了则跳到第五行,pam_permit.so 代表通过,最后由 pam_env.so 配置环境变量。如果是域用户,则由 pam_winbind.so 处理,如果成功,同样跳到第 5 条。如果本地用户和域用户都失败,就 pam_deny.so 认证失败。

第二部分:account

account required                        pam_unix.so
account [success=1 default=ignore]      pam_localuser.so
account required                        pam_winbind.so
account optional                        pam_permit.so
account required                        pam_time.so

这一部分仍有疑问。留待以后来补充。

第三部分:password

password [success=1 default=ignore]     pam_localuser.so
password [default=die]                  pam_echo.so file=/etc/pam.d/messages/ad_reject_change_passwd.txt
password optional                       pam_echo.so file=/etc/pam.d/messages/local_user_passwd.txt
password [success=1 default=die]        pam_unix.so sha512 shadow
password requisite                      pam_deny.so
password optional                       pam_permit.so

这里实现了我们的需求:如果是本地用户,提醒用户当前要修改的是本地用户的密码;如果是域用户,则输出信息后直接拒绝。

这里的 /etc/pam.d/messages/ad_reject_change_passwd.txt 内容如下:

Hi %u, please go to xxxxxxx to change your Active Directory password!

第四部分:session

session   required                      pam_limits.so
session   required                      pam_mkhomedir.so skel=/etc/skel/ umask=0022
session   required                      pam_unix.so
session   [success=1 default=ignore]    pam_localuser.so
session   required                      pam_winbind.so
session   optional                      pam_permit.so

这里与 Wiki 上内容无异。

然后修改 /etc/pam.d/passwd :

password        required        pam_cracklib.so difok=2 minlen=8 dcredit=2 ocredit=2 retry=3
password        include         system-auth
#password       requisite       pam_deny.so
#password       required        pam_unix.so sha512 shadow nullok

首先判断密码强度。通过后则直接用刚才更改的 system-auth 中的 password 部分规则。

这样就配好了认证。自己对这套东西的理解还不够深,以后遇到了要继续钻研。

扩展阅读: PAM 配置简介 - 王邈

在服务器上安装 Archlinux 记录

有一台服务器的 Ubuntu 挂了,我们想在上面重装一个 Archlinux。我们首先下载了 archlinux-2018.04.01 的 ISO, 直接 dd 到 U 盘上,但是遇到了问题。

首先遇到的问题是,一启动之后就会花屏。我们一开始怀疑是 NVIDIA 驱动的问题,于是想改 kernel param 但是发现,这个 ISO 是 hybrid 的,我们在 macOS 和 Windows 上都不能 mount 上这种类型的盘。于是我们选择自己搞分区表。我们把 U 盘插到电脑上,然后在 Linux 虚拟机内重新分区为 GPT,然后 mount 到 /mnt/usb,再重新下载 archlinux iso,不过此时刚好上游更新了 archlinux-2018.05.01 的影响。我们把 ISO 中根分区 mount 到 /mnt/iso 上来,然后 cp -a /mnt/iso/* /mnt/usb 。调整了 grub 中的内核参数,仍然无果。我们认为问题可能在显卡上,就把那张显卡拔下来了,果然显示就正常了,但是新的问题就来了。

一启动,fstab 尝试把 LABEL=ARCHISO_201805 挂在上来,但是失败。于是我们把 U 盘插到 mac 上,用 Disk Utility 给分区命了名,再插回去,然后这个 Live CD 的 Systemd 就成功起来了。接下来就是根据官方的 Installation Guide 进行安装各种东西。安装完后,在 /boot/EFI 的操作上也出现了一些问题,一开始忘记调用 grub-mkconfig ,导致重启以后进入 grub-rescue,所以又回到 Live CD 重新 grub-mkconfig 。同时对 systemd-networkd 也进行了相应的调整,这样开机以后可以配好网络。主要就是在网卡上配上两个 VLAN 和相应的 DHCP 和静态地址。

接下来对以前的东西进行迁移。主要就是按照十分详细的 Arch Wiki 进行相应的配置。由于空间所限,我们把原来的 home 目录直接 mount --bind 到 /home,但是不可避免地,会出现用户 id 不对应的问题。于是我们把需要用到的用户的 /etc/{passwd,group,shadow} 统统拷贝到新的系统的相应地方。然后是配置 winbind,就是按部就班地按照 Arch Wiki 和以前的配置进行更新,然后成功地把 AD 上的用户获取到。此时再次出现了 uid 不对应的问题,此时我们使用 chown -R user:user /home/user 的方法。

剩下的工作就是琐碎的安装各种常用软件。不必多说。

P.S. 我研究出了一个很好用的 mosh + tmux 的 fish function: (但是有时工作有时不工作,不明白什么回事)

function tmosh
    mosh $argv -- tmux new-session bash -c 'tmux set -g mouse on; tmux setw -g mode-keys vi; fish'
end

使用 Cisco AC + AP 组合搭建网络实践

有一台已配置好直接可用的 AC 在地址 ac-address。我们需要搭建交换机 + AP 的网络,并且用一台 Linux 服务器进行 DHCP 从而给 AP 分发 AC 的地址。这里以 systemd-networkd 为例。

我们约定,vlan 2 上联外网,vlan 3 为 Linux 服务器和 AP 的内部网络。

接下来,配置交换机给 Linux 服务器的端口为 trunk 口,然后将下联 Cisco AP 的端口都设为 access vlan 3 模式。接下来在 Linux 服务器上配置 DHCP 服务器和 NAT。

如果 Linux 服务器的 interface 名称为 eno1 :

配置两个 VLAN interface:

$ cat /etc/systemd/network/eno1.network
[Match]
Name=eno1

[Network]
VLAN=eno1.2
VLAN=eno1.3

相应添加 VLAN 配置:

$ cat /etc/systemd/network/eno1.2.network
[NetDev]
Name=eno1.2

[VLAN]
Id=2
$ cat /etc/systemd/network/eno1.3.network
[NetDev]
Name=eno1.3

[VLAN]
Id=3

配置上行的 eno1.2 interface 的静态地址:

$ cat /etc/systemd/network/eno1.2.network
[Match]
Name=eno1.2

[Network]
Address=123.123.123.123/24
Gateway=123.123.123.1
DNS=1.2.4.8

配置内部网络 eno1.3 interface:

$ cat /etc/systemd/network/eno1.3.network
[Match]
Name=eno1.3

[Network]
Address=192.168.1.1/24

配置 dhcpd (isc-dhcp-server):

$ cat /etc/default/isc-dhcp-server
INTERFACESv4="eno1.3"
$ cat /etc/dhcpd.conf
option space Cisco_LWAPP_AP;
option Cisco_LWAPP_AP.server-address code 241 = array of ip-address;

subnet 192.168.1.0 netmask 255.255.255.0 {
  range 192.168.1.100 192.168.1.200;
  option routers 192.168.1.1;
  vendor-option-space Cisco_LWAPP_AP;
  option Cisco_LWAPP_AP.server-address $ac-address;
}

配置 iptables 做 NAT:

iptables -t nat -A POSTROUTING -o eno1.2 -j MASQUERADE
iptables-save > /etc/iptables/iptables.rules

打开 ipv4 forwarding:

echo 1 > /proc/sys/net/ipv4/ip_forward
echo 'net.ipv4.conf.all.forwarding=1' >> /etc/sysctl.d/99-ipv4-forwarding.conf

把 GDB 降级到 8.0.1

在 macOS 上使用 GDB 需要 codesigning。但是在 GDB 升级到 8.1 后这种方法不知道为何失效了。所以我安装回了 GDB 8.0.1 并且重新 codesigning,现在又可以正常升级了。

对 Formula 进行 patch:

diff --git a/Formula/gdb.rb b/Formula/gdb.rb
index 29a1c590..25360893 100644
--- a/Formula/gdb.rb
+++ b/Formula/gdb.rb
@@ -1,14 +1,15 @@
 class Gdb < Formula
   desc "GNU debugger"
   homepage "https://www.gnu.org/software/gdb/"
-  url "https://ftp.gnu.org/gnu/gdb/gdb-8.1.tar.xz"
-  mirror "https://ftpmirror.gnu.org/gdb/gdb-8.1.tar.xz"
-  sha256 "af61a0263858e69c5dce51eab26662ff3d2ad9aa68da9583e8143b5426be4b34"
+  url "https://ftp.gnu.org/gnu/gdb/gdb-8.0.1.tar.xz"
+  mirror "https://ftpmirror.gnu.org/gdb/gdb-8.0.1.tar.xz"
+  sha256 "3dbd5f93e36ba2815ad0efab030dcd0c7b211d7b353a40a53f4c02d7d56295e3"

   bottle do
-    sha256 "43a6d6cca157ef70d13848f35c04e11d832dc0c96f5bcf53a43330f524b3ac40" => :high_sierra
-    sha256 "fe7c6261f9164e7a744c9c512ba7e5afff0e74e373ece9b5aa19d5da6443bfc2" => :sierra
-    sha256 "cd89001bcf8c93b5d6425ab91a400aeffe0cd5bbb0eccd8ab38c719ab5ca34ba" => :el_capitan
+    sha256 "e98ad847402592bd48a9b1468fefb2fac32aff1fa19c2681c3cea7fb457baaa0" => :high_sierra
+    sha256 "0fdd20562170c520cfb16e63d902c13a01ec468cb39a85851412e7515b6241e9" => :sierra
+    sha256 "f51136c70cff44167dfb8c76b679292d911bd134c2de3fef40777da5f1f308a0" => :el_capitan
+    sha256 "2b32a51703f6e254572c55575f08f1e0c7bc2f4e96778cb1fa6582eddfb1d113" => :yosemite
   end

   deprecated_option "with-brewed-python" => "with-python@2"

近来做 Stanford CS140e 的一些进展和思考(8)

上一篇文章之后,我其实还是很忙,但是一直心理惦记着这件事,毕竟只剩最后的一点点就可以做完了,不做完总是觉得心痒。

今天做的部分是调度。我们目前只在 EL0 运行了一个 shell,每当触发 exception 时回到 kernel 进行处理,再回到原来的地方。但现在,我要实现一个 preemtive round-robin scheduler,就需要管理当前的所有进程,并且维护当前的进程状态,当时钟中断到来的时候,决定下一个 time slice 要执行的进程,再切换过去。这个过程当然会遇到不少的坑。

首先,我们需要判断一个进程是否可以执行了。考虑到阻塞的 IO,作者提供了一个优雅的方法:如果这个进程阻塞在 IO 上,那么,提供一个函数,在 scheduler 中调用,判断所需要的数据是否到达。这样,我们就可以一个循环把下一个 time slice 要执行的线程找到。如果找不到,就等待 interrupt 再尝试。

困难的地方在于,在启动的时候,切换到一个起始线程。并且在上下文切换的时候,在 process 1 -> kernel -> process 2 这两步过程中,有许多寄存器都需要仔细考虑如何实现。并且在这个过程中,我也发现了之前写的代码中的问题,最终修复了(目前来看是 working 了)。

我的代码实现在 这里 。下一步就要写 syscall 了。希望能在期中前抽时间赶紧把这个做完。

18:54 PM Update: 刚实现完了 sleep 的 syscall。比预想中要简单。果然找到了自己实现的调度器的 BUG。此系列大概是完结了。

2019-02-12 Update: 下一篇文章

近来做 Stanford CS140e 的一些进展和思考(7)

上一篇文章之后,我很长时间都没有在继续我这个项目,清明节刚好闲下来了我就回来继续啃它。Stanford 那边已经结课,最后的 3-spawn 也只有一部分,剩下的部分不知道什么时候作者才会填上去了。

这次主要要写的代码就是,对异常的处理。这里的异常并不是我们编程语言中的 catch/throw,而是硬件的异常。AArch64 和 x86 一样,也有不同的特权级别的区分,前者是 EL0~EL3,后者则是 RING0 和 RING3。特权级别高可以往特权级别低转换,但是反过来,只能通过异常的方式提高特权等级,并且切换特权等级后只有固定的一些代码可能会跳转,这就是 exception handler/vectors 。这些函数可以知道是什么原因调用了他们,根据硬件规定好的文档,我们可以知道发生了什么事情,是对齐出错了呢,还是用户调用了 syscall 呢,等等。根据不同的情况,我们需要进行不同的处理。当处理完之后,我们需要考虑,跳转回用户代码的时候,回到哪里,提供什么值,不提供什么。

实现的话,需要很多步骤。首先是构造好 exception vector ,这里作者已经写好了一个宏(这里 @BenYip 遇到了一个 assembler 的 BUG),直接用宏就可以把它写出来。然后,我们需要把它加载到当前 EL 的 VBAR_ELx 寄存器中,当 CPU 抛出异常的时候,就会找到这里相应的处理器进行处理。进到这里以后,我们首先先不考虑太多上下文保存的事情--我们先保证能处理异常,恢复也是个有很多坑的步骤,作者也是在这里分成了两个 Subphase。首先还是从 ESR_ELx 中解析到错误的来源的具体内容,如果是我们在 shell 中自己调用的 brk 2 指令,我们就自己新开一个 shell ,修改了提示符以示区别。这样,我们就成功地捕捉到了这个异常。由于我们还无法恢复回去,所以我们直接死循环。

接下来我们要做的是,从异常中恢复出来。由于用户代码可能在各种地方抛出异常,异常也分同步和异步两种情况,这里有许多需要考虑的问题。为了简化,我们目前只考虑同步的 brk 2 导致的 Brk 异常。为了能恢复之后能够正常运行,我们需要把所有的寄存器都保存下来,即 TrapFrame 。保存的时候需要讲究 AArch64 平台下 SP 寄存器的对齐问题。我们也要把一些特殊的寄存器保存下来。还有一点,就是,因为 exception handler 中调用了 context_save 函数,所以此时的 lr 本身也需要进行保存,这个地方也卡了我很久。最后,再把这些一个一个地恢复到原来的样子,调整 ELR_EL1 使得退回到原来的状态时,会跳过当前的 brk 2 指令,调用它的下一调指令。这样,我们就成功地在遇到异常时,弹出一个 shell ,而且还可以回退回来。

学到了很多很多。之后大三,我们可能需要做自己的 CPU,在自己的 CPU 上跑自己的操作系统,在自己的操作系统中跑自己的编译器,在自己的编译器中编译一个数据库。希望到时我还活着吧。#flag

更新:下一篇在这里

最近比较忙

最近一直没有更新我的 CS140e 系列文章,是因为最近一直忙于各种事情。等有空了再更新吧。

偶遇清华吴文虎教授

今天百团大战,正准备收摊的时候,天空工场那边来了一位长者,在和他们聊着什么。我很感兴趣,就上去听。老人大概已有八十高龄(后来查,是 1936 年生),但依然精神矍铄,首先和我们讲,作为工科的学生,一定在理解原理的基础上,多多去实践。他举了他自己的例子,他首先在电机系学习,后来,计算机系成立(当时还是自动控制系),他转到了计算机系,重新学起了计算机,说计算机编程学起来并没有什么难的。当年,苹果公司送过来了中国第一台 Apple-2,他们就把电脑拆了下来研究原理,又装上去继续工作。后来,他就在计算机系任教,教的正是《程序设计基础》这门课程。他十分重视实践,在第一年开课的时候就说,最关键的就是实践,安排了一些编程实验课,期中期末就是大作业。一开始有一些同学不重视实践,结果期末就挂科了。后来同学们就明白了实践的重要性,实践起来发现并没有那么难,最后就说,“吴老师,你说得对”。他又谈到了他的体育,他当年是北京长跑代表队的集训队选手,擅长一千五百米项目,他三千米只需要九分钟就能跑完。我们都感到自愧不如。我们说,现在的《程序设计基础》是徐明星老师在教,他说徐明星是他的博士生,邬晓钧也是他的博士生,他另外还有一个高徒我记不清楚了。他还是 IOI 中国队的前教练,听到我们有过一些 OI 基础,表示了赞许和鼓励。还有一些细节记不清楚了,记起来了再补充吧。

〖新手向〗绕过 C++ 类的访问限制

这是一篇很水的文章,面向萌新,已经知道了的可以自觉绕道。

昨天上课,有同学问,如果用户偷偷把 private 改成 public 再和原有的库链接,是不是就可以在用户代码里更改了。这个答案是肯定的。下面我们就做个实验:

首先,创建 good_class.h 和 good_class.cpp:

class SomeClass {
private:
    int data;
public:
    int getData();
};
#include "good_class.h"

int SomeClass::getData() {
    return data;
}

然后,首先编译,

clang++ -c good_class.cpp -o good_class.o

然后,修改 good_class.cpp 并写一个 evil_user.cpp

class SomeClass {
public:
    int data;
public:
    int getData();
};
#include <stdio.h>
#include "good_class.h"

int main() {
    SomeClass a;
    a.data = 37;
    printf("%d\n", a.getData());
    return 0;
}

编译:

clang++ good_class.o evil_user.cpp -o evil

然后 evil 如愿地输出了 37

一些提醒:

  1. C++ 的访问控制十分的弱,仅仅是编译期。所以是很容易绕过的。
  2. 对于不想泄露源代码的库,不要导出 C++ 的类和函数。选择导出 C 函数,结构体用 incomplete type 或者干脆 void *

扩展阅读: L 叔的通过虚函数表访问私有函数

近来做 Stanford CS140e 的一些进展和思考(6)

上一篇文章之后,作者终于更新了测试的用例,我的程序终于可以成功跑过所有测试,也成功在树莓派跑起来。不过,我的代码中很多地方的错误处理比较偷懒,往往直接 panic ,显然并不友好。同时,我想到了使用 cargo-fuzz 来进行自动化测试,果然,使用这个很快就修复了不少我没想到的会出错的地方,比如乘法溢出,目录项没有正确结束等等。目前还发现一个 timeout 的问题,研究发现大概是文件的 cluster chain 中出现了环,导致一直读取文件而没有停止。要解决这个问题,我目前想到的是 Floyd 的判圈算法,但还没上实现。等过几天,新的 Assignment 3 出了以后,再继续更新。希望作者少点跳票,多点勤奋,哈哈哈哈哈

更新:下一篇在这里

近来做 Stanford CS140e 的一些进展和思考(5)

上一篇文章之后,作者多次延期跳票之后(again),终于放出了 Assignment 2 Phase 3: Saddle Up 。这次,我们要做的变成了把已经写好的(错漏百出)的 fat32 的驱动搬到树莓派里面去,然后实现一些基本的 shell 命令: ls cat cd 等等。作者首先更新了老版本的新的测试样例,放了一些映像然后提供了预期的结果,结果发现,这里的 fat32 有一些不同,主要的就是 bytes_per_sector 不是 512 了,意味着物理的扇区和逻辑扇区并不一致。同时, sectors_per_cluster 也不是 1 了,需要考虑多个扇区的情况。同时, read_cluster 传入的 offset 也可能不再是第一个 sector 中的,所以需要做一个处理。对于物理和逻辑扇区的问题,作者推荐的方案是,把 fat32 之外的扇区保持不变,把其内的扇区视为逻辑扇区。这样,其它代码都可以透明地工作,而不用到处更改,这就体现了封装的威力。接着,作者提供了一个写好了的 libsd 和一些导出的函数,使用这些函数即可。不过,在错误处理和 timeout 上也遇到了一些坑。后面,把东西搬到树莓派上运行,问题就出现了:读取了第一个扇区(即 MBR 所在的扇区)之后,直接就死掉了。想了半天都没找到方案,突然想起可以利用 panic! 对错误语句进行二分查找。查找了大概有七八个小时之后,终于发现,问题出现在读取一个 u32 类型的变量上。我起初怀疑是栈出了问题,所以放到堆上分配,然而还是不行。忽然想起以前遇到的对齐问题,在 AArch64 架构上,可能为了简化,读取的 u32 必须对齐到四个字节上。于是找了找 Rust 中的对齐方面的文档,找到了 #[repr(align=4)] 这种表示方法,代替了原来的 #[repr(packed)] ,并且把数据先拷贝到对齐后的栈上的对应数据结构,然后再读取对应的项。果然,这个问题就解决了。然后又发现我的盘中会出现 lfn 项并不是从后往前的情况,于是我又修改了一下相关的代码。现在,终于可以成功地 ls cat cd

不过还是要吐槽一下,作者的测试用的映像文件中,会出现 0xE5 表示这个项已经被删除的情况,但是似乎作者的代码并没有处理这个,所以在预期的输出中出现了一些明显不正确的结果,导致我的代码跑测试并不能通过。而且,作者的代码在一些情况下会把文件的后缀漏掉。作者后来更新了几次测试的文件,不过这个问题只解决了一部分,并没有完全解决。坐等作者继续放出新的测试文件吧。

更新:下一篇在这里

近来做 Stanford CS140e 的一些进展和思考(4)

上一篇文章之后,作者多次延期跳票之后,终于放出了 Assignment 2 Phase 2:32-bit Lipids ,这两天就把只读 FAT32 写完了(不过封装得并不好,许多地方利用了 pub(super) 把变量可以访问的范围控制到 vfat 中,然后直接读,只有少数需要特殊处理的进行了函数的封装)。首先当然是研究了半天 MBRFAT32 的结构,拿了不同来源的 FAT 结构说明进行对比和验证,最后终于把格式搞清楚了,先实现了 MasterBootRecord ,这个其实很好实现,以前也有接触过 MBR ,本身也很简单。然后就是根据 MBR 找到第一个 FAT32 的分区,根据偏移找到分区的开头,开头的第一个扇区就是 EBPB 数据结构,里面保存了 FAT32 分区的各种信息。根据里面的信息,可以找到 FAT 表的位置和数量,还有数据部分的 Cluster 的位置和数量。接着,解析一下 FAT 表,实际上是一个与 Cluster 一一对应的链表结构,用特殊的数据代表链表的尾和空、坏扇区。利用这些,和 EBPB 中根目录所在的第一个 Cluster ,先在 VFat 里面实现了读取一个 Cluster 链的内容的函数,利用这个函数读取一个一个的目录项,解析目录项,把长文件名的项合并到一个之中,然后对应地丢到 Entry 对象中,目录则可以枚举子目录项,根据名字比较去找子目录或者子文件夹,文件则实现了 io::Readio::Seek 使得可以读取文件的内容。实现好了这些以后,就拿了 raspbian-strech-lite.img 作为硬盘映像,从文件里读取文件信息,成功地把 config.txt 读取出来。

其中还是遇到许多困难,如各种偏移的计算,如何处理跨 Cluster 和跨 Sector 的读写,等等,有不少的坑在其中,花了两天的空余时间才差不多完善了这个功能。还有就是利用 Rust 现有的功能完成 C 里面很轻易就可以实现的指针操作,也花了不少时间。

更新:下一篇在这里