From 15d90a7dbad2e73f6ef3e6bad3bace0ec11fa405 Mon Sep 17 00:00:00 2001
From: Christian Heimes <cheimes@redhat.com>
Date: Fri, 11 Sep 2020 14:49:16 +0200
Subject: [PATCH] [Container] Unify access to FQDN

FreeIPA's Python and C code used different approaches to get the FQDN of
the host. Some places assumed that gethostname() returns a FQDN. Other
code paths used glibc's resolver to resolve the current node name to a
FQDN.

Python code now uses the ipalib.constants.FQDN where a fully qualified
domain name is expected. The variable is initialized only once and avoids
potential DNS lookups.

C code uses a new helper function ipa_gethostfqdn() in util package. The
function implements similar logic as gethostfqdn() except it uses more
modern getaddrinfo(). The result is cached as well.

Signed-off-by: Christian Heimes <cheimes@redhat.com>
---
 daemons/ipa-otpd/Makefile.am                  |   2 +
 daemons/ipa-otpd/main.c                       |   5 +-
 daemons/ipa-sam/ipa_sam.c                     |  11 +-
 .../ipa-slapi-plugins/ipa-cldap/Makefile.am   |   3 +
 .../ipa-cldap/ipa_cldap_netlogon.c            |   7 +-
 install/tools/ipa-csreplica-manage.in         |   7 +-
 install/tools/ipa-custodia-check.in           |   9 --
 install/tools/ipa-replica-conncheck.in        |   3 +-
 install/tools/ipa-replica-manage.in           |   3 +-
 ipaclient/discovery.py                        |   4 +-
 ipaclient/install/client.py                   |   6 +-
 ipalib/constants.py                           |  11 +-
 ipapython/config.py                           |   4 +-
 ipapython/fqdn.py                             |  31 +++++
 ipaserver/dcerpc.py                           |   4 +-
 ipaserver/install/installutils.py             |  20 ++--
 ipaserver/install/ldapupdate.py               |   2 +-
 ipaserver/install/schemaupdate.py             |   6 +-
 ipaserver/install/server/install.py           |   6 +-
 ipaserver/install/service.py                  |   4 +-
 ipaserver/plugins/join.py                     |   5 +-
 ipatests/test_install/test_updates.py         |   5 +-
 util/Makefile.am                              |  10 +-
 util/ipa_hostname.c                           | 111 ++++++++++++++++++
 util/ipa_hostname.h                           |  19 +++
 25 files changed, 228 insertions(+), 70 deletions(-)
 create mode 100644 ipapython/fqdn.py
 create mode 100644 util/ipa_hostname.c
 create mode 100644 util/ipa_hostname.h

diff --git a/daemons/ipa-otpd/Makefile.am b/daemons/ipa-otpd/Makefile.am
index 8672693004..975a0c935d 100644
--- a/daemons/ipa-otpd/Makefile.am
+++ b/daemons/ipa-otpd/Makefile.am
@@ -1,9 +1,11 @@
+AM_CPPFLAGS := -I$(top_srcdir)/util
 AM_CFLAGS := @LDAP_CFLAGS@ @LIBVERTO_CFLAGS@ @KRB5_CFLAGS@ @NSPR_CFLAGS@
 AM_LDFLAGS := @LDAP_LIBS@ @LIBVERTO_LIBS@ @KRAD_LIBS@ @KRB5_LIBS@
 
 noinst_HEADERS = internal.h
 appdir = $(libexecdir)/ipa/
 app_PROGRAMS = ipa-otpd
+ipa_otpd_LDADD = $(top_builddir)/util/libutil.la
 dist_noinst_DATA = ipa-otpd.socket.in ipa-otpd@.service.in test.py
 systemdsystemunit_DATA = ipa-otpd.socket ipa-otpd@.service
 
diff --git a/daemons/ipa-otpd/main.c b/daemons/ipa-otpd/main.c
index aebc039bc0..1538cb8614 100644
--- a/daemons/ipa-otpd/main.c
+++ b/daemons/ipa-otpd/main.c
@@ -32,6 +32,7 @@
 
 #include <signal.h>
 #include <stdbool.h>
+#include "ipa_hostname.h"
 
 /* Our global state. */
 struct otpd_context ctx;
@@ -212,7 +213,7 @@ static krb5_error_code setup_ldap(const char *uri, krb5_boolean bind,
 
 int main(int argc, char **argv)
 {
-    char hostname[HOST_NAME_MAX + 1];
+    char hostname[IPA_HOST_NAME_LEN];
     krb5_error_code retval;
     krb5_data hndata;
     verto_ev *sig;
@@ -227,7 +228,7 @@ int main(int argc, char **argv)
     memset(&ctx, 0, sizeof(ctx));
     ctx.exitstatus = 1;
 
-    if (gethostname(hostname, sizeof(hostname)) < 0) {
+    if (ipa_gethostfqdn(hostname) < 0) {
         otpd_log_err(errno, "Unable to get hostname");
         goto error;
     }
diff --git a/daemons/ipa-sam/ipa_sam.c b/daemons/ipa-sam/ipa_sam.c
index 6431417b08..c0d63b9821 100644
--- a/daemons/ipa-sam/ipa_sam.c
+++ b/daemons/ipa-sam/ipa_sam.c
@@ -36,6 +36,7 @@ char *smb_xstrdup(const char *s);
 #include <sasl/sasl.h>
 #include <krb5/krb5.h>
 #include <sss_idmap.h>
+#include "ipa_hostname.h"
 #include "ipa_asn1.h"
 #include "ipa_pwd.h"
 #include "ipa_mspac.h"
@@ -4440,7 +4441,7 @@ static char *sec_key(TALLOC_CTX *mem_ctx, const char *d)
 
 static NTSTATUS save_sid_to_secret(struct ipasam_private *ipasam_state)
 {
-	char hostname[255];
+	char hostname[IPA_HOST_NAME_LEN];
 	int ret;
 	char *p;
 	TALLOC_CTX *tmp_ctx;
@@ -4466,13 +4467,12 @@ static NTSTATUS save_sid_to_secret(struct ipasam_private *ipasam_state)
 		goto done;
 	}
 
-	ret = gethostname(hostname, sizeof(hostname));
+	ret = ipa_gethostfqdn(hostname);
 	if (ret == -1) {
 		DEBUG(1, ("gethostname failed.\n"));
 		status = NT_STATUS_UNSUCCESSFUL;
 		goto done;
 	}
-	hostname[sizeof(hostname)-1] = '\0';
 	p = strchr(hostname, '.');
 	if (p != NULL) {
 		*p = '\0';
@@ -4724,7 +4724,7 @@ static NTSTATUS ipasam_generate_principals(struct ipasam_private *ipasam_state)
 	int ret;
 	krb5_context context;
 	NTSTATUS status = NT_STATUS_UNSUCCESSFUL;
-	char hostname[255];
+	char hostname[IPA_HOST_NAME_LEN];
 	char *default_realm = NULL;
 
 	if (!ipasam_state) {
@@ -4736,12 +4736,11 @@ static NTSTATUS ipasam_generate_principals(struct ipasam_private *ipasam_state)
 		return status;
 	}
 
-	ret = gethostname(hostname, sizeof(hostname));
+	ret = ipa_gethostfqdn(hostname);
 	if (ret == -1) {
 		DEBUG(1, ("gethostname failed.\n"));
 		goto done;
 	}
-	hostname[sizeof(hostname)-1] = '\0';
 
 	rc = krb5_get_default_realm(context, &default_realm);
 	if (rc) {
diff --git a/daemons/ipa-slapi-plugins/ipa-cldap/Makefile.am b/daemons/ipa-slapi-plugins/ipa-cldap/Makefile.am
index 0eae746e99..da600d0630 100644
--- a/daemons/ipa-slapi-plugins/ipa-cldap/Makefile.am
+++ b/daemons/ipa-slapi-plugins/ipa-cldap/Makefile.am
@@ -5,6 +5,7 @@ PLUGIN_COMMON_DIR = $(srcdir)/../common
 AM_CPPFLAGS =							\
 	-I$(srcdir)						\
 	-I$(PLUGIN_COMMON_DIR)					\
+	-I$(top_srcdir)/util			\
 	-DPREFIX=\""$(prefix)"\" 				\
 	-DBINDIR=\""$(bindir)"\"				\
 	-DLIBDIR=\""$(libdir)"\" 				\
@@ -31,6 +32,7 @@ libipa_cldap_la_SOURCES = 		\
 libipa_cldap_la_LDFLAGS = -avoid-version
 
 libipa_cldap_la_LIBADD = 		\
+	$(top_builddir)/util/libutil.la	\
 	$(LDAP_LIBS)			\
 	$(NDRNBT_LIBS)			\
 	$(NULL)
@@ -49,6 +51,7 @@ ipa_cldap_tests_LDFLAGS =	\
 	-rpath $(shell pkg-config --libs-only-L dirsrv | sed -e 's/-L//')	\
 	$(NULL)
 ipa_cldap_tests_LDADD =	\
+	$(top_builddir)/util/libutil.la	\
 	$(CMOCKA_LIBS)	\
 	$(NDRNBT_LIBS)	\
 	$(DIRSRV_LIBS)	\
diff --git a/daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c b/daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c
index 460f96cd59..eb0d183157 100644
--- a/daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c
+++ b/daemons/ipa-slapi-plugins/ipa-cldap/ipa_cldap_netlogon.c
@@ -38,6 +38,7 @@
  * END COPYRIGHT BLOCK **/
 
 #include "ipa_cldap.h"
+#include "ipa_hostname.h"
 #include <endian.h>
 #include <talloc.h>
 #include <ctype.h>
@@ -236,7 +237,7 @@ int ipa_cldap_netlogon(struct ipa_cldap_ctx *ctx,
                        struct ipa_cldap_req *req,
                        struct berval *reply)
 {
-    char hostname[MAXHOSTNAMELEN + 1]; /* NOTE: lenght hardcoded in kernel */
+    char hostname[IPA_HOST_NAME_LEN];
     char *domain = NULL;
     char *our_domain = NULL;
     char *guid = NULL;
@@ -321,13 +322,11 @@ int ipa_cldap_netlogon(struct ipa_cldap_ctx *ctx,
         goto done;
     }
 
-    ret = gethostname(hostname, MAXHOSTNAMELEN);
+    ret = ipa_gethostfqdn(hostname);
     if (ret == -1) {
         ret = errno;
         goto done;
     }
-    /* Make double sure it is terminated */
-    hostname[MAXHOSTNAMELEN] = '\0';
     dot = strchr(hostname, '.');
     if (!dot) {
         /* this name is not fully qualified, therefore invalid */
diff --git a/install/tools/ipa-csreplica-manage.in b/install/tools/ipa-csreplica-manage.in
index d612054333..5faed5a841 100644
--- a/install/tools/ipa-csreplica-manage.in
+++ b/install/tools/ipa-csreplica-manage.in
@@ -30,6 +30,7 @@ from ipaplatform.paths import paths
 from ipaserver.install import (replication, installutils, bindinstance,
     cainstance)
 from ipalib import api, errors
+from ipalib.constants import FQDN
 from ipalib.util import has_managed_topology
 from ipapython import ipautil, ipaldap, version
 from ipapython.admintool import ScriptError
@@ -343,7 +344,7 @@ def re_initialize(realm, options):
     if not options.fromhost:
         sys.exit("re-initialize requires the option --from <host name>")
 
-    thishost = installutils.get_fqdn()
+    thishost = FQDN
 
     try:
         repl = replication.get_cs_replication_manager(realm, options.fromhost,
@@ -383,7 +384,7 @@ def force_sync(realm, thishost, fromhost, dirman_passwd):
 
 def set_renewal_master(realm, replica):
     if not replica:
-        replica = installutils.get_fqdn()
+        replica = FQDN
 
     ca = cainstance.CAInstance(realm)
     if ca.is_renewal_master(replica):
@@ -434,7 +435,7 @@ def main():
     if options.host:
         host = options.host
     else:
-        host = installutils.get_fqdn()
+        host = FQDN
 
     options.host = host
 
diff --git a/install/tools/ipa-custodia-check.in b/install/tools/ipa-custodia-check.in
index 5143dc4983..fa8db31157 100644
--- a/install/tools/ipa-custodia-check.in
+++ b/install/tools/ipa-custodia-check.in
@@ -9,7 +9,6 @@ import argparse
 import logging
 import os
 import platform
-import socket
 import warnings
 
 from custodia.message.kem import KEY_USAGE_SIG, KEY_USAGE_ENC, KEY_USAGE_MAP
@@ -134,7 +133,6 @@ class IPACustodiaTester:
 
     def check(self):
         self.status()
-        self.check_fqdn()
         self.check_files()
         self.check_client()
         self.check_jwk()
@@ -154,13 +152,6 @@ class IPACustodiaTester:
         if self.host == self.args.server:
             self.warning("Performing self-test only.")
 
-    def check_fqdn(self):
-        fqdn = socket.getfqdn()
-        if self.host != fqdn:
-            self.warning(
-                "socket.getfqdn() reports hostname '{}'".format(fqdn)
-            )
-
     def check_files(self):
         for filename in self.files:
             if not os.path.isfile(filename):
diff --git a/install/tools/ipa-replica-conncheck.in b/install/tools/ipa-replica-conncheck.in
index b22db11397..fdc3224d6b 100644
--- a/install/tools/ipa-replica-conncheck.in
+++ b/install/tools/ipa-replica-conncheck.in
@@ -28,6 +28,7 @@ from ipapython.dn import DN
 from ipapython import version
 from ipapython import ipautil, certdb
 from ipalib import api, errors, x509
+from ipalib.constants import FQDN
 from ipaserver.install import installutils
 # pylint: disable=deprecated-module
 from optparse import OptionGroup, OptionValueError
@@ -205,7 +206,7 @@ def parse_options():
        parser.error("No action: you should select either --replica or --master option.")
 
     if not options.hostname:
-        options.hostname = socket.getfqdn()
+        options.hostname = FQDN
 
     return safe_options, options
 
diff --git a/install/tools/ipa-replica-manage.in b/install/tools/ipa-replica-manage.in
index f4e949407b..a29c550d20 100644
--- a/install/tools/ipa-replica-manage.in
+++ b/install/tools/ipa-replica-manage.in
@@ -38,6 +38,7 @@ from ipaserver.install import bindinstance, cainstance
 from ipaserver.install import opendnssecinstance, dnskeysyncinstance
 from ipapython import version, ipaldap
 from ipalib import api, errors
+from ipalib.constants import FQDN
 from ipalib.util import has_managed_topology, verify_host_resolvable
 from ipapython.ipa_log_manager import standard_logging_setup
 from ipapython.dn import DN
@@ -1525,7 +1526,7 @@ def main(options, args):
     if options.host:
         host = options.host
     else:
-        host = installutils.get_fqdn()
+        host = FQDN
 
     options.host = host
 
diff --git a/ipaclient/discovery.py b/ipaclient/discovery.py
index fb344a34a8..035e33c5e7 100644
--- a/ipaclient/discovery.py
+++ b/ipaclient/discovery.py
@@ -20,13 +20,13 @@
 from __future__ import absolute_import
 
 import logging
-import socket
 
 import six
 
 from dns import rdatatype
 from dns.exception import DNSException
 from ipalib import errors
+from ipalib.constants import FQDN
 from ipalib.util import validate_domain_name
 from ipapython.dnsutil import query_srv, resolve
 
@@ -222,7 +222,7 @@ def search(self, domain="", servers="", realm=None, hostname=None,
             if not domain:  # domain not provided do full DNS discovery
                 # get the local host name
                 if not hostname:
-                    hostname = socket.getfqdn()
+                    hostname = FQDN
                     logger.debug('Hostname: %s', hostname)
                 if not hostname:
                     return BAD_HOST_CONFIG
diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py
index 960e25e061..175a56cb76 100644
--- a/ipaclient/install/client.py
+++ b/ipaclient/install/client.py
@@ -36,7 +36,7 @@
 
 from ipalib import api, errors, x509
 from ipalib import sysrestore
-from ipalib.constants import IPAAPI_USER, MAXHOSTNAMELEN
+from ipalib.constants import FQDN, IPAAPI_USER, MAXHOSTNAMELEN
 from ipalib.install import certmonger, certstore, service
 from ipalib.install import hostname as hostname_
 from ipalib.facts import is_ipa_client_configured, is_ipa_configured
@@ -2121,7 +2121,7 @@ def install_check(options):
         hostname = options.hostname
         hostname_source = 'Provided as option'
     else:
-        hostname = socket.getfqdn()
+        hostname = FQDN
         hostname_source = "Machine's FQDN"
     if hostname != hostname.lower():
         raise ScriptError(
@@ -3270,7 +3270,7 @@ def uninstall(options):
         pass
 
     if hostname is None:
-        hostname = socket.getfqdn()
+        hostname = FQDN
 
     ipa_db = certdb.NSSDatabase(paths.IPA_NSSDB_DIR)
     sys_db = certdb.NSSDatabase(paths.NSS_DB_DIR)
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 91d885acc0..1a414905a5 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -23,17 +23,12 @@
 """
 
 import os
-import socket
 from ipapython.dn import DN
+from ipapython.fqdn import gethostfqdn
 from ipapython.version import VERSION, API_VERSION
 
-try:
-    FQDN = socket.getfqdn()
-except Exception:
-    try:
-        FQDN = socket.gethostname()
-    except Exception:
-        FQDN = None
+
+FQDN = gethostfqdn()
 
 # TLS related constants
 # * SSL2 and SSL3 are broken.
diff --git a/ipapython/config.py b/ipapython/config.py
index 1f4bfe0119..d26f8c4d8f 100644
--- a/ipapython/config.py
+++ b/ipapython/config.py
@@ -25,12 +25,12 @@
 from copy import copy
 from configparser import SafeConfigParser
 from urllib.parse import urlsplit
-import socket
 import functools
 
 from dns.exception import DNSException
 import dns.name
 
+from ipalib.constants import FQDN  # pylint: disable=ipa-forbidden-import
 from ipaplatform.paths import paths
 from ipapython.dn import DN
 from ipapython.dnsutil import query_srv
@@ -212,7 +212,7 @@ def __discover_config(discover_server = True):
             except DNSException:
                 # try cycling on domain components of FQDN
                 try:
-                    domain = dns.name.from_text(socket.getfqdn())
+                    domain = dns.name.from_text(FQDN)
                 except DNSException:
                     return False
 
diff --git a/ipapython/fqdn.py b/ipapython/fqdn.py
new file mode 100644
index 0000000000..baf778ad38
--- /dev/null
+++ b/ipapython/fqdn.py
@@ -0,0 +1,31 @@
+#
+# Copyright (C) 2020  FreeIPA Contributors see COPYING for license
+#
+"""Get host's FQDN
+"""
+import socket
+
+
+def gethostfqdn():
+    hostname = socket.gethostname()
+
+    # optional optimization, consider hostname with dot as FQDN
+    if "." in hostname:
+        return hostname
+
+    # this call can never fail except for misconfigured nsswitch.conf
+    # without nss-myhostname provider. The myhostname provider translates
+    # gethostname() to local interfaces.
+    gai = socket.getaddrinfo(
+        hostname,
+        None,  # service/port is irrelevant
+        family=socket.AF_UNSPEC,  # IPv4 or IPv6
+        type=socket.SOCK_DGRAM,  # optimization, TCP/RAW gives same result
+        proto=None,   # any protocol
+        # include canonical name in first addrinfo struct
+        # only use address family when at least one non-local interface
+        # is configured with that address family
+        flags=socket.AI_CANONNAME | socket.AI_ADDRCONFIG
+    )
+    # first addrinfo struct, fourth field is canonical name
+    return gai[0][3]
diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py
index 27d075425c..ff5627060f 100644
--- a/ipaserver/dcerpc.py
+++ b/ipaserver/dcerpc.py
@@ -30,11 +30,11 @@
 
 from ipalib import api, _
 from ipalib import errors
+from ipalib.constants import FQDN
 from ipapython import ipautil
 from ipapython.dn import DN
 from ipapython.dnsutil import query_srv
 from ipapython.ipaldap import ldap_initialize
-from ipaserver.install import installutils
 from ipaserver.dcerpc_common import (TRUST_BIDIRECTIONAL,
                                      TRUST_JOIN_EXTERNAL,
                                      trust_type_string)
@@ -1645,7 +1645,7 @@ def __populate_local_domain(self):
         ld.creds.set_kerberos_state(credentials.MUST_USE_KERBEROS)
         ld.creds.guess(ld.parm)
         ld.creds.set_workstation(ld.hostname)
-        ld.retrieve(installutils.get_fqdn())
+        ld.retrieve(FQDN)
         self.local_domain = ld
 
     def populate_remote_domain(self, realm, realm_server=None,
diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py
index 60dd6dcebc..6eb7f7c84b 100644
--- a/ipaserver/install/installutils.py
+++ b/ipaserver/install/installutils.py
@@ -49,7 +49,7 @@
 from ipapython import ipautil, admintool, version, ipaldap
 from ipapython.admintool import ScriptError, SERVER_NOT_CONFIGURED  # noqa: E402
 from ipapython.certdb import EXTERNAL_CA_TRUST_FLAGS
-from ipalib.constants import MAXHOSTNAMELEN
+from ipalib.constants import FQDN, MAXHOSTNAMELEN
 from ipalib.util import validate_hostname
 from ipalib import api, errors, x509
 from ipapython.dn import DN
@@ -116,16 +116,16 @@ def __init__(self, top_dir=None):
 
     subject_base = ipautil.dn_attribute_property('_subject_base')
 
+
 def get_fqdn():
-    fqdn = ""
-    try:
-        fqdn = socket.getfqdn()
-    except Exception:
-        try:
-            fqdn = socket.gethostname()
-        except Exception:
-            fqdn = ""
-    return fqdn
+    """Get fully qualified domain name of current host
+
+    :note: used by ansible_freeipa
+    :deprecated: use ipalib.constants.FQDN
+    :return: str
+    """
+    return FQDN
+
 
 def verify_fqdn(host_name, no_host_dns=False, local_hostname=True):
     """
diff --git a/ipaserver/install/ldapupdate.py b/ipaserver/install/ldapupdate.py
index fd98098a0e..fff0d09a4d 100644
--- a/ipaserver/install/ldapupdate.py
+++ b/ipaserver/install/ldapupdate.py
@@ -284,7 +284,7 @@ def __init__(self, dm_password=None, sub_dict={},
         if suffix is not None:
             assert isinstance(suffix, DN)
 
-        fqdn = installutils.get_fqdn()
+        fqdn = constants.FQDN
         if fqdn is None:
             raise RuntimeError("Unable to determine hostname")
 
diff --git a/ipaserver/install/schemaupdate.py b/ipaserver/install/schemaupdate.py
index 0370ef3196..ab0dd8ac4b 100644
--- a/ipaserver/install/schemaupdate.py
+++ b/ipaserver/install/schemaupdate.py
@@ -24,9 +24,9 @@
 
 import ipapython.version
 from ipalib import api
+from ipalib.constants import FQDN
 from ipapython.dn import DN
 from ipaserver.install.ldapupdate import connect
-from ipaserver.install import installutils
 
 
 SCHEMA_ELEMENT_CLASSES = (
@@ -105,9 +105,7 @@ def update_schema(schema_files, ldapi=False):
     """
     SCHEMA_ELEMENT_CLASSES_KEYS = [x[0] for x in SCHEMA_ELEMENT_CLASSES]
 
-    conn = connect(ldapi=ldapi,
-                   realm=api.env.realm,
-                   fqdn=installutils.get_fqdn())
+    conn = connect(ldapi=ldapi, realm=api.env.realm, fqdn=FQDN)
 
     old_schema = conn.schema
 
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index 50a0a60ddb..a2cfda516a 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -31,7 +31,7 @@
 from ipaplatform.paths import paths
 from ipaplatform.tasks import tasks
 from ipalib import api, errors, x509
-from ipalib.constants import DOMAIN_LEVEL_0
+from ipalib.constants import DOMAIN_LEVEL_0, FQDN
 from ipalib.facts import is_ipa_configured, is_ipa_client_configured
 from ipalib.util import (
     validate_domain_name,
@@ -44,7 +44,7 @@
     otpdinstance, custodiainstance, replication, service,
     sysupgrade, cainstance)
 from ipaserver.install.installutils import (
-    BadHostError, get_fqdn, get_server_ip_address,
+    BadHostError, get_server_ip_address,
     load_pkcs12, read_password, verify_fqdn, update_hosts_file,
     validate_mask)
 
@@ -491,7 +491,7 @@ def install_check(installer):
     if options.host_name:
         host_default = options.host_name
     else:
-        host_default = get_fqdn()
+        host_default = FQDN
 
     if installer.interactive and not options.host_name:
         host_name = read_host_name(host_default)
diff --git a/ipaserver/install/service.py b/ipaserver/install/service.py
index 2123a17de4..9f23651df0 100644
--- a/ipaserver/install/service.py
+++ b/ipaserver/install/service.py
@@ -23,7 +23,6 @@
 import sys
 import os
 import pwd
-import socket
 import time
 import traceback
 import tempfile
@@ -36,6 +35,7 @@
 from ipapython.dn import DN
 from ipapython import kerberos
 from ipalib import api, errors, x509
+from ipalib.constants import FQDN
 from ipaplatform import services
 from ipaplatform.paths import paths
 from ipaserver.masters import (
@@ -290,7 +290,7 @@ def __init__(self, service_name, service_desc=None, sstore=None,
         self.steps = []
         self.output_fd = sys.stdout
 
-        self.fqdn = socket.gethostname()
+        self.fqdn = FQDN
 
         if sstore:
             self.sstore = sstore
diff --git a/ipaserver/plugins/join.py b/ipaserver/plugins/join.py
index eb0d309ac4..606e3b3751 100644
--- a/ipaserver/plugins/join.py
+++ b/ipaserver/plugins/join.py
@@ -25,7 +25,8 @@
 from ipalib import Command, Str
 from ipalib import errors
 from ipalib import _
-from ipaserver.install import installutils
+from ipalib.constants import FQDN
+
 
 __doc__ = _("""
 Joining an IPA domain
@@ -60,7 +61,7 @@ class join(Command):
             validate_host,
             cli_name='hostname',
             doc=_("The hostname to register as"),
-            default_from=lambda: unicode(installutils.get_fqdn()),
+            default_from=lambda: FQDN,
             autofill=True,
             #normalizer=lamda value: value.lower(),
         ),
diff --git a/ipatests/test_install/test_updates.py b/ipatests/test_install/test_updates.py
index 1c9ba90aaa..b6744cf467 100644
--- a/ipatests/test_install/test_updates.py
+++ b/ipatests/test_install/test_updates.py
@@ -28,8 +28,8 @@
 
 from ipalib import api
 from ipalib import errors
+from ipalib.constants import FQDN
 from ipaserver.install.ldapupdate import LDAPUpdate, BadSyntax
-from ipaserver.install import installutils
 from ipapython import ipaldap
 from ipaplatform.constants import constants as platformconstants
 from ipapython.dn import DN
@@ -56,7 +56,6 @@ class TestUpdate:
 
     @pytest.fixture(autouse=True)
     def update_setup(self, request):
-        fqdn = installutils.get_fqdn()
         pwfile = api.env.dot_ipa + os.sep + ".dmpw"
         if os.path.isfile(pwfile):
             with open(pwfile, "r") as fp:
@@ -64,7 +63,7 @@ def update_setup(self, request):
         else:
             pytest.skip("No directory manager password")
         self.updater = LDAPUpdate(dm_password=self.dm_password, sub_dict={})
-        self.ld = ipaldap.LDAPClient.from_hostname_secure(fqdn)
+        self.ld = ipaldap.LDAPClient.from_hostname_secure(FQDN)
         self.ld.simple_bind(bind_dn=ipaldap.DIRMAN_DN,
                             bind_password=self.dm_password)
         self.testdir = os.path.abspath(os.path.dirname(__file__))
diff --git a/util/Makefile.am b/util/Makefile.am
index a2400063d0..997e665f1d 100644
--- a/util/Makefile.am
+++ b/util/Makefile.am
@@ -1,17 +1,23 @@
+NULL =
+
 AUTOMAKE_OPTIONS = 1.7 subdir-objects
 
 AM_CPPFLAGS = $(CRYPTO_CFLAGS) $(KRB5_CFLAGS) $(LDAP_CFLAGS)
 
 noinst_LTLIBRARIES = libutil.la
 
-libutil_la_SOURCES =	ipa_krb5.c \
+libutil_la_SOURCES = \
+			ipa_hostname.c \
+			ipa_hostname.h \
+			ipa_krb5.c \
 			ipa_krb5.h \
 			ipa_mspac.h \
 			ipa_ldap.c \
 			ipa_ldap.h \
 			ipa_pwd.c \
 			ipa_pwd.h \
-			ipa_pwd_ntlm.c
+			ipa_pwd_ntlm.c \
+			$(NULL)
 
 libutil_la_LIBADD = $(CRYPTO_LIBS) $(KRB5_LIBS) $(LDAP_LIBS)
 
diff --git a/util/ipa_hostname.c b/util/ipa_hostname.c
new file mode 100644
index 0000000000..97fe54cc63
--- /dev/null
+++ b/util/ipa_hostname.c
@@ -0,0 +1,111 @@
+
+/*
+ * Copyright (C) 2020  FreeIPA Contributors see COPYING for license
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <netdb.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include "ipa_hostname.h"
+
+int
+ipa_gethostname(char *name)
+{
+    int ret;
+
+    ret = gethostname(name, IPA_HOST_NAME_LEN - 1);
+
+    /* Make double sure it is terminated */
+    name[IPA_HOST_NAME_LEN - 1] = '\0';
+
+    return ret;
+}
+
+static int
+_get_fqdn(char *fqdn)
+{
+    char hostname[IPA_HOST_NAME_LEN];
+    char *canonname = NULL;
+    struct addrinfo hints;
+    struct addrinfo *ai = NULL;
+    int r;
+
+    r = ipa_gethostname(hostname);
+    if (r != 0) {
+        goto error;
+    }
+
+    memset(&hints, 0, sizeof(struct addrinfo));
+    /* use IPv4 or IPv6 */
+    hints.ai_family = AF_UNSPEC;
+    /* optimize, RAW and STREAM return same kind of information */
+    hints.ai_socktype = SOCK_DGRAM;
+    /* any protocol */
+    hints.ai_protocol = 0;
+    /* get canonical name
+     * only use IPv4/6 when at least one interface for proto is configured */
+    hints.ai_flags = AI_CANONNAME | AI_ADDRCONFIG;
+
+    r = getaddrinfo(hostname, NULL, &hints, &ai);
+    if (r != 0) {
+        /* getaddrinfo() for gethostname() should never fail. The
+         * nss-myhostname provider should always add a positive match. */
+        errno = ENOENT;
+        goto error;
+    }
+
+    /* only the first addrinfo struct holds a canonical name value */
+    canonname = ai->ai_canonname;
+
+    /* check that canon name is filled and not too long */
+    if (!canonname) {
+        errno = ENOENT;
+        goto error;
+    }
+    if (strlen(canonname) >= IPA_HOST_NAME_LEN) {
+        errno = ENAMETOOLONG;
+        goto error;
+    }
+#if 0
+    /* refuse non-qualified short names and localhost */
+    if ((strchr(canonname, '.') == NULL) ||
+            (strcasecmp(canonname, "localhost.localdomain") == 0)) {
+        errno = EINVAL;
+        goto error;
+    }
+#endif
+
+    strcpy(fqdn, canonname);
+    /* Make double sure it is terminated */
+    fqdn[IPA_HOST_NAME_LEN - 1] = '\0';
+    freeaddrinfo(ai);
+    return 0;
+
+  error:
+    fqdn[0] = '\0';
+    if (ai != NULL) {
+        freeaddrinfo(ai);
+    }
+    return -1;
+}
+
+int ipa_gethostfqdn(char *name)
+{
+    static char cached_fqdn[IPA_HOST_NAME_LEN] = {0};
+
+    if (!cached_fqdn) {
+        int res = _get_fqdn(cached_fqdn);
+        if (res != 0) {
+            return -1;
+        }
+    }
+    strcpy(name, cached_fqdn);
+    return 0;
+}
diff --git a/util/ipa_hostname.h b/util/ipa_hostname.h
new file mode 100644
index 0000000000..75c10a5dc0
--- /dev/null
+++ b/util/ipa_hostname.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020  FreeIPA Contributors see COPYING for license
+ */
+
+#include <limits.h>
+#include <unistd.h>
+
+/*
+ * host name length including NULL byte
+ *
+ * NOTE: length hardcoded in kernel
+ */
+#define IPA_HOST_NAME_LEN (HOST_NAME_MAX + 1)
+
+int
+ipa_gethostname(char *name);
+
+int
+ipa_gethostfqdn(char *name);
