tmux 分享内网终端

30 Apr 2018

tmux 有个共享会话的功能,利用这个功能可以解决多人协作场景中终端分享问题:比如结对编程、远程协助、直播写代码……

在一台服务器上直接共享服务器上的 tmux 会话的例子已经有很多了, 实际操作可以参考使用 tmux 结对编程Remote Pair Programming Made Easy with SSH and tmux

如果要分享 NAT 内部的机器终端给不同 NAT 背后的电脑操作或者观看,可以实现吗? 考虑到一位来自远古时期的开发者 Alice,她只在终端上开发,突然一天要转行做网红程序员了,就可能会有直播写代码的需求,而且她希望操作的是自己本地电脑的开发环境终端,直播分享给遍布全世界的拥趸观看。这时上面介绍的分享公网服务器上 tmux 会话的操作就行不通了。

我对这类位于不同 NAT 内部的机器共享终端的问题想了下,发现结合 tmux 和反向 shell 是可以解决的,再对期间可能出现的权限问题进行分析,利用在跳板机新增一个中间用户及 tmux 的只读模式的方法,可以实现分享出去的终端对其余用户只能观看,无法获取写的操作权限,从而保证分享者主机的安全。

首先,简单说下一些常用的反向 shell 的方法,它是分享 NAT 内网终端的前提。

反向 shell

假设跳板机服务器地址为 ServerIP, Alice 的内网机器地址为 AliceIP, 要实现的是在服务器控制 Alice 的终端,思路就是在服务器运行一个终端,并监听一个 TCP 端口,它的标准输入重定向之后建立的 TCP 连接;Alice 的机器也运行一个终端,并连接服务器端口,它的标准输入、标准输出、标准出错都重定向到开始时建立的那个 TCP 连接,这样服务器就能获取到 Alice 的 shell 了。

实现方法就很多了,如:

  • 使用 netcat 和 bash

    在服务器监听 3333 端口:

    nc -nl 3333
    

    Alice 机器运行:

    bash -c "bash -i >/dev/tcp/ServerIP/3333 2>&1 <&1"
    

    之后服务器就可以操作 Alice 机器了。 这种方法很简单原始,但缺点很明显:不支持颜色高亮及格式化输出,不支持触发信号的按键捕获,不是一个合格的 TTY。如果在上面打开 vim 就会得到 “Vim: Warning: Output is not to a terminal”、”Vim: Warning: Input is not from a terminal” 的警告,根本不能用来写代码。

  • 使用 socat

    在服务器监听 3333 端口:

    socat file:`tty`,raw,echo=0 tcp-listen:3333 
    

    Alice 机器运行:

    socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:ServerIP:3333
    
  • SSH 反向 shell

    Alice 机器运行:

    ssh -N -R 3333:localhost:22 alice@ServerIP
    

    服务器运行:

    ssh localhost -p 3333
    

    ssh 支持反向隧道,安全可靠,但要求 Alice 机器也开启 ssh 服务 sshd。下文将采用这种反向 shell 方式。

    借助反向 shell,就可以将 Alice 机器的终端分享出去了:

分享内网终端

  1. Alice 通过反向 shell 控制自己机器:

    Alice 机器运行:

    ssh -N -R 3333:localhost:22 alice@ServerIP
    

    然后通过 ssh 方式登录服务器,在服务器上新建一个名为 alice_live_session 的 tmux 会话,并在该会话连接 Alice 内网机器的终端:

    tmux -S /tmp/alice_socket new -s alice_live_session
    ssh localhost -p 3333
    

    之后就能在这个 alice_live_session tmux 会话操作自己机器了。

  2. 观众 Bob 获取 Alice 共享的 alice_live_session

    先介绍一种对 Alice 来说是不安全的共享方式,后面再谈如何解决。

    # 1. 在服务器为 Bob 用户加入 alice 所在用户组,
    # 以获取 tmux 会话 socket 文件 /tmp/alice_socket 的
    # 读写权限(连接 tmux 会话必须要有该 socket 文件的读写权限):
    sudo usermod -aG alice bob
    # 2. Bob 以 tmux 的只读方式连接 `alice_live_session`, 
    # 从而观看 Alice 的实时直播终端:
    tmux -S /tmp/alice_socket attach -t alice_live_session -r
    

    这里的安全问题是:Bob 选择只读方式完全靠自觉,要是 Bob 不加上只读选项 “-r”,连接时就获得了和 Alice 一样的 shell, 随时可以干扰 Alice 的终端操作,Alice 不终止该会话的话,Bob 可以对 Alice 机器进行任何操作,这只适合于结对编程等多人协作场景。

    解决方式是 Alice 在服务端增加一个中间人用户 Carol,Carol 也开启一个新的 socket 方式的 tmux 会话 share_live_session,该 socket 文件对任何用户都可读可写,在这个 share_live_session 会话中,以只读的方式连接 alice_live_session(嵌套的 tmux),这样任何其他用户都可以连接 share_live_session 间接地观看到一个只读的 alice_live_session,而这些用户对 /tmp/alice_socket 都没有读写权限,无法获取一个对 alice_live_session 可写的权限,从而 Alice 可以放心地直播了。

    具体操作如下:

    # 1. 在服务器增加 carol 用户,并加入 alice 用户组:
    sudo adduser carol
    sudo usermod -aG alice carol
    # 2. 通过 carol 用户新建 tmux 会话:
    tmux -S /tmp/carol_socket attach new -s share_live_session
    # 3. 给所有用户分配 /tmp/carol_socket 的读写权限:
    chmod a+rw /tmp/carol
    # 4. 在 `share_live_session` 里以只读方式连接 `alice_live_session`:
    tmux -S /tmp/alice_socket attach -t alice_live_session -r
    # 5. Bob 连接 `share_live_session`,获取到只读的 `alice_live_session`:
    tmux -S /tmp/carol_socket a -t share_live_session -r
    

目前有许多类似的工具专门实现 NAT 背后的终端共享,比如 tmate, teleconsole, Robo-TiTO,其实现有的通用工具也是可以解决的,有时不过是组合起来麻烦一点罢了。想起 David Frank 说过的一句话:现在造轮子的理由不再是「那个轮子不够好」,而是「那个轮子功能太多了」。

Update: 如果还想把终端共享到网页去的话,还可以使用 GoTTYttyd

# 分享者执行:
gotty tmux new -A -s gotty
tmux new -A -s gotty
# 浏览器打开:
open http://ServerIP:ServerPort/

参考链接: