Subject: Re: [bug#41011] [PATCH] gnu: grub: Support for network boot via tftp/nfs.

From: Stefan <[email-masked]>
Date: Sun, 24 May 2020 02:22:47 +0200

Hi Mathieu!

> Am 23.05.2020 um 10:10 schrieb Mathieu Othacehe <[email-masked]>:
> 
>> +    #~(lambda (bootloader target mount-point)
>> +        "Install GRUB as e.g. \"bootx64.efi\" or \"bootarm64.efi\" \"into
>> +EFI-SUBDIR, which is usually \"efi/boot\" or \"efi/Guix\" below the directory
> 
> Is that TARGET or EFI-SUBDIR?

I really mean with EFI-SUBDIR the argument from the enclosing install-grub-efi-net function, and with TARGET the argument of the lambda function. The prototype of the lambda function is fixed by some other guix machinery, it is not possible to pass an EFI-SUBDIR argument directly.

Take for example this path, the default: "/mnt/boot/efi/boot". Then MOUNT-POINT is "/mnt" (like in 'guix system init config.scm /mnt'), TARGET is "/boot" (which you also have to specify inside (operating-system (bootloader (target "/boot") …) …), and EFI-SUBDIR is the remaining "efi/boot".

In my personal case I need MOUNT-POINT as "/" (well, 'guix system init …' was only necessary once), TARGET as "boot-nfs" (my special setup), and EFI-SUBDIR as "efi/boot" (because the U-Boot is using this path by default).

For a TFFP server this TARGET with /boot (or /boot-nfs in my case) is the directory whose content has to be served. It can’t be /boot/efi (like for the normal grub-efi bootloader), as the TFTP server needs to serve several firmware files and the U-Boot.¹ When the U-Boot takes control, by default it tries to load /efi/boot/bootaa64.efi via TFTP.

It may be possible to configure the U-Boot to look for files below a /efi/Guix directory or any other path, I’m not really sure about this yet. At least /efi/boot is the documented last resort for UEFI systems to look for some “removable madia” to boot over TFTP.

Next GRUB takes control and looks for its grub.cfg and all its other files via TFTP at the path specified during 'grub-mknetdir --subdir=…'. As the TFTP root is /boot (where U-Boot and other stuff may live), this now has to be efi/boot. So you get all GRUB files like /boot/efi/boot/grub.cfg served via TFTP from the directory /boot/efi/boot.

For the usual grub-efi-bootloader this is a bit different. Here the normal path is /mnt/boot/efi/Guix and for the 'grub-install' command these parameters are required: '—-boot-directory /mnt/boot --bootloader-id Guix --efi-directory /boot/efi'. Here the "boot" part to '--boot-directory' is hard coded, as well as "Guix". So the only part you are free to chose is "efi" in (operating-system (bootloader (target "boot/efi") …) …). Additionally the usual grub-efi-bootloader looks for /boot/grub.cfg which thus is not residing inside the efi partition like all its other files.

>> +TARGET for the system whose root is mounted at MOUNT-POINT."
>> +        (let* ((mount-point-list (delete "" (string-split mount-point #\/)))
>> +               (target-list (delete "" (string-split target #\/)))
>> +               (net-dir
>> +                (string-append "/" (string-join (append
>> +                                                 mount-point-list
>> +                                                 target-list)
>> +                                                "/")))
> 
> I think you can use something like "(in-vicinity mount-point target)"
> to do the same job.

MOUNT-POINT may be just "/", TARGET is "/boot". I’m avoiding double slashes, like "//boot", also for all the other paths. In an earlier version I had the “problem” that GRUB searched files with the prefix "//efi/boot" via TFTP. This confused me on one side, but on the other side I feared that another TFTP server might get problems with this.

Avoiding double slashes seems not to be something that in-vicinity is taking care about. And TARGET with its "/boot" looks like an absolute path, making it an improper argument to in-vicinity: “in-vicinity should allow filename to override vicinity when filename is an absolute pathname and vicinity is equal to the value of (user-vicinity). The behavior of in-vicinity when filename is absolute and vicinity is not equal to the value of (user-vicinity) is unspecified.”

A bit further down I need to count the directory levels up to the "/gnu" store to construct a proper link, so I need the result of a string-split anyway. 

So all in all – although it looks a bit complicated – I’d like to keep this.

Hm, but I wasn’t that strict with the EFI-SUBDIR argument. And potentially MOUNT-POINT may be a relative path, may it? Do you know? In that case there is a bug in prepending the "/" to net-dir, turning a relative into an absolute path.

> 
>> +          ;; Tell 'grub-install' that there might be a LUKS-encrypted /boot or
>> +          ;; root partition.
>> +          (setenv "GRUB_ENABLE_CRYPTODISK" "y")
>> +          (invoke/quiet (string-append bootloader "/bin/grub-mknetdir")
>> +                        (string-append "--net-directory=" net-dir)
>> +                        (string-append "--subdir=" subdir))
>> +          (false-if-exception
>> +            (delete-file efi-bootloader-link))
>> +          (symlink #$efi-bootloader
>> +                   efi-bootloader-link)
>> +          (false-if-exception
>> +            (delete-file store-link))
>> +          (symlink store
>> +                   store-link)))))
> 
> What's the purpose of those two symlinks, isn't grub-mknetdir taking
> care of all this?

No, it doesn’t. The final .efi file after calling 'grub-mknetdir' is /boot/efi/boot/arm64-efi/core.efi. I guess when using PXE there is a way to point to this efi file, so GRUB doesn’t care. But without this the U-Boot looks for the standardised /efi/boot/bootaa64.efi file via TFTP. So the first symlink fixes this gap without removing other possibilities.

The second symlink is for GRUB itself. Inside /boot/efi/boot/grub.cfg there are commands to specify the root device, usually via a file system label or a UUID. But in this case the root device specification is just ”set root=(tftp)”. Following lines look up files by e.g. /gnu/store/…-grub-2.04/share/grub/unicode.pf2, which GRUB will then try to access via TFTP. But the root for the TFTP server is /boot from the TARGET argument, and not /. So GRUB trying to access /gnu/store/… results in an access to /boot/gnu/store/…. Therefore the symlink from /boot/gnu to ../gnu is needed. And as TARGET can freely be chosen the number of up-levels needs to be calculated.

Where the usual grub-efi-bootloader is able to find and access the real root device directly, the grub-efi-net-bootloader can only use TFTP. Therefore we have to make the real “root device” accessible via TFPT. I’m not sure if this is wise from a security point of view. But otherwise all files listed in the grub.cfg would need to be copied. This would be an overhead and leads to more complications, when deleting system generations; then obsolete copies need determined and removed again. 

> Creating a system test for this may be a bit difficult, but if you could
> add a section in the documentation describing how to setup a
> 'grub-efi-net-bootloader, that would be great!

I have some difficulties with this – despite not having knowledge with texi yet. So help is very welcome.

You need to make use of an NFS server, serving your / directory. 

You need a TFTP server set up to serve the files of our /boot directory with access to your /gnu store via the /boot/gnu symlink as well. 

You need to setup your DNS server to send some boot-options. 

I use a “DiskStation” with its UI for all this but needed to fiddle around with dnsmasq for the DNS. So this is nothing for a guix documentation. However, this might be even out of scope. But ideally there would be examples of how to achieve all this with guix itself. I have no experience using guix for anything of this.

Then I use all this to boot a Raspberry Pi 3b in aarch64 mode. This requires a none free firmware, additional configuration files for the firmware, the (modified) U-Boot. And there is even more. I compiled the kernel linux (currently from the mainline) with a Raspberry specific defconfig-package as an input for the linux package and an additional function to modify the defconfig on the fly.

There are more patches to follow for at least parts of all this.

But what I actually want to say with this: Even to reproduce my setup, still a lot other stuff is missing. And I have no other computer for tinkering. I can’t try out this patch on a usual x86…64 machine. I can't really tell, if this will work right out of the box for other people on different computers.

Before documenting this for the public, I would wish that at least someone else with access to an x86_64 UEFI system gives it a try. I'm pretty sure it will work.

Besides all the server stuff with NFS, TFTP and DNS, there are only few specialities to know:

(operating-system
  (file-systems (cons (file-system
                        (mount-point "/")
                        (type "nfs")
                        (device ":/your/servers/path/to/guix-root")
                        (options "addr=10.11.12.2,vers=4.1"))
                       %base-file-systems)) 
  (bootloader 
    (target "/boot")
    (bootloader-configuration (bootloader grub-efi-net-bootloader)
    …))
  …)

The IP address of your server needs to be specified via an “addr=” option. To make use of the defaults of the new grub-efi-net-bootloader, the target field has to be "/boot".

Bye

Stefan

¹ Well, actually – as for grub-efi-net-bootloader you are now free to use any path for target – you can use "/boot/efi". But then you need to make this your TFTP server’s root, and you need to use the make-grub-efi-net-bootloader macro with "/boot/efi" as well, and you will end up in having double efi folders as in /boot/efi/efi/boot/grub.cfg etc. So this doesn’t make sense.