std::forward_list
为 C++ 11 STL 的 单向 链表:
push_back
和 size
方法。std::list
(双向链表,以下简化为 list
)
forward_list::sort
时间复杂度 O(n log n) ,但效率不如 list::sort
,尤其链表包含大量元素时。因 forward_list
与 list
大部分方法相同,这里仅介绍 forward_list
不具备的能力。
因无 forward_list::push_back
方法,顺序插入 forward_list
一组元素得依靠调用者 自行 保存尾指针。
1 2 3 4 5 6 |
|
before_begin
返回首元素之前的元素(placeholder)。该元素实际不存在,访问它将导致未定义行为。
back_inserter
返回的 std::back_insert_iterator
要求容器实现了 push_back
方法。inserter
返回的 std::insert_iterator
要求容器实现了 insert
方法。显然,两者皆不适用于 forward_list
,然 inserter
最接近:
1 2 3 4 |
|
为使上述代码能被编译和运行,须偏特化 std::insert_iterator
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
此法不支持让用户定义 forward_list
的 allocator 。
完美办法为重新实现 forward_list_insert_iterator
和 forward_list_inserter
。
因 forward_list
未保存长度——无 size
方法,故须 O(n) 时间复杂度的 std::distance
计算长度:
1 2 |
|
而 forward_list::empty
(长度是否为 0
)的时间复杂度 O(1)
值得一提 boost::slist
高度相似于 forward_list
,但前者保存长度,即 boost::slist::size
方法时间复杂度 O(1)
最近,我遇到了几乎一样的问题,即 clock_gettime
兼容性。
为了将问题最小化,这里我以 hello.c
模拟该问题:
1 2 3 4 5 6 7 8 9 |
|
在 Ubuntu 20.04 中,用系统内置 GCC 9.3.0 构建它:
1
|
|
-fno-stack-protector
: 禁用 stack 保护特性。目的为最小化未定义符号,最小化问题场景。-s
: 除去调试相关符号,目的同上。通过 readelf -sV hello
展示其符号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
问题在于修改 .gnu.version_r
段偏移 0x0020
的相关值。回顾 Elfxx_Vernaux
结构:
1 2 3 4 5 6 7 |
|
实际仅须修改 vna_hash
和 vna_name
,前者实为后者的校验和。
以下提供 2 种方法修改 hello
二进制文件,使其无须重编译就能运行于 RHEL/CentOS 6 中。
1 2 3 4 5 |
|
0x538
为 .gnu.version_r
段首地址。0x10
为 GLIBC_2.2.5
记录的段偏移,即 Elfxx_Vernaux::vna_hash
偏移。0x08
为 GLIBC_2.2.5
记录内 Elfxx_Vernaux::vna_name
偏移。为保持 hello
不改变, 修改 hello
至 hello-dd
1 2 3 4 5 6 7 |
|
0x20
为 GLIBC_2.1.7
记录的段偏移,即 Elfxx_Vernaux::vna_hash
偏移。0x08
为 GLIBC_2.1.7
记录内 Elfxx_Vernaux::vna_name
偏移。通过 readelf -sV hello
观察修改是否生效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
最后,将 librt.so.1
添加至 hello-dd
的库依赖列表中。
1
|
|
LIEF 是一个能分析和修改 ELF , PE , MachO 和 Android 格式的库,它提供了 C/C++ 和 Python 的 API.
以下通过 LIEF 的 Python API 达到上述同样的效果。
1
|
|
创建 lief-hello.py
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
以上 symbol_version_auxiliary
相当于前述 Elfxx_Vernaux
执行 python3 lief-hello.py
将生成 hello-lief
。
通过 readelf -sV hello-lief
观察修改是否生效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
1 2 3 |
|
epel-release
: Extra Packages for Enterprise Linux (EPEL) 仓库配置文件,因 EPEL 才包含 LXC 相关包。dnsmasq
和 iptables
为后续配置 LXC 网络。debootstrap
为后续安装 Ubuntu 容器。安装 LXC 模板
1 2 3 4 5 |
|
lxc-templates
包,但该包未包含 Ubuntu 容器模板。故还须从其源码安装。lxc-templates-3.0.4
并不需要编译任何 C 程序,但 configure
将检查 gcc
。故须安装 gcc
gcc
和 make
,则可自行 卸载 。LXC 存在 3 种网络:
创建虚拟网卡 lxcbr0
,作为容器虚拟网络的网桥:
1 2 3 4 5 6 7 8 9 |
|
1
|
|
因容器与 host 在同一网络名字空间,容器中进程监听端口不能与 host 中端口相同,否则监听失败,如 sshd
1
|
|
默认不配置或如上述,容器仅存在 loopback 网络。
以安装 Ubuntu 20.04 容器为例:
1 2 3 4 |
|
-n
:容器名。-t
:模板名,对应 /usr/share/lxc/templates/lxc-*
文件。--
:其后为传入 /usr/share/lxc/templates/lxc-ubuntu
的参数。-r
: Ubuntu 发行代号,默认为主机的 Ubuntu 发行代号,否则为最近 LTS--mirror
和 --security-mirror
:为加速创建,两者设置为国内镜像。其它参数:
-u <user>
: 容器用户名,默认 ubuntu
。--password <password>
:容器密码,默认 ubuntu
。-S <keyfile>
: SSH 私钥文件。其它模板,如 CentOS ,可通过 /usr/share/lxc/templates/lxc-centos -h
查看相关参数。
1
|
|
登录
1
|
|
1
|
|
输出形如:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
仅以将主机端口 2222
重定向至容器 22
为例(须将 <container ip>
替换为容器 IP )。
以下两者取其一,若未安装 firewalld
,则采用 iptables
:
1
|
|
将上述命令写入 /etc/rc.local
,从而保证重启依旧生效:
1
|
|
1 2 3 |
|
鉴于互联网针对 VPS 暴力破解 SSH 越来越频繁,以下以 Ubuntu 为例,介绍如何利用 Fail2Ban 有效禁止 SSH 破解。
1
|
|
Fail2Ban 配置文件格式 INI,存于 /etc/fail2ban
目录:
fail2ban.conf
: fail2ban
程序运行的日志和数据库等参数。jail.conf
: 禁止(ban)相关参数。filter.d/*
: jail.conf
中涉及“节”(section,如 [sshd]
)的过滤器,由正则表达式构成。action.d/*
: jail.conf
中涉及“节”(section,如 [sshd]
)的处理器。它们皆为安装文件,直接修改将导致后续升级 无法自动合并 配置文件。Fail2Ban 提供了自定义配置文件的机制:
fail2ban.conf
可依此通过 fail2ban.d/*
和 fail2ban.local
来重定义相关选项。jail.conf
可依此通过 jail.d/*
和 jail.local
来重定义相关选项。默认安装,/etc/fail2ban/jail.d/defaults-debian.conf
已启用 sshd
的 jail
通常,除 jail.conf
外,不需要改变配置。以下着重介绍 jail.conf
中的参数,它们不仅是默认(全局)参数(隶属于 [DEFAULT]
),而且可在具体 jail
中重定义(如 [sshd]
)。
ignoreip
: 忽略不 IP 地址(CIDR 格式)或机器名,以空格分隔。bantime
: 主机被禁止时长,默认 600 秒。maxretry
: 在 findtime
时间窗口中,允许主机认证失败次数。达到最大次数,主机将被禁止。findtime
: 查找主机认证失败的时间窗口。 不意味 着每隔 findtime
时间扫描一次日志。高版本 Fail2ban 支持 s
(秒), m
(分)和 d
(天)作为时间单位,如 10m
和 1d
backend
默认 auto
,支持 4 种文件修改的监控后端:
pynotify
: inotify 的 Python 绑定。在 Debian/Ubuntu 中,对应 python3-pyinotify
包。gamin
: KDE 和 GNOME 等桌面使用的文件监控系统,通常基于 inotify 实现。 在 Debian/Ubuntu 中,对应 gamin
包。polling
: 周期性扫描相关系统日志。相对于其它几种后端,或将占用 更多 CPU 时间。systemd
: 通过 Systemd 的 Python 绑定来访问 systemd 日志,logpath
将无效。 在 Debian/Ubuntu 中,对应 python3-systemd
包。auto
: 依此尝试 pyinotify
, gamin
和 polling
通常,在安装 fail2ban
安装包时,将自动安装 iptables
, whois
, python3-pyinotify
和 python3-systemd
,即启用 pyinotify
后端。除非 APT 配置 APT::Install-Recommends "false";
(如:/etc/apt/apt.conf
)或 --no-install--recommends
。
建议将 bantime
设为 86400
(即 1 天)。如 jail.local
:
1 2 3 4 |
|
或
1 2 3 4 |
|
修改后,须重载 fail2ban
:
1
|
|
/var/log/fail2ban.log
将出现类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
1
|
|
输出类似:
1 2 3 4 5 6 7 8 9 |
|
列表 iptables 的规则:
1
|
|
输出类似:
1 2 3 4 5 6 7 8 9 10 |
|
当时,基本思路为挂载问题虚拟机的虚拟盘至新虚拟机,通过后者导入原虚拟盘的卷组,从而备份出重要数据。
然而,恰好该云平台仅有该虚拟机的原始模板,新实例的虚拟盘与旧盘:
导致无法激活旧卷组。
以下以 VirtualBox 安装 Ubuntu 20.04 虚拟机,克隆其系统盘为新虚拟盘,并将新盘加入该虚拟机来模拟场景。
启动虚拟机将遇到 重复 PV 错误:
进入 initramfs shell ,为了避免激活 root 卷组失败,暂时删除 /dev/sdb
:
1 2 3 |
|
将正常进入系统。
/dev/sdb
登录系统,切换 root 用户, 重新扫密 /dev/sdb
:
1
|
|
查看内核日志:
1
|
|
可发现 /dev/sdb
被加入系统:
1 2 3 4 5 6 7 8 |
|
这里 /dev/sda
位于 SCSI host 0 ,而 /dev/sdb
位于 SCSI host 1 。实际环境中,若无法确定目标盘在第几个 host ,可全部扫描一遍:
1 2 3 |
|
以新卷组名 ubuntu
执行 vgimportclone 导入重复 VG :
1
|
|
输出:
1 2 |
|
/dev/sdb5
为新盘 VG 的 PV 。实际环境中,若 VG 存在多个 PV,则须全部作为该命令的参数。
1
|
|
输出:
1 2 |
|
它由3个组建构成:
lxd
:系统守护进程,它导出能被本地和网络访问的 RESTful APIlxc
:客户端命令行,它能跨网络管理多个容器主机。nova-compute-lxd
: OpenStack Nova 插件,它使 OpenStack 如虚拟机一般,管理容器。因 Ubuntu 16.04 LXD 2.x 和 Ubuntu 18.04 LXD 3.x 版本都较旧,且 Ubuntu 20.04 放弃 apt
安装 LXD 。
请据 Snap简介 安装 LXD
为避免每次 sudo lxc
,可将 lxd
组加入当前(非root)用户的附加组:
1
|
|
1
|
|
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
参看: How to install LXD on Ubuntu
1
|
|
LXD 默认提供3个远程镜像:
ubuntu
: Ubuntu 稳定版镜像ubuntu-daily
: Ubuntu 每日构建的镜像images
: 其它发行版本的镜像,主要包括:
1
|
|
1
|
|
1
|
|
因上述镜像都在国外,复制镜像速度非常慢。可注册清华大学的镜像来加速使用:
1
|
|
以下主要以tuna-images
为例。
1
|
|
--copy-aliases
: 复制所有镜像别名。 每个镜像可能有多个别名,如 Ubuntu 20.04 的镜像的别名为 20.04
和 focal
等。--auto-uppdate
: 自动更新镜像--public
: 公开镜像,让其它机器可以复制它。后面将进一步介绍如何公开 LXD 端口。默认为 64-bit 容器:
1
|
|
若须 32-bit 容器,则
1
|
|
1
|
|
相当于
1 2 |
|
直接从远程镜像初始化或启动,存在如下缺点:
lxc image ls
:1 2 3 4 5 |
|
故最佳实践为首先复制镜像,然后初始化或启动:
1 2 |
|
1 2 |
|
1
|
|
或 快速列表
1
|
|
1
|
|
1
|
|
1 2 |
|
LXD 3.19 开始支持创建虚拟机:
简单的说,所有容器相关的命令加--vm
。
若类似复制容器命令:
1
|
|
则可能 覆盖 本地容器 ubuntu 20.04 的别名,造成后者 无别名:
1 2 3 4 5 6 7 |
|
为了避免上述问题,自定义镜像别名,如以 vm/
作为镜像别名前缀:
1
|
|
1
|
|
Ubuntu 16.04 默认内核 4.4 ,将遇到
1 2 3 4 |
|
须安装 4.15 (HWE) 内核,并重启:
1
|
|
1
|
|
注:无列表快照的直接操作,只能通过获取容器的详细信息 lxc info
来获取的快照名字。
1
|
|
1
|
|
模仿 Docker 删除镜像的 docker rmi
, 创建 lxc rmi
:
1
|
|
类似:
1 2 3 |
|
1
|
|
1
|
|
lxd将数据存放于 /var/snap/lxd/common/lxd
:
images
: 存放镜像文件lxc
: 存放容器lxd.db
:lxd元数据数据库,基于sqlite3server.crt
:服务器证书server.key
:服务器密钥unix.socket
:lxd监听的本地套接口优势:
劣势主要为安装包占用较多存储空间。
以下主要以 LXD 的 snap 包为例。
1
|
|
查看版本
1
|
|
为避免系统可能存在旧版本 LXD 与 snap 安装的最新版 LXD 冲突:
1
|
|
然后:
1
|
|
默认 stable
频道,也可以指定 edge
频道:
1
|
|
安装后,可切换频道。
1
|
|
相比 RPM 和 Debian 包需手动更新,snap 包将在后台自动更新。若需手动更新,则
1
|
|
snap 还能切换频道并更新
1
|
|
snap 应用程序位于 /snap/bin
,如: /snap/bin/lxd
为便于使用,可将该路径追加于 ~/.bashrc
或 ~/.zshrc
环境变量 PATH
,如:
1
|
|
1
|
|
1 2 |
|
1
|
|
也可通过浏览器在应用市场 Snapcraft 上搜索需要的包(应用)。
1
|
|
输出:
1 2 3 4 |
|
若 --all
, 则列表包的所有版本 (revision)
1
|
|
输出:
1 2 |
|
1
|
|
若遇到当前版本bug,则可考虑回退程序。当前跟踪的 channel 不会因上一版本源于不同 channel 而改变。
snap refesh
不会更新已回退的包,除非指定包名,如:snap refresh lxd
1
|
|
卸载旧版本(释放空间)。
1
|
|
为避免卸载和重装而禁用:
1
|
|
反之:
1
|
|
1
|
|
输出:
1 2 3 |
|
1 2 3 |
|
停止服务,并禁用自动启动:
1
|
|
开始服务,并启用自动启动:
1
|
|
1 2 3 |
|
1
|
|
输出:
1 2 3 4 |
|
或指定包
1
|
|
若 --no-wait
, 则后台运行。
1
|
|
或指定Set ID
1
|
|
1
|
|
1
|
|
1
|
|
相比 RPM 和 Debian 等传统安装包,通过解开来安装。
存于 /var/lib/snapd
,格式为 squashfs 的 snap 包,不直接解开,而是(只读)挂载至 /snap/<snapname>/<revision>
目录。如:
lxd
的 revision 14890 的包存储于 /var/lib/snapd/snaps/lxd_14890.snap
:
1
|
|
发现:
1
|
|
另外, /snap/<snapname>/current
为当前版本挂载点,它为指向 /snap/<snapname>/<revision>
的符号链接。
而且,每个 snap 包含了不依赖系统库的完整的运行时库。
snap 为了加速二次安装,首次安装会将 snap 包缓存至 /var/lib/snapd/cache
。
目前为止, snap 未提供命令清楚缓存。若需 释放空间 ,须手动删除该目录中的文件。
/var/snap
存储每个包的运行数据(或元数据)。如:/var/snap/lxd
主要为 lxd 的元数据。
该目录或将消耗大量的存储空间,因受制于 AppArmor ,不能通过移动目录(至另外分区)和符号链接来释放空间,须 mount --bind
移动的目录。
/snap/bin
存储指向包的应用程序(符号链接),如:
1
|
|
看到 lxd
仅为 snap
的符号链接
1
|
|
每个包快照用独立的 zip 文件存储,包含:
meta.json
: 描述快照内容、配置和校验码。archive.tgz
: 包含系统数据。user/<username>.tgz
: 包含每个系统的用户数据。快照存储于 /var/lib/snapd/snapshots
ACE 拥有多种实现的Reactor:
ACE_Select_Reactor
: 基于 select
的实现。ACE_Dev_Poll_Reactor
: 基于 Linux epoll
或 BSD /dev/poll
的实现。ACE_WFMO_Reactor
: 基于 Windows WaitForMultipleObjects
的实现它们都可用于实现 one loop per thread 模式。 相比 ACE_TP_Reactor
:
ACE_TP_Reactor
继承于 ACE_Select_Reactor
,加锁 保证多线程竞争同一 Reactor 的安全。并行可扩展能力受限,且最大支持 1024 个描述符。ACE_Dev_Poll_Reactor
的 one loop er thread,每个线程拥有独立的 Reactor,线程之间不存在竞争。充分发挥并行能力,且最大支持几十万甚至百万个描述符。以下以 ACE_Select_Reactor
为例浅析实现的关键代码。更完备示例代码请参看我的 ace_echod
event_loop
将作为线程函数运行于独立线程中,它与 ACE 教科书基本一样:
1 2 3 4 5 6 7 8 9 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
据 ACE 的 Acceptor 模式, Echo_Handler
继承于 ACE_Svc_Handler<ACE_SOCK_STREAM, ACE_NULL_SYNCH>
,它与 ACE 教科书实现相似。
Echo_Acceptor
注册默认 Reactor ,其事件循环运行于主线程。Echo_Handler
对象时,事件循环管理器以简单循环方式分配 Reactor 给新对象。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
ACE_Reactor::size
实现不一致,除 ACE_Dev_Poll_Reactor
返回当前注册的 handler 数量,其它实现返回handler表的容量。导致无法直接通过注册 handler 的数量,实现 Round-Robin,即均衡分配 handlerACE_Reactor
无法获取当前是否等待事件(即空闲)状态,导致无法直接将 handler 分配到空闲事件循环中。解决上述问题,要么直接修改 ACE 的实现,要么在具体 handler 实现中增加状态或统计变量,两者都需用原子变量来避免加锁。
]]>dockerd
)默认监听在Unix套接口 /var/run/docker.sock
。仅能本地或SSH至Docker运行的主机访问,不利于自动化开发测试。
若需远程访问,得配置它同时监听在TCP端口(默认2376)。
然而,Docker不支持用户认证。 可通过配置TLS客户端和服务端认证,规避非法客户端远程访问服务端。
1 2 |
|
交互式输入各种选项,其中
1
|
|
<HOST>
为Docker远程服务端域名(允许不存在的域名)。
创建 extfile.cnf
1 2 |
|
<HOST>
与上同。<IP>
为Docker远程服务端IP然后:
1 2 3 |
|
创建 extfile-client.cnf
1
|
|
然后:
1 2 3 |
|
至此,删除中间文件,并降低相关密钥访问权限。
1 2 3 |
|
通过scp
,将 ca.pem
, server-cert.pem
和 server-key.pem
复制至远程服务端 /etc/docker
目录中。
注:ca-key.pem
不应复制至远程服务端。因既不意义,又增大泄漏可能性。
/etc/docker/daemon.json
1 2 3 4 5 6 7 8 |
|
为保证证书和密钥安全,须
1 2 |
|
在 /lib/systemd/system/docker.service
的 ExecStart
中, -H fd://
与 /etc/docker/daemon.json
的 hosts
冲突,将导致Docker服务启动失败。
若在 /lib/systemd/system/docker.service
中直接删除 -H fd://
,docker-ce包升级将恢复原样。
可创建 /etc/systemd/system/docker.service.d/options.conf
及其父目录,并编辑
1 2 3 |
|
或执行如下命令:
1 2 3 4 5 6 7 8 9 10 |
|
然后:
1 2 |
|
在客户端和CA相关证书和密钥所在目录,创建docker.env
:
1 2 3 |
|
在远程访问Docker前,载入该文件:
1
|
|
然后,控制远程Docker与本地Docker类似,如:
1
|
|
1 2 |
|
在国内,为了加速下述安装:
1
|
|
激活rbenv
1
|
|
为了每次登录后自动激活rbenv,需将NMV_DIR
、nvm.sh
和补齐加入bash的~/.bashrc(或zsh的~/.zshrc)
1 2 |
|
验证是否安装正确:
1
|
|
或
1
|
|
输出类似:
1 2 3 4 5 6 7 8 |
|
1
|
|
除了Ruby官方版本,还支持RBX和JRuby等。
安装过程,实际为下载并编译指定版本的Ruby源码,故需系统安装:
1
|
|
然后:
1
|
|
Ruby版本安装在 ~/.rbenv/versions
目录中。
1
|
|
1
|
|
等同于
1
|
|
清除RBENV_VERSION
1
|
|
1 2 3 4 5 6 |
|
若Ruby程序须通过Systemd启动,则其Systemd脚本类似:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
username
为服务运行的用户名,通常为 RBENV_ROOT
所属用户。group
为服务运行的组名,通常为 RBENV_ROOT
所属组。RBENV_VERSION
为Ruby版本号。app dir
为Ruby程序的目录。app
为Ruby程序或启动脚本。若不希望使用Ruby的官方Docker镜像,可利用rbenv创建镜像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
RUBY_CFLAGS
为 -O3
可编译优化的Ruby程序。在某些情况,可提高应用20-30%的运行效率。1
|
|
shadowsocks-rust是Shadowsocks的Rust语言实现,它不仅具有传统Shadowsocks特性,而且还具有:
Shadowsocks-Rust未提供DEB安装包。为了方便安装,可下载其静态链接版本
1
|
|
以root用户创建目录/etc/shadowsocks-rust,编辑/etc/shadowsocks-rust/config.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
address
为服务端地址。server_port
为服务端监听端口。password
为客户端和服务端预设的共享密码,它最好由安全密码生成器生成(如LastPass或KeePass),且长度不小于6个字符。timeout
为连接超时时间。method
为加密算法,aes-256-cfb
的安全性较好。servers
元素为一个Shadowsocks服务器配置。因Ubuntu 14.04过保,下面仅以Ubuntu 16.04及以后版本为例。
以root用户创建/etc/systemd/system/shadowsocks-rust-local.service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
注册并启动服务:
1 2 3 4 5 |
|
注:
sslocal
进程权限,以nobody用户和nogroup组运行它。sslocal
进程仅能监听socket.1
|
|
1 2 3 4 5 6 7 8 9 10 |
|
1
|
|
1 2 |
|
1
|
|
1
|
|
1
|
|
1
|
|
为了便于测试,我将上述诸多过程归纳成脚本:gluster_docker
gluster-web-interface是一个管理GlusterFS的Web应用应用,它基于Ruby on Rails实现。
为了简化其安装,我创建其docker镜像docker-gluster-web-interface:
1
|
|
与SSH动态代理相似,客户端呈现为SOCKS 5代理服务,客户端与服务器之间采用加密通信。服务器部署于GFW之外,从而实现代理翻墙服务。
网络中普遍采用Python版本的shadowsocks,该版本看似安装简单,却存在如下缺点:
而shadowsocks-libev是Shadowsocks在嵌入式和低端设备的轻量级实现:
下面基于Ubuntu 14.04/16.04介绍它的安装和配置。
客户端和服务端的安装方法相同:
1 2 3 |
|
编辑/etc/shadowsocks-libev/config.json:
1 2 3 4 5 6 |
|
server_port
为服务端监听端口password
为客户端和服务端预设的共享密码,它最好由安全密码生成器生成(如LastPass或KeePass),且长度不小于6个字符。timeout
为连接超时时间。method
为加密算法,aes-256-cfb
的安全性较好。配置完成后,需重启:
1
|
|
创建/etc/shadowsocks-libev/client.json (文件名可修改):
1 2 3 4 5 6 7 |
|
安装包没有提供系统服务脚本,故须自己创建Upstart脚本/etc/init/ss-local.conf (文件名可修改):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
为了client.json的安全:
1 2 3 |
|
启动客户端
1
|
|
最后,通过修改/etc/default/shadowsocks-libev的START=no
禁止在客户机启动服务端程序——它在客户机没有作用。
1
|
|
安装包提供了systemd的服务模板/lib/systemd/system/shadowsocks-libev-local@.service
默认ss-local
以root用户运行,可修改上述模板为nobody用户和nogroup组,从而提高安全:
1 2 3 4 5 6 7 |
|
注意,升级shadowsocks-libev,模板将回复原状,须再次修改。
1 2 3 |
|
注意,@client与client.json的基本名必须一致。
最后,禁用并停止服务端程序:
1 2 |
|
类似SSH集群,多个shadowsocks也可以构建SOCKS 5集群,具体请参考《SSH翻墙集群》的“HAProxy的配置方法”。
实用中发现,ss-local
不会因为shadowsocks服务器是否可达,而停止运行或拒绝HAProxy连接。
导致HAProxy无法探测shadowsocks服务器是否离线或不可访问,部分负载将失败或重试(浏览器),从而影响体验。
]]>Python内置的multiprocessing模块不仅支持并行计算,而且与Gevent接口相似。所以,模仿Gevent的Actor实现multiprocessing的Actor并不困难。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
将并行Actor扩展为发布-订阅者模式,基本与Gevent的实现一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
基于Publisher实现Ping-Pong,与Gevent的实现差异也不大。
不同的是它实际启动3个进程。除主进程外,每个actor分别运行于独立进程,从而实现多核计算。主进程监督2个actor进程运行,如启动、停止以及异常处理等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Python 3.5推出了async/await语法,在语法层面简化了异步编程。官方库asyncio是应用async/await的途径。
Ubuntu 16.04默认安装Python 3.5,或者通过pyenv安装它。
基于asyncio,可以实现async actor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
上述代码的关键是通过asyncio.Queue异步接收消息,并异步处理接收到的消息。
通过这个类,实现Ping-Pong示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
该示例代码中,actor之间同步发送消息(asyncio.Queue.put_nowait),由于运行在单线程上,并不存在竞争。
某些应用场景需要周期性激活Actor,当Actor没有收到任何消息时。
基于上述代码,利用asyncio.wait_for的超时功能来实现接收消息超时。如此,进一步加强Actor的并发能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
由于message仅支持Python 2,而且Google Code已经停止服务。
基于原代码基础上,我在GitHub创建python-message,并扩展支持Python 3.
新版本message,也可以通过pip安装:
1
|
|
在此基础上,将异步Actor扩展为发布-订阅者模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
基于Publisher实现Ping-Pong,从而解耦发送者与接收者,且支持发送者发送1条消息时,多个接收者接收同1条消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
相比Gevent实现的Actor,异步Actor并不透明支持所有的I/O函数,它仅支持基于asyncio实现的库,如aiohttp。
1 2 3 |
|
激活nvm
1
|
|
为了每次登录后自动激活nvm,需将NMV_DIR
、nvm.sh
和补齐加入bash的~/.bashrc(或zsh的~/.zshrc)
1 2 3 |
|
在国内,为了加速下述安装,可在bash的~/.bashrc(或zsh的~/.zshrc)加入:
1
|
|
1
|
|
除了Node.js官方版本,还支持io.js
1
|
|
它会自动下载指定版本的Node.js二进制包(不需要编译源码),安装在~/.nvm/versions/node
通常,最好安装最近的长周期版本:
1
|
|
1
|
|
1
|
|
它将Node.js指定版本的bin路径加入PATH.
还原环境变量PATH
1
|
|
1
|
|
或
1
|
|
它存储在工程根目录中,用于记录该工程依赖的Node.js版本
1
|
|
进入工程目录(当前目录),运行
1
|
|
将根据.nvmrc指定shell的Nodejs版本
1 2 3 |
|
升级完成后,需要重新激活nvm
1
|
|
若不希望使用NodeJS的官方Docker镜像,可利用nvm创建镜像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 |
|
将PYENV_ROOT
和pyenv init
加入bash的~/.bashrc(或zsh的~/.zshrc)
1 2 3 4 |
|
1
|
|
除了Python官方版本,还支持
安装过程,实际为下载并编译指定版本的Python源码,故需系统安装:
1
|
|
还可选择安装:
1
|
|
然后:
1 2 |
|
.pyenv/cache
目录中,在安装完后可手动删除。1
|
|
1
|
|
等同于
1
|
|
清除PYENV_VERSION
1
|
|
pyenv-virtual是pyenv的插件,它支持管理多个virtualenv
1 2 |
|
1
|
|
1
|
|
1
|
|
1 2 |
|
将指定virtualenv,迁移至另一virtualenv,须安装pyenv插件pyenv-pip-migrate:
1
|
|
然后:
1
|
|
1 2 3 |
|
更简单的办法为安装pyenv插件pyenv-update:
1
|
|
它不仅能更新pyenv,还能更新pyenv所有已安装的插件:
1
|
|
若Python程序须通过Upstart启动,则其Upstart脚本可以类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
或
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
username
为服务运行的用户名,通常为PYENV_ROOT
所属用户。group
为服务运行的组名,通常为PYENV_ROOT
所属组。PYENV_VERSION
为Python版本号或virtualenv的名字。app dir
为Python程序的目录。app
为Python程序或启动脚本。若Python程序须通过Systemd启动,则其Systemd脚本类似:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
username
为服务运行的用户名,通常为PYENV_ROOT
所属用户。group
为服务运行的组名,通常为PYENV_ROOT
所属组。PYENV_VERSION
为Python版本号或virtualenv的名字。app dir
为Python程序的目录。app
为Python程序或启动脚本。若不希望使用Python的官方Docker镜像,可利用pyenv创建镜像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Actor模型(中文版)是一种基于消息传递(message-passing)的并发(concurrent)计算模型。
它与OOP异同:
gevent(中文版)是一个基于libev的并发库,它为各种并发和网络相关的任务提供了整洁的API。
其Actors(中文版)章节已介绍了如何基于Greenlet和 Queue实现
该实现存在的问题:发送者与接收者紧耦合,发送者持有接收者的对象引用。
在此基础上,我利用message库将其扩展为发布-订阅者模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
如此,不仅将发送者与接收者解耦,而且支持发送者发送1条消息时,多个接收者接收同1条消息。
类似Ping-Pong的示例,Pinger对象订阅了Ponger对象的evt.pong事件,Ponger对象订阅Pinger对象的evt.ping事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
某些应用场景需要周期性激活Actor,当Actor没有收到任何消息时。
基于上述代码,利用gevent.queue.get的超时功能来实现接收消息超时。如此,进一步加强Actor的并发能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
然而在实际使用中,它存在如下缺点:
在大概2年前,我摸索出SSH动态代理集群的办法。并将之部署于我所服务的公司,成功负载了20-30人日常翻墙学习与工作的需求。
SSH动态代理,即为SOCK5代理,所以我们需要的是SOCK5集群。
若搜索socks 5 load balance不难发现一些有用信息:
What is the best way to load balance multiple sock5 proxys on seperate VM’s in the same datacenter?
我将分别介绍3种方法搭建SOCK5集群:
关于SSH动态代理的配置方法,请参看AutoSSH简介
Ubuntu的Nginx并没有将nginx_tcp_proxy_module编译进去。为了简化安装,我基于Ubuntu的Nginx包,做了Nginx的PPA:
添加我的PPA
1 2 |
|
若未安装nginx,则
1
|
|
若已安装nginx,则
1
|
|
在/etc/nginx/nginx.conf中,增加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
为了查看集群的状态,在/etc/nginx/sites-enabled/default的中,增加如下内容:
1 2 3 4 5 6 7 |
|
重启Nginx:
1
|
|
如此,访问http://<cluster IP>/status将能查看集群的详细状态。
Ubuntu 15.10之前的官方Nginx版本都小于1.9,须通过ppa:nginx/development升级nginx。
添加ppa:nginx/development
1 2 |
|
若未安装nginx,则
1
|
|
若已安装nginx,则
1
|
|
在/etc/nginx/nginx.conf中,增加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
重启Nginx:
1
|
|
安装haproxy
1
|
|
在/etc/haproxy/haproxy.cfg中,增加如下内容:
1 2 3 4 5 6 7 8 9 10 11 |
|
为了查看集群的状态,在/etc/haproxy/haproxy.cfg中,增加如下内容:
1 2 3 4 5 |
|
默认安装,haproxy处于不活动状态,须要激活它。
在/etc/default/haproxy中,修改如下行:
1
|
|
最后,启动haproxy:
1
|
|
如此,访问http://<cluster IP>:9090/haproxy?stats将能查看集群的详细状态。
1
|
|
-M port[:echo_port]
指定监控端口(和echo端口,默认为前者加1)。
echo_port
,仅需服务监听地址为 localhost
。port
设置为 0
,则将禁用监控功能。仅在 ssh 退出后重启它。-f
使 autossh 在后台运行。
另外,autossh 还提供了一组环境变量来控制其行为, 这里仅介绍几个有代表性的,其可以 man autossh
AUTOSSH_FIRST_POLL
: 首次论询测试时间。AUTOSSH_POLL
: 连接论询时间,默认 600 。若该值小于两次网络超时(默认 15 秒),则网络超将被调整为该值的 ½AUTOSSH_GATETIME
: 等待 ssh 连接成功建立的时间,默认 30 秒,超时表示首次运行失败,将退出 autossh 。若设为 0 ,则禁用该功能,通常用于启动时运行 autossh 。AUTOSSH_MAXLIFETIME
: autossh 最长运行时间,达到该时间,autossh 将退出,并杀死 ssh 进程。AUTOSSH_MAXSTART
: ssh 最大启动次数。默认-1,表示无限制。以 SSH 动态代理(即 SSH 翻墙)为例:
init script 和 Upstart 都可以将 autossh 变成服务,然 Upstart 的 respawn
容错能力更强,它能在服务进程掉线,重新启动该服务。
创建 /etc/init/autossh.conf
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
setuid
和 setgid
为了让 autossh 运行在指定的用户和用户组上。start on
表示当 eth0
或 wlan0
激活时,启动 autossh , stop on
反之。其目的为避免系统启动或网络掉线时,频繁尝试启动 autossh 。sshproxy
为 ssh 别名,须在 setuid
和 setgid
指定用户的 ~/.ssh/config
中配置。开始服务:
1
|
|
systemd 也能自动重启掉线的服务进程。
创建 /etc/systemd/system/autossh.service
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
开启服务:
1 2 |
|
最近 OpenSSH 都支持选项 ServerAliveInterval
和 ServerAliveCountMax
,实际为建立在 SSH 协议上的心跳测试。当测试失败后, SSH 客户端进程将退出。通过 Upstart 的 respawn 功能重启 SSH 客户端进程,也能达到 autossh 目的。
仍以 SSH 动态代理为例:
创建 /etc/init/sshproxy.conf
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
开始服务:
1
|
|
创建 /etc/systemd/system/sshproxy.service
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Restart
, RestartSec
和 RestartSec
为当对等端 sshd
停止或网络异常时,服务 sshproxy 将退出并每 3 秒重启服务,直至连接上对等端 sshd
. 参见 Systemd service that is always restarted
开启服务:
1 2 |
|
若存在多个 SSH 动态代理,则可模板化服务。
创建 /etc/systemd/system/sshproxy@.service
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
为每个动态代理在 ~/.ssh/config
中创建别名:
1 2 3 4 5 6 7 8 |
|
开启服务:
1 2 |
|