diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03cb27d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.zig-cache/ +zig-out/ +zig-pkg/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..eb50f2a --- /dev/null +++ b/build.zig @@ -0,0 +1,241 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +comptime { + if (builtin.zig_version.major != 0 or builtin.zig_version.minor != 16) { + @compileError("nxdns requires Zig 0.16.x, found " ++ builtin.zig_version_string); + } +} + +const cross_targets = [_][]const u8{ + "x86_64-linux-musl", + "aarch64-linux-musl", +}; + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const integration = b.option(bool, "integration", "Run hermetic integration tests (loopback sockets only)") orelse false; + const live = b.option(bool, "live", "Run tests that reach external network hosts") orelse false; + const version_string = b.option([]const u8, "version-string", "Version reported by `nxdns version`") orelse "0.1.0-dev"; + const git_commit = b.option([]const u8, "git-commit", "Git commit reported by `nxdns version`") orelse "unknown"; + + const options = b.addOptions(); + options.addOption(bool, "integration", integration); + options.addOption(bool, "live", live); + options.addOption([]const u8, "version_string", version_string); + options.addOption([]const u8, "git_commit", git_commit); + options.addOption([]const u8, "zig_version_string", builtin.zig_version_string); + + const exe = addExecutable(b, target, optimize, options); + b.installArtifact(exe); + + const run = b.addRunArtifact(exe); + run.step.dependOn(b.getInstallStep()); + if (b.args) |args| run.addArgs(args); + b.step("run", "Run nxdns").dependOn(&run.step); + + const tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("src/tests.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + tests.root_module.addOptions("build_options", options); + tests.root_module.linkLibrary(sqliteLibrary(b, target, optimize)); + tests.root_module.linkLibrary(mbedtlsLibrary(b, target, optimize)); + tests.root_module.addCSourceFile(.{ .file = b.path("src/platform/mbedtls_shim.c") }); + addMbedtlsThreadingMacros(tests.root_module); + tests.root_module.addAnonymousImport("test_fixtures", .{ + .root_source_file = b.path("tests/fixtures/fixtures.zig"), + }); + b.step("test", "Run the test suite").dependOn(&b.addRunArtifact(tests).step); + + const cross = b.step("cross", "Build static musl executables for every deploy target"); + for (cross_targets) |triple| { + const query = std.Target.Query.parse(.{ .arch_os_abi = triple }) catch |err| { + std.debug.panic("invalid cross target '{s}': {t}", .{ triple, err }); + }; + const cross_exe = addExecutable(b, b.resolveTargetQuery(query), optimize, options); + cross_exe.linkage = .static; + const install = b.addInstallArtifact(cross_exe, .{ + .dest_dir = .{ .override = .{ .custom = b.fmt("cross/{s}", .{triple}) } }, + }); + cross.dependOn(&install.step); + } +} + +fn addExecutable( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + options: *std.Build.Step.Options, +) *std.Build.Step.Compile { + const exe = b.addExecutable(.{ + .name = "nxdns", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + exe.root_module.addOptions("build_options", options); + exe.root_module.linkLibrary(sqliteLibrary(b, target, optimize)); + exe.root_module.linkLibrary(mbedtlsLibrary(b, target, optimize)); + return exe; +} + +/// SQLite 3.53.4 amalgamation (see build.zig.zon for the pinned URL and hash). +fn sqliteLibrary( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) *std.Build.Step.Compile { + const dep = b.dependency("sqlite", .{}); + const lib = b.addLibrary(.{ + .name = "sqlite3", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + lib.root_module.addIncludePath(dep.path("")); + lib.root_module.addCSourceFile(.{ + .file = dep.path("sqlite3.c"), + .flags = &.{ + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_THREADSAFE=1", + "-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1", + "-DSQLITE_OMIT_LOAD_EXTENSION", + }, + }); + return lib; +} + +/// Mbed TLS 3.6.7 LTS, stock `mbedtls_config.h` (see build.zig.zon for the pinned URL and hash). +fn mbedtlsLibrary( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) *std.Build.Step.Compile { + const dep = b.dependency("mbedtls", .{}); + const lib = b.addLibrary(.{ + .name = "mbedtls", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + + addMbedtlsThreadingMacros(lib.root_module); + + for ([_][]const u8{ + "include", + "library", + "3rdparty/everest/include", + "3rdparty/everest/include/everest", + "3rdparty/everest/include/everest/kremlib", + "3rdparty/p256-m/p256-m/include", + "3rdparty/p256-m/p256-m_driver_interface", + }) |include_dir| { + lib.root_module.addIncludePath(dep.path(include_dir)); + } + + lib.root_module.addCSourceFiles(.{ + .root = dep.path("library"), + .files = &mbedtls_library_sources, + }); + lib.root_module.addCSourceFiles(.{ + .root = dep.path("3rdparty"), + .files = &mbedtls_3rdparty_sources, + }); + lib.installHeadersDirectory(dep.path("include/mbedtls"), "mbedtls", .{}); + lib.installHeadersDirectory(dep.path("include/psa"), "psa", .{}); + return lib; +} + +/// Context sizes change with threading enabled, so every compilation unit that +/// includes mbedTLS headers (the library itself and `mbedtls_shim.c`) must see +/// the same macros. Concurrent handshakes share `ssl_config`, the CTR-DRBG, and +/// global PSA state; without MBEDTLS_THREADING_C those race. +fn addMbedtlsThreadingMacros(m: *std.Build.Module) void { + m.addCMacro("MBEDTLS_THREADING_C", "1"); + m.addCMacro("MBEDTLS_THREADING_PTHREAD", "1"); +} + +/// Every `library/*.c` of the release, matching `library/Makefile`. +const mbedtls_library_sources = [_][]const u8{ + "aes.c", "aesce.c", + "aesni.c", "aria.c", + "asn1parse.c", "asn1write.c", + "base64.c", "bignum.c", + "bignum_core.c", "bignum_mod.c", + "bignum_mod_raw.c", "block_cipher.c", + "camellia.c", "ccm.c", + "chacha20.c", "chachapoly.c", + "cipher.c", "cipher_wrap.c", + "cmac.c", "constant_time.c", + "ctr_drbg.c", "debug.c", + "des.c", "dhm.c", + "ecdh.c", "ecdsa.c", + "ecjpake.c", "ecp.c", + "ecp_curves.c", "ecp_curves_new.c", + "entropy.c", "entropy_poll.c", + "error.c", "gcm.c", + "hkdf.c", "hmac_drbg.c", + "lmots.c", "lms.c", + "md.c", "md5.c", + "memory_buffer_alloc.c", "mps_reader.c", + "mps_trace.c", "net_sockets.c", + "nist_kw.c", "oid.c", + "padlock.c", "pem.c", + "pk.c", "pk_ecc.c", + "pk_wrap.c", "pkcs12.c", + "pkcs5.c", "pkcs7.c", + "pkparse.c", "pkwrite.c", + "platform.c", "platform_util.c", + "poly1305.c", "psa_crypto.c", + "psa_crypto_aead.c", "psa_crypto_cipher.c", + "psa_crypto_client.c", "psa_crypto_driver_wrappers_no_static.c", + "psa_crypto_ecp.c", "psa_crypto_ffdh.c", + "psa_crypto_hash.c", "psa_crypto_mac.c", + "psa_crypto_pake.c", "psa_crypto_random.c", + "psa_crypto_rsa.c", "psa_crypto_se.c", + "psa_crypto_slot_management.c", "psa_crypto_storage.c", + "psa_its_file.c", "psa_util.c", + "ripemd160.c", "rsa.c", + "rsa_alt_helpers.c", "sha1.c", + "sha256.c", "sha3.c", + "sha512.c", "ssl_cache.c", + "ssl_ciphersuites.c", "ssl_client.c", + "ssl_cookie.c", "ssl_debug_helpers_generated.c", + "ssl_msg.c", "ssl_ticket.c", + "ssl_tls.c", "ssl_tls12_client.c", + "ssl_tls12_server.c", "ssl_tls13_client.c", + "ssl_tls13_generic.c", "ssl_tls13_keys.c", + "ssl_tls13_server.c", "threading.c", + "timing.c", "version.c", + "version_features.c", "x509.c", + "x509_create.c", "x509_crl.c", + "x509_crt.c", "x509_csr.c", + "x509write.c", "x509write_crt.c", + "x509write_csr.c", +}; + +/// The object lists of `3rdparty/everest/Makefile.inc` and `3rdparty/p256-m/Makefile.inc`. +/// The stock config enables neither driver, so these compile to empty objects. +const mbedtls_3rdparty_sources = [_][]const u8{ + "everest/library/everest.c", + "everest/library/x25519.c", + "everest/library/Hacl_Curve25519_joined.c", + "p256-m/p256-m_driver_entrypoints.c", + "p256-m/p256-m/p256-m.c", +}; diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..e3df070 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .nxdns, + .version = "0.1.0", + .minimum_zig_version = "0.16.0", + .paths = .{""}, + .fingerprint = 0x3307b311dded1d91, + .dependencies = .{ + .sqlite = .{ + .url = "https://sqlite.org/2026/sqlite-amalgamation-3530400.zip", + .hash = "N-V-__8AAGVtrgCcOcmjrOJnagmnRyMrcKaOo09KbU-vu8w8", + }, + .mbedtls = .{ + .url = "https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/mbedtls-3.6.7.tar.gz", + .hash = "N-V-__8AALrvlQKVtYlvv9dpBnbrJfdwR_F0wAgwsvZhAF1Y", + }, + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..ca5169a --- /dev/null +++ b/src/main.zig @@ -0,0 +1,57 @@ +const std = @import("std"); +const version = @import("version.zig"); + +const usage = + \\usage: nxdns + \\ + \\commands: + \\ run serve DNS + \\ check validate the configuration + \\ export write local records to stdout + \\ import read local records from stdin + \\ version print version information + \\ +; + +const exit_ok = 0; +const exit_not_implemented = 2; +const exit_usage = 64; + +pub fn main(init: std.process.Init) u8 { + var args = init.minimal.args.iterate(); + _ = args.skip(); + const command = args.next() orelse return fail(init.io, usage); + + if (std.mem.eql(u8, command, "version")) { + return print(init.io, "nxdns {s} ({s})\nzig {s}\n", .{ + version.string, + version.git_commit, + version.zig_version_string, + }); + } + + for ([_][]const u8{ "run", "check", "export", "import" }) |known| { + if (std.mem.eql(u8, command, known)) { + _ = print(init.io, "not implemented\n", .{}); + return exit_not_implemented; + } + } + + return fail(init.io, usage); +} + +fn print(io: std.Io, comptime format: []const u8, arguments: anytype) u8 { + var buffer: [512]u8 = undefined; + var file_writer = std.Io.File.stdout().writer(io, &buffer); + file_writer.interface.print(format, arguments) catch return 1; + file_writer.interface.flush() catch return 1; + return exit_ok; +} + +fn fail(io: std.Io, message: []const u8) u8 { + var buffer: [512]u8 = undefined; + var file_writer = std.Io.File.stderr().writer(io, &buffer); + file_writer.interface.writeAll(message) catch {}; + file_writer.interface.flush() catch {}; + return exit_usage; +} diff --git a/src/tests.zig b/src/tests.zig new file mode 100644 index 0000000..3fc4cd0 --- /dev/null +++ b/src/tests.zig @@ -0,0 +1,28 @@ +const std = @import("std"); + +comptime { + _ = @import("main.zig"); + _ = @import("version.zig"); + _ = @import("platform/address.zig"); + _ = @import("platform/tls_client.zig"); + _ = @import("platform/tls_client_integration_test.zig"); + _ = @import("platform/tls_server.zig"); +} + +extern fn sqlite3_libversion() [*:0]const u8; +/// Mbed TLS documents 18 bytes as the minimum buffer size for this call. +extern fn mbedtls_version_get_string_full(buffer: [*]u8) void; + +test "sqlite3 links and reports a version" { + const reported = std.mem.span(sqlite3_libversion()); + try std.testing.expect(reported.len > 0); + try std.testing.expect(std.mem.startsWith(u8, reported, "3.")); +} + +test "mbedtls links and reports a version" { + var buffer: [32]u8 = undefined; + mbedtls_version_get_string_full(&buffer); + const reported = std.mem.sliceTo(&buffer, 0); + try std.testing.expect(reported.len > 0); + try std.testing.expect(std.mem.startsWith(u8, reported, "Mbed TLS 3.6")); +} diff --git a/src/version.zig b/src/version.zig new file mode 100644 index 0000000..1b65ee5 --- /dev/null +++ b/src/version.zig @@ -0,0 +1,5 @@ +const build_options = @import("build_options"); + +pub const string: []const u8 = build_options.version_string; +pub const git_commit: []const u8 = build_options.git_commit; +pub const zig_version_string: []const u8 = build_options.zig_version_string;