diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index a7661670d7323c73dd5362a36d1691cf24690b2c..9d9208edddab443c92e796b92490cd92b4f7372d 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -95,8 +95,10 @@ include_directories(${EXTRA_BOX_INCLUDE_DIRS})
 add_library(box_error STATIC error.cc errcode.c mp_error.cc)
 target_link_libraries(box_error core stat mpstream vclock)
 
+add_library(node_name STATIC node_name.c)
+
 add_library(xrow STATIC xrow.c iproto_constants.c iproto_features.c)
-target_link_libraries(xrow server core small vclock misc box_error
+target_link_libraries(xrow server core small vclock misc box_error node_name
                       ${MSGPUCK_LIBRARIES})
 
 set(tuple_sources
@@ -324,6 +326,6 @@ add_custom_command(OUTPUT ${SQL_BIN_DIR}/opcodes.c
         ${SQL_BIN_DIR}/opcodes.h)
 
 target_link_libraries(box box_error tuple stat xrow xlog vclock crc32 raft
-                      ${common_libraries})
+                      node_name ${common_libraries})
 
 add_dependencies(box build_bundled_libs generate_sql_files)
diff --git a/src/box/node_name.c b/src/box/node_name.c
new file mode 100644
index 0000000000000000000000000000000000000000..4443fbd2be5b4dc8fc57b4fa3c16d95a727ba089
--- /dev/null
+++ b/src/box/node_name.c
@@ -0,0 +1,24 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#include "node_name.h"
+
+#include <ctype.h>
+
+bool
+node_name_is_valid_n(const char *name, size_t len)
+{
+	if (len == 0 || len > NODE_NAME_LEN_MAX || !isalpha(*name))
+		return false;
+	const char *end = name + len;
+	while (name < end) {
+		char c = *(name++);
+		if (!isalnum(c) && c != '-')
+			return false;
+		if (tolower(c) != c)
+			return false;
+	}
+	return true;
+}
diff --git a/src/box/node_name.h b/src/box/node_name.h
new file mode 100644
index 0000000000000000000000000000000000000000..c11fdee1818855eefc8f98e848e95db030bdb115
--- /dev/null
+++ b/src/box/node_name.h
@@ -0,0 +1,55 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#pragma once
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+/**
+ * Name suitable for a node visible in the network. Its format matches the
+ * sub-domain label in RFC 1035, section 2.3.1
+ * (https://www.rfc-editor.org/rfc/rfc1035#section-2.3.1).
+ *
+ * It allows to use the node name as a sub-domain and a host name.
+ *
+ * The limitations are: max 63 symbols (not including term 0); only lowercase
+ * letters, digits, and hyphen. Can start only with a letter. Note that the
+ * sub-domain name rules say that uppercase is allowed but the names are
+ * case-insensitive. In Tarantool the lowercase is enforced.
+ */
+
+enum {
+	NODE_NAME_LEN_MAX = 63,
+	NODE_NAME_SIZE_MAX = NODE_NAME_LEN_MAX + 1,
+};
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+/** Check if the node name of the given length is valid. */
+bool
+node_name_is_valid_n(const char *name, size_t len);
+
+static inline bool
+node_name_is_valid(const char *name)
+{
+	return node_name_is_valid_n(name, strnlen(name, NODE_NAME_SIZE_MAX));
+}
+
+static inline const char *
+node_name_str(const char *name)
+{
+	if (name == NULL || *name == 0)
+		return "<no-name>";
+	return name;
+}
+
+#if defined(__cplusplus)
+}
+#endif /* defined(__cplusplus) */
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 24f45f201d2d71f15e4798ebe14dd71a4d588284..4649c6e897da2fc1b87a6c7a3bfe49d046e9b925 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -600,3 +600,8 @@ create_unit_test(PREFIX lua_tweaks
                            ${LIBYAML_LIBRARIES}
                            ${READLINE_LIBRARIES}
 )
+
+create_unit_test(PREFIX node_name
+                 SOURCES node_name.c core_test_utils.c
+                 LIBRARIES core unit node_name
+)
diff --git a/test/unit/node_name.c b/test/unit/node_name.c
new file mode 100644
index 0000000000000000000000000000000000000000..d17d4d3ffee7c67379962bb8b3f1ae5028ce775a
--- /dev/null
+++ b/test/unit/node_name.c
@@ -0,0 +1,79 @@
+#include "node_name.h"
+
+#include "trivia/util.h"
+#define UNIT_TAP_COMPATIBLE 1
+#include "unit.h"
+
+static void
+test_node_name_is_valid(void)
+{
+	header();
+	plan(27);
+
+	const char *bad_names[] = {
+		"",
+		"1",
+		"1abc",
+		"*",
+		"a_b",
+		"aBcD",
+		"a~b",
+		"{ab}",
+	};
+	for (int i = 0; i < (int)lengthof(bad_names); ++i) {
+		const char *str = bad_names[i];
+		ok(!node_name_is_valid(str), "bad name %d", i);
+		ok(!node_name_is_valid_n(str, strlen(str)),
+		   "bad name n %d", i);
+	}
+	const char *good_names[] = {
+		"a",
+		"a-b-c",
+		"abc",
+		"a1b2c3-d4-e5-",
+	};
+	for (int i = 0; i < (int)lengthof(good_names); ++i) {
+		const char *str = good_names[i];
+		ok(node_name_is_valid(str), "bad name %d", i);
+		ok(node_name_is_valid_n(str, strlen(str)),
+		   "bad name n %d", i);
+	}
+	char name[NODE_NAME_SIZE_MAX + 1];
+	memset(name, 'a', sizeof(name));
+	ok(!node_name_is_valid_n(name, NODE_NAME_SIZE_MAX), "max + 1");
+	ok(node_name_is_valid_n(name, NODE_NAME_LEN_MAX), "max n");
+	name[NODE_NAME_SIZE_MAX - 1] = 0;
+	ok(node_name_is_valid(name), "max");
+
+	check_plan();
+	footer();
+}
+
+static void
+test_node_name_str(void)
+{
+	header();
+	plan(3);
+
+	const char *stub = "<no-name>";
+	is(strcmp(node_name_str("abc"), "abc"), 0, "name");
+	is(strcmp(node_name_str(""), stub), 0, "empty");
+	is(strcmp(node_name_str(NULL), stub), 0, "null");
+
+	check_plan();
+	footer();
+}
+
+int
+main(void)
+{
+	header();
+	plan(2);
+
+	test_node_name_is_valid();
+	test_node_name_str();
+
+	int rc = check_plan();
+	footer();
+	return rc;
+}