计算机 – ntzyz's blog https://archive.ntzyz.io Wed, 20 Dec 2017 16:49:56 +0000 zh-CN hourly 1 https://wordpress.org/?v=5.8 使用 systemd-nspawn 快速创建 Linux 容器 https://archive.ntzyz.io/2017/12/21/use-systemd-nspawn-to-create-linux-container/ https://archive.ntzyz.io/2017/12/21/use-systemd-nspawn-to-create-linux-container/#respond Wed, 20 Dec 2017 16:32:08 +0000 https://ntzyz.io/?p=1003 继续阅读使用 systemd-nspawn 快速创建 Linux 容器]]> div.main > p { text-indent: 2em; }

使用 `systemd-nspawn` 这个命令我们可以很方便的创建一个 Linux 容器,需要的只是一个使用 systemd 作为 init 的 Linux 发行版的根文件系统。通过创建容器,我们可以获得一个可以随便折腾而不用担心损坏的 Linux 环境。这里用 Ubuntu 16.04 和 CentOS 7 为例,整个过程可以说是非常简单(虽然比起 Docker 还是麻烦了点)

对于 Ubuntu,可以直接从源里下载到它的根文件系统。下载一份,并解压到 `/var/lib/machines/ubuntu1604`:

sudo mkdir -p /var/lib/machines/ubuntu1604
wget http://mirrors.ustc.edu.cn/ubuntu-cdimage/ubuntu-base/releases/16.04.3/release/ubuntu-base-16.04.1-base-amd64.tar.gz -O /tmp/rootfs.tgz
sudo tar xpzf /tmp/rootfs.tgz -C /var/lib/machines/ubuntu1604

OK,到此为止我们就得到了一个可以被 `systemd-nspawn` 启动的 rootfs,不过我们还需要一些配置,例如修改 root 密码等等:

chroot /var/lib/machines/ubuntu1604 /usr/bin/passwd root
echo ubuntu > /var/lib/machines/ubuntu1604/etc/hostname

下面只需要用 `systemd-nspawn` 来“启动”这个容器:

systemd-nspawn -b -D /var/lib/machines/ubuntu1604 --bind=/lib/firmware

这样就完成了!相当简单吧~输出内容大概是这样:

Spawning container ubuntu1604 on /var/lib/machines/ubuntu1604.
Press ^] three times within 1s to kill container.
systemd 229 running in system mode. (+PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ -LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD -IDN)
Detected virtualization systemd-nspawn.
Detected architecture x86-64.

Welcome to Ubuntu 16.04.1 LTS!

Set hostname to .
Failed to install release agent, ignoring: No such file or directory
[  OK  ] Listening on Journal Socket (/dev/log).
[  OK  ] Started Dispatch Password Requests to Console Directory Watch.
[  OK  ] Started Forward Password Requests to Wall Directory Watch.
[  OK  ] Reached target Paths.
[  OK  ] Reached target Remote File Systems (Pre).
[  OK  ] Reached target Remote File Systems.
[  OK  ] Listening on /dev/initctl Compatibility Named Pipe.
[  OK  ] Created slice System Slice.
[  OK  ] Reached target Slices.
[  OK  ] Created slice system-getty.slice.
[  OK  ] Reached target Swap.
[  OK  ] Reached target Encrypted Volumes.
[  OK  ] Listening on Journal Socket.
         Mounting Huge Pages File System...
[  OK  ] Reached target Sockets.
         Starting Remount Root and Kernel File Systems...
         Mounting POSIX Message Queue File System...
         Starting Journal Service...
         Mounting FUSE Control File System...
[  OK  ] Mounted POSIX Message Queue File System.
[  OK  ] Mounted Huge Pages File System.
[  OK  ] Mounted FUSE Control File System.
[  OK  ] Started Remount Root and Kernel File Systems.
[  OK  ] Reached target Local File Systems (Pre).
[  OK  ] Reached target Local File Systems.
         Starting Load/Save Random Seed...
[  OK  ] Started Load/Save Random Seed.
[  OK  ] Started Journal Service.
         Starting Flush Journal to Persistent Storage...
[  OK  ] Started Flush Journal to Persistent Storage.
         Starting Create Volatile Files and Directories...
[  OK  ] Started Create Volatile Files and Directories.
         Starting Update UTMP about System Boot/Shutdown...
[  OK  ] Reached target System Time Synchronized.
[  OK  ] Started Update UTMP about System Boot/Shutdown.
[  OK  ] Reached target System Initialization.
[  OK  ] Started Daily Cleanup of Temporary Directories.
[  OK  ] Reached target Basic System.
         Starting Permit User Sessions...
         Starting LSB: Set the CPU Frequency Scaling governor to "ondemand"...
         Starting /etc/rc.local Compatibility...
[  OK  ] Started Daily apt activities.
[  OK  ] Reached target Timers.
[  OK  ] Started Permit User Sessions.
[  OK  ] Started /etc/rc.local Compatibility.
[  OK  ] Started Console Getty.
[  OK  ] Reached target Login Prompts.
[  OK  ] Started LSB: Set the CPU Frequency Scaling governor to "ondemand".
[  OK  ] Reached target Multi-User System.
[  OK  ] Reached target Graphical Interface.
         Starting Update UTMP about System Runlevel Changes...
[  OK  ] Started Update UTMP about System Runlevel Changes.

Ubuntu 16.04.1 LTS ubuntu console

ubuntu login: 

值得注意的是,这个容器和虽然看起来很像那么一回事儿,但是它的内核和网络仍然是使用的宿主机的,如果宿主机已经运行了 sshd,容器里尝试运行则会提示端口被占用,不过 `systemd-nspawn` 提供了桥接网络之类的功能,具体方法可以 Google。关闭容器很简单,在里面执行 `systemctl poweroff` 即可。

CentOS 稍微复杂一点,因为他没有直接提供最小的 rootfs,我们要自己从 ISO 中解压安装,整个过程如下:

# 切换到超级用户
sudo -s

# 建立一些文件夹
mkdir -p /var/lib/machines/centos7
mkdir -p /tmp/iso
mkdir -p /tmp/squashfs
mkdir -p /tmp/rootfs

# 挂载 CentOS 7 的系统盘
mount /mnt/Disk2/OS/Linux/CentOS-7-x86_64-Minimal-1708.iso /tmp/iso
mount /tmp/iso/LiveOS/squashfs.img /tmp/squashfs
mount /tmp/squashfs/LiveOS/rootfs.img /tmp/rootfs

# 复制系统文件,速度看你的硬盘,可能会比较慢_(:з」∠)_
cp -pr /tmp/rootfs/* /var/lib/machines/centos7

# 卸载一些不会再用到的镜像
umount /tmp/{rootfs,squashfs}

# 安装一下 yum
mkdir -p /var/lib/machines/centos7/mnt/iso
mount --bind /tmp/iso /var/lib/machines/centos7/mnt/iso

chroot /var/lib/machines/centos7 /usr/bin/rpm -ivh --nodeps /mnt/iso/Packages/rpm-4.11.3-25.el7.x86_64.rpm
chroot /var/lib/machines/centos7 /usr/bin/rpm -ivh --nodeps /mnt/iso/Packages/yum-3.4.3-154.el7.centos.noarch.rpm

# 配置一下基本系统,执行最小安装
echo "[cdrom]
name=Install CD-ROM 
baseurl=file:///mnt/iso
enabled=0
gpgcheck=1
gpgkey=file:///mnt/iso/RPM-GPG-KEY-CentOS-7" > /var/lib/machines/centos7/etc/yum.repos.d/cdrom.repo

chroot /var/lib/machines/centos7 /usr/bin/yum --disablerepo=\* --enablerepo=cdrom -y reinstall yum
chroot /var/lib/machines/centos7 /usr/bin/yum --disablerepo=\* --enablerepo=cdrom -y groupinstall "Minimal Install"

# 删掉 ISO 源
rm /var/lib/machines/centos7/etc/yum.repos.d/cdrom.repo

# 卸载 ISO
umount /var/lib/machines/centos7/mnt/iso /tmp/iso

# 设置一下 root 密码之类的
chroot /var/lib/machines/centos7 /usr/bin/passwd root

# 进入虚拟环境,执行这段脚本
systemd-nspawn -D /var/lib/machines/centos7 --bind=/lib/firmware << _END_POSTINSTALL_
# 换源,先备份
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
curl https://lug.ustc.edu.cn/wiki/_export/code/mirrors/help/centos\?codeblock=3 > /etc/yum.repos.d/CentOS-Base.repo

# 更新
yum makecache
# 安装一个缺失的依赖,为什么会缺我也不知道…
yum install lvm2-libs -y

# 这些服务是作为一个容器不需要的,把他们关掉
systemctl disable auditd.service
systemctl disable kdump.service
systemctl disable multipathd.service
systemctl disable network.service
systemctl disable smartd.service
systemctl disable lvm2-monitor.service
systemctl disable sshd.service

# 设置 locale,如果需要中文就用 zh_CN.UTF-8
echo LANG=en_US.UTF-8 > /etc/locale.conf

# 设置主机名
echo CentOS-7 > /etc/hostname

_END_POSTINSTALL_

# 然后就可以开机了(((
systemd-nspawn -b -D /var/lib/machines/centos7 --bind=/lib/firmware

参考资料:
]]>
https://archive.ntzyz.io/2017/12/21/use-systemd-nspawn-to-create-linux-container/feed/ 0
小实验:扩展 PS4 的存储 https://archive.ntzyz.io/2017/09/18/extend-storage-with-iscsi-and-usb-gadget-for-your-ps4/ https://archive.ntzyz.io/2017/09/18/extend-storage-with-iscsi-and-usb-gadget-for-your-ps4/#respond Mon, 18 Sep 2017 05:40:11 +0000 https://ntzyz.io/?p=866 继续阅读小实验:扩展 PS4 的存储]]> PS4 到手之后游戏买买买,500G的磁盘换成了1TB也快用完了,但是买2TB的话就感觉很麻烦,一来是要全部重新迁移数据,二是换下来的1TB WD Black也没其他地方用,就很难受。

查了一圈后发现,PS4 在某次系统更新后提供了将 USB 存储格式化为扩展存储的功能,也就是说可以用 USB3.0 的移动硬盘来装游戏。可能是有偏见吧我一直觉得移动硬盘是非常不靠谱的东西,不过这个特性倒是一个不错的拓展存储的切入点。

要说扩展存储,如果是普通的 PC 上有这个需求,解决方法非常多,比如我可以建立一个 SMB 协议的共享,Windows 和 macOS 原生兼容,Linux 只需要加载 cifs.ko 即可;当然 NFS 来实现也没有任何问题;如果对加密有需求,或是需要一个裸磁盘,还可以用 iSCSI 来做到。可是 PS4 目前没有破解,以上方法都不可行。

于是就想,PS4 支持 USB 存储的扩展,我能不能将上面提到的一些方式通过 USB 暴露给 PS4 来实现呢?比如这样一个设备:它本身是一个 USB 从机,同时有一个 GbE 接口,可以千兆访问到局域网里的 iSCSI Target,然后通过一些魔法,将 iSCSI Target 拿到的磁盘设备直通到 USB,让 PS4 认为这是一个大容量的 USB 存储。

为了探究这个想法的可行性,我就去某宝买了个 RK3399 的开发板:这货带一个 USB Type-C 的接口和一个千兆以太网接口,同时还有这不错的 CPU 性能,只可惜 Mali GPU 的驱动暂时没法搞到。软件上支持 Android 和 Linux。

板子到手上电后发现预装了 Android 和 Ubuntu 双系统,二话不说果断重灌,Android 我没有需求,而 Ubuntu 不太想用(Arch 大法好),于是找了张 SD 卡往里面放了 ARM64 的 Arch Linux ARM 的根文件系统,使用 Rockchip 的工具修改 parameter 分区内容,指定 root 分区为 SD 卡并重启,等到开机结束后就可以安装 arch-install-scripts 并把 eMMC 内的 Ubuntu 彻底带走了。不过记得要备份一下 Ubuntu 的 /system 目录,里面的 firmware 以后还是要用的(鬼知道为什么是 /system/etc/firmware 而不是 /etc/firmware,rockchip 改过的内核毒性不小)。如果对 U-boot 没有操作需求的话这样就基本够用了,重启后进入 Arch Linux 并进行基本的配置(网络,pacman 源,blabla)。想要使用全功能的 U-boot 的话,可以参考这篇里的一些信息自己编译 rockchip 提供的 u-boot,firefly 给的裁剪严重基本没法用。

然后就要试试如何将 TypeC 接口的 USB 变身成为一个假 U 盘了。本来以为这块要写内核模块的,结果看了看文档发现其实很简单:内核将 USB Gadget 的配置接口通过一个文件系统暴露给了用户,只需要在这个 configfs 里面比划比划,创建点文件,搞点链接就可以完成了。代码和注释放一起吧:

# 首先是要确认一下 configfs 有没有挂载,以及挂载的位置
mount | grep configfs
# 嗯,通常都是在 /sys/kernel/config,于是我们就过去呗
pushd /sys/kernel/config/usb_gadget
# 创建一个 USB Gadget 并添加一个大容量存储的 function
mkdir g.1/functions/mass_storage.0 -p
# 等待内核完成相关工作(脚本时必要)
sleep 1
# 写入需要被直通的设备文件,或者直接是文件也行,不过小心 MMC 被写穿(笑)
# 这里先用 SD 卡试试
echo "/dev/mmcblk0" > g.1/functions/mass_storage.0/lun.0/file
# 标记为不可移动,即不是 U 盘
echo 0 > g.1/functions/mass_storage.0/lun.0/removable

# 创建一些字符串啊和 ID 的信息
mkdir g.1/strings/0x409
mkdir -p g.1/configs/c.1/strings/0x409
echo 0xa4a2 > g.1/idProduct
echo 0x0525 > g.1/idVendor
echo 1234567890 > g.1/strings/0x409/serialnumber
echo ntzyz > g.1/strings/0x409/manufacturer
echo "iSCSI over USB" > g.1/strings/0x409/product

# 创建配置,并将之前设置的 function 链接过来
echo "cnf1" > g.1/configs/c.1/strings/0x409/configuration
ln -s g.1/functions/mass_storage.0 g.1/configs/c.1

# 指定这个 Gadget 使用的控制器,可以通过 find /sys -name gadget 来确认
echo fe800000.dwc3 > g.1/UDC

# 回到之前的CWD
popd

掏出你的 USB 3.0 Type-C 数据线,连接好 RK3399 和你的电脑,然后执行这个脚本,不出意外的话就能看到新硬件了,同时内核大概有这些输出:

[   52.614900] fusb302 4-0022: PD disabled
[   52.617919] cdn-dp fec00000.dp: [drm:cdn_dp_pd_event_work] Not connected. Disabling cdn
[   52.628791] rockchip-dwc3 usb@fe800000: USB peripheral connected
[   52.636063] android_work: did not send uevent (0 0           (null))
[   52.649973] android_work: sent uevent USB_STATE=CONNECTED
[   52.679112] configfs-gadget gadget: super-speed config #1: c
[   52.679840] android_work: sent uevent USB_STATE=CONFIGURED

这就完成了!是不是很简单?

然后就是让 RK3399 能访问到一个远程磁盘了,当然你也可以往 RK3399 上加 SATA 硬盘,不过这样似乎还不如直接用移动硬盘来得方便(笑)。原则上你可以用 NFS/CIFS 之类的网络文件系统,访问到 NAS 的一个分区,然后 DD 出一个比较大的稀疏磁盘镜像,将他格式化,然后直接用这个文件来充当磁盘。可是不知道为什么我这样试的时候,写入一个 1GB 的动画片在刚起步的时候飙到了 300MB/s,没到两秒就降速到了 0,同时看 RK3399 的串口输出看到了内核 panic 的报错((

果然这种情况还是需要用 iSCSI 啊!Windows 上如果想开启 iSCSI 目标服务,可以选择换一个 Server 版的 Windows,内置了 iSCSI 目标服务器的功能,当然买不起/不想换的可以找一些用户层的软件来实现,比如对个人用户免费的 StarWind ,不过这货好像不支持创建稀疏的 img,有点蛋疼。Linux 上可以参考 Arch Wiki 上 Open iSCSI 相关条目的信息。iSCSI 目标服务的配置这里就不废话了,比较简单,用户认证什么的用 CHAP 就行了,不需要太复杂。

RK3399 方面,首先向 iSCSI 服务器发起一个 sendtargets 请求:

iscsiadm -m discovery -t sendtargets -p 10.10.28.75

得到 target 列表后就可以登陆了:

iscsiadm -m node --targetname=iqn.2008-08.com.starwindsoftware:ntzyz-gen8-ps4volume --login

不出意外这时候内核会甩你一脸日志,比如发现 sda 这种,这时候你就成功的创建了与服务器的连接,所有在 sda 上的变更都会被写入到远程磁盘上。只要将这个设备文件作为 Gadget 中 Mass Storage 的 lun.0 对应的 file,就实现了我们的终极目标:USB 接口的远程磁盘!

将 USB 口从电脑上拔下,插到隔壁 PS4 上,进入设置->周边设备->USB 设备就能看到一个名为iSCSI over USB,制造商为 ntzyz 的移动硬盘,将他格式化为扩展存储后就可以使用了。经测试,将 NieR Automatic 移动到该拓展存储并执行没有明显的体验上的区别。

]]>
https://archive.ntzyz.io/2017/09/18/extend-storage-with-iscsi-and-usb-gadget-for-your-ps4/feed/ 0
简易 Web Terminal 的实现 https://archive.ntzyz.io/2017/08/26/implementation-of-web-terminal/ https://archive.ntzyz.io/2017/08/26/implementation-of-web-terminal/#respond Sat, 26 Aug 2017 09:02:30 +0000 https://ntzyz.io/?p=848 继续阅读简易 Web Terminal 的实现]]> p.indent { text-indent: 2em; }

说起来也是有趣,本来是研究一下 WebSocket 准备给论坛/博客增加实时更新之类的特性,结果看着看着就脑洞大开搞了这么个玩意儿((

首先明确一下,这里说的 Web Terminal 是指再网页中实现的,类似于终端模拟器的玩意儿。举例的话应该是类似于 Linode 的 LiSH 和 Visual Studio Code 中内置的那个终端,而不是 ConoHa 提供的 VNC 式的终端(其实那玩意儿是个远程桌面了)。最终目标的效果就是和 Secure Shell 类似:打开一个网页,就能启动一个网页所在服务器的 shell,比如到处都有的 bash 或者非常强大的 zsh,然后就可以与这个终端进行交互式的操作,比如使用 vim 编辑文件,或者查阅 man 中的手册。

让我们从最简单的一些需求开始,如果只是需要远程执行一些命令或者脚本,那么我们只需要任何一个能调用系统 shell 的编程语言就行了。这里以 node 为例,代码很简单:

'use strict';
const express = require('express');
const child_process = require('child_process');
let server = express();

server.get('/eval', (req, res) => {
  if (req.query.cmd) {
    child_process.exec(req.query.cmd, (err, stdout, stderr) => {
      res.send({
        status: 'ok',
        stdout,
        stderr,
      });
    })
  } else {
    return res.send({
      status: 'ok';
    })
  }
});

server.listen(8123, 'localhost', () => {
 console.log(`Server running at http://localhost:8123`);
});

安装好 express 后,执行这段代码,打开另一个终端,执行代码:

curl 'http://localhost:8123/eval?command=ls'

我们就能看到 node 所在目录的文件列表了。看起来不错,但是如果需要执行面向终端的程序呢?

面向终端的程序,顾名思义,这种程序需要控制一个终端,同时有能力进行 job-control(fg 等) 和终端相关的信号(SIGINT 等)。典型的面向终端的程序就有:nano、vim、htop、less,等等。要让这些程序执行,我们需要通过 POSIX 的接口来创建一个伪终端(pseudoterminal,简称 pty)。

伪终端由两个虚拟的设备组成:一个 pseudoterminal master 和一个 pseudoterminal slave(pts)。这两个虚拟设备之间可以互相通讯,类似一个串口设备。两个进程可以打开这两个虚拟设备进行通讯,类似于一个管道。伪终端的关键就是 pts,这个设备在操作上和真实的终端设备(tty1, ttyS1, …)基本一致,不同之处在于 pts 没有速率之类的属性。所有能在 tty 上使用的操作都能在 pts 上使用,不支持的部分属性会被自动忽略,反正没什么卵用((

知道这些东西之后,终端模拟器的工作原理就很简单了:终端模拟器创建了一对 pty 设备,同时在 pty slave 上启动当前用户的默认 shell,比如`execlp(“/usr/bin/zsh”, [ “–login” ])`。pts 会将程序所有的输出发送给 pty master,终端模拟器在拿到这些数据后,再按照指定终端的标准将其输出。同时,所有的键盘输入也会发送给 pty slave。大致就是如下:

+----------+
| X Server |
+----+-----+
     |
+----+--------------+  +------------+
| Terminal Emulator +--+ pty master +
+-------------------+  +--+-----+---+  
                          |     |
                       +--+-----+--+  
                       + pty slave +
                       +--+-----+--+  
                          |     |
        +-----------------+-----+---+
        + Terminal-oriented program |
        +---------------------------+

Secure Shell 的远程登录的原理同样类似:ssh 客户端首先和 sshd 协商加密,互相认证,然后建立一个 SSH channel,由服务端创建一对 pty,然后将 pty master 的输出放到 SSH channel 中。ssh 客户端与服务端之间通过 SSH channel 通讯,便实现了远程登陆。

那么,Web Terminal 的实现思路就很明确了:在浏览器上,我们需要找到一个比较好用的终端框架(或者自己撸一个),在服务器上,我们需要一个当前程序语言与 ptmx 的接口(或者自己撸一个)。而通讯方面,SSH 用的是 TCP,Web 上能用的也就是 WebSocket 了(除非你想 XMLHttpRequest 然后疯狂刷新),这里能找到框架最好,全都自己撸就太累了(

嘛。虽然 npm 上坑爹的包非常多,但是在这种时候基本上还是能做到想要啥就有啥的。这里我选择了 xterm.js 作 HTML5 中的终端组件,node-pty 做服务端的 pty 操作工具。这两个也正是 Visual Studio Code 中内置的终端所采用的依赖。WebSocket 方面,我选择了 Socket.IO 这个框架。当然,为了让 ES6 Module 正常工作,我们还需要用
webpack 来处理。依靠着强大的 xterm.js 和 node-pty,需要我们来完成的工作非常少。以下晒代码:

服务端 JavaScript :

const express = require('express');
const site = express();
const http = require('http').Server(site);
const io = require('socket.io')(http);
const net = require('net');
const pty = require('node-pty');

site.use('/', express.static('.'));

io.on('connection', function (socket) {
  let ptyProcess = pty.spawn('bash', ['--login'], {
    name: 'xterm-color',
    cols: 80,
    rows: 24,
    cwd: process.env.HOME,
    env: process.env
  });
  ptyProcess.on('data', data => socket.emit('output', data));
  socket.on('input', data => ptyProcess.write(data));
  socket.on('resize', size => {
    console.log(size);
    ptyProcess.resize(size[0], size[1])
  });
});

http.listen(8123);

浏览器端 JavaScript:

import Terminal from 'xterm';
import 'xterm/src/xterm.css';
import io from 'socket.io-client';

Terminal.loadAddon('fit');

const socket = io(window.location.href);

const term = new Terminal({
  cols: 80,
  rows: 24,
});
term.open(document.getElementById('#terminal'));
term.on('resize', size => {
  socket.emit('resize', [size.cols, size.rows]);
})

term.on('data', data => socket.emit('input', data));

socket.on('output', arrayBuffer => {
  term.write(arrayBuffer);
});

window.addEventListener('resize', () => {
  term.fit()
});
term.fit()

Webpack 配置:

const path = require('path');

module.exports = {
    entry: "./src/entry.js",
    output: {
        path: path.join(__dirname, 'dist'),
        filename: "bundle.js"
    },
    module: {
        loaders: [
            { test: /\.css$/, loader: "style-loader!css-loader" }
        ]
    }
};

运行效果:

完整的代码可以参考 GitHub 上的 playground 仓库。

]]>
https://archive.ntzyz.io/2017/08/26/implementation-of-web-terminal/feed/ 0
[作业] AES-128-ECB 实现 https://archive.ntzyz.io/2017/05/17/implement-aes-128-ecb/ https://archive.ntzyz.io/2017/05/17/implement-aes-128-ecb/#respond Wed, 17 May 2017 07:23:49 +0000 https://ntzyz.io/?p=804 继续阅读[作业] AES-128-ECB 实现]]> p.indent { text-indent: 2em }

首先呢这份代码是这学期选修课的大作业,前后也花了点心思,于是还是贴到这儿,顺便给博客除除草ww

作业的题目有两大类,一类是使用常见的对称/非对称加密算法(DES/AES/RC5/RSA/Blowfish/blabla)实现一个可用的加解密工具,另一个则是实现一个HASH程序,类似于 md5sum 或者是 sha256sum 这种。我选择了做 AES 主要是有几个原因:

  1. 老师说了,杂凑算法于加解密算法比较起来,实现难度低,因此基础成绩就会相对低一点(然而我觉得两个都蛮简单的啊)。
  2. 在上课之前就已经接触过 AES 这个算法了(归功于 Shadowsocks 啦),用了这么久的东西,也算是对它的实现比较感兴趣的(然而怎么想都知道自己写的破代码性能会被 OpenSSL 吊着打)。

当然按照老师的说法,简单的实现只是最基本的要求,所以我这里打算做三分实现,分别是 CPU 单线程,CPU 多线程和 GPGPU。其实这里面核心代码都是完全一样的,无非就是分组加密和密钥更新上有点区别,但是听起来就高大上了很多(没毛病)。

AES 本身一次只能使用 128bit 的密钥对 128bit 的数据进行加密,所以在实际中必须使用分组加密算法来完成大文件的处理。往简单里讲,分组加密不过是把数据分成等长的分片,然后每次对一个分片进行加密,然后在将加密后的分片组合成文件。解密过程与之类似。这种最简单粗暴地分组加密被称为电子密码本(ECB)模式。然而实际上,由于相同分片数据使用相同的密钥进行加密后,输出必然也是相同的,这样也就会使加密的内容带有特
征,存在潜在的隐患,于是人们提出一些会修改后续分组的密钥的分组加密算法,比如密码分组链接模式(CBC)、密文反馈模式(CFB)和计数器模式(CTR)。CBC 和 CFB 在设计上,下一分片的密钥受到上一分片的影响,也就意味着这个算法必须线性的对密码进行加密,不利于多线程的实现。那么可选的就是 CTR 和 ECB 模式了。二选一的话,选择 ECB 而不是 CTR 主要是因为我懒,ECB 只要一个函数改变传入指针就能跑了(

分区模式敲定之后,就轮到 AES 算法的了解和实现了。整个算法主要由两部分组成:数据变换和密钥拓展。

首先实现一份密钥拓展函数,其算法参考:Rijndael key schedule – Wikipedia,使用上很简单,直接将密钥指针传入即可,返回的是堆上分配的结果,用完记得要 free((

extern uint8_t round_const[];
uint8_t *key_schedule(const uint8_t *input_key) {
	uint8_t *output = (uint8_t *)malloc((10 + 1) * 16 * sizeof(uint8_t));
	uint8_t temp[4], *cursor_4_before, *cursor_16_before, *cursor_current;
	size_t counter, i, j;

	// Copy the first 16 bytes
	memcpy(output, input_key, 16 * sizeof(uint8_t));

	// Initialize cursors.
	cursor_current = output + 16;
	cursor_4_before = cursor_current - 4;
	cursor_16_before = cursor_current - 16;

	// Rock and roll.
	
	for (counter = 0; counter != 10; counter++) {
		// RotWord
		temp[3] = cursor_4_before[0];
		temp[0] = cursor_4_before[1];
		temp[1] = cursor_4_before[2];
		temp[2] = cursor_4_before[3];

		// SubBytes and add
		for (i = 0; i != 4; ++i) {
			cursor_current[i] = sbox[temp[i]] ^ cursor_16_before[i];
			if (i == 0) {
				cursor_current[i] ^= round_const[counter];
			}
		}

		// Forward all cursors
		cursor_current += 4, cursor_4_before += 4, cursor_16_before += 4;

		// Three step remaining is simple.
		for (i = 0; i != 3; ++i) {
			for (j = 0; j != 4; ++j) {
				cursor_current[j] = cursor_4_before[j] ^ cursor_16_before[j];
			}
			cursor_current += 4, cursor_4_before += 4, cursor_16_before += 4;
		}
	}

	return output;
}

AES 128 会对输入数据进行十轮处理,其中 AddRoundKey 步骤使用的密钥就是由上面函数生成的。

AES 128 的十轮处理中,必然会有的四个步骤:字节替换、行移位、列混淆和密钥相加(XOR)。

字节替换提供了加密法非线性的变换能力,实现起来相当容易:将每一个元素代入S盒中即可:

extern uint8_t sbox[];
void sub_bytes(uint8_t *input) {
	int i = 15;
	do {
		input[i] = sbox[input[i]];
	} while (--i >= 0);
}

行移位就是将矩阵的每一行左移一定量,AES 128 中,第一行不变,第二行移 1 位,第三行移 2 位,blabla。

void shift_rows(uint8_t *input) {
	size_t index, inner_index;
#define OFFSET(i, j) ((j) * 4 + (i))
	for (index = 0; index != 4; ++index) {
		uint8_t temp;
		inner_index = index;
		while (inner_index-- > 0) {
			temp = input[OFFSET(index, 0)];
			input[OFFSET(index, 0)] = input[OFFSET(index, 1)];
			input[OFFSET(index, 1)] = input[OFFSET(index, 2)];
			input[OFFSET(index, 2)] = input[OFFSET(index, 3)];
			input[OFFSET(index, 3)] = temp;
		}
	}
#undef OFFSET
}

这里补充一下,AES 对输入的明文和密钥的处理有一点反人类,比如明文 {a0, a1, a2, a3, …, a15},我们要把它看成这样的一个矩阵:

a0  a4  a8  a12
a1  a5  a9  a13
a2  a6  a10 a14
a3  a7  a11 a15

所以上面给出的 OFFSET 宏乍一看比较反人类。。对应的,行移位后的矩阵应该是这样的:

a0  a4  a8  a12
a5  a9  a13 a1  
a10 a14 a2  a6  
a15 a3  a7  a11

在内存里也就是这样的数组: {a0, a5, a10, a15, a4, a9, a14, a3, …}

剩下两个函数就是列混淆和密钥相加。其中列混淆可以视为Rijndael有限域之下的矩阵乘法,详情可以看Wikipedia,或者是翻翻书。这里我提前打了个表,来加速完成。变量名叫 cache 是本来想实时计算缓存结果的,最后还是干脆直接打全了。。

static inline uint8_t mul2(uint8_t a) {
	static uint8_t cache[256] = {
		0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E,
		0x20, 0x22, 0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E,
		0x40, 0x42, 0x44, 0x46, 0x48, 0x4A, 0x4C, 0x4E, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5A, 0x5C, 0x5E,
		0x60, 0x62, 0x64, 0x66, 0x68, 0x6A, 0x6C, 0x6E, 0x70, 0x72, 0x74, 0x76, 0x78, 0x7A, 0x7C, 0x7E,
		0x80, 0x82, 0x84, 0x86, 0x88, 0x8A, 0x8C, 0x8E, 0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E,
		0xA0, 0xA2, 0xA4, 0xA6, 0xA8, 0xAA, 0xAC, 0xAE, 0xB0, 0xB2, 0xB4, 0xB6, 0xB8, 0xBA, 0xBC, 0xBE,
		0xC0, 0xC2, 0xC4, 0xC6, 0xC8, 0xCA, 0xCC, 0xCE, 0xD0, 0xD2, 0xD4, 0xD6, 0xD8, 0xDA, 0xDC, 0xDE,
		0xE0, 0xE2, 0xE4, 0xE6, 0xE8, 0xEA, 0xEC, 0xEE, 0xF0, 0xF2, 0xF4, 0xF6, 0xF8, 0xFA, 0xFC, 0xFE,
		0x1B, 0x19, 0x1F, 0x1D, 0x13, 0x11, 0x17, 0x15, 0x0B, 0x09, 0x0F, 0x0D, 0x03, 0x01, 0x07, 0x05,
		0x3B, 0x39, 0x3F, 0x3D, 0x33, 0x31, 0x37, 0x35, 0x2B, 0x29, 0x2F, 0x2D, 0x23, 0x21, 0x27, 0x25,
		0x5B, 0x59, 0x5F, 0x5D, 0x53, 0x51, 0x57, 0x55, 0x4B, 0x49, 0x4F, 0x4D, 0x43, 0x41, 0x47, 0x45,
		0x7B, 0x79, 0x7F, 0x7D, 0x73, 0x71, 0x77, 0x75, 0x6B, 0x69, 0x6F, 0x6D, 0x63, 0x61, 0x67, 0x65,
		0x9B, 0x99, 0x9F, 0x9D, 0x93, 0x91, 0x97, 0x95, 0x8B, 0x89, 0x8F, 0x8D, 0x83, 0x81, 0x87, 0x85,
		0xBB, 0xB9, 0xBF, 0xBD, 0xB3, 0xB1, 0xB7, 0xB5, 0xAB, 0xA9, 0xAF, 0xAD, 0xA3, 0xA1, 0xA7, 0xA5,
		0xDB, 0xD9, 0xDF, 0xDD, 0xD3, 0xD1, 0xD7, 0xD5, 0xCB, 0xC9, 0xCF, 0xCD, 0xC3, 0xC1, 0xC7, 0xC5,
		0xFB, 0xF9, 0xFF, 0xFD, 0xF3, 0xF1, 0xF7, 0xF5, 0xEB, 0xE9, 0xEF, 0xED, 0xE3, 0xE1, 0xE7, 0xE5,
	};
	return cache[a];
}
void mix_columns(uint8_t *input) {
	size_t i;
	uint8_t temp[16], tmp;

	memcpy(temp, input, 16);

	for (i = 0; i != 16; i += 4) {
		tmp = temp[i] ^ temp[i + 1] ^ temp[i + 2] ^ temp[i + 3];
		input[i + 0] = mul2(temp[i + 0] ^ temp[i + 1]) ^ temp[i + 0] ^ tmp;
		input[i + 1] = mul2(temp[i + 1] ^ temp[i + 2]) ^ temp[i + 1] ^ tmp;
		input[i + 2] = mul2(temp[i + 2] ^ temp[i + 3]) ^ temp[i + 2] ^ tmp;
		input[i + 3] = mul2(temp[i + 3] ^ temp[i + 0]) ^ temp[i + 3] ^ tmp;
	}
}

最后是密钥相加。太简单了对应位置 Xor 就行了,甚至不用单独写个函数。将这以上四部分组合起来之后,只要写一个函数对明文进行十轮加密,函数大概这样:

void aes_128_single_block(const uint8_t *input, uint8_t *round_keys, uint8_t *output) {
	size_t i, j;
	uint8_t *result, *cursor_round_key;

	// initialize cursors.
	result = output;
	cursor_round_key = round_keys;

	// Copy plain text.
	for (i = 0; i != 16; ++i) {
		result[i] = input[i] ^ cursor_round_key[i];
	}
	cursor_round_key += 16;

	// Run rounds excluding the last round.
	for (i = 0; i != 9; ++i) {
		sub_bytes(result);
		shift_rows(result);
		mix_columns(result);
		// Add round
		for (j = 0; j != 16; ++j) {
			result[j] = result[j] ^ cursor_round_key[j];
		}
		cursor_round_key += 16;
	}

	// Now the last round.
	sub_bytes(result);
	shift_rows(result);
	for (i = 0; i != 16; ++i) {
		result[i] = result[i] ^ cursor_round_key[i];
	}
}

其中 round_keys 就是最开头给的函数生成的带拓展的密钥。

完成以上的加密相关代码后,只要小幅改动部分代码就能实现密文的解密了。比如 AES 里四个步骤的对应函数,除了轮密钥相加以外,其他三个函数都需要编写对应的反函数(姑且这么叫吧)。至于轮密钥相加以外,本质工作就是将轮密钥和体(state)进行异或,根据异或的性质,两次异或直接能就是本身,所以没有再去折腾了。

void aes::inverse_sub_bytes(byte *input) {
    int i = 15;
    do {
        input[i] = inverse_sbox[input[i]];
    } while (--i >= 0);
}

void inverse_shift_rows(byte *input) {
	int index, inner_index;
#define OFFSET(i, j) ((j) * 4 + (i))
	for (index = 0; index != 4; ++index) {
		byte temp;
		inner_index = index;
		while (inner_index-- > 0) {
			temp = input[OFFSET(index, 3)];
			input[OFFSET(index, 3)] = input[OFFSET(index, 2)];
			input[OFFSET(index, 2)] = input[OFFSET(index, 1)];
			input[OFFSET(index, 1)] = input[OFFSET(index, 0)];
			input[OFFSET(index, 0)] = temp;
		}
	}
#undef OFFSET
}

void aes::inverse_mix_columns(byte *input) {
    size_t i;
    byte temp[16], tmp;

    memcpy(temp, input, 16);

    for (i = 0; i != 16; i += 4) {
        tmp = temp[i] ^ temp[i + 1] ^ temp[i + 2] ^ temp[i + 3];
        input[i + 0] = mul2(temp[i + 0] ^ temp[i + 1]) ^ temp[i + 0] ^ tmp;
        input[i + 1] = mul2(temp[i + 1] ^ temp[i + 2]) ^ temp[i + 1] ^ tmp;
        input[i + 2] = mul2(temp[i + 2] ^ temp[i + 3]) ^ temp[i + 2] ^ tmp;
        input[i + 3] = mul2(temp[i + 3] ^ temp[i + 0]) ^ temp[i + 3] ^ tmp;

        byte u, v;
        u = mul2(mul2(temp[i + 0] ^ temp[i + 2]));
        v = mul2(mul2(temp[i + 1] ^ temp[i + 3]));
        tmp = mul2(v ^ u);

        input[i + 0] ^= tmp ^ u;
        input[i + 2] ^= tmp ^ u;
        input[i + 1] ^= tmp ^ v;
        input[i + 3] ^= tmp ^ v;
    }
}

最后的解密过程就是加密过程反着来,写起来也没什么困难:

void aes::aes_decrypt_single_block(const byte *input, byte * const round_keys, byte *output) {
    size_t i, j;
    byte *result, *cursor_round_key;

    // initialize cursors.
    result = output;
    cursor_round_key = round_keys + 160;

    for (i = 0; i != 16; ++i) {
        result[i] = input[i];
    }

    // The last round
    for (i = 0; i != 16; ++i) {
        result[i] = input[i] ^ cursor_round_key[i];
    }

    inverse_shift_rows(result);

    inverse_sub_bytes(result);

    cursor_round_key -= 16;

#ifdef DEBUG
    printf("# InvRound %d\n", 10);
    print_key(result);
#endif

    // Run rounds excluding the last round.
    for (i = 0; i != 9; ++i) {
        for (j = 0; j != 16; ++j) {
            result[j] = result[j] ^ cursor_round_key[j];
        }
        inverse_mix_columns(result);
        inverse_shift_rows(result);
        inverse_sub_bytes(result);
        cursor_round_key -= 16;
#ifdef DEBUG
        printf("# InvRound %d\n", 10 - i);
        print_key(result);
#endif
    }

    // Now the last addRoundKey.
    for (i = 0; i != 16; ++i) {
        result[i] = result[i] ^ cursor_round_key[i];
    }
}

以上就是 AES 的所有核心代码了,剩下的就是如何去调用这些函数。多线程方面,依靠 C++ 的 STL 库能很容易地实现线程的创建和锁控制。首先准备一个函数,将 key_schedule 和 aes 加密/解密函数包裹在一起。再准备一个函数按照指定步长去对输入指针内容进行加密/解密操作,代码很简单:

void process_unit(uint8_t *data_in, uint8_t *data_out, uint8_t *key, bool isEncrypt) {
	aes_cpu::byte *rkey = aes_cpu::aes::key_schedule(key);
	if (isEncrypt) {
		aes_cpu::aes::aes_128_single_block(data_in, rkey, data_out);
	}
	else {
		aes_cpu::aes::aes_decrypt_single_block(data_in, rkey, data_out);
	}
	delete[] rkey;
}

void thread_entry(uint8_t *data_in, uint8_t *data_out, uint8_t *key, size_t file_size, size_t current, size_t total, bool isEncrypt) {
	for (uint32_t i = current; i < file_size / 16; i += total) {
		if (i % 10000 == 0 && current == 0) {
			printf("process: %.2lf\r", (double)i * 100 / (file_size / 16));
		}
		process_unit(data_in + 16 * i, data_out + 16 * i, key, isEncrypt);
	}
}

现在只需要根据用户传入的线程数,创建若干线程即可完成工作:

bool isEncrypt = true;
std::vector threads(thread_count);

// Construct all threads, and let them go.
for (int i = 0; i != threads.size(); ++i) {
    threads[i] = std::thread(thread_entry, data_in, data_out, key, full_block_size, i, threads.size(), isEncrypt);
}

// Sync for all threads.
for (int i = 0; i != threads.size(); ++i) {
    threads[i].join();
}

涉及到任意文件加的时候就会遇到一个问题:文件大小不一定是 128 比特的整数倍。这时候就需要使用一些密码学上的填充算法。我简单的看了一下 PKCS#7 下的填充方法,简单说来就是:

  1. 若文件恰好为 16 字节的整数倍,后面强行补十六位;
  2. 填充内容为当前大小与填充后大小的差,比如 当前是 126 字节,就补充两个 0x02。对于上方的特例,就补充 16 个 0x16;

这里就不把函数列出来了,一个 realloc 一个 for 就能解决。这样我们的多线程 AES 加解密就已经全部完成了,接下来就是将以上代码迁移到 CUDA 平台。

从整体上看,CUDA 和 CPU 的代码基本一致(废话都是 AES 能不一样有鬼了),主要的区别就在于调用 GPU 是一个类似于 Master 和 Slave 通讯的过程。在完成 PKCS#7 的填充后,我们要把明文,密钥等复制到显存,同时在显存里分配一块缓冲区来保存密文,代码大致如下:

然后就可以启动 GPU 上的函数了:

>>(gpu_input, gpu_round_keys, gpu_output, size / 16, offset, threadsPerBlock * blockSize);
    }
    else {
        aes_decrypt_single_block <<>>(gpu_input, gpu_round_keys, gpu_output, size / 16, offset, threadsPerBlock * blockSize);
    }
    offset++;
}

最后,把显存上的密文复制回内存,再写入到文件,就完成了整个加密过程:

# 待续(GPU和CPU要在xx上比一比(比就比))

]]> https://archive.ntzyz.io/2017/05/17/implement-aes-128-ecb/feed/ 0 [OpenWrt] 简单的策略路由 https://archive.ntzyz.io/2016/10/30/simple-policy-routing-on-openwrt/ https://archive.ntzyz.io/2016/10/30/simple-policy-routing-on-openwrt/#respond Sun, 30 Oct 2016 09:24:44 +0000 https://ntzyz.io/?p=722 继续阅读[OpenWrt] 简单的策略路由]]> p.indent { text-indent: 2em; }

上学期期末移动宽带质量不断劣化,我们宿舍被迫转向电信宽带。为了解决那个该死的防共享机制,我自费购买了一个坑爹的破解路由器,本质就是一个替换了 PPP 的 OpenWrt 路由器。选择购买而不是自己动手破解的主要原因是不想在这上面消耗太多精力,既然有现成的解决方案,那就掏钱解决了(

那个破解的路由器很渣,是个看起来就不像那种能拖宿舍里所有电子产品的破路由,所以我只将其作为拨号路由,而主要的 NAT 工作和 Access Point 仍然交给 WNDR 4300 来完成。电信的路由器 LAN IP: 192.168.43.1 并开启 DHCP,然后 WNDR 4300 直接将 WAN 接至任意一个 LAN 口,完成后顺手关掉了破解路由器的 WiFi

当然大家都知道电信的国际出口有多惨,上个 Google 查东西都让人想砸电脑(不),同时宿舍移动的接口不用白不用。于是决定尝试一下组两个网关,让部分海(fan)外(qiang)流量走江苏移动,其余的走江苏电信,来提升整体的网络体验

首先是要修改 WNDR 4300 上的交换机配置,让他将一个 LAN 口以 WAN 的形式工作:在 OpenWrt 的交换机配置界面,添加一个 VLAN,ID 任意,将 CPU 和准备当作 WAN 的那个口分别设置为 tagged 和 untagged,剩下的其他端口设置成关就好了。

switches

然后去网络 -> 接口,添加一个类型为 PPPoE 的接口,物理设置接口为刚刚创建的 VLAN 接口。因为路由器的主要接口仍然是之前的 DHCP 客户端,所以我们要去掉高级设置中的使用默认网关。填写好 PAP/CHAP 用户名/密码后,提交变更。不出意外就成功的拨号完毕,下面需要的就是如何策略路由了。

int

使用 SSH 连接到路由器,更新软件包并安装 iproute2。完成后输入 ip route,可以看到类似这样的一行结果:

10.49.196.1 dev pppoe-cmcc proto kernel scope link src 10.49.199.127

很容易想到 10.49.196.1 就是移动 PPPoE 的网关了,我们只要将部分网段的默认路由修改到这个网关上,就可以实现高速访问国际互联网了(

#!/bin/sh

INT=pppoe-cmcc
TABLE=cmcc

DEST=`ip route show dev $INT | head -n 1 | awk '{print $1}'`

# ConoHa JP
route add -net 133.130.96.0/19 gw $DEST
route add -net 133.130.88.0/21 gw $DEST
route add -net 133.130.126.0/24 gw $DEST

# Linode JP
route add -net 106.185.24.0/21 gw $DEST
route add -net 106.186.16.0/20 gw $DEST

# CCZU CERNET
route add -net 219.230.144.0/20 gw $DEST

# JS Cernet
route add -net 211.65.74.0/24 gw $DEST

# CCZU Spoc
route add -net 202.195.100.0/24 gw $DEST

嗯,最基本的目的已经达到了。无压力播放 1080P 清晰度的油管视频最棒了(

不过不久又有新的问题了,江苏电信的校园宽带采用的是动态密码,PPPoE 断开后因为密码失效,无法自动恢复接口。这对于寝室一个玩梦幻西游的同学来说是非常恐怖的——每次掉线都要花大量的时间重新登陆所有的账号。

于是,经过讨论决定,将 192.168.1.240/28 这个网段献祭出来,做纯移动的接口,以用来挂机下载或者是长时间不掉线的网络。经过编辑的新脚本如下:

#!/bin/sh

INT=pppoe-cmcc
TABLE=cmcc

DEST=`ip route show dev $INT | head -n 1 | awk '{print $1}'`
SRC=`ip route show dev $INT | head -n 1 | awk '{print $7}'`

if [ "$1" == "pppoe-cmcc" ]; then
    # Remove all old rules
    # while ip rule delete from 0/0 to 0/0 table cmcc 2>/dev/null; do true; done

    # Add rules to let 192.168.1.240/28's traffic to CMCC gateway.
    ip rule add from 192.168.1.240/28 table cmcc priority 32765
    ip rule flush cache
    ip route add default via $SRC dev $INT table cmcc

    # ConoHa JP
    route add -net 133.130.96.0/19 gw $DEST
    route add -net 133.130.88.0/21 gw $DEST
    route add -net 133.130.126.0/24 gw $DEST

    # Linode JP
    route add -net 106.185.24.0/21 gw $DEST
    route add -net 106.186.16.0/20 gw $DEST

    # CCZU CERNET
    route add -net 219.230.144.0/20 gw $DEST

    # JS Cernet
    route add -net 211.65.74.0/24 gw $DEST

    # CCZU Spoc
    route add -net 202.195.100.0/24 gw $DEST
fi

将这个脚本放置到 /etc/ppp/ip-up.d/cmcc 中,并添加可执行权限,我们的简单的策略路由脚本就能在移动拨号成功后自动应用到系统路由表上。

]]> https://archive.ntzyz.io/2016/10/30/simple-policy-routing-on-openwrt/feed/ 0 使用 PPTP 和 IPv6 Tunnel Broker 访问 IPv6 资源 https://archive.ntzyz.io/2016/09/24/access-ipv6-resources-with-pptp-and-he-tunnel/ https://archive.ntzyz.io/2016/09/24/access-ipv6-resources-with-pptp-and-he-tunnel/#respond Sat, 24 Sep 2016 07:51:27 +0000 https://blog.ntzyz.io/?p=711 继续阅读使用 PPTP 和 IPv6 Tunnel Broker 访问 IPv6 资源]]> p.indent { text-indent: 2em; }

这几天一直折腾没啥意义的IPv6,首先是看到了 @蔓舞寻樱 dalao 的文章:利用代理隧道接入HE.net的IPv6网,就顺便在阿里云的学生优惠服务器上尝试部署了一下,拿到了前缀 2001:470:36:a90,前缀长度为 64 的 IPv6 地址池,就是这个速度实在是比较尴尬 _(:3」∠)_,从青岛阿里云到江苏教育网的 MTR 输出大致这样 (╯-_-)╯╧╧

                             My traceroute  [v0.85]
ntzyz-aliyun (::)                                      Sat Sep 24 14:46:35 2016
Keys:  Help   Display mode   Restart statistics   Order of fields   quit
                                       Packets               Pings
 Host                                Loss%   Snt   Last   Avg  Best  Wrst StDev
 1. zhangyuze320602-1.tunnel.tserv25  0.0%    10  408.3 408.8 408.1 410.3   0.5
 2. 10ge1-6.core1.sin1.he.net         0.0%    10  400.0 405.3 399.7 409.9   4.5
 3. 10ge1-16.core1.hkg1.he.net        0.0%    10  432.5 438.2 432.0 464.2   9.7
 4. 100ge10-1.core1.tyo1.he.net       0.0%    10  484.9 488.1 484.5 494.7   4.0
 5. 10ge1-13.core1.lax2.he.net        0.0%    10  584.6 590.1 583.7 600.6   6.2
 6. cngi-bjix-as-ap-as23911.10gigabi 77.8%    10  587.2 587.2 587.2 587.3   0.0
 7. 2001:252:0:302::1                 0.0%    10  731.0 730.9 730.4 731.2   0.0
 8. 2001:252:0:100::1                88.9%    10  729.8 729.8 729.8 729.8   0.0
 9. 2001:252:0:1::1                  11.1%    10  729.7 729.5 729.2 730.0   0.0
10. 2001:da8:1:1006::2               11.1%    10  736.0 734.4 731.6 739.2   2.4
11. 2001:da8:257:0:101:4:19:11        0.0%    10  727.7 738.3 727.7 776.6  16.0
12. 2001:da8:257:0:101:4:1:5          0.0%    10  727.4 727.9 727.4 728.6   0.0
13. 2001:da8:257:0:101:4:1:2          0.0%    10  736.9 736.5 732.0 739.8   2.4
14. ???
15. 2001:da8:257:0:101:4:67:e         0.0%     8  756.8 758.2 756.8 766.4   3.2
16. 2001:da8:257:0:101:4:67:1        12.5%     8  762.1 762.4 761.9 763.5   0.4
17. 2001:da8:1:504::2                 0.0%     8  768.0 766.4 765.0 768.2   1.1
18. cernet2.net                      12.5%     8  767.5 768.0 767.5 768.5   0.0
19. 2001:da8:1008:500::2              0.0%     7  767.0 767.2 766.7 767.8   0.0
20. 2001:da8:1008:20::1              16.7%     7  788.4 774.2 770.0 788.4   7.9
21. 2001:da8:1008:1030:bdd2:aba0:a20  0.0%     7  816.3 791.0 770.0 831.8  26.6

不过再怎么说也算是有了自己的 IPv6 地址了(这延迟这速度能用?)。但是这手里有这么多 IPv6 地址不搞点什么也真是太浪费了,所以想到的问题就是,有没有什么简单的办法,能让任意设备接入这个隧道?

首先想到的是使用 sit 隧道,直接借助 6in4 实现这个功能,但是实际上这个隧道在很多时候不可用(比如防火墙不允许 6in4 流量),同时两端的 IPv4 地址必须指定,不是很灵活(虽然几个脚本就能解决但是我懒),遂放弃 sit 隧道。

再然后想到的就是虚拟专用网络了,VPN 可以通过互联网传递内网信息,因为可以通过这项技术来加密流量并改变出口,VPN 也被大范围的拿来用作科学上网的工具。此处我只是需要试验一下这个思路是否可行,所以只搭建了简单而并不算安全的 PPTP VPN。我使用的服务器安装的系统为 Ubuntu 14.04.5 LTS,其他发行版操作也是类似的。首先安装 pptpd

sudo apt install -y pptpd

安装完成后,修改几个配置文件,印象里要编辑的就是 /etc/ppp/chap-secrets, /etc/ppp/pptpd-options 和 /etc/pptpd.conf,这些在网上都有大量的教程,我就不赘述了(其实还是懒)。通常的安装步骤都需要编辑 /etc/sysctl.conf 来启用 IPv4 Forward 功能并编辑 iptables 的 NAT 表,来实现转发 IPv4 数据包的功能,但是我并不打算用这个隧道走 IPv4 流量,这些步骤就可以忽略不看了。

配置完成后,就可以在 Windows 下的网络和控制中心中创建一个新的 VPN 连接来测试一下,协议类型选择 PPTP,身份验证选 MS-CHAPv2。创建完成后连接,如果没有其他问题就能成功联通,获得了一个 IPv4 地址(同时你会发现你上不了网了2333)。如果出现其他问题,可以参照 /var/log/syslog 和 Windows 下的错误代码,面向 Google 进行调试。

下面就是如何让这个 PPTP 支持 IPv6 了。首先,我们要修改一下 /etc/ppp/pptpd-options,在文件最后添加一些内容:

cat >> /etc/ppp/pptpd-options << EOF
ipparam pptpd
ipv6 ,
EOF

然后就是安装 radvd (Linux IPv6 Router Advertisement Daemon):

sudo apt install -y radvd

完成后,你需要创建一个脚本,他们会在每个 ppp 连接建立之后运行,来完成 IPv6 相关的设置。首先是 /etc/ppp/ipv6-up.d/radvd,你需要将其中的 IPv6 地址(2001:470:35:a90)换成你自己的地址:

#!/bin/sh
if test $PPP_IPPRARM != pptpd ;then
        exit 0
fi

ADDR=$(echo $PPP_REMOTE | cut -d : -f 3,4,5,6)

if test x$ADDR == x ; then
        echo "Unable to generate IPv6 Address"
        exit 0
fi
ADDR=2001:470:35:a90:$ADDR

#add route
route -6 add $ADDR/128 dev $PPP_IFACE

#generate radvd config
RAP=/etc/ppp/ipv6-radvd/$PPP_IFACE
RA=$RAP.conf

cat <$RA
interface $PPP_IFACE{
        AdvManagedFlag off;
        AdvOtherConfigFlag on;
        AdvSendAdvert on;
        MinRtrAdvInterval 5;
        MaxRtrAdvInterval 100;
        UnicastOnly on;
        AdvSourceLLAddress on;
        prefix 2001:470:35:a90::/64 {};
};
EOF

#start radvd
/usr/sbin/radvd -C $RA -p $RAP.pid

#start tchdpd
/usr/sbin/tdhcpd \
 --dns-server=2001:470:20::2 \
 --dns-name=$PPP_IFACE.tunnel.ipv6.icybear.net \
 --pid-file=$RAP.dhcp.pid \
 --local-id=tunnel.ipv6.icybear.net -L debug\
 $PPP_IFACE

#update dns
ARPA=$(ipv6_rev $ADDR)
nsupdate << EOF
update delete $ARPA
update add $ARPA 10 ptr $PPP_IFACE.tunnel.ipv6.icybear.net
send
update delete $PPP_IFACE.tunnel.ipv6.icybear.net
update add $PPP_IFACE.tunnel.ipv6.icybear.net 10 aaaa $ADDR
send
EOF

exit 0

再然后是 /etc/ppp/ipv6-down.d/radvd:

#!/bin/sh

RAP=/etc/ppp/ipv6-radvd/$PPP_IFACE

kill `cat $RAP.pid` || true
kill `cat $RAP.dhcp.pid` || true

rm -f $RAP.*

ADDR=$(echo $PPP_REMOTE | cut -d : -f 3,4,5,6)
ADDR=2001:470:35:a90:$ADDR
ARPA=$(ipv6_rev $ADDR)

nsupdate << EOF
update delete $ARPA
send
update delete $PPP_IFACE.tunnel.ipv6.icybear.net
send
EOF

exit 0

最后,给他们可执行权限并重启 pptpd:

chmod +x /etc/ppp/ipv6-*.d/radvd
systemctl restart pptpd.service

不出意外的话,再进行 PPTP 连接后,你就可以获得一个 IPv6 地址啦但是他真的非常慢(废话)

完成这个 IPv6 Over PPTP 后,我开始有些奇怪的想法:由于阿里云的 IPv6 地址池是走的 sit 隧道,速度非常慢,同时 Linode 机房可是有原生的 IPv6 支持的,所以如果先开一个 ticket 给自己的 Linode 搞到一个 V6 地址池,并把这个 PPTP 放到 Linode 上,会不会好很多呢?(望天)于是我准备试一下,但是装 PPTP 的时候出现了问题:

Setting up pptpd (1.4.0-7ubuntu0.1) ...
Job for systemd-modules-load.service failed because the control process exited with error code. See "systemctl status systemd-modules-load.service" and "journalctl -xe" for details.
dpkg: error processing package pptpd (--configure):
 subprocess installed post-installation script returned error exit status 1
Errors were encountered while processing:
 pptpd
E: Sub-process /usr/bin/dpkg returned an error code (1)

查了一下似乎是 Ubuntu 的新 Bug(掀桌)。既然是 bug 那么想必过一段时间就会修复。其实在 Linode 上跑 PPTP 是一件十分不安全的事儿(GFW is watching you)搞不好自己的服务器就被搞了那还玩个什么鬼,所以觉得是时候用 OpenVPN 代替 PPTP,使用 OpenVPN over Shadowsocks 来获得一个 IPv6 地址,但是似乎吃力不讨好啊……

所以还是勉强接受了这个国内的 PPTP 隧道了(躺地

]]>
https://archive.ntzyz.io/2016/09/24/access-ipv6-resources-with-pptp-and-he-tunnel/feed/ 0
在 Linux 下使用 SVP 4 播放视频 https://archive.ntzyz.io/2016/08/18/video-playback-with-svp4-under-linux/ https://archive.ntzyz.io/2016/08/18/video-playback-with-svp4-under-linux/#respond Thu, 18 Aug 2016 09:36:50 +0000 https://blog.ntzyz.io/?p=702 继续阅读在 Linux 下使用 SVP 4 播放视频]]> p.indent { text-indent: 2em; }

SVP 作为一个插帧神器,是我们看番的好伙伴。从 SVP 4 开始,Smooth Video Project 开始提供对 Linux/macOS 的支持(vapoursynth is GOOD)。今天尝试了一下效果还不错~

首先是按惯例安装一些必备的软件包,我使用的是 Arch Linux,其他平台自行处理一下就行了

$ sudo pacman -S vapoursynth libmediainfo qt5-3d

然后解决一下视频播放软件,由于 SVP 4 在 Linux 下使用了 vapoursynth,所以我们需要自行编译 MPV 来添加对 vapoursynth 的支持:

$ git clone https://aur.archlinux.org/mpv-git.git

进入文件夹 mpv-git,编辑 PKGBUILD,搜索 ./waf configure,在最后补上 –enable-vapoursynth 即可。然后编译安装之:

$ makepkg -i

完成安装后,下载并解压 SVP 4:

$ curl -O http://www.svp-team.com/files/svp4-linux-64.tbz2
tar xjf svp4-linux-64.tbz2
./svp4-linux-64.run

启动安装向导后,没什么好说的自行配置 SVP 的安装目录并安装。

然后呢,你就可以在你的文件管理器(我用的是 nautilus)中找到你要回放的视频,右击并选择“使用 SVP 4 Linux 打开”,即可享受丝滑体验(

如果你无法在文件管理器中选择“使用 SVP 4 Linux 打开”,或是想对所有使用 mpv 打开的视频都使用 SVP 4,那么可以简单的在 ~/.config/mpv/mpv.conf 中添加一行(不存在就新建):

input-unix-socket=/tmp/mpvsocket

当然这样你就需要一直保持 SPV 4 在后台运行了(

]]>
https://archive.ntzyz.io/2016/08/18/video-playback-with-svp4-under-linux/feed/ 0
没事儿干写了个新的 Blog 框架 https://archive.ntzyz.io/2016/07/30/the-new-blog-project/ https://archive.ntzyz.io/2016/07/30/the-new-blog-project/#comments Fri, 29 Jul 2016 16:02:00 +0000 https://blog.ntzyz.io/?p=691 继续阅读没事儿干写了个新的 Blog 框架]]> p.indent { text-indent: 2em; }

JavaScript 自己也看了一段时间了,Node.js 相关的内容也随手糊过一点了,但是总是没有一个像样的作品,这样是不行的。

所以就必须想点办法,搞个大新闻,把脑子里的知识运用一番,顺带学学新的姿势,提高自己的水平。不编了好吧我承认是自己在家无聊了QAQ

在基佬集线器上逛逛的话,会发现很多人都自己撸过一个简单的 Blog 框架,毕竟这种简单的网页应用逻辑简单,同时实现的参考啊也很多,比如 PHP 就有 WordPress, Node.js 就有 Hexo,等等。所以我也就学着做个玩玩咯~

花了大概一天吧,撸出了现在的这个最简单的 Blog Framework,虽然还没想到取什么名字。前段框架使用了 Vue.js,并且才用了 Materialize 提供的部分样式和组件。后端使用了 Express.js,数据存储则按照国际惯例选择了 MySQL(MariaDB)。全站数据使用 Ajax 获得,并完全在浏览器端渲染,同时除了博客名称以外没有任何触发刷新的组件。整体风格自认为是极简风格,实际上是自己没本事搞出什么好看的设计。

现在这个框架准确的说还在开发阶段,需要解决的问题是评论以及管理页面。当然部分代码需要优化优化,写得实在是太随性了。

样例页面可以在这里查看,里面的内容大多是从这里搬运过去的。源代码则可以前往GitHub来查看(顺便请允许我无耻的求个 Star QwQ)。

卧槽我这个 Framework 国内访问速度真他喵快啊

]]>
https://archive.ntzyz.io/2016/07/30/the-new-blog-project/feed/ 3
[水] Minecraft 服务器周边功能小记 https://archive.ntzyz.io/2016/07/21/things-around-the-minecraft-server/ https://archive.ntzyz.io/2016/07/21/things-around-the-minecraft-server/#comments Thu, 21 Jul 2016 03:09:58 +0000 https://blog.dimension.moe/?p=671 继续阅读[水] Minecraft 服务器周边功能小记]]> p.indent { text-indent: 2em; }

按照以往运行 Minecraft 服务器的情况,玩家们常常需要一些游戏以外的信息:

  • 服务器负载情况
  • 服务器是否还活着
  • 是死宅要上传皮肤

那么,作为一个不怎么管服务器里发生了什么的管理,我就主动把这些功能实现了一下。其他的游戏内管理就交给 @kasora 了。

服务器负载

我们目前使用的是腾讯机房的双核 4G 的云服务器,讲道理这种阵容并不适合跑 MC 这种单线程大作。与此同时这次服务器应要求添加了村庄Mod(Millenaire),而 Millenaire 的作者自述此 Mod 和 Bukkit 存在兼容性问题,直接导致我们不再能使用 Bukkit 或者是基于 Bukkit 的服务端(例如 Cauldron),进而失去了 Bukkit API 和民间带来的一些优化。于是,在任意时刻知道服务器负荷以及一段时间内的负荷就显得比较有意义了。

至于如何去实现,就比较简单了。由于备案之类的种种问题,我们运行着 Minecraft Server 的服务器原则上不可以运行任何 HTTP Server 服务,所以我的思路是本机采集数据,然后通过某些办法发送到另一个已经备案了的服务器。

收集数据

在 Linux 设备上,我们有很多途径可以知道设备的负载和内存开销。系统负载的话,比如 `top`、`uptime` 命令,或者是 `/proc/loadavg` 这类文件,同样系统时间和内存占用也不难获得。于是我们可以准备这么一个 shell 脚本:

#!/bin/bash

TIMESTAMP=`date +%s`
LOAD=`cat /proc/loadavg | awk '{print $1}'`
MEM=`free -m | grep 'Mem' | awk '{print $3}'`

URL="https://status.mc.ntzyz.cn/set.php?timestamp=$TIMESTAMP&mem=$MEM&load=$LOAD"

curl $URL > /dev/null

执行一次,就能得到当前时间戳,1 分钟内负载和内存使用,并把这些数据作为请求发送到 status.mc.ntzyz.cn 所在的服务器了。这个脚本需要定时执行,使用 `crontab` 就可以完成。执行 `crontab -e`,并在文末添上:

* * * * * /bin/bash /home/ntzyz/send.sh

就可以实现每分钟一次请求了。

记录数据

服务器的各种数据发送到 status.mc.ntzyz.cn 之后,就需要将这些数据保存下来,准备随时被需要数据的情况下使用。是时候让全世界最好的语言上场了(雾

status = "ERROR";
    $res->message = "You shall not access.";
    die(json_encode($res));
  }

  $conn = mysql_connect("localhost", "_(:3」∠)_", "(╯-_-)╯╧╧");
  if (!$conn) {
    $res->status = "ERROR";
    $res->message = "Could not connect: " . mysql_error();
    die(json_encode($res));
  }

  $mem_used = $_GET["mem"];
  $cpu_load = $_GET["load"];
  $timestamp = time();
  if (!isset($mem_used) || !isset($cpu_load)) {
    $res->status = "ERROR";
    $res->message = "required field(s) is empty.";
    die(json_encode($res));
  }

  mysql_select_db("mc_stat", $conn);
  $sql = "insert into mc_stat(timestamp, cpu_load, mem_used) values('$timestamp', '$cpu_load', '$mem_used')";

  if (!mysql_query($sql,$conn)) {
    $res->status = "ERROR";
    $res->message = "SQL Error: " . mysql_error();
    die(json_encode($res));
  }
  else {
    $res->status = "success";
    echo(json_encode($res));
  }
?>

这样你就可以将使用 cURL 发来的数据保存到数据库了。然后就是再写一段脚本来读取这些数据,准备让其他人读取。

status = "ERROR";
    $res->message = "Could not connect: " . mysql_error();
    die(json_encode($res));
  }

  mysql_select_db("mc_stat", $conn);

  $res = mysql_query("select * from mc_stat order by timestamp desc limit $limit                                                                              ");
  $i = 0;

  $resp->status = "success";
  $resp->data = array();

  while ($row = mysql_fetch_array($res)) {
    $resp->data[$i]->timestamp = $row["timestamp"];
    $resp->data[$i]->cpu_load = $row["cpu_load"];
    $resp->data[$i]->mem_used = $row["mem_used"];
    $i = $i + 1;
  }

  die(json_encode($resp));
?>

数据的保存和读取就都完成了。

显示数据

世界上没什么比图形好的数据可视化方案了。我选择使用 highcharts 这个图表框架。由于只需要简单的显示两张折线图(负载和内存),我就直接对着 Demo 改了改,也算是 Make data come alive 了。

两张表的结构基本一致,所以只要准备一份模板,然后两次填入数据并使用框架就行了。首先是模板:

var template = {
  title: {
    text: 'Monthly Average Temperature',
    x: -20 //center
  },
  xAxis: {
    categories: null
  },
  yAxis: {
    plotLines: [{
      value: 0,
      width: 1,
      color: '#808080'
    }]
  },
  tooltip: {
    valueSuffix: ''
  },
  legend: {
    layout: 'vertical',
    align: 'right',
    verticalAlign: 'middle',
    borderWidth: 0
  },
  series: [{
    name: 'Tokyo',
    data: []
  }]
};

嗯,稍微了解过 Highcharts 的人都能看出,这基本就是第一个 Demo —— Basic Lines 的内容,只是少了一些个数据。然后就是使用 Ajax,将保存在 status.mc.ntzyz.cn 上的数据获得到浏览器,填入 template 并显示。

  $.ajax({
    url: "https://status.mc.ntzyz.cn/get.php?limit=" + limit,
    type: "GET",
    beforeSend: function() {
      $('#loadchart').html('获取数据中');
      $('#memchart').html('获取数据中');
    }
  }).done(function(data) {
    var res = JSON.parse(data);
    template.xAxis.categories = Array(limit).fill(0).map(function (it, off) {return limit - off;});
    template.title.text = 'Load Average';
    template.series[0].name = 'Load';
    template.series[0].data = Array(limit).fill(0).map(function (it, off) {return res.data[off].cpu_load * 1;}).reverse();
    $('#loadchart').highcharts(template);
    template.title.text = 'Memory Used';
    template.series[0].name = 'MB';
    template.series[0].data = Array(limit).fill(0).map(function (it, off) {return res.data[off].mem_used * 1;}).reverse();
    $('#memchart').highcharts(template);
  });

这样,就完成了所有的数据采集、记录和显示工作了。在那段 PHP 脚本中可以看出,只要修改 limit 的值,就能获得不同数量的数据,因此短时间的数据显示和长时间的数据显示就都可以完成了。

皮肤配置、上传与预览

去年暑假剁了正版 Minecraft 之后才知道这个游戏是有皮肤这个设定的,然而在 Online Mode 设置为 Off 的服务器上,客户端并不会去从 Mojang 的服务器上获得其他玩家的皮肤数据,这样别人就看不到我的皮肤了(雾

皮肤配置

由于没有使用正版验证的玩家是不能通过官方途径更换自己的皮肤,我们需要使用额外的 Mod 来实现皮肤的自定义。这里我们使用了 UniSkinMode配置的方法也不算麻烦。

皮肤上传

按照 UniSkinMod 的 API,我们需要准备每个玩家的 JSON 信息,同时准备一个 textures 目录来存储 PNG 格式的皮肤。上传皮肤只需要修改对应的 JSON 文件,同时添加/更新 textures 目录下对应的文件即可。综上,这些工作只需要一段 PHP 就可以完成。

皮肤预览

做这个纯粹是我比较想折腾折腾,毕竟 WebGL 出来这么多年了,同时也有 Three.js 这种很好用的 JavaScript 3D 库。这里的工作相当繁杂(然而逻辑很简单,都特么 UV 贴图数据要手撕)。想了解的可以直接去看下源码

]]>
https://archive.ntzyz.io/2016/07/21/things-around-the-minecraft-server/feed/ 4
[Node.js] 学校网关登陆脚本 https://archive.ntzyz.io/2016/06/05/use-nodejs-to-finish-gateway-authorization/ https://archive.ntzyz.io/2016/06/05/use-nodejs-to-finish-gateway-authorization/#respond Sun, 05 Jun 2016 12:21:10 +0000 https://blog.dimension.moe/?p=507 继续阅读[Node.js] 学校网关登陆脚本]]> .indent {text-indent: 2em;margin-top: 0.75em; margin-bottom: 0.75em;}

虽然现在我在的学校很辣鸡,但是偶然间发现所有的教学区设备,在通过网关认证后,就可以获得一个江苏省常州市教育网的公网 IP 地址,同时拥有 10Mbps 的上下对等带宽,还是蛮良心的(

然后我们就在某办公室内放置了一个配置极其破烂的台式机,用来转发内网端口,VPN 远程接入和其他奇奇怪怪的服务。然而所有这些的前提就是通过了网关认证。比较尴尬的是那台电脑并没有显示器,所以我们只能想其他办法实现这一步骤。

最初我们使用了 VPN ——搭建一个 PPTP VPN 服务器并配置好 NAT 和 IP Forward,然后在内网使用 PPTP 连接至服务器,打开浏览器访问任意非 SSL 网站,就会被 Redirect 到网关认证,然后浏览器里操作就可以完成了。

后来发现 PPTP 的配置实在是比较繁琐(虽然比较其他形式的 VPN,PPTP 是搭建最快速的一个),同时存在各种各样的问题导致 PPP 认证不通过,进而无法建立 PPTP 连接,我们开始改用 Shadowsocks,在需要认证的机器上搭建一个 ss-server,在内网的其他机器上配置好 Shadowsocks 客户端,就可以和 PPTP 一样进行操作了。

再后来发现 Shadowsocks 的编译还是要准备一堆依赖,配置也不足够简单。反思 PPTP,我们使用 PPTP 的目的就是将两个机器组成一个虚拟专用网络,然后所有流量从待认证机器出去以实现认证。仔细想想不难发现其实我们用来认证的机器和待认证机器本来就在一个网段了,为什么还要专门组一个呢?(智商-10)所以我们只需要在待认证服务器上执行这句 iptables 语句:

iptables -t nat -A POSTROUTING -s 219.230.153.0/24 -j MASQUERADE

然后再另一个设备上,将默认网关调整至待认证设备IP,就可以和之前一样操作并通过认证了。

今天在数据结构上机的时候,突然想到,我们进行网关的认证,其实就是在网页上填写一个表单,并提交这个表单到认证服务器,随后就解锁了国际互联网访问。那为什么要搞的这么复杂绕了一大圈就为了进行一个 POST 呢?(智商+10)

想到这些就随后几分钟写了段 JavaScript,来实现这些个操作,实测可行。代码如下:

var request = require('request');
request = request.defaults({jar: true});

// Your login info here.
var uname = '';
var passwd = '';

request.get('http://www.bilibili.com/', (err, res, body) => {
    var loginURL = body.substr(28, body.length - 38);
    request.get(loginURL, (err, res, body) => {
        request.post({
            url: 'http://219.230.159.25/eportal/webgateuser.do?method=login_ajax_pure_internet¶m=true&' + loginURL.substr(40) + '&username=' + uname + '&pwd=' + passwd,
            form: {
                is_auto_land: false,
                usernameHidden: uname,
                username_tip: 'Username',
                username: uname,
                strTypeAu:null,
                uuidQrCode:null,
                authorMode:null,
                pwd_tip: 'Password',
                pwd: passwd
            },
            referer: loginURL,
            headers: {
                'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36',
                'Connection': 'keep-alive',
                'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                'Accept-Encoding': 'gzip, deflate',
                'Accept-Language': 'zh,zh-CN;q=0.8,en-US;q=0.6,en;q=0.4'
            }
        }, (err, res, body) => {
            if (res.caseless.dict['auth-result'] == 'success') {
                console.log('Login Success.');
            }
            else {
                console.log('Something happened.');
            }
        });
    })
})

这段代码需要一个叫 `request` 的依赖,直接输入 `npm insall request` 即可完成安装。将个人的登录信息修改好后,执行脚本:`node login.js`,如果看到输出`Login Success.`,就表明认证通过,可以开始上网啦((

现在看来当时的解决方案真是智障(不)

]]>
https://archive.ntzyz.io/2016/06/05/use-nodejs-to-finish-gateway-authorization/feed/ 0