久闻 Let’s Encrypt 的大名,一直没抽出空来折腾一下。刚好昨天看到下面这篇文章,晚上就折腾了一下。以下内容全部遵循这篇文章Let's Encrypt,免费好用的 HTTPS 证书的教程,在有些我自己遇到的坑的地方,做了一些修改和提醒。

创建帐号

首先创建一个目录,例如 /home/xxx/ssl,用来存放各种临时文件和最后的证书文件。进入这个目录,创建一个 RSA 私钥用于 Let’s Encrypt 识别你的身份:

openssl genrsa 4096 > account.key

创建 CSR 文件

接着就可以生成 CSR(Certificate Signing Request,证书签名请求)文件了。在之前的目录中,再创建一个域名私钥(一定不要使用上面的账户私钥):

openssl genrsa 4096 > domain.key

PS:这里是创建了 4096 位的证书,为了一定的兼容性,我创建的是 2048 位的证书。

生成 CSR 时推荐至少把域名带 www 和不带 www 的两种情况都加进去,其它子域可以根据需要添加(目前一张证书最多可以包含 100 个域名):

1
openssl req -new -sha256 -key domain.key -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:yoursite.com,DNS:www.yoursite.com")) > domain.csr

执行这一步时,如果提示找不到 /etc/ssl/openssl.cnf 文件,应该是没有安装 openssl 库。

配置验证服务

我们知道,CA 在签发 DV(Domain Validation)证书时,需要验证域名所有权。传统 CA 的验证方式一般是往 [email protected] 发验证邮件,而 Let’s Encrypt 是在你的服务器上生成一个随机验证文件,再通过创建 CSR 时指定的域名访问,如果可以访问则表明你对这个域名有控制权。

首先创建用于存放验证文件的目录,例如:

mkdir /home/xxx/www/challenges/

PS:这里一定要开辟另外的一个目录,千万不要和上面的 ssl 的目录放在一起。因为这个目录下的文件会开发的互联网给别人访问的,如果也放在 ssl 目录下,你的一些私钥会被人扫描到的。

然后配置一个 HTTP 服务,以 Nginx 为例:

1
2
3
4
5
6
7
server {
server_name www.yoursite.com yoursite.com;
location /.well-known/acme-challenge/ {
alias /home/xxx/www/challenges/;
}
}

这个验证服务以后更新证书还要用到,需要一直保留。

获取网站证书

先把 acme-tiny 脚本保存到之前的 ssl 目录:

wget https://raw.githubusercontent.com/diafygi/acme-tiny/master/acme_tiny.py

指定账户私钥、CSR 以及验证目录,执行脚本:

1
python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /home/xxx/www/challenges/ > ./signed.crt

如果一切正常,当前目录下就会生成一个 signed.crt,这就是申请好的证书文件。
如果你把域名 DNS 解析放在国内,这一步很可能会遇到类似这样的错误:

1
ValueError: Wrote file to /home/xxx/www/challenges/oJbvpIhkwkBGBAQUklWJXyC8VbWAdQqlgpwUJkgC1Vg, but couldn't download http://www.yoursite.com/.well-known/acme-challenge/oJbvpIhkwkBGBAQUklWJXyC8VbWAdQqlgpwUJkgC1Vg

这是因为你的域名很可能在国外无法访问,可以找台国外 VPS 验证下。我的域名最近从 DNSPod 换到了阿里云解析,最后又换到了 CloudXNS,就是因为最近前两家在国外都很不稳定。如果你也遇到了类似情况,可以暂时使用国外的 DNS 解析服务商。

搞定网站证书后,还要下载 Let’s Encrypt 的中间证书。我在之前的文章中讲过,配置 HTTPS 证书时既不要漏掉中间证书,也不要包含根证书。在 Nginx 配置中,需要把中间证书和网站书合在一起:

1
2
wget -O - https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem > intermediate.pem
cat signed.crt intermediate.pem > chained.pem

最终,修改 Nginx 中有关证书的配置并 reload 服务即可:

1
2
ssl_certificate /home/xxx/ssl/chained.pem;
ssl_certificate_key /home/xxx/ssl/domain.key;

ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY

到了这一步,nginx 重新 reload 应该可以来着,但是我用浏览器访问的时候遇到了这样的错误。可能和我使用了 1.9.7 版本的 Nginx 有关。

解决方案是需要加上 ssl_prefer_server_ciphersssl_ciphers 这样的东西。ssl_ciphers 有各种各样的版本,复杂的、简单的,复杂的一般都是为了保持尽可能多的兼容性的情况下的安全性。我使用的是 Mozilla SSL Configuration Generator 中的 Intermediate 的配置。也有诸如 CloudFlare 的简单配置。

隐去一些自己的信息的 Nginx 配置如下。开启 http2 需要较新版的 Nginx 1.9.x 才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server {
listen 443 http2;
ssl on;
server_name youdomain.com www.yourdomain.com;
ssl_certificate /home/xxx/ssl/chained.pem;
ssl_certificate_key /home/xxx/ssl/domain.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security max-age=2592000; # HSTS 特性自选
location /.well-known/acme-challenge/ {
alias /home/xxx/www/challenges/;
}
}
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://yourdomain.com$request_uri;
}

配置自动更新

Let’s Encrypt 签发的证书只有 90 天有效期,但可以通过脚本定期更新。例如我创建了一个 renew_cert.sh,内容如下:

1
2
3
4
5
6
7
#!/bin/bash
cd /home/xxx/ssl/
python acme_tiny.py --account-key account.key --csr domain.csr --acme-dir /home/xxx/www/challenges/ > signed.crt || exit
wget -O - https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem > intermediate.pem
cat signed.crt intermediate.pem > chained.pem
service nginx reload

这个脚本需要以 root 帐号运行,使用绝对路径比较保险。最后,修改 root 帐号的 crontab 配置,加入以下内容:

1
0 0 1 * * /home/xxx/root_shell/renew_cert.sh >/dev/null 2>&1

这样以后证书每个月都会自动更新,一劳永逸。

DNS

本文属于 DNS 相关的进阶文章,需要读者对 DNS 的基础有所了解。基础文章可以看看这篇

DNSSEC

DNSSEC(Domain Name System Security Extensions)是 DNS 协议的一个安全性拓展。其地位就相当于 HTTPS 于 HTTP。DNS 系统设计的时候,网络规模还很小,没有考虑到现在复杂的安全性问题。所以 DNS 的数据很容易被篡改。

中间人攻击

中间人攻击在中国是一种十分常见的 DNS 攻击技术。被某防火墙广泛应用。

Man-in-the-middle attacks

中间人攻击很容易理解,就是在用户的解析器和域名的解析服务器之间,有一个中间人,他会抢在解析服务器返回结果之前,抢先把一个错误的假结果伪装成原 DNS 请求的返回结果,然后由于 DNS 协议的缺陷,解析器会认为收到的第一个结果就是真的,抛弃后续收到的返回。

DNS 投毒

DNS 投毒是促使 DNSSEC 标准尽快推进的一个动力。

DNS poisoning

由于 DNS 通常都是基于无连接的 UDP 协议的,并且由于上面说的,DNS 解析器会认为接收到的第一个结果就是真实的,而抛弃后续的结果。所以,DNS 投毒就是利用一大批的肉鸡,向解析器发送伪装的假结果,让解析器误把假结果当作是正确的返回。从而使得诱导用户访问到错误的地方。

Resource Records

Resource Records 也就是所谓的资源记录,用于表示 DNS 请求的哪种类型的资源。其中最常用的是以下几种:

记录类型 含义
A 主机的 IPv4 地址记录
AAAA 主机的 IPv6 地址记录
CNAME 一个域名的别名
MX 域名的邮件域记录
NS 域名的权威解析服务器的地址

DNSSEC 加密

DNSSEC 为了保证请求结果的正确性,是通过对请求的资源记录使用非对称加密算法进行加密,然后把公钥放到 DNS 的请求结果中,私钥由权威解析服务器自己保留。然后解析器在拿到结果的时候,使用拿到的公钥,解密出 DNS 请求,来确认结果的正确性。为了实现这样的目的,DNSSEC 增加了几种资源记录:

记录类型 含义
DNSKEY 非对称加密算法中的公钥
DS 权威服务器使用的私钥的 hash
RRSIG 使用私钥对资源记录加密过的数据
KSK 用于解开 DNSKEY 资源记录使用的公钥
ZSK 用于解开其他资源记录使用的公钥

信任链

那么 DNSSEC 具体是如何利用上面的几种资源记录工作的呢。

DNSSEC

这是一个工作的信任链。假如我需要请求 example.com 的 A 记录,也就是想拿到它的 IPv4 地址。那么在 DNSSEC 体系中,我肯定需要拿到这个 A 记录的 RRSIG,也就是加密过的数据用于比较。为了解开这个加密过的数据,我肯定是得拿公钥。而在 DNSSEC 中,除了DNSKEY 公钥本身,其他资源记录都是使用 ZSK 进行加密的。所以这里我们需要拿到 ZSK 记录。同样的道理啊,ZSK 也是一种资源记录,和前面的 A 记录一样,为了保证我拿到的 ZSK 也是可信的,没有被篡改过的,我也得拿 DNSKEY ZSK 记录的 RRSIG,也就是加密数据。为了解开这个加密数据,我又得拿非对称加密算法中的公钥,这里我们为了解开 KEY,所以我们需要拿的就是 KSK 了。只要拿到这么些个数据,我们就可以解开所有被非对称加密算法加密过的数据,然后确保所有的数据的正确性了。

但是,这个 KSK 记录如何保证它的真实性呢?这里我们就不能继续在 example.com 的权威服务器里解决了,这里无法构成信任链,成了一个死循环。所以我们要请求 example.com 的上一层权威服务器,也就是 .com 的权威服务器,请求 example.com 的 KSK 的 hash 值,也就是上面提到的 DS 资源记录。通过比较这个 hash 值,我们就可以确定这个 KSK 的正确性。

当然,为了能够请求到正确的,未被篡改的 DS 记录,我们又得重复上述的过程,请求 DS 记录的 RRSIG 加密数据。 这样不断的重复上述过程,最终我们会如上图一样,一路走到根服务器,请求根服务器的 KSK。这样,信任链就走到根服务器的 KSK 记录了。而,全球十几个根服务器的 KSK 记录,是公开的,所有人都知道的,无法被篡改的,可信的。就这样,我们的信任链就建立起来了。

Anycast

Anycast 中文叫任播。其特性就是分布在全球的不同的服务器,使用同一个 IP 地址,然后通过动态路由协议,使得用户的请求会被路由到距离他们最近的一个服务器上去。通常用于实现 Public DNS 和 CDN。

Public DNS 就是我们经常见到的例如 114.114.114.114,或者 8.8.8.8 之类的 DNS 解析服务器。特点就是在全球各地,ping 这些服务器的延迟都非常低,都在几毫秒到三十几毫秒之间。

Anycast 就这么简单,全球各地部署多个机房,每个对外服务的机器都是用同一个 IP 地址,然后通过动态路由协议使得用户的请求可以被送到距离他们最近的服务器上去。

BGP

到这里我们来讲讲上面 Anycast 中,是如何实现动态路由,使得用户的请求可以被路由到最近的服务器上去的。

自治系统

自治系统(AS)是指在一个实体管辖下的所有网络。通常比如中国电信是一个自治系统,中国联通是一个自治系统,阿里云的所有服务器是一个自治系统。

边界网关协议

BGP 就是边界网关协议,用于在自治系统间进行路由的协议。在自治系统内部,通常使用 RIP 和 OSPF 进行动态路由。RIP 由于历史原因,已经基本被 OSPF 替代。

现实的互联网,其实是两层网络组成的。我们平常接触到的网络是属于一个自治系统,是一个网状的结构。然后我们上一层,也就是在自治系统外,又有一个网络,也就是所谓的骨干网。

BGP 面对的就是一个个的自治系统之间的路由问题。BGP 是一种基于 TCP 协议的动态路由协议,他通过启动时和周围的其他 BGP 邻居建立 TCP 连接的方式交换路由数据。

BGP 的路由策略有 13 条之多,所以十分复杂。其中最容易理解,最简单的就是 AS_PATH 的概念。AS_PATH 就是自治系统之间的路径。通常而言,路径越短的,肯定越优先选择。

所以我们来谈谈,Anycast 是如何利用 BGP 实现动态路由到最近的服务器这件事情的。

OpenDNS

一个 Anycast 系统,通常都会申请一个 ASN,也就是一个自治系统编号,也就是自己就是一个自治系统。然后在全球的各个服务器上,运行 BGP 程序。比如上图的 Miami 的服务器,他会和自己附近的自治系统建立 BGP 邻居关系,宣告自己负责的 IP 网段。更新附近 AS 的路由信息。由于 Miami 的服务器,在地理位置上距离最下面的 AS64496 最近,所以他们之间建立了一个直连的 BGP 邻居关系。这样 Miami 的机器和 AS64496 就只有一跳的距离。所以 AS64496 会把请求路由到最近的 Miami 的服务器上去。

参考资料

DNSSEC: An Introduction

Understanding DNSSEC

BIND DNSSEC Guide

List of DNS record types

Resource Record Types

A Brief Primer on Anycast

Anycast DNS - Part 1, Overview

Best Practices in
IPv4 Anycast Routing

How OpenDNS achieves high availability with Anycast routing

http://www.ccietea.com/

吃饱了撑得,想重新买个路由器。个人其实需求不大,支持 2.4/5 GHz,802.11ac,千兆之类的就行了。本来想买个好看的,最喜欢 Google 的 Onhub,但是在国内用,实在是有点蛋疼。其他也没找到颜值能和 Onhub 一拼的。只能退而求其次了。刚好看到京东上,现在华硕 AC66U 只要 599,比上半年降了非常多,于是就入手了。

开箱

就不贴什么开箱照了,没什么意思。网上搜搜一大片。

翻墙

入手之后,就刷了网上大神集成 Shadowsocks 的梅林固件。详情可见这里

要配置 Shadowsocks 非常简单,先在系统设置里开启 JFFS2。然后在 Tools 里面,选用智能模式,下面填上 Shadowsocks 的帐号就行。由于固件的问题,shadowsocks 的服务器地址不能使用域名,只能填 IP。应用一下设置,之后就可以愉快的翻墙了。

简单的做一些说明。代理模式就是在服务器端启动 socks v5 的代理和 HTTP 代理。需要客户端把自己的代理指向这里。HTTP 代理主要的用途是给家里面的 PS4 之类的设备翻墙使用。智能模式就是国外的地址一律走翻墙线路。白名单模式就是指定域名走翻墙线路。

由于我的 shadowsocks 线路是香港的,网络比较好,所以我用了智能模式。感觉很不错。但是有个问题,我的 telegram 客户端没有走翻墙线路。因为 Telegram 是直接使用 IP 直连的,没有经过域名的国内外判断。所以我还得手工解决这个问题。

先开启路由器的 SSH 之后,查看一下 iptables。

1
2
3
4
5
6
7
8
9
[email protected]3280:/tmp/home/root# iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
VSERVER all -- anywhere 192.168.1.5
REDIRECT tcp -- anywhere anywhere set goss dst redir ports 3333
REDIRECT tcp -- anywhere 91.108.56.0/22 redir ports 3333
REDIRECT tcp -- anywhere 91.108.4.0/22 redir ports 3333
REDIRECT tcp -- anywhere 109.239.140.0/24 redir ports 3333
REDIRECT tcp -- anywhere 149.154.160.0/20 redir ports 3333

其中PREROUTING链的第二条,就是指名 goss 这个 ipset 里面的全部走本地的ss-redir线路。这里为了让 Telegram 翻墙,我们手工加了后面四条规则。

1
2
3
4
iptables -t nat -A PREROUTING -p tcp -d 91.108.56.0/22 -j REDIRECT --to-ports 3333
iptables -t nat -A PREROUTING -p tcp -d 91.108.4.0/22 -j REDIRECT --to-ports 3333
iptables -t nat -A PREROUTING -p tcp -d 109.239.140.0/24 -j REDIRECT --to-ports 3333
iptables -t nat -A PREROUTING -p tcp -d 149.154.160.0/20 -j REDIRECT --to-ports 3333

但是手工加的规则,路由器一旦重启就会没了,所以我们写个脚本,让启动自动执行。根据梅林固件的官方说明,只有在/jffs/scripts/下面的,指定的几个名字的脚本会被执行。不同名字的脚本会在不同的时候被执行。这里我们需要加的是 NAT 规则,所以建立一个脚本nat-start,把命令放进去。

1
2
3
4
5
#!/bin/sh
iptables -t nat -A PREROUTING -p tcp -d 91.108.56.0/22 -j REDIRECT --to-ports 3333
iptables -t nat -A PREROUTING -p tcp -d 91.108.4.0/22 -j REDIRECT --to-ports 3333
iptables -t nat -A PREROUTING -p tcp -d 109.239.140.0/24 -j REDIRECT --to-ports 3333
iptables -t nat -A PREROUTING -p tcp -d 149.154.160.0/20 -j REDIRECT --to-ports 3333

别忘了chmod a+rx /jffs/scripts/*,设置一下脚本可执行的权限。

你应该选用一套管理代码格式的简单规则,然后贯彻这些规则。如果在团队工作,则团队应该一致同意采用一套简单的格式规则,所有成员都要遵从。

格式的目的

可读性比可用性更重要。

垂直格式

短文件通常比长文件易于理解。

文件名应当简单且一目了然。

垂直方向上,每组代码行组成的思路,用空白行间隔开。

紧密相关的代码在垂直方向上也应该靠近。

自上至下展示函数的调用顺序。

横向格式

横向80字符较佳,不反对到达100字符或者120字符。不要再多。

注意缩进。

团队规则

严格遵循团队制定的规则。

注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。

注释不能美化糟糕的代码

带有少量注释的简洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样的多。与其花时间编写解释你搞出来的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。

用代码来阐述

好注释

法律信息。如版权及著作权声明。

提供信息的注释。更好的方式是尽量利用函数名称传达信息。

警示信息。

TODO 注释。

公共 API 中的 Javadoc。

坏注释

喃喃自语。

多余的注释。相对于代码,并没有提供更多的信息。

误导性注释。

循规式注释。不是每个函数、每个变量都要有注释。

日志式注释。在有了版本控制系统的情况下,这种注释就是废话。

废话注释。

能用函数或变量时就不要用注释。

归属与署名是不必要的。版本控制系统全部记着呢。

注释掉的代码。千万不要这么干!

HTML 注释。不要这么写。不够易读。

信息过多。不要说多余的东西。

为只做一件事的短函数选个好名字,通常比函数头写注释要好。