SOCKSプロキシ経由でリモートサーバへSFTP接続を行う

はじめに

データアナリティクス事業本部のkobayashiです。

SFTP転送でリモートサーバからデータを取得する必要がありました。Pythonでその様な処理を行おうとするといくつかモジュールの選択肢がありますが今回はParamikoを利用しました。通常の環境ですとあとはPythonのスクリプトでParamikoを呼んで利用すればいだけなのですが、リモートサーバへの接続が接続元IPを制限されている環境で且つSOCKSプロキシを通して接続する必要があったのでその方法をまとめます。

GitHub - paramiko/paramiko

接続元IPを制限された環境へのParamikoでのアクセス

Paramikoを使ってリモートサーバへSFTP接続を試みます。

import paramiko

with paramiko.SSHClient() as client:
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect("target_hostname", port=22, username="username", password="password", timeout=30)

    sftp_connection = client.open_sftp()
    dirs = sftp_connection.listdir('/')
    print(dirs)


結果

当然の結果ながら接続に失敗してしまいます。

Traceback (most recent call last):
  File "/Users/koba/sockes.py", line 22, in <module>
    client.connect("target_hostname", port=22, username="bisnextsftpt11", password="h@MesTee1", timeout=30)
  File "/Users/koba/.pyenv/versions/3.7/lib/python3.7/site-packages/paramiko/client.py", line 416, in connect
    self, server_hostkey_name, server_key
  File "/Users/koba/.pyenv/versions/3.7/lib/python3.7/site-packages/paramiko/client.py", line 824, in missing_host_key
    "Server {!r} not found in known_hosts".format(hostname)
paramiko.ssh_exception.SSHException: Server 'target_hostname' not found in known_hosts

Pysocks

色々情報を漁ったところPySocksが要件を満たすことがわかりましたのでPySocksを使ってSOCKSプロキシ経由で接続する設定を行います。

PySocks · PyPI

PySocks lets you send traffic through SOCKS and HTTP proxy servers. It is a modern fork of SocksiPy with bug fixes and extra features.

Acts as a drop-in replacement to the socket module. Seamlessly configure SOCKS proxies for any socket object by calling socket_object.set_proxy().

PySocksのインストール

pip install PySocks

では具体的にどの様に使うのかというとPyPiのPySocksページ最下部に記述があリます。

In addition to the socksocket class, an additional function worth mentioning is the set_default_proxy function. The parameters are the same as the set_proxy method. This function will set default proxy settings for newly created socksocket objects, in which the proxy settings haven't been changed via the set_proxy method. This is quite useful if you wish to force 3rd party modules to use a SOCKS proxy, by overriding the socket object.

  >>> socks.set_default_proxy(socks.SOCKS5, "socks.example.com")
  >>> socket.socket = socks.socksocket
  >>> urllib.urlopen("http://www.sourceforge.net/")
  

設定の仕方は前述のスクリプトを以下の様に修正すればSocksプロキシ経由で接続できるようになります。

import paramiko
import socks
import socket

socks.set_default_proxy(socks.SOCKS5, 'proxy_hostname', 1080)
socket.socket = socks.socksocket

with paramiko.SSHClient() as client:
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect("target_hostname", port=22, username="username", password="password", timeout=30)

    sftp_connection = client.open_sftp()
    dirs = sftp_connection.listdir('/')
    print(dirs)

結果

['bin', 'dev', 'home', 'opt', 'root', 'sys', 'usr', 'boot', 'etc', 'lib', 'mnt',  'proc', 'sbin', 'tmp', 'var']

上記の修正箇所を解説すると

  1. PySocksモジュールとsocketをインポートする。
  2. Proxyサーバの情報を引数としてsocks.set_default_proxy()に渡してsocks.socksocketオブジェクトを生成する。
  3. socketオブジェクトを2で生成したsocks.socksocketで上書きする。

これでParamikoがPySocksで生成したソケットを通して接続を行えるようになり、接続元をSOCKSプロキシとしてリモートサーバへ接続できる様になります。

他のモジュールでの利用

socketオブジェクトを上書いているのでこれを使っている他のモジュールでもSOCKSプロキシ経由でのリモートサーバへのアクセスができるようになります。

import urllib.request
import socks
import socket


socks.set_default_proxy(socks.SOCKS5, 'proxy_hostname', 1080)
socket.socket = socks.socksocket

with  urllib.request.urlopen("https://dev.classmethod.jp/") as res:
    print(res.read())

結果

b'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta content="\x90\xa2\x8aE\x92\x86\x82\xcc\x82\xa0\x82\xe7\x82\xe4\x82\xe9\x8f\xee\x95\xf1\x82\xf0\x8c\x9f\x8d\xf5\x82\xb7\x82\xe9\x82\xbd\x82\xdf\x82
....

まとめ

リモートで業務を行う際には接続元を制限された環境にアクセスするためにひと手間加える必要があります。セキュリティを考えれば当然ですのでその対応方法をきちんと抑えておく事が大切です。

最後まで読んで頂いてありがとうございました。