找回RAR压缩包密码

背景:某无产者(我就不点名批判一番了)分享了一个压缩包,然而有密码保护,只有密码提示“是xxx的niconico番号”。当然帖子底下有好多伸手党问密码到底是什么,不出意外,没有什么特别的发现。

首先是估算问题的规模,根据niconico在发帖时大概有2千万个视频,基本可以认为这个问题的规模时2千万个候选密码。不算特别大,取个常用对数还在10以下,那还是能搞一搞的。

那么首先是核心代码,这边就会有一点点讲究。直觉上,如果我们想要知道压缩包的密码是否正确,那么我们可以使用密码测试一下压缩包。但是实际上,这么做的速度非常慢:在AMD Ryzen5上速度只有不到48 rps(round per second),但是当我们直接尝试去使用密码直接解压时,速度可以提升到1214 rps。下面是示例代码

def do(guess, filepath):
    from zipfile import ZipFile
    from zlib import error as ZlibError
    fp = ZipFile(filepath)
    for passwd in guess:
        try:
            fp.extractall(pwd=passwd )
            print('password is %s' % passwd)
            break
        except ZlibError:
            pass
        except RuntimeError:
            pass

那么对于CPython的祖传单线程,启用多进程池就行了,示例如下

from multiprocessing.pool import Pool
if __name__ == "__main__":
    pool = Pool()
    for i in [b'some', b'password', b'here']:
        pool.apply_async(do, args=(i, '/path/to/zipfile'))
    pool.close()
    pool.join()

当然根据具体问题适当划分一下问题规模会比较好,这里就不再展开了,毕竟开一个进程是要时间的,传参是要占内存的。

小议WordPress的容器化

一般来说无状态服务比较适合进行容器化,以获得极强水平扩展能力,但是对于WordPress这种应用,进行容器化就不可避免地要遇到数据持久化的问题。数据持久化主要在体现在两个方面:wp-content文件夹和数据库。

经典的部署方案是本地创建一个数据卷,然后再加上一个容器化的数据库。那么对于容器化的终极目的——水平扩展能力——这样的经典方案是远不能达到要求的。

首先是经典方案中容器化的数据库,个人建议还是独立到其他服务器,或者干脆直接安排在宿主机上。第一、虚拟化是有开销的,像数据库这类偏重IO的应用,潜在地会降低性能;第二、容器化后的数据库并没有得到更高的水平扩展能力,同时各数据库间的数据同步将成为一个大问题(会有一个dockerd和mysqld谁先挂掉的问题,谁能保证dockerd这头鲸鱼搁浅之前能恰当地杀掉mysqld这只海豚?)

另一个就是本地数据卷的问题,个人建议数据卷直接挂载NFS共享,而不是挂载本地文件系统,这样就能省去很多wp-content目录数据同步的麻烦。挂载NFS共享还可能有一个读写分离的好处,毕竟读者只需要读操作,编者才需要写操作(然而并没有太大意义)。

因此总体而言docker-compose.yml文件看起来就会像下面这样

version: "3"
services:
   wordpress:
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:port       # <---这里是数据库的地址和端口
       WORDPRESS_DB_USER: wordpress     # <---这里是数据库用户名
       WORDPRESS_DB_PASSWORD: wordpress # <---这里是数据库密码
     volumes:
       - wp-data:/var/www/html

volumes:
   wp-data:
      driver: local
      driver_opts:
        type: nfs
        o: "addr=192.168.1.54,rw,sync" # <---这里是NFS的地址
        device: ":/wordpress-files"    # <---这里是NFS的共享路径

那么此时水平扩展只需要多开几个docker-compose,然后前端nginx恰当地反向代理、负载均衡即可(一定不要把proxy_set_header Host漏了,不然就会冒出充满血泪的Warning: Cannot modify header information - headers already sent by ...,一般的troubleshot只会提示你说可能php文件里有个空格。

内网穿透——FRP途径

环境

权限与运行环境

Server A :不作要求;不作要求

Server B :不作要求;不作要求

Server C :不做要求;不作要求

步骤

  1. 根据A、B的体系结构从FRP发行版中下载下来,A上解压出frpc(客户端),B上解压出frps(服务端)。
  2. A、B上分别配置frpc.ini和frps.ini。
  3. A、B上分别编写systemd服务文件,并启用

讨论

相对ssh隧道来说,frp途径更多的是配置文件的问题。所以从步骤上看好像很短很简单的样子,但写配置文件倒确实是有一点点麻烦的好用和配置繁琐的trade off。接下来就最常见的端口转发与http转发,对相关配置文件简要讨论。

1. frps.ini

http的转发在服务器端的配置主要体现在[common]块中的vhost*项,因此http只需要在客户端定义。对整个连接的保护,则体现在[common]块下的token项。其次dashboard*项,正如其名字所示,浏览器访问指定端口就可以查看服务端运行状态,遗憾的是只能查询

[common]

bind_port = 7000
kcp_bind_port = 7000
vhost_http_port = 7080
vhost_https_port = 7081

dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = 12345687

token = 12345687

1.5 间章 针对HTTP服务穿透的特别部署姿势

对于内网穿透来说,更多时候会变动的是外网服务器,内网服务器一般是不会变动的。有的时候发现了更便宜的服务器,考虑迁移的话,这么配置一圈还是有那么点麻烦的(又是跑到GitHub去下载release,又是建配置文件,又是配置服务的),对于仅需要HTTP穿透的需求来讲,服务端的配置文件一般是不需要变动的(尽管从FRP开发的历史进程上看,好像开始倾向于所有的配置交给客户端来设置;服务端往后可能也不负担配置任务了),于是我们可以利用Dokcer把服务端封装起来,迁移外网服务器的时候只需要在新服务器上启动镜像即可。

本人做了一点微小的工作 (*/ω\*) 👉 kitakami/frp-server

在这个镜像中,只需要通过环境变量配置Dashboard的用户名与密码和验证用的Token即可,此外切换版本也会相对比较方便。当然如果HTTP服务仅仅只有透过FRP穿透而来的话,也可以加上nginx镜像组成docker-compose来启动。

2. frpc.ini

客户端的配置,主要是以正确连接上服务器([common]块),与定义具体服务(其他块)。http的部分相对来说一看就懂,可能需要提一下的就是protocol = kcp的部分,传说可以改善客户端与服务端因延迟、丢包而造成的问题。端口转发的部分需要注意,服务端定义了一个监听端口,客端端上的远程端口就要写那个监听端口。

[common] 
server_addr = #远程主机IP 
server_port = 7000 
auth = qwerty
token = 12345687
[mc] 
local_port = 25565 
remote_port = 25565  
[drive]
type = http 
local_ip = 127.0.0.1 
local_port = 80 
use_encryption = true 
use_compression = false 
custom_domains = #自定义域名 
protocol = kcp  

2.5. 间章 http转发

有一些程序(例如gogs、minio)内置http服务器,但有一些程序(主要是php程序,例如nextcloud)需要借助外部http服务器(例如apache2、nginx)的帮助才能访问。对于同时有这两类程序的后端,http的转发方法就有两种:

  1. frp作为反向代理
  1. frp作为端口转发

个人建议第一个方案,如果要用第二个方案为什么不用ssh-tunnel。如果采纳方案1的话,比较推荐frp中段采取http协议进行加密,但不进行压缩。理由是压缩的任务,理应由http服务器来完成,盲目地对流量进行压缩,潜在地,会造成大量对图片无谓的压缩。

此外,并不是很推荐转发https,除非Server B上连nginx也配置不起(怕不是树莓派、单片机吧)。原则上讲,加证书这种事情应该由最末端的http服务器来做。因为https有一个握手环节,转发https不过是徒增延迟。

3. frp(s/c).service

为frp注册一个systemd服务,还是比较简单的。例子如下

[Unit]
Description=frps Daemon
After=syslog.target network.target
Wants=network.target

[Service]
Type=simple

# ExecStart=/usr/local/bin/frpc -c /root/frpc.ini
ExecStart=/usr/local/bin/frps -c /root/frps.ini
Restart=always
RestartSec=1min
ExecStop=/usr/bin/killall frps

[Install]
WantedBy=multi-user.target

关于mysqldump在命令行接口使用密码时的警告

数据库的备份策略通常有主从同步、数据库导出等等。对于小规模的数据库而言,例如家用文件服务器(我本来就只有一个节点,我上哪再去搞从库),使用mysqldump命令配置每日备份不失为一个更好的方案。

但在备份脚本中明确使用账户密码,mysqldump将会报警告

mysqldump: [Warning] Using a password on the command line interface can be insecure.

既然有警告,那就要去解决,其实完全不理他也是没太大关系的。问题出在在脚本中放账户与密码,确实不是很安全,亟需用一种更安全的方法登陆数据库,于是就有了--login-path开关(MySQL 5.6及以上)。当然也可以运行脚本时重定向stderr流,但这样做会把其他的错误内容一并解决掉,一刀切可能会对备份策略的可靠性造成潜在危害。

使用mysql_config_editor命令,例如mysql_config_editor set --login-path=local --host=localhost --user=db_user --password,就会生成一个名为local的登陆点(login path,直译),其账户密钥数据加密储存于$HOME/.mylogin.cnf,然后使用mysqldump时使用--login-path开关指定登陆点即可,例如mysqldump --login-path=local ...

Appendix

下面是一个我放在小服务器上的一个自动备份脚本,希望能抛砖引玉。

#!/bin/sh
for name in "$@"
do
    echo "========================="
    echo "Processing database $name"
    echo "-------------------------"
    now="$(date +'%Y_%m_%d_%H_%M_%S')"
    filename="$name"_backup_"$now".gz
    echo "File Saved as $filename"
    backupfolder="/vault/111/mysql_backup" # <=== 这里是备份保存的根目录 
    fullpathbackupfile="$backupfolder/$filename" 
    mysqldump --login-path=localbackup -x -f $1 | gzip > "$fullpathbackupfile"
    find "$backupfolder" -name "$name"_backup_* -mtime +8 -exec rm {} \;
    echo "========================="
    echo ""
done
exit 0

从RSS到SQL,Python语言描述

RSS(Really Simple Syndication, 简易信息聚合)是一种用于聚合发布经常更新的网站内容的一种格式规范,在一些启用RSS源的网站上,通过访问其RSS源可以获得目标网站的最新发布信息,也可以通过目标网站提供的一些筛选手段,获得感兴趣的那部分内容。因此RSS最大的特征就在于其时效性,查陈年老黄历显然没办法用RSS来完成,但有时候确实需要查一下,那就需要将RSS数据持久化。在本篇文章中,我们选用关系型数据库MySQL为例,并以Python语言为工具,示范一个持久化的例子。

首先需要明确一点,RSS的定义是十分松散的,一个字段可以是任何类型的数据(尽管从技术上讲,数据的类型都是字符串),因此就数据表结构而言,就必须具体问题具体分析,从实际出发根据需求建表(那些主张一键转换的歪果仁都是些犯形而上学错误的家伙)。当然什么都不管,就是想把RSS储存下来,也不是不可以,就是数据库的体积会稍微大一些(通常会大30倍),详情可以直接跳到第二章

马克思主义活的灵魂(笑)

目标RSS源是Torrent发布站点nyaa.si,该站提供RSS源,地址为https://nyaa.si/?page=rss,观察RSS字段,我们可以发现有下列几个我们会比较感兴趣。那么接下来就根据这些字段建表,例如名叫nyaa

字段
字段语义
guid
全局唯一编号
title描述字符串
nyaa:category分类字符串
nyaa:infoHash信息散列
link链接地址
pubDate发布时间
nyaa:size大小信息
CREATE TABLE `nyaa` (
`sid` INT(10) UNSIGNED NOT NULL,
`title` TEXT NOT NULL COLLATE 'utf8mb4_unicode_ci',
`link` TEXT NOT NULL COLLATE 'utf8mb4_unicode_ci',
`pubtime` INT(10) UNSIGNED NOT NULL,
`size` VARCHAR(20) NOT NULL COMMENT 'File size' COLLATE 'utf8mb4_unicode_ci',
`cls` TINYTEXT NOT NULL COMMENT 'Class of feed' COLLATE 'utf8mb4_unicode_ci',
`infohash` CHAR(40) NOT NULL COLLATE 'utf8mb4_unicode_ci',
PRIMARY KEY (`sid`),
INDEX `infohash` (`infohash`)
)
COMMENT='Feeds From nyaa'
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
ROW_FORMAT=COMPRESSED
;

需要提醒的是,注意到ROW_FORMAT=COMPRESSED,该表使用了压缩的行格式。这是因为在表定义中使用了很多不定长数据类型TEXT,同时link字段下会有大量重复的片段,因此使用压缩会有效降低数据表大小。另一方面注意到`pubtime` INT(10) UNSIGNED NOT NULL,,时间这一项,在这里用了无符号整型来储存,当然使用TIMESTAMP类型储存也是完全没问题的,倒不如说,那样更合情合理,因此可以根据个人偏好决定。

尽管对于link字段下的重复片段可以用程序手动进行压缩(比如只储存变动部分,不变部分硬编码到程序中),但正如前面所说的,RSS是定义十分松散的,能以原始数据储存的,尽量以原始数据储存。比起在持久化完成后的后处理中Raise Exception,在持久化过程中Raise Exception更令人生厌。

接下来就是主要的Python代码,代码假定表结构不再变化,且字段顺序不变(因为那样代码会短很多)。对付重复数据,这里采用的是INSERT IGNORE(第26行),当然用ON DUPLICATE KEY UPDATE也是没有问题的。当然数据库连接那边的具体参数(第40行),也要实事求是。值得注意的是由于executemany(第43行)对INSERT操作有魔法buff增益,请务必不要用for循环与execute,否则性能损失空前巨大。

PROXIES={}
DB_USER=None
DB_NAME=None
DB_HOST=None


def restruct(entry):
    # 按数据库列顺序重排
    from time import mktime
    if entry:
        return (
            int(entry.id.split('/')[-1]),
            entry.title,
            entry.link,
            round(mktime(entry.published_parsed)),
            entry.nyaa_size,
            entry.nyaa_category,
            entry.nyaa_infohash,
        )
    return 7


def store():
    rssurl = 'https://nyaa.si/?page=rss'

    sqlsentence = 'INSERT IGNORE INTO nyaa VALUES ({place});'.format(place=','.join(['%s'] * restruct(None)),)

    import requests as req
    from feedparser import parse

    ret = req.get(url=rssurl, proxies=PROXIES)

    if not ret.ok:
        raise RuntimeError('Response %s', ret.status_code)

    parsedfeed = parse(ret.content)

    from mysql.connector import connect

    connection = connect(user=DB_USER, db=DB_NAME, host=DB_HOST)
    cursor = connection.cursor()

    cursor.executemany(sqlsentence, map(restruct, parsedfeed.entries))

    connection.commit()


if __name__ == '__main__':
    store()

新时代中国特色方法

你有没有发现如果用马克思主义经典作家的方法,那么当你需要爬好多RSS的时候,会遇到一个写了好多相同的代码——如果能将代码妥善地包装起来,只要写一个配置文件那该多好。

🎉 锵锵锵 🎉
全球首个(暂定)RSS转SQL工具(Python实现)上线啦 !
🎉 🎉 🎉
命令行输入pip install rss2sql即刻拥有
🎉 🎉 🎉
详情请访问https://github.com/jsjyhzy/rss2sql

安装完成之后就只需要编写配置文件,配置文件使用的yaml语法,接下来用一个例子,稍微解释一下该怎么写配置文件

rss:
  url: https://nyaa.si/?page=rss
  proxies:
    https: http://localhost:8188
sql:
  tablename: nyaa
  field:
    - name: nyaa_id
      val: "None"
      type: INT
      nullable: false
      primary_key: true
      autoincrement: true
    - name: isSukebei
      val: "False"
      type: BOOLEAN
    - name: title
      val: "x.get('title')"
      type: TEXT
    - name: link
      val: "x.get('link')"
      type: VARCHAR
      type_parameter: 50
      unique: true
    - name: category
      val: "x.get('nyaa_category')"
      type: REFTABLE
      type_parameter:
        - VARCHAR
        - 50
    - name: pubtime
      val: "ToolKit.struct_time_To_datetime(x.get('published_parsed'))"
      type: TIMESTAMP
      index: true

RSS 段

这里主要定义两个东西,RSS的地址(url)和访问RSS时使用的代理(proxies),如果不需要使用代理,那么proxies那里留空也许,当然proxies都不写也行

SQL 段

这一块就是最繁重的部分,主要定义两方面内容:表名(tablename)和表字段(fields),表名就不多说了,不要写些奇奇怪怪的名字就行,重点在于表字段。

自增主键

有的时候只是单纯想要一个主键,以便于其他表建立外键,虽说通常我们可以选那些unique的字段当主键,但unique的字段很有可能时VARCHAR,那么其他表建立外键时就要用这种类型,无形中就增加了其他表的大小。这时候可以选择一个INT类型作为主键。比如在例子中,我们建立了一个nyaa_id
(通过定义name) 的主键(通过定义primary_keytrue);其类型为INT(通过定义type);不可空置(通过定义nullablefalse);自增(通过定义autoincrementtrue);取值为None(通过定义val"None"),亦即插入记录时,该字段无值,那么数据库会自动分配一个值。这样就完成了自增主键的定义

常量

可能会奇怪为什么有常量字段,其实这通常会发生在一个RSS具有表世界与里世界两个世界,然后就是想把两个世界的记录放在一张表里。比如例子中isSukebei字段取值恒为"False",要注意这个False首字母要大写,
Python 里是 True/False;
YAML 里是 true/false。

最小配置

注意到定义title字段时取值是"x.get('title')",这里面的x就是feedparser解析完成之后每一个RSS item的字典对象。因为只不过是个字典,当然写起来能写出"x['title']",但这不是用get方法还能设置默认值嘛。

含参数的字段

某些类型,需要一些参数,比如说VARCHAR,需要定义最大长度,那就可以通过定义type_parameter50来取得VARCHAR(50)

参考表(Reference Table)

很多时候,会遇到某些字段是个类型语义的,直接储存为VARCHAR,恕我直言,过于愚蠢。而作为ENUM类型储存,倒也不是不可以,只不过ENUM要增加一个种类时,会比较令人揪心。完全不会增加删减类别的字段又可遇不可求,比如“性别”这个语义本来只需要“男”或“女”,看起来好像不需要变动,现在不也是….😂。因此在标准SQL类型上,追加了一个REFTABLE类型,将这个愚蠢的VARCHAR移到另一张表,变成一个睿智的VARCHAR(通过 type_parameter定义了VARCHAR(50)),再用一个INT类型作为沟通桥梁。

一行写不下的取值

val定义的东西,程序中是通过eval方法对其求值。有些东西对于eval方法来说,它就是不太好实现,比如说要引入其他库的方法,对取值进行一下转换, 最常见的问题就是如何将RSS中的时间字符串,变换成数据库支持的时间字符串。eval的环境变量中有一个ToolKit的类,这个类中包含着一些常见的转换方法,比如在例子中用ToolKit.struct_time_To_datetime方法将time.struct_time类型转换为了datetime.datetime类型,从而让SQLAlchemy得以自动转换。
当然如果ToolKit里没有要用的函数,现阶段只能fork之后自己加上去,以后也许会加入可以向ToolKit动态追加方法的功能吧😀

当然提个Pull request也是欢迎的。

未来形而上学

首先,要搞明白什么叫做将RSS储存下来。RSS究其本质是一个XML标记文件,当我们用Python的feedparser库解析完成之后,对于每一个entry而言(或者说是RSS规定的概念 item ),就是一个Python的Dict对象。就从Duck Typing哲学的角度讲,我只要把这个Dict对象储存起来,那我就相当于把RSS储存了下来;于此同时,也可以把XML保存下来(看起来很蠢,其实这也算不上太蠢,储存的时候相当于可以做 column compression ,相比MySQL只能做 row compression ,体积会小很多。比如一个6k的json格式推特推文数据集,储存于MySQL占用22MB,然而导出为sql文件,哪怕只用zip压缩一下,也不过3.9MB 🤣 ),但只用关系型数据库实现起来,不是很合适,所以姑且不谈。

接下来就是要思考如何唯一确定一个Dict对象。关于这一点,看起来有点难,其实很简单:feedparser解析后产生的entry,必有一个id属性。

最后就是如何储存这个Dict对象。那就更简单了,MySQL在5.7.8版本后支持JSON类型,然后把这个对象变成JSON,就只需要from json import dumps; dumps(...)

可能需要提醒的一点就是,RSS规范要求 item 必须要有三个字段:title、link、description,对应Dict对象就是title、link、summary三个属性。设计数据表的时候如果有需要用到FULLTEXT、UNIQUE索引,可以把这几个字段单列出来;如果只是需要对这几个字段做个普通索引或者干脆不做索引,那直接做个虚列就可以了。

Windows下删除文件提示未找到文件

通常来说,当Windows提示未找到文件的时候,意味着该文件基本已经无法通过Windows自身的GUI(右键删除)或者命令行(cmd下的del命令,即使根据其他极不负责任的信息来源,启用/F开关也是如此,倒不如说是否强制删除只读文件和当前问题本身毫无关系)来删除了。通常情况下意味着待删除文件的文件名在当前Windows下是不合法的;当然也可能文件名本身是合法的,绝对路径不合法(例如Windows极为愚蠢的路径长度限制,也许是怕哪边有个循环路径?)。值得注意的是,文件名(路径)不合法归属于文件系统错误,因此chkdsk命令也不会报告任何错误。

综上所示,快速解决方案就有两条路线:

  1. 关闭操作系统,使用任意Linux发行版(例如Ubuntu,因为好看,绝不是因为没有节操)引导启动,直接删除(正因为不是文件系统错误)
  2. 安装任意压缩软件(例如7-zip,夹带私心),右键添加到压缩包,勾选压缩后删除文件(正因为只是对Windows来说不合法,并不会构成MSVC的运行时错误)

当然个人比较建议第二条路线。

■ Q.E.D

配置LAMP环境以启用HTTP2

先决条件

在Apache2 <=2.4.33;mod_php的情况下,http2模块将会出现mod_php不支持http2的情况。因此需要调整为Apache2 + FastCGI + PHP-FPM的结构。

在Apache2 <=2.4.33;PHP7.1-FPM <=7.1.19的情况下,PHP会因Apache2默认的多进程模块(MPM)为Prefork而无法正常工作。因此需要调整Apache2的MPM模块为event mpm。

实现

以PHP 7.1为例

sudo systemctl stop apache2
sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
sudo apt-get install php7.1-fpm 
# 注意记住此时提示的php.ini位置,便于之后调整配置
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php7.1-fpm
sudo a2dismod php7.1 
sudo systemctl start apache2

Socks5隧道在Ubuntu上的部署

模型

存在一个非受限区(比如互联网)和一个受限区(比如路由器后的局域网),受限区中存在一个Server A因某种原因可以被非限制区的Client访问,但Client不一定能被Server A访问到。其次受限区中的域名无法正确地被非限制区的DNS所解析,尽管受限区中的DNS可以正确解析受限区与非限制区的终端,但无法被非限制区的Client访问。模型图示下

我们的目标是从Client访问Server B。

方案

一般而言,如果DNS能正确解析,那么通过Server A配置iptables转发即可,但是模型中Client无从知晓Server B正确的IP,必须要通过Server A代转域名查询请求。

因此建议使用socks5做隧道,privoxy做http协议转socks5协议。

实现

socks5使用shadowsocks-libev的实现。

Client & Server A

执行

sudo apt-get install --no-install-recommends \
gettext build-essential autoconf libtool libpcre3-dev \
asciidoc xmlto libev-dev libc-ares-dev automake \
libmbedtls-dev libsodium-dev

git clone https://github.com/shadowsocks/shadowsocks-libev.git
cd shadowsocks-libev
./autogen.sh && ./configure && make
sudo make install

Server A

创建文件/etc/shadowsocks/server.json,下为范例

{
"server":"$IP",
"server_port":$Port,
"password":"$Password",
"timeout":300,
"method":"aes-256-cfb",
"fast_open": true,
"workers": 1
}

创建文件/etc/systemd/system/ssserver.service

[Unit]
Description=Daemon to start Shadowsocks Server
Wants=network-online.target
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/ss-server -c /etc/shadowsocks/server.json
User=nobody
[Install]
WantedBy=multi-user.target

执行sudo systemctl enable ssserver

Client

创建文件/etc/shadowsocks/client.json,下为范例

{
"server":"$IP",
"server_port":$Port,
"local_port":$Port,
"password":"$Password",
"timeout":300,
"method":"aes-256-cfb",
"fast_open": true,
"workers": 1
}

创建文件/etc/systemd/system/sslocal.service

[Unit]
Description=Daemon to start Shadowsocks Client
Wants=network-online.target
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/ss-local -c /etc/shadowsocks/client.json
User=nobody
[Install]
WantedBy=multi-user.target

执行sudo apt update && sudo apt install privoxy

在文件/etc/privoxy/config末尾追加

forward-socks5 / 127.0.0.1:$Port .
listen-address  127.0.0.1:$Port

最后执行

sudo systemctl enable sslocal
sudo systemctl enable privoxy

用法

在Client通过$Port代理访问Server B即可

Linux 基于duplicity的备份与还原

与Windows不同,Linux在运行状态下任何文件都是可读的,因此对Linux的备份,其重点主要在于如何打包“/”目录下所有文件。首先,显而易见的是,可以用tar命令,将所有文件打包。这种方法,十分简单,同时也可以启用tar的增量压缩功能,以减少定期备份的空间占用,但正因为简单,所以使用时也有些许不便。其一、使用增量压缩时,如何清理过于古老的备份是需要花费一定精力的地方;其二、打包得到的文件只能放于本地文件系统(远程的则需要使用各种软件映射到本地)。

因此引入duplicity软件包,可以充分解决以上两个难点。

0 安装

sudo apt update
sudo apt install duplicity

1 确定备份范围

  • 包含范围:/
  • 必须排除:/sys、/proc(因为这两者是虚拟文件系统)和设备文件
  • 建议排除:/tmp和/lost+found
  • 按需排除:/mnt和/media

2 建议备份配置

每日增量备份、每周全量备份、保留6个全量备份,保存到可移动存储设备(地址URL用file:///…)或者公共云的对象储存空间中(根据服务商选取URL头,可用duplicity --help查询)

3 Crontab配置

以备份到/media/backup为例,执行命令如下:

sudo duplicity --full-if-older-than 1W \
--no-encryption \
--exclude-device-files \
--exclude /mnt \
--exclude /tmp \
--exclude /proc \
--exclude /lost+found \
--exclude /media \
--exclude /run \
--exclude /sys \
/ file:///media/backup/ \
&& \
sudo duplicity remove-all-but-n-full 6 \
--no-encryption \
--force \
file:///media/backup/ \

然后Crontab中配置为每日运行即可。elegant 🙂

值得注意的是,crontab不支持任何换行符,亦即上面的执行命令分行只是为了方便阅读,具体填入crontab中时,需要合成完整的一行。

4 还原

视损坏的轻重有不同的处理方法,这里以最严重的情况为例,例如错误地将/etc整个删除了(source:不愿透露姓名的朋友)。那么可以按如下建议步骤进行抢救:

  1. 先喝一杯冷水压压惊
  2. 制作一个Ubuntu Live USB
    • 具体而言就是将ISO文件写入到U盘
  3. 从Live USB启动
  4. 安装duplicity sudo apt install duplicity
  5. 将存有备份文件的硬盘挂载,例如说/media/ubuntu/backup
  6. 将上文所述/etc所在卷挂载,例如说/media/ubuntu/broken
  7. 执行命令duplicity restore --no-encryption -r etc /media/ubuntu/backup /media/ubuntu/broken/etc
  8. 通常而言若当前系统状态与最新备份系统状态差异不大的话,就可以重新启动了。抢救到此结束。
    • 但能做出 sudo rm -rf /etc这种举动的,想必是在艰苦卓绝的系统配置过程中意外手滑了一下(source:不愿透露姓名的朋友)。此时的差异应该相当大。
      • 在这种情况下,可能需要视情况恢复/usr目录(source:不愿透露姓名的朋友)

解决home目录下出现的dead.letter

一个萌新通过ssh连接到远程服务器,列出home目录时,惊异地发现竟然多出一个文件,名叫dead.letter。萌新害怕地以为是个hacker掏出了一本death.note,写上了这个服务器的名字。【一个真实的故事】

dead.letter

dead.letter是通过邮件程序无法发送的邮件最终的去向。通常的来源是corn服务,在corn中注册的计划任务如果没有重定向stdout与stderr,将会将两者通过sendmail发送给unix用户。例如服务器域为domain,用户名为user,则最终会将stdout与stderr发给user@domain。对付此类原因产生的dead.letter,一方面可以通过重定向流,使其不发送邮件;当然另一方面可以通过正确配置邮件服务,让它正确的发送出去。

对于有专职运维的服务器来说,发送到本地邮箱就可以了。运维只需要用mail命令查收邮件即可。然而对于个人服务器,并不会天天用ssh连到服务器,只为查收邮件,因为更优雅的方案是使用SMTP向个人的邮箱(e.g Gmail etc.)发送。当然为避免某些邮箱提供商温馨地提示异地登陆,可以考虑新注册一个邮箱。当然考虑到最近个人邮箱注册收紧,建议还是搞个免费版的企业邮箱。

安装 ssmtp

ssmtp是一个简单的smtp的程序(角色为mail user agent),可通过包管理器直接安装。若已安装其他smtp程序,也可通过变更默认mail程序(ubuntu下使用update-alternatives命令),选择使用ssmtp。

配置ssmtp

ssmtp的配置文件通常在/etc/ssmtp目录下,目录下有两个文件:ssmtp.conf与revaliases。这里以使用第三方邮件服务器的SMTP进行邮件发送为例,具体来说采取与Wiki中不同的另一种配置方式:

/etc/ssmtp/ssmtp.conf
 # 能收到所有邮件的邮箱
 root=ROOT@DOMAIN
 # 第三方邮件服务商的smtp服务器(含端口号)
 mailhub=smtp.xxx:port
 # 主机的Full Qualified主机名
 hostname=DOMAIN
 # 这里与wiki不同,不允许用户自定义From
 FromLineOverride=NO
 # 用于登陆第三方邮件服务商的用户名及密码
 AuthUser=username@server
 AuthPass=password
 UseTLS=YES
 UseSTARTTLS=YES
/etc/ssmtp/revaliases
 user:username@server

与wiki中的配置不同的是,不允许用户自定义From为的是:如果只注册了一个第三方邮件服务的邮箱,那么对于这个邮箱,发信只能用这个邮箱作为From,否则会被拒绝发送,因而本例尤其适用于个人服务器。

测试ssmtp

使用sendmail的verbose模式可以检查发信过程中本机与服务器的交流,可以用来观察ssmtp是否正确配置。例如给自己发一份测试邮件:

echo test | mail -v -s "Test mail" me@mydomain