Author Archives: mine260309

异地NAS备份

之前的文章介绍了我的乞丐版NAS及配置,是放在家里的单个NAS。虽然做了SnapRAID,坏一块盘也没关系,然而假如由于某种原因这个NAS里的盘全坏了,数据就没了。
数据备份有个3-2-1原则:

  • 3份数据
  • 2种存储介质
  • 1个异地备份

过年回老家之后,由于新冠病毒(2019-nCOV)一直呆在老家,正好可以折腾一下异地的NAS备份,这样我也有自己的2-1-1的备份了:

  • 2份数据
  • 1种存储介质
  • 1个异地备份

对于家用来说,已经足够安全了。
这时记录一下我的异地NAS备份的配置。

硬件

  • 跟之前一样,还是蜗牛星际A款单口千兆,350包邮(2020.02价格,比之前涨了100!)
  • 西数紫盘 4T*1 (目前的容量,一块就够了,以后有需要就再添加)

软件

对于放在老家的NAS,出于成本考虑,就不做RAID了;而目前只用一块硬盘,为了方便以后扩展,LVS是最方便的。
既然是异地备份,一开始想的是用rsync就好了。
不过后来想想,当人在老家的时候,可以直接备份数据在老家的NAS上;并且老爸老妈他们手机里的数据也可以直接备份在本地,所以我需要的是一个双向的同步工具。用两条rsync命令当然也可以,不过应该会有更合适的工具。

综合上面的需要,最后决定用的是:

  • Ubuntu (目前装的是19.10)
  • LVS方便未来扩展空间
  • wireguard用来连接两台NAS
  • unison用来双向同步
  • 其它工具和原来的NAS相同(比如hdparm, smartmontools等)

LVS

以前不太用LVS,觉得麻烦,后来在公司的某台服务器上配置了LVS之后,觉得用起来很爽,现在在这台异地NAS上正好可以用起来。
把这块4T的硬盘分成两个LV(Logical Volume),一个用来放不用长期保存的(如下载的视频之类)文件,另一个用来作为我的NAS的异地备份,都用ext4文件系统。

# 假设用fdisk或者parted工具创建了一个/dev/sdb1,包含/dev/sdb的整个空间
pvcreate /dev/sdb1   # 创建physical volume
vgcreate nas_data_vg /dev/sdb1   # 创建名为nas_data_vg的virtual group
lvcreate -n nas_download -L 800G nas_data_vg   # 创建一个800G大小的LV
lvcreate -n nas_data nas_data_vg -l 100%FREE   # 创建一个包含剩余所有空间的LV

# 格式化
mkfs.ext4 /dev/nas_data_vg/nas_download
mkfs.ext4 /dev/nas_data_vg/nas_data

# 查看blkid
blkid /dev/nas_data_vg/nas_data
blkid /dev/nas_data_vg/nas_download

# 编辑/etc/fstab自动挂载
# cat /etc/fstab
UUID=<blk-id-of-nas_data> /mnt/storage ext4 defaults 0 0
UUID=<blk-id-of-nas_download> /mnt/downloads ext4 defaults 0 0

Wireguard

之前写过一篇文章讲怎么配置Wireguard server和怎么在手机上用Wireguard,在Ubuntu里配置一个client也是超级简单的事情。然后用systemd让它开机自动启动就好了,这样这个异地的NAS开机完,就自动连着我原来的NAS。

sudo apt install wireguard

# cat /etc/wireguard/wg0.conf
[Interface]
Address = <Self IP in the VPN>/24
PrivateKey = <private-key>
[Peer]
PublicKey = <public-key-on-wireguard-server>
# AllowedIPs = 0.0.0.0/0 # 如果想让NAS的默认网关走远程的网络
AllowedIPs = 192.168.2.0/24 # 如果只想在WireGuard的网段互相访问
Endpoint = <wireguard-server>:<port>

sudo systemctl enable [email protected]   # Enable这个service
systemctl start [email protected]   #开始这个service

unison

搜索了一下双向同步的工具,有一些开源的,根据https://askubuntu.com/questions/727304/automatically-do-a-two-way-sync-of-two-directories 这里面的推荐,我最后选择了用unison,看上去比较清晰简单。
安装很简单,Ubuntu官方源里有就有,直接apt就装好了。

sudo apt install unison

unison的基础用法很简单,unison root1 root2就把root1和root2同步了,先按官网的教程里,把玩一下a.tmpb.tmp,然后就可以写自己的脚本了。

$ cat ~/bin/sync_between_nas.sh
#!/bin/bash

if [ $# -ne 1 ]; then
  echo "Usage:"
  echo "$0 <dir>"
  echo ""
  echo "Two-way sync <dir> between my NAS in /mnt/storage"
  exit 1 
fi

dir_to_sync=$1
src=ssh://[email protected]$nas//mnt/storage/$1
dst=/mnt/storage/$1

if ssh [email protected]$nas "[ ! -d $dst ]"; then
  echo $dst does not exist!
  exit 1
fi

echo To sync from \"$src\" to \"$dst\"...
sudo unison $src $dst -batch -owner -group -prefer newer -times -nodeletion $src -nodeletion $dst

这个脚本会同步两个NAS的/mnt/storage/目录,主要功能在最后一行:

  • -batch: 表示不需要用户的输入(否则它会问用户使用哪种action)
  • -owner, -group: 保留原有的owner:group。注意:
  • 这里两个NAS都需要创建同样的owner、group
  • 因为文件传输之后需要chown,所以这个脚本要以su用户来执行(不知道有没有更好的方式)
  • -prefer newer:如果有冲突,总是以更新的文件为准
  • -times:同步文件的修改时间
  • -nodeletion $src -nodeletion $dst: 对$src和$dst都不进行删除,以防有文件被误删,并被同步到另一个NAS

然后,有一个辅助脚本来指定要备份的目录,并调用上面这个脚本。

#!/bin/bash

set -e
dirs_to_sync="<dir1> <dir2> ..."
for d in $dirs_to_sync; do
  echo $d
  /home/mine/bin/sync_between_nas.sh $d
done

最后,配置一个cron job,就可以愉快地每天同步数据啦。记得让cron发邮件,以便检查同步的状态。

$ sudo crontab -l
MAILTO="<my-email-address>"
10 2 * * * /usr/bin/flock /tmp/sync_my_nas.lock /path/to/sync_my_nas_dirs.sh

后记

后来有朋友推荐了Syncthing,看上去有点像BTSync之类的,以后也许可以试试。

Share

From a CI bug to systemd, to GCC

I was tracing a bug in sdbusplus found by OpenBMC CI, it leads me into the code in systemd, and eventually get me into GCC.
To get a short introduction, refer to https://lists.ozlabs.org/pipermail/openbmc/2019-December/019884.html
Here is the full story of the investigation.

The sdbusplus CI issue

A CI issue is found in sdbusplus, that the valgrind reports the below error.

==5290== Syscall param epoll_ctl(event) points to uninitialised byte(s)
==5290== at 0x4F2FB08: epoll_ctl (syscall-template.S:79)
==5290== by 0x493A8F7: UnknownInlinedFun (sd-event.c:961)
==5290== by 0x493A8F7: sd_event_add_time (sd-event.c:1019)
==5290== by 0x190BB3: phosphor::Timer::Timer(sd_event*, std::function) (timer.hpp:62)
==5290== by 0x192B93: TimerTest::TimerTest() (timer.cpp:25)
==5290== by 0x193A13: TimerTest_timerExpiresAfter2seconds_Test::TimerTest_timerExpiresAfter2seconds_Test() (timer.cpp:85)
...
==5290== by 0x4A90917: main (gmock_main.cc:69)
==5290== Address 0x1fff00eafc is on thread 1's stack
==5290== in frame #0, created by epoll_ctl (syscall-template.S:78)
==5290==

Clearly, valgrind detects that some uninitialized data is used.
However, this issue is not 100% reproduced, it only occurs sometimes, how could that be?

At a first glance, it’s sdbusplus‘s Timer class that invokes sd_event_add_time() from libsystemd, and eventually invokes epoll_ctl().
So I would suspect something may be wrong in Timer that it may pass uninitialized data to sd_event_add_time().

Investigation in sdbusplus

Let’s see the related code.

void initialize()
{
    ...
    auto r = sd_event_add_time(
        event, &eventSource,
        CLOCK_MONOTONIC, // Time base
        UINT64_MAX,      // Expire time - way long time
        0,               // Use default event accuracy
        [](sd_event_source* eventSource, uint64_t usec, void* userData) {
            auto timer = static_cast<Timer*>(userData);
            return timer->timeoutHandler();
        },     // Callback handler on timeout
        this); // User data
    ...
}

The event is a pointer that is already initialized;
The eventSource is the out-parameter.
Others are just simple data or a lambda, nothing suspicious.

Investigation in libsystemd

So let’s dive into libsystemd to see what exactly happens.
The related code is in sd_event_add_time().

_public_ int sd_event_add_time(sd_event *e, ...) {
    ...
    if (d->fd < 0) {
        r = event_setup_timer_fd(e, d, clock);
        if (r < 0)
            return r;
    }
    ...
}

Where:

  • e is sd_event*, and clock is clockid_t, both are passed into this function
  • d is struct clock_data* initialized in this function So nothing is wrong.

Let’s see event_setup_timer_fd() then.

static int event_setup_timer_fd(...) {
    struct epoll_event ev;
    ...
    ev = (struct epoll_event) {
        .events = EPOLLIN,
        .data.ptr = d,
    };
    r = epoll_ctl(e->epoll_fd, EPOLL_CTL_ADD, fd, &ev);
    ...
}

The epoll_fd, fd, and ev, are all initialized, are they?

Let’s see how epoll_ctl is implemented in kernel source.

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
        struct epoll_event __user *, event)
{
    ...
    if (ep_op_has_event(op) &&
        copy_from_user(&epds, event, sizeof(struct epoll_event)))
        goto error_return;
    ...
}

Be noted that valgrind says Syscall param epoll_ctl(event) points to uninitialised byte(s), and here we do see that it’s copying the whole event struct from userspace, which means the event contains uninitialized bytes.
Let’s go back to see how the event struct is initialized:

    ev = (struct epoll_event) {
        .events = EPOLLIN,
        .data.ptr = d,
    };

And let’s see how the struct is defined in glibc.

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

struct epoll_event
{
  uint32_t events;  /* Epoll events */
  epoll_data_t data;    /* User data variable */
} __EPOLL_PACKED;

Hmm, the events is a uint32_t and is initialized, and the data is initialized as well, it looks fine…
Is it really fine?
data is a union and should be at least 64 bits, while events is uint32_t, which is 32 bits, there could be padding inside the epoll_event padding if it’s not packed.
Hey, there is __EPOLL_PACKED… let’s grep it in glibc:

$ gg __EPOLL_PACKED
ChangeLog.old/ChangeLog.18:     (__EPOLL_PACKED): Define to empty if not defined by
ChangeLog.old/ChangeLog.18:     (struct epoll_event): Use __EPOLL_PACKED to make possibly packed.
sysdeps/unix/sysv/linux/sys/epoll.h:#ifndef __EPOLL_PACKED
sysdeps/unix/sysv/linux/sys/epoll.h:# define __EPOLL_PACKED
sysdeps/unix/sysv/linux/sys/epoll.h:} __EPOLL_PACKED;
sysdeps/unix/sysv/linux/x86/bits/epoll.h:#define __EPOLL_PACKED __attribute__ ((__packed__))

It is defined to __attribute__ ((__packed__)) for x86, and not defined for other architectures.
Remember that the issue is not 100% reproduced, right?
The OpenBMC CI backend has both x86-64 and ppc64le servers, so we could guess that the padding causes the valgrind error, and it only happens on ppc64le but not on x86-64, because on x86-64 there is no padding at all.
From the CI log, it confirms that the guess is correct: the issue only occurs on the ppc64le CI server!

So let’s go back to the question code:

    ev = (struct epoll_event) {
        .events = EPOLLIN,
        .data.ptr = d,
    };

It’s using GCC’s Designated Initializers extension
I tried to google how GCC initialize the padding, there are discussions in StackOverlows and blogs, e.g. according to https://stackoverflow.com/questions/37642026/does-c-initialize-structure-padding-to-zero, it looks like the case:

padding for the remaining objects are guaranteed to be 0, but not for the members which has received the initializers.

But I do not see an official GCC doc talking about this.
Let’s do some experiments.

Testing in GCC

Giving below demo code to test how the padding is initialized:

#include <string.h>
#include <stdint.h>
#include <stdio.h>

struct struct_with_padding {
        uint32_t a;
        uint64_t b;
        uint32_t c;
};
int main()
{
        struct struct_with_padding s;
        memset(&s, 0xff, sizeof(s));
        s = (struct struct_with_padding) {
                .a = 0xaaaaaaaa,
                .b = 0xbbbbbbbbbbbbbbbb,
#ifdef SHOW_GCC_BUG
                .c = 0xdddddddd,
#endif
        };
        uint8_t* p8 = (uint8_t*)(&s);
        printf("data: ");
        for (size_t i = 0; i < sizeof(s); ++i)
        {
                printf("0x%02x ", p8[i]);
        }
        printf("\n");
        return 0;
}

Compile with or without SHOW_GCC_BUG, the result is different:

$ gcc -o test_padding test_padding.c
$ ./test_padding
data: 0xaa 0xaa 0xaa 0xaa 0x00 0x00 0x00 0x00 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

$ gcc -DSHOW_GCC_BUG -o test_padding test_padding.c
$ ./test_padding
data: 0xaa 0xaa 0xaa 0xaa 0xff 0xff 0xff 0xff 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xdd 0xdd 0xdd 0xdd 0xff 0xff 0xff 0xff

GCC behaves like:

  • If a struct is partial initialized, the all the padding are initialized to zero;
  • If a struct is fully initialized, the padding remains the old data. This is exactly what happens in my case!

How about clang?

$ clang -o test_padding test_padding.c
$ ./test_padding
data: 0xaa 0xaa 0xaa 0xaa 0x00 0x00 0x00 0x00 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

$ clang -DSHOW_GCC_BUG -o test_padding test_padding.c
$ ./test_padding
data: 0xaa 0xaa 0xaa 0xaa 0x00 0x00 0x00 0x00 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xdd 0xdd 0xdd 0xdd 0x00 0x00 0x00 0x00

OK, clang initializes the padding in both cases, good!

While I tried to file a bug to GCC, I found an exact same bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77992, which is reported in 2016, and it looks like GCC is not going to fix it…

Follow-ups

  1. I tried to send a PR to systemd as a workaround that manually zero initialize the struct epoll_event.
    It’s under discussion, but likely the maintainer of systemd (@poettering) will not accept the PR, because it’s not really a systemd bug, instead, @poettering treats it a Valgrind bug (if not a GCC bug).
  2. Although I do not think it’s a Valgrind bug, a bug is filed to https://bugs.kde.org/show_bug.cgi?id=415621, there is no further feedback yet.
  3. Without GCC or systemd or valgrind’s fix, I have to add a valgrind suppression to OpenBMC CI https://gerrit.openbmc-project.xyz/c/openbmc/sdbusplus/+/25548, problem solved.

Summary

  • GCC has a bug of not initializing the padding.
  • systemd hits the bug on initializing struct epoll_event on non-x86 systems.
  • OpenBMC CI has both x86-64 and ppc64le systems. If a CI is run on ppc64le, the issue occurs.
  • Adding a valgrind suppression file fixes (or workarounds) the issue.
Share

我的乞丐版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 [email protected]
    

额外的配置

为了让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