hostname: add $ hostname substitution and petnames
This commit adds support to /etc/hostname for substitution
of $ wordlists from {/etc,/run,/usr/lib}/systemd/hostname-wordlist.
The first $ will lookup hostname-wordlist/1, the next
hostname-wordlist/2 and so on.
With that we can do a petname [1] style hostname in systemd, e.g.
below a possible expansion for a hostname template:
$-$-$-???? -> wildly-happy-octopus-92a9
The substitution of words is stable (based on machine-id) but
not persisted, it is picked on every boot via a stable file
offset so the operation is cheap. But this means that if the
wordlist changes the hostname would change. The next commit
will add the pattern to the firstboot.hostname credential which
is persistet with the resolved names to avoid this issue.
This also includes a wordlist from the "petname" project
that can be optionally installed.
Thanks to Dustin Kirkland for this wonderful project.
[1] https://github.com/dustinkirkland/petname
This commit is contained in:
+179
-33
@@ -1,13 +1,16 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sched.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "sd-daemon.h"
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "constants.h"
|
||||
#include "creds-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "fileio.h"
|
||||
@@ -23,6 +26,8 @@
|
||||
#include "proc-cmdline.h"
|
||||
#include "process-util.h"
|
||||
#include "siphash24.h"
|
||||
#include "stat-util.h"
|
||||
#include "stdio-util.h"
|
||||
#include "string-table.h"
|
||||
#include "string-util.h"
|
||||
|
||||
@@ -119,9 +124,13 @@ int read_etc_hostname_stream(FILE *f, bool substitute_wildcards, char **ret) {
|
||||
continue;
|
||||
|
||||
if (substitute_wildcards) {
|
||||
r = hostname_substitute_wildcards(line);
|
||||
_cleanup_free_ char *substituted = NULL;
|
||||
|
||||
r = hostname_substitute_wildcards(line, &substituted);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
free_and_replace(line, substituted);
|
||||
}
|
||||
|
||||
hostname_cleanup(line); /* normalize the hostname */
|
||||
@@ -130,7 +139,7 @@ int read_etc_hostname_stream(FILE *f, bool substitute_wildcards, char **ret) {
|
||||
if (!hostname_is_valid(
|
||||
line,
|
||||
VALID_HOSTNAME_TRAILING_DOT|
|
||||
(substitute_wildcards ? 0 : VALID_HOSTNAME_QUESTION_MARK)))
|
||||
(substitute_wildcards ? 0 : VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN)))
|
||||
return -EBADMSG;
|
||||
|
||||
*ret = TAKE_PTR(line);
|
||||
@@ -254,48 +263,178 @@ static const char* const hostname_source_table[] = {
|
||||
|
||||
DEFINE_STRING_TABLE_LOOKUP(hostname_source, HostnameSource);
|
||||
|
||||
int hostname_substitute_wildcards(char *name) {
|
||||
static int hostname_open_wordlist(const char *file, FILE **ret) {
|
||||
_cleanup_fclose_ FILE *f = NULL;
|
||||
int r;
|
||||
|
||||
assert(file);
|
||||
assert(ret);
|
||||
|
||||
/* Opens one of the numbered hostname word list files ("1", "2", "3", ...) for the '$' wildcards. */
|
||||
const char *override = secure_getenv("SYSTEMD_HOSTNAME_WORDLIST_PATH");
|
||||
r = search_and_fopen(
|
||||
file,
|
||||
"re",
|
||||
/* root= */ NULL,
|
||||
override ? (const char**) STRV_MAKE(override) : (const char**) CONF_PATHS_STRV("systemd/hostname-wordlist"),
|
||||
&f,
|
||||
/* ret_path= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret = TAKE_PTR(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int hostname_pick_word(sd_id128_t mid, size_t pos, char **ret) {
|
||||
static const sd_id128_t word_key = SD_ID128_MAKE(2d,9f,1c,7a,4b,8e,43,11,9a,6d,5f,02,c8,77,e3,14);
|
||||
_cleanup_fclose_ FILE *f = NULL;
|
||||
struct stat st;
|
||||
bool wrapped = false;
|
||||
uint64_t h;
|
||||
int r;
|
||||
|
||||
assert(pos >= 1);
|
||||
assert(ret);
|
||||
|
||||
/* The n-th '$' in a template reads the word list file named after its position, i.e. "1", "2", ... */
|
||||
char file[DECIMAL_STR_MAX(size_t)];
|
||||
xsprintf(file, "%zu", pos);
|
||||
|
||||
r = hostname_open_wordlist(file, &f);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (fstat(fileno(f), &st) < 0)
|
||||
return -errno;
|
||||
r = stat_verify_regular(&st);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (st.st_size == 0)
|
||||
return -ENOENT;
|
||||
|
||||
/* Pick a word without reading the whole list into memory: hash the machine ID and word position to a
|
||||
* byte offset. This stream is independent of the '?' nibble stream, so pure-'?' templates keep
|
||||
* producing byte-identical output. Stable as long as the wordlist is stable. */
|
||||
struct siphash state;
|
||||
siphash24_init(&state, word_key.bytes);
|
||||
siphash24_compress_typesafe(mid, &state);
|
||||
siphash24_compress_typesafe(pos, &state);
|
||||
h = siphash24_finalize(&state);
|
||||
|
||||
if (fseeko(f, (off_t) (h % (uint64_t) st.st_size), SEEK_SET) < 0)
|
||||
return -errno;
|
||||
|
||||
/* We mostly landed mid-line, so read/discard the current line here. If the file was shrunk by a
|
||||
* concurrent modification we might have seeked at/past EOF, so wrap around to the beginning. */
|
||||
r = read_line(f, LONG_LINE_MAX, NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0) {
|
||||
wrapped = true;
|
||||
rewind(f);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *line = NULL;
|
||||
|
||||
r = read_stripped_line(f, LONG_LINE_MAX, &line);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0) { /* hit EOF: we started at a random offset, wrap around to the beginning */
|
||||
if (wrapped) /* already wrapped once, the file contains no usable word at all */
|
||||
return -ENOENT;
|
||||
wrapped = true;
|
||||
rewind(f);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Skip empty lines and comments */
|
||||
if (IN_SET(line[0], '\0', '#'))
|
||||
continue;
|
||||
|
||||
/* Each word must be a valid single hostname label on its own; lowercase it and silently skip
|
||||
* bogus entries. */
|
||||
ascii_strlower(line);
|
||||
if (!hostname_is_valid(line, /* flags= */ 0))
|
||||
continue;
|
||||
|
||||
*ret = TAKE_PTR(line);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int hostname_substitute_wildcards(const char *name, char **ret) {
|
||||
static const sd_id128_t key = SD_ID128_MAKE(98,10,ad,df,8d,7d,4f,b5,89,1b,4b,56,ac,c2,26,8f);
|
||||
sd_id128_t mid = SD_ID128_NULL;
|
||||
_cleanup_free_ char *result = NULL;
|
||||
size_t left_bits = 0, counter = 0;
|
||||
size_t word_pos = 0;
|
||||
uint64_t h = 0;
|
||||
int r;
|
||||
|
||||
assert(name);
|
||||
assert(ret);
|
||||
|
||||
/* Replaces every occurrence of '?' in the specified string with a nibble hashed from
|
||||
* /etc/machine-id. This is supposed to be used on /etc/hostname files that want to automatically
|
||||
* configure a hostname derived from the machine ID in some form.
|
||||
if (isempty(name))
|
||||
return strdup_to(ret, "");
|
||||
|
||||
/* Expands wildcards in the specified string, deriving the inserted values deterministically from
|
||||
* /etc/machine-id:
|
||||
*
|
||||
* Note that this does not directly use the machine ID, because that's not necessarily supposed to be
|
||||
* public information to be broadcast on the network, while the hostname certainly is. */
|
||||
* '?' is replaced by a single hex nibble hashed from the machine ID.
|
||||
* '$' is replaced by a word picked from a word list; the n-th '$' in the string uses the list
|
||||
* file named "n"
|
||||
*
|
||||
* This is supposed to be used on /etc/hostname files that want to automatically configure a hostname
|
||||
* derived from the machine ID in some form, e.g. "$-$-????".
|
||||
*
|
||||
* Note that this does not directly expose the machine ID, because that's not necessarily supposed to
|
||||
* be public information to be broadcast on the network, while the hostname certainly is. */
|
||||
|
||||
for (char *n = name; ; n++) {
|
||||
n = strchr(n, '?');
|
||||
if (!n)
|
||||
return 0;
|
||||
|
||||
if (left_bits <= 0) {
|
||||
if (sd_id128_is_null(mid)) {
|
||||
r = sd_id128_get_machine(&mid);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
struct siphash state;
|
||||
siphash24_init(&state, key.bytes);
|
||||
siphash24_compress_typesafe(mid, &state);
|
||||
siphash24_compress_typesafe(counter, &state); /* counter mode */
|
||||
h = siphash24_finalize(&state);
|
||||
left_bits = sizeof(h) * 8;
|
||||
counter++;
|
||||
for (const char *n = name; *n; n++) {
|
||||
if (IN_SET(*n, '?', '$') && sd_id128_is_null(mid)) {
|
||||
r = sd_id128_get_machine(&mid);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
assert(left_bits >= 4);
|
||||
*n = hexchar(h & 0xf);
|
||||
h >>= 4;
|
||||
left_bits -= 4;
|
||||
if (*n == '?') {
|
||||
if (left_bits <= 0) {
|
||||
struct siphash state;
|
||||
siphash24_init(&state, key.bytes);
|
||||
siphash24_compress_typesafe(mid, &state);
|
||||
siphash24_compress_typesafe(counter, &state); /* counter mode */
|
||||
h = siphash24_finalize(&state);
|
||||
left_bits = sizeof(h) * 8;
|
||||
counter++;
|
||||
}
|
||||
|
||||
assert(left_bits >= 4);
|
||||
char c = hexchar(h & 0xf);
|
||||
h >>= 4;
|
||||
left_bits -= 4;
|
||||
|
||||
if (!strextendn(&result, &c, 1))
|
||||
return -ENOMEM;
|
||||
|
||||
} else if (*n == '$') {
|
||||
/* Each '$' is an independent word token; the n-th one picks from word list "n".
|
||||
* There is no escape for a literal '$', as it is not a valid hostname character. */
|
||||
_cleanup_free_ char *w = NULL;
|
||||
r = hostname_pick_word(mid, ++word_pos, &w);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!strextend(&result, w))
|
||||
return -ENOMEM;
|
||||
|
||||
} else if (!strextendn(&result, n, 1))
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
*ret = TAKE_PTR(result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* get_default_hostname(void) {
|
||||
@@ -305,13 +444,20 @@ char* get_default_hostname(void) {
|
||||
if (!h)
|
||||
return NULL;
|
||||
|
||||
r = hostname_substitute_wildcards(h);
|
||||
_cleanup_free_ char *substituted = NULL;
|
||||
r = hostname_substitute_wildcards(h, &substituted);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to substitute wildcards in hostname, falling back to built-in name: %m");
|
||||
return strdup(FALLBACK_HOSTNAME);
|
||||
}
|
||||
|
||||
return TAKE_PTR(h);
|
||||
/* Each token expands to a whole word, so the concrete name may exceed the length limit. */
|
||||
if (!hostname_is_valid(substituted, VALID_HOSTNAME_TRAILING_DOT)) {
|
||||
log_debug("Substituted hostname '%s' is invalid, falling back to built-in name.", substituted);
|
||||
return strdup(FALLBACK_HOSTNAME);
|
||||
}
|
||||
|
||||
return TAKE_PTR(substituted);
|
||||
}
|
||||
|
||||
int gethostname_full(GetHostnameFlags flags, char **ret) {
|
||||
|
||||
@@ -22,7 +22,7 @@ int read_etc_hostname(const char *path, bool substitute_wildcards, char **ret);
|
||||
void hostname_update_source_hint(const char *hostname, HostnameSource source);
|
||||
int hostname_setup(bool really);
|
||||
|
||||
int hostname_substitute_wildcards(char *name);
|
||||
int hostname_substitute_wildcards(const char *name, char **ret);
|
||||
|
||||
char* get_default_hostname(void);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user