(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.
--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+# 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
--- /dev/null
+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
]>
<!-- 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>
<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
#####################################################################
subdir('docs/var-log')
+subdir('hostname-wordlist')
subdir('hwdb.d')
subdir('man')
subdir('modprobe.d')
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',
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);
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);
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;
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;
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_;
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);
}
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");
/* 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"
#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"
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 */
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);
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. */
-
- 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;
- }
+ * '?' 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. */
- 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) {
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) {
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);
#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"
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);
}
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