跳转到内容
Doc Record

Blog

Mihomo SSH Config Alias Support

  • Fork 仓库: https://github.com/iuin8/mihomo
  • 分支: (请确保查看包含 ssh-config-alias 功能的分支, 例如: ssh_system_v1.19.17)
  • 核心文件: adapter/outbound/ssh.go, adapter/outbound/ssh_system.go

在复杂的网络环境中,用户往往已经维护了一套完善的 SSH 配置文件 (~/.ssh/config)。这些配置可能包含:

  1. ProxyJump (跳板机):需要通过一台或多台跳板机才能访问目标内网服务器。
  2. ProxyCommand:使用第三方认证工具(如 Cloudflare Access, AWS SSM)建立连接。
  3. IdentityFile:针对不同主机使用不同的密钥文件。
  4. Host 别名:使用简短的别名代替长 IP 或域名。

原有的 Mihomo SSH 适配器使用 Go 语言原生 SSH 库,无法直接利用这些系统级配置,导致用户必须把复杂的跳板逻辑手动转化为 Mihomo 的代理链(Dialer Proxy),配置繁琐且不支持部分高级指令(如特殊的 ProxyCommand)。

目标:让 Mihomo 可以直接”借用”系统 SSH 客户端的能力,只需填一个主机别名(如 my-server),剩下的认证、跳转全交给系统 SSH 处理。

通过调用系统 ssh 命令建立隧道,Mihomo 可以支持系统 SSH 客户端支持的所有指令:

  • ✅ ProxyJump / JumpHost
  • ✅ ProxyCommand (支持 cloudflared, nc 等)
  • ✅ IdentityFile, User, Port 等自动读取
  • ✅ Match, Include 等高级配置逻辑

Mihomo 通常以 root 权限运行(为了 TUN 模式等),而用户的 SSH 配置位于普通用户目录下。本功能实现了:

  • 自动检测或指定实际用户。
  • 使用 sudo -u <user> -i 切换身份执行 SSH。
  • -i 参数确保加载用户的完整登录环境(PATH),保证 cloudflared 等命令可被找到。

无需在 Mihomo 配置文件中填入私钥内容,它会自动读取 ~/.ssh/id_rsa 或配置中指定的 IdentityFile。

字段说明
type必须为 ssh
server关键:填写 ~/.ssh/config 中的 Host 别名
port注意:填写目标服务内部监听端口(通常是 22)。
⚠️ 不要填 ~/.ssh/config 中的映射端口。
use-ssh-config-alias设置为 true 开启此功能
ssh-user指定本地执行 SSH 命令的用户名(你的 macOS/Linux 用户名)

SSH Config (~/.ssh/config):

Host bastion
HostName 1.2.3.4
User admin
Host internal-db
HostName 10.0.0.5
User root
ProxyJump bastion # 自动经过 bastion 跳转

Mihomo 配置:

proxies:
- name: "Internal-DB-SSH"
type: ssh
server: "internal-db" # 直接填别名
port: 22 # 目标内部 SSH 端口
use-ssh-config-alias: true
ssh-user: "your-username" # 你的本地用户名

场景 2:Cloudflare Access (ProxyCommand)

Section titled “场景 2:Cloudflare Access (ProxyCommand)”

SSH Config:

Host my-cf-server
HostName ssh.example.com
User root
# 需要 cloudflared 命令在 PATH 中
ProxyCommand cloudflared access ssh --hostname %h

Mihomo 配置:

proxies:
- name: "CF-SSH"
type: ssh
server: "my-cf-server"
port: 22
use-ssh-config-alias: true
ssh-user: "your-username"

Mihomo 收到连接请求后,会在底层执行类似以下的命令:

终端窗口
sudo -u your-username -i ssh -W localhost:22 my-host-alias

这条命令会建立一个标准输入/输出到目标 SSH 端口的 TCP 隧道。Mihomo 随后在这个隧道上进行自己的 SSH 协议握手,建立代理连接。

SSH 远程端口转发配置指南:使用 socat 实现灵活的端口映射

  1. 概述

  2. 配置详解

  3. 工作原理

  4. 使用方法

  5. 故障排除

  6. 最佳实践

  7. 高级配置

SSH 远程端口转发是一种强大的网络技术,允许您将远程服务器的端口转发到本地机器或其他目标地址。结合 socat 工具,您可以实现更灵活的端口映射和网络连接管理。

本文档详细介绍了如何配置和使用 SSH 远程端口转发,特别关注如何确保 socat 进程在 SSH 连接断开时自动终止,避免资源泄漏和端口占用问题。

以下是完整的 SSH 配置文件示例:

Host fa.remote.intranet.company
&#x20; HostName 10.0.16.146
&#x20; User root
&#x20; IdentityFile \~/.ssh/id\_ed25519\_iu
&#x20; RemoteForward 9094 127.0.0.1:9093
&#x20; RemoteCommand bash -c "trap 'kill %%1' EXIT; socat TCP-LISTEN:9093,fork TCP:127.0.0.1:9094 & exec bash"
&#x20; ForwardAgent yes
&#x20; RequestTTY yes
Host fa.remote.intranet.company
&#x20; HostName 10.0.16.146
&#x20; User root
&#x20; IdentityFile \~/.ssh/id\_ed25519\_iu
  • Host:定义主机别名,方便后续引用

  • HostName:远程服务器的实际 IP 地址或域名

  • User:登录远程服务器使用的用户名

  • IdentityFile:指定用于身份验证的 SSH 密钥文件

RemoteForward 9094 127.0.0.1:9093

这是核心配置项,实现远程端口转发:

  • 9094:远程服务器上监听的端口

  • 127.0.0.1:9093:本地机器上的目标地址和端口

效果:远程服务器的 9094 端口收到的所有连接都会被转发到本地机器的 9093 端口。

RemoteCommand bash -c "trap 'kill %%1' EXIT; socat TCP-LISTEN:9093,fork TCP:127.0.0.1:9094 & exec bash"

这个复杂的命令实现了以下功能:

  • bash -c "...":在远程服务器上执行 bash 命令

  • trap 'kill %%1' EXIT:设置退出陷阱,当 shell 退出时终止 socat 进程

  • socat TCP-LISTEN:9093,fork TCP:127.0.0.1:9094:启动 socat 进行端口转发

    • TCP-LISTEN:9093:在远程服务器的 9093 端口监听

    • fork:为每个连接创建新的进程

    • TCP:127.0.0.1:9094:转发到本地的 9094 端口

  • &:在后台运行 socat

  • exec bash:启动交互式 bash shell

ForwardAgent yes
RequestTTY yes
  • ForwardAgent yes:启用 SSH 代理转发,允许在远程服务器上使用本地的 SSH 密钥

  • RequestTTY yes:强制分配伪终端,确保获得交互式 shell

整个配置的工作流程如下:

  1. SSH 连接建立:当您运行ssh fa.remote.intranet.company时,SSH 客户端会:
  • 使用指定的密钥文件进行身份验证

  • 在远程服务器上建立 9094 端口的转发

  • 执行 RemoteCommand 中指定的命令

  1. socat 启动:RemoteCommand 会:
  • 设置退出陷阱

  • 启动 socat 监听 9093 端口

  • 启动交互式 bash shell

  1. 端口转发流程:
外部客户端 → 远程服务器:9093 → socat → 远程服务器:9094 → SSH转发 → 本地机器:9093
  1. 连接关闭:当您退出 bash shell 时:
  • trap 机制会捕获 EXIT 信号

  • 终止 socat 进程

  • SSH 连接关闭

  • 所有转发端口释放

  1. 编辑 SSH 配置文件
nano \~/.ssh/config

添加上述配置内容。

  1. 建立连接
ssh fa.remote.intranet.company
  1. 验证配置

在远程服务器上执行:

ss -tunlp | grep 9093
\# 应该显示socat在监听9093端口
ss -tunlp | grep 9094
\# 应该显示ssh在监听9094端口
  1. 测试端口转发

从另一台机器测试:

curl http://10.0.16.146:9093

如果您希望在后台运行端口转发,可以使用:

ssh -fN fa.remote.intranet.company
  • -f:在后台运行

  • -N:不执行远程命令

症状:

Address already in use

解决方案:

检查并终止占用端口的进程:

\# 在远程服务器上执行
ss -tunlp | grep 9093
kill -9 <进程ID>

或者修改配置使用其他端口。

症状:连接后直接退出或没有 bash 提示符

解决方案:

确保配置中包含:

RequestTTY yes

或者在命令行中使用:

ssh -t fa.remote.intranet.company

症状:

ps -ef | grep socat
\# 显示socat进程,父ID为1

解决方案:

手动终止进程:

pkill socat

或者修改 RemoteCommand 确保 trap 正确工作:

RemoteCommand bash -c "trap 'pkill socat' EXIT; socat TCP-LISTEN:9093,fork TCP:127.0.0.1:9094 & exec bash"

避免使用常见服务端口(如 80、443、22 等),减少冲突风险。

修改 RemoteCommand,添加端口占用检查:

RemoteCommand bash -c "if ! ss -tunlp | grep -q 9093; then trap 'kill %%1' EXIT; socat TCP-LISTEN:9093,fork TCP:127.0.0.1:9094 & fi; exec bash"

对于需要长期运行的转发,使用 autossh:

autossh -M 0 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" fa.remote.intranet.company

添加日志记录以便故障排除:

RemoteCommand bash -c "trap 'kill %%1' EXIT; socat -d -d TCP-LISTEN:9093,fork TCP:127.0.0.1:9094 > /var/log/socat.log 2>&1 & exec bash"

您可以配置多个 RemoteForward:

RemoteForward 9094 127.0.0.1:9093
RemoteForward 9095 127.0.0.1:9094
RemoteForward 9096 192.168.1.100:80

根据本地环境变量决定是否启动转发:

Match exec "test -n \\"\$ENABLE\_FORWARD\\""
&#x20; RemoteForward 9094 127.0.0.1:9093

在 RemoteCommand 中使用环境变量:

RemoteCommand bash -c "PORT=\${PORT:-9093}; trap 'kill %%1' EXIT; socat TCP-LISTEN:\\\$PORT,fork TCP:127.0.0.1:9094 & exec bash"

然后在连接时设置变量:

PORT=9095 ssh fa.remote.intranet.company

SSH 远程端口转发结合 socat 是一种强大的网络工具,能够实现灵活的端口映射和连接管理。通过本文档介绍的配置方法,您可以:

  1. 建立可靠的远程端口转发

  2. 确保 socat 进程在 SSH 连接断开时自动终止

  3. 获得完整的交互式 shell 体验

  4. 避免常见的端口冲突和资源泄漏问题

这种配置特别适用于需要从外部访问内部服务、构建安全隧道或实现复杂网络拓扑的场景。


更新时间:2025 年 12 月 6 日

版本:1.0

适用场景:SSH 端口转发、远程访问、网络隧道

(注:文档部分内容可能由 AI 生成)

(frps selfhost版)有没想过起个容器就能打通整个内网呢? 使用容器打通受限网络: frp+ssh组合镜像以及clash(mihomo)实现打通网络

使用容器打通受限网络: frp+ssh组合镜像以及clash(mihomo)实现打通任意环境所有内网服务(包括k8s)(web页面以及终端访问) 这里还有个需要提到的是, 因为我的科学上网环境是用的clash(mihomo)软件, 所以这里就用clash(mihomo)来实现打通各种内网 这样我就可以只需要一个代理软件就行了

开发过程中总是能遇到需要访问其他公司内网的情况, 一般常规方案都是由其他公司提供vpn访问, 或者jumpserver进行内网服务器连接.

这时, 一般就会遇到几个痛点: - 想要访问k8s中的容器服务, k8s内部域名svc.cluster.local无法直接使用 - 要装很多乱七八糟的vpn软件, 不同公司用的vpn可能就不一样, 有的vpn甚至像流氓软件一样, 有各种限制 - 有的公司不提供vpn或jumpserver, 只能去现场 - 家里网络和公司网络不互通的问题(在公司想访问家里的一些服务, 或在家想访问公司的一些服务等)

然而, 打通网络后, 都有哪些好处呢? - 自己本地电脑不在需要安转太多的vpn软件了 - 所能触碰到的每台内网服务器, 都能成为你在何时何地都能访问或当做跳板机的工具 - 内网才能打开的页面, 随时都能打开了 - 网页系统应用能用浏览器打开了 - nacos能用浏览器打开了 - 内网才能访问的服务器, 随时都能访问了 - 数据库能通过idea或者DBeaver可视化连接了

也就是说, 打通了网络, 也就打通了ssh访问, ssh能到达的地方, 都能将自己的本地电脑拉入到同一网络中进行互通操作

需要有机会把容器起起来, 一般有以下几种方式, 选一个方便去操作的就行, 当容器起来之后, 网络就打通了, 虚拟机等过渡工具就可以删掉了, 不需要了 - 通过jumpserver页面登录 - 自己本身就安转了vpn - 找安装了vpn的同事 - 专门找台机用于安装各种乱七八糟的vpn也行 - 当然起个虚拟机去安转也行

这里的服务端, 就是你有公网IP的云服务器, 我这里用的是Debian, 你可以根据自己的情况, 选择合适的镜像

# vi /www/dev-jumpbox/server/docker-compose.yml
services:
frps-ssh:
image: registry.cn-hangzhou.aliyuncs.com/iuin/frps-ssh:tcpmux
network_mode: host
environment:
- TZ="Asia/Shanghai"
- auth_token="xxx"
- bindPort=18000
- tcpmuxHTTPConnectPort=12222
restart: unless-stopped
终端窗口
# 构建镜像并启动容器
docker-compose up -d

共用端口的方式, 参考文章

PS: 自定义域名需要配合proxycommand代理使用ssh -o 'proxycommand socat - PROXY:x.x.x.x:%h:%p,proxyport=5002' test@machine-a.example.com

# vi /www/dev-jumpbox/client/docker-compose.yml
services:
dev-jumpbox:
image: registry.cn-hangzhou.aliyuncs.com/iuin/dev-jumpbox:tcpmux-v6.1.1
container_name: dev-jumpbox
environment:
TZ: "Asia/Shanghai"
serverAddr: '"55.44.33.33"'
serverPort: 18000
auth_token: '"jumpboxs-ssh"'
client_name: '"jumpboxc-ssh-fa"'
customDomains: '["fa.intranet.company"]'
volumes:
# 需要先创建这个文件, 不然会自动创建为文件夹
- ./.ssh/authorized_keys:/root/.ssh/authorized_keys:ro
restart: unless-stopped

不想要多配置个proxycommand的话, 用下面的方式会简单点, 但需要独占端口

终端窗口
services:
dev-jumpbox:
image: registry.cn-hangzhou.aliyuncs.com/iuin/dev-jumpbox:frpc-ssh-v6.1
container_name: dev-jumpbox
environment:
TZ: "Asia/Shanghai"
serverAddr: '"55.44.33.33"'
client_title: proxies
serverPort: 18000
auth_token_line: auth.token = "jumpboxs-ssh"
client_name: '"fa.intranet.company"'
client_type: '"tcp"'
localIP_proxies_line: localIP="127.0.0.1"
localPort_proxies_line: localPort=22
# 独占端口方式, 需要外网防火墙开放这个端口
remotePort_proxies_tcp_line: remotePort=12202
extra_hosts:
- "container.host:host-gateway"
volumes:
# 需要先创建这个文件, 不然会自动创建为文件夹
- ./.ssh/authorized_keys:/root/.ssh/authorized_keys:ro
restart: unless-stopped
终端窗口
# 构建镜像并启动容器
docker-compose up -d
  • 关于卷(volumes)的说明

这里的卷也可以不挂载, 也可以通过进入容器中执行命令去写入authorized_keys文件中, 不过容器重启后, 会丢失写入的内容

终端窗口
# 在本地电脑中执行, 打印公钥
cat ~/.ssh/id_ed25519.pub
# 复制打印的公钥内容, 需要写入到`./.ssh/authorized_keys`, 这个文件是需要挂载到容器中的文件
# 全路径: /www/dev-jumpbox/client/.ssh/authorized_keys(注意: 别跟宿主机的authorized_keys文件搞混了)
# 这里也可以由dev-jumpbox容器去管理ssh公私钥, 把宿主机生成好的公私钥, 挂载到容器中, 然后在容器中执行命令去写入authorized_keys文件中, 最后把私钥给到clash(mihomo)工具使用
# 如果clash用户想用自己的私钥的话, 那就在生成私钥的时候, 选择将公私钥生成到clash配置目录下, 然后在clash配置文件中, 引用自己的私钥文件即可(clash verge rev只支持读取根目录下的私钥文件)
终端窗口
# 在客户端宿主机或者容器中执行, 写入公钥到authorized_keys文件中
echo 'ssh-ed25519 xxxxx xxx' > ./.ssh/authorized_keys

第四步: 启动docker-compose, 测试容器以及上传进行

Section titled “第四步: 启动docker-compose, 测试容器以及上传进行”
终端窗口
# 构建镜像并启动容器
docker-compose up -d
  • 使用ssh远程连接下, 试试效果
终端窗口
# 上传公钥, 开启免密登录, 这一步也是顺便检查了是否能够正常通过内网穿透ssh到容器中
ssh-copy-id root@129.204.8.8 -p 12222 -i ~/.ssh/id_ed25519
# 然后, 通过ssh免密登录
ssh root@129.204.8.8 -p 12222

参考详情链接

到这就已经基本完成了在任何地方都能联通ssh了, 接下来的就是高级应用了, 配合clash(mihomo)工具, 实现网络流量代理到内容容器中

配合clash(mihomo)工具使用, 方便访问网页

Section titled “配合clash(mihomo)工具使用, 方便访问网页”

这里, 我们借助clash(mihomo)工具, 通过ssh将流量代理转发到容器中, 实现像访问局域网一样访问容器那边的对应内网上的网页

// 代理流量(script.js[这里用了全局脚本的方式, 兼容自己的原有的订阅, 不影响原有的订阅, 只做扩展])
function main(config, profileName) {
const privateKeyContent = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZxxxABAAAAMwAAA
QyNTUxOQAAAxxxAAAIgIqewcCKn
HAAAAAtxxxDYC8YhlRDIhM+GUeg
-----END OPENSSH PRIVATE KEY-----`;
const extra = {
proxies: [
{
name: "company_container",
type: "ssh",
server: "183.11.11.11",
port: 11111,
username: "root",
// 用密钥的情况下, 这里需要把密钥复制到软件配置目录下的.ssh目录中, 才能正常使用
// "private-key": "./.ssh/id_ed25519_iu"
"private-key": privateKeyContent
}
],
proxyGroups: [
{
name: "company_g",
type: "select",
proxies: ["DIRECT", "company_container"]
}
],
rules: [
"IP-CIDR,10.0.11.0/24,company_g",
"DOMAIN-SUFFIX,company.com,company_g"
]
};
if (!Array.isArray(config.proxies)) config.proxies = [];
for (const p of extra.proxies) {
if (p && p.name && !config.proxies.some(x => x && x.name === p.name)) {
config.proxies.unshift(p);
}
}
if (!Array.isArray(config["proxy-groups"])) config["proxy-groups"] = [];
for (const g of extra.proxyGroups) {
let existing = config["proxy-groups"].find(x => x && x.name === g.name);
if (!existing) {
config["proxy-groups"].unshift({ name: g.name, type: g.type, proxies: Array.isArray(g.proxies) ? [...g.proxies] : [] });
} else {
if (!Array.isArray(existing.proxies)) existing.proxies = [];
for (const pn of g.proxies || []) {
if (!existing.proxies.includes(pn)) existing.proxies.unshift(pn);
}
}
}
if (!Array.isArray(config.rules)) config.rules = [];
for (const r of extra.rules) {
if (!config.rules.includes(r)) config.rules.unshift(r);
}
return config;
}
# 单独订阅配置(ssh.yml)
proxies:
- name: company_container
type: ssh
server: 183.11.11.11
port: 11111
username: root
# 用密钥的情况下, 这里需要把密钥复制到软件配置目录下的.ssh目录中, 才能正常使用
# private-key: ./.ssh/id_ed25519_iu
private-key: |
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZxxxABAAAAMwAAA
QyNTUxOQAAAxxxAAAIgIqewcCKn
HAAAAAtxxxDYC8YhlRDIhM+GUeg
-----END OPENSSH PRIVATE KEY-----
proxy-groups:
- name: company_g
type: select
proxies:
- company_container
rules:
- "IP-CIDR,10.0.11.0/24,company_g"
- "DOMAIN-SUFFIX,company.com,company_g"

k8s中使用, 打通命名空间中的网络

Section titled “k8s中使用, 打通命名空间中的网络”
## k8s等(有内部DNS功能的系统)用法
# 其他配置省略
rules:
- "IP-CIDR,10.0.11.0/24,company_g"
- "IP-CIDR,100.20.0.0/16,company_g"
- "IP-CIDR,100.19.0.0/16,company_g"
- "DOMAIN-SUFFIX,svc.cluster.local,company_g"
- "DOMAIN-SUFFIX,company.com,company_g"
  • 说明: 这里挂载了三个网段
    • 其中, 第一个网段是需要代理其他正常流量的网段
    • 第二和第三个网段, 则是命名空间中的容器使用的网段
      • 作用是, 让我们能通过k8s内部域名访问, k8s中容器提供的服务
        • 即通过svc.cluster.localk8s中内部域名访问

到这就基本完成了我们的目标了, 能够正常像在一个局域网中一样, 访问网页以及连接数据库等了

要是有什么问题, 欢迎留言交流

(frps selfhost版)有没想过起个容器就能打通整个内网呢? 使用容器打通受限网络: frp+ssh组合镜像以及clash(mihomo)实现打通网络(full)

使用容器打通受限网络: frp+ssh组合镜像以及clash(mihomo)实现打通任意环境所有内网服务(包括k8s)(web页面以及终端访问) 这里还有个需要提到的是, 因为我的科学上网环境是用的clash(mihomo)软件, 所以这里就用clash(mihomo)来实现打通各种内网 这样我就可以只需要一个代理软件就行了

开发过程中总是能遇到需要访问其他公司内网的情况, 一般常规方案都是由其他公司提供vpn访问, 或者jumpserver进行内网服务器连接.

这时, 一般就会遇到几个痛点: - 想要访问k8s中的容器服务, k8s内部域名svc.cluster.local无法直接使用 - 要装很多乱七八糟的vpn软件, 不同公司用的vpn可能就不一样, 有的vpn甚至像流氓软件一样, 有各种限制 - 有的公司不提供vpn或jumpserver, 只能去现场 - 家里网络和公司网络不互通的问题(在公司想访问家里的一些服务, 或在家想访问公司的一些服务等)

然而, 打通网络后, 都有哪些好处呢? - 自己本地电脑不在需要安转太多的vpn软件了 - 所能触碰到的每台内网服务器, 都能成为你在何时何地都能访问或当做跳板机的工具 - 内网才能打开的页面, 随时都能打开了 - 网页系统应用能用浏览器打开了 - nacos能用浏览器打开了 - 内网才能访问的服务器, 随时都能访问了 - 数据库能通过idea或者DBeaver可视化连接了

也就是说, 打通了网络, 也就打通了ssh访问, ssh能到达的地方, 都能将自己的本地电脑拉入到同一网络中进行互通操作

需要有机会把容器起起来, 一般有以下几种方式, 选一个方便去操作的就行, 当容器起来之后, 网络就打通了, 虚拟机等过渡工具就可以删掉了, 不需要了 - 通过jumpserver页面登录 - 自己本身就安转了vpn - 找安装了vpn的同事 - 专门找台机用于安装各种乱七八糟的vpn也行 - 当然起个虚拟机去安转也行

第一步: 编写Dockerfile, 用于制作镜像

Section titled “第一步: 编写Dockerfile, 用于制作镜像”

随便找台Linux系统, 或者自己电脑也行(就是麻烦点, 可能构建镜像时, 需要指定构建平台等), 我这里选择用x86架构的centos系统, 然后, 找个合适的目录, 例如: /www/container/frp-ssh

  • 创建Dockerfile文件
终端窗口
# vim Dockerfile
FROM debian:trixie-slim
WORKDIR /www
# 安装必要的软件包
RUN apt-get update && \
apt-get install -y openssh-server openssh-client curl wget locales gettext tini && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 生成并配置 locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen && \
update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
# 设置环境变量
ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LANGUAGE=en_US.UTF-8
# 解压frp
COPY ./frp_0.62.1_linux_amd64.tar.gz ./frp_0.62.1_linux_amd64.tar.gz
RUN tar -xzf ./frp_0.62.1_linux_amd64.tar.gz && \
mv frp_0.62.1_linux_amd64 frp
# 创建包装服务
RUN tee /www/frp/frpc.toml <<-'EOF'
serverAddr = ${serverAddr}
serverPort = ${serverPort}
[[${client_title}]]
name = ${client_name}
type = ${client_type}
${secretKey_stcp_line}
${localIP_proxies_line}
${localPort_proxies_line}
${serverName_visitors_line}
${bindAddr_visitors_line}
${bindPort_visitors_line}
${remotePort_proxies_tcp_line}
EOF
# 创建初始化脚本
RUN tee /entrypoint.sh <<-'EOF'
#!/bin/sh
# 替换环境变量的值
envsubst < /www/frp/frpc.toml > /tmp/frpc.toml.tmp && mv /tmp/frpc.toml.tmp /www/frp/frpc.toml
# 确保目录存在
[ ! -d "/var/run/sshd" ] && mkdir -p /var/run/sshd
# 启动 SSH(后台运行)
/usr/sbin/sshd -D &
# 启动 FRPC
/www/frp/frpc -c /www/frp/frpc.toml
# 保持容器运行
wait
EOF
RUN chmod +x /entrypoint.sh
# 暴露 SSH 端口
EXPOSE 22
# 使用 tini 作为 PID 1
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]

第二步: 编写docker-compose.yml, 方便构建和运行容器

Section titled “第二步: 编写docker-compose.yml, 方便构建和运行容器”
终端窗口
# vim docker-compose.yml
services:
dev-jumpbox:
build:
context: .
dockerfile: Dockerfile
container_name: dev-jumpbox
environment:
TZ: "Asia/Shanghai"
# 配置服务端的IP
serverAddr: '"129.204.8.8"'
client_title: proxies
serverPort: 7000
# 名称随便给, 不重复就行
client_name: '"dev-jumpbox-6666"'
client_type: '"tcp"'
localIP_proxies_line: localIP="127.0.0.1"
localPort_proxies_line: localPort=22
# 配置云服务器中开放的端口, 随便开放一个都行, 用于远程连接ssh
remotePort_proxies_tcp_line: remotePort=6666
extra_hosts:
- "me.host:host-gateway"
restart: unless-stopped
volumes:
- ./.ssh/authorized_keys:/root/.ssh/authorized_keys
# 以上环境变量, 除了备注的内容, 其他的都可以保持不动就行
# authorized_keys的内容示例
# ssh-ed25519 xxxxx xxx
  • authorized_keys的内容示例
终端窗口
# 在本地电脑中执行, 打印公钥
cat ~/.ssh/id_ed25519.pub
# 复制打印的公钥内容, 需要写入到`./.ssh/authorized_keys`, 这个文件是需要挂载到容器中的文件
  • 关于卷(volumes)的说明

这里的卷也可以不挂载, 也可以通过进入容器中执行命令去写入authorized_keys文件中

终端窗口
# 使用挂载券方式时, 这一步可省略
echo 'ssh-ed25519 xxxxx xxx' > /root/.ssh/authorized_keys

第三步: 需要一台有公网IP的云服务器(2c2g1m就差不多了, 我的镜像是Debian)

Section titled “第三步: 需要一台有公网IP的云服务器(2c2g1m就差不多了, 我的镜像是Debian)”

frp官网安装地址 frp官方GitHub下载地址

终端窗口
# 下载下来解压
tar -xzf ./frp_0.62.1_linux_amd64.tar.gz && mv frp_0.62.1_linux_amd64 frp
# 进入解压后的目录, 启动frp服务端, 设置开机自启
cd frp && systemctl start frps && systemctl enable frps

然后, 开放下端口, 例如: 开放端口:6666, 用于远程连接ssh

第四步: 启动docker-compose, 测试容器以及上传进行

Section titled “第四步: 启动docker-compose, 测试容器以及上传进行”
终端窗口
# 构建镜像并启动容器
docker-compose up -d
  • 使用ssh远程连接下, 试试效果
终端窗口
# 上传公钥, 开启免密登录, 这一步也是顺便检查了是否能够正常通过内网穿透ssh到容器中
ssh-copy-id root@129.204.8.8 -p 6666 -i ~/.ssh/id_ed25519
# 然后, 通过ssh免密登录
ssh root@129.204.8.8 -p 6666

到这就已经基本完成了在任何地方都能联通ssh了, 接下来的就是简化配置, 以及高级应用了

上传镜像到阿里云, 简化启动容器的配置

Section titled “上传镜像到阿里云, 简化启动容器的配置”
终端窗口
# 登录
docker login --username=xxx@qq.com registry.cn-hangzhou.aliyuncs.com
## 标记本地镜像并指向目标仓库(ip:port/image_name:tag,该格式为标记版本号)
docker tag dev-jumpbox registry.cn-hangzhou.aliyuncs.com/xxx/dev-jumpbox:frpc-ssh
## 推送镜像到仓库
docker push registry.cn-hangzhou.aliyuncs.com/xxx/dev-jumpbox:frpc-ssh

简化后, 就只需要docker-compose配置即可, 当然, 如果没有将authorized_keys配置整合到Dockerfile中的情况下, 还是需要挂载配置的

参考详情链接

终端窗口
services:
dev-jumpbox:
image: registry.cn-hangzhou.aliyuncs.com/iuin/dev-jumpbox:tcpmux-v6.1.1
container_name: dev-jumpbox
environment:
TZ: "Asia/Shanghai"
serverAddr: '"183.11.11.11"'
serverPort: 11100
auth_token: '"xx-jumpbox-ssh"'
client_name: '"container.prod.xxx.customer"'
customDomains: '["container.prod.xxx.customer"]'
restart: unless-stopped

配合clash(mihomo)工具使用, 方便访问网页

Section titled “配合clash(mihomo)工具使用, 方便访问网页”

这里, 我们借助clash(mihomo)工具, 通过ssh将流量代理转发到容器中, 实现像访问局域网一样访问容器那边的对应内网上的网页

终端窗口
# 代理流量(script.js[这里用了全局脚本的方式, 兼容自己的原有的订阅, 不影响原有的订阅, 只做扩展])
function main(config, profileName) {
const extra = {
proxies: [
{
name: "company_container",
type: "ssh",
server: "183.11.11.11",
port: 11111,
username: "root",
"private-key": "./.ssh/id_ed25519_iu"
}
],
proxyGroups: [
{
name: "company_g",
type: "select",
proxies: ["DIRECT", "company_container"]
}
],
rules: [
"IP-CIDR,10.0.11.0/24,company_g",
"DOMAIN-SUFFIX,company.com,company_g"
]
};
if (!Array.isArray(config.proxies)) config.proxies = [];
for (const p of extra.proxies) {
if (p && p.name && !config.proxies.some(x => x && x.name === p.name)) {
config.proxies.unshift(p);
}
}
if (!Array.isArray(config["proxy-groups"])) config["proxy-groups"] = [];
for (const g of extra.proxyGroups) {
let existing = config["proxy-groups"].find(x => x && x.name === g.name);
if (!existing) {
config["proxy-groups"].unshift({ name: g.name, type: g.type, proxies: Array.isArray(g.proxies) ? [...g.proxies] : [] });
} else {
if (!Array.isArray(existing.proxies)) existing.proxies = [];
for (const pn of g.proxies || []) {
if (!existing.proxies.includes(pn)) existing.proxies.unshift(pn);
}
}
}
if (!Array.isArray(config.rules)) config.rules = [];
for (const r of extra.rules) {
if (!config.rules.includes(r)) config.rules.unshift(r);
}
return config;
}
# 单独订阅配置(ssh.yml)
proxies:
- name: company_container
type: ssh
server: 183.11.11.11
port: 11111
username: root
private-key: ./.ssh/id_ed25519_iu
proxy-groups:
- name: company_g
type: select
proxies:
- company_container
rules:
- "IP-CIDR,10.0.11.0/24,company_g"
- "DOMAIN-SUFFIX,company.com,company_g"

k8s中使用, 打通命名空间中的网络

Section titled “k8s中使用, 打通命名空间中的网络”
## k8s等(有内部DNS功能的系统)用法
# 其他配置省略
rules:
- "IP-CIDR,10.0.11.0/24,company_g"
- "IP-CIDR,100.20.0.0/16,company_g"
- "IP-CIDR,100.19.0.0/16,company_g"
- "DOMAIN-SUFFIX,svc.cluster.local,company_g"
- "DOMAIN-SUFFIX,company.com,company_g"
  • 说明: 这里挂载了三个网段
    • 其中, 第一个网段是需要代理其他正常流量的网段
    • 第二和第三个网段, 则是命名空间中的容器使用的网段
      • 作用是, 让我们能通过k8s内部域名访问, k8s中容器提供的服务
        • 即通过svc.cluster.localk8s中内部域名访问

到这就基本完成了我们的目标了, 能够正常像在一个局域网中一样, 访问网页以及连接数据库等了