From nobody Fri Mar 20 12:33:28 2026 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4fchn4526Rz6Vvmq for ; Fri, 20 Mar 2026 12:33:28 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R12" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4fchn439wyz3j7S for ; Fri, 20 Mar 2026 12:33:28 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1774010008; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=nOFJokkRWyV87RolXNLqfKMgC1T4VuEXlcLk2owhTgA=; b=pP0aa5oWygT1UZpSY80d9fzI94PH6WmHIyotPMSo4puVzuB3JChjrNdl+SHcwc+5oKjFmO 90sLSAUG6BVED+tigBbJjOCWJTG7OvMfRZzaElKXp6Of3GJSPO4z+HvLDGTNMQNWre3VDM klByLD3+xnZfsO3CEaI9652+ntmt2Kw0Jm+6eoen+S7VmZcltMbLv1WtxO9RN7uI1jd87t xTtFlbnb+v3Dgofo773LnFuOMsacFcVbIBUAML0gmPDrfp0Fzp3WmG2vECjALyXqZKNJYv OQqmABRM8TFsVHG5eB+dasS2abuhutCwi+MslKK/5Twpc6a9fslJdzkVrOmcwg== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1774010008; a=rsa-sha256; cv=none; b=Og4TZk6zJIrwxkv6JFw9XPdA5sXZ1io4KQu0laafXJgM9vReGskTLo8F2o4iZZBRX4kxej IJM+caSS03qv8LN7dRlGTU3J32nXhXLKzhUDzmiqIyjmsTHu6ZKifYA3WrZYhIWI1ZAWsQ eNL6fSbMD5640PnpskSj6AcIU3hgLVXhOjzHN3JzSl67NEF5fojwLSXO8tHvzQH9rN4pvk czCaqSEeoNzleh0R8jFH9VkeYzHefw11S6jY9MAj2MYjgDoWqhke8A0XopSgfH26eyp/J7 FGcwe2k5NhvBpfLN45iDl1URC83A96TL0b0pn6sX9omJILNgC86LQGjly0SEwQ== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1774010008; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=nOFJokkRWyV87RolXNLqfKMgC1T4VuEXlcLk2owhTgA=; b=wS6kMIBlVUNj6VAHf/suWRcwI+7yg160/PLE5p2zSTwE4b4xPYgI/qEPr5SAbZG8IW9xUV lH5LjwxU/OwyuM2uIT5aXLz2/xnucRXcV2ITI9dae6iA4fuwHmkq0zibMUQrpGW+AtPjhp lJs0oswngkLVpnxYDJ12APVxKeoiv4HGrQC4bppcIwrk8FAYgJ6YV679e6k3JM3yf5qSGm vVoEtUljwp4o45ycH/xodUeLIQBZikQC0gYaugyLCQhdrUTbtjexndtdbNl/vaB5zPuiMb nL8tBOmbaV3mcU94wrlwGy4LnSUWc5TgoIe6x0MhkmZZ4T48t4cV+QduLngQYA== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4fchn42GKdz9hq for ; Fri, 20 Mar 2026 12:33:28 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 3734b by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Fri, 20 Mar 2026 12:33:28 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Baptiste Daroussin Subject: git: b42e852e89cb - main - pkg-serve(8): serve pkg repositories over TCP via inetd (8) List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@FreeBSD.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: bapt X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: b42e852e89cb04cceb6e0226d6a08cab13fb6e90 Auto-Submitted: auto-generated Date: Fri, 20 Mar 2026 12:33:28 +0000 Message-Id: <69bd3e98.3734b.5710d059@gitrepo.freebsd.org> The branch main has been updated by bapt: URL: https://cgit.FreeBSD.org/src/commit/?id=b42e852e89cb04cceb6e0226d6a08cab13fb6e90 commit b42e852e89cb04cceb6e0226d6a08cab13fb6e90 Author: Baptiste Daroussin AuthorDate: 2026-03-17 11:02:28 +0000 Commit: Baptiste Daroussin CommitDate: 2026-03-20 12:29:48 +0000 pkg-serve(8): serve pkg repositories over TCP via inetd (8) Reviewed by: manu, bdrewery (previous version) Differential Revision: https://reviews.freebsd.org/D55895 --- etc/mtree/BSD.tests.dist | 2 + libexec/Makefile | 4 + libexec/pkg-serve/Makefile | 9 ++ libexec/pkg-serve/pkg-serve.8 | 107 ++++++++++++++ libexec/pkg-serve/pkg-serve.c | 180 +++++++++++++++++++++++ libexec/pkg-serve/tests/Makefile | 5 + libexec/pkg-serve/tests/pkg_serve_test.sh | 230 ++++++++++++++++++++++++++++++ share/mk/src.opts.mk | 1 + tools/build/mk/OptionalObsoleteFiles.inc | 5 + tools/build/options/WITHOUT_PKGSERVE | 2 + 10 files changed, 545 insertions(+) diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist index 4fa35d1a17d0..bb1e40e69ba0 100644 --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -477,6 +477,8 @@ .. nuageinit .. + pkg-serve + .. rc .. rtld-elf diff --git a/libexec/Makefile b/libexec/Makefile index 180dd10b5d29..bfcd55b255c7 100644 --- a/libexec/Makefile +++ b/libexec/Makefile @@ -65,6 +65,10 @@ _dma= dma _hyperv+= hyperv .endif +.if ${MK_PKGSERVE} != "no" +_pkgserve= pkg-serve +.endif + .if ${MK_NIS} != "no" _mknetid= mknetid _ypxfr= ypxfr diff --git a/libexec/pkg-serve/Makefile b/libexec/pkg-serve/Makefile new file mode 100644 index 000000000000..3ac6bf34be14 --- /dev/null +++ b/libexec/pkg-serve/Makefile @@ -0,0 +1,9 @@ +.include + +PROG= pkg-serve +MAN= pkg-serve.8 +BINDIR= /usr/libexec + +SUBDIR.${MK_TESTS}+= tests + +.include diff --git a/libexec/pkg-serve/pkg-serve.8 b/libexec/pkg-serve/pkg-serve.8 new file mode 100644 index 000000000000..b51daff029db --- /dev/null +++ b/libexec/pkg-serve/pkg-serve.8 @@ -0,0 +1,107 @@ +.\" Copyright (c) 2026 Baptiste Daroussin +.\" +.\" SPDX-License-Identifier: BSD-2-Clause +.\" +.Dd March 17, 2026 +.Dt PKG-SERVE 8 +.Os +.Sh NAME +.Nm pkg-serve +.Nd serve pkg repositories over TCP via inetd +.Sh SYNOPSIS +.Nm +.Ar basedir +.Sh DESCRIPTION +The +.Nm +utility serves +.Xr pkg 8 +repositories using the pkg TCP protocol. +It is designed to be invoked by +.Xr inetd 8 +and communicates via standard input and output. +.Pp +The +.Ar basedir +argument specifies the root directory containing the package repositories. +All file requests are resolved relative to this directory. +.Pp +On startup, +.Nm +enters a Capsicum sandbox, restricting filesystem access to +.Ar basedir . +.Sh PROTOCOL +The protocol is line-oriented. +Upon connection, the server sends a greeting: +.Bd -literal -offset indent +ok: pkg-serve +.Ed +.Pp +The client may then issue commands: +.Bl -tag -width "get file age" +.It Ic get Ar file age +Request a file. +If the file's modification time is newer than +.Ar age +(a Unix timestamp), the server responds with: +.Bd -literal -offset indent +ok: +.Ed +.Pp +followed by +.Ar size +bytes of file data. +If the file has not been modified, the server responds with: +.Bd -literal -offset indent +ok: 0 +.Ed +.Pp +On error, the server responds with: +.Bd -literal -offset indent +ko: +.Ed +.It Ic quit +Close the connection. +.El +.Sh INETD CONFIGURATION +Add the following line to +.Xr inetd.conf 5 : +.Bd -literal -offset indent +pkg stream tcp nowait nobody /usr/libexec/pkg-serve pkg-serve /usr/local/poudriere/data/packages +.Ed +.Pp +And define the service in +.Xr services 5 : +.Bd -literal -offset indent +pkg 62000/tcp +.Ed +.Sh REPOSITORY CONFIGURATION +On the client side, configure a repository in +.Pa /usr/local/etc/pkg/repos/myrepo.conf +to use the +.Ic tcp:// +scheme: +.Bd -literal -offset indent +myrepo: { + url: "tcp://pkgserver.example.com:62000/myrepo", +} +.Ed +.Pp +The path component of the URL is resolved relative to the +.Ar basedir +given to +.Nm . +For example, if +.Nm +is started with +.Pa /usr/local/poudriere/data/packages +as +.Ar basedir , +the above configuration will serve files from +.Pa /usr/local/poudriere/data/packages/myrepo/ . +.Sh SEE ALSO +.Xr inetd 8 , +.Xr inetd.conf 5 , +.Xr pkg 8 +.Sh AUTHORS +.An Baptiste Daroussin Aq Mt bapt@FreeBSD.org diff --git a/libexec/pkg-serve/pkg-serve.c b/libexec/pkg-serve/pkg-serve.c new file mode 100644 index 000000000000..56770ef37f88 --- /dev/null +++ b/libexec/pkg-serve/pkg-serve.c @@ -0,0 +1,180 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 Baptiste Daroussin + */ + +/* + * Speaks the same protocol as "pkg ssh" (see pkg-ssh(8)): + * -> ok: pkg-serve + * <- get + * -> ok: \n or ok: 0\n or ko: \n + * <- quit + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define VERSION "0.1" +#define BUFSZ 32768 + +static void +usage(void) +{ + fprintf(stderr, "usage: pkg-serve basedir\n"); + exit(EXIT_FAILURE); +} + +int +main(int argc, char *argv[]) +{ + struct stat st; + cap_rights_t rights; + char *line = NULL; + char *file, *age; + size_t linecap = 0, r, toread; + ssize_t linelen; + off_t remaining; + time_t mtime; + char *end; + int fd, ffd; + char buf[BUFSZ]; + const char *basedir; + + if (argc != 2) + usage(); + + basedir = argv[1]; + + if ((fd = open(basedir, O_DIRECTORY | O_RDONLY | O_CLOEXEC)) < 0) + err(EXIT_FAILURE, "open(%s)", basedir); + + cap_rights_init(&rights, CAP_READ, CAP_FSTATAT, CAP_LOOKUP, + CAP_FCNTL); + if (cap_rights_limit(fd, &rights) < 0 && errno != ENOSYS) + err(EXIT_FAILURE, "cap_rights_limit"); + + if (cap_enter() < 0 && errno != ENOSYS) + err(EXIT_FAILURE, "cap_enter"); + + printf("ok: pkg-serve " VERSION "\n"); + fflush(stdout); + + while ((linelen = getline(&line, &linecap, stdin)) > 0) { + /* trim newline */ + if (linelen > 0 && line[linelen - 1] == '\n') + line[--linelen] = '\0'; + + if (linelen == 0) + continue; + + if (strcmp(line, "quit") == 0) + break; + + if (strncmp(line, "get ", 4) != 0) { + printf("ko: unknown command '%s'\n", line); + fflush(stdout); + continue; + } + + file = line + 4; + + if (*file == '\0') { + printf("ko: bad command get, expecting 'get file age'\n"); + fflush(stdout); + continue; + } + + /* skip leading slash */ + if (*file == '/') + file++; + + /* find the age argument */ + age = file; + while (*age != '\0' && !isspace((unsigned char)*age)) + age++; + + if (*age == '\0') { + printf("ko: bad command get, expecting 'get file age'\n"); + fflush(stdout); + continue; + } + + *age++ = '\0'; + + /* skip whitespace */ + while (isspace((unsigned char)*age)) + age++; + + if (*age == '\0') { + printf("ko: bad command get, expecting 'get file age'\n"); + fflush(stdout); + continue; + } + + errno = 0; + mtime = (time_t)strtoimax(age, &end, 10); + if (errno != 0 || *end != '\0' || end == age) { + printf("ko: bad number %s\n", age); + fflush(stdout); + continue; + } + + if (fstatat(fd, file, &st, AT_RESOLVE_BENEATH) == -1) { + printf("ko: file not found\n"); + fflush(stdout); + continue; + } + + if (!S_ISREG(st.st_mode)) { + printf("ko: not a file\n"); + fflush(stdout); + continue; + } + + if (st.st_mtime <= mtime) { + printf("ok: 0\n"); + fflush(stdout); + continue; + } + + if ((ffd = openat(fd, file, O_RDONLY | O_RESOLVE_BENEATH)) == -1) { + printf("ko: file not found\n"); + fflush(stdout); + continue; + } + + printf("ok: %" PRIdMAX "\n", (intmax_t)st.st_size); + fflush(stdout); + + remaining = st.st_size; + while (remaining > 0) { + toread = sizeof(buf); + if ((off_t)toread > remaining) + toread = (size_t)remaining; + r = read(ffd, buf, toread); + if (r <= 0) + break; + if (fwrite(buf, 1, r, stdout) != r) + break; + remaining -= r; + } + close(ffd); + if (remaining > 0) + errx(EXIT_FAILURE, "%s: file truncated during transfer", + file); + fflush(stdout); + } + + return (EXIT_SUCCESS); +} diff --git a/libexec/pkg-serve/tests/Makefile b/libexec/pkg-serve/tests/Makefile new file mode 100644 index 000000000000..9c62cf855f83 --- /dev/null +++ b/libexec/pkg-serve/tests/Makefile @@ -0,0 +1,5 @@ +PACKAGE= tests + +ATF_TESTS_SH= pkg_serve_test + +.include diff --git a/libexec/pkg-serve/tests/pkg_serve_test.sh b/libexec/pkg-serve/tests/pkg_serve_test.sh new file mode 100644 index 000000000000..fc9cf2101369 --- /dev/null +++ b/libexec/pkg-serve/tests/pkg_serve_test.sh @@ -0,0 +1,230 @@ +#- +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2026 Baptiste Daroussin + +PKG_SERVE="${PKG_SERVE:-/usr/libexec/pkg-serve}" + +serve() +{ + printf "$1" | "${PKG_SERVE}" "$2" +} + +check_output() +{ + local pattern="$1" ; shift + output=$(serve "$@") + case "$output" in + *${pattern}*) + return 0 + ;; + *) + echo "Expected pattern: ${pattern}" + echo "Got: ${output}" + return 1 + ;; + esac +} + +atf_test_case greeting +greeting_head() +{ + atf_set "descr" "Server sends greeting on connect" +} +greeting_body() +{ + mkdir repo + check_output "ok: pkg-serve " "quit\n" repo || + atf_fail "greeting not found" +} + +atf_test_case unknown_command +unknown_command_head() +{ + atf_set "descr" "Unknown commands get ko response" +} +unknown_command_body() +{ + mkdir repo + check_output "ko: unknown command 'plop'" "plop\nquit\n" repo || + atf_fail "expected ko for unknown command" +} + +atf_test_case get_missing_file +get_missing_file_head() +{ + atf_set "descr" "Requesting a missing file returns ko" +} +get_missing_file_body() +{ + mkdir repo + check_output "ko: file not found" "get nonexistent.pkg 0\nquit\n" repo || + atf_fail "expected file not found" +} + +atf_test_case get_file +get_file_head() +{ + atf_set "descr" "Requesting an existing file returns its content" +} +get_file_body() +{ + mkdir repo + echo "testcontent" > repo/test.pkg + output=$(serve "get test.pkg 0\nquit\n" repo) + echo "$output" | grep -q "ok: 12" || + atf_fail "expected ok: 12, got: ${output}" + echo "$output" | grep -q "testcontent" || + atf_fail "expected testcontent in output" +} + +atf_test_case get_file_leading_slash +get_file_leading_slash_head() +{ + atf_set "descr" "Leading slash in path is stripped" +} +get_file_leading_slash_body() +{ + mkdir repo + echo "testcontent" > repo/test.pkg + check_output "ok: 12" "get /test.pkg 0\nquit\n" repo || + atf_fail "leading slash not stripped" +} + +atf_test_case get_file_uptodate +get_file_uptodate_head() +{ + atf_set "descr" "File with old mtime returns ok: 0" +} +get_file_uptodate_body() +{ + mkdir repo + echo "testcontent" > repo/test.pkg + check_output "ok: 0" "get test.pkg 9999999999\nquit\n" repo || + atf_fail "expected ok: 0 for up-to-date file" +} + +atf_test_case get_directory +get_directory_head() +{ + atf_set "descr" "Requesting a directory returns ko" +} +get_directory_body() +{ + mkdir -p repo/subdir + check_output "ko: not a file" "get subdir 0\nquit\n" repo || + atf_fail "expected not a file" +} + +atf_test_case get_missing_age +get_missing_age_head() +{ + atf_set "descr" "get without age argument returns error" +} +get_missing_age_body() +{ + mkdir repo + check_output "ko: bad command get" "get test.pkg\nquit\n" repo || + atf_fail "expected bad command get" +} + +atf_test_case get_bad_age +get_bad_age_head() +{ + atf_set "descr" "get with non-numeric age returns error" +} +get_bad_age_body() +{ + mkdir repo + check_output "ko: bad number" "get test.pkg notanumber\nquit\n" repo || + atf_fail "expected bad number" +} + +atf_test_case get_empty_arg +get_empty_arg_head() +{ + atf_set "descr" "get with no arguments returns error" +} +get_empty_arg_body() +{ + mkdir repo + check_output "ko: bad command get" "get \nquit\n" repo || + atf_fail "expected bad command get" +} + +atf_test_case path_traversal +path_traversal_head() +{ + atf_set "descr" "Path traversal with .. is rejected" +} +path_traversal_body() +{ + mkdir repo + check_output "ko: file not found" \ + "get ../etc/passwd 0\nquit\n" repo || + atf_fail "path traversal not rejected" +} + +atf_test_case get_subdir_file +get_subdir_file_head() +{ + atf_set "descr" "Files in subdirectories are served" +} +get_subdir_file_body() +{ + mkdir -p repo/sub + echo "subcontent" > repo/sub/file.pkg + output=$(serve "get sub/file.pkg 0\nquit\n" repo) + echo "$output" | grep -q "ok: 11" || + atf_fail "expected ok: 11, got: ${output}" + echo "$output" | grep -q "subcontent" || + atf_fail "expected subcontent in output" +} + +atf_test_case multiple_gets +multiple_gets_head() +{ + atf_set "descr" "Multiple get commands in one session" +} +multiple_gets_body() +{ + mkdir repo + echo "aaa" > repo/a.pkg + echo "bbb" > repo/b.pkg + output=$(serve "get a.pkg 0\nget b.pkg 0\nquit\n" repo) + echo "$output" | grep -q "ok: 4" || + atf_fail "expected ok: 4 for a.pkg" + echo "$output" | grep -q "aaa" || + atf_fail "expected content of a.pkg" + echo "$output" | grep -q "bbb" || + atf_fail "expected content of b.pkg" +} + +atf_test_case bad_basedir +bad_basedir_head() +{ + atf_set "descr" "Non-existent basedir causes exit failure" +} +bad_basedir_body() +{ + atf_check -s not-exit:0 -e match:"open" \ + "${PKG_SERVE}" /nonexistent/path +} + +atf_init_test_cases() +{ + atf_add_test_case greeting + atf_add_test_case unknown_command + atf_add_test_case get_missing_file + atf_add_test_case get_file + atf_add_test_case get_file_leading_slash + atf_add_test_case get_file_uptodate + atf_add_test_case get_directory + atf_add_test_case get_missing_age + atf_add_test_case get_bad_age + atf_add_test_case get_empty_arg + atf_add_test_case path_traversal + atf_add_test_case get_subdir_file + atf_add_test_case multiple_gets + atf_add_test_case bad_basedir +} diff --git a/share/mk/src.opts.mk b/share/mk/src.opts.mk index fa91c537188d..5a5bdd16298e 100644 --- a/share/mk/src.opts.mk +++ b/share/mk/src.opts.mk @@ -156,6 +156,7 @@ __DEFAULT_YES_OPTIONS = \ PAM \ PF \ PKGBOOTSTRAP \ + PKGSERVE \ PMC \ PPP \ PTHREADS_ASSERTIONS \ diff --git a/tools/build/mk/OptionalObsoleteFiles.inc b/tools/build/mk/OptionalObsoleteFiles.inc index fd25359c7c0b..3a45c7c9a01c 100644 --- a/tools/build/mk/OptionalObsoleteFiles.inc +++ b/tools/build/mk/OptionalObsoleteFiles.inc @@ -6911,6 +6911,11 @@ OLD_FILES+=usr/share/snmp/defs/pf_tree.def OLD_FILES+=usr/share/snmp/mibs/BEGEMOT-PF-MIB.txt .endif +.if ${MK_PKGSERVE} == no +OLD_FILES+=usr/libexec/pkg-serve +OLD_FILES+=usr/share/man/man8/pkg-serve.8.gz +.endif + .if ${MK_PKGBOOTSTRAP} == no OLD_FILES+=usr/sbin/pkg OLD_FILES+=usr/share/man/man7/pkg.7.gz diff --git a/tools/build/options/WITHOUT_PKGSERVE b/tools/build/options/WITHOUT_PKGSERVE new file mode 100644 index 000000000000..ecd3602fc03d --- /dev/null +++ b/tools/build/options/WITHOUT_PKGSERVE @@ -0,0 +1,2 @@ +Do not build or install +.Xr pkg-serve 8 .