Nullable

旧レガシーガジェット研究所

RESP(Redisプロトコル)を話す in Golang.

概要

ひょんなことをキッカケにGo言語で簡易的なRedisのサーバを書いたので簡単にまとめようと思う。

https://github.com/0n1shi/beehive-redis

動作としては以下のように普通のRedisサーバとなんら変わりなく、redis-cliからも接続することができる(コマンド数は極端に少ないが)。

# terminal 1
$ beehive-redis run --config conf.yaml
2022/02/20 09:17:27 starting Beehive Redis server ...
# terminal 2
$ redis-cli
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set msg "hello world"
OK
127.0.0.1:6379> get msg
"hello world"

背景

社会人になりたての頃、趣味で運用していたハニーポットだったが、低レイヤーの方へ興味を持ったタイミングに閉じた以来それっきりとなった。それから数年が経ち最近の転職を機にハニーポットへのモチベーションが再燃し、運用を再開したいと考えるようになった今日この頃だったが、しかしなんせ今は状況が変わってサーバを湯水のように利用できる環境ではなくなっている。以前はT-Potを16GBのメモリを載っけたVMで稼働さており潤沢に許されたリソースを余すことなく享受していたが、今は子も二人目が産まれ握りしめた千円札で何とか借りれるVPSググる始末。

野口英世1枚で巨大なOSSが動かせるはずもなく、泣く泣くT-Potを諦めKagoyaでほぼワンコインな2Core&2GBの(コスパ最強)VPSを借りた。これで何ができるのかと考えた時に閃いたのは自作の軽量ハニーポットで、真っ先に思い付いたのが以前訳あって一瞬外部に公開した際に香ばしい文字列が格納されていたRedisだった。

RESP

そうとなればと早々にRedisプロトコルを調べると、なんと驚くほどヒューマンリーダブルである。

例えばよく利用するKEYSコマンド。

127.0.0.1:6379> keys *
1) "msg1"
2) "msg2"

これはクライアントから以下のフォーマットで送信され

*2
$4
keys
$1
*

レスポンスは以下。

*2
$4
msg2
$4
msg1

各行はCRLF(\r\n)で改行される。

上記を見ても何となくわかる通り、*nは配列を表現し、そのnが要素数$nは文字列を表現しており、nは文字列長、その後に実際の文字列が続く。

初見でも勘の良い人であれば何となく想像が付いてしまうほどシンプルである。

RedisのプロトコルRESP (REdis Serialization Protocol)と呼ばれ、Redisのためにデザインされており

  • 実装の容易性
  • パースの高速性
  • ヒューマンリーダブル`

の3つを軸に策定されているよう。

(以下原文)

RESP is a compromise between the following things: - Simple to implement. - Fast to parse. - Human readable.

データ型

RESPには以下の5つのデータ型が定義されている。

  • Simple Strings
  • Errors
  • Integers
  • Bulk Strings
  • Arrays

Simple strings

例えばSETコマンドのレスポンスとして表示される

# e.g.
127.0.0.1:6379> set msg hello
OK

"OK"の文字列などがSimple stringsとしてハンドリングされており、実際には+OK\r\nとして、+から始まるデータがサーバから返される。

Errors

存在しないコマンドを送信した際などに返ってくる以下のようなメッセージは Errors として返されており

# e.g.
127.0.0.1:6379> NONEXISTENT
(error) ERR unknown command `NONEXISTENT`, with args beginning with:\r\n

実際のデータは

-ERR unknown command `NONEXISTENT`, with args beginning with:

上記のように-から始まる。

Intergers

数値データは : から始まり、Redisに実装されたコマンド数を返す

127.0.0.1:6379> COMMAND COUNT
(integer) 224

上記のコマンドでは実際に:224\r\nというデータがサーバから返される。

Bulk strings

INFOコマンドのレスポンスなどはBulk stringsとして扱われ

127.0.0.1:6379> info
# Server
redis_version:6.2.6
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:b61f37314a089f19
redis_mode:standalone
os:Linux 5.10.76-linuxkit x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:10.2.1
process_id:1
process_supervised:no

:

上記は実際に以下のような$[文字列長]から始まるデータとしてサーバから送信されてくる。

$4158
# Server
redis_version:6.2.6
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:b61f37314a089f19
redis_mode:standalone
os:Linux 5.10.76-linuxkit x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:10.2.1
process_id:1
process_supervised:no

:

Arrays型

クライアントコマンドは全て前述のようなArraysとして送信され、例えば

127.0.0.1:6379> set msg hello

とコマンドを送信した場合

*3    // 要素数3の配列
$3    // 長さ3の文字列が続く
set   // [文字列]
$3    // 長さ3の文字列が続く
msg   // [文字列]
$5    // 長さ5の文字列が続く
hello // [文字列]

Bulk stringsのArraysとして上記のようなデータをサーバは受け取り、1つ目の文字列をコマンド、それ以降を引数と解釈することができる。

ちなみにIntergersのArraysは以下のようになる。

*3
:16
:2
:1024

実装

ここまで理解すれば実装は容易で、まず最初に基本的なTCPサーバを書き、クライアントコマンドのパーサとレスポンスのビルダさえあれば十分Redisサーバとして動作する。加えてクライアントから送信されたコマンドデータの保存を行えばハニーポットとしても十分に役割を果たすことができる。

まずクライアントのコマンドは以下のような構造体に格納する。

type Command struct {
    Length      int
    Cmd         string
    Args        []string
    IP          string
    Implemented bool
}

Implementedは自作のRedisサーバに当該コマンドが実装されているか否かを保持し、定期的に未実装コマンドを減らしていくことを目的に設けた。

データの保存部分はInterfaceとして定義し

type Repository interface {
    Save(*Command) error
}

とりあえずMySQLに保存できる実装レイヤを用意した。

メインの処理は以下のようになっており

for {
    // コマンドのパース
    cmd, err := getCmd(conn)
    if err != nil {
        log.Println(err)
        break
    }

    // 送信されたコマンドデータの保存
    log.Printf("received command \"%s\" from %s", cmd.ToString(), cmd.IP)
    if err := repo.Save(cmd); err != nil {
        log.Println(err)
        break
    }
    
    // レスポンスの作成及び送信
    res := makeResStr(cmd)
    if _, err := io.WriteString(conn, res); err != nil {
        log.Println(err)
        break
    }
}

コマンドのパース、受け取ったデータの保存、レスポンスの作成及び送信といった流れになる。

最後に

今回書いたハニーポットとしてのRedisサーバ実際にKagoyaのVPS上で稼働しており、香ばしいペイロードも眺めることができる。

一応収集したデータは以下で公開しているので興味があれば是非に(ペイロードに含まれるURLの中には生きているホストもありそうなので悪用は厳禁です🙅‍♂️)。

https://beehive.0n1shi.dev/

参考