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
无线临时网络文件传输工具 https://archive.ntzyz.io/2017/09/19/wireless-ad-hoc-file-transfer/ https://archive.ntzyz.io/2017/09/19/wireless-ad-hoc-file-transfer/#respond Tue, 19 Sep 2017 07:58:37 +0000 https://ntzyz.io/?p=978 继续阅读无线临时网络文件传输工具]]> 该文章需要被重构!

这个项目我也不知道现在该不该将他设置成 public,毕竟是别人的毕业设计(捂脸逃)

不管怎样这里我就写一些制作思路吧,题目要求完成的是在 WANET 环境下实现文件的发送与接受,这里我选择的开发平台是 Linux,在网络相关 API 上感觉用起来比 Windows 舒服不少,同时 WANET 的创建也更加方便。

WANET 的最大特点就是,网络中的每一个节点都充当路由,所以整个网络中某一个节点的掉线是不会影响到其他设备的通讯,同时也可以动态的加入和移除节点。带来的麻烦也就是整张网络的带宽上不去,所以基于 TCP 的 HTTP 我们是不能用了,这里就需要自己实现一套。同时,网络的去中心化也使得不能像传统程序一样设置一个服务端和一组客户端,必须由自己实现一个网络发现的机制。结合以上两个特点,UDP 无疑成了最好的协议选择。

组件 IBSS 网络

WANET 在 Linux 上创建的方法很简单,你只需要安装好 iw 工具,然后通过如下几个命令来简单创建:

iw $iface set type ibss
ip link set $iface up
iw $iface ibss join $ssid $freq

其中,`$iface` 是指定的无线网卡,使用前记得查看是否支持 IBSS 模式,`$ssid` 则是接入点名称,对于部分操作系统可以直接加入这个网络,`$freq` 是网络的频率,实际值我选择了 2412 这个信道 1 对应的频率。

配置 IP 地址

仅仅启动了网络仍然是无法通讯的,原因是没有配置 TCP/IP 相关的属性。由于去中心化,我们不能使用传统的 DHCP 协议来分配 IP,只能有每个设备给自己分配一个 IP,所以防止冲突也比较重要,在这方面我选择了直接使用一个 `10.0.0.0/8` 这么个的大网段,其中三个数字随机产生,来满足地址的需求。

传送算法设计

文件发送方面,UDP 协议是无状态协议,同时大小最好也不要超过整个网络的 MTU 来保证可靠性,所以实际发送过程中会将文件分解成一个一个很小的切片,逐一发送。要实现可靠的文件传输,必然需要一个记录包送达的机制,我这里设计了一个比较简单(naive)的思路:定义一个数组,其中包含了数据包的传递状态,以及自身在文件中的偏移,以及这个分片的重传次数:

typedef struct WindowItem {
    uint32_t offset;
    uint8_t sendCount;
    uint32_t nextTick;
    bool isFinished = false;
    bool operator==(const WindowItem& other);
    WindowItem();
    WindowItem(uint32_t);
} WindowItem;

然后程序执行时,主循环每次循环都将扫描一次数组,对其中的元素,也就是数据包状态,执行以下操作:

  1. 检查这个数据包有没有收到 ACK 确认包,若收到,将他从数组移除
  2. 根据已完成的数据包数量,继续添加新的为发送的数据包
  3. 找到需要重新发送的包(超过若干 ticks 却没有收到 ACK 的,或者是刚加入的),并将它们发送
  4. 找到超出重试次数的包,如果有,则放弃发送,宣布失败,结束循环

以上这些过程被我称为一个 tick,为了防止网络拥塞和 CPU 过载,每个 tick 最后都会让该线程等待 10ms 的时间,来等待数据包的送达。

报文结构设计

下一步就是设计接收方拿到数据的处理方案了,如果每一次的数据报都只有文件内容,那么接收方将会很尴尬:他既不知道这个数据包时哪个会话里的,也不知道这个数据包在文件中的位置,要知道,UDP 作为无状态协议只保证数据的送达,不保证送达的顺序:

一个程序员遇到一个问题,他打算用 UDP 解决,现他在到遇了一大问个。题

所以对于每个数据包的格式,双方也必须都有明确的定义,我这里是这样的:

// 发送文件内容的报文结构
struct FilePacket {
  uint32_t offset;   // 该分片在文件中的位置
  uint8_t data[];    // 分片数据
}

当然还有其他的报文类型,这里暂时先不提,等用到再说。这样一番折腾后,我们的接受进程在接收到数据报文后,按照 offset 的偏移,将数据放到内存的缓冲区内,并发送 ACK 包。接收端的思路实在是太简单了,当时我甚至是用的 Node.js 写了原型来测试发送端(

接下来就是遇到的一个问题,对于一个接收端同时接收来次多个设备的文件,甚至是一个发送端同时发送的多个文件时,如何区分这个数据报的归属呢?这时候会话的概念就来了,HTTP 时通过每次传输都加上一个 Cookie 的 header 来实现会话,而我们则选择发送方广播寻找接受端时携带一个 UUID。同时,对于发送文件不同阶段也需要区分。整理一下,就是说所有的数据包都应该额外有两个字段的标记:会话UUID和阶段标记Tag。改进后的报文如下:

// 基础的报文结构,用于所有的传送
struct GenericHeader {
  uint8_t uuid[16];  // 会话 ID
  uint8_t tag;       // 报文类型标记
}

// 发送文件内容的报文结构
struct FilePacket {
  struct GenericHeader header;
  uint32_t offset;   // 该分片在文件中的位置
  uint8_t data[];    // 分片数据
}

其他几个类型的报文包括:

  • `DISCOVER` 报文:用于发现内网内的所有主机
  • `REQUEST` 报文:用于向发送方请求文件
  • `ACK` 报文:用于确认文件分片的接受
  • `FINISH` 报文:用于发送方向接收方宣告结束

UI 界面设计

题目要求使用 Qt 我还是比较高兴的,要使用 GTK 估计整个人都不好了(雾),有了 Qt Creator 创建界面还是很简单的,但是由于之前开发的程序都是纯命令行下的,要想接入到 Qt Application 还是比较蛋疼了,所以我就将之前的代码封装成了一个叫 `http` 的二进制程序,监听本地的 8000 端口,并执行操作。Qt 端程序就很方便了,只需要用 Qt Network 相关的东西去进行 HTTP 请求,就能拿到当前所有会话的状态,也能很轻松的创建请求。值得一提的是,这个 `http` 守护进程的 HTTP 模块完全是自己写的,代码也比较丑,这里就只放个头文件意思一下吧((

#pragma once

#include "common.h"
#include 

using handler_t = std::function;

class HttpServer {
private:
    boost::asio::io_service io_service;
    boost::asio::ip::tcp::acceptor acceptor;
    map handler;
    string decodeURL(string url);
public:
    HttpServer();
    void AddHandler(string, handler_t);
    void Run();
};

两边通讯大概就是这样:

// 刷新接收任务列表函数 1(HTTP 请求完成前)
void MainWindow::RefreshReceiveTaskList() {
    QNetworkAccessManager *manager = new QNetworkAccessManager();
    QNetworkRequest request;

    request.setUrl(QUrl("http://localhost:8000/receiving"));
    manager->get(request);

    connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(RefreshReceiveTaskListCallback(QNetworkReply *)));
}

// 刷新接收任务列表的函数 2(HTTP 完成后)
void MainWindow::RefreshReceiveTaskListCallback(QNetworkReply *reply) {
    QString response = reply->readAll();
    QStringList dataset = response.split(QChar('\n'));

    ui->receiveTable->setRowCount(0);

    for (int i = 0; i != dataset.length(); ++i) {
        QStringList rowData = dataset.at(i).split(QChar('\t'));
        if (rowData.length() != 4) {
            continue;
        }
        auto rowOffset = ui->receiveTable->rowCount();

        ui->receiveTable->insertRow(rowOffset);
        ui->receiveTable->setItem(rowOffset, 0, new QTableWidgetItem(rowData[0]));
        ui->receiveTable->setItem(rowOffset, 1, new QTableWidgetItem(rowData[1]));
        ui->receiveTable->setItem(rowOffset, 2, new QTableWidgetItem(rowData[2]));
        ui->receiveTable->setItem(rowOffset, 3, new QTableWidgetItem(rowData[3]));
    }
}

性能测试

题目的要求环境本身是有多个电脑互相发送,然而作为开发和测试的我只有一个笔记本和一个支持 IBSS 的 USB 无线网卡,所以我也只能做点小范围测试了。由于历史久远我也不记得具体速度能跑到多少,好像就比 HTTP 快那么一点点,也算是不亏了吧。

假模假样的总结

讲道理这个毕业设计真的是比我们学校的不知道高到哪里去了,涉及的范围还挺广的,至少得有 UDP 编程,Qt 编程,操作系统接口的调用(Linux 下 ioctl 就够了)和一点点算法设计的能力,对 C++ 和相关库也需要有个了解。相比而言我们学校的毕设我也做过几个,基本就是 C# WPF/WinForm 拖点控件,搞点文件保存数据或者上 SQLite,然后就没了,十分尴尬。

]]>
https://archive.ntzyz.io/2017/09/19/wireless-ad-hoc-file-transfer/feed/ 0
serainTalk https://archive.ntzyz.io/2017/09/19/seraintalk/ https://archive.ntzyz.io/2017/09/19/seraintalk/#respond Tue, 19 Sep 2017 03:29:58 +0000 https://ntzyz.io/?p=954 继续阅读serainTalk]]> serainTalk 是由 @ZephRay 取名和策划,由我和 @kasora 合作完成的论坛引擎,预期唯一用户大概就是 cnCalc 了,所以可能会成为专用引擎,不保证具有通用性,目前还在开发中。

目前采用的技术栈大概是这样的:`Node.js` 服务端软件,`Express` 服务端 Web 框架,`MongoDB` 数据库,`Vue.js` 前端 `MVVM` 框架,`Socket.io` `WebSocket` 通讯框架。

预览版地址

]]>
https://archive.ntzyz.io/2017/09/19/seraintalk/feed/ 0
new-blog https://archive.ntzyz.io/2017/09/19/new-blog/ https://archive.ntzyz.io/2017/09/19/new-blog/#respond Tue, 19 Sep 2017 03:24:29 +0000 https://ntzyz.io/?p=951 继续阅读new-blog]]> 讲道理其实还没取名字(逃

一个使用 `Vue.js` + `MongoDB` 实现的给自己用的博客框架,比较粗糙(

随着隔壁 serainTalk 的开发,这里变得更像是一个技术的实验田,比如 WebSocket 啊,Vuex 啊都是在这里先尝试,摸熟了再搬到论坛里去的。

点击链接查看

]]>
https://archive.ntzyz.io/2017/09/19/new-blog/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
网易云音乐 Linux 版高分屏问题解决方案 https://archive.ntzyz.io/2017/09/07/fix-cloud-music-linux-client-hidpi-issue/ https://archive.ntzyz.io/2017/09/07/fix-cloud-music-linux-client-hidpi-issue/#respond Thu, 07 Sep 2017 05:54:42 +0000 https://ntzyz.io/?p=861 继续阅读网易云音乐 Linux 版高分屏问题解决方案]]> .emm > p { text-indent: 2em; }

网易云音乐 Linux 版推出很久了,肯定已经有很多人反馈过 HiDPI 下不能正常工作的问题了,然而似乎网易生娃不养娃,版本号 1.0.0 从一开始就没有变过(

在网上搜索 HiDPI 下字太小难以阅读就能找到最简单的解决方案:添加参数 `–force-device-scale-factor=2` 就能让他强制两倍渲染。不过这个方法在我这儿(ArchLinux,KDE)上并不是什么好的解决方案:启动程序时,程序按照环境 DPI 创建了窗体,但是内容部分则依然使用原分辨率渲染,效果就是只有坐上 1/4 能正常显示内容,剩下的都是白色的空白。只要我的全局 DPI 不是 100%,这些空白就一直存在。

既然网易支持强制缩放倍数,只要我让他认为自己运行的环境是 100% DPI 即可,解决方案如下:

QT_SCREEN_SCALE_FACTORS=1 /usr/bin/netease-cloud-music --force-device-scale-factor=2

为了方便使用,我们可以在 `/usr/local/bin/` 下创建同名脚本,执行后就可以直接 pin 到任务栏了。

]]>
https://archive.ntzyz.io/2017/09/07/fix-cloud-music-linux-client-hidpi-issue/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
单页应用下实现动态的脚本加载 https://archive.ntzyz.io/2017/08/13/dynamically-load-scripts-on-sap-page/ https://archive.ntzyz.io/2017/08/13/dynamically-load-scripts-on-sap-page/#respond Sat, 12 Aug 2017 16:33:23 +0000 https://ntzyz.io/?p=836 继续阅读单页应用下实现动态的脚本加载]]> p.indent { text-indent: 2em; }

之前在写自己的博客框架的时候遇到了一个问题:文章中的 `script` 标签没有任何作用,以 Vue 为 MVVM 框架举个例子:


  


这段代码的意思很简单,创建一个 ID 为 app 的 DIV,其中包含一个 p 标签,vm 实例化之后,p 的内容就是 data 字段的 html,渲染结束后效果应该是这样:


  

好吧……看起来这代码很不规矩,但是浏览器应该是接受在任意位置出现的 script 标签的,也就是按照预期,我们会在控制台里看到 JavaScript is awesome 这句话,没毛病。

然而实际结果是……并没有。原因很简单,v-html 是通过 innerHTML 来实现的,而 HTML5 的标准规定了通过 innerHTML 得到的 script 标签不会被执行。此举的目的是为了防止利用这个特性来进行 XSS 攻击,但是依然有不少场景需要使用这些脚本,所以我们需要一些 trick 来让浏览器加载这些脚本。与此同时,如果 script 是通过 appendChild 添加到 DOM,这段脚本是能被正常执行的。

所以解决方案也就很简单了:找到需要执行的 script 标签,然后通过 createElement 创建空 script,并将待执行标签的数据复制到空 script 中(比如 src,或者 innerHTML),最后通过 appendChild 将它添加到 DOM 中去。

其中,找到 script 标签的方法比较多,比如可以通过正则对字符串进行匹配。不过考虑到通过 innerHTML 设置的 script 标签仅仅只是没有执行,他们仍然在 DOM 树中,所以 querySelector 在鲁棒性(划掉)上就比正则高出一截,代码如下:


  

这样,我们就能顺利地在控制台里看到输出的字符串啦(

当然这样做也有他的缺点:

  1. 使用了全局的 JavaScript 环境。如果代码中出现了一些声明(比如用 let 定义了变量),或者是通过 new Audio() 之类的方式播放了音频,即使你在适当的时候将他们从 DOM 树中移除,他们产生的影响仍然将保留在当前的上下文中。我个人的解决方案是,使用一个 flag 来记录是否添加过 script,若添加过,则后续的路由操作将在 beforeEach 的钩子处被劫持,并将 fullPath 直接应用到 window.location.href 上,强制刷新当前页面,来获得一个全新的 JavaScript 上下文。
  2. 没有充分模拟 script 标签的特性。按照正常的网页加载模式,浏览器在加载和执行 script 标签时是阻塞的,顺序执行的。通过这种方式添加的 script 执行顺序并没有任何明确的规定,可能导致出现依赖上的问题。
]]>
https://archive.ntzyz.io/2017/08/13/dynamically-load-scripts-on-sap-page/feed/ 0
初音未来 歌姬计划 Future Tone 游戏视频 https://archive.ntzyz.io/2017/06/23/hatsune-miku-future-tone-game-play/ https://archive.ntzyz.io/2017/06/23/hatsune-miku-future-tone-game-play/#respond Thu, 22 Jun 2017 17:36:16 +0000 https://ntzyz.io/?p=832 继续阅读初音未来 歌姬计划 Future Tone 游戏视频]]> 前几天拿到了游戏仅有的两个金杯中的一个:節奏遊戲大師[FS],发个视频庆祝一下(大雾

第一个是 Extreme 难度的こちら、幸福安心委员会です,洗脑曲,这个难度可以说是相当简单:

然后是 DECO*27 的二息歩行,Extreme 难度,其实也不难,就是容易脑抽按错(

最后是现实逃避P的里表ラバーズ,10星Extreme其实水分比较大(然而你还是没 pass(明明只差一点

]]>
https://archive.ntzyz.io/2017/06/23/hatsune-miku-future-tone-game-play/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