From 7a6310c93e4d2a801c5a2704a4d19096c9790b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Cluseau?= Date: Thu, 7 May 2026 23:41:29 +0200 Subject: [PATCH] support UKI --- Dockerfile | 8 +- cmd/dkl-local-server/boot-img.go | 104 ++++++++++--------- cmd/dkl-local-server/boot-tar.go | 167 +++++++++++++++++++++++-------- cmd/dkl-local-server/main.go | 7 +- cmd/dkl-local-server/ws-host.go | 49 ++++++--- hack/build | 2 +- hack/docker-build | 2 +- 7 files changed, 230 insertions(+), 109 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2682230..58def4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,11 +27,11 @@ from debian:trixie entrypoint ["/bin/dkl-local-server"] env _uncache=1 -run apt-get update \ - && yes |apt-get install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \ +run apt update \ + && yes |apt install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \ grub2 grub-pc-bin grub-efi-amd64-bin ca-certificates curl openssh-client qemu-utils wireguard-tools \ - erofs-utils erofsfuse cryptsetup \ - && apt-get clean + erofs-utils erofsfuse cryptsetup systemd-boot-efi \ + && apt clean copy --from=dkl /bin/dkl /bin/dls /bin/ copy --from=build /src/dist/ /bin/ diff --git a/cmd/dkl-local-server/boot-img.go b/cmd/dkl-local-server/boot-img.go index dd1a17a..7fce2a4 100644 --- a/cmd/dkl-local-server/boot-img.go +++ b/cmd/dkl-local-server/boot-img.go @@ -12,18 +12,16 @@ import ( "path/filepath" "strings" "syscall" - - "github.com/pierrec/lz4" ) -func buildBootImg(out io.Writer, ctx *renderContext) (err error) { +func buildBootImg(out io.Writer, ctx *renderContext, uki bool, cmdline string) (err error) { bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-") if err != nil { return } defer rmTempFile(bootImg) - err = setupBootImage(bootImg, ctx) + err = setupBootImage(bootImg, ctx, uki, cmdline) if err != nil { return } @@ -34,21 +32,10 @@ func buildBootImg(out io.Writer, ctx *renderContext) (err error) { return } -func buildBootImgLZ4(out io.Writer, ctx *renderContext) (err error) { - lz4Out := lz4.NewWriter(out) - - if err = buildBootImg(lz4Out, ctx); err != nil { - return - } - - lz4Out.Close() - return -} - -func buildBootImgGZ(out io.Writer, ctx *renderContext) (err error) { +func buildBootImgGZ(out io.Writer, ctx *renderContext, uki bool, cmdline string) (err error) { gzOut := gzip.NewWriter(out) - if err = buildBootImg(gzOut, ctx); err != nil { + if err = buildBootImg(gzOut, ctx, uki, cmdline); err != nil { return } @@ -56,7 +43,7 @@ func buildBootImgGZ(out io.Writer, ctx *renderContext) (err error) { return } -func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) (err error) { +func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string, uki bool, cmdline string) (err error) { imgPath, err := func() (imgPath string, err error) { bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-") if err != nil { @@ -64,7 +51,7 @@ func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) ( } defer rmTempFile(bootImg) - err = setupBootImage(bootImg, ctx) + err = setupBootImage(bootImg, ctx, uki, cmdline) if err != nil { return } @@ -100,37 +87,56 @@ func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) ( io.Copy(out, img) return } -func qemuImgBootImg(format string) func(out io.Writer, ctx *renderContext) (err error) { +func qemuImgBootImg(format string, uki bool, cmdline string) func(out io.Writer, ctx *renderContext) (err error) { return func(out io.Writer, ctx *renderContext) (err error) { - return buildBootImgQemuConvert(out, ctx, format) + return buildBootImgQemuConvert(out, ctx, format, uki, cmdline) } } var grubSupportVersion = flag.String("grub-support", "1.1.0", "GRUB support version") -func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) { - path, err := distFetch("grub-support", *grubSupportVersion) - if err != nil { - return +func setupBootImage(bootImg *os.File, ctx *renderContext, uki bool, cmdline string) (err error) { + if uki { + err = bootImg.Truncate(128 << 20) + if err != nil { + return + } + err = bootImg.Sync() + if err != nil { + return + } + + err = run("sgdisk", bootImg.Name(), "--clear", "--largest-new=1", "--typecode=1:EF00", "--change-name=1:ESP") + if err != nil { + return + } + + } else { + err = func() (err error) { + path, err := distFetch("grub-support", *grubSupportVersion) + if err != nil { + return + } + + baseImage, err := os.Open(path) + if err != nil { + return + } + + defer baseImage.Close() + + baseImageGz, err := gzip.NewReader(baseImage) + if err != nil { + return + } + + defer baseImageGz.Close() + _, err = io.Copy(bootImg, baseImageGz) + return + }() } - - baseImage, err := os.Open(path) if err != nil { - return - } - - defer baseImage.Close() - - baseImageGz, err := gzip.NewReader(baseImage) - if err != nil { - return - } - - defer baseImageGz.Close() - _, err = io.Copy(bootImg, baseImageGz) - - if err != nil { - return + return err } log.Print("running losetup...") @@ -163,6 +169,14 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) { }() devp1 := dev + "p1" + + if uki { + err = run("mkfs.vfat", "-F", "32", devp1) + if err != nil { + return fmt.Errorf("mkfs.vfat: %w", err) + } + } + err = syscall.Mount(devp1, tempDir, "vfat", 0, "") if err != nil { return fmt.Errorf("failed to mount %s to %s: %v", devp1, tempDir, err) @@ -176,10 +190,10 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) { // add system elements tarOut, tarIn := io.Pipe() go func() { - err2 := buildBootTar(tarIn, ctx) + e := buildBootTar(tarIn, ctx, uki, cmdline) tarIn.Close() - if err2 != nil { - err = err2 + if e != nil { + err = e } }() diff --git a/cmd/dkl-local-server/boot-tar.go b/cmd/dkl-local-server/boot-tar.go index 74b1df2..6b7adee 100644 --- a/cmd/dkl-local-server/boot-tar.go +++ b/cmd/dkl-local-server/boot-tar.go @@ -3,11 +3,13 @@ package main import ( "archive/tar" "bytes" + "debug/pe" + "fmt" "io" "log" "os" - - "novit.tech/direktil/local-server/pkg/utf16" + "path/filepath" + "time" ) func rmTempFile(f *os.File) { @@ -17,7 +19,124 @@ func rmTempFile(f *os.File) { } } -func buildBootTar(out io.Writer, ctx *renderContext) (err error) { +func buildUki(out io.Writer, ctx *renderContext, uki bool, cmdline string) error { + if !uki { + return fmt.Errorf("buildUki only builds UKI") + } + + const efiStubPath = "/usr/lib/systemd/boot/efi/linuxx64.efi.stub" + + stub, err := pe.Open(efiStubPath) + if err != nil { + return fmt.Errorf("efi stub open: %w", err) + } + + hdr := stub.OptionalHeader.(*pe.OptionalHeader64) + align := uint64(hdr.SectionAlignment) + + var offset uint64 + for _, s := range stub.Sections { + end := uint64(s.VirtualAddress) + uint64(s.VirtualSize) + if end > offset { + offset = end + } + } + + offset += hdr.ImageBase + + stub.Close() + + // kernel + kernelPath, err := distFetch("kernels", ctx.Host.Kernel) + if err != nil { + return fmt.Errorf("fetch kernel: %w", err) + } + + // temp dir + tempDir, err := os.MkdirTemp(os.TempDir(), "uki-") + if err != nil { + return fmt.Errorf("mkdir temp: %w", err) + } + defer os.RemoveAll(tempDir) + + // osrel + osrelPath := filepath.Join(tempDir, "osrel") + if err := os.WriteFile(osrelPath, fmt.Appendf(nil, "ID=direktil\nPRETTY_NAME='Direktil %s %s+0'\n", ctx.Host.Name, + time.Now().UTC().Format(time.DateTime)), 0o600); err != nil { + return fmt.Errorf("create osrel: %w", err) + } + + // cmdline + cmdlinePath := filepath.Join(tempDir, "cmdline") + if err := os.WriteFile(cmdlinePath, append([]byte(cmdline), 0), 0o600); err != nil { + return fmt.Errorf("create cmdline: %w", err) + } + + // initrd + initrdPath := filepath.Join(tempDir, "initrd") + if err := func() (err error) { + initrd, err := os.Create(initrdPath) + if err != nil { + return + } + defer initrd.Close() + return buildInitrd(initrd, ctx) + }(); err != nil { + return fmt.Errorf("create initrd: %w", err) + } + + // assemble + + args := make([]string, 0) + for _, i := range []struct { + section string + path string + }{ + {"osrel", osrelPath}, + {"cmdline", cmdlinePath}, + {"initrd", initrdPath}, + {"linux", kernelPath}, + } { + offset += align - offset%align + args = append(args, + "--add-section", "."+i.section+"="+i.path, "--change-section-vma", fmt.Sprintf(".%s=0x%x", i.section, offset)) + + stat, err := os.Stat(i.path) + if err != nil { + return fmt.Errorf("stat %s: %w", i.section, err) + } + + offset += uint64(stat.Size()) + } + + ukiPath := filepath.Join(tempDir, "uki") + args = append(args, efiStubPath, ukiPath) + + if err := run("objcopy", args...); err != nil { + return fmt.Errorf("objcopy: %w", err) + } + + // read + ukiBytes, err := os.ReadFile(ukiPath) + if err != nil { + return fmt.Errorf("read uki: %w", err) + } + + io.Copy(out, bytes.NewBuffer(ukiBytes)) + + // done + return nil +} + +func buildBootTar(out io.Writer, ctx *renderContext, uki bool, cmdline string) (err error) { + if uki { + return buildUkiBootTar(out, ctx, cmdline) + } else { + return buildGrubBootTar(out, ctx) + } +} + +func buildGrubBootTar(out io.Writer, ctx *renderContext) (err error) { arch := tar.NewWriter(out) defer arch.Close() @@ -62,7 +181,7 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) { return nil } -func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) { +func buildUkiBootTar(out io.Writer, ctx *renderContext, cmdline string) (err error) { arch := tar.NewWriter(out) defer arch.Close() @@ -75,46 +194,14 @@ func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) { return } - const ( - prefix = "EFI/dkl/" - efiPrefix = "\\EFI\\dkl\\" - ) - - // boot.csv - // -> annoyingly it's UTF-16... - bootCsvBytes := utf16.FromUTF8([]byte("" + - "current_kernel.efi,dkl current,initrd=" + efiPrefix + "current_initrd.img,Direktil current\n" + - "previous_kernel.efi,dkl previous,initrd=" + efiPrefix + "previous_initrd.img,Direktil previous\n")) - - err = archAdd(prefix+"BOOT.CSV", []byte(bootCsvBytes)) + // UKI + uki := new(bytes.Buffer) + err = buildUki(uki, ctx, true, cmdline) if err != nil { return } - // kernel - kernelPath, err := distFetch("kernels", ctx.Host.Kernel) - if err != nil { - return - } - - kernelBytes, err := os.ReadFile(kernelPath) - if err != nil { - return - } - - err = archAdd(prefix+"current_kernel.efi", kernelBytes) - if err != nil { - return - } - - // initrd - initrd := new(bytes.Buffer) - err = buildInitrd(initrd, ctx) - if err != nil { - return - } - - err = archAdd(prefix+"current_initrd.img", initrd.Bytes()) + err = archAdd("EFI/BOOT/BOOTX64.EFI", uki.Bytes()) if err != nil { return } diff --git a/cmd/dkl-local-server/main.go b/cmd/dkl-local-server/main.go index b72663f..0dd0883 100644 --- a/cmd/dkl-local-server/main.go +++ b/cmd/dkl-local-server/main.go @@ -18,7 +18,10 @@ const ( etcDir = "/etc/direktil" ) -var Version = "dev" +var ( + Version = "dev" + Date = "now" +) var ( address = flag.String("address", ":7606", "HTTP listen address") @@ -38,7 +41,7 @@ func main() { log.Fatal("no listen address given") } - log.Print("Direktil local-server version ", Version) + log.Print("Direktil local-server version ", Version, " (", Date, ")") wPublicState.Change(func(s *PublicState) { s.ServerVersion = Version }) computeUIHash() diff --git a/cmd/dkl-local-server/ws-host.go b/cmd/dkl-local-server/ws-host.go index b344ac5..d0e76f8 100644 --- a/cmd/dkl-local-server/ws-host.go +++ b/cmd/dkl-local-server/ws-host.go @@ -2,6 +2,7 @@ package main import ( "flag" + "io" "log" "net/http" "path" @@ -75,10 +76,7 @@ func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBu // metal/local HDD upgrades b("boot.tar"). Produces(mime.TAR). - Doc("Get the " + ws.hostDoc + "'s /boot archive (ie: for metal upgrades)"), - b("boot-efi.tar"). - Produces(mime.TAR). - Doc("Get the " + ws.hostDoc + "'s /boot archive (ie: for metal upgrades)"), + Doc("Get the " + ws.hostDoc + "'s /boot archive"), // read-only ISO support b("boot.iso"). @@ -98,6 +96,9 @@ func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBu b("initrd"). Produces(mime.OCTET). Doc("Get the " + ws.hostDoc + "'s initial RAM disk (ie: for netboot)"), + b("uki"). + Produces(mime.OCTET). + Doc("Get the " + ws.hostDoc + "'s Unified Kernel Image"), // - bootstrap config b("bootstrap-config"). @@ -169,6 +170,23 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local return } + cmdline := r.FormValue("cmdline") + + if s := r.FormValue("serial"); s != "" { + if cmdline != "" { + cmdline += " " + } + cmdline += "console=ttyS" + s + ",115200" + } + + _, uki := r.Form["uki"] + + withUki := func(callback func(out io.Writer, ctx *renderContext, uki bool, cmdline string) (err error)) func(io.Writer, *renderContext) error { + return func(out io.Writer, ctx *renderContext) error { + return callback(out, ctx, uki, cmdline) + } + } + switch what { case "config": err = renderConfig(w, r, ctx, false) @@ -182,32 +200,31 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local err = renderKernel(w, r, ctx) case "initrd": err = renderCtx(w, r, ctx, what, buildInitrd) + case "uki": + err = renderCtx(w, r, ctx, what, withUki(buildUki)) + case "bootstrap.tar": err = renderCtx(w, r, ctx, what, buildBootstrap) case "boot.img": - err = renderCtx(w, r, ctx, what, buildBootImg) + err = renderCtx(w, r, ctx, what, withUki(buildBootImg)) case "boot.img.gz": - err = renderCtx(w, r, ctx, what, buildBootImgGZ) - case "boot.img.lz4": - err = renderCtx(w, r, ctx, what, buildBootImgLZ4) + err = renderCtx(w, r, ctx, what, withUki(buildBootImgGZ)) case "boot.qcow2": - err = renderCtx(w, r, ctx, what, qemuImgBootImg("qcow2")) + err = renderCtx(w, r, ctx, what, qemuImgBootImg("qcow2", uki, cmdline)) case "boot.qed": - err = renderCtx(w, r, ctx, what, qemuImgBootImg("qed")) + err = renderCtx(w, r, ctx, what, qemuImgBootImg("qed", uki, cmdline)) case "boot.vdi": - err = renderCtx(w, r, ctx, what, qemuImgBootImg("vdi")) + err = renderCtx(w, r, ctx, what, qemuImgBootImg("vdi", uki, cmdline)) case "boot.vmdk": - err = renderCtx(w, r, ctx, what, qemuImgBootImg("vmdk")) + err = renderCtx(w, r, ctx, what, qemuImgBootImg("vmdk", uki, cmdline)) case "boot.vpc": - err = renderCtx(w, r, ctx, what, qemuImgBootImg("vpc")) + err = renderCtx(w, r, ctx, what, qemuImgBootImg("vpc", uki, cmdline)) case "boot.iso": err = renderCtx(w, r, ctx, what, buildBootISO) case "boot.tar": - err = renderCtx(w, r, ctx, what, buildBootTar) - case "boot-efi.tar": - err = renderCtx(w, r, ctx, what, buildBootEFITar) + err = renderCtx(w, r, ctx, what, withUki(buildBootTar)) // boot v2 case "bootstrap-config": diff --git a/hack/build b/hack/build index cb69503..5d3657e 100755 --- a/hack/build +++ b/hack/build @@ -1,3 +1,3 @@ #! /bin/sh set -ex -go build -o dist/ -trimpath -ldflags "-X main.Version=${GIT_TAG:-$(git describe --always --dirty)}" $* +go build -o dist/ -trimpath -ldflags "-X main.Version=${GIT_TAG:-$(git describe --always --dirty)} -X main.Date=$(date --utc +%Y%m%d_%H%M%S)" $* diff --git a/hack/docker-build b/hack/docker-build index 8413d2b..ae27fc2 100755 --- a/hack/docker-build +++ b/hack/docker-build @@ -8,4 +8,4 @@ commit) tag=$GIT_TAG ;; "") tag=latest ;; *) tag=$1 ;; esac -docker build -t novit.tech/direktil/local-server:$tag . --build-arg GIT_TAG=$GIT_TAG +docker build --network=host -t novit.tech/direktil/local-server:$tag . --build-arg GIT_TAG=$GIT_TAG