LoginSignup
14
8

More than 5 years have passed since last update.

Go で TCPプロキシを作る

Last updated at Posted at 2017-09-10

Goの勉強として、TCPプロキシを作りました。
任意のTCP接続の間にはさまって、通信ログを取ったりできます。

このようなイメージです。

image.png

image.png

構成要素

  1. TCP接続を受け入れる
  2. 受け入れ後、本物のサーバーにつなぐ
  3. その後は 受け入れたSocketと本物のサーバーを双方向にリレーする

TCP接続受け入れ

まずは外部からのTCP接続を受け入れる部分を作ります。
普通にTCPサーバーを作るときと同様です。

新しい接続がきたら handleConn 関数を goroutine で実行することで複数の接続を受け入れ可能にしています。

handleConn の中身はこの後に記述していきます。

type Server struct {}

func (s *Server) Start(listenAddr *net.TCPAddr) error {
    lt, err := net.ListenTCP("tcp", listenAddr)
    if err != nil {
        return err
    }
    defer lt.Close()

    for {
        conn, err := lt.AcceptTCP()
        if err != nil {
            return err
        }
        go s.handleConn(conn)
    }
}

func (s *Server) handleConn(c *net.TCPConn) {
    ...
}

受け入れ後の双方向リレー

クライアントから接続したあとは、本物のサーバーへ接続して、
クライアントとサーバーの通信内容をリレー(双方向につなげる)します。

双方向ということは、

  • クライアント→サーバー
  • サーバー→クライアント

の2方向なので、2つの goroutine を並列させて行います。

image.png

func (s *Server) handleConn(c *net.TCPConn) {
    defer c.Close()

    // 何らかの方法で本物のサーバーのアドレスを取得しておく
    serverAddr := ....

    // 本物のサーバーへ接続
    serverConn, err := net.DialTCP("tcp", nil, serverAddr)
    if err != nil {
        return err
    }
    defer serverConn.Close()

    // 双方向のリレーを作る
    p := &Proxy{}
    p.Start(c, serverConn)
}

// プロキシ本体
type Proxy struct{}

// プロキシの開始(双方向リレー開始)
func (p *Proxy) Start(clientConn, serverConn) error {
    defer clientConn.Close()
    defer serverConn.Close()

    var eg errGroup.Group

    eg.Go(func() error { return p.relay(&eg, clientConn, serverConn) })
    eg.Go(func() error { return p.relay(&eg, serverConn, clientConn) })

    // wait for stop
    return eg.Wait()
}

const (
    BUFFER_SIZE = 0xFFFF
)

// fromConn -> toConn へ通信内容をリレーする
func (p *Proxy) relay(eg *errGroup.Group, fromConn, toConn *net.TCPConn) error {
    buff := make([]byte, BUFFER_SIZE)
    for {
        n, err := fromConn.Read(buff)
        if err != nil {
            return err
        }
        b := buff[:n]

        // ここでログなどを取ることができる

        n, err = toConn.Write(b)
        if err != nil {
            return err
        }
    }
}

まとめ

  • サーバー部分とリレー部分で分けて考えるとわかりやすい
  • goroutineをまたぐ通知は channel をつかう
  • 複数のgoroutineの待機をしたい場合は sync.WaitGrouperrGroup.Group を使うと良い(キャンセルなどもしたい場合は、context を使う?)

参考にしたページ

14
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8