利用SSH隧道技术穿越内网访问远程设备

本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/11899478.html

通常,我们用于调试的计算机无法远程访问位于局域网中的待调试设备。通过 ssh 的端口转发(又称 ssh 隧道)技术,可以实现这种远程调试功能。

下文中,sshc 指 ssh 客户端,sshd 指 ssh 服务器。

1. ssh 端口转发模式简介

ssh 客户端运行于本地机器,它的作用是:登录到目标机器并在目标机器上执行命令。它可以建立一个安全通道,为不安全网络上两个不受信任的主机提供安全的加密通信。X11 连接、任意 TCP 端口和 UNIX 域套接字也可以通过 ssh 安全通道进行转发。

ssh 连接并登录到指定的主机名(用户名可选)。如果指定了命令,命令将在远程主机上执行,而不是在本机 shell 里执行。

1.1 ssh 常用选项简介

ssh 端口转发相关的常用选项如下:


-C

请求压缩所有数据(包括 stdin、stdout、stderr 和用于转发的 X11、TCP 和 UNIX 域连接的数据)。压缩算法与 gzip 使用的算法相同,压缩级别由 ssh 协议版本 1 的 CompressionLevel 选项控制。在调制解调器线路和其他慢速连接上采用压缩是可取的,但它会减慢快速网络上的速度。


-f

请求 ssh 在执行命令之前转到后台。如果用户希望 ssh 在后台运行,但 ssh 需要用户提供密码或口令,使用 -f 选项就很有用,在用户输入密码之后,ssh 就会转入后台运行。这个选项隐含了 -n 选项的功能(-n 选项将 stdin 重定向到 /dev/null,从而避免后台进程读 stdin)。在远程站点上启动 X11 程序的推荐方法是使用 "ssh -f host xterm" 。

如果 ExitOnForwardFailure 配置选项设置的是 "yes",则使用 -f 选项启动的 ssh 客户端会等所有的远程端口转发建立成功后才将自己转到后台运行。


-n

将 stdin 重定向到 /dev/null (实际上是为了防止后台进程从stdin读取数据)。当 ssh 在后台运行时必须使用此选项。

一个常见的技巧是使用它在目标机器上运行 X11 程序。例如,ssh -n shadow.cs.hut.fi emacs & 将在 shadows.cs.hut.fi 上启动 emacs 程序。X11 的连接将通过加密通道自动转发。ssh 程序将在后台运行。(如果 ssh 需要请求密码或口令,则此操作无效;参见-f选项。)


-N

不执行远程命令。此选项用于只需要端口转发功能时。


-g

允许远程主机连接到本地转发端口。如果用于多路复用连接,则必须在主进程上指定此选项。


-t

强制分配一个伪终端。在目标机上执行任意的基于屏幕的程序时(例如,实现菜单服务),分配伪终端很有用。使用多个 -t 选项则会强制分配终端,即使 ssh 没有本地终端。


-T

禁止分配伪终端。


-L [bind_address:]port:host:hostport
-L [bind_address:]port:remote_socket
-L local_socket:host:hostport
-L local_socket:remote_socket

数据从本机转发到远程。本机上指定 TCP 端口或 UNIX 套接字的连接将被转发到目标机上指定端口或套接字。

上述参数中,bind_address 指本地地址;port 指本地端口;local_socket 指本地 UNIX 套接字;host 指远程主机地址;hostport 指远程端口;remote_socket 指远程 UNIX 套接字。

本地(ssh 客户端)与远程(ssh 服务端)建立一条连接,此连接的形式有四种:

本地 [bind_address:]port    <====>   远程 host:hostport  
本地 [bind_address:]port    <====>   远程 remote_socket  
本地 local_socket           <====>   远程 host:hostport  
本地 local_socket           <====>   远程 remote_socket  

位于本机的 ssh 客户端会分配一个套接字来监听本地 TCP 端口(port),此套接字可绑定本机地址(bind_address, 可选,本机不同网卡具有不同的 IP 地址)或本地 UNIX 套接字(local_socket)。每当一个连接建立于本地端口或本地套接字时,此连接就会通过安全通道进行转发。

也可在配置文件中设置端口转发功能。只有超级用户可以转发特权端口。

默认情况下,本地端口是根据 GatewayPorts 设置选项绑定的。但是,使用显式的bind_address 可将连接绑定到指定地址。bind_address 值是 “localhost”时,表示仅监听本机内部数据[TODO: 待验证],值为空或“*”时,表示监听本机所有网卡的监听端口。

注意:localhost 是个域名,不是地址,它可以被配置为任意的 IP 地址,不过通常情况下都指向 127.0.0.1(ipv4)和 ::1。127.0.0.1 这个地址通常分配给 loopback 接口。loopback 是一个特殊的网络接口(可理解成虚拟网卡),用于本机中各个应用之间的网络交互。

GatewayPorts 说明 (查阅 man sshd_config):指定是否允许远程主机(ssh客户端)连接到本机(ssh服务端)转发端口。默认情况下,sshd(8)将远程端口转发绑定到环回地址,这将阻止其他远程主机连接到本机转发端口。GatewayPorts 也可设置为将将远程端口转发绑定到非环回地址,从而允许其他远程主机连接到本机。GatewayPorts 值“no”,表示强制远程端口转发仅对本机可用;值“yes”,表示强制远程端口转发绑定到通配符地址;值“clientspecified”,表示允许客户端选择转发绑定到的地址。默认是“no”。


-R [bind_address:]port:host:hostport
-R [bind_address:]port:local_socket
-R remote_socket:host:hostport
-R remote_socket:local_socket

此选项在本地机上执行,目标机上指定 TCP 端口或 UNIX 套接字的连接将被转发到本机上指定端口或套接字。

上述参数中,bind_address 指远程地址;port 指远程端口;remote_socket 指远程 UNIX 套接字;host 指本地地址;hostport 指本地端口;local_socket 指本地 UNIX 套接字。

工作原理:位于远程的 ssh 服务端会分配一个套接字来监听 TCP 端口或 UNIX 套接字。当目标机(服务端)上有新的连接建立时,此连接会通过安全通道进行转发,本地机执行当前命令的进程收到此转发的连接后,会在本机内部新建一条 ssh 连接,连接到当前选项中指定的端口或套接字。参 2.3 节分析。

也可在配置文件中设置端口转发功能。只有超级用户可以转发特权端口。

默认情况下,目标机(服务端)上的 TCP 监听套接字只绑定回环接口。也可将目标机上的监听套接字绑定指定的 bind_address 地址。bind_address 值为空或 “*” 时,表示目标机上的监听套接字会监听目标机上的所有网络接口。仅当目标机上 GatewayPorts 设置选项使能时,通过此选项为目标机指定 bind_address 才能绑定成功(参考 sshd_config(5))。

如果 port 参数是 ‘0’,目标机(服务端)可在运行时动态分配监听端口并通知本地机(客户端),如果同时指定了 “-O forward” 选项,则动态分配的监听端口会被打印在标准输出上。


-D [bind_address:]port

指定本地“动态”应用程序级端口转发。它的工作方式是分配一个套接字来监听本地端口(可选绑定指定的 bind_address)。每当连接到此端口时,连接都通过安全通道进行转发,然后使用应用程序协议确定将远程计算机连接到何处。目前支持 SOCKS4 和 SOCKS5 协议,ssh 将充当 SOCKS 服务器。只有 root 用户可以转发特权端口。还可以在配置文件中指定动态端口转发。

IPv6 地址可以通过将地址括在方括号中来指定。只有超级用户可以转发特权端口。默认情况下,本地端口是根据 GatewayPorts 设置选项进行绑定的。但是,可以使用显式的 bind_address 将连接绑定到特定的地址。bind_address 值为 “localhost” 时表示监听端口仅绑定为本地使用,而空地址或 “*” 表示监听所有网络接口的此端口。


1.2 ssh 端口转发模式

ssh 的端口转发有三种模式:

  • 本地:ssh -C -f -N -g -L local_listen_port:remote_host:remote_port agent_user@agent_host

    将本地机监听端口 local_listen_port 上的数据转发到远程端口 remote_host:remote_port

  • 远程:ssh -C -f -N -g -R agent_listen_port:local_host:local_port agent_user@agent_host

    将代理机监听端口 agent_listen_port 上的数据转发到本地端口 local_host:local_port

  • 动态:ssh -C -f -N -g -D listen_port agent_user@agent_host

2. 利用 ssh 隧道建立远程调试环境

组网环境下设备角色如下:

代理机:把一个具有公网 IP 的中间服务器用作 ssh 代理,将这台代理机称作代理 A(Agent)。

目标机:把待调试的目标机器称作目标机 T(Target)。目标机通常是待调试的设备,处于局域网内,外网无法直接访问内网中的设备。

本地机:把调试用的本地计算机称作本地机 L(Local)。本地机通常也位于局域网内。

ssh隧道组网示意图

L 和 T 无法互相访问,但 L 和 T 都能访问 A。我们将 T 通过 ssh 连接到A,将 L 也通过 ssh 连接到A,A 用于转发数据,这样就能使用本地计算机 L 来访问远端设备 R。

2.1 目标机 T (sshc)

2.1.1 shell 中 T 连接 A

目标机 T 上的 sshc 连接代理机 A 上的 sshd:

ssh -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

这条命令的作用:
1. 建立一条 ssh 连接,T 上的 ssh 客户端连接到 A 上的 ssh 服务器,A 的 IP 是 120.198.45.126,端口号是 10022,账号是10022;
2. 如果有其他 ssh 客户端连接到了 A 的 10022 端口上,则 A 会将这条连接转发到 T,T 在内部建立新的连接,连接到本机 22 端口。

这条命令在 T 上执行。在 T 连接 A 这条命令里,T 是本地主机(local),A 是远程主机(remote)。

解释一下此命令各选项:

  • -T 不分配伪终端;
  • -f 使 ssh 进程在用户输入密码之后转入后台运行;
  • -N 不执行远程指令,即远程主机(代理机A)不需执行指令,只作端口转发;
  • -g 允许远程主机(代理机A)连接到本地转发端口;
  • -R 将远程主机(代理机A)指定端口上的连接转发到本机端口;
  • frank@120.198.45.126
    表示使用远程主机 120.198.45.126 上的用户 frank 来连接远程主机;
  • :10022:127.0.0.1:22
    表示本机回环接口(127.0.0.1,也可使用本机其他网络接口的地址,比如以太网 IP 或 WiFi IP)的 22 端口连接到远程主机的 10022 接口,因远程主机 10022 绑定的地址为空,所以远程主机会监听其所有网络接口的 10022 端口。

在目标机 shell 中查看连接是否建立:

root@localhost:~# ps | grep ssh
22850 root      2492 S    ssh -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126
22894 root      3500 S    grep ssh

在目标机 shell 中关闭 ssh 连接:

kill -9 $(pidof ssh)

此时在目标机 T 和代理机 A 中查看 ssh 连接信息,两端都可以看到 ssh 连接不存在了。

2.1.2 C 代码中 T 连接 A 的处理

C 代码中主要还是调用 2.1.1 节中的命令。但是由 C 代码编译生成的进程无法在命令行和用户进行交互,因此要避免交互问题。

1. 避免首次连接时的 y/n(或yes/no) 询问

如果是首次登录代理机 A,本机(目标机 T)没有 A 的信息,需用用户手动输入 y 之后才能继续。打印如下:

root@localhost:~# ssh -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

Host '120.198.45.126' is not in the trusted hosts file.
(ssh-rsa fingerprint md5 86:09:0c:1b:fd:0b:02:8c:29:62:7f:ff:70:1b:64:f5)
Do you want to continue connecting? (y/n) 

如果 T 上有 A 的信息,可通过执行删除操作:rm ~/.ssh/known_hosts 再进行上述测试。

如果是在 C 代码中执行登录命令,进程在后台自动运行,是无法和用户进行交互的。为了避免交互动作,应该禁止 ssh 发出 y/n 的询问。

如果 ssh 客户端是 dropbear ssh,则添加 -y 参数,如下:

ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

如果 ssh 客户端是 openssh,则添加 -o StrictHostKeyChecking=no 选项,如下:

ssh -o "StrictHostKeyChecking no" -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

2. 避免输入登录密码

避免由用户手动输入登录密码有如下方法:

1) 用 ssh-copy-id 把本地主机的公钥复制到远程主机的authorized_keys文件上,登录不需要输入密码。
2) 用 expect 调用 shell 脚本,向 shell 脚本发送密码。这种方式是模拟键盘输入。
3) 如果是 openssh,则用 sshpass 向 ssh 命令行传递密码。如果是 dropbear,则通过 DROPBEAR_PASSWORD 环境变量向 ssh 命令行传递密码。

我们采用第 3 种方法。

假如代理机 A 上用户 frank 密码是 123456,则最终 C 代码里应执行的指令如下:

# openssh
sshpass -p '123456' ssh -o "StrictHostKeyChecking no" -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

# dropbear
DROPBEAR_PASSWORD='123456' ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

dropbear 无法接收 DROPBEAR_PASSWORD 变量传递密码的处理方法:

dropbear 包含 ssh 客户端和 ssh 服务器,体积小巧,常用于嵌入式设备。dropbear ssh 无法接收 sshpass 传入的密码信息。但 dropbear ssh 可以通过环境变量 DROPBEAR_PASSWORD 传入密码信息。openwrt 从某一版开始,通过打补丁的方式禁用了 DROPBEAR_PASSWORD 选项,我们可以找到对应的补丁,开启 DROPBEAR_PASSWORD 选项,再重新编译生成 dropbear。如下:

修改 dropbear patch 文件(如下路径位于 openwrt 源码根目录):

vim package/network/services/dropbear/patches/120-openwrt_options.patch

将如下几行删除:

@@ -226,7 +226,7 @@ much traffic. */
  * note that it will be provided for all "hidden" client-interactive
  * style prompts - if you want something more sophisticated, use 
  * SSH_ASKPASS instead. Comment out this var to remove this functionality.*/
-#define DROPBEAR_PASSWORD_ENV "DROPBEAR_PASSWORD"
+/*#define DROPBEAR_PASSWORD_ENV "DROPBEAR_PASSWORD"*/
 
 /* Define this (as well as ENABLE_CLI_PASSWORD_AUTH) to allow the use of
  * a helper program for the ssh client. The helper program should be

重新编译生成 dropbear,并替换设备里已安装的 dropbear。

#define DEFAULT_SSH_AGENT_HOST      "120.198.45.126"
#define DEFAULT_SSH_AGENT_PORT      "10022"
#define DEFAULT_SSH_AGENT_USER      "ssha_debug"
#define DEFAULT_SSH_AGENT_PASSWD    "220011ssha"
int login_to_ssh_agent(const char *host, const char *port, const char *user, const char *passwd)
{
    // openssh client:
    // sshpass -p '123456' ssh -o "StrictHostKeyChecking no" -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

    // dropbear ssh clent:
    // DROPBEAR_PASSWORD='123456' ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

    char cmd[256];
    snprintf(cmd, sizeof(cmd), "DROPBEAR_PASSWORD='%s' ssh -y -T -f -N -g -R :%s:127.0.0.1:22 %s@%s", 
             (passwd != NULL) ? passwd : DEFAULT_SSH_AGENT_PASSWD, 
             (port != NULL) ? port : DEFAULT_SSH_AGENT_PORT,
             (user != NULL) ? user : DEFAULT_SSH_AGENT_USER,
             (host != NULL) ? host : DEFAULT_SSH_AGENT_HOST);
    printf("login to ssh agent: \n%s\n", cmd);
    system(cmd);

    return 0;
}

2.2 代理机 A (sshd)

在 /etc/ssh/sshd_config 中添加如下几行后重启 ssh 服务:

GatewayPorts yes
UseDNS no
GSSAPIAuthentication no

目标机 T 发起连接后,在代理机 A 上查询目标机 T 是否连接成功:

sudo netstat -anp | grep 10022

打印形如:

tcp        0      0 0.0.0.0:10022           0.0.0.0:*               LISTEN      8264/sshd: frank
tcp6       0      0 :::10022                :::*                    LISTEN      8264/sshd: frank

上述打印中,8264 就是和目标机 T 保持连接的 sshd 进程号,如需关闭当前连接重新建立一个新的连接,则先在代理机 A 上执行:

kill -9 8264

然后再执行 2.1 节的指令,就会建立一次新的代理连接。

为了安全,我们可以专门新建一个用户,仅用于 ssh 端口转发功能,不能在 shell 中使用此用户登录。如下创建一个 ssha_debug 的用户,无 shell 登录权限。然后为此用户创建密码。注意系统中 nologin 文件的位置,不同系统可能路径不同。

sudo useradd ssha_debug -M -s /usr/sbin/nologin
sudo passwd ssha_debug

2.3 本地机 L (sshc)

2.3.1 本地机 L 登录目标机 T

有三种方式:

1. 在本地机 L 上通过 ssh 登录代理机 A,在 A 的 shell 中再登录目标机 T

代理服务器的公网 ip 是 120.198.45.126,内网 ip 是 192.168.1.102。

1) 先使用 ssh(SecureCRT 或 OpenSSH 命令行) 登录上代理服务器的 shell。如果调试机在内网,既可登录代理机的外网 ip,也可登录其内网 ip。

2) 在代理机的 shell 中执行如下命令登录远程设备:

ssh -p 10022 root@127.0.0.1 -vvv

注意,此命令中用户 root 及其密码是远程设备上的账户。

如果提示 Host key 认证失败之类的信息,请按提示执行如下命令:

ssh-keygen -f "/home/frank/.ssh/known_hosts" -R [127.0.0.1]:10022

也可直接删除当前用户目录下的 .ssh/known_hosts 文件。
然后重新执行登录设备操作。

建议优先使用此方法。

2. 在本地机 L 上使用 ssh 命令登录目标机 T

Win 10 系统默认安装有 OpenSSH 客户端。可以在调试机 Windows 命令行中执行:

ssh -p 10022 root@120.198.45.126 -vvv

对于本地计算机来说,待调试的设备 ip 地址不可见。本机登录到代理机 120.198.45.126 的转发端口 10022,通过代理机转发功能,本地机能成功登录到远程设备上。注意,此命令中用户 root 及其密码是设备上的账户,不是 SSH 代理服务器上的账户。

如果出现认证失败之类的信息。可删除 C:/Users/当前用户/.ssh/known_hosts 文件,然后再试。

3. 在本地机 L 上使用 SecureCRT 工具登录目标机 T

也可以直接使用 SecureCRT 软件,设置好代理机的 ip(120.198.45.126) 和端口号(10022),填上设备的登录用户和登录密码。

不建议使用此方法。因为连接过程太长或连接失败的话,无法看到错误提示信息。

2.3.2 查看代理机 A 打印信息

在 L 执行登录 T 之前查看打印信息:

frank@SERVER:~$ sudo netstat -anp  | grep 10022
tcp        0      0 0.0.0.0:10022           0.0.0.0:*               LISTEN      106438/sshd: frank
tcp6       0      0 :::10022                :::*                    LISTEN      106438/sshd: frank

在 L 执行登录 T 之后查看打印信息:

frank@SERVER:~$ sudo netstat -anp  | grep 10022
tcp        0      0 0.0.0.0:10022           0.0.0.0:*               LISTEN      106438/sshd: frank
tcp        0      0 192.168.1.102:10022     120.229.163.51:27027    ESTABLISHED 106438/sshd: frank
tcp6       0      0 :::10022                :::*                    LISTEN      106438/sshd: frank

可以看到,上述第二行是 L 执行登录命令后新出现的打印信息。表示新建立了一条 L 到 A 的 ssh 连接。

L 端的外网地址 120.229.163.51:27027 连接到 A 上的 192.168.1.102:10022,L 通常位于局域网内、具有一个内网地址,120.229.163.51 可能是 L 连接的路由器的公网 IP。

这条连接建立后,A 将这条连接转发到 R。

2.3.3 查看目标机 T 打印信息

在 L 执行登录 T 之前查看打印信息:

root@localhost:~# netstat -anp | grep 22
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      917/sshd
tcp        0      0 192.168.202.140:47989   120.198.45.126:22       ESTABLISHED 9452/ssh
tcp        0      0 192.168.202.140:22      192.168.202.100:64737   ESTABLISHED 2041/sshd
tcp        0      0 :::22                   :::*                    LISTEN      917/sshd

在 L 执行登录 T 之后查看打印信息:

root@localhost:~# netstat -anp | grep 22
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      917/sshd
tcp        0      0 192.168.202.140:47989   120.198.45.126:22       ESTABLISHED 9452/ssh
tcp        0      0 192.168.202.140:51732   192.168.202.140:22      ESTABLISHED 9452/ssh
tcp        0      0 192.168.202.140:22      192.168.202.140:51732   ESTABLISHED 9579/sshd
tcp        0      0 :::22                   :::*                    LISTEN      917/sshd

可以看到,上述第 3 行和第 4 行是登录之后新增加的打印信息。

第 2 行,表示 T 上的 ssh 客户端连接到了 A 上的 ssh 服务端,进程号是 9452。第 3 行,表示进程 9452 收到了 A 转发来的 ssh 连接后,在本机内部建立新的 ssh 连接,使用 51732 端口号作为 ssh 客户端,连接到本机 22 端口,22 端口是 sshd 端口。第 4 行,表示本机新启动一个 sshd 进程,来接收 sshc 的连接。

这样,L 到 T 的 ssh 通路彻底打通了。A 将来自 L 的连接转发到 R,R 在内部启动了 sshd 来处理来自 L 的请求,通过 A 的代理作用,实现了 L 上的 sshc 和 T 上的 sshd 的交互。

3. 典型使用场景步骤总结

上文已涵盖详细使用方法,但篇幅太长。此处简单总结使用步骤如下:

3.1 在代理机 A 上执行

使用 SecureCRT 登录代理机 A。代理机外网 ip 120.198.45.126,内网 ip 192.168.1.102,端口 22。如果本地机与代理机在同一个局域网里,使用代理机的内网 ip 登录即可。

在代理机 shell 中查看是否有未关闭的 ssh 隧道:

sudo netstat -anp | grep 10022

若打印形如:

tcp        0      0 0.0.0.0:10022           0.0.0.0:*               LISTEN      8264/sshd: frank
tcp6       0      0 :::10022                :::*                    LISTEN      8264/sshd: frank

则表示有未关闭的 ssh 隧道连接。执行如下命令可关闭连接。

kill -9 8264

3.2 在目标机 T 上执行

使用远程应用程序接口或者在远程设备 T 上做一些特殊操作,触发 T 执行如下两条指令之一:

# openssh
sshpass -p '123456' ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

# dropbear
DROPBEAR_PASSWORD='123456' ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126

3.3 在本地机 L 上执行

在本地机 L 上执行如下指令,登录远程目标机 T:

ssh -vvv -p 10022 root@120.198.45.126

另外一种变通的方式是,在本地机先通过 ssh 登录上代理机 A 的 shell。然后在 A 的 shell 中执行如下指令:

ssh -vvv -p 10022 root@127.0.0.1

4. 注意事项

1. 确保代理机 A 所在的网络防火墙不屏蔽 10022 端口
2. 确保代理机 A 上 /etc/ssh/sshd_config 配置文件设置正确
3. 关闭 ssh 隧道既可在代理机 A 上进行(关闭相应的 sshd 进程),也可在目标机 T 上进行(关闭相应的 ssh 进程)
4. 每次只能访问一台目标机。如果想同时访问多台,可以代理机上设置多个转发端口,每条连接使用一个端口进行转发
5. 为保证安全,打开 ssh 隧道时尽量使用无登录权限的用户,并且此用户的密码建议经常更新

5. 参考资料

[1] 阮一峰, SSH原理与运用(二):远程操作与端口转发
[2] SSH 代理设置
[3] ssh命令和SSH服务详解

6. 修改记录

2019-11-20 V1.0 初稿

posted @ 2019-11-21 09:53  叶余  阅读(4810)  评论(0编辑  收藏  举报