はじめに
このエントリは Linux Advent Calendar 2017 の18日分の記事です。
Linux 4.13で カーネル内TLS (KTLS) がマージされました。カーネルでTLSを行なうことのモチベーションは、例えばsendfile
を使ってデータを送信するときに、ユーザランドにデータをコピーすることなく暗号化することなどがあります。詳細は LWNの記事 や netdev 1.2のプレゼンテーション を参照してもらうとして、この記事ではKTLSがマージされるときに一緒にマージされた ULP (Upper Layer Protocol)について扱います。
ULPとは?
ULPとは、ソケットとレイヤ4(現在はTCPのみ)の間にプロトコルを追加するためのフレームワークです。KTLSの場合は、ソケットからTCPにデータを渡す前にTLSの暗号化を行なっています。
カーネルモジュールとして実装可能なので、わりと簡単に簡単にプロトコルを追加することができます。(実装が簡単かどうかはプロトコル次第ですが。)
ユーザプログラムにはほとんど修正は必要なく、追加されたプロトコルを使うかどうかをsetsockopt
を指定するだけです。
KTLSの場合は大体以下のような手順で通信を行ないます(Documentation/networking/tls.txtから抜粋)。
sock = socket(AF_INET, SOCK_STREAM, 0);
// Tell to use ktls
setsockopt(sock, SOL_TCP, TCP_ULP, "tls", sizeof("tls"));
// Set parameters through crypto_info
setsockopt(sock, SOL_TLS, TLS_TX, &crypto_info, sizeof(crypto_info));
const char *msg = "hello world\n";
// Send a message
send(sock, msg, strlen(msg));
// Send a file using sendfile
file = open(filename, O_RDONLY);
fstat(file, &stat);
sendfile(sock, file, &offset, stat.st_size);
1つ目のsetsockopt
で"tls"という名前のプロトコルを使うことを指定して、2つ目のsetsockopt
でTLSに必要なパラメタを設定してます。(crypto_info
の初期化は省略しています。) かなり簡単に使えることがわかると思います。
ULPの初期化
通信の処理は個々のプロトコルに依存して、あまりULPは関係ないため、ULPを使うための初期化のコードだけを見てみます。以下はKTLSのコードの一部です。
static int __init tls_register(void)
{
tls_base_prot = tcp_prot;
tls_base_prot.setsockopt = tls_setsockopt;
tls_base_prot.getsockopt = tls_getsockopt;
tls_sw_prot = tls_base_prot;
tls_sw_prot.sendmsg = tls_sw_sendmsg;
tls_sw_prot.sendpage = tls_sw_sendpage;
tls_sw_prot.close = tls_sk_proto_close;
tcp_register_ulp(&tcp_tls_ulp_ops);
return 0;
}
static void __exit tls_unregister(void)
{
tcp_unregister_ulp(&tcp_tls_ulp_ops);
}
module_init(tls_register);
module_exit(tls_unregister);
モジュールがロードされるとtls_register
が呼ばれます。ここではKTLSで使う関数を用意します。
tls_setsockopt
は先程の例でいう2つ目のsetsockopt
で呼ばれることになります。sendmsg
やsendpage
は名前から推測できる通りソケットにsend
やsendfile
したときに呼ばれる関数を指定しています。
static int tls_init(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct tls_context *ctx;
int rc = 0;
/* allocate tls context */
ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
if (!ctx) {
rc = -ENOMEM;
goto out;
}
icsk->icsk_ulp_data = ctx;
ctx->setsockopt = sk->sk_prot->setsockopt;
ctx->getsockopt = sk->sk_prot->getsockopt;
sk->sk_prot = &tls_base_prot;
out:
return rc;
}
static struct tcp_ulp_ops tcp_tls_ulp_ops __read_mostly = {
.name = "tls",
.owner = THIS_MODULE,
.init = tls_init,
};
tls_init
は先程の例の1つ目のsetsockopt
で呼ばれます。
キモはsk->sk_prot = &tls_base_prot;
で、ここでレイヤ4の関数(TCPの場合はtcp_sendmsg
などをKTLSのもので置き換えています。これ以降、ソケットで通信を行なうとまずKTLSの関数が呼ばれることになります。
setsockopt
とgetsockopt
を保存しているのは、オリジナルの挙動にフォールバックさせるときに使うためです。
ULPを使ってみる
せっかくなのでULPを使って独自プロトコルを実装してみました。プロトコルといっても複雑なものではなく、単にソケットから渡されたデータを勝手に圧縮してヘッダを付加して送信するだけです。
実装はgistに貼っておきました→ https://gist.github.com/peo3/56e894e8d796c7f1a8816def188a73b2
詳しい説明は省略しますが、Linuxが持つzlib_deflate/zlib_inflate関数を使って圧縮しています。
使う時は以下のように"comp"というプロトコル名を指定します。
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
s.setsockopt(socket.SOL_TCP, 31, "comp".encode())
おわりに
今回は新しく導入されたULPを紹介し、それを使ってオレオレプロトコルを実装してみました。みなさんも面白プロトコルを実装してみてはいかがでしょうか。