小さい netcat コマンド tncl - fujiwara-ware advent calendar 2024 day 6
この記事は fujiwara-ware advent calendar 2024 の6日目です。
tncl とは
tncl は、Zig言語 で書かれた小さい netcat コマンドのようなツールです。"tiny nc -l" の略です。「てぃんくる」と読みます。
なぜ作ったのか
4日目に紹介した ecsta で ecsta 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