MeteorCat / 自签证书内网穿透

Created Wed, 24 Apr 2024 21:18:35 +0800 Modified Wed, 29 Oct 2025 23:24:53 +0800
4784 Words

自签证书内网穿透

这里是基于内网的自签名证书对外开放服务功能, 主要流程:

  1. Linux 定时自动 生成自签证书生成放置于 Nginx 特定目录, 建议每日|每月自动更新证书数据
  2. 在生成证书的同时挂载自签证书提供对外服务, 所有服务都必须经由自签证书访问
  3. 在生成证书的同时写入到公钥和证书数据到 Redis 之中保存
  4. 对外挂起单独 Web 服务提供登录服务用于统一登录授权
  5. 用户登录认证之后服务器返回 地址+端口+证书+公钥进行 从而保存到本地挂起 Web 通过自签证书访问服务
  6. 用户从登录多个返回授权服务列表可以直接访问到内部自签名服务

具体的请求时序图如下:

Program UML

这种访问方式可以在外网防止中间人窃取访问数据, 从而保证内部服务的安全可靠性; 这里采用 Python|Bash 脚本处理都行, 另外还需要知道怎么 构建自签名证书.

上面的构建自签证书需要手动输入必要的信息, 这里采用连贯命令直接单行全部编写( 先测试脚本: /etc/nginx/auto.cer.sh ):

#!/bin/bash

# 注意: 这个脚本最后采用 root 方式管理, 因为要Nginx重载配置
if [ $UID -ne 0 ]; then
  echo "require permissions, please run as root"
  exit 1
fi


# 证书有效天数, 最好采用动态生成不断周期更新
CER_DAY=60

# 构建的动态端口起始值
CER_INDEX=2

# 转发的内网服务地址
CER_PROXY="http://127.0.0.1:3000"

# 证书输出路径, 这里默认在 Nginx 上构建出来, 注意必须先创建好目录
# mkdir -p /etc/nginx/auto_ssl
CER_PATH="/etc/nginx/auto_ssl"

# Nginx 放置的配置加载目录, 注意必须要创建好目录
# mkdir -p /etc/nginx/auto.d
NGINX_CONF_PATH="/etc/nginx/auto.d"

# Nginx 替换内容数据模板文件
# 这个模板文件后续会提供用于替换数据
NGINX_TPL_FILE="/etc/nginx/auto.conf.tpl"

# 判断模板是否存在
if [ ! -f "${NGINX_TPL_FILE}" ];then
  echo "file not exists: ${NGINX_TPL_FILE}"
  exit 1
fi


# 证书的相关信息
CER_COUNTRY="CN"
CER_STATE="GuangDong"
CER_CITY="GuangZhou"
CER_ORGANIZATION="HaiZhu"
CER_ORGANIZATIONAL_UNIT="101"
CER_COMMON_NAME="MeteorCat"
CER_EMAIL="[email protected]"

# 这里先测试采用按照当天生成证书确定脚本可用, 也就是不断在当天产生 `2024.03.22.auto.cer|2024.03.22.auto.key` 之类
# 这里先不考虑多端口挂起多服务, 先命令脚本跑出自动签名证书效果, 验证完成之后再做进一步配置

# 构建出当天年月日格式和端口时间
CER_YMD=`date +'%Y.%m.%d'`
NGINX_YMD=`date +'%y%m'`

# 构建文件名
CER_FILE="${CER_YMD}.auto.cer"
KEY_FILE="${CER_YMD}.auto.key"
P12_FILE="${CER_YMD}.auto.p12"

# 这里使用动态端口访问: 采用 起始单值+短年+长月 作为端口号, 端口号最高 65535
# 假如 CER_INDEX 输入 2, 按照年份就是 2403, 最后结果就是 22403 作为最后端口
# 如果固定是端口号直接写入到变量即可, 直接长期固定端口号
NGINX_CONF_PORT="${CER_INDEX}${NGINX_YMD}"
NGINX_CONF_FILE="${NGINX_CONF_PORT}.auto.conf"

# 打印文件路径和动态端口加载
echo "create cer: ${CER_PATH}/${CER_FILE}"
echo "create key: ${CER_PATH}/${KEY_FILE}"
echo "create p12: ${CER_PATH}/${P12_FILE}"
echo "create conf: ${NGINX_CONF_PATH}/${NGINX_CONF_FILE}"
echo "create proxy: ${CER_PROXY}"

# 构建命令, 直接等待让其执行
openssl req -x509 -nodes -days ${CER_DAY} \
-keyout ${CER_PATH}/${KEY_FILE} \
-out ${CER_PATH}/${CER_FILE} \
-subj "/C=${CER_COUNTRY}/ST=${CER_STATE}/L=${CER_CITY}/O=${CER_ORGANIZATION}/OU=${CER_ORGANIZATIONAL_UNIT}/CN=${CER_COMMON_NAME}/emailAddress=${CER_EMAIL}"

# 转化成通用 p12 证书
openssl pkcs12 -export -in ${CER_PATH}/${CER_FILE} \
-inkey ${CER_PATH}/${KEY_FILE} \
-out ${CER_PATH}/${P12_FILE} -passout pass:

# 构建Nginx的服务配置文件, 直接替换模板服务文本
# 直接覆盖对应的标签内容写入到 nginx 配置
awk -v _port_="${NGINX_CONF_PORT}" -v _cer_="${CER_PATH}/${CER_FILE}" -v _key_="${CER_PATH}/${KEY_FILE}" -v _proxy_="${CER_PROXY}" \
'{gsub("__PORT__", _port_);gsub("__CER__", _cer_);gsub("__KEY__", _key_);gsub("__PROXY__", _proxy_); print $0}' \
${NGINX_TPL_FILE} > ${NGINX_CONF_PATH}/${NGINX_CONF_FILE}


# 这里是需要构建 Nginx 加载配置, 因为运行在 root 所以直接 `systemctl reload nginx.service`
# 也可以直接修改证书的可访问权限, 这里默认给了 444 方便给只读处理
chmod 444 ${CER_PATH}/${KEY_FILE}
chmod 444 ${CER_PATH}/${CER_FILE}
systemctl reload nginx


# 这里可以将CER和KEY数据写入 Redis, 然后让独立的授权登录接口返回 RestApi 让用户再次去连接对应服务

这里需要补充个模板 /etc/nginx/auto.conf.tpl 用来文本替换写入到具体配置目录:

server {
        # 设置监听端口, __PORT__ 就是替换模板变量
        listen __PORT__ ssl http2;
        
        # 设置服务器访问域名
        server_name _;
        
        # http support versions,
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

        # check server cipher
        ssl_prefer_server_ciphers on;

        # server cipher methods
        ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";


        # certificate path, CER 文件变量
        ssl_certificate __CER__;

        # certificate key path, KEY 文件变量
        ssl_certificate_key __KEY__;

        # bidirectional check
        ssl_verify_client on;
        ssl_client_certificate __CER__;
        
        # 声明代理转发给服务器池, 地址模板变量模板
        location / {
            proxy_pass __PROXY__;
            proxy_set_header Host $host:$server_port;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-real-ip $remote_addr;
        }
}

注: 这里 proxy| 是我自己编写的 Web 服务, 这里用于测试是否能够访问

现在就是在 /etc/nginx/nginx.conf 当中引入启动转发自签证书服务:

# 注意是 http 区块
http {
    # 前面代码略
    
    # 引入自签证书服务模块
    include /etc/nginx/auto.d/*.conf;
}

测试执行脚本确认挂起自签证书协议, 确认是否访问安全:

# 只授权给 root 管理, 22404 是动态构建证书的端口
sudo bash /etc/nginx/auto.cer.sh
sudo lsof -i :22404

# 这里服务器测试访问自签证书协议的地址
curl -I http://192.168.1.100:22404 # 提示 400 错误请求
curl -I https://192.168.1.100:22404 # 提示 ERR_CERT_AUTHORITY_INVALID|SSL certificate problem: self-signed certificate, 要求加载自签证书
curl -I -k https://192.168.1.100:22404 # 采用 -k 忽略证书会被打回提示 400 错误请求

# 使用自签证书访问, 注意自签证书必须要开启 -k 忽略认证证书
# 最后确认访问到状态200, 代表可以直接证书访问到数据
# 注意这里是在 Linux 环境下测试, Window 环境很复杂可能没办法访问, 可以查看后续利用其他编程脚本来读取
curl -k -I --key /etc/nginx/auto_ssl/2024.04.24.auto.key --cert /etc/nginx/auto_ssl/2024.04.24.auto.cer https://192.168.1.100:22404

之后就是挂载在 root 的定时脚本运行来更新签名, 设置 每天凌晨 00:01:00 运行下脚本构建出来并且让 nginx 加载.

后续需要结合系统将数据放置与 Redis|MariaDB 之中, 方便和另外独立授权接口进行认证联调, 需要注意 Window 平台采用p12证书来进行访问

总所周知, https 证书访问能够防止中间人监听到你访问的网页内容, 中间人只能了解你访问的 url 信息和证书加密之后的网页内容, 所以也就无法得知你在传输的数据内容, 也因为周期性自动更新证书导致了证书并不是一成不变, 从而加大了破解证书内容的难度.

授权暴露服务

这里的授权服务是单独部署的 Web 服务, 需要和之前动态证书部署的服务区分开来, 这里展示该接口请求和响应数据:

// [用户端:请求] 登录接口: POST /auth/login 
{
  "username": "meteorcat",
  "password": "meteorcat"
}

// [服务端:响应] 登录接口
{
  "error": 0,
  "message": "success",
  "data": {
    "uid": 1,
    "token": "MD5TOKEN",
    // 返回的服务列表, 注意这里列表保存本地
    // 保存分为 'server_1.json,server_2.json', 后续访问就直接加载指定服务去校验接口数据
    // 这里也可以单独提供下载接口, 用户去下载端口信息放置于自己配置文件路径下读取
    "servers": [
      {
        "id": 1,
        "address": "https://192.168.1.100:22404",
        // 其他证书相关数据
        "cer": "CER_DATA",
        "key": "KEY_DATA",
        "p12": "P12_DATA"
      }
    ]
  }
}

// =====================================================

// [用户端:请求] 配置下发: POST /auth/download (这个接口并不是必要的)
{
  "token": "MD5TOKEN",
  // 需要下载的配置ID
  "id": 1
}

// [服务端:响应] 这里直接构建推送下载

// =====================================================

// [用户端:请求] 校验接口: POST /auth/check
{
  "token": "MD5TOKEN",
  "id": 1,
  // 服务ID
  "address": "https://192.168.1.100:22404",
  // 其他证书相关数据
  "cer": "CER_DATA",
  "key": "KEY_DATA",
  "p12": "P12_DATA"
}

// [服务端:响应] 校验接口 
{
  // 如果可用, 错误码应该返回0, 否则其他情况取 message 错误消息显示给用户要求重新登录
  "error": 0,
  "message": "success",
  "data": {}
}

用户首次登录的时候需要确认是否本地缓存 server.json 之类的登录授权, 如果没有需要提示用户跳转登录等待获取授权; 如果已经带有授权需要访问校验接口, 同步确认下证书授权信息是否过期, 如果过期则删除本地授权文件再次跳转到授权服务页面.

这里的 Web 服务更像是构建代理访问接口用来代理到内网的服务, 用户拿到授权之后可以直接本地加载证书访问内网服务.

这里的 Web 服务没有限定什么编程语言, 基本上是主流编程语言就行了, 因为这里可以用太多语言实现所以只需要接口格式就行了.

和传统 RestApi 其实差不多, 只是常规的时候请求都是单个地址持续请求, 而上面就是分离成两个请求端并采用自签名证书访问.

这里提供 SpringBoot 测试样例挂起 Web 的授权服务监听:

package com.meteorcat.cer.api;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;


/**
 * 授权登录, 先单独集成该文件, 后续重构直接分离处理
 * 这里仅仅作为示例编写
 */
@RestController
@RequestMapping("/auth")
public class AuthApi {


    /**
     * 日志句柄
     */
    final Logger logger = LoggerFactory.getLogger(getClass());


    /**
     * 先暂时手动编写的返回的响应体
     *
     * @param error   错误码
     * @param message 消息内容
     * @param data    返回数据
     * @return JsonMap
     */
    protected Map<String, Object> response(int error, String message, Object data) {
        return new HashMap<>(3) {{
            put("error", error);
            put("message", message);
            put("data", data == null ? new HashMap<>(0) : data);
        }};
    }


    /**
     * 暂时放置的请求提交体
     */
    public static class UserForm implements Serializable {
        private String username;
        private String password;

        public String getPassword() {
            return password;
        }

        public String getUsername() {
            return username;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        @Override
        public String toString() {
            return "UserForm{" +
                    "username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
    }


    /**
     * 已经完成登录授权的玩家
     */
    private final Map<String, Integer> online = new HashMap<>();

    /**
     * 登录接口
     *
     * @param userForm 玩家提交格式
     * @return JSON
     */
    @PostMapping("/login")
    public Object login(@RequestBody UserForm userForm) {

        logger.debug("User: {}", userForm);

        // Todo:这里先手动验证账号信息, 后续如果想数据库挂载出内容这里再处理
        if (!"meteorcat".equals(userForm.username) || !"meteorcat".equals(userForm.password)) {
            return response(1, "找不到玩家信息", null);
        }


        // todo: 临时写死的玩家ID
        Integer uid = 1;


        // 生成登录Token
        String format = String.format("%s-%d", userForm.username, System.currentTimeMillis());
        String hash = DigestUtils.md5DigestAsHex(format.getBytes(StandardCharsets.UTF_8));

        // 构建出具体的暴露穿透的内容
        ArrayList<Map<String, Object>> servers = new ArrayList<>();


        // Todo: 这里先写死返回的数据内容, 后续通过加载本地来处理
        servers.add(new HashMap<>() {{
            put("id", 1);
            put("address", "https://192.168.1.100:22404");
            // 下面的内容需要去读取内容返回
            put("cer", "CER_DATA内容");
            put("key", "KEY_DATA内容");
            put("p12", "P12_DATA内容");
        }});


        // 确认已经在线的数据
        if (online.containsValue(uid)) {
            for (Map.Entry<String, Integer> active : online.entrySet()) {
                if (active.getValue().equals(uid)) {
                    online.remove(active.getKey());
                    break;
                }
            }
        }

        // 写入在线数据
        online.put(hash, uid);

        // 响应出内容
        return response(0, "success", new HashMap<>() {{
            put("uid", uid);
            put("token", hash);
            put("servers", servers);
        }});
    }


    /**
     * 暂时放置的请求提交体
     */
    public static class CheckForm implements Serializable {
        private String token;

        private Integer id;

        private String address;

        private String cer;

        private String key;

        private String p12;

        public String getToken() {
            return token;
        }

        public void setToken(String token) {
            this.token = token;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        public Integer getId() {
            return id;
        }

        public void setAddress(String address) {
            this.address = address;
        }

        public void setCer(String cer) {
            this.cer = cer;
        }

        public void setKey(String key) {
            this.key = key;
        }

        public void setP12(String p12) {
            this.p12 = p12;
        }

        public String getAddress() {
            return address;
        }

        public String getCer() {
            return cer;
        }

        public String getKey() {
            return key;
        }

        public String getP12() {
            return p12;
        }

        @Override
        public String toString() {
            return "CheckForm{" +
                    "token='" + token + '\'' +
                    ", id=" + id +
                    ", address='" + address + '\'' +
                    ", cer='" + cer + '\'' +
                    ", key='" + key + '\'' +
                    ", p12='" + p12 + '\'' +
                    '}';
        }
    }


    /**
     * 检查最新授权验证
     *
     * @param checkForm 请求结构体
     * @return JSON
     */
    @PostMapping("/check")
    public Object check(@RequestBody CheckForm checkForm) {
        logger.debug("Check: {}", checkForm);

        // 首先确认授权是否存在
        String token = checkForm.token;
        if (!online.containsKey(token)) {
            return response(1, "用户登录授权过期, 请重试", null);
        }

        // todo: 去服务器本地比较授权数据

        // 最后确认授权验证通过
        return response(0, "success", null);
    }

}

重点要记住, 独立的 Web 登录授权服务 https 必须要公网CA授权证书而不要自签证书访问.

这里就是简单编写的 SpringBoot 授权样例, 后续可以按照这方向去细化处理.

用户端自签证书访问

之前编写完服务端相关工作, 而现在用户已经可以下载所有证书内容到本地, 之后就是怎么在用户端使用这些证书文件.

这里假设的前提是已经跑通获取到 cer/key/p12 证书所有信息, 这才是构建整套内网访问体系的基础

这里实际上就是本地挂起另外 Web 服务来做转发, 用户通过访问本地端口然后通过自签证书转发到远程服务.

这里列举些其他编程语言调用自签证书访问的样例( 摘录网路, 不对其有效保证 ).

PHP 版本转发

<?php

// PHP 版本带证书转发
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://192.168.1.100:22404");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 关闭验证证书, 自签证书必须要用到
curl_setopt($ch, CURLOPT_SSLCERT, "2024.04.24.auto.cer"); // 这里采用cer而不是p12证书, 只有 window 流量器直接访问才需要import
curl_setopt($ch, CURLOPT_SSLKEY, "2024.04.24.auto.key"); // 采用自签证书Key
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); // 关闭主机名验证
$response = curl_exec($ch); // 执行cURL会话
if(curl_errno($ch)){
    // 检查是否有错误发生
    echo 'cURL error: ' . curl_error($ch);
    exit(1);
}

curl_close($ch); // 关闭cURL资源,并释放系统资源
echo $response; // 打印响应内容

Python 版本转发

# !/usr/bin/python
# -*- coding: UTF-8 -*-

import sys

# 安装请求库: pip install requests
import requests


# 入口方法
def main(argv):
    cer_file = "2024.04.24.auto.cer"
    key_file = "2024.04.24.auto.key"
    response = requests.get("https://192.168.1.100:22404", cert=(cer_file, key_file), verify=False)
    print(response.text)


if __name__ == "__main__":
    main(sys.argv[1:])

注意: 上面的都是需要运行环境的, 比如 php/python 之类都是要求用户本地必须要安装好执行二进制, 这无疑增大的用户处理使用的成本.

原生平台转发

排除掉所有需要安装执行的方案, 那么直接只有编译语言编译二进制处理的方案, 这里目前常见方案如下:

  • Golang: Google 的跨平台编译方案, 内部的高级处理比较简陋
  • Rust: Mozilla 的跨平台编译方案, 上手所有权概念比较复杂

尽可能减少用户使用成本, 甚至直接采用单个配置运行最好: run.exe server.ini

这里具体最后选中方案采用比较熟练的 Rust 开发, 模块库也相对比较广泛可以直接调用.

Cargo.toml 配置库:

# 其他略

[dependencies]
log = { version = "0.4" }
env_logger = { version = "0.11" }
native-tls = { version = "0.2.11" }
tokio = { version = "1", features = ["full"] }
tokio-native-tls = { version = "0.3.1" }
axum = { version = "0.7.5" }
axum-extra = { version = "0.9" }
hyper = { version = "1.3", features = ["full"] }
hyper-tls = { version = "0.6" }
hyper-util = { version = "0.1.3", features = ["client-legacy", "http2"] }
reqwest = { version = "0.12", features = ["http2", "native-tls", "stream"] }

之后就是业务逻辑代码:

use axum::body::Body;
use axum::extract::{Request, State};
use axum::handler::Handler;
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use axum::response::Response;
use axum::Router;
use axum::routing::get;
use log::{error, info};
use reqwest::Method;

/// 客户端请求配置
#[derive(Clone)]
struct ClientConfig {
    address: String,
    cer: Vec<u8>,
    key: Vec<u8>,
    p12: Vec<u8>,
}

#[tokio::main]
async fn main() {
    // todo:加载外部配置文件信息, 后续主要工作就是将这里的配置读取成配置加载
    let address = "127.0.0.1:3000";
    let remote = "https://192.168.1.100:22404";
    let key_file = "2024.04.24.auto.key";
    let p12_file = "2024.04.24.auto.p12";
    let cer_file = "2024.04.24.auto.cer";

    // 日志构建, 测试过程采用 debug 打印所有异常
    let mut builder = env_logger::Builder::from_default_env();
    builder.filter_level(log::LevelFilter::Debug);
    builder.init();
    info!("启动 CER 代理");
    info!("已加载PEM: {}",cer_file);
    info!("已加载KEY: {}",key_file);
    info!("已加载P12: {}",p12_file);


    // 构建共享数据
    let cer_data = match tokio::fs::read(cer_file).await {
        Ok(c) => c,
        Err(e) => {
            error!("{:?}",e);
            std::process::exit(1);
        }
    };
    let key_data = match tokio::fs::read(key_file).await {
        Ok(c) => c,
        Err(e) => {
            error!("{:?}",e);
            std::process::exit(1);
        }
    };

    let p12_data = match tokio::fs::read(p12_file).await {
        Ok(c) => c,
        Err(e) => {
            error!("{:?}",e);
            std::process::exit(1);
        }
    };

    let conf = ClientConfig {
        address: remote.to_string(),
        cer: cer_data,
        key: key_data,
        p12: p12_data,
    };


    // 挂起本地路由
    let app = Router::new()
        .route("/", get(process))
        .route("/*path", get(process))
        .with_state(conf);


    // 挂起本地代理服务
    let listener = match tokio::net::TcpListener::bind(&address).await {
        Ok(l) => l,
        Err(e) => {
            error!("{:?}",e);
            std::process::exit(1);
        }
    };

    // axum 启动监听
    info!("代理地址: {}",&address);
    if let Err(e) = axum::serve(listener, app).await {
        error!("{:?}",e);
        std::process::exit(1);
    }
}


/// 访问代理转发
async fn process(State(config): State<ClientConfig>, mut req: Request) -> Result<Response, StatusCode> {
    let path = req.uri().path();
    let url = req
        .uri()
        .path_and_query()
        .map(|v| v.as_str())
        .unwrap_or(path);

    // 挂起请求
    let remote = format!("{}{}", &config.address, url);
    info!("挂起代理: {} -> {}",url,remote);


    // 推送请求
    let cert = match reqwest::Certificate::from_pem(&config.cer.as_slice()) {
        Ok(c) => c,
        Err(e) => {
            error!("{:?}",e);
            return Err(StatusCode::BAD_REQUEST);
        }
    };

    let key = match reqwest::Identity::from_pkcs8_pem(&config.cer.as_slice(), &config.key.as_slice()) {
        Ok(k) => k,
        Err(e) => {
            error!("{:?}",e);
            return Err(StatusCode::BAD_REQUEST);
        }
    };

    // P12证书暂时用不到
    // let p12 = match reqwest::Identity::from_pkcs12_der(&config.p12.as_slice(), "") {
    //     Ok(k) => k,
    //     Err(e) => {
    //         error!("{:?}",e);
    //         return Err(StatusCode::BAD_REQUEST);
    //     }
    // };


    return match reqwest::ClientBuilder::new()
        .add_root_certificate(cert)
        .identity(key)
        .https_only(true)
        .danger_accept_invalid_certs(true)
        .danger_accept_invalid_hostnames(true)
        .build() {
        Ok(c) => {
            match c.request(Method::GET, remote).send().await {
                Ok(r) => {
                    // Here the mapping of headers is required due to reqwest and axum differ on the http crate versions
                    let mut headers = HeaderMap::with_capacity(r.headers().len());
                    headers.extend(r.headers().into_iter().map(|(name, value)| {
                        let name = HeaderName::from_bytes(name.as_ref()).unwrap();
                        let value = HeaderValue::from_bytes(value.as_ref()).unwrap();
                        (name, value)
                    }));

                    // write remote headers
                    let mut builder = Response::builder()
                        .status(r.status().as_u16());
                    for header in headers {
                        if header.0.is_some() {
                            if let Some(active) = builder.headers_mut() {
                                active.insert(header.0.unwrap(), header.1);
                            }
                        }
                    }
                    
                    match builder.body(Body::from_stream(r.bytes_stream())) {
                        Ok(res) => Ok(res),
                        Err(e) => {
                            error!("{:?}",e);
                            return Err(StatusCode::BAD_REQUEST);
                        }
                    }
                }
                Err(e) => {
                    error!("{:?}",e);
                    return Err(StatusCode::BAD_REQUEST);
                }
            }
        }
        Err(e) => {
            error!("{:?}",e);
            Err(StatusCode::BAD_REQUEST)
        }
    };
}

后续打出各自平台二进制执行文件就能直接本地只用转发了.

上面那些样例实际上都有隐藏问题, 那就是仅仅支持 GET 请求的转发

这里先实现初版 Rust 的转发功能, 确认最后请求能够被转发内网服务当中.

直接访问 http://127.0.0.1:3000 就是被代理起来的本地穿透内网服务