🐈

小さい netcat コマンド tncl - fujiwara-ware advent calendar 2024 day 6

2024/12/06に公開

この記事は fujiwara-ware advent calendar 2024 の6日目です。

tncl とは

https://github.com/fujiwara/tncl

tncl は、Zig言語 で書かれた小さい netcat コマンドのようなツールです。"tiny nc -l" の略です。「てぃんくる」と読みます。

なぜ作ったのか

4日目に紹介した ecstaecsta cp の実装を検討した際に、ポートフォワード経由でファイルを送受信するために、簡単なTCPサーバーが必要となりました。その用途には大昔から netcat(nc) コマンドがありますが、一般的なコンテナイメージには含まれていないことが多いため、ごく小さいバイナリで同じ機能を持つものを作成したものです。

そもそもECSタスクのコンテナにコマンドを送り込むために、ECS Execでshellのコマンドとしてbase64でエンコードした文字列を出力し、デコードしてファイルに書き込めるサイズであることが必須でした。そのために、小さくて静的リンクされたバイナリが必要だったのです。

このような場合にGoで作るとバイナリサイズが大きすぎます。Zigならば小さいバイナリができるのでは?ということで作ってみました。

tncl の使い方

引数はポート番号のみです。ポート番号を指定して起動すると、TCPのリッスンを開始します。

$ tncl 12345 > /path/to/file

起動してクライアントから接続されると、標準出力に受信したデータを出力します。つまり、ファイルにリダイレクトすればファイルを作成できます。また、標準入力をリダイレクトすれば、ファイルの内容をクライアントに送信することができます。

$ tncl 12345 < /path/to/file

できることはこれだけです。

どれぐらい小さいか

まず、ソースコードです。これを書いている v0.0.4 の時点でわずか74行です。短いので全部貼り付けてしまいましょう。

const std = @import("std");

pub fn main() !void {
    const opt = try getOptionsFromArgs();
    const addr = std.net.Address.initIp4(.{ 0, 0, 0, 0 }, opt.port);
    var server = try addr.listen(.{ .reuse_address = true });
    std.log.info("listening on port {d}...", .{opt.port});

    var client = try server.accept();
    std.log.info("accepted connection from {}", .{client.address});
    defer client.stream.close();

    var sender_thread = try std.Thread.spawn(.{}, sender, .{&client.stream});
    var receiver_thread = try std.Thread.spawn(.{}, receiver, .{&client.stream});

    sender_thread.join();
    receiver_thread.join();
}

fn receiver(stream: *std.net.Stream) !void {
    const stdout = std.io.getStdOut().writer();
    const reader = stream.reader();
    while (true) {
        var buffer: [4096]u8 = undefined;
        const read_bytes = try reader.read(buffer[0..]);
        if (read_bytes == 0) {
            std.log.info("client closed connection", .{});
            // client closed connection
            std.process.exit(0);
        }
        try stdout.writeAll(buffer[0..read_bytes]);
    }
}

fn sender(stream: *std.net.Stream) !void {
    const stdin = std.io.getStdIn().reader();
    while (true) {
        var buffer: [4096]u8 = undefined;
        const read_bytes = try stdin.read(buffer[0..]);
        if (read_bytes == 0) {
            std.log.info("stdin closed", .{});
            // stdin closed
            std.process.exit(0);
        }
        try stream.writeAll(buffer[0..read_bytes]);
    }
}

const Options = struct {
    port: u16,
};

const OptionsError = error{
    MissingPortNumber,
};

fn getOptionsFromArgs() !Options {
    var args = std.process.args();
    _ = std.mem.sliceTo(args.next().?, 0); // cmd name is not used
    var port: u16 = 0;
    while (args.next()) |arg| {
        port = try std.fmt.parseInt(u16, arg, 10);
        break;
    }
    if (port == 0) {
        return OptionsError.MissingPortNumber;
    }
    return .{ .port = port };
}

pub const std_options = .{
    // Set the log level to info
    .log_level = .info,
};

これをサイズを小さくすることを優先して zig build -Doptimize=ReleaseSmall でビルドしたところ、Linux amd64, aarch64 ともに 21,144 バイトとなりました。わずか21KBで静的リンクされた動作するバイナリができあがりました。

まとめ

小さいバイナリを作る必要があったので、Zig言語を使ってごく機能の限られた netcat コマンドのようなツールを作成しました。ecsta cp のためだけに作ったものですが、満足に使えるものができたのでよかったですね。

それでは、明日もお楽しみに!

Discussion