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:
Michael Vogt
2026-06-20 14:26:41 +02:00
parent aa0f05173b
commit df12d58f8b
16 changed files with 1551 additions and 53 deletions
+5
View File
@@ -80,6 +80,11 @@ All tools:
(relevant in particular for the system manager and `systemd-hostnamed`).
Must be a valid hostname (either a single label or a FQDN).
* `$SYSTEMD_HOSTNAME_WORDLIST_PATH` — search this directory for the numbered
hostname word list files used by the `$` wildcard in hostname patterns (see
`hostname(5)`), instead of the built-in search path. Only useful for
debugging and testing.
* `$SD_EVENT_PROFILE_DELAYS=1` — if set, the sd-event event loop implementation
will print latency information at runtime.
+68
View File
@@ -0,0 +1,68 @@
Hostname word lists
====================
These files provide the word lists for the "$" wildcard understood by
/etc/hostname (see hostname(5)). The "$" token is positional: the n-th "$" in a
template is replaced by a word from the list file named "n", i.e. the first "$"
uses the file "1", the second "2", and so on. A template such as:
$-$-$-???? -> wildly-happy-octopus-92a9
is expanded deterministically from the machine ID, so a given machine always
gets the same name.
The numbered files are shipped as symlinks to the semantic lists, so the same
words back both names:
1 -> adverbs
2 -> adjectives
3 -> nouns
This keeps the lookup flexible (a deployment can add a "4", "5", … or repoint
the symlinks) while the actual word lists keep meaningful names.
Files
-----
Each file is a plain list of words, one per line, with no comment or blank
lines: a word is picked by hashing the machine ID to a byte offset into the
file, so comment/blank lines (although skipped) would bias the selection and
should be avoided. Each word must be a valid single hostname label (lowercase
letters, digits, hyphens); invalid entries are skipped. The file is used as-is
from the highest-priority directory that provides it (/etc wins over /run wins
over /usr/lib); files are not merged across directories.
Search path (highest priority first):
/etc/systemd/hostname-wordlist/{1,2,3,...}
/run/systemd/hostname-wordlist/...
/usr/local/lib/systemd/hostname-wordlist/...
/usr/lib/systemd/hostname-wordlist/...
Caveats
-------
The word for each token is derived deterministically from the machine ID and
recomputed on every boot; it is not persisted. The position is folded into the
hash, so repeated "$" tokens stay independent even when they resolve to the same
list. Changing a word list may change the name a machine gets. If a referenced
list is missing the name is treated as invalid and the built-in fallback
hostname is used.
Because a word is chosen by byte offset into the file (rather than loading and
indexing the whole list), the words are not all equally likely: a word's chance
tracks the length of the word that precedes it in the list (not its own length),
so a word listed right after a long word is slightly more likely to be picked.
The effect is small: about a 12% non-uniformity, i.e. the effective name space
is ~88% of the nominal product for $-$-$. This is an accepted trade for not
reading the whole list into memory. If exact uniformity is ever needed, pad
every word to a fixed width (e.g. with trailing '#') and have the loader strip
the padding.
Origin
------
These are the "small" word lists taken from the petname project
(https://github.com/dustinkirkland/petname), distributed under the Apache
License 2.0. Distributions are encouraged to ship larger lists (petname also
provides "medium" and "large") for a bigger name space.
+449
View File
@@ -0,0 +1,449 @@
able
above
absolute
accepted
accurate
ace
active
actual
adapted
adapting
adequate
adjusted
advanced
alert
alive
allowed
allowing
amazed
amazing
ample
amused
amusing
apparent
apt
arriving
artistic
assured
assuring
awaited
awake
aware
balanced
becoming
beloved
better
big
blessed
bold
boss
brave
brief
bright
bursting
busy
calm
capable
capital
careful
caring
casual
causal
central
certain
champion
charmed
charming
cheerful
chief
choice
civil
classic
clean
clear
clever
climbing
close
closing
coherent
comic
communal
complete
composed
concise
concrete
content
cool
correct
cosmic
crack
creative
credible
crisp
crucial
cuddly
cunning
curious
current
cute
daring
darling
dashing
dear
decent
deciding
deep
definite
delicate
desired
destined
devoted
direct
discrete
distinct
diverse
divine
dominant
driven
driving
dynamic
eager
easy
electric
elegant
emerging
eminent
enabled
enabling
endless
engaged
engaging
enhanced
enjoyed
enormous
enough
epic
equal
equipped
eternal
ethical
evident
evolved
evolving
exact
excited
exciting
exotic
expert
factual
fair
faithful
famous
fancy
fast
feasible
fine
finer
firm
first
fit
fitting
fleet
flexible
flowing
fluent
flying
fond
frank
free
fresh
full
fun
funky
funny
game
generous
gentle
genuine
giving
glad
glorious
glowing
golden
good
gorgeous
grand
grateful
great
growing
grown
guided
guiding
handy
happy
hardy
harmless
healthy
helped
helpful
helping
heroic
hip
holy
honest
hopeful
hot
huge
humane
humble
humorous
ideal
immense
immortal
immune
improved
in
included
infinite
informed
innocent
inspired
integral
intense
intent
internal
intimate
inviting
joint
just
keen
key
kind
knowing
known
large
lasting
leading
learning
legal
legible
lenient
liberal
light
liked
literate
live
living
logical
loved
loving
loyal
lucky
magical
magnetic
main
major
many
massive
master
mature
maximum
measured
meet
merry
mighty
mint
model
modern
modest
moral
more
moved
moving
musical
mutual
national
native
natural
nearby
neat
needed
neutral
new
next
nice
noble
normal
notable
noted
novel
obliging
on
one
open
optimal
optimum
organic
oriented
outgoing
patient
peaceful
perfect
pet
picked
pleasant
pleased
pleasing
poetic
polished
polite
popular
positive
possible
powerful
precious
precise
premium
prepared
present
pretty
primary
prime
pro
probable
profound
promoted
prompt
proper
proud
proven
pumped
pure
quality
quick
quiet
rapid
rare
rational
ready
real
refined
regular
related
relative
relaxed
relaxing
relevant
relieved
renewed
renewing
resolved
rested
rich
right
robust
romantic
ruling
sacred
safe
saved
saving
secure
select
selected
sensible
set
settled
settling
sharing
sharp
shining
simple
sincere
singular
skilled
smart
smashing
smiling
smooth
social
solid
sought
sound
special
splendid
square
stable
star
steady
sterling
still
stirred
stirring
striking
strong
stunning
subtle
suitable
suited
summary
sunny
super
superb
supreme
sure
sweeping
sweet
talented
teaching
tender
thankful
thorough
tidy
tight
together
tolerant
top
topical
tops
touched
touching
tough
true
trusted
trusting
trusty
ultimate
unbiased
uncommon
unified
unique
united
up
upright
upward
usable
useful
valid
valued
vast
verified
viable
vital
vocal
wanted
warm
wealthy
welcome
welcomed
well
whole
willing
winning
wired
wise
witty
wondrous
workable
working
worthy
+261
View File
@@ -0,0 +1,261 @@
abnormally
absolutely
accurately
actively
actually
adequately
admittedly
adversely
allegedly
amazingly
annually
apparently
arguably
awfully
badly
barely
basically
blatantly
blindly
briefly
brightly
broadly
carefully
centrally
certainly
cheaply
cleanly
clearly
closely
commonly
completely
constantly
conversely
correctly
curiously
currently
daily
deadly
deeply
definitely
directly
distinctly
duly
eagerly
early
easily
eminently
endlessly
enormously
entirely
equally
especially
evenly
evidently
exactly
explicitly
externally
extremely
factually
fairly
finally
firmly
firstly
forcibly
formally
formerly
frankly
freely
frequently
friendly
fully
generally
gently
genuinely
ghastly
gladly
globally
gradually
gratefully
greatly
grossly
happily
hardly
heartily
heavily
hideously
highly
honestly
hopefully
hopelessly
horribly
hugely
humbly
ideally
illegally
immensely
implicitly
incredibly
indirectly
infinitely
informally
inherently
initially
instantly
intensely
internally
jointly
jolly
kindly
largely
lately
legally
lightly
likely
literally
lively
locally
logically
loosely
loudly
lovely
luckily
mainly
manually
marginally
mentally
merely
mildly
miserably
mistakenly
moderately
monthly
morally
mostly
multiply
mutually
namely
nationally
naturally
nearly
neatly
needlessly
newly
nicely
nominally
normally
notably
noticeably
obviously
oddly
officially
only
openly
optionally
overly
painfully
partially
partly
perfectly
personally
physically
plainly
pleasantly
poorly
positively
possibly
precisely
preferably
presently
presumably
previously
primarily
privately
probably
promptly
properly
publicly
purely
quickly
quietly
radically
randomly
rapidly
rarely
rationally
readily
really
reasonably
recently
regularly
reliably
remarkably
remotely
repeatedly
rightly
roughly
routinely
sadly
safely
scarcely
secondly
secretly
seemingly
sensibly
separately
seriously
severely
sharply
shortly
similarly
simply
sincerely
singularly
slightly
slowly
smoothly
socially
solely
specially
steadily
strangely
strictly
strongly
subtly
suddenly
suitably
supposedly
surely
terminally
terribly
thankfully
thoroughly
tightly
totally
trivially
truly
typically
ultimately
unduly
uniformly
uniquely
unlikely
urgently
usefully
usually
utterly
vaguely
vastly
verbally
vertically
vigorously
violently
virtually
visually
weekly
wholly
widely
wildly
willingly
wrongly
yearly
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
if get_option('hostname-wordlist')
install_data(
'adverbs',
'adjectives',
'nouns',
install_dir : libexecdir / 'hostname-wordlist')
# The '$' hostname tokens look up word lists by position ("1", "2", "3", …); ship those names as
# symlinks to the semantic lists so the same files back both.
foreach link : [['1', 'adverbs'], ['2', 'adjectives'], ['3', 'nouns']]
install_symlink(
link[0],
install_dir : libexecdir / 'hostname-wordlist',
pointing_to : link[1])
endforeach
endif
+449
View File
@@ -0,0 +1,449 @@
ox
ant
ape
asp
bat
bee
boa
bug
cat
cod
cow
cub
doe
dog
eel
eft
elf
elk
emu
ewe
fly
fox
gar
gnu
hen
hog
imp
jay
kid
kit
koi
lab
man
owl
pig
pug
pup
ram
rat
ray
yak
bass
bear
bird
boar
buck
bull
calf
chow
clam
colt
crab
crow
dane
deer
dodo
dory
dove
drum
duck
fawn
fish
flea
foal
fowl
frog
gnat
goat
grub
gull
hare
hawk
ibex
joey
kite
kiwi
lamb
lark
lion
loon
lynx
mako
mink
mite
mole
moth
mule
mutt
newt
orca
oryx
pika
pony
puma
seal
shad
slug
sole
stag
stud
swan
tahr
teal
tick
toad
tuna
wasp
wolf
worm
wren
yeti
adder
akita
alien
aphid
bison
boxer
bream
bunny
burro
camel
chimp
civet
cobra
coral
corgi
crane
dingo
drake
eagle
egret
filly
finch
gator
gecko
ghost
ghoul
goose
guppy
heron
hippo
horse
hound
husky
hyena
koala
krill
leech
lemur
liger
llama
louse
macaw
midge
molly
moose
moray
mouse
panda
perch
prawn
quail
racer
raven
rhino
robin
satyr
shark
sheep
shrew
skink
skunk
sloth
snail
snake
snipe
squid
stork
swift
tapir
tetra
tiger
troll
trout
viper
wahoo
whale
zebra
alpaca
amoeba
baboon
badger
beagle
bedbug
beetle
bengal
bobcat
caiman
cattle
cicada
collie
condor
cougar
coyote
dassie
dragon
earwig
falcon
feline
ferret
gannet
gibbon
glider
goblin
gopher
grouse
guinea
hermit
hornet
iguana
impala
insect
jackal
jaguar
jennet
kitten
kodiak
lizard
locust
maggot
magpie
mammal
mantis
marlin
marmot
marten
martin
mayfly
minnow
monkey
mullet
muskox
ocelot
oriole
osprey
oyster
parrot
pigeon
piglet
poodle
possum
python
quagga
rabbit
raptor
rodent
roughy
salmon
sawfly
serval
shiner
shrimp
spider
sponge
tarpon
thrush
tomcat
toucan
turkey
turtle
urchin
vervet
walrus
weasel
weevil
wombat
anchovy
anemone
bluejay
buffalo
bulldog
buzzard
caribou
catfish
chamois
cheetah
chicken
chigger
cowbird
crappie
crawdad
cricket
dogfish
dolphin
firefly
garfish
gazelle
gelding
giraffe
gobbler
gorilla
goshawk
grackle
griffon
grizzly
grouper
haddock
hagfish
halibut
hamster
herring
javelin
jawfish
jaybird
katydid
ladybug
lamprey
lemming
leopard
lioness
lobster
macaque
mallard
mammoth
manatee
mastiff
meerkat
mollusk
monarch
mongrel
monitor
monster
mudfish
muskrat
mustang
narwhal
oarfish
octopus
opossum
ostrich
panther
pegasus
pelican
penguin
phoenix
piranha
polecat
primate
quetzal
raccoon
rattler
redbird
redfish
reptile
rooster
sawfish
sculpin
seagull
skylark
snapper
spaniel
sparrow
sunbeam
sunbird
sunfish
tadpole
terrier
unicorn
vulture
wallaby
walleye
warthog
whippet
wildcat
aardvark
airedale
albacore
anteater
antelope
arachnid
barnacle
basilisk
blowfish
bluebird
bluegill
bonefish
bullfrog
cardinal
chipmunk
crayfish
dinosaur
doberman
duckling
elephant
escargot
flamingo
flounder
foxhound
glowworm
goldfish
grubworm
hedgehog
honeybee
hookworm
humpback
kangaroo
killdeer
kingfish
labrador
lacewing
ladybird
lionfish
longhorn
mackerel
malamute
marmoset
mastodon
moccasin
mongoose
monkfish
mosquito
pangolin
parakeet
pheasant
pipefish
platypus
polliwog
porpoise
reindeer
ringtail
sailfish
scorpion
seahorse
seasnail
sheepdog
shepherd
silkworm
squirrel
stallion
starfish
starling
stingray
stinkbug
sturgeon
terrapin
titmouse
tortoise
treefrog
werewolf
+30 -1
View File
@@ -6,7 +6,7 @@
]>
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="hostname">
<refentry id="hostname" xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>hostname</title>
<productname>systemd</productname>
@@ -52,6 +52,35 @@
<literal>foobar-????-????</literal> will automatically expand to <literal>foobar-92a9-061c</literal> or
similar, depending on the local machine ID.</para>
<para id="word-hostname-pattern">In addition, the token <literal>$</literal> is substituted by a word picked
deterministically from a word list, again derived from the
<citerefentry><refentrytitle>machine-id</refentrytitle><manvolnum>5</manvolnum></citerefentry> by
cryptographic hashing. Each <literal>$</literal> is positional: the first <literal>$</literal> uses the word
list file named <filename>1</filename>, the second <filename>2</filename>, and so on. This allows
human-friendly names, for example <literal>$-$-$-????</literal> might expand to
<literal>wildly-happy-octopus-92a9</literal>. The word lists are searched for in
<filename>/etc/systemd/hostname-wordlist/</filename>, <filename>/run/systemd/hostname-wordlist/</filename>,
<filename>/usr/local/lib/systemd/hostname-wordlist/</filename> and
<filename>/usr/lib/systemd/hostname-wordlist/</filename> (the first directory providing a given list wins,
lists are not merged); one word per line, with empty lines and lines starting with <literal>#</literal>
ignored.</para>
<para>The word for each token is derived deterministically from the machine ID and recomputed on every
boot (the lists are not loaded wholesale: a word is chosen by hashing to a byte offset into the file).
Consequently the word lists must be kept stable: changing a list (adding, removing, or reordering words)
may change the name a machine already has, so installations that rely on persistent hostnames must not
modify the lists after deployment. If a referenced list is missing the
name is treated as invalid and the built-in fallback hostname is used. The combined name space is the
product of the list sizes, so collisions follow the birthday bound; append a few <literal>?</literal>
characters for extra entropy when uniqueness across a large fleet matters.</para>
<para>Note that hostname can be at most 64 characters long. The word lists and the pattern should be
chosen so that the longest possible expansion (the longest words from each list plus any literal and
<literal>?</literal> characters and separators) stays within this limit. An expanded name that exceeds
the limit is considered invalid and the built-in fallback hostname is used.</para>
<xi:include href="version-info.xml" xpointer="v262"/>
<para>You may use
<citerefentry><refentrytitle>hostnamectl</refentrytitle><manvolnum>1</manvolnum></citerefentry> to change
the value of this file during runtime from the command line. Use
+1
View File
@@ -2454,6 +2454,7 @@ subdir('test')
#####################################################################
subdir('docs/var-log')
subdir('hostname-wordlist')
subdir('hwdb.d')
subdir('man')
subdir('modprobe.d')
+2
View File
@@ -248,6 +248,8 @@ option('configfiledir', type : 'string', value : '',
option('fallback-hostname', type : 'string', value : 'localhost',
description : 'the hostname used if none configured')
option('hostname-wordlist', type : 'boolean', value : false,
description : 'install default word lists for $ hostname wildcards')
option('extra-net-naming-schemes', type : 'string',
description : 'comma-separated list of extra net.naming_scheme= definitions')
option('default-net-naming-scheme', type : 'string', value : 'latest',
+6 -4
View File
@@ -18,7 +18,7 @@ char* get_default_hostname_raw(void) {
const char *e = secure_getenv("SYSTEMD_DEFAULT_HOSTNAME");
if (e) {
if (hostname_is_valid(e, VALID_HOSTNAME_QUESTION_MARK))
if (hostname_is_valid(e, VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN))
return strdup(e);
log_debug("Invalid hostname in $SYSTEMD_DEFAULT_HOSTNAME, ignoring: %s", e);
@@ -29,7 +29,7 @@ char* get_default_hostname_raw(void) {
if (r < 0)
log_debug_errno(r, "Failed to parse os-release, ignoring: %m");
else if (f) {
if (hostname_is_valid(f, VALID_HOSTNAME_QUESTION_MARK))
if (hostname_is_valid(f, VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN))
return TAKE_PTR(f);
log_debug("Invalid hostname in os-release, ignoring: %s", f);
@@ -82,7 +82,9 @@ bool hostname_is_valid(const char *s, ValidHostnameFlags flags) {
hyphen = true;
} else {
if (!valid_ldh_char(*p) && (*p != '?' || !FLAGS_SET(flags, VALID_HOSTNAME_QUESTION_MARK)))
if (!valid_ldh_char(*p) &&
(*p != '?' || !FLAGS_SET(flags, VALID_HOSTNAME_QUESTION_MARK)) &&
(*p != '$' || !FLAGS_SET(flags, VALID_HOSTNAME_WORD_TOKEN)))
return false;
dot = false;
@@ -124,7 +126,7 @@ char* hostname_cleanup(char *s) {
dot = false;
hyphen = true;
} else if (valid_ldh_char(*p) || *p == '?') {
} else if (valid_ldh_char(*p) || IN_SET(*p, '?', '$')) {
*(d++) = *p;
dot = false;
hyphen = false;
+1
View File
@@ -15,6 +15,7 @@ typedef enum ValidHostnameFlags {
VALID_HOSTNAME_TRAILING_DOT = 1 << 0, /* Accept trailing dot on multi-label names */
VALID_HOSTNAME_DOT_HOST = 1 << 1, /* Accept ".host" as valid hostname */
VALID_HOSTNAME_QUESTION_MARK = 1 << 2, /* Accept "?" as place holder for hashed machine ID value */
VALID_HOSTNAME_WORD_TOKEN = 1 << 3, /* Accept "$" as place holder for a word list substitution */
} ValidHostnameFlags;
bool hostname_is_valid(const char *s, ValidHostnameFlags flags) _pure_;
+6 -8
View File
@@ -147,13 +147,13 @@ static void context_read_etc_hostname(Context *c) {
if (r != -ENOENT)
log_warning_errno(r, "Failed to read /etc/hostname, ignoring: %m");
} else {
_cleanup_free_ char *substituted = strdup(c->data[PROP_STATIC_HOSTNAME]);
if (!substituted)
return (void) log_oom();
_cleanup_free_ char *substituted = NULL;
r = hostname_substitute_wildcards(substituted);
r = hostname_substitute_wildcards(c->data[PROP_STATIC_HOSTNAME], &substituted);
if (r < 0)
log_warning_errno(r, "Failed to substitute wildcards in /etc/hostname, ignoring: %m");
else if (!hostname_is_valid(substituted, VALID_HOSTNAME_TRAILING_DOT))
log_warning("Hostname '%s' in /etc/hostname is invalid after expansion, ignoring.", substituted);
else
c->data[PROP_STATIC_HOSTNAME_SUBSTITUTED_WILDCARDS] = TAKE_PTR(substituted);
}
@@ -1376,11 +1376,9 @@ static int validate_and_substitute_hostname(const char *name, char **ret_substit
return 0;
}
_cleanup_free_ char *substituted = strdup(name);
if (!substituted)
return log_oom();
_cleanup_free_ char *substituted = NULL;
r = hostname_substitute_wildcards(substituted);
r = hostname_substitute_wildcards(name, &substituted);
if (r < 0)
return log_error_errno(r, "Failed to substitute wildcards in hostname: %m");
+179 -33
View File
@@ -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) {
+1 -1
View File
@@ -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);
+38 -6
View File
@@ -10,8 +10,10 @@
#include "hostname-setup.h"
#include "hostname-util.h"
#include "id128-util.h"
#include "path-util.h"
#include "pidref.h"
#include "process-util.h"
#include "rm-rf.h"
#include "tests.h"
#include "tmpfile-util.h"
@@ -86,24 +88,54 @@ TEST(hostname_substitute_wildcards) {
return (void) log_tests_skipped_errno(r, "skipping wildcard hostname tests, no machine ID defined");
_cleanup_free_ char *buf = NULL;
ASSERT_NOT_NULL((buf = strdup("")));
ASSERT_OK(hostname_substitute_wildcards(buf));
ASSERT_OK(hostname_substitute_wildcards("", &buf));
ASSERT_STREQ(buf, "");
ASSERT_NULL(buf = mfree(buf));
ASSERT_NOT_NULL((buf = strdup("hogehoge")));
ASSERT_OK(hostname_substitute_wildcards(buf));
ASSERT_OK(hostname_substitute_wildcards("hogehoge", &buf));
ASSERT_STREQ(buf, "hogehoge");
ASSERT_NULL(buf = mfree(buf));
ASSERT_NOT_NULL((buf = strdup("hoge??hoge??foo?")));
ASSERT_OK(hostname_substitute_wildcards(buf));
ASSERT_OK(hostname_substitute_wildcards("hoge??hoge??foo?", &buf));
log_debug("hostname_substitute_wildcards(\"hoge??hoge??foo?\"): → \"%s\"", buf);
ASSERT_EQ(fnmatch("hoge??hoge??foo?", buf, /* flags= */ 0), 0);
ASSERT_TRUE(hostname_is_valid(buf, /* flags= */ 0));
ASSERT_NULL(buf = mfree(buf));
}
TEST(hostname_substitute_wildcards_words) {
_cleanup_(rm_rf_physical_and_freep) char *d = NULL;
int r;
r = sd_id128_get_machine(NULL);
if (ERRNO_IS_NEG_MACHINE_ID_UNSET(r))
return (void) log_tests_skipped_errno(r, "skipping word hostname tests, no machine ID defined");
ASSERT_OK(mkdtemp_malloc("/tmp/hostname-wordlist.XXXXXX", &d));
/* The n-th '$' reads the word list file named after its position. */
_cleanup_free_ char *one_list = ASSERT_PTR(path_join(d, "1"));
_cleanup_free_ char *two_list = ASSERT_PTR(path_join(d, "2"));
ASSERT_OK(write_string_file(one_list, "happy\nsad\n# comment\n\njolly\n", WRITE_STRING_FILE_CREATE));
ASSERT_OK(write_string_file(two_list, "octopus\nfalcon\nINVALID_WORD!\nbadger\n", WRITE_STRING_FILE_CREATE));
ASSERT_OK(setenv("SYSTEMD_HOSTNAME_WORDLIST_PATH", d, /* overwrite= */ true));
_cleanup_free_ char *a = NULL, *b = NULL;
ASSERT_OK(hostname_substitute_wildcards("$-$", &a));
log_debug("hostname_substitute_wildcards(\"$-$\"): → \"%s\"", a);
ASSERT_TRUE(hostname_is_valid(a, /* flags= */ 0));
/* Fully deterministic: same machine ID + same lists → same name */
ASSERT_OK(hostname_substitute_wildcards("$-$", &b));
ASSERT_STREQ(a, b);
/* Missing list (no file "3") → error (caller falls back to built-in hostname) */
_cleanup_free_ char *e = NULL;
ASSERT_ERROR(hostname_substitute_wildcards("$-$-$", &e), ENOENT);
ASSERT_OK(unsetenv("SYSTEMD_HOSTNAME_WORDLIST_PATH"));
}
TEST(hostname_setup) {
hostname_setup(false);
}
+37
View File
@@ -283,6 +283,43 @@ testcase_wildcard() {
assert_in "Static hostname: foo-" "$(hostnamectl)"
}
restore_wildcard_words() {
rm -rf /etc/systemd/hostname-wordlist
if [[ -d /tmp/hostname-wordlist.bak ]]; then
mv /tmp/hostname-wordlist.bak /etc/systemd/hostname-wordlist
fi
hostnamectl set-hostname "$SAVED"
}
testcase_wildcard_words() {
# The n-th '$' token is substituted deterministically from the machine ID using the
# word list file named after its position (see hostname(5) and hostname-wordlist/README).
SAVED=""
[[ -f /etc/hostname ]] && SAVED="$(cat /etc/hostname)"
[[ -d /etc/systemd/hostname-wordlist ]] && mv /etc/systemd/hostname-wordlist /tmp/hostname-wordlist.bak
trap restore_wildcard_words EXIT
mkdir -p /etc/systemd/hostname-wordlist
printf 'wildly\nquietly\n' >/etc/systemd/hostname-wordlist/1
printf 'happy\nsad\n' >/etc/systemd/hostname-wordlist/2
printf 'octopus\nfalcon\n' >/etc/systemd/hostname-wordlist/3
# each '$' expands to a word from the list at its position
hostnamectl set-hostname '$-$-$'
H="$(hostname)"
assert_neq "$H" '$-$-$'
assert_eq "$(cat /etc/hostname)" '$-$-$'
IFS='-' read -r w1 w2 w3 <<<"$H"
grep -Fx -- "$w1" /etc/systemd/hostname-wordlist/1 >/dev/null
grep -Fx -- "$w2" /etc/systemd/hostname-wordlist/2 >/dev/null
grep -Fx -- "$w3" /etc/systemd/hostname-wordlist/3 >/dev/null
# the choice is deterministic: setting the same template again yields the same name
hostnamectl set-hostname testhost
hostnamectl set-hostname '$-$-$'
assert_eq "$(hostname)" "$H"
}
teardown_hostnamed_alternate_paths() {
set +eu