记录一下局域网网站穿透到外网的反向代理配置

这两天把学校局域网里的自建网盘服务器做了内网穿透, 并且上了 https, 实现过程比较折腾, 记录起来免得以后忘掉


发现直接按思考过程记下来, 以后再看的时候会好理解得多

目录

内网穿透

因为服务器所在的网络环境没有外网IP, 所以使用了 frp 进行了内网穿透. 简单来说就是内网主动与外网的一个服务器建立起连接并保的持不断, 这样内网服务器就可以随时接到来外网服务器转发过来的请求了. frp 的具体使用就不在这里介绍了, github 上解释得很详细, 实际上配置起来也很简单.

反向代理

有反向代理就有正向代理, 正向代理就是我们平时常用的, 代理过程如下:

  • 客户端向代理服务器发送请求, 请求目标信息为目的服务器信息
  • 代理服务器向目的服务器发送请求, 请求源信息为代理服务器自身的信息
  • 目的服务器向代理服务器返回请求资源
  • 代理服务器向客户端返回请求资源

正向代理过程中, 客户端是知道目的服务器存在的, 而目的服务器不知道客户端的存在, 因为代理服务器向目的服务器发送请求使用的代理服务器自己的信息进行请求, 没有使用客户端的信息. 正向代理通常用在客户端无法直接连接到目的服务器, 或者想要向目的服务器隐藏客户端的信息.

反向代理过程如下:

  • 客户端向代理服务器发送请求, 请求目标信息为代理服务器信息
  • 代理服务器向目的服务器发送请求, 请求源信息为客户端信息
  • 目的服务器向代理服务器返回请求资源
  • 代理服务器向客户端返回请求资源

可以看出, 反向代理与正向代理相反的地方是: 正向代理中目的服务器不知道客户端信息, 反向代理中客户端不知道服务器信息. 反向代理多用于服务器端的负载均衡, 或者想要对客户端隐藏服务器信息. 因为这个特点, 我们平时也不知道自己是不是遇到了反向代理.

看一下需求

上面的算是基础知识, 现在思考一下我们需要实现的目标:

  • 使用同一个域名进行访问

为了保证客户端不需要额外进行配置, 使用同一个域名是必须的.

  • 内外网流量进行分流

因为是网盘服务器, 为了保证传输速度, 内网用户要使用内网直接与服务器进行数据传输

  • 所有流量都使用 https 传输

本身在内网开洞就是不安全的行为, 所以传输过程中要保证数据的安全性

先让它能跑

本着标题的原则, 也就是先不管内网分流和 https, 只把内网穿透做好. 下面假设内网服务器 ip 为 192.168.1.10, 外网服务器域名为 shino.space(不用试了, 实际配置中我根本没用这个域名)

穿透设置

内网的服务开在 80 端口, 通过穿透软件映射在外网服务器的一个端口即可, 因为外网服务器可能开了其他的网页服务, 所以不能直接映射在80端口, 需要映射在其他端口, 再进行反向代理. 那么我将内网的 80 端口映射在外网的 8010 端口上:

 1
192.179.1.10:80 -> shino.space:8010

外网服务器进行一下反向代理, 将域名定为 pan.shino.space:

 1
 2
 3
 4
 5
 6
 7
 8
server {
    listen         80;
    server_name    pan.shino.space;

    location / {
        proxy_pass http://127.0.0.1:8010;
    }
}

访问过程是这样的:

 1
 2
client -> pan.shino.space:80 -> 127.0.0.1:8010 -> 192.168.1.10:80 
              (反向代理至外网服务器其他端口)  (映射到本地服务器端口)

域名 & 分流设置

现在我们访问 http://shino.space:8110 和 内网访问 http://192.168.1.10 效果是一样的, 不过为了让内网外网使用同一个域名进行访问, 要设置一下跳转. 为了区分内网网请求, 假设内网的出口外网 ip 为 202.118.1.10, 并将统一的域名定为 pan.shino.space, 外网的 nginx 应该这样配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
server {
    listen          80;
    server_name:    pan.shino.space;

    location / {
        if ($remote_addr ~ "202.118.1.10") {
            return 307 http://192.168.1.10;
        }
        proxy_pass: http://127.0.0.1:8010
    }
}

在浏览器中输入 pan.shino.space 就可以区分内网还是外网访问, 进行跳转或者反向代理. 注意内网用户是跳转到内网 ip 连接, 此时浏览器显示的 url 也变为了 http://192.168.1.10, 保证了内网不走反向代理, 加快上传下载速度. 使用 307 跳转而不是 302 是为让浏览器正确处理 POST 方法的跳转. 这样, 用户访问过程变为如下:

 1
 2
 3
 4
client -> pan.warshiprpg.xyz:80 -> 判断是否为内网 -> 192.168.1.10:80
                                                | (在内网就跳转到本地直接 ip 访问)
                                                -> 127.0.0.1:8010 -> 192.168.1.10:80
                                                  (在外网则进行反向代理)

这就能访问了?

跨域请求

看上去这样就可以正常访问了, 确实, 如果只是静态网页访问起来是没问题的, 但是实际上的 http 请求要复杂得多. 比如网盘文件的上传下载要生成 url, 为了保证内网外网都可以访问这个 url , url 的域名要设置为 pan.shino.space. 对于外网用户来说, 没什么影响; 但是对于内网用户来说, 如上面所说的, 访问的网址为 http://192.168.1.10, 接到一个 pan.shino.space 的请求链接时(注意这是浏览器自身要发起的请求, 与用户手动再打开一个网页有明显的不同), 浏览器发现与正在访问的网站不在同一个域中, 也就是这个访问 pan.shino.space 的请求属于跨域请求. 为了安全起见, http 协议中跨域请求是被认为是不变全的, 浏览器会拒绝连接. 体现在实际的使用, 就是用户可以正常访问网盘的主页, 但是内网用户无法进行上传和下载操作.

CORS 跨资源共享

这种跨域请求在稍微大一点一网站上是非常常见的, 为了处理这种问题, 让浏览器认为这个跨域请求是安全的, 引入了跨资源共享(CORS). 简单来说, 对于简单的 get, post 跨域请求, http 头部会加上 origin 字段, 代表请求跨域访问该域, 如果服务器允许跨域访问, 会在回复的头部加上 Access-Control-Allow-Origin 等字段来表示允许跨域请求的域, 请求方法等信息, 比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
request header:
Origin: http://b.com
Host: a.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0 ...
...

response header:
Access-Control-Allow-Methods: GET,POST,OPTIONS
Access-Control-Allow-Origin: http://b.com
Content-Type: text/html
...

而对于复杂一些的请求, 比如有自定义的头部, 或者请求头 Content-Type 为其他类型, 浏览器会先发送一个 option 方法的请求来确认要请求的域, 方法等是否被允许, 如果允许, 再发送正式的跨域请求.

现在可以访问了

知道是跨域请求的问题后, 修改下 nginx 配置就可以解决了. 对复杂的跨域请求, 服务器收到 option 方法的请求时, 直接返回 200 状态码, 并且加上 Access-Control-Allow-MethodsAccess-Control-Allow-Origin 头; 对简单的跨域请求, 服务器直接在所有的请求回复头部加上 Access-Control-Allow-MethodsAccess-Control-Allow-Origin 头:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
server {
    listen          80;
    server_name:    pan.shino.space;

    location / {
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin $http_origin;
            add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
            return 200;
        }
        if ($remote_addr ~ "202.118.1.10") {
            add_header Access-Control-Allow-Origin $http_origin;
            add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
            return 307 http://192.168.1.10;
        }
        proxy_pass: http://127.0.0.1:8010
    }
}

为了写起来简单, 没有对跨域访问的域进行验证, 实际上为了安全要进行验证的. 外网用户一直在使用 pan.shino.space 的域名进行访问, 不涉及跨域访问.

加上 https

外网 https

现在基本没几个网站是 http 的了, 我们的网站自然要上 https, 外网服务器部分很好办, 在80端口跳转到443端口, 443端口加上证书即可:

 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
server {
    listen         80;
    server_name    pan.shino.space;
    
    location / {
        add_header    Strict-Transport-Security max-age=15768000 always;
        return    301 https://$host$request_uri;
    }
}

server {
    listen                 443 ssl;
    server_name:           pan.shino.space;
    ssl_certificate        /usr/local/etc/shino.space.cer;
    ssl_certificate_key    /usr/local/etc/shino.space.key;

    location / {
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin $http_origin;
            add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
            return 200;
        }
        if ($remote_addr ~ "202.118.1.10") {
            add_header Access-Control-Allow-Origin $http_origin;
            add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
            return 307 http://192.168.1.10;
        }
        proxy_pass: http://127.0.0.1:8010
    }
}

内网 https

之前内网一直是用 ip 访问的, 要想也使用 https 需要加一个域名, 所以建立一个子域名 lan.shino.space 添加A记录 192.168.1.10, nginx 配置和上面外网的配置差不多.

https 反向代理

这样修改完, 之前的反向代理就会跪掉…反向代理到目前为止只有一行:

 1
proxy_pass: http://127.0.0.1:8010;

只是把 http 改成 https 是没有作用的, 因为 https 的目的就是验证服务器身份, 这里使用的本地ip 127.0.0.1 显然与证书上的 shino.space 不符, 自然不会建立连接. 额外加上 Host 头部指定域名就可以正常验证了.

 1
 2
proxy_pass: http://127.0.0.1:8010;
proxy_set_header HOST pan.warshiprpg.xyz;

这个域名也不是随便填上 lan.shino.space 就可以的, 因为内网的网盘服务器本身会跟据 Host 头部信息做一些跳转, lan.shino.space 是指向内网 ip 的, 外网跳转不到, 所以要设置为 pan.shino.space 这个外网域名, 这就要求内网的服务器上还要加上 pan.shino.space 的证书. 因为我实际使用的是泛域名证书, 所以证书不用动, server_name 上再加一个域名就可以了

EX: 内网有时访问不到外网服务器

本来上面的步骤做完就大功告成了, 但学校这破网, 有的时候与外网服务器建立 https 连接时, ssl 无法正常所握手…(更奇怪的时, 内网 https 穿透工作正常)从上面的配置可以看出, 内网使用 pan.shino.space 访问网盘时, 要先连接外网服务器再跳转到内网直连的. 而且网盘的上传和下载 url 都是 pan.shino.space 域名, 无法与外网连接就没办法上传和下载.

正常的解决方法就是换个外网服务器, 不过我在机房之外的地方连接外网服务器是正常的, 没理由换掉我的祖传 vultr 2.5刀. 手里还有一个腾讯云1块钱套餐的服务器, 不过只有 1MB 带宽, 而且没备案不开放 80 和 443 端口. 所以保持原来的外网服务器做反向代理, 用国内的腾讯云服务器做上传下载链接的分流跳转. 腾讯云上 nginx 配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server {
    listen       88 ssl default_server;
    server_name  t.shino.space;
    ssl_certificate        /usr/local/etc/shino.space.cer;
    ssl_certificate_key    /usr/local/etc/shino.space.key;

    location / {
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin $http_origin;
            add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
            return 200;
        }

        if ($remote_addr ~ "202.118.1.10") {
            add_header Access-Control-Allow-Origin $http_origin;
            add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
            return     307 https://lan.shino.space$request_uri;
        }

        add_header Access-Control-Allow-Origin $http_origin;
        add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
        return     307 https://pan.shino.space$request_uri;
    }
}

因为 https 访问, 所以加上了 t.shino.space 的域名; 跳转链接是浏览器自动跳转的, 所以设置成 88 端口也没关系(国内也不给我开80); 对于外网(pan.shino.space)和内网(lan.shino.space)用户来说, 这个跳转都是跨域请求, 所以要加上相关的处理. 最后把网盘服务器的上传下载链接改成 t.shino.space:88 就可以了. 上传下载的流程如下:

  • 服务器给出上传/下载的 url
  • 浏览器向 t.shino.space 发出上传/下载跨域 option 请求
  • t.shinos.pace 服务器回应 option 请求 200 OK
  • 浏览器向 t.shinos.pace 发出上传/下载请求
  • t.shino.space 服务器判断客户端是否为内网, 返回对应的跳转网址
  • 浏览器向对应的服务器发送上传/下载请求
  • 上传/下载完成

emmmm, 上次更新服务器系统时忘了备份数据库, 再加上评论比较少, 暂时关闭评论功能