CLOVER🍀

That was when it all began.

複数の宛先に対する簡単なTCPプロキシサーバーをsocat+Pythonで書く

これは、なにをしたくて書いたもの?

時々、TCPでの通信をローカルポートから別の宛先に転送したくなることがあります。いわゆるプロキシです。

個人的に、こういう時にはsocatをよく使っているのですが。

socatでTCPプロキシサーバーを立てる - CLOVER🍀

転送先が増えてくると、これを複数書くのが面倒になってきます。そして、シェルスクリプトだとちょっと苦しい感じがします。
なので、スクリプトでも書こうかなと。

お題

今回は、以下のお題で書いてみようかなと思います。

  • 作成のスクリプト内で、複数の宛先に対するTCP通信の転送定義を書けるようにする
  • TCPプロキシの機能は、socatをプロセス起動で使う
  • 作成するスクリプトは、実装する言語の標準機能で行う

個人的に、こういうものはコンパイルする言語ではなくてスクリプト系の言語で書きたいと思っているのと、その環境に持っていったら
すぐに使える感じにしたいので、あまりライブラリなどは入れたくありません。
なので、実装する言語もちょっと迷うのですが、今回はPythonにすることにしました。

まあ、socatはインストールしなくてはいけないんじゃないの?という話はありますが。

こういう時に書くスクリプトPerlかどうかで迷うのですが、今はPythonかな、と…。

環境

今回の環境は、こちら。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
Codename:       jammy


$ uname -srvmpio
Linux 5.15.0-58-generic #64-Ubuntu SMP Thu Jan 5 11:43:13 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

Pythonとsocat。

$ python3 -V
Python 3.10.6


$ socat -v
2023/01/15 15:01:59 socat[2943] E exactly 2 addresses required (there are 0); use option "-h" for help

作成したスクリプト

こんな感じにしました。

start-tcp-proxies.py

#!/usr/bin/python3

from subprocess import Popen

target_host = '192.168.0.6'

port_mappings: dict = {
    "8080": f'{target_host}:8080',
    "3306": f'{target_host}:3306',
}

def start_proxy(from_port: str, to_address: str):
    process: Popen = Popen(['socat', f'tcp-listen:{from_port},fork,reuseaddr', f'tcp-connect:{to_address}'])
    return process

def start_proxies(port_mappings: dict):
    processes:list = []
    for from_port, to_address in port_mappings.items():
        processes.append(start_proxy(from_port, to_address))
        print(f'bind local-port {from_port} -> to {to_address}')

    return processes

def wait_processes(processes: list):
    for process in processes:
        process: Popen = process
        process.wait()

def terminate_processes(processes: list):
    for process in processes:
        process: Popen = process
        process.terminate()


if __name__ == '__main__':
    try:
        proxy_processes: list = start_proxies(port_mappings)
        wait_processes(proxy_processes)
    except KeyboardInterrupt as err:
        pass
    finally:
        terminate_processes(proxy_processes)

Popenを使って、socatを指定した数だけ立ち上げます。

subprocess --- サブプロセス管理 / Popen コンストラクター

def start_proxy(from_port: str, to_address: str):
    process: Popen = Popen(['socat', f'tcp-listen:{from_port},fork,reuseaddr', f'tcp-connect:{to_address}'])
    return process

コマンド実行時に完了は待たず、あとでまとめて待ち合わせを行います。

def wait_processes(processes: list):
    for process in processes:
        process: Popen = process
        process.wait()

基本的には、Ctrl-cで止めるかなと思いますが。

あとはローカルポートと転送先をdictで定義します。

target_host = '192.168.0.6'

port_mappings: dict = {
    "8080": f'{target_host}:8080',
    "3306": f'{target_host}:3306',
}

今回は、転送先にWildFlyMySQLを起動しておきました。

接続先を変更したい場合は、このdictを変更します。

確認

起動。

$ python3 /usr/local/bin/start-tcp-proxies.py
bind local-port 8080 -> to 192.168.0.6:8080
bind local-port 3306 -> to 192.168.0.6:3306

確認。

$ curl -I localhost:8080
HTTP/1.1 200 OK
Connection: keep-alive
Last-Modified: Fri, 16 Dec 2022 00:28:16 GMT
Content-Length: 1504
Content-Type: text/html
Accept-Ranges: bytes
Date: Sun, 15 Jan 2023 06:32:05 GMT


$ curl telnet://localhost:3306 --output -
J
8.0.31

OKですね。

/usr/local/binに置いて実行権限を付けて起動してもいいかもしれません。

$ sudo cp start-tcp-proxies.py /usr/local/bin/start-tcp-proxies
$ sudo chmod a+x /usr/local/bin/start-tcp-proxies


$ start-tcp-proxies
bind local-port 8080 -> to 192.168.0.6:8080
bind local-port 3306 -> to 192.168.0.6:3306

こんな感じで。