はじめに

このエントリは 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から抜粋)。

sample-tcp_ulp-tls.c
    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のコードの一部です。

net/tls/tls_main.c
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で呼ばれることになります。sendmsgsendpageは名前から推測できる通りソケットにsendsendfileしたときに呼ばれる関数を指定しています。

net/tls/tls_main.c
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の関数が呼ばれることになります。

setsockoptgetsockoptを保存しているのは、オリジナルの挙動にフォールバックさせるときに使うためです。

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を紹介し、それを使ってオレオレプロトコルを実装してみました。みなさんも面白プロトコルを実装してみてはいかがでしょうか。