Alloc.nix, a tool to help you allocate ranges to services

So today I’m bringing a newly uploaded project, which, as the title suggests, is designed to solve the dynamic allocation problem of ranges at eval time, such as port, uid ranges, etc. (We may be able to extend this to IP in the future, once we have IP encoding and decoding capabilities).

I saw an issue today reporting the assignment of uid ranges is unreliable (`users.users.<name>.autoSubUidGidRange = true` creates overlapping Ids for normal and system users · Issue #259588 · NixOS/nixpkgs · GitHub), which makes me a bit mad. I was thinking that if we could allocate this range at eval time, we could even do more dynamic configuration, but we ended up allocating this with a Perl script clumsily (of course, this might not be necessary after switching to userborn).

本来应该从从容容,游刃有余!现在是匆匆忙忙,连滚带爬!

So I wrote this. It’s computed using First Fit algorithm; I originally wanted to use a balanced binary tree, but

  1. I’m not very good at algorithms.
  2. For interpreted languages ​​like Nix, processing a large number of tree nodes can consume a significant amount of time in object allocation and garbage collection, resulting in a very high Constant Factor.
  3. I’m not very good at algorithms :sob:

I didn’t benchmark my implementation. I didn’t even write a full test for it either, but anyway, here’s the example usage:

For the function:

alloc 1000 100 {
  a = { start = 1001; length = 2; };
  b = { length = 9; };
  c = { start = 1007; length = 2; };
  d = { length = 1; };
}
=> {
  a = { start = 1001; length = 2; };
  b = { start = 1009; length = 9; };
  c = { start = 1007; length = 2; };
  d = { start = 1003; length = 1; };
}

With 1000 being the starting offset and 100 being interval length. You can find that it dynamically assigns a start point to all blocks that did not have it before.

For the module:

{ config, lib, ... }:

{
  alloc.ports = {
    start = 1;
    interval = 65535;
    blocks = {
      someService = {
        length = 10;
      };
      anotherService = {
        start = 1714;
        length = 50;
      };
      someOtherService = {
        start = 8;
        length = 6;
      };
    };
  };
  b = config.alloc.ports.blocks.someService.start;
}

b will eval to 14. I wrapped the alloc function directly over apply, so once your module system (Not limited to NixOS, home-manager or Nix-darwin, as long as it has lib) has this module, you can easily add dynamically allocated capabilities to your configuration.

My goal is to add this to Nixpkgs eventually, but before that, please take a look, use it, and give me some suggestions and ideas. :slightly_smiling_face:‍:arrow_up_down:

17 Likes

A previous proposal on something similar: https://github.com/NixOS/rfcs/pull/151

The biggest problem from my view was making dynamically allocating ports as stable as possible. Otherwise adding or removing ports would cause restarts of completely unrelated services, which is very unintuitive and annoying.

2 Likes

The implementation I daily drive uses the hash of the name of the service to decide the port. Been smooth sailing for some years already.

5 Likes