header image

Ngxin HTTPS代理Transmission Web UI

在上一篇文章中,我设置了用ngrok转发端口来实现对内网NAS的访问,但是如果转发用的是国内的VPS,会由于域名没有备案而被关停。解决方法一个是搞个备案过的域名。现在阿里云等云服务都有便宜的域名,所以这不是大问题。还有个方法就是绕开HTTP访问,比如我这里用的HTTPS。

方法简述

就是在NAS上建立一个Nginx服务,然后启用HTTPS(这部分就不详细介绍了,网上文章很多,我配置的是8443端口以免常用端口被检测),然后通过代理将访问路径/transmission/的请求转发到NAS的9091端口上。

初步方案

一开始就直接做了个转发,在Nginx的网站配置如下:

    location /transmission {
        proxy_pass: http://127.0.0.1:9091;
    }

但是这样却出现了问题,由于缺少Transmission的一个特有的请求头会造成409错误,而且不知为啥有静态文件无法获取。

改进方案

有这种需求的肯定不止我一个,所以马上就放狗搜了一下,还真找到了一篇文章,把上面的配置改成如下:

    location ^~ /transmission {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_pass_header X-Transmission-Session-Id;
        add_header   Front-End-Https   on;

        location /transmission/rpc {
            proxy_pass http://127.0.0.1:9091;
        }

        location /transmission/web/ {
            proxy_pass http://127.0.0.1:9091;
        }

        location /transmission/upload {
            proxy_pass http://127.0.0.1:9091;
        }

        location /transmission/web/style/ {
            alias /usr/share/transmission/web/style/;
        }

        location /transmission/web/javascript/ {
            alias /usr/share/transmission/web/javascript/;
        }

        location /transmission/web/images/ {
            alias /usr/share/transmission/web/images/;
        }

        location /transmission/ {
            return 301 https://$server_name/transmission/web;
        }
}

这样就行了,今后只要访问VPS的/transmission/(如http://example.com:8443/transmission)就和直接访问Transmission的9091端口一样了。

更进一步

但是这样还是能直接访问8443端口,能不能让这个端口不能直接访问呢,答案是可以。

我们需要用到cookie,cookie是什么就不多解释了。我们只要在Nginx的网站配置中最初的location之前添加如下内容就可以了。

    if ($http_cookie !~ "key=abcdef") {
        reutrn 444;
    }

上面配置的意思是,访问网站时的请求要包含一个名称是key、值是abcdef的cookie(这两个参数可以自己设定),否则就抛弃请求。444是一个特殊的错误码,Nginx遇到这个错误码会直接抛弃请求(就好像没有服务似的),而不会真正地返回错误。

这样,我们只要用一些特殊手段(Firefox或者Chrome的开发者工具或者一些附加组件/扩展程序)给网站添加一个名称是key、值是abcdef的cookie,就能访问了,而没有这个cookie时访问就好像服务不存在一样,彻底断绝检测到未备案网站的可能性。

内网穿透之ngrok [2017-10-13 UPDATED]

2017年10月13日更新:转发其他IP的端口

鉴于IPv4地址资源的枯竭,而电信公司又不部署IPv6,越来越多的宽带用户难以获得公网IP,导致NAS、私有云等应用难以实现,所以我们需要一种有效的能穿透NAT的工具。

省事的方法

目前有很多提供NAT穿透服务的网站,比如老牌的花生壳,以及NAT123等等。但缺点显而易见——免费的服务限制太多(限端口、限带宽),收费的又太贵。

还有一些基于这次提到ngrok提供服务的,虽然收费便宜点,但还是不能随心所欲。

ngrok介绍

ngrok是国外一个开发者基于GO语言写的服务,并且通过ngrok.com提供服务,不过1.0版本的软件是开源的(虽然停止了更新)。上面说到的基于ngrok和我们这次要架设的服务,都是基于1.0版本的。

准备工作

首先你要有自己的服务器(运行Linux的VPS),用来转发请求,然后你需要穿透NAT的设备要能够运行第三方的程序。最后你需要一台Linux的电脑(当然你在服务器上执行操作也可以,Windows 10的WSL也是不错的选择)。下面的代码会说明是在服务器本地机器还是需要穿透NAT的设备上执行。

所有的Linux操作都是基于Ubuntu或Debian,其他系统的操作大同小异,如果不会请留言。

你需要给你的服务器绑定一个域名,对于大多数人这不是问题了。如果你要用到子域名的话,再把子域名的泛解析绑定。

比如绑定了example.com的域名,如果要用子域名的话就要添加*的泛解析,DNS服务商都会有说明的,请详细阅读。

获取源代码

我们首先要在本地机器上部署编译环境:

sudo apt-get install build-essential golang mercurial git

然后获取源代码:

git clone https://github.com/inconshreveable/ngrok.git

这样会把源代码克隆岛ngrok目录下,我们切换一下目录:

cd ngrok

创建证书

因为ngrok的隧道是通过TLS传输数据的,我们可以用默认的证书,但这样谁都可以连接你的服务器了,所以我们要自己创建证书。

以下代码在本机一条一条地执行。第一条中的example.com记得改成你自己的域名。

NGROK_DOMAIN="example.com"
openssl genrsa -out base.key 2048
openssl req -new -x509 -nodes -key base.key -days 10000 -subj "/CN=$NGROK_DOMAIN" -out base.pem
openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=$NGROK_DOMAIN" -out server.csr
openssl x509 -req -in server.csr -CA base.pem -CAkey base.key -CAcreateserial -days 10000 -out server.crt

我们需要的是server.keyserver.crtbase.pem三个文件。我们要放到对应的目录里:

cp server.key assets/server/tls/snakeoil.key
cp server.crt assets/server/tls/snakeoil.crt
cp base.pem assets/client/tls/ngrokroot.crt

修改代码

因为ngrok会默认监听IPv6端口,考虑到国内的网络环境,我们需要修改一下代码,否则将无法使用。主要修改两个地方,一个是src/ngrok/server/tunnel.go的109行net.ListenTCP后面的"tcp"改成"tcp4",另一处是src/ngrok/conn/conn.go的57行的net.Listen后的"tcp"同样改成"tcp4"

编译

编译很简单,我们首先通过GOOSGOARCH两个变量指定编译的目标系统和目标架构,目标系统有darwin(即OS X)、freebsdlinux,目标架构有386(32位系统)、amd64(64位系统)和arm(ARM嵌入式系统)。然后再通过serverclientrelease-serverrelease-client来指定编译的是客户端还是服务器端。有没有release的区别是,包含release的编译结果会把assets目录下的内容包括进去,从而可以独立执行。如果你今后还要更换证书,建议编译不包含release的版本。

以下代码都在本机执行。

编译服务器:

GOOS=linux GOARCH=amd64 make release-server

得到的服务器文件是bin/ngrokd

编译客户端,因为我的NAS是ARM架构的所以如下,各位请按自己的需求编译:

GOOS=linux GOARCH=arm make release-client

得到的客户端文件是bin/linux_arm/ngrok

不同系统或架构编译得到的文件位置可能不同,但都在bin目录下,并且文件名服务器是ngrokd客户端是ngrok,下面请根据实际情况操作。

配置服务器

首先把文件复制到服务器上。请选择你熟悉的手段。我是直接scp上传:

scp bin/ngrokd example.com:~

然后就要到服务器上操作了,通过SSH连接到服务器上。

首先把文件放到/opt/ngrokd目录下(这步可根据你的习惯来):

sudo mkdir /opt/ngrokd
sudo cp ngrokd /opt/ngrokd/

然后我们就要建立服务了。我使用的是Ubuntu 16.04 LTS,所以用systemd很简单,把如下内容保存到/lib/systemd/system/ngrokd.service这个文件(自己创建):

[Unit]
Description=ngrok server
After=network.target

[Service]
Type=simple
ExecStart=/opt/ngrokd/ngrokd -domain example.com -httpAddr "" -httpsAddr "" -tunnelAddr ":4443" -log "/var/log/ngrokd.log"
Restart=on-failure

[Install]
WantedBy=multi-user.target

其中ExecStart这一项后面就是我们ngrokd执行命令了。参数-domain表示服务器域名,请改成你自己的域名-httpAddr表示默认监听的HTTP端口,-httpsAddr表示默认监听的HTTPS端口,因为我用不到所以都设置成空字符串""来关闭监听,如果需要打开的话记得格式是:12345(冒号+端口号)这样的;-tunnelAddr表示服务器监听客户端连接的隧道端口号,格式和前面一样;-log表示日志文件位置;还有个-log-level用来控制日志记录的事件级别,选项有DEBUGINFOWARNINGERROR

如果编译的是不带release的版本,还可以通过-tlsCrt-tlsKey选项来指定证书文件的位置。

设置好之后,我们就要启用服务了,很简单:

sudo systemctl enable ngrokd

其中enable是命令表示启用服务(开机会自动启动),其他命令还有disable(禁用服务)、start(启动服务)、restart(重启服务)、stop(停止服务)、status(查看服务状态)等。所以我们就启动服务:

sudo systemctl start ngrokd

然后通过status查看服务,如果是active (runing)那么就OK了。

配置设备

先把从本机把客户端复制到我们的设备(这里我的是NAS)上。请选择你熟悉的手段。我还是一样使用scp

scp bin/linux_arm/ngrok 192.168.11.50:~

接下来我们就要到设备上操作了,同样通过SSH连接到设备上。

首先还是一样的把程序复制到/opt/ngrok目录下(这步同样可根据你的习惯来):

sudo mkdir /opt/ngrok
sudo cp ngrok /opt/ngrok/

然后还是一样的建立服务,因为我的NAS运行的是Debian,还是用systemd,操作和服务器上几乎一样。把如下内容保存到/lib/systemd/system/ngrok.service这个文件:

[Unit]
Description=ngrok client
After=network.target

[Service]
Type=simple
ExecStart=/opt/ngrok/ngrok -config "/opt/ngrok/ngrok.yml" -log "/var/log/ngrok.log" start transmission ssh
Restart=on-failure

[Install]
WantedBy=multi-user.target

同样还是看ExecStart后面的命令,这里加载了一个配置文件/opt/ngrok/ngrok.yml(之后我们会建立),-log记录日志和服务器端是类似的,最后用start transmission ssh启动了配置文件中的transmissonssh两个隧道。

那么配置文件是什么样呢?我们把如下内容保存到/opt/ngrok/ngrok.yml中:

server_addr: example.com:4443
trust_host_root_certs: false
tunnels:
  transmission:
    remote_port: 9091
    proto:
      tcp: 9091
  ssh:
    remote_port: 23333
    proto:
      tcp: 22

server_addr就是我们在服务器配置的域名和端口。

tunnels下就是隧道设置。

每一级前面多两个空格缩进的表示是上一级的子项。比如transmissionssh就是tunnels的子项,remote_portproto又是他们的子项,以此类推。

transmissionssh就是隧道的名称,每个对应一个隧道。上面的启动命令中start后面的就是这个。

remote_port表示远程对应的端口。

proto表示协议,支持的有tcphttphttps。因为我没有开启服务器上的http和https,所以我都是用的tcp(TCP也可以支持HTTP或HTTPS,之所以有独立的http和https选项是为了简化设置,直接对应服务器上的相应设置)。

tcp就是协议,后面跟着的是端口号。

所以上面的设置文件就表示,将服务器的9091端口的连接转发到设备的9091端口上,服务器的23333端口的连接转发到设备的22端口上。这样我们就可以通过访问服务器的相应端口来访问设备上的服务了。

2017年10月13日更新

还可以转发其他IP的端口,方法就是在proto下的tcp(或httphttps)后的端口号写成IP地址:端口号的格式(中间是英文冒号)。

如:

      tcp: 192.168.11.1:80

最后别忘了启用服务:

sudo systemctl enable ngrok
sudo systemctl start ngrok

最后

这样,我们就将设备上的9091端口和22端口的对应到了服务器的9091和23333端口上了。比如我要访问设备的SSH,就可以这样:

ssh -p23333 example.com

这样有个缺点,就是只能建立到运行ngrok的设备的隧道,如果你的设备不能运行ngrok,那就有点麻烦了。 2017年10月13日更新:其实是可以转发其他IP的端口的,详见上面的配置设备一节。

如果不能运行ngrok的设备提供的是HTTP或者HTTPS访问,还有一种变通的方法是在运行ngrok的设备(比如树莓派)上通过nginx的建立代理,代理到不能运行ngrok的设备上。

虽然有局限,但我们已经成功地穿透内网了。只希望IPv6快点到来,让我们不必再处理这些麻烦事。

删除Firefox过期的HPKP信息

前不久手贱,试了试HPKP,结果不知为突然出问题了。然后Firefox就彻底拒绝了我的网站。

HPKP,即The Public Key Pinning Extension for HTTP,是将证书的发行者的验证信息添加到一个HTTP头信息。用户代理在访问时,不光验证证书的有效性,还验证证书的发行者,从而避免中间人攻击。

出问题的原因很简单,证书的发行者不知为啥和原来有所不同(我用的是上一篇介绍的Let’s Encrypt,证书是自动颁发的),而HPKP又没有修改,所以Firefox就拒绝了。

但是不想折腾了,于是就删除了HPKP头信息,结果由于之前设置的过期时间太长,造成Firefox还是拒绝。很明显是Firefox缓存了相关信息。放狗找了一下,找到了一篇文章(见引用来源)。

只要把Firefox用户档案下面的SiteSecurityServiceState.txt删除就行了。

Linux是在用户的家目录下面,也就是~/.mozilla/firefox/xxxxxxxx.default/SiteSecurityServiceState.txt,其中xxxxxxxx是一串随机字符。

而Windows就在%APPDATA%\Mozilla\Firefox\Profiles\xxxxxxxx.default\SiteSecurityServiceState.txt%APPDATA%是一个系统变量,代表C:\Users\[username]\AppData\Roaming这个目录([username]是你的用户名)。

删除之前关闭Firefox,删除后再打开就正常了。

在Ubuntu上获取Let’s Encrypt免费证书 [2019-07-05 UPDATED]

2017年02月20日更新:第4节
2019年07月05日更新:第2、4节

Let’s Encrypt是电子前哨基金会(EFF)发布的免费SSL证书服务,受到微软、谷歌等大佬的支持,但是配置比较复杂(主要是证书有效事件较短,需要重新签发,虽然签发的过程都是在自己的机器上通过命令行实现),之前研究过一下,最终还是放弃了。

但最最近由消息称沃通的CA将有可能被Mozilla拒绝,所以还是重新弄起来吧。下面说一下步骤。

安装Certbot

Certbot是EFF开发的一个简单的工具,比起我之前研究的时候弄的ACME要简单得多,访问https://certbot.eff.org/ 之后,选择你的服务器(webserver)和操作系统(Operating System),就会给出简单的步骤了,我们还是一步一步来吧。

我的服务器是Nginx,操作系统是Ubuntu 16.04。由于自带了软件包,安装只需要一行命令即可:

sudo apt-get install letsencrypt

如果是其他版本的Ubuntu,只需要下载一个脚本就行了(下面的命令在需要下载到的目录里执行):

wget https://dl.eff.org/certbot-auto
chmod a+x certbot-auto
./certbot-auto

签发证书

很简单,直接运行letsencrypt命令即可(使用其他版本的Ubuntu的把letsencrypt换成上面下载的certbot-auto,注意执行脚本需要完整路径/path/to/certbot-auto,在脚本所在目录下是就是./certbot-auto)。

sudo letsencrypt certonly --webroot -w /var/www -d example.com -d www.example.com -w /var/www/sub -d sub.example.com

第一次运行时会问你一些你的信息,以后就不会再询问了。

解释一下,certonly是它的子命令,表示只颁发证书。--webroot是它的插件,用于自动验证域名。-w是指定网站的目录,-d是指定目录对应的域名。一个-w后可以跟着多个-d就对应多个域名。可以用多个-w设置多个目录对应不同的域名。这些域名都在一个证书里。

2019年07月05日更新

可以用-m选项来设置Email地址,这样就会收到Let’s Encrypt的证书失效提醒。可以用--cert-name来设置别名,这个选项影响/etc/letsencrypt/live/下的目录名称,如果设置了的话请注意下面的配置对应修改。

需要注意的是,域名下的\.well-know\acme-challenge\目录下的文件必须要可以访问。这个是Let’s Encrypt用于验证域名所有权的。它会在上面-w参数设置的目录下建立临时文件,然后通过HTTP访问,比如上面的设置就会新建诸如/var/www/.well-known/acme-chanllenge/xxxxxxx(xxxxxxx是随机字符),然后通过http://example.com/.well-known/acme-chanllenge/xxxxxxx来验证。如果你的网站是纯HTTPS的,你可以用把这个地址重定向到https://example.com/.well-known/acme-chanllenge/xxxxxxx,可以成功通过验证。

看到Congratulations!就代表成功了,下面我们就可以配置Nginx了。

配置HTTP服务器

证书所在的地方是/etc/letsencrypt/下,archive里面是所有证书的存档,keys里面是所有证书,不过我们不用管,我们需要的证书在live下面,对应网站域名的目录下面就是了。一共有四个文件。

  • privkey.pem 这是私匙,对应Nginx的ssl_certificate_key选项,或者Apache2的SSLCertificateKeyFile选项。

  • cert.pem 服务器证书,这个只有Apache2低于2.4.8版本需要,对应SSLCertificateFile选项。

  • chain.pem 除服务器证书之外的所有证书,对于1.3.7版以上的Nginx对应ssl_trusted_certificate选项,对于低于2.4.8的Apache2对应SSLCertificateChainFile选项。

  • fullchain.pem 包括上面的服务器证书和其他证书,Nginx对应ssl_certificate选项,2.4.8版以上的Apache2对应SSLCertificateFile

所以对于我用的Nginx来说,只需要privkey.pemfullchain.pem这两个就够了。Apache2的话参考设置HTTPS的文章,对应设置上面提到的文件和选项就行了。

在Nginx的server下设置如下响应的ssl选项就行了,如:

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    root /var/www;
    index index.html;

    ssl on;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        try_files $uri $uri/ =404;
    }
}

如果你是第一次设置HTTPS,记得在listen选项那里要监听443端口,并在后面加上ssl。如果要启用HTTP/2,请参考这篇文章

最好在原来的80端口上设置好跳转:

server {
    listen 80;
    server_name example.com www.example.com;
    rewrite ^(.*)$ https://example.com$1 permanent;
}

设置好了别忘了重启Nginx。

自动更新证书

2019年07月05日更新

如果使用新版Ubuntu(18.04),官方源的letsencrypt软件包已经包含了更新脚本,所以不用做任何操作,其会自动更新。

更新证书很简单,直接如下命令就可以了:

sudo letsencrypt renew

可以用--dry-run选项来模拟更新证书,看看会不会出错。由于Ubuntu自带的版本有点旧,所以会出错……(certbot-auto的是最新的所以不会有问题)。这时候就需要加上--force-renew选项来强制更新。第一次运行可能还要你同意服务条款,所以根据提示加上--agree-tos(以后就不需要了)。

在正式更新的时候,要使证书生效还需要重启Nginx。如果是certbot-auto我们可以用--pre-hook--post-hook来设置更新前后的命令,用来停止和启动Nginx。

sudo /path/to/certbot-auto renew --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"

但是Ubuntu自带的版本有点旧,所以就只好自己写脚本了。

#!/bin/sh
systemctl stop nginx
letsencrypt renew --force-renew
systemctl start nginx

2017年2月20日更新

上述命令中不能使用systemctl stop nginx来停止nginx,因为更新证书时同样需要验证,如果停止了服务就无法验证了。我们只需要在更新后重启nginx就行了。所以:

如果使用新版certbot-auto我们只需要:

sudo /path/to/certbot-auto renew --post-hook "systemctl restart nginx"

自己编写脚本的话改成这样:

#!/bin/sh
letsencrypt renew
systemctl restart nginx

我们还可以在这个脚本中将输出重定向来生成日志文件,以便以后检查。

保存文件,这里我们保存到/opt/scripts/letsencrypt_renew

我们可以用crond或者systemd来自动更新证书,这里就简单点,用crond吧。用sudo crontab -e命令编辑root用户的crontab。在最后面新插入行并添加如下内容:

0 */12 * * * sh /opt/scripts/letsencrypt_renew

上面的意思是,每12小时的第0分执行脚本一次。因为Let’s Encrypt的推荐每12小时更新一次。更新操作并不一定会真的更新证书,只有在快到期的时候才会更新。

如果不想修改root的crontab,可以在/etc/cron.d下新建文件写入如下内容:

0 */12 * * * root sh /opt/scripts/letsencrypt_renew

比起crontab,就是在要执行的命令前面多了一个表示用户的root

更新证书是检测/etc/letsencrypt/renewal下的配置文件进行的,所以即使不管颁发多少个证书,上面的更新脚本都不用修改。

最后

如果要删除证书的话,把/etc/letsencrypt/下的archiveliverenewal中对应域名的文件或目录删除就可以了。

此外,还可以用-m选项来设置Email地址。还有更多的功能就去看看官方的文档吧:https://certbot.eff.org/docs/

Nginx启用HTTP/2

本着更快、更快、更快精神搞出来的HTTP/2,不用还是人?下面就来说一下吧。

准备工作

首先你需要Nginx 1.9.5以上版本。如果是Ubuntu Server 16.04 LTS的话,自带的是1.10的版本所以没有问题。其他系统请自行解决。

其次你需要启用ngx_http_v2_module模块。如果是Ubuntu Server 16.04 LTS的话就需要安装nginx-corenginx-fullnginx-extras,反正nginx-light是没有的。如果是自行编译需要在设置时加上--with-http_v2_module选项。

再次需要配合OpenSSL 1.0.2以上版本使用,如果是Ubuntu Server 16.04 LTS的话,自带的是1.0.2g的版本所以没有问题。其他系统请自行解决。

最后,你需要在Nginx上启用HTTPS,这涉及到SSL证书的申请、配置,又是一篇文章的内容了,以后有机会再说,这里请自行解决吧。

配置

禁用旧版加密

如果不禁用的话,会造成虽然启用了但是没法正常访问的问题。

Ubuntu上,只要修改/etc/nginx/nginx.conf文件,在http一节里找到或新加入这样一行(如果是软件包自带的配置文件的话,可以加在ssl_prefer_server_ciphers一行后面):

ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;

配置HTTP/2

之后我们只要修改站点的配置文件,在listen监听的HTTPS端口443后面加上ssl http2就行了。监听80端口的并不能启用HTTP/2,请放弃。

比如:

server {
    listen 443 default_server ssl http2;
    listen [::]:443 default_server ssl http2;

    server_name example.com;

    ssl on;
    ssl_certificate /path/to/example.com.crt;
    ssl_certificate_key /path/to/example.com.key;

    root /var/www;

    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;             
    }
}

重启Nginx

Ubuntu只要如下命令就行了:

sudo systemctl restart nginx

最后

如果使用Firefox,可以安装一个叫做HTTP/2 and SPDY indicator的扩展,安装之后会在地址栏右侧显示一个闪电样的小图标,如果网站是HTTP/2就会显示蓝色,如果是SPDY(HTTP/2的前身)就会显示绿色,如果没有则显示灰色。

也可以打开开发者工具里的网络,可以看到“版本”一项显示“HTTP/2.0”。

HTTP/2好处都有啥,可以参考维基百科HTTP/2条目英文的更详细)。