diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml index 3bbb7d71d49..e17e8ac24d1 100644 --- a/nixos/doc/manual/release-notes/rl-2009.xml +++ b/nixos/doc/manual/release-notes/rl-2009.xml @@ -415,6 +415,32 @@ systemd.services.nginx.serviceConfig.ReadWritePaths = [ "/var/www" ]; continue to work through Breezy. + + + In addition to the hostname, the fully qualified domain name (FQDN), + which consists of ${cfg.hostName} and + ${cfg.domain} is now added to + /etc/hosts, to allow local FQDN resolution, as used by the + hostname --fqdn command and other applications that + try to determine the FQDN. These new entries take precedence over entries + from the DNS which could cause regressions in some very specific setups. + Additionally the hostname is now resolved to 127.0.0.2 + instead of 127.0.1.1 to be consistent with what + nss-myhostname (from systemd) returns. + The old behaviour can e.g. be restored by using + networking.hosts = lib.mkForce { "127.0.1.1" = [ config.networking.hostName ]; };. + + + + + The hostname (networking.hostName) must now be a valid + DNS label (see RFC 1035) and as such must not contain the domain part. + This means that the hostname must start with a letter, end with a letter + or digit, and have as interior characters only letters, digits, and + hyphen. The maximum length is 63 characters. Additionally it is + recommended to only use lower-case characters. + + diff --git a/nixos/modules/config/networking.nix b/nixos/modules/config/networking.nix index 03944de8249..4cb7d81c997 100644 --- a/nixos/modules/config/networking.nix +++ b/nixos/modules/config/networking.nix @@ -8,9 +8,6 @@ let cfg = config.networking; - localhostMapped4 = cfg.hosts ? "127.0.0.1" && elem "localhost" cfg.hosts."127.0.0.1"; - localhostMapped6 = cfg.hosts ? "::1" && elem "localhost" cfg.hosts."::1"; - localhostMultiple = any (elem "localhost") (attrValues (removeAttrs cfg.hosts [ "127.0.0.1" "::1" ])); in @@ -147,12 +144,6 @@ in config = { assertions = [{ - assertion = localhostMapped4; - message = ''`networking.hosts` doesn't map "127.0.0.1" to "localhost"''; - } { - assertion = !cfg.enableIPv6 || localhostMapped6; - message = ''`networking.hosts` doesn't map "::1" to "localhost"''; - } { assertion = !localhostMultiple; message = '' `networking.hosts` maps "localhost" to something other than "127.0.0.1" @@ -161,22 +152,34 @@ in ''; }]; - networking.hosts = { - "127.0.0.1" = [ "localhost" ]; - } // optionalAttrs (cfg.hostName != "") { - "127.0.1.1" = [ cfg.hostName ]; + # These entries are required for "hostname -f" and to resolve both the + # hostname and FQDN correctly: + networking.hosts = let + hostnames = # Note: The FQDN (canonical hostname) has to come first: + optional (cfg.hostName != "" && cfg.domain != null) "${cfg.hostName}.${cfg.domain}" + ++ optional (cfg.hostName != "") cfg.hostName; # Then the hostname (without the domain) + in { + "127.0.0.2" = hostnames; } // optionalAttrs cfg.enableIPv6 { - "::1" = [ "localhost" ]; + "::1" = hostnames; }; networking.hostFiles = let + # Note: localhostHosts has to appear first in /etc/hosts so that 127.0.0.1 + # resolves back to "localhost" (as some applications assume) instead of + # the FQDN! By default "networking.hosts" also contains entries for the + # FQDN so that e.g. "hostname -f" works correctly. + localhostHosts = pkgs.writeText "localhost-hosts" '' + 127.0.0.1 localhost + ${optionalString cfg.enableIPv6 "::1 localhost"} + ''; stringHosts = let oneToString = set: ip: ip + " " + concatStringsSep " " set.${ip} + "\n"; allToString = set: concatMapStrings (oneToString set) (attrNames set); in pkgs.writeText "string-hosts" (allToString (filterAttrs (_: v: v != []) cfg.hosts)); extraHosts = pkgs.writeText "extra-hosts" cfg.extraHosts; - in mkBefore [ stringHosts extraHosts ]; + in mkBefore [ localhostHosts stringHosts extraHosts ]; environment.etc = { # /etc/services: TCP/UDP port assignments. diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix index 44677d417ea..12cff6b038f 100644 --- a/nixos/modules/tasks/network-interfaces.nix +++ b/nixos/modules/tasks/network-interfaces.nix @@ -376,10 +376,20 @@ in networking.hostName = mkOption { default = "nixos"; - type = types.str; + # Only allow hostnames without the domain name part (i.e. no FQDNs, see + # e.g. "man 5 hostname") and require valid DNS labels (recommended + # syntax). Note: We also allow underscores for compatibility/legacy + # reasons (as undocumented feature): + type = types.strMatching + "^[[:alpha:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$"; description = '' - The name of the machine. Leave it empty if you want to obtain - it from a DHCP server (if using DHCP). + The name of the machine. Leave it empty if you want to obtain it from a + DHCP server (if using DHCP). The hostname must be a valid DNS label (see + RFC 1035 section 2.3.1: "Preferred name syntax") and as such must not + contain the domain part. This means that the hostname must start with a + letter, end with a letter or digit, and have as interior characters only + letters, digits, and hyphen. The maximum length is 63 characters. + Additionally it is recommended to only use lower-case characters. ''; }; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 58120987364..47ca015632c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -134,6 +134,7 @@ in hitch = handleTest ./hitch {}; hocker-fetchdocker = handleTest ./hocker-fetchdocker {}; home-assistant = handleTest ./home-assistant.nix {}; + hostname = handleTest ./hostname.nix {}; hound = handleTest ./hound.nix {}; hydra = handleTest ./hydra {}; hydra-db-migration = handleTest ./hydra/db-migration.nix {}; diff --git a/nixos/tests/hostname.nix b/nixos/tests/hostname.nix new file mode 100644 index 00000000000..3b87303d73e --- /dev/null +++ b/nixos/tests/hostname.nix @@ -0,0 +1,66 @@ +{ system ? builtins.currentSystem, + config ? {}, + pkgs ? import ../.. { inherit system config; } +}: + +with import ../lib/testing-python.nix { inherit system pkgs; }; +with pkgs.lib; + +let + makeHostNameTest = hostName: domain: + let + fqdn = hostName + (optionalString (domain != null) ".${domain}"); + in + makeTest { + name = "hostname-${fqdn}"; + meta = with pkgs.stdenv.lib.maintainers; { + maintainers = [ primeos blitz ]; + }; + + machine = { lib, ... }: { + networking.hostName = hostName; + networking.domain = domain; + + environment.systemPackages = with pkgs; [ + inetutils + ]; + }; + + testScript = '' + start_all() + + machine = ${hostName} + + machine.wait_for_unit("network-online.target") + + # The FQDN, domain name, and hostname detection should work as expected: + assert "${fqdn}" == machine.succeed("hostname --fqdn").strip() + assert "${optionalString (domain != null) domain}" == machine.succeed("dnsdomainname").strip() + assert ( + "${hostName}" + == machine.succeed( + 'hostnamectl status | grep "Static hostname" | cut -d: -f2' + ).strip() + ) + + # 127.0.0.1 and ::1 should resolve back to "localhost": + assert ( + "localhost" == machine.succeed("getent hosts 127.0.0.1 | awk '{print $2}'").strip() + ) + assert "localhost" == machine.succeed("getent hosts ::1 | awk '{print $2}'").strip() + + # 127.0.0.2 should resolve back to the FQDN and hostname: + fqdn_and_host_name = "${optionalString (domain != null) "${hostName}.${domain} "}${hostName}" + assert ( + fqdn_and_host_name + == machine.succeed("getent hosts 127.0.0.2 | awk '{print $2,$3}'").strip() + ) + ''; + }; + +in +{ + noExplicitDomain = makeHostNameTest "ahost" null; + + explicitDomain = makeHostNameTest "ahost" "adomain"; +}