Skip to content
This repository was archived by the owner on Jul 21, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions Formula/netcat.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
require (Tap.fetch("automattic", "kandelo-homebrew").path/"Kandelo/formula_support/kandelo_formula_support").to_s

class Netcat < Formula
include KandeloFormulaSupport

desc "GNU TCP and UDP networking utility for Kandelo"
homepage "https://netcat.sourceforge.net/"
url "https://downloads.sourceforge.net/project/netcat/netcat/0.7.1/netcat-0.7.1.tar.gz"
sha256 "30719c9a4ffbcf15676b8f528233ccc54ee6cba96cb4590975f5fd60c68a066f"
license "GPL-2.0-or-later"

depends_on "binaryen" => :build
depends_on "wabt" => :build

skip_clean "bin/netcat"

patch :DATA

def install
kandelo_require_arch!("wasm32")

kandelo_wasm_build do |root|
prefix_maps = [
"-ffile-prefix-map=#{buildpath}=/usr/src/netcat",
"-fdebug-prefix-map=#{buildpath}=/usr/src/netcat",
"-fmacro-prefix-map=#{buildpath}=/usr/src/netcat",
"-ffile-prefix-map=#{root}=/usr/src/kandelo",
"-fdebug-prefix-map=#{root}=/usr/src/kandelo",
"-fmacro-prefix-map=#{root}=/usr/src/kandelo",
]
ENV["CFLAGS"] = ["-O2", "-gline-tables-only", "-fdebug-compilation-dir=.", *prefix_maps].join(" ")
# The SDK site owns target function facts. GNU Netcat's optional
# resolver-library probes are package-specific and libresolv is absent.
ENV["ac_cv_header_resolv_h"] = "no"
ENV["ac_cv_lib_resolv_main"] = "no"

configure_args = kandelo_std_configure_args
# Upstream's 2004 config.sub predates arm64 Darwin. This identifies the
# build machine only; the SDK wrapper supplies the Wasm target triplet.
configure_args << "--build=arm-apple-darwin" if OS.mac? && Hardware::CPU.arm?
system kandelo_configure, *configure_args,
"--disable-nls",
"--without-included-gettext"
system "make", "-j#{ENV.make_jobs}"
kandelo_validate_wasm_artifact(buildpath/"src/netcat", fork: :forbidden)
end

kandelo_install_bin(buildpath/"src", "netcat", "netcat")
bin.install_symlink "netcat" => "nc"
man1.install "doc/netcat.1"
end

test do
version_output = kandelo_run_wasm(bin/"netcat", ["--version"])
assert_match(/netcat \(The GNU Netcat\) 0\.7\.1/, version_output)

help_output = kandelo_run_wasm(bin/"nc", ["--help"], preserve_argv0: true)
assert_includes help_output, "-l, --listen"
assert_includes help_output, "-u, --udp"
assert_includes help_output, "-c, --close"

pair_output = kandelo_run_virtual_network_pairs(
bin/"netcat",
[
{
name: "tcp",
transport: "tcp",
serverArgs: %w[nc -n -l -p 25125 -w 3],
clientArgs: %w[nc -n -c 10.88.0.2 25125],
serverStdin: "",
clientStdin: "from-tcp\n",
expectedServerStdout: "from-tcp\n",
},
{
name: "udp",
transport: "udp",
serverArgs: %w[nc -n -c -u -l -p 25126 -w 3],
clientArgs: %w[nc -n -u -c 10.88.0.2 25126],
serverStdin: "",
clientStdin: "from-udp\n",
expectedServerStdout: "from-udp\n",
},
],
)
assert_includes pair_output, '"tcp"'
assert_includes pair_output, '"udp"'

binary = File.binread(bin/"netcat")
refute_includes binary, prefix.to_s
refute_match %r{/Users/[^/]+/}, binary
end
end

__END__
diff --git a/src/netcat.c b/src/netcat.c
index 8fd6b51..3c19d64 100644
--- a/src/netcat.c
+++ b/src/netcat.c
@@ -494,2 +494,5 @@ int main(int argc, char *argv[])
if (netcat_mode == NETCAT_LISTEN) {
+ /* A completed listen loop is successful; upstream leaves glob_ret at
+ its EXIT_FAILURE initializer. */
+ glob_ret = EXIT_SUCCESS;
if (opt_exec) {
diff --git a/src/core.c b/src/core.c
index 7e6f3dd..158720a 100644
--- a/src/core.c
+++ b/src/core.c
@@ -81,7 +81,11 @@ static int core_udp_listen(nc_sock_t *ncsock)
static int core_udp_listen(nc_sock_t *ncsock)
{
int ret, *sockbuf, sock, sock_max, timeout = ncsock->timeout;
- bool need_udphelper = TRUE;
+ /* GNU netcat's fallback enumerates interfaces with SIOCGIFCONF and binds
+ one socket per address. Kandelo exposes ordinary INADDR_ANY UDP bind,
+ but not that non-POSIX interface-enumeration ioctl. Select upstream's
+ single-socket path. */
+ bool need_udphelper = FALSE;
#ifdef USE_PKTINFO
int sockopt = 1;
#endif
diff --git a/src/netcat.h b/src/netcat.h
index 88a2974..3ee95f0 100644
--- a/src/netcat.h
+++ b/src/netcat.h
@@ -94 +94,5 @@
-# define USE_PKTINFO
+/* Kandelo's current POSIX socket layer exposes the constants via libc headers,
+ but does not implement IP_PKTINFO ancillary data. Keep GNU netcat on its
+ portable UDP path until the kernel supports the sockopt and recvmsg control
+ messages. */
+/* # define USE_PKTINFO */
34 changes: 34 additions & 0 deletions Kandelo/formula_support/kandelo_formula_support.rb
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,40 @@ def kandelo_run_framebuffer_wasm(
shell_output("cd #{Shellwords.escape(root)} && #{command} < /dev/null")
end

# Run paired programs on isolated Kandelo machines joined by the host's
# LocalVirtualNetwork. This exercises bind/listen/connect and datagram paths
# that a single-process formula runner cannot cover.
def kandelo_run_virtual_network_pairs(bin_path, cases)
root = kandelo_require_root!
if (node = ENV.fetch("HOMEBREW_KANDELO_NODE", nil)).to_s != ""
ENV.prepend_path "PATH", File.dirname(node)
end

wasm_path = Pathname(bin_path)
if wasm_path.extname != ".wasm"
staged_wasm = testpath/"#{wasm_path.basename}.wasm"
File.binwrite(staged_wasm, File.binread(wasm_path))
wasm_path = staged_wasm
end

config = JSON.generate({ cases: cases })
# Compiled host output shadows TypeScript source under tsx. Network-pair
# tests must exercise the checkout supplied by HOMEBREW_KANDELO_ROOT.
FileUtils.rm_rf(Pathname(root)/"host/dist")

runner = Pathname(__dir__)/"run-virtual-network-pairs.ts"
command = "cd #{Shellwords.escape(root)} && "
command << "KANDELO_FORMULA_VIRTUAL_PAIRS_JSON=#{Shellwords.escape(config)} "
command << "node --experimental-wasm-exnref --import tsx/esm "
command << "#{Shellwords.escape(runner.to_s)} #{Shellwords.escape(root)} "
command << Shellwords.escape(wasm_path.to_s)
command << " 2>&1"

output = shell_output(command)
kandelo_record_node_execution!(wasm_path, [], launcher: "kandelo_run_virtual_network_pairs")
output
end

def kandelo_record_node_execution!(wasm_path, argv, launcher: "kandelo_run_wasm")
receipt = ENV.fetch("HOMEBREW_KANDELO_NODE_RECEIPT_PATH", nil)
return if receipt.to_s.empty?
Expand Down
140 changes: 140 additions & 0 deletions Kandelo/formula_support/run-virtual-network-pairs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { join } from "node:path";
import { pathToFileURL } from "node:url";

interface PairCase {
name: string;
transport: "tcp" | "udp";
serverArgs: string[];
clientArgs: string[];
serverStdin: string;
clientStdin: string;
expectedServerStdout: string;
timeoutMs?: number;
}

interface PairConfig {
cases: PairCase[];
}

async function main(): Promise<void> {
const [root, programPath] = process.argv.slice(2);
if (!root || !programPath) {
throw new Error("usage: run-virtual-network-pairs.ts KANDELO_ROOT PROGRAM");
}

const config = JSON.parse(
process.env.KANDELO_FORMULA_VIRTUAL_PAIRS_JSON ?? "{}",
) as PairConfig;
if (!Array.isArray(config.cases) || config.cases.length === 0) {
throw new Error("KANDELO_FORMULA_VIRTUAL_PAIRS_JSON must contain cases");
}

const moduleUrl = (path: string) => pathToFileURL(join(root, path)).href;
const [
{ LocalVirtualNetwork },
{ NodePlatformIO },
{ runCentralizedProgram },
] = await Promise.all([
import(moduleUrl("host/src/networking/virtual-network.ts")),
import(moduleUrl("host/src/platform/node.ts")),
import(moduleUrl("host/test/centralized-test-helper.ts")),
]);

const summaries: Record<string, unknown> = {};
for (const pair of config.cases) {
const network = new LocalVirtualNetwork();
const serverIO = new NodePlatformIO();
const clientIO = new NodePlatformIO();
serverIO.network = network.attachMachine({
id: `${pair.name}-server`,
address: [10, 88, 0, 2],
hostnames: [`${pair.name}-server`],
});
clientIO.network = network.attachMachine({
id: `${pair.name}-client`,
address: [10, 88, 0, 3],
hostnames: [`${pair.name}-client`],
});

let resolveServerReady!: () => void;
const serverReady = new Promise<void>((resolve) => {
resolveServerReady = resolve;
});
if (pair.transport === "tcp") {
const listenTcp = serverIO.network.listenTcp?.bind(serverIO.network);
if (!listenTcp)
throw new Error("virtual network has no TCP listener support");
serverIO.network.listenTcp = (listenerId, addr, port, target) => {
const status = listenTcp(listenerId, addr, port, target);
if (status === 0) resolveServerReady();
return status;
};
} else if (pair.transport === "udp") {
const bindUdp = serverIO.network.bindUdp?.bind(serverIO.network);
if (!bindUdp) throw new Error("virtual network has no UDP bind support");
serverIO.network.bindUdp = (endpointId, addr, port, target) => {
const status = bindUdp(endpointId, addr, port, target);
if (status === 0) resolveServerReady();
return status;
};
} else {
throw new Error(
`${pair.name} has unsupported transport ${String(pair.transport)}`,
);
}

const timeout = pair.timeoutMs ?? 10_000;
const serverRun = runCentralizedProgram({
programPath,
argv: pair.serverArgs,
io: serverIO,
stdin: pair.serverStdin,
timeout,
});
await Promise.race([
serverReady,
serverRun.then((result) => {
throw new Error(
`${pair.name} server exited before ${pair.transport} readiness: ` +
JSON.stringify({
status: result.exitCode,
stdout: result.stdout,
stderr: result.stderr,
}),
);
}),
]);
const clientRun = runCentralizedProgram({
programPath,
argv: pair.clientArgs,
io: clientIO,
stdin: pair.clientStdin,
timeout,
});
const [server, client] = await Promise.all([serverRun, clientRun]);
const summary = {
serverStatus: server.exitCode,
clientStatus: client.exitCode,
serverStdout: server.stdout,
serverStderr: server.stderr,
clientStderr: client.stderr,
};
if (
server.exitCode !== 0 ||
client.exitCode !== 0 ||
server.stdout !== pair.expectedServerStdout ||
server.stderr !== "" ||
client.stderr !== ""
) {
throw new Error(`${pair.name} failed: ${JSON.stringify(summary)}`);
}
summaries[pair.name] = summary;
}

process.stdout.write(`${JSON.stringify(summaries)}\n`);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
26 changes: 26 additions & 0 deletions Kandelo/formula_support/test/kandelo_formula_support_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,32 @@ def test_pty_execution_rejects_an_empty_guest_argv0
assert_includes error.message, "guest argv0 must be a nonempty normalized absolute path"
end

def test_virtual_network_pairs_use_tap_owned_runner_and_remove_stale_host_dist
Dir.mktmpdir("kandelo-formula-support") do |dir|
root = Pathname(dir)/"kandelo root"
host_dist = root/"host/dist"
host_dist.mkpath
(host_dist/"stale.js").binwrite("stale")

harness = Harness.new
harness.root_path = root.to_s
output = harness.kandelo_run_virtual_network_pairs(
"program.wasm",
[{ name: "tcp", transport: "tcp", serverArgs: ["nc", "-l"], clientArgs: ["nc", "host"] }],
)

assert_equal "runtime-ok\n", output
assert_includes harness.command, "run-virtual-network-pairs.ts"
assert_includes harness.command, "KANDELO_FORMULA_VIRTUAL_PAIRS_JSON="
assert_includes harness.command, "serverArgs"
assert_includes harness.command, "clientArgs"
assert_includes harness.command, "transport"
assert_includes harness.command, "program.wasm"
assert_equal "kandelo_run_virtual_network_pairs", harness.recorded_launcher
refute_path_exists host_dist
end
end

private

def artifact_validation_harness(dir, harness_class = Harness)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Current migration controls and pilots include:
Kandelo's musl `catopen` and `catgets` implementation.
- `ctags`, Universal Ctags' maintained tag generator, `readtags` query client,
and optscript interpreter with complete C and C++ workflows.
- `netcat`, GNU TCP and UDP client/listener workflows across virtual Kandelo machines.

The SDK is not yet a Homebrew dependency. Trusted builds supply an
`HOMEBREW_KANDELO_ROOT` checkout containing the SDK, sysroot, kernel, and Node
Expand Down