CLOVER🍀

That was when it all began.

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

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

socatを使って、プロキシサーバーが立てられそうだったので、試してみようかなと。

socatとは

socatとは、2つのストリーム間のデータ転送を行うコマンドラインツールです。

ストリームとしては、TCP、UDP、UNIXドメインソケット、ファイル、コマンドラインツールを扱ったりできます。SSL/TLSも扱えます。

環境

今回の環境は、こちらです。Ubuntu Linux 18.04 LTS。

$ uname -srvmpio
Linux 4.15.0-96-generic #97-Ubuntu SMP Wed Apr 1 03:25:46 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux


$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.4 LTS
Release:    18.04
Codename:   bionic

socatをインストールする

まずは、socatをインストールしましょう。Ubuntu Linuxの場合、aptでインストールできます。

$ sudo apt install socat

今回は、バージョン1.7.3.2がインストールされました。

$ socat -V
socat by Gerhard Rieger and contributors - see www.dest-unreach.org
socat version 1.7.3.2 on Apr  4 2018 10:06:49
   running on Linux version #97-Ubuntu SMP Wed Apr 1 03:25:46 UTC 2020, release 4.15.0-96-generic, machine x86_64
features:
  #define WITH_STDIO 1
  #define WITH_FDNUM 1
  #define WITH_FILE 1
  #define WITH_CREAT 1
  #define WITH_GOPEN 1
  #define WITH_TERMIOS 1
  #define WITH_PIPE 1
  #define WITH_UNIX 1
  #define WITH_ABSTRACT_UNIXSOCKET 1
  #define WITH_IP4 1
  #define WITH_IP6 1
  #define WITH_RAWIP 1
  #define WITH_GENERICSOCKET 1
  #define WITH_INTERFACE 1
  #define WITH_TCP 1
  #define WITH_UDP 1
  #define WITH_SCTP 1
  #define WITH_LISTEN 1
  #define WITH_SOCKS4 1
  #define WITH_SOCKS4A 1
  #define WITH_PROXY 1
  #define WITH_SYSTEM 1
  #define WITH_EXEC 1
  #undef WITH_READLINE
  #define WITH_TUN 1
  #define WITH_PTY 1
  #define WITH_OPENSSL 1
  #undef WITH_FIPS
  #define WITH_LIBWRAP 1
  #define WITH_SYCLS 1
  #define WITH_FILAN 1
  #define WITH_RETRY 1
  #define WITH_MSGLEVEL 0 /*debug*/

ヘルプを表示してみます。

$ socat -h
socat by Gerhard Rieger and contributors - see www.dest-unreach.org
Usage:
socat [options] <bi-address> <bi-address>
   options:
      -V     print version and feature information to stdout, and exit
      -h|-?  print a help text describing command line options and addresses
      -hh    like -h, plus a list of all common address option names
      -hhh   like -hh, plus a list of all available address option names
      -d     increase verbosity (use up to 4 times; 2 are recommended)
      -D     analyze file descriptors before loop
      -ly[facility]  log to syslog, using facility (default is daemon)
      -lf<logfile>   log to file
      -ls            log to stderr (default if no other log)
      -lm[facility]  mixed log mode (stderr during initialization, then syslog)
      -lp<progname>  set the program name used for logging
      -lu            use microseconds for logging timestamps
      -lh            add hostname to log messages
      -v     verbose data traffic, text
      -x     verbose data traffic, hexadecimal
      -b<size_t>     set data buffer size (8192)
      -s     sloppy (continue on error)
      -t<timeout>    wait seconds before closing second channel
      -T<timeout>    total inactivity timeout in seconds
      -u     unidirectional mode (left to right)
      -U     unidirectional mode (right to left)
      -g     do not check option groups
      -L <lockfile>  try to obtain lock, or fail
      -W <lockfile>  try to obtain lock, or wait
      -4     prefer IPv4 if version is not explicitly specified
      -6     prefer IPv6 if version is not explicitly specified
   bi-address:
      pipe[,<opts>]   groups=FD,FIFO
      <single-address>!!<single-address>
      <single-address>
   single-address:
      <address-head>[,<opts>]
   address-head:
      abstract-client:<filename>  groups=FD,SOCKET,RETRY,UNIX
      abstract-connect:<filename> groups=FD,SOCKET,RETRY,UNIX
      abstract-listen:<filename>  groups=FD,SOCKET,LISTEN,CHILD,RETRY,UNIX
      abstract-recv:<filename>    groups=FD,SOCKET,RETRY,UNIX
      abstract-recvfrom:<filename>    groups=FD,SOCKET,CHILD,RETRY,UNIX
      abstract-sendto:<filename>  groups=FD,SOCKET,RETRY,UNIX
      create:<filename>   groups=FD,REG,NAMED
      exec:<command-line> groups=FD,FIFO,SOCKET,EXEC,FORK,TERMIOS,PTY,PARENT,UNIX
      fd:<num>    groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      gopen:<filename>    groups=FD,FIFO,CHR,BLK,REG,SOCKET,NAMED,OPEN,TERMIOS,UNIX
      interface:<interface>   groups=FD,SOCKET
      ip-datagram:<host>:<protocol> groups=FD,SOCKET,RANGE,IP4,IP6
      ip-recv:<protocol>  groups=FD,SOCKET,RANGE,IP4,IP6
      ip-recvfrom:<protocol>  groups=FD,SOCKET,CHILD,RANGE,IP4,IP6
      ip-sendto:<host>:<protocol>   groups=FD,SOCKET,IP4,IP6
      ip4-datagram:<host>:<protocol>    groups=FD,SOCKET,RANGE,IP4
      ip4-recv:<protocol> groups=FD,SOCKET,RANGE,IP4
      ip4-recvfrom:<protocol> groups=FD,SOCKET,CHILD,RANGE,IP4
      ip4-sendto:<host>:<protocol>  groups=FD,SOCKET,IP4
      ip6-datagram:<host>:<protocol>    groups=FD,SOCKET,RANGE,IP6
      ip6-recv:<protocol> groups=FD,SOCKET,RANGE,IP6
      ip6-recvfrom:<protocol> groups=FD,SOCKET,CHILD,RANGE,IP6
      ip6-sendto:<host>:<protocol>  groups=FD,SOCKET,IP6
      open:<filename> groups=FD,FIFO,CHR,BLK,REG,NAMED,OPEN,TERMIOS
      openssl:<host>:<port> groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP,OPENSSL
      openssl-listen:<port>   groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,IP6,TCP,OPENSSL
      pipe:<filename> groups=FD,FIFO,NAMED,OPEN
      proxy:<proxy-server>:<host>:<port>  groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP,HTTP
      pty   groups=FD,NAMED,TERMIOS,PTY
      sctp-connect:<host>:<port>    groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,SCTP
      sctp-listen:<port>  groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,IP6,SCTP
      sctp4-connect:<host>:<port>   groups=FD,SOCKET,CHILD,RETRY,IP4,SCTP
      sctp4-listen:<port> groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,SCTP
      sctp6-connect:<host>:<port>   groups=FD,SOCKET,CHILD,RETRY,IP6,SCTP
      sctp6-listen:<port> groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP6,SCTP
      socket-connect:<domain>:<protocol>:<remote-address> groups=FD,SOCKET,CHILD,RETRY
      socket-datagram:<domain>:<type>:<protocol>:<remote-address>   groups=FD,SOCKET,RANGE
      socket-listen:<domain>:<protocol>:<local-address>   groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE
      socket-recv:<domain>:<type>:<protocol>:<local-address>    groups=FD,SOCKET,RANGE
      socket-recvfrom:<domain>:<type>:<protocol>:<local-address>    groups=FD,SOCKET,CHILD,RANGE
      socket-sendto:<domain>:<type>:<protocol>:<remote-address> groups=FD,SOCKET
      socks4:<socks-server>:<host>:<port> groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP,SOCKS4
      socks4a:<socks-server>:<host>:<port>    groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP,SOCKS4
      stderr    groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      stdin groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      stdio groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      stdout    groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      system:<shell-command>  groups=FD,FIFO,SOCKET,EXEC,FORK,TERMIOS,PTY,PARENT,UNIX
      tcp-connect:<host>:<port> groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP
      tcp-listen:<port>   groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,IP6,TCP
      tcp4-connect:<host>:<port>    groups=FD,SOCKET,CHILD,RETRY,IP4,TCP
      tcp4-listen:<port>  groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,TCP
      tcp6-connect:<host>:<port>    groups=FD,SOCKET,CHILD,RETRY,IP6,TCP
      tcp6-listen:<port>  groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP6,TCP
      tun[:<ip-addr>/<bits>]    groups=FD,CHR,NAMED,OPEN,INTERFACE
      udp-connect:<host>:<port> groups=FD,SOCKET,IP4,IP6,UDP
      udp-datagram:<host>:<port>    groups=FD,SOCKET,RANGE,IP4,IP6,UDP
      udp-listen:<port>   groups=FD,SOCKET,LISTEN,CHILD,RANGE,IP4,IP6,UDP
      udp-recv:<port> groups=FD,SOCKET,RANGE,IP4,IP6,UDP
      udp-recvfrom:<port> groups=FD,SOCKET,CHILD,RANGE,IP4,IP6,UDP
      udp-sendto:<host>:<port>  groups=FD,SOCKET,IP4,IP6,UDP
      udp4-connect:<host>:<port>    groups=FD,SOCKET,IP4,UDP
      udp4-datagram:<remote-address>:<port> groups=FD,SOCKET,RANGE,IP4,UDP
      udp4-listen:<port>  groups=FD,SOCKET,LISTEN,CHILD,RANGE,IP4,UDP
      udp4-recv:<port>    groups=FD,SOCKET,RANGE,IP4,UDP
      udp4-recvfrom:<host>:<port>   groups=FD,SOCKET,CHILD,RANGE,IP4,UDP
      udp4-sendto:<host>:<port> groups=FD,SOCKET,IP4,UDP
      udp6-connect:<host>:<port>    groups=FD,SOCKET,IP6,UDP
      udp6-datagram:<host>:<port>   groups=FD,SOCKET,RANGE,IP6,UDP
      udp6-listen:<port>  groups=FD,SOCKET,LISTEN,CHILD,RANGE,IP6,UDP
      udp6-recv:<port>    groups=FD,SOCKET,RANGE,IP6,UDP
      udp6-recvfrom:<port>    groups=FD,SOCKET,CHILD,RANGE,IP6,UDP
      udp6-sendto:<host>:<port> groups=FD,SOCKET,IP6,UDP
      unix-client:<filename>  groups=FD,SOCKET,NAMED,RETRY,UNIX
      unix-connect:<filename> groups=FD,SOCKET,NAMED,RETRY,UNIX
      unix-listen:<filename>  groups=FD,SOCKET,NAMED,LISTEN,CHILD,RETRY,UNIX
      unix-recv:<filename>    groups=FD,SOCKET,NAMED,RETRY,UNIX
      unix-recvfrom:<filename>    groups=FD,SOCKET,NAMED,CHILD,RETRY,UNIX
      unix-sendto:<filename>  groups=FD,SOCKET,NAMED,RETRY,UNIX

使い方

最初の紹介に、socatは2つのストリームをつなげるコマンドラインツールです。

ヘルプにちらっと書いているのですが、「bi-address」の部分にストリームを定義していきます。

socat [options] <bi-address> <bi-address>

「bi-address」の書式。

   bi-address:
      pipe[,<opts>]     groups=FD,FIFO
      <single-address>!!<single-address>
      <single-address>
   single-address:
      <address-head>[,<opts>]

「address-head」の例。TCP関連です。

      tcp-connect:<host>:<port> groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP
      tcp-listen:<port> groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,IP6,TCP
      tcp4-connect:<host>:<port>        groups=FD,SOCKET,CHILD,RETRY,IP4,TCP
      tcp4-listen:<port>        groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,TCP
      tcp6-connect:<host>:<port>        groups=FD,SOCKET,CHILD,RETRY,IP6,TCP
      tcp6-listen:<port>        groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP6,TCP

address-headにはオプションを指定することができ、どのようなものが指定できるかはgroupsと突き合わせて見ることになります。

      -h|-?  print a help text describing command line options and addresses
      -hh    like -h, plus a list of all common address option names
      -hhh   like -hh, plus a list of all available address option names

たとえば、forkというオプションはCHILDに属し、intervalというオプションはRETRYに属します。

      fork      groups=CHILD    phase=PASTACCEPT        type=BOOL

      interval  groups=RETRY    phase=INIT      type=STRUCT-TIMESPEC

なので、先ほど挙げたTCP関連のaddress-headで使うことができますね。

オプションの説明は、こちらのページを見たらよさそうです。

socat(1): Multipurpose relay - Linux man page

fork、intervalの例だと、CWfork、CWintervalとかですね。

とまあ、説明はこれくらいにして使っていってみましょう。

TCPサーバーを立ててみる

最初は、簡単なTCPサーバーを立ててみましょう。

以下で、「tcp-listen」を使い、指定したTCP ポート(今回は8000)でリッスンして、受信した内容を標準出力(stdout)に流すサーバーに
なります。

$ socat tcp-listen:8000 stdout

コマンドとしては、このままフォアグラウンドで受信待ちになります。

telnetで、メッセージを送信してみます。

$ curl telnet://localhost:8000
Hello World!!

socat側。送信されてきたメッセージが標準出力に現れたことが確認できました。

$ socat tcp-listen:8000 stdout
Hello World!!

ところで、このsocatのプロセスですが、telnetの接続を切ると、終了してしまいます。

この動きを変更して、複数の接続を受け付けられるようにするには「fork」オプションを指定します。

$ socat tcp-listen:8000,fork stdout

これで、telnetを切断しても終了しなくなりました。

あとは再起動時に同じアドレスへのバインドがすぐにできるように、reuseaddrもつけておくと良いでしょう。

$ socat tcp-listen:8000,fork,reuseaddr stdout

ところで、tcp-connectがバインドするアドレスは、「0.0.0.0」みたいですね。

$ ss -tnl | grep 8000
LISTEN   0         5                   0.0.0.0:8000             0.0.0.0:* 

プロキシサーバーを立ててみる

次は、socatを使って別のサーバーに対するプロキシサーバーを立ててみましょう。

socatを使っているサーバーとは、別のサーバーを用意してApacheをインストールします。

$ sudo apt install apache2

このサーバーのIPアドレスは、「192.168.33.11」とします。

socatで、このApacheに対するTCPプロキシサーバーを立ててみます。「tcp-listen」で接続を待ち受け、送信先のサーバーは「tcp-connect」で
指定します。

$ socat tcp-listen:8000,fork,reuseaddr tcp-connect:192.168.33.11:80

「tcp-connect」の場合は、送信先のIPアドレスを指定することができます。「tcp-listen」の場合は指定できません。
ヘルプで記載されていた、「address-head」の定義がこうでしたからね。

      tcp-connect:<host>:<port> groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP
      tcp-listen:<port> groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,IP6,TCP

socat側で確認。

$ curl -I localhost:8000
HTTP/1.1 200 OK
Date: Sat, 11 Apr 2020 05:07:09 GMT
Server: Apache/2.4.29 (Ubuntu)
Last-Modified: Sat, 11 Apr 2020 05:05:41 GMT
ETag: "2aa6-5a2fccd2ca128"
Accept-Ranges: bytes
Content-Length: 10918
Vary: Accept-Encoding
Content-Type: text/html

OKそうですね。

ところで、こういうのを使う時には無条件に転送するのも良くないケースはあると思うので(tcp-listenは全ネットワークインターフェースに
バインドしてますし)、firewalld(Ubuntu Linuxだとufw)などで適宜接続元を制限するなどしましょう。

systemdに組み込んでみる

オマケとして、このサーバーをsystemdに組み込んでdaemon化してみましょう。

サービスユニット定義ファイルを用意。ほぼ最小です。
/etc/systemd/system/socat-http-proxy.service

[Unit]
Description=socat http proxy
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/bin/socat tcp-listen:8000,fork,reuseaddr tcp-connect:192.168.33.11:80

[Install]
WantedBy=multi-user.target

反映。

$ sudo systemctl enable socat-http-proxy

起動。

$ sudo systemctl start socat-http-proxy

確認。

$ curl -I localhost:8000
HTTP/1.1 200 OK
Date: Sat, 11 Apr 2020 05:19:13 GMT
Server: Apache/2.4.29 (Ubuntu)
Last-Modified: Sat, 11 Apr 2020 05:05:41 GMT
ETag: "2aa6-5a2fccd2ca128"
Accept-Ranges: bytes
Content-Length: 10918
Vary: Accept-Encoding
Content-Type: text/html

停止。

$ sudo systemctl stop socat-http-proxy

こんなところですね。