ssh proxycommand on ssr

如果你想ssh到某个国外机器上的话,因为一些众所周知的原因,链接会非常不稳定或者是被切断。ssh有个"proxycommand"选项,允许配置代理来完成连接。

如果我们使用ssr做socks5代理服务器的话,假设绑定在62221端口上,那么有两种方式来使用代理进行连接。一种是在命令行里面指定

➜  private git:(master) ✗ ssh -o ProxyCommand="nc -x 127.0.0.1:62221 -X 5 %h %p" app0
Warning: Permanently added 'app0' (ECDSA) to the list of known hosts.
Last login: Tue Apr 17 02:42:58 2018 from dev0

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2015.09-release-notes/
123 package(s) needed for security, out of 246 available
Run "sudo yum update" to apply all updates.
Amazon Linux version 2017.09 is available.

这种方式不太好的地方是,每次都需要在命令行里面指定,并且不能和其他工具比如rsync/scp共享。如果写在配置文件里面的话就方便多了

➜  private git:(master) ✗ cat ~/.ssh/config
# Good articles
# http://blogs.perl.org/users/smylers/2011/08/ssh-productivity-tips.html

TCPKeepAlive no
ServerAliveInterval 10
ServerAliveCountMax 6

ForwardAgent yes
UserKnownHostsFile /dev/null
StrictHostKeyChecking no

ControlMaster auto
ControlPath ~/.ssh-%r@%h:%p
ControlPersist 4h

Host app0 dev0
User ec2-user
ProxyCommand nc -x 127.0.0.1:62221 -X 5 %h %p

不过我经常遇到这类问题,使用代理命令去连接的时候出现下面错误。一段时间还是偶尔出现,后来经常出现所以我干脆就放弃使用这种办法了。当时我的猜测是,可能ssr的socks5协议不太完整,ssh里面有些指令ssr没有办法支持。

➜  ~ ssh -o ProxyCommand="nc -x 127.0.0.1:62221 -X 5 %h %p" app0
ssh_exchange_identification: Connection closed by remote host

直到前段时间,同事提醒我可能是因为host没有办法解析,我才觉得有可能是这个问题。如果真是的host没有办法解析的话,那么很好解决,只需要在ssr-server所在的机器上加入一些代码观察是否需要解析host(这里是app0),以及如果在/etc/hosts里面添加app0的ip应该就可以work. 因为ssr使用python编写的,之前我通读过ss的代码,ssr比较core的部分还是使用ss的代码,所以也非常好定位解析host的代码。下面我在ssr-server上面加入的patch代码

diff --git a/shadowsocksr/shadowsocks/asyncdns.py b/shadowsocksr/shadowsocks/asyncdns.py
index 797704e..40f9dc7 100644
--- a/shadowsocksr/shadowsocks/asyncdns.py
+++ b/shadowsocksr/shadowsocks/asyncdns.py
@@ -451,6 +451,10 @@ class DNSResolver(object):
             self._sock.sendto(req, server)

     def resolve(self, hostname, callback):
+        # NOTE(yan): 定位DNS解析问题
+        # import traceback
+        # traceback.print_stack()
+        # print(hostname)
         if type(hostname) != bytes:
             hostname = hostname.encode('utf8')
         if not hostname:

然后的确在ssr-server的log里面发现了解析app0的请求。进一步只要在/etc/hosts里面添加app0的ip, 整个流程就可以work了。

不过在server端添加代码始终不太灵活,如果ssr-client可以fanout导多个ssr-server的话,那么每个ssr-server都需要修改/etc/hosts文件,或者ssr-server是租用的没有办法修改。所以最好的办法还是在ssr-client解析,好在这个工作也不复杂,因为ss里面提供了很多基础组件和抽象。代码比较粗糙,也只考虑了ipv4这个情况,大致意思就是修改连接头数据。

diff --git a/shadowsocksr/shadowsocks/common.py b/shadowsocksr/shadowsocks/common.py
index c4484c0..3db202c 100644
--- a/shadowsocksr/shadowsocks/common.py
+++ b/shadowsocksr/shadowsocks/common.py
@@ -240,6 +240,12 @@ def parse_header(data):
         return None
     return connecttype, addrtype, to_bytes(dest_addr), dest_port, header_length

+def pack_ipv4_header(dest_addr, dest_port):
+    data = [0] * 7
+    data[0] = ADDRTYPE_IPV4
+    data[1:5] = socket.inet_aton(to_str(dest_addr))
+    data[5:7] = struct.pack('>H', dest_port)
+    return bytearray(data)

 class IPNetwork(object):
     ADDRLENGTH = {socket.AF_INET: 32, socket.AF_INET6: 128, False: 0}
diff --git a/shadowsocksr/shadowsocks/tcprelay.py b/shadowsocksr/shadowsocks/tcprelay.py
index 595e2be..7a7cd71 100644
--- a/shadowsocksr/shadowsocks/tcprelay.py
+++ b/shadowsocksr/shadowsocks/tcprelay.py
@@ -30,7 +30,7 @@ import platform
 import threading

 from shadowsocks import encrypt, obfs, eventloop, shell, common, lru_cache, version
-from shadowsocks.common import pre_parse_header, parse_header
+from shadowsocks.common import pre_parse_header, parse_header, pack_ipv4_header

 # we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time
 TIMEOUTS_CLEAN_SIZE = 512
@@ -641,6 +641,16 @@ class TCPRelayHandler(object):
             else:
                 common.connect_log('TCP request %s:%d by user %d' %
                         (common.to_str(remote_addr), remote_port, self._user_id))
+
+            # NOTE(yan): 如果不是IP并且域名在自己的hosts文件里面,那么直接在本地解析
+            # import traceback
+            # traceback.print_stack()
+            # print(remote_addr)
+            if not common.is_ip(remote_addr) and remote_addr in self._dns_resolver._hosts:
+                remote_addr = self._dns_resolver._hosts[remote_addr]
+                print('fix to {}:{}'.format(remote_addr, remote_port))
+                data = pack_ipv4_header(remote_addr, remote_port)
+
             self._remote_address = (common.to_str(remote_addr), remote_port)
             self._remote_udp = (connecttype != 0)
             # pause reading