我的乞丐版NAS及配置

几个月前跟风买了矿难之后的蜗牛星际来当NAS,目前已经稳定运行了几个月了,在这里记录一下。

硬件

  • 蜗牛星际A款单口,265包邮(2019.03的价格),包括以下配置。(就这个价格来说,等于不要钱了。)
    • 主板+ J1900 CPU(单网口,千兆)
    • DDR3L内存4G(注意是笔记本用的内存条)
    • 杂牌固态硬盘16G
    • 机箱、电源
    • 4个盘位
  • 西部数据紫盘6TB * 3,单块盘980,硬盘好贵!
  • AC Arctic F8 PWM 8厘米机箱风扇+减速线
  • 超频三风扇调速器

之所以要买机箱风扇和调速器,是因为这个NAS的主板的风扇是3针的,不支持调速,所以一开始就是全速转,声音有点吵。
换一个静音点的风扇,加上手动调速,可以把噪音降到一个合理的范围。

软件

我对于NAS的需求是:
1. 大容量的安全的存储(主要是照片+视频)
2. 24小时开机+下载
以上两点是必需的
3. 方便的照片、视频管理
4. 远程访问
以上两点是不是必要的需求,尤其是远程访问,还是有被黑的风险。

卖星际蜗牛的店铺都直接写这个东东可以装黑群辉,不过这个不是我的菜。我还是用Ubuntu吧。
稍微研究了一番各种RAID和类RAID技术,最后选定了如下的方案。

  1. Ubuntu 18.04,这个仅仅是个人喜好,换任何一个Linux应该都可以。
  2. SnapRAID,这是一个”软“RAID,是基于文件的RAID,比较轻量,相比RAID5来说,优点在于方便(不需要全盘RAID)安全,唯一的缺点是,这个不是实时的RAID,而是要定期sync的RAID。不过对于家庭用途来说,完全够用了——最坏情况,也只是损失了某一天新备份的内容。
  3. Aufs,这个是Ubuntu的kernel自带的,别的OS可能需要编kernel module。
  4. 之所以需要aufs,是为了把多块硬盘的目录”合并“成一个目录,这样对于NAS来说,只要操作一个目录就可以了。aufs会负责根据配置把文件写入某块硬盘。
  5. smartmontools,这个是为了监控硬盘的状态,并发送邮件通知。
  6. hdparm,这个是为了在空闲的时候把硬盘spin down,省电。(当然,可能的负作用是影响硬盘寿命。。。)
  7. cron,定时跑一些脚本,完成定期做snapraid sync,scrub等工作。
  8. samba,方便共享,也方便别的设备备份数据。
  9. FolderSync,这个是Android上的软件,把手机里的照片备份到NAS上。
  10. aria2c,这个是著名的下载软件了,支持HTTP,BT,等等,配合nginx反向代理,方便远程管理。

以上的配置可以完美地解决我的需求1,2,而需求3,4,放在以后再想吧。

具体的配置

(注:以下内容基本参考Setting up SnapRAID on Ubuntu to Create a Flexible Home Media Fileserver这篇文章,细节上有一些区别,比如说安装snapraid的方法、选用的unionfs等)

SnapRAID

上面引用的文章里是自己编译snapraid,其实没有必要,因为已经有PPA了!
所以安装很简单:

sudo apt install software-properties-common
sudo add-apt-repository ppa:tikhonov/snapraid
sudo apt update
sudo apt install snapraid

给每一块硬盘分区(注意我只有3块硬盘,sdb, sdc, sdd)

# 下面用parted工具给/dev/sdb分区
parted -a optimal /dev/sdb
GNU Parted 2.3
Using /dev/sdb
Welcome to GNU Parted! Type 'help' to view a list of commands.
(parted) mklabel gpt
(parted) mkpart primary 1 -1
(parted) align-check
alignment type(min/opt)  [optimal]/minimal? optimal
Partition number? 1
1 aligned
(parted) quit

# 下面和sgdisk工具备份/dev/sdb的分区情况,并应用到另外两块硬盘上
sgdisk --backup=table /dev/sdb
sgdisk --load-backup=table /dev/sdc
sgdisk --load-backup=table /dev/sdd

格式化硬盘

# 数据盘预留2%的空间
mkfs.ext4 -m 2 -T largefile4 /dev/sdb1
mkfs.ext4 -m 2 -T largefile4 /dev/sdc1
# 校验盘使用全部的空间
mkfs.ext4 -m 0 -T largefile4 /dev/sdd1

创建目录准备mount这些硬盘

mkdir -p /mnt/data/{disk1,disk2}
mkdir -p /mnt/parity/parity1

配置fstab,来自动mount

# 数据盘
/dev/disk/by-id/ata-WDC_WD60EJRX-89MP9Y1_WD-WX31D88R41HT-part1 /mnt/data/disk1 ext4 defaults 0 2
/dev/disk/by-id/ata-WDC_WD60EJRX-89MP9Y1_WD-WX31D88AEC9Z-part1 /mnt/data/disk2 ext4 defaults 0 2
# 校验盘
/dev/disk/by-id/ata-WDC_WD60EJRX-89MP9Y1_WD-WX31D88AERKL-part1 /mnt/parity/parity1 ext4 defaults 0 2

配置snapraid

$ cat /etc/snapraid.conf

parity /mnt/parity/parity1/snapraid.parity

content /var/snapraid/snapraid.content    # 这个目录是可选的
content /mnt/data/disk1/snapraid.content
content /mnt/data/disk2/snapraid.content

data d1 /mnt/data/disk1/
data d2 /mnt/data/disk2/

exclude *.unrecoverable
exclude /tmp/
exclude /lost+found/
exclude *.bak
exclude .AppleDouble
exclude ._AppleDouble
exclude .DS_Store
exclude .Thumbs.db
exclude .fseventsd
exclude .Spotlight-V100
exclude .TemporaryItems
exclude .Trashes
exclude .AppleDB

创建需要的目录,然后开始愉快的sync吧

mkdir -p /var/snapraid/
snapraid sync

SnapRAID配置好了,现在如果在/mnt/data/disk[1|2]/目录里写数据,在snapraid sync之后,这些数据就会有额外的parity来保护,即使坏了一块盘,数据也能找回来。
接下来,要配置AUFS,把这两个目录合并成一个目录,方便使用。

AUFS

之所以选用aufs,是因为Ubuntu默认支持它,并且性能还不错。

$ cat /etc/rc.local
mount -t aufs -o br:/mnt/data/disk1=rw:/mnt/data/disk2=rw,sum,create=mfs,udba=reval none /mnt/storage

上面的命令把/mnt/data/disk1/mnt/data/disk2这两个目录“merge”成/mnt/storage目录,以后所有的读写都在这个目录里操作就好了。

其中:

  • br: 定义了两个目录作为branch,都是rw
  • sum: 告诉df要显示所有的branch的block/inode的总和
  • create=mfs: 创建新文件的时候,选择free space最多的那个branch,这样两块硬盘的空间会比较均衡
  • udba=reval: 这个定义了aufs如何对待”绕过aufs”直接操作branch里的文件,一般来说我们尽量不直接去操作/mnt/data/disk[1|2],只操作mount point /mnt/storage,就不会有问题。
  • 参考:http://manpages.ubuntu.com/manpages/cosmic/man5/aufs.5.html

smartmontools

假设配置好了SMTP服务,下面的smartd.conf配置会在硬盘出问题的时候发邮件通知。
不过,smartd的配置有点复杂,下面的配置是抄来的,并不是特别了解它到底作了哪些测试。
如果仅仅想测试能否收到邮件,用下面那行注释掉的配置,重启smartd,应该就能收到邮件了。
参考:is-smartd-properly-configured-to-send-alerts-by-email

$ cat /etc/smartd.conf
DEVICESCAN -S on -o on -a -I 194 -m <my-email-address> -s (S/../.././02|L/../../6/03) -n standby,q
# Enable below to test if email is sent or not
#DEVICESCAN -M test -S on -o on -a -m <my-email-address>-s (S/../.././02|L/../../6/03)

enable smartd,让它开机自动启动

$ cat /etc/default/smartmontools
# uncomment to start smartd on system startup
start_smartd=yes
# uncomment to pass additional options to smartd on startup
smartd_opts="-q never -i 7200"

hdparm

这个工具可以配置硬盘空闲时spin down。

$ cat /etc/hdparm.conf
quiet
/dev/disk/by-id/ata-WDC_WD60EJRX-89MP9Y1_WD-WX31D88R41HT {
apm = 127
keep_features_over_reset = on
spindown_time = 242
}
/dev/disk/by-id/ata-WDC_WD60EJRX-89MP9Y1_WD-WX31D88AEC9Z {
apm = 127
keep_features_over_reset = on
spindown_time = 242
}
/dev/disk/by-id/ata-WDC_WD60EJRX-89MP9Y1_WD-WX31D88AERKL {
apm = 127
keep_features_over_reset = on
spindown_time = 242
}

其中spindown_time = 242表示如果一块硬盘idle了1个小时之后,会把它spindown

cron

上面介绍了,SnapRAID不是一个实时的RAID,所以可以配置crontab让它每天夜里做sync,然后每周做一次scrub,把结果通过邮件发到我的邮箱

MAILTO="<your-email-address>"
# SnapRAID sync every day at 02:00 and check temperatures
0 2 * * * /usr/bin/flock /tmp/snapraid.lock /usr/bin/snapraid sync; /home/mine/bin/cputemp.sh; /home/mine/bin/hddtemp.sh
# SnapRAID scrub every Sunday at 02:30
30 2 * * 0 /usr/bin/flock /tmp/snapraid.lock /usr/bin/snapraid scrub

其中,cputemp.shhddtemp.sh是两个check CPU/HDD温度的小脚本:

$ cat /home/mine/bin/cputemp.sh
#!/bin/sh
echo Mine NAS CPU Info.
echo ---------------------
MHZ0=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq)
TEMP0=$(cat /sys/class/thermal/thermal_zone0/temp)
MHZ1=$(cat /sys/devices/system/cpu/cpu1/cpufreq/scaling_cur_freq)
TEMP1=$(cat /sys/class/thermal/thermal_zone1/temp)
echo Hardware:
echo CPU0 Speed $(($MHZ0/1000)) Mhz "|" CPU Temp $(($TEMP0/1000)) C
echo CPU1 Speed $(($MHZ1/1000)) Mhz "|" CPU Temp $(($TEMP1/1000)) C

$ cat /home/mine/bin/hddtemp.sh
#!/bin/sh
echo Mine NAS HDD Info.
echo ---------------------
/usr/sbin/hddtemp /dev/sd[a-d]

samba

通过samba,把NAS上的文件共享出去,方便小米盒子之类的播放视频,也方便用手机来备份照片。

首先,创建两个不同的帐户,一个用来写(备份照片,文件等),一个用来读。

sudo adduser --home /mnt/storage/ --no-create-home --shell /usr/sbin/nologin --ingroup sambashare nas
# 这个是用来写的帐户,把它添加到sambashare这个group里
sudo smbpasswd -a nas # 设置密码

sudo adduser --no-create-home --disabled-password --disabled-login ent #这个是用来读的帐户
sudo smbpasswd -a ent # 设置密码

配置samba的config,这里创建了3个共享:

  • 一个是用来备份数据的,只要在@sambashare这个group里就都有写权限;
  • 一个是专门用来共享/mnt/storage/Videos/这个目录,主要是给盒子播放视频用的;
  • 还有一个是用来共享/mnt/storage/aria2Download/目录,这个目录是给aria2下载用的,方便共享下载完,还没有整理的视频。
$ cat /etc/samba/smb.conf
[NAS_Home]
comment = Home_NAS
path = /mnt/storage/
browseable = yes
read only = no
wide links = yes
valid users = @sambashare @sadmin

[Videos]
comment = NAS_Videos
path = /mnt/storage/Videos/
browseable = yes
read only = yes
wide links = yes
valid users = ent

[Aria2Downloads]
comment = Aria2Downloads
path = /mnt/storage/aria2Download/
browseable = yes
read only = yes
wide links = yes
valid users = ent

aria2c

aria2c是一个支持多数协议的下载软件(不支持电驴),并且支持RPC方便远程管理。
它自己的配置这里就不贴了,网上到处都能找到。
为了方便远程管理下载,比如说,贴一个link给NAS,让它下载东西,可以用webui-aria2这个webui,配合nginx来使用。

server {
  listen 80 default_server;
  listen [::]:80 default_server;
  root /var/www/html/webui-aria2-master/docs;
  index index.html index.htm;
  location / {
    try_files $uri $uri/ /index.html;
  }
}

上面这个最简单的配置在80端口上enable了aria2的webui。

如果放到公网上,建议换个端口,并且enable https证书。
比如说,我的网络环境里,网络的出口是树莓派,NAS只接在内网里,所以在树莓派的nginx配置里,要有类似这样的配置

server {
  listen <custom-port>
  server_name <your server name>
  ssl on;
  ssl_certificate <your crt>
  ssl_certificate_key <your key>
  ...
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;

  location /jsonrpc/ {
    proxy_pass http://192.168.1.90:6800/jsonrpc; # aria2c的rpc端口
  }
  location / {
    proxy_pass http://192.168.1.90/; # aria2-webui的网页端口
  }
}

这样访问树莓派的特定的端口,就可以在外面通过HTTPS来访问到NAS上的aria2了。

其它

MergerFS

参考的文章里提到他最终用了MergerFS来作union fs,这个一开始也试了。
因为是用户态的文件系统,使用起来是很方便,/etc/fstab里加上下面的配置就行。

# Mergerfs
/mnt/data/* /mnt/storage fuse.mergerfs defaults,allow_other,use_ino,category.create=lfs,moveonenospc=true,minfreespace=20G,fsname=DiskPool 0 0

然而,因为是用户态的文件系统,性能实在是,一言难尽。用dd,fio等工具测试下来,写入性能只有10几MiB/s,实在是太慢太慢了。
所以放弃了MergerFS。

ownCloud

之前的所有配置,只是在NAS上用了Samba,然后配置一些备份工具(如FolderSync)来作备份,太过简单,所以也想找一些开源的私有云来尝试一下。
找了一圈,决定试用ownCloud,一方面是开源,另一方面,支持docker,所以用起来也很方便。
参考owncloud的github就可以很方便的使用了。

然而——配合ownCloud的手机app,用起来很不方便:

  • 没有导入已有图片的功能(或者说这个功能太难找?)所以硬盘里已有的照片没办法轻松的导入进去;
  • 手机上预览图片,竟然是要下载的本地的!?(这还cloud啥啊。。。)

相比起来,还是FolderSync的同步功能要好用多了,完美地完成了备份照片的功能,只缺少了图片管理的功能——这个功能以后再慢慢找开源软件来搞定吧。

WebDAV

FolderSync现在的设置是通过samba来同步,而samba服务是只在内网里能用的,所以得人在家里才能同步照片。
那人在外面,能否利用FolderSync来同步呢?答案是可以的,比如说,用WebDAV。

Nginx默认不支持WebDAV,需要编译才行,有点麻烦。幸好,我们有docker,并且找到了可用的dockerfile(虽然有bug)。
我fork了一份,把明显的错误改正之后,放在了https://github.com/mine260309/docker-nginx-webdav
用docker跑起来之后,测试的过程中发现了一些没解决的问题:

  1. 虽然用curl测试没问题,但是通过FolderSync的WebDAV来跑,总是有奇怪的错误,也许和FolderSync发的WebDAV请求有关。但是FolderSync不开源,所以也不知道具体的情况。
  2. 即使用FolderSync没问题,这个配置用的用户名、密码是放在HTTP请求里的,每次请求都会带;虽然跑在https里,但安全性总是觉得不够。。。

所以对于WebDAV也就浅尝辄止了。

Share

自己搭建WireGuard给Android用

WireGuard这个VPN最近有点热门,搜了一下,大都在讲点对点VPN,Android上的配置基本上都用AzireVPN已经配置好的config,如何在自己的VPS搭建,并给自己的Android用,稍微摸索了一下,记录一下。

服务器端的配置

在VPS上的配置很简单,按照网上的主流的教程配置就好,比如说Linode上的文章写得很清楚了,不过其中漏掉了一点点东西(ipv4 forwarding),这里放一个完整的步骤。

安装

  • 依赖linux-headers
    sudo apt install linux-headers-$(uname -r)
    
  • 安装WireGuard
    sudo add-apt-repository ppa:wireguard/wireguard
    sudo apt-get install wireguard
    

配置

  • 创建key
    umask 077
    wg genkey | tee privatekey | wg pubkey > publickey
    
  • 编辑config文件/etc/wireguard/wg0.conf,具体见注释
    [Interface]
    Address = 192.168.2.1/24   # VPN server自己的地址,可以改成任意的内网地址
    Address = fd86:ea04:1115::1/64   # VPN server自己的ipv6的地址(可选)
    SaveConfig = true   # 设为true之后,每次重启服务(stop service时)都会自动保存config
    
    # 以下是重点: 当服务启动时,通过iptables配置wg0来的流量forward到eth0
    # 如果你的device不是eth0而是别的名字,把下面的eth0改成别的。
    # 当服务停止的时候,删除相关的iptables规则
    PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
    PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
    
    ListenPort = 10443   # 随便选一个空闲的端口
    PrivateKey = <private-key>   #在上一步里生成的privatekey的内容
    

这样我们就创建好了一个wg0的WireGurad网络的配置。

启用服务

  • 手动开启、关闭wg0的网络:
    wg-quick up wg0
    wg-quick down wg0
    
  • 通过systemd来启用这个service,这样系统重新启动时这个服务也会自动启动
    systemctl enable wg-quick@wg0
    

额外的配置

为了让VPN能正常工作,需要enable ipv4的ip_forward功能:

sysctl -w net.ipv4.ip_forward=1

这样,服务端一开始的配置就弄好了。

手机端的配置

接下来在Android上配置WireGuard。

安装

直接在PlayStore或者F-Droid上下载WireGuard。

配置

如下图
WireGuard-Android-Config

    • Name: 自己起个名字
    • 点击GENERATE,它会自动生成Private/Public Key
      点击Public key,它会把public key复制到剪贴版,之后要用到。
    • Addresses: 填跟服务器端的配置里同样网段的IP,比如说192.168.2.2/24
    • DNS: 我填了8.8.8.8,这个应该是可选的
    • 添加一个Peer,在Peer里:
      • Public key: 填服务器端生成的public key
      • Allowed IPs: 填0.0.0.0/0,允许所有IP(这个很重要,否则即使连上了VPN,也无法访问别的网站)
      • Endpoint: 填服务器的IP:端口(比如上面服务端的配置10443

这样手机端的配置也好了。

服务端添加client

再次回到服务器,添加这个手机的public key

# android-public-key填手机的WireGuard里生成的public key
sudo wg set wg0 peer <android-public-key> allowed-ips 192.168.2.2/24

这样,所有的配置都弄好了,在手机上打开WireGuard,启动刚才配置好的服务,enjoy the freedom

后话

总体来说,WireGuard的配置相比别的要简单多了,而且速度确实很快。
不过,有几个问题:

  1. 目前的Android的WireGuard的功能很有限,只有简单的Exclude apps来排除不需要用的app。如果加上了GFWList,应该会实用很多。
  2. 虽然没有看过code,但是从原理上来说,这个VPN应该还是很容易从协议层面被GFW识别,如果没有混淆,GFW只要愿意,很容易定点封IP。

综上,WireGuard适合喜欢尝鲜的朋友,作为日常的翻墙工具,还需要国人在目前的基础上增加工能。GFWList应该好办,能不能混淆,需要去看看白皮书了。。。

Share

做了一个查询上海居转户状态的小工具

去年下半年申请了居转户,据说最多要等1年半,所以偶尔会要去上海居转户的网站上查询一下状态。
既然是重复性的劳动,那就写个脚本来处理吧。

Short version

用Chrome抓包,分析一下网页的表单,然后用python的各种库发送请求、解析验证码,最后得到结果。

工具放在了github上,执行shjzh_username=$your_id_number shjzh_password=$your_password ./query.py -q,把your_id_number, your_password替换成自己的身份证号、密码,或者设置为环境变量即可。

结果大概长这样:

['例子', '123123199001011234', '受理通过']

Long version

首先通过Chrome把登录网站、查询的过程抓成har包,方便之后分析。
screenshot-chrome

整个过程包含:

  1. 第一次HTTP GET访问网站,得到cookie(即requests里的session)
  2. 发送一次HTTP GET请求验证码
    screenshot-chrome-captcha
  3. 发送一次HTTP POST把用户名、密码、验证码发送到网站
    screenshot-chrome-login
  4. 成功登陆后,发送一次HTTP POST点击”我接受”的按钮
    screenshot-chrome-accept
  5. 进入后续的页面,再发送一次HTTP GET点击”我的申报信息”的按钮
    screenshot-chrome-myinfo

这个过程本身很简单,这里只记录几个要注意的点:

  1. 用户名是经过处理的,具体是md5.jshex_md5()这个函数,把字符串作为hex值然后算md5就可以了;
  2. 密码是明文,通过HTTP发送的,政府的网站无力吐槽。。。
  3. 通过pytesseract解析验证码不一定每次都成功,如果失败,可以再试一次,之后可以在这个脚本的基础上加一个retry
  4. beautifulsoup解析网页,发现这里面table的格式实现是——太不规则了。目前只能hard code来得到最后的结果。所以如果网页稍微有点变化,这个脚本可能就需要更新。
  5. 这个网站开放时间不是全天,而是8:00至22:00。 而22:00至次日8:00是打不开的。。。

然后把这个脚本设个cron task定期跑一下,结果通过邮件发送给自己,就不再需要自己去网站上check啦!

最后,附上github的code: https://github.com/mine260309/shjzh_query

Share

记一个Nexus 6p解锁loop问题

用了两年多的Nexus 6p出现问题了。
表现为:启动到主屏的时候,输入正确的密码后,系统闪退,然后再次要求输入密码。
在Android Central里,有人报过类似的问题

adb log显示:

01-11 19:26:41.950 4549 5940 W System.err: java.security.UnrecoverableKeyException: Failed to obtain information about key
01-11 19:26:41.950 4549 5940 W System.err: at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStoreSecretKeyFromKeystore(AndroidKeyStoreProvider.java:282)
01-11 19:26:41.950 4549 5940 W System.err: at android.security.keystore.AndroidKeyStoreSpi.engineGetKey(AndroidKeyStoreSpi.java:98)
01-11 19:26:41.950 4549 5940 W System.err: at java.security.KeyStore.getKey(KeyStore.java:1062)
01-11 19:26:41.950 4549 5940 W System.err: at com.android.server.locksettings.SyntheticPasswordCrypto.decryptBlob(SyntheticPasswordCrypto.java:120)
01-11 19:26:41.950 4549 5940 W System.err: at com.android.server.locksettings.SyntheticPasswordManager.decryptSPBlob(SyntheticPasswordManager.java:1040)
01-11 19:26:41.950 4549 5940 W System.err: at com.android.server.locksettings.SyntheticPasswordManager.unwrapSyntheticPasswordBlob(SyntheticPasswordManager.java:906)
01-11 19:26:41.950 4549 5940 W System.err: at com.android.server.locksettings.SyntheticPasswordManager.unwrapPasswordBasedSyntheticPassword(SyntheticPasswordManager.java:843)
01-11 19:26:41.950 4549 5940 W System.err: at com.android.server.locksettings.LockSettingsService.spBasedDoVerifyCredential(LockSettingsService.java:2105)
01-11 19:26:41.950 4549 5940 W System.err: at com.android.server.locksettings.LockSettingsService.doVerifyCredential(LockSettingsService.java:1553)
01-11 19:26:41.950 4549 5940 W System.err: at com.android.server.locksettings.LockSettingsService.checkCredential(LockSettingsService.java:1526)
01-11 19:26:41.950 4549 5940 W System.err: at com.android.internal.widget.ILockSettings$Stub.onTransact(ILockSettings.java:164)
01-11 19:26:41.950 4549 5940 W System.err: at android.os.Binder.execTransact(Binder.java:697)
01-11 19:26:41.950 4549 5940 W System.err: Caused by: android.security.KeyStoreException: Key not yet valid
01-11 19:26:41.950 4549 5940 W System.err: at android.security.KeyStore.getKeyStoreException(KeyStore.java:697)
01-11 19:26:41.950 4549 5940 W System.err: at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStoreSecretKeyFromKeystore(AndroidKeyStoreProvider.java:283)
01-11 19:26:41.950 4549 5940 W System.err: ... 11 more
01-11 19:26:41.951 4549 5940 E JavaBinder: *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
01-11 19:26:41.951 4549 5940 E JavaBinder: java.lang.RuntimeException: Failed to decrypt blob
01-11 19:26:41.951 4549 5940 E JavaBinder: at com.android.server.locksettings.SyntheticPasswordCrypto.decryptBlob(SyntheticPasswordCrypto.java:129)
01-11 19:26:41.951 4549 5940 E JavaBinder: at com.android.server.locksettings.SyntheticPasswordManager.decryptSPBlob(SyntheticPasswordManager.java:1040)
01-11 19:26:41.951 4549 5940 E JavaBinder: at com.android.server.locksettings.SyntheticPasswordManager.unwrapSyntheticPasswordBlob(SyntheticPasswordManager.java:906)
01-11 19:26:41.951 4549 5940 E JavaBinder: at com.android.server.locksettings.SyntheticPasswordManager.unwrapPasswordBasedSyntheticPassword(SyntheticPasswordManager.java:843)
01-11 19:26:41.951 4549 5940 E JavaBinder: at com.android.server.locksettings.LockSettingsService.spBasedDoVerifyCredential(LockSettingsService.java:2105)
01-11 19:26:41.951 4549 5940 E JavaBinder: at com.android.server.locksettings.LockSettingsService.doVerifyCredential(LockSettingsService.java:1553)
01-11 19:26:41.951 4549 5940 E JavaBinder: at com.android.server.locksettings.LockSettingsService.checkCredential(LockSettingsService.java:1526)
01-11 19:26:41.951 4549 5940 E JavaBinder: at com.android.internal.widget.ILockSettings$Stub.onTransact(ILockSettings.java:164)
01-11 19:26:41.951 4549 5940 E JavaBinder: at android.os.Binder.execTransact(Binder.java:697)
01-11 19:26:41.951 4549 5940 E JavaBinder: Caused by: java.security.UnrecoverableKeyException: Failed to obtain information about key
01-11 19:26:41.951 4549 5940 E JavaBinder: at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStoreSecretKeyFromKeystore(AndroidKeyStoreProvider.java:282)
01-11 19:26:41.951 4549 5940 E JavaBinder: at android.security.keystore.AndroidKeyStoreSpi.engineGetKey(AndroidKeyStoreSpi.java:98)
01-11 19:26:41.951 4549 5940 E JavaBinder: at java.security.KeyStore.getKey(KeyStore.java:1062)
01-11 19:26:41.951 4549 5940 E JavaBinder: at com.android.server.locksettings.SyntheticPasswordCrypto.decryptBlob(SyntheticPasswordCrypto.java:120)
01-11 19:26:41.951 4549 5940 E JavaBinder: ... 8 more
01-11 19:26:41.951 4549 5940 E JavaBinder: Caused by: android.security.KeyStoreException: Key not yet valid
01-11 19:26:41.951 4549 5940 E JavaBinder: at android.security.KeyStore.getKeyStoreException(KeyStore.java:697)
01-11 19:26:41.951 4549 5940 E JavaBinder: at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStoreSecretKeyFromKeystore(AndroidKeyStoreProvider.java:283)
01-11 19:26:41.951 4549 5940 E JavaBinder: ... 11 more

根据crash的log看,应该是系统在从keystore里读key的时候,认为时间无效:

01-11 19:26:41.950 4549 5940 W System.err: Caused by: android.security.KeyStoreException: Key not yet valid

相关的code,大概是在https://android.googlesource.com/platform/system/security/+/master/keystore/keystore_keymaster_enforcement.h#41

bool activation_date_valid(uint64_t activation_date) const override {
  time_t now = time(NULL);
  if (now == static_cast<time_t>(-1)) {
    // Failed to obtain current time -- fail safe: activation_date hasn't yet occurred.
    return false;
  } else if (now < 0) {
    // Current time is prior to start of the epoch -- activation_date hasn't yet occurred.
    return false;
  }
  // time(NULL) returns seconds since epoch and "loses" milliseconds information. We thus add
  // 999 ms to now_date to avoid a situation where an activation_date of up to 999ms in the
  // past may still be considered to still be in the future. This can be removed once
  // time(NULL) is replaced by a millisecond-precise source of time.
  uint64_t now_date = static_cast<uint64_t>(now) * 1000 + 999;
  return now_date >= activation_date;
}

因为有root,可以尝试设置系统时间,结果还是没用,即使设置到2250年,它还是报同样的错。所以我也不知道到底问题是不是在这里了。

然后注意到hwclock不太对:

angler:/sys/class/rtc/rtc0 # date "030100002018"
Thu Mar 1 00:00:00 CST 2018
angler:/sys/class/rtc/rtc0 # hwclock -l
Mon Jan 12 09:21:32 1970 0.000000 seconds
angler:/sys/class/rtc/rtc0 # hwclock -w
hwclock: ioctl 4024700a: Invalid argument

高通的这个RTC莫非根本不能写,只能读?

angler:/sys/class/rtc/rtc0 # ls -l
total 0
-r--r--r-- 1 root root 4096 2018-03-01 00:01 date
-r--r--r-- 1 root root 4096 2018-03-01 00:01 dev
lrwxrwxrwx 1 root root 0 2018-03-01 00:01 device -> ../../../qpnp-rtc-8
-r--r--r-- 1 root root 4096 2018-03-01 00:01 hctosys
-rw-r--r-- 1 root root 4096 2018-03-01 00:01 max_user_freq
-r--r--r-- 1 root root 4096 2018-03-01 00:01 name
drwxr-xr-x 2 root root 0 2018-03-01 00:01 power
-r--r--r-- 1 root root 4096 2018-03-01 00:01 since_epoch
lrwxrwxrwx 1 root root 0 2018-03-01 00:01 subsystem -> ../../../../../class/rtc
-r--r--r-- 1 root root 4096 2018-03-01 00:01 time
-rw-r--r-- 1 root root 4096 2018-03-01 00:01 uevent

最后我怀疑是time(NULL);返回了-1,但是没有log,也不知道是不是这样。

总之,这个问题,好像只能factory reset了。
如果再出问题,能只编个AOSP,加上log看一下了。

Share

记一个systemd的NFS boot问题

前段时间遇到个问题,一个基于systemd的系统在从NFS启动的时候,user space启动到一半,网络掉了,于是NFS hang住,系统也不可用了。

从log上看,很明显是systemd-networkd启动之后,IP被release掉,又没有拿到新的IP,于是网络就断了。

由于systemd-networkd是通过读取配置文件来初始化网络的,一开始猜测,是不是它先release掉IP,然后再读配置文件,导致这样的问题的。
然而从code上看,它倒是先读了config,然后再初始化网络的。

先是在SO上提了个问题,没人回答。
然后经过一番Google,发现了CriticalConnection这个config,作用是:

When true, the connection will never be torn down even if the DHCP lese expires.

虽然它在说DHCP的connection不会被释放,但是经过测试,其实它也适用于static ip。

也就是说,只要在config文件里加上了这么一段:

[DHCP]
CriticalConnection=true

这个interface的网络在systemd-networkd启动的时候就不会被断开,这样NFS mount就不会hang住,系统也就能正常启动了。(顺便自己回答了SO上的问题。。。)

Share