support UKI

This commit is contained in:
Mikaël Cluseau
2026-05-07 23:41:29 +02:00
parent e89b164581
commit 7a6310c93e
7 changed files with 230 additions and 109 deletions
+4 -4
View File
@@ -27,11 +27,11 @@ from debian:trixie
entrypoint ["/bin/dkl-local-server"] entrypoint ["/bin/dkl-local-server"]
env _uncache=1 env _uncache=1
run apt-get update \ run apt update \
&& yes |apt-get install -y genisoimage gdisk dosfstools util-linux udev binutils systemd \ && 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 \ grub2 grub-pc-bin grub-efi-amd64-bin ca-certificates curl openssh-client qemu-utils wireguard-tools \
erofs-utils erofsfuse cryptsetup \ erofs-utils erofsfuse cryptsetup systemd-boot-efi \
&& apt-get clean && apt clean
copy --from=dkl /bin/dkl /bin/dls /bin/ copy --from=dkl /bin/dkl /bin/dls /bin/
copy --from=build /src/dist/ /bin/ copy --from=build /src/dist/ /bin/
+41 -27
View File
@@ -12,18 +12,16 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "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-") bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
if err != nil { if err != nil {
return return
} }
defer rmTempFile(bootImg) defer rmTempFile(bootImg)
err = setupBootImage(bootImg, ctx) err = setupBootImage(bootImg, ctx, uki, cmdline)
if err != nil { if err != nil {
return return
} }
@@ -34,21 +32,10 @@ func buildBootImg(out io.Writer, ctx *renderContext) (err error) {
return return
} }
func buildBootImgLZ4(out io.Writer, ctx *renderContext) (err error) { func buildBootImgGZ(out io.Writer, ctx *renderContext, uki bool, cmdline string) (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) {
gzOut := gzip.NewWriter(out) gzOut := gzip.NewWriter(out)
if err = buildBootImg(gzOut, ctx); err != nil { if err = buildBootImg(gzOut, ctx, uki, cmdline); err != nil {
return return
} }
@@ -56,7 +43,7 @@ func buildBootImgGZ(out io.Writer, ctx *renderContext) (err error) {
return 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) { imgPath, err := func() (imgPath string, err error) {
bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-") bootImg, err := os.CreateTemp(os.TempDir(), "boot.img-")
if err != nil { if err != nil {
@@ -64,7 +51,7 @@ func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) (
} }
defer rmTempFile(bootImg) defer rmTempFile(bootImg)
err = setupBootImage(bootImg, ctx) err = setupBootImage(bootImg, ctx, uki, cmdline)
if err != nil { if err != nil {
return return
} }
@@ -100,15 +87,32 @@ func buildBootImgQemuConvert(out io.Writer, ctx *renderContext, format string) (
io.Copy(out, img) io.Copy(out, img)
return 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 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") var grubSupportVersion = flag.String("grub-support", "1.1.0", "GRUB support version")
func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) { 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) path, err := distFetch("grub-support", *grubSupportVersion)
if err != nil { if err != nil {
return return
@@ -128,9 +132,11 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
defer baseImageGz.Close() defer baseImageGz.Close()
_, err = io.Copy(bootImg, baseImageGz) _, err = io.Copy(bootImg, baseImageGz)
if err != nil {
return return
}()
}
if err != nil {
return err
} }
log.Print("running losetup...") log.Print("running losetup...")
@@ -163,6 +169,14 @@ func setupBootImage(bootImg *os.File, ctx *renderContext) (err error) {
}() }()
devp1 := dev + "p1" 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, "") err = syscall.Mount(devp1, tempDir, "vfat", 0, "")
if err != nil { if err != nil {
return fmt.Errorf("failed to mount %s to %s: %v", devp1, tempDir, err) 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 // add system elements
tarOut, tarIn := io.Pipe() tarOut, tarIn := io.Pipe()
go func() { go func() {
err2 := buildBootTar(tarIn, ctx) e := buildBootTar(tarIn, ctx, uki, cmdline)
tarIn.Close() tarIn.Close()
if err2 != nil { if e != nil {
err = err2 err = e
} }
}() }()
+127 -40
View File
@@ -3,11 +3,13 @@ package main
import ( import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"debug/pe"
"fmt"
"io" "io"
"log" "log"
"os" "os"
"path/filepath"
"novit.tech/direktil/local-server/pkg/utf16" "time"
) )
func rmTempFile(f *os.File) { 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) arch := tar.NewWriter(out)
defer arch.Close() defer arch.Close()
@@ -62,7 +181,7 @@ func buildBootTar(out io.Writer, ctx *renderContext) (err error) {
return nil 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) arch := tar.NewWriter(out)
defer arch.Close() defer arch.Close()
@@ -75,46 +194,14 @@ func buildBootEFITar(out io.Writer, ctx *renderContext) (err error) {
return return
} }
const ( // UKI
prefix = "EFI/dkl/" uki := new(bytes.Buffer)
efiPrefix = "\\EFI\\dkl\\" err = buildUki(uki, ctx, true, cmdline)
)
// 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))
if err != nil { if err != nil {
return return
} }
// kernel err = archAdd("EFI/BOOT/BOOTX64.EFI", uki.Bytes())
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())
if err != nil { if err != nil {
return return
} }
+5 -2
View File
@@ -18,7 +18,10 @@ const (
etcDir = "/etc/direktil" etcDir = "/etc/direktil"
) )
var Version = "dev" var (
Version = "dev"
Date = "now"
)
var ( var (
address = flag.String("address", ":7606", "HTTP listen address") address = flag.String("address", ":7606", "HTTP listen address")
@@ -38,7 +41,7 @@ func main() {
log.Fatal("no listen address given") 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 }) wPublicState.Change(func(s *PublicState) { s.ServerVersion = Version })
computeUIHash() computeUIHash()
+33 -16
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"flag" "flag"
"io"
"log" "log"
"net/http" "net/http"
"path" "path"
@@ -75,10 +76,7 @@ func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBu
// metal/local HDD upgrades // metal/local HDD upgrades
b("boot.tar"). b("boot.tar").
Produces(mime.TAR). Produces(mime.TAR).
Doc("Get the " + ws.hostDoc + "'s /boot archive (ie: for metal upgrades)"), Doc("Get the " + ws.hostDoc + "'s /boot archive"),
b("boot-efi.tar").
Produces(mime.TAR).
Doc("Get the " + ws.hostDoc + "'s /boot archive (ie: for metal upgrades)"),
// read-only ISO support // read-only ISO support
b("boot.iso"). b("boot.iso").
@@ -98,6 +96,9 @@ func (ws wsHost) register(rws *restful.WebService, alterRB func(*restful.RouteBu
b("initrd"). b("initrd").
Produces(mime.OCTET). Produces(mime.OCTET).
Doc("Get the " + ws.hostDoc + "'s initial RAM disk (ie: for netboot)"), 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 // - bootstrap config
b("bootstrap-config"). b("bootstrap-config").
@@ -169,6 +170,23 @@ func renderHost(w http.ResponseWriter, r *http.Request, what string, host *local
return 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 { switch what {
case "config": case "config":
err = renderConfig(w, r, ctx, false) 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) err = renderKernel(w, r, ctx)
case "initrd": case "initrd":
err = renderCtx(w, r, ctx, what, buildInitrd) err = renderCtx(w, r, ctx, what, buildInitrd)
case "uki":
err = renderCtx(w, r, ctx, what, withUki(buildUki))
case "bootstrap.tar": case "bootstrap.tar":
err = renderCtx(w, r, ctx, what, buildBootstrap) err = renderCtx(w, r, ctx, what, buildBootstrap)
case "boot.img": case "boot.img":
err = renderCtx(w, r, ctx, what, buildBootImg) err = renderCtx(w, r, ctx, what, withUki(buildBootImg))
case "boot.img.gz": case "boot.img.gz":
err = renderCtx(w, r, ctx, what, buildBootImgGZ) err = renderCtx(w, r, ctx, what, withUki(buildBootImgGZ))
case "boot.img.lz4":
err = renderCtx(w, r, ctx, what, buildBootImgLZ4)
case "boot.qcow2": case "boot.qcow2":
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qcow2")) err = renderCtx(w, r, ctx, what, qemuImgBootImg("qcow2", uki, cmdline))
case "boot.qed": case "boot.qed":
err = renderCtx(w, r, ctx, what, qemuImgBootImg("qed")) err = renderCtx(w, r, ctx, what, qemuImgBootImg("qed", uki, cmdline))
case "boot.vdi": case "boot.vdi":
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vdi")) err = renderCtx(w, r, ctx, what, qemuImgBootImg("vdi", uki, cmdline))
case "boot.vmdk": case "boot.vmdk":
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vmdk")) err = renderCtx(w, r, ctx, what, qemuImgBootImg("vmdk", uki, cmdline))
case "boot.vpc": case "boot.vpc":
err = renderCtx(w, r, ctx, what, qemuImgBootImg("vpc")) err = renderCtx(w, r, ctx, what, qemuImgBootImg("vpc", uki, cmdline))
case "boot.iso": case "boot.iso":
err = renderCtx(w, r, ctx, what, buildBootISO) err = renderCtx(w, r, ctx, what, buildBootISO)
case "boot.tar": case "boot.tar":
err = renderCtx(w, r, ctx, what, buildBootTar) err = renderCtx(w, r, ctx, what, withUki(buildBootTar))
case "boot-efi.tar":
err = renderCtx(w, r, ctx, what, buildBootEFITar)
// boot v2 // boot v2
case "bootstrap-config": case "bootstrap-config":
+1 -1
View File
@@ -1,3 +1,3 @@
#! /bin/sh #! /bin/sh
set -ex 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)" $*
+1 -1
View File
@@ -8,4 +8,4 @@ commit) tag=$GIT_TAG ;;
"") tag=latest ;; "") tag=latest ;;
*) tag=$1 ;; *) tag=$1 ;;
esac 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