An evening with Guix

Okay, I haven’t really spent an evening with Guix, it’s actually been longer than that. But I have been using Guix, and I do want somewhere to document the silly stuff I’ve been doing and my thoughts.

Reproducibility isn’t personally useful to me

A lot has been said about reproducible builds and the immutability of Guix. But in retrospect, it hasn’t benefitted me in a particularly large way. I can see the use. Having dealt with patching various versions of large software packages at work, it would certainly be nice to have a clear and consistent reference for a working package set at all times. But at home? Practically all I run is sway, firefox, and emacs. Sometimes I write Zig, sometimes Ruby. But I’m not building anything substantial, so there are few reasons to take advantage of tools like guix environment.

With that said, I’d still recommend guix.

Immutability can be annoying

So, I do silly stuff on my local network. One of the silly things I’ve done is disable SLAAC because I don’t want the Apple TV I have to use IPv6 (I go to unreasonable lengths to avoid seeing adverts in its interface). Usually, this would be a case of just turning off the IPv6 option in the network configuration. Apple, in their wisdom, do not allow this. In fact, you can’t even see the IPv6 addresses the bloody device chose. So SLAAC is off, and my router advertisements have the managed flag set. It doesn’t get an address. But I do want freedom-respecting devices to get addresses.

Here’s the problem - Guix, at the time of writing, doesn’t provide a way to configure a static IPv6 address. The issue is open and being worked on, but has yet to be resolved. I had believed I’d solved the problem by enabling DHCPv6 - only to find that the dhcp client it runs only requests v4 addresses by default, and it is not possible to fetch both in one process. static ipv6 in guix single-process DHCPv4+DHCPv6

This is where immutability comes in. There’s a lot of effort gone into Guix to keep your system immutable, and clean. But the product of the effort is a complex system, and doing something as (usually) simple as getting a command to run on boot as the root user is far more difficult than you would expect.

A lot of that complexity is justifiable. If you have an /etc/rc.local, what would manage it? How would it persist between generations of your system? How would you ensure that two systems were the same if you had files like that lying around doing things completely unmanaged by Guix? It’s not a trivial problem.

But in the same vein, I really just want to run a service that’s already mentioned in my system configuration:

   (list (service openssh-service-type)
         (service dhcp-client-service-type))

A second time, with different arguments. Which may have looked something like this:

 ;; This isn't real code - if you're just looking for the answer, this isn't it.
   (list (service openssh-service-type)
         (service dhcp-client-service-type
                 (version 4)))
         (service dhcp-client-service-type 
                 (pid-file "/var/run/")
                 (version 6)))

dhclient -6

Unfortunately, this isn’t possible. Looking at networking.scm, the arguments passed to dhclient are static, and there’s no definition of a to lean on. I’d consider contributing it myself, if not for the fact I’m not remotely familliar with guile or the development process of Guix - and ultimately, how many people are running DHCPv6 anyway? Android doesn’t even support it. networking.scm

In any case, the solution I came to was to create a loose copy of the existing dhcp-client-service-type, but with the flags I needed:

(define dhcp6-client-service-type
  (shepherd-service-type 'dhcp6-client 
   (lambda (dhcp)
     (define dhclient
       (file-append dhcp "/sbin/dhclient"))
     (define pid-file
      (documentation "Set up networking via DHCPv6.")
      (requirement '(user-processes udev))
      (provision '(networking6))
      (start #~(lambda _
                 (define valid?
                   (lambda (interface)
                     (and (arp-network-interface? interface)
                          (not (loopback-network-interface? interface))
                           (set-network-interface-up interface)))))
                 (define ifaces
                   (filter valid? (all-network-interface-names)))
                 (false-if-exception (delete-file #$pid-file))
                 (let ((pid (fork+exec-command
                             (cons* #$dhclient "-6" "-nw"
                                    "-pf" #$pid-file ifaces))))
                   (and (zero? (cdr (waitpid pid)))
                        (read-pid-file #$pid-file)))))
      (stop #~(make-kill-destructor))))
   (description "Run @command{dhcp}, a Dynamic Host Configuration
Protocol v6 (DHCPv6) client, on all the non-loopback network interfaces.")))

This was added to config.scm, which in turn referenced it in my list of services:

   (list (service openssh-service-type)
         (service dhcp-client-service-type)
     (service dhcp6-client-service-type))

After a quick guix system reconfigure /etc/config.scm, I now have two dhclients running from boot allocating IPv4 and IPv6 addresses from my router to my (singular) NIC. $ ps aux | grep dhclient root 265 0.0 0.0 6288 2280 ? Ss Dec04 0:00 /gnu/store/z97k42srk2kdl6kdg1l7zgf27dlxyc98-isc-dhcp-4.4.2-P1/sbin/dhclient -6 -nw -pf /var/run/ eno1 root 289 0.0 0.0 6288 2132 ? Ss Dec04 0:00 /gnu/store/z97k42srk2kdl6kdg1l7zgf27dlxyc98-isc-dhcp-4.4.2-P1/sbin/dhclient -nw -pf /var/run/ eno1

Caveat emptor

This worked/works for me - but I can’t say with any confidence I’m doing something appropriate. I’m not clear where user-defined services should go. They obviously can’t just sit in a directory on the system. Presumably I should be putting this stuff in my own channel, which is then referenced in config.scm like nonguix, but perhaps not.