diff --git a/src/address.rs b/src/address.rs
index 5914231d7f4aa85011d86784d8dc5c75f387c5b1..e9b18ef8a89ce87f3e5722cfdc2f65cd5d5ddbcb 100644
--- a/src/address.rs
+++ b/src/address.rs
@@ -1,5 +1,9 @@
+use std::fmt;
+use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs};
+use std::num::ParseIntError;
 use std::str::FromStr;
 use tarantool::tlua;
+use thiserror::Error;
 
 pub const DEFAULT_USERNAME: &str = "guest";
 pub const DEFAULT_LISTEN_HOST: &str = "127.0.0.1";
@@ -7,27 +11,244 @@ pub const DEFAULT_HTTP_PORT: &str = "8080";
 pub const DEFAULT_IPROTO_PORT: &str = "3301";
 pub const DEFAULT_PGPROTO_PORT: &str = "4327";
 
-////////////////////
-// IPROTO ADDRESS //
-////////////////////
+pub type Port = u16;
 
-#[derive(Debug, Clone, PartialEq, Eq, tlua::Push, tlua::PushInto)]
-pub struct IprotoAddress {
-    pub user: Option<String>,
-    pub host: String,
-    pub port: String,
+#[derive(Debug, Clone, Eq, PartialEq, tlua::Push, tlua::PushInto)]
+pub struct TarantoolAddress<const DEFAULT_PORT: Port> {
+    host: Host,
+    port: Port,
+}
+
+impl<const PORT: Port> ToSocketAddrs for TarantoolAddress<PORT> {
+    type Iter = Box<dyn Iterator<Item = SocketAddr>>;
+
+    fn to_socket_addrs(&self) -> std::io::Result<Self::Iter> {
+        match &self.host {
+            Host::IpAddr(ip) => {
+                let addrs = (*ip, self.port).to_socket_addrs()?;
+                Ok(Box::new(addrs))
+            }
+            Host::HostName(host) => {
+                let addrs = (host.0.as_str(), self.port).to_socket_addrs()?;
+                Ok(Box::new(addrs))
+            }
+        }
+    }
 }
 
-impl Default for IprotoAddress {
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+enum Host {
+    IpAddr(IpAddr),
+    HostName(HostName),
+}
+
+impl<L> tlua::Push<L> for Host
+where
+    L: tlua::AsLua,
+{
+    type Err = tlua::Void;
+    fn push_to_lua(&self, lua: L) -> tlua::PushResult<L, Self> {
+        Ok(match self {
+            Self::IpAddr(ip) => lua.push(ip.to_string()),
+            Self::HostName(host) => lua.push(host),
+        })
+    }
+}
+
+impl<L> tlua::PushInto<L> for Host
+where
+    L: tlua::AsLua,
+{
+    type Err = tlua::Void;
+    fn push_into_lua(self, lua: L) -> tlua::PushResult<L, Self> {
+        Ok(match self {
+            Self::IpAddr(ip) => lua.push(ip.to_string()),
+            Self::HostName(host) => lua.push(host),
+        })
+    }
+}
+
+impl<L> tlua::PushOne<L> for Host where L: tlua::AsLua {}
+impl<L> tlua::PushOneInto<L> for Host where L: tlua::AsLua {}
+
+impl fmt::Display for Host {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Host::IpAddr(ip) => write!(f, "{ip}"),
+            Host::HostName(name) => write!(f, "{name}"),
+        }
+    }
+}
+
+impl<const PORT: Port> TarantoolAddress<PORT> {
+    fn to_host_port(&self) -> String {
+        self.to_string()
+    }
+}
+
+impl<const PORT: Port> Default for TarantoolAddress<PORT> {
     fn default() -> Self {
-        Self {
-            user: None,
-            host: DEFAULT_LISTEN_HOST.into(),
-            port: DEFAULT_IPROTO_PORT.into(),
+        TarantoolAddress {
+            host: Host::IpAddr(IpAddr::V4(Ipv4Addr::LOCALHOST)),
+            port: PORT,
+        }
+    }
+}
+
+impl<const PORT: Port> fmt::Display for TarantoolAddress<PORT> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}:{}", self.host, self.port)
+    }
+}
+
+/// Low-level address parsing error, which provides the exact cause of the error.
+#[derive(Debug, Error)]
+pub enum AddressError {
+    #[error(
+        "address part after the last `:` must contain a valid port number in decimal notation"
+    )]
+    Port(#[from] ParseIntError),
+    #[error(transparent)]
+    Host(#[from] HostError),
+}
+
+/// Wrapper type which provides a more user-friendly high-level error description.
+#[derive(Debug, Error)]
+#[error(
+    "expected socket address, IP address or domain name with optional port number `host[:port]`"
+)]
+pub struct SocketAddrError(#[from] AddressError);
+
+/// Wrapper type which provides a more user-friendly high-level error description for iproto
+/// address errors.
+#[derive(Debug, Error)]
+#[error(
+    "iproto address must consist of an optional username, followed by a domain \
+        name with optional port or a socket address: `[user@]host[:port]`"
+)]
+pub struct IprotoError(#[from] AddressError);
+
+impl<const PORT: Port> FromStr for TarantoolAddress<PORT> {
+    type Err = AddressError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        // Note that IPv6 socket addresses are more complex than just `Ip:Port`.
+        // Also note that Rust doesn't support parsing of non-numeric scope labels,
+        // e.g. `%eth2`. This is likely to avoid both allocations and OS dependency in
+        // socket address parsing. In the future we could need a different IPv6 parser.
+        if let Ok(addr) = s.parse::<SocketAddr>() {
+            return Ok(TarantoolAddress {
+                host: Host::IpAddr(addr.ip()),
+                port: addr.port(),
+            });
+        }
+
+        // Use the default port if only IP address is given.
+        if let Ok(addr) = s.parse::<IpAddr>() {
+            return Ok(TarantoolAddress {
+                host: Host::IpAddr(addr),
+                port: PORT,
+            });
+        }
+
+        // If the address doesn't parse as IP or socket address, assume that we have
+        // a domain name with optional port.
+        let (host, port) = if let Some((host, port)) = s.rsplit_once(':') {
+            let port: Port = port.parse()?;
+            (host, port)
+        } else {
+            (s, PORT)
+        };
+        let host_name: HostName = host.parse()?;
+
+        Ok(TarantoolAddress {
+            host: Host::HostName(host_name),
+            port,
+        })
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, tlua::Push, tlua::PushInto)]
+pub struct HostName(String);
+
+#[derive(Debug, Error)]
+pub enum HostError {
+    #[error("host name label cannot be empty")]
+    EmptyLabel,
+    #[error("host name labels cannot start or end with `-`")]
+    InvalidHyphen,
+    #[error("top level host name cannot be an integer")]
+    UnexpectedInteger,
+    #[error("host name cannot be empty")]
+    EmptyHost,
+    #[error("host names cannot contain {}", err_desc_for_byte(.0))]
+    InvalidSymbol(u8),
+}
+
+fn err_desc_for_byte(b: &u8) -> String {
+    if b.is_ascii_graphic() || *b == b' ' {
+        format!("character `{}`", char::from(*b))
+    } else {
+        format!("byte 0x{b:02X}")
+    }
+}
+
+impl FromStr for HostName {
+    type Err = HostError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        // IETF RFC 1035 requires domain names to consist of english letters, digits, `.` and `-`.
+        // It also places restriction on label name length, but we omit it, since it's less
+        // likely to be enforced or useful for error reporting.
+
+        let top_label = s.rsplit('.').next().ok_or(HostError::EmptyHost)?;
+        if top_label.bytes().all(|c| c.is_ascii_digit()) {
+            // Top-level host name cannot contain only digits.
+            // Internet host names must be alphabetic, but it's possible to have more complex
+            // site-specific top-level host names.
+            // Still, we forbid fully-numeric hosts, since those are likely to be an error on
+            // the part of the user (e.g. a user mistyped an IP address, or expected it to mean
+            // a port with default address, e.g. 8080 to bind to `127.0.0.1:8080`
+            return Err(HostError::UnexpectedInteger);
         }
+
+        for label in s.split('.') {
+            if label.is_empty() {
+                return Err(HostError::EmptyLabel);
+            }
+            for byte in label.bytes() {
+                // Underscore is discouraged by RFC 1035, but is allowed by certain systems,
+                // including Tarantool. We do not allow more general host name syntax
+                // (RFC 3986 allows various forms of punctuation).
+                if !(byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) {
+                    return Err(HostError::InvalidSymbol(byte));
+                }
+            }
+            if label.starts_with('-') || label.ends_with('-') {
+                return Err(HostError::InvalidHyphen);
+            }
+        }
+
+        Ok(HostName(s.to_owned()))
     }
 }
 
+impl fmt::Display for HostName {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+////////////////////
+// IPROTO ADDRESS //
+////////////////////
+
+#[derive(Debug, Clone, PartialEq, Eq, Default, tlua::Push, tlua::PushInto)]
+pub struct IprotoAddress {
+    pub user: Option<String>,
+    address: TarantoolAddress<3301>,
+}
+
 impl IprotoAddress {
     #[inline(always)]
     pub const fn default_host_port() -> &'static str {
@@ -38,57 +259,42 @@ impl IprotoAddress {
 
     #[inline(always)]
     pub fn to_host_port(&self) -> String {
-        format!("{}:{}", self.host, self.port)
+        self.address.to_host_port()
     }
 
-    fn parse_address(addr: &str) -> Result<Self, String> {
-        let format_err = || Err("valid format: [user@][host][:port]".to_string());
-        let (user, host_port) = match addr.rsplit_once('@') {
-            Some((user, host_port)) => {
-                if user.contains(':') || user.contains('@') {
-                    return format_err();
-                }
-                if user.is_empty() {
-                    return format_err();
-                }
-                (Some(user), host_port)
-            }
-            None => (None, addr),
-        };
-        let (host, port) = match host_port.rsplit_once(':') {
-            Some((host, port)) => {
-                if host.contains(':') || port.is_empty() {
-                    return format_err();
-                }
-                let host = if host.is_empty() { None } else { Some(host) };
-                (host, Some(port))
-            }
-            None => (Some(host_port), None),
-        };
-        Ok(Self {
-            user: user.map(Into::into),
-            host: host.unwrap_or(DEFAULT_LISTEN_HOST).into(),
-            port: port.unwrap_or(DEFAULT_IPROTO_PORT).into(),
-        })
+    pub fn port(&self) -> Port {
+        self.address.port
+    }
+
+    pub fn host(&self) -> String {
+        self.address.host.to_string()
     }
 }
 
-impl std::fmt::Display for IprotoAddress {
-    #[inline(always)]
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl fmt::Display for IprotoAddress {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         if let Some(user) = &self.user {
-            write!(f, "{user}@{}:{}", self.host, self.port)
-        } else {
-            write!(f, "{}:{}", self.host, self.port)
+            write!(f, "{user}@")?;
         }
+        write!(f, "{}", self.address)
     }
 }
 
 impl FromStr for IprotoAddress {
-    type Err = String;
-
-    fn from_str(addr: &str) -> Result<Self, Self::Err> {
-        Self::parse_address(addr)
+    type Err = IprotoError;
+
+    fn from_str(mut addr: &str) -> Result<Self, Self::Err> {
+        let mut user = None;
+        if let Some((u, a)) = addr.split_once('@') {
+            // TODO: Currently we don't enforce any restrictions on the user authority part
+            //      of the address. We probably should at least forbid whitespace, non-printable
+            //      characters, and more than one `:` (a single colon can separate username and
+            //      password).
+            user = Some(u.to_owned());
+            addr = a;
+        }
+        let address = addr.parse()?;
+        Ok(IprotoAddress { user, address })
     }
 }
 
@@ -117,19 +323,9 @@ impl<'de> serde::Deserialize<'de> for IprotoAddress {
 // HTTP ADDRESS //
 //////////////////
 
-#[derive(Debug, Clone, PartialEq, Eq, tlua::Push, tlua::PushInto)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, tlua::Push, tlua::PushInto)]
 pub struct HttpAddress {
-    pub host: String,
-    pub port: String,
-}
-
-impl Default for HttpAddress {
-    fn default() -> Self {
-        Self {
-            host: DEFAULT_LISTEN_HOST.into(),
-            port: DEFAULT_HTTP_PORT.into(),
-        }
-    }
+    address: TarantoolAddress<8080>,
 }
 
 impl HttpAddress {
@@ -142,40 +338,30 @@ impl HttpAddress {
 
     #[inline(always)]
     pub fn to_host_port(&self) -> String {
-        format!("{}:{}", self.host, self.port)
+        self.address.to_host_port()
     }
 
-    fn parse_address(addr: &str) -> Result<Self, String> {
-        let format_err = || Err("valid format: [host][:port]".to_string());
-        let (host, port) = match addr.rsplit_once(':') {
-            Some((host, port)) => {
-                if host.contains(':') || port.is_empty() {
-                    return format_err();
-                }
-                let host = if host.is_empty() { None } else { Some(host) };
-                (host, Some(port))
-            }
-            None => (Some(addr), None),
-        };
-        Ok(Self {
-            host: host.unwrap_or(DEFAULT_LISTEN_HOST).into(),
-            port: port.unwrap_or(DEFAULT_HTTP_PORT).into(),
-        })
+    pub fn port(&self) -> Port {
+        self.address.port
+    }
+
+    pub fn host(&self) -> String {
+        self.address.host.to_string()
     }
 }
 
-impl std::fmt::Display for HttpAddress {
-    #[inline(always)]
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}:{}", self.host, self.port)
+impl fmt::Display for HttpAddress {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.address)
     }
 }
 
 impl FromStr for HttpAddress {
-    type Err = String;
+    type Err = SocketAddrError;
 
     fn from_str(addr: &str) -> Result<Self, Self::Err> {
-        Self::parse_address(addr)
+        let address = addr.parse()?;
+        Ok(HttpAddress { address })
     }
 }
 
@@ -200,23 +386,21 @@ impl<'de> serde::Deserialize<'de> for HttpAddress {
     }
 }
 
+impl ToSocketAddrs for HttpAddress {
+    type Iter = <TarantoolAddress<8080> as ToSocketAddrs>::Iter;
+
+    fn to_socket_addrs(&self) -> std::io::Result<Self::Iter> {
+        self.address.to_socket_addrs()
+    }
+}
+
 /////////////////////
 // PGPROTO ADDRESS //
 /////////////////////
 
-#[derive(Debug, Clone, PartialEq, Eq, tlua::Push, tlua::PushInto)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, tlua::Push, tlua::PushInto)]
 pub struct PgprotoAddress {
-    pub host: String,
-    pub port: String,
-}
-
-impl Default for PgprotoAddress {
-    fn default() -> Self {
-        Self {
-            host: DEFAULT_LISTEN_HOST.into(),
-            port: DEFAULT_PGPROTO_PORT.into(),
-        }
-    }
+    address: TarantoolAddress<4327>,
 }
 
 impl PgprotoAddress {
@@ -229,40 +413,22 @@ impl PgprotoAddress {
 
     #[inline(always)]
     pub fn to_host_port(&self) -> String {
-        format!("{}:{}", self.host, self.port)
-    }
-
-    fn parse_address(addr: &str) -> Result<Self, String> {
-        let format_err = || Err("valid format: [host][:port]".to_string());
-        let (host, port) = match addr.rsplit_once(':') {
-            Some((host, port)) => {
-                if host.contains(':') || port.is_empty() {
-                    return format_err();
-                }
-                let host = if host.is_empty() { None } else { Some(host) };
-                (host, Some(port))
-            }
-            None => (Some(addr), None),
-        };
-        Ok(Self {
-            host: host.unwrap_or(DEFAULT_LISTEN_HOST).into(),
-            port: port.unwrap_or(DEFAULT_PGPROTO_PORT).into(),
-        })
+        self.address.to_host_port()
     }
 }
 
-impl std::fmt::Display for PgprotoAddress {
-    #[inline(always)]
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}:{}", self.host, self.port)
+impl fmt::Display for PgprotoAddress {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.address)
     }
 }
 
 impl FromStr for PgprotoAddress {
-    type Err = String;
+    type Err = SocketAddrError;
 
     fn from_str(addr: &str) -> Result<Self, Self::Err> {
-        Self::parse_address(addr)
+        let address = addr.parse()?;
+        Ok(PgprotoAddress { address })
     }
 }
 
@@ -287,6 +453,14 @@ impl<'de> serde::Deserialize<'de> for PgprotoAddress {
     }
 }
 
+impl ToSocketAddrs for PgprotoAddress {
+    type Iter = <TarantoolAddress<4327> as ToSocketAddrs>::Iter;
+
+    fn to_socket_addrs(&self) -> std::io::Result<Self::Iter> {
+        self.address.to_socket_addrs()
+    }
+}
+
 ///////////
 // TESTS //
 ///////////
@@ -294,134 +468,286 @@ impl<'de> serde::Deserialize<'de> for PgprotoAddress {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::{error::Error as StdError, iter};
+
+    const DOMAINS: &[&str] = &[
+        "localhost",
+        "ocean13",
+        "host",
+        "host.com",
+        "local.example.org",
+        "l-0ca1.exam-p1e.0-rg",
+    ];
+
+    const IPV4_ADDRESSES: &[&str] = &[
+        "127.0.0.1",
+        "10.0.192.11",
+        "234.110.5.8",
+        "0.0.0.0",
+        "255.255.255.255",
+    ];
+
+    const IPV6_ADDRESSES: &[&str] = &[
+        "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+        "2001:0DB8:85A3:0000:0000:8A2E:0370:7334",
+        "2001:db8::1",
+        "2001:db8::1:0",
+        "2001:db8::1:0:0:1",
+        "2001:db8:0:0:0:0:2:1",
+        "2001:db8::2:1",
+        "2001:db8:0:1:1:1:1:1",
+        "::1",
+        "::",
+        "::ffff:192.0.2.128",
+        "64:ff9b::",
+        "5f00::",
+    ];
+
+    fn parse_host_generic<T>(addrs: &[&str])
+    where
+        T: FromStr,
+        <T as FromStr>::Err: fmt::Display,
+    {
+        for addr in addrs {
+            addr.parse::<T>().unwrap_or_else(|err| panic!("{err}"));
+        }
+    }
+
+    trait WithPort {
+        fn port(&self) -> Port;
+    }
+
+    impl<const PORT: Port> WithPort for TarantoolAddress<PORT> {
+        fn port(&self) -> Port {
+            self.port
+        }
+    }
+
+    impl WithPort for IprotoAddress {
+        fn port(&self) -> Port {
+            self.address.port
+        }
+    }
+
+    impl WithPort for PgprotoAddress {
+        fn port(&self) -> Port {
+            self.address.port
+        }
+    }
+
+    impl WithPort for HttpAddress {
+        fn port(&self) -> Port {
+            self.address.port
+        }
+    }
+
+    fn parse_socket_generic<T>()
+    where
+        T: FromStr + WithPort,
+        <T as FromStr>::Err: fmt::Display,
+    {
+        let port: Port = 0xABBA;
+
+        for addr in IPV4_ADDRESSES.iter().chain(DOMAINS) {
+            let addr_with_port = format!("{addr}:{port}");
+            match addr_with_port.parse::<T>() {
+                Ok(address) => {
+                    assert_eq!(address.port(), port);
+                }
+                Err(err) => {
+                    panic!("error while parsing `{addr_with_port}`: {err}");
+                }
+            }
+        }
+    }
+
+    fn parse_socket_ipv6_generic<T>()
+    where
+        T: FromStr + WithPort,
+        <T as FromStr>::Err: fmt::Display,
+    {
+        let port: Port = 0xABBA;
+        // Only numeric scopes are supported.
+        let zones = ["", "%3", "%282828"];
+
+        for addr in IPV6_ADDRESSES {
+            for zone in zones {
+                let addr_with_port = format!("[{addr}{zone}]:{port}");
+                match addr_with_port.parse::<T>() {
+                    Ok(address) => {
+                        assert_eq!(address.port(), port);
+                    }
+                    Err(err) => {
+                        panic!("error while parsing `{addr_with_port}`: {err}");
+                    }
+                }
+            }
+        }
+    }
+
+    mod base {
+        use super::*;
+
+        type Addr = TarantoolAddress<0xBEEF>;
+
+        #[test]
+        fn parse_ipv4() {
+            parse_host_generic::<Addr>(IPV4_ADDRESSES);
+        }
+
+        #[test]
+        fn parse_ipv6() {
+            parse_host_generic::<Addr>(IPV6_ADDRESSES);
+        }
+
+        #[test]
+        fn parse_domains() {
+            parse_host_generic::<Addr>(DOMAINS);
+        }
+
+        #[test]
+        fn parse_socket() {
+            parse_socket_generic::<Addr>();
+        }
+
+        #[test]
+        fn parse_socket_ipv6() {
+            parse_socket_ipv6_generic::<Addr>();
+        }
+
+        #[test]
+        fn non_normalized_ipv4() {}
+    }
+
+    // #[test]
+    // fn try_parse_address() {
+    //     //
+    //     // check parsing of correct addresses
+    //     //
+    //
+    //     assert_eq!(
+    //         "example".parse(),
+    //         Ok(IprotoAddress {
+    //             user: None,
+    //             host: "example".into(),
+    //             port: DEFAULT_IPROTO_PORT.into()
+    //         })
+    //     );
+    //     assert_eq!(
+    //         "127.0.0.1:1234".parse(),
+    //         Ok(IprotoAddress {
+    //             user: None,
+    //             host: "127.0.0.1".into(),
+    //             port: "1234".into()
+    //         })
+    //     );
+    //     assert_eq!(
+    //         "1.2.3.4:1234".parse(),
+    //         Ok(IprotoAddress {
+    //             user: None,
+    //             host: "1.2.3.4".into(),
+    //             port: "1234".into()
+    //         })
+    //     );
+    //     assert_eq!(
+    //         "example:1234".parse(),
+    //         Ok(IprotoAddress {
+    //             user: None,
+    //             host: "example".into(),
+    //             port: "1234".into()
+    //         })
+    //     );
+    //
+    //     assert_eq!(
+    //         "user@host:port".parse(),
+    //         Ok(IprotoAddress {
+    //             user: Some("user".into()),
+    //             host: "host".into(),
+    //             port: "port".into()
+    //         })
+    //     );
+    //
+    //     assert_eq!(
+    //         "user@:port".parse(),
+    //         Ok(IprotoAddress {
+    //             user: Some("user".into()),
+    //             host: "127.0.0.1".into(),
+    //             port: "port".into()
+    //         })
+    //     );
+    //
+    //     assert_eq!(
+    //         "user@host".parse(),
+    //         Ok(IprotoAddress {
+    //             user: Some("user".into()),
+    //             host: "host".into(),
+    //             port: DEFAULT_IPROTO_PORT.into()
+    //         })
+    //     );
+    //
+    //     //
+    //     // check parsing of incorrect addresses
+    //     //
+    //
+    //     assert!("example::1234".parse::<IprotoAddress>().is_err());
+    //     assert!("user@@example".parse::<IprotoAddress>().is_err());
+    //     assert!("user:pass@host:port".parse::<IprotoAddress>().is_err());
+    //     assert!("a:b@c".parse::<IprotoAddress>().is_err());
+    //     assert!("user:pass@host".parse::<IprotoAddress>().is_err());
+    //     assert!("@host".parse::<IprotoAddress>().is_err());
+    //     assert!("host:".parse::<IprotoAddress>().is_err());
+    // }
+
+    fn err_for<T: StdError + 'static, R>(host: &str) -> impl '_ + FnOnce(T) -> R {
+        move |err| {
+            eprintln!("{err}");
+            let mut err: &dyn StdError = &err;
+            while let Some(e) = err.source() {
+                eprintln!("\tcaused by: {e}");
+                err = e;
+            }
+            panic!("error while parsing {host}");
+        }
+    }
 
     #[test]
-    fn try_parse_address() {
-        //
-        // check parsing of correct addresses
-        //
-
-        assert_eq!(
-            "1234".parse(),
-            Ok(IprotoAddress {
-                user: None,
-                host: "1234".into(),
-                port: "3301".into()
-            })
-        );
-        assert_eq!(
-            ":1234".parse(),
-            Ok(IprotoAddress {
-                user: None,
-                host: "127.0.0.1".into(),
-                port: "1234".into()
-            })
-        );
-        assert_eq!(
-            "example".parse(),
-            Ok(IprotoAddress {
-                user: None,
-                host: "example".into(),
-                port: "3301".into()
-            })
-        );
-        assert_eq!(
-            "127.0.0.1:1234".parse(),
-            Ok(IprotoAddress {
-                user: None,
-                host: "127.0.0.1".into(),
-                port: "1234".into()
-            })
-        );
-        assert_eq!(
-            "1.2.3.4:1234".parse(),
-            Ok(IprotoAddress {
-                user: None,
-                host: "1.2.3.4".into(),
-                port: "1234".into()
-            })
-        );
-        assert_eq!(
-            "example:1234".parse(),
-            Ok(IprotoAddress {
-                user: None,
-                host: "example".into(),
-                port: "1234".into()
-            })
-        );
-
-        assert_eq!(
-            "user@host:port".parse(),
-            Ok(IprotoAddress {
-                user: Some("user".into()),
-                host: "host".into(),
-                port: "port".into()
-            })
-        );
-
-        assert_eq!(
-            "user@:port".parse(),
-            Ok(IprotoAddress {
-                user: Some("user".into()),
-                host: "127.0.0.1".into(),
-                port: "port".into()
-            })
-        );
-
-        assert_eq!(
-            "user@host".parse(),
-            Ok(IprotoAddress {
-                user: Some("user".into()),
-                host: "host".into(),
-                port: "3301".into()
-            })
-        );
-
-        //
-        // check parsing of incorrect addresses
-        //
-
-        assert!("example::1234".parse::<IprotoAddress>().is_err());
-        assert!("user@@example".parse::<IprotoAddress>().is_err());
-        assert!("user:pass@host:port".parse::<IprotoAddress>().is_err());
-        assert!("a:b@c".parse::<IprotoAddress>().is_err());
-        assert!("user:pass@host".parse::<IprotoAddress>().is_err());
-        assert!("@host".parse::<IprotoAddress>().is_err());
-        assert!("host:".parse::<IprotoAddress>().is_err());
-
-        //
-        // check conflicting ports in address parsing
-        //
-
-        let iproto_conflict_with_http = format!("host:{DEFAULT_HTTP_PORT}");
-        assert!(iproto_conflict_with_http.parse::<IprotoAddress>().is_ok(),);
-        let iproto_conflict_with_pg = format!("host:{DEFAULT_PGPROTO_PORT}");
-        assert!(iproto_conflict_with_pg.parse::<IprotoAddress>().is_ok());
-
-        let http_conflict_with_iproto = format!("host:{DEFAULT_IPROTO_PORT}");
-        assert!(http_conflict_with_iproto.parse::<HttpAddress>().is_ok(),);
-        let http_conflict_with_pg = format!("host:{DEFAULT_PGPROTO_PORT}");
-        assert!(http_conflict_with_pg.parse::<HttpAddress>().is_ok(),);
-
-        let pg_conflict_with_iproto = format!("host:{DEFAULT_IPROTO_PORT}");
-        assert!(pg_conflict_with_iproto.parse::<PgprotoAddress>().is_ok(),);
-        let pg_conflict_with_http = format!("host:{DEFAULT_HTTP_PORT}");
-        assert!(pg_conflict_with_http.parse::<PgprotoAddress>().is_ok(),);
-
-        //
-        // check correctness of default values to avoid human factor
-        //
-
-        let default_iproto_addr = DEFAULT_LISTEN_HOST.parse::<IprotoAddress>().unwrap();
-        assert!(default_iproto_addr.host == DEFAULT_LISTEN_HOST);
-        assert!(default_iproto_addr.port == DEFAULT_IPROTO_PORT);
-
-        let default_http_addr = DEFAULT_LISTEN_HOST.parse::<HttpAddress>().unwrap();
-        assert!(default_http_addr.host == DEFAULT_LISTEN_HOST);
-        assert!(default_http_addr.port == DEFAULT_HTTP_PORT);
-
-        let default_pgproto_addr = DEFAULT_LISTEN_HOST.parse::<PgprotoAddress>().unwrap();
-        assert!(default_pgproto_addr.host == DEFAULT_LISTEN_HOST);
-        assert!(default_pgproto_addr.port == DEFAULT_PGPROTO_PORT);
+    fn default_ports() {
+        // Check that default ports match the relevant string constants.
+        for host in DOMAINS.iter().chain(IPV4_ADDRESSES).chain(IPV6_ADDRESSES) {
+            assert_eq!(
+                host.parse::<IprotoAddress>()
+                    .unwrap_or_else(err_for(host))
+                    .port()
+                    .to_string(),
+                DEFAULT_IPROTO_PORT
+            );
+            assert_eq!(
+                host.parse::<PgprotoAddress>()
+                    .unwrap_or_else(err_for(host))
+                    .port()
+                    .to_string(),
+                DEFAULT_PGPROTO_PORT
+            );
+            assert_eq!(
+                host.parse::<HttpAddress>()
+                    .unwrap_or_else(err_for(host))
+                    .port()
+                    .to_string(),
+                DEFAULT_HTTP_PORT
+            );
+        }
+    }
+
+    #[test]
+    fn conflicting_ports_allowed() {
+        // Check that default ports are allowed for differing protocols.
+        let ports = [DEFAULT_IPROTO_PORT, DEFAULT_HTTP_PORT, DEFAULT_PGPROTO_PORT];
+
+        for port in ports {
+            let addr = format!("host:{port}");
+            assert!(addr.parse::<IprotoAddress>().is_ok());
+            assert!(addr.parse::<PgprotoAddress>().is_ok());
+            assert!(addr.parse::<HttpAddress>().is_ok());
+        }
     }
 }
diff --git a/src/cli/connect.rs b/src/cli/connect.rs
index b34ead538617abf2ed4761937e35084cd9a31460..3d8f8ecb68c3090c6c4a6c0f6996f36c622a87ef 100644
--- a/src/cli/connect.rs
+++ b/src/cli/connect.rs
@@ -186,18 +186,11 @@ pub fn determine_credentials_and_connect(
     config.auth_method = auth_method;
     config.connect_timeout = Some(timeout);
 
-    let port = match address.port.parse::<u16>() {
-        Ok(port) => port,
-        Err(err) => {
-            return Err(Error::other(ReplError::Other(format!(
-                "Error while parsing instance port '{}': {err}",
-                address.port
-            ))))
-        }
-    };
-
-    let client =
-        ::tarantool::fiber::block_on(Client::connect_with_config(&address.host, port, config))?;
+    let client = ::tarantool::fiber::block_on(Client::connect_with_config(
+        &address.host(),
+        address.port(),
+        config,
+    ))?;
 
     Ok((client, user.into()))
 }
@@ -224,8 +217,8 @@ fn sql_repl(args: args::Connect) -> Result<(), ReplError> {
     let mut console = Console::new()?;
 
     console.greet(&format!(
-        "Connected to interactive console by address \"{}:{}\" under \"{}\" user",
-        args.address.host, args.address.port, user
+        "Connected to interactive console by address \"{}\" under \"{}\" user",
+        args.address, user
     ));
 
     const HELP_MESSAGE: &'static str = "
diff --git a/src/config.rs b/src/config.rs
index 4d634b84ed1a46110f330f733cbeb47f1ce2d38d..dd7760076c3369a702de7ec870dc01e60bd4d460 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1965,11 +1965,22 @@ macro_rules! config_parameter_path {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::address::PgprotoAddress;
-    use crate::util::{on_scope_exit, ScopeGuard};
+    use crate::address::{PgprotoAddress, DEFAULT_HTTP_PORT};
+    use crate::util::{on_scope_exit, unwrap_error_trace, ScopeGuard};
     use clap::Parser as _;
     use pretty_assertions::assert_eq;
-    use std::{str::FromStr, sync::Mutex};
+    use std::{iter, str::FromStr, sync::Mutex};
+
+    const IPROTO_PORT: u16 = 3301;
+    const HTTP_PORT: u16 = 8080;
+    const PGPROTO_PORT: u16 = 4327;
+
+    #[test]
+    fn default_constants_match() {
+        assert_eq!(IPROTO_PORT.to_string(), DEFAULT_IPROTO_PORT);
+        assert_eq!(HTTP_PORT.to_string(), DEFAULT_HTTP_PORT);
+        assert_eq!(PGPROTO_PORT.to_string(), DEFAULT_PGPROTO_PORT);
+    }
 
     #[test]
     fn config_from_yaml() {
@@ -2112,21 +2123,21 @@ instance:
 
         let yaml = r###"
 instance:
-    iproto_listen:  kevin->  :spacey   # <- some more trailing space
+    iproto_listen:  kevin-spacey:3421   # <- some more trailing space
 "###;
         let config = PicodataConfig::read_yaml_contents(&yaml.trim_start()).unwrap();
         let listen = config.instance.iproto_listen.unwrap();
-        assert_eq!(listen.host, "kevin->  ");
-        assert_eq!(listen.port, "spacey");
+        assert_eq!(listen.host(), "kevin-spacey");
+        assert_eq!(listen.port(), 3421);
 
         let yaml = r###"
 instance:
-    iproto_listen:  kevin->  <-spacey
+    iproto_listen:    kevin-spacey
 "###;
         let config = PicodataConfig::read_yaml_contents(&yaml.trim_start()).unwrap();
         let listen = config.instance.iproto_listen.unwrap();
-        assert_eq!(listen.host, "kevin->  <-spacey");
-        assert_eq!(listen.port, "3301");
+        assert_eq!(listen.host(), "kevin-spacey");
+        assert_eq!(listen.port(), IPROTO_PORT);
     }
 
     #[test]
@@ -2137,18 +2148,20 @@ instance:
 "###;
         let config = PicodataConfig::read_yaml_contents(&yaml.trim_start()).unwrap();
         let listen = config.instance.http_listen.unwrap();
-        assert_eq!(listen.host, "127.0.0.1");
-        assert_eq!(listen.port, "8080");
+        assert_eq!(listen.host(), "127.0.0.1");
+        assert_eq!(listen.port(), HTTP_PORT);
     }
 
-    #[track_caller]
-    fn setup_for_tests(yaml: Option<&str>, args: &[&str]) -> Result<Box<PicodataConfig>, Error> {
+    fn setup_for_tests_inner(
+        yaml: Option<&str>,
+        args: &[&str],
+    ) -> Result<Box<PicodataConfig>, Error> {
         let mut config = if let Some(yaml) = yaml {
-            PicodataConfig::read_yaml_contents(yaml).unwrap()
+            PicodataConfig::read_yaml_contents(yaml)?
         } else {
             Default::default()
         };
-        let args = args::Run::try_parse_from(args).unwrap();
+        let args = args::Run::try_parse_from(args).map_err(Error::other)?;
         let mut parameter_sources = Default::default();
         mark_non_none_field_sources(&mut parameter_sources, &config, ParameterSource::ConfigFile);
         config.set_from_args_and_env(args, &mut parameter_sources)?;
@@ -2158,6 +2171,16 @@ instance:
         Ok(config)
     }
 
+    #[track_caller]
+    fn setup_for_tests<'a>(yaml: impl Into<Option<&'a str>>, args: &[&str]) -> Box<PicodataConfig> {
+        setup_for_tests_inner(yaml.into(), args).unwrap_or_else(unwrap_error_trace)
+    }
+
+    #[track_caller]
+    fn setup_for_tests_invalid<'a>(yaml: impl Into<Option<&'a str>>, args: &[&str]) -> Error {
+        setup_for_tests_inner(yaml.into(), args).unwrap_err()
+    }
+
     // This is a helper for tests that modify environment variables to test how
     // we're parsing them. But rust unit-tests are ran in parallel, so we
     // must be careful: we can't allow other test which also read env to
@@ -2167,7 +2190,13 @@ instance:
     #[must_use]
     fn protect_env() -> ScopeGuard<impl FnOnce() -> ()> {
         static ENV_LOCK: Mutex<()> = Mutex::new(());
-        let locked = ENV_LOCK.lock().unwrap();
+        let locked = ENV_LOCK.lock().unwrap_or_else(|err| {
+            // We use the lock only to modify environment variables in tests.
+            // We restore the variables to their initial state regardless of panic,
+            // so the environment is never inconsistent.
+            ENV_LOCK.clear_poison();
+            err.into_inner()
+        });
 
         // Save environment before test, to avoid breaking something unrelated
         let env_before: Vec<_> = std::env::vars()
@@ -2186,196 +2215,274 @@ instance:
         })
     }
 
-    #[rustfmt::skip]
     #[test]
-    fn parameter_source_precedence() {
+    fn config_default() {
         let _guard = protect_env();
 
-        //
-        // Defaults
-        //
-        {
-            let config = setup_for_tests(None, &["run"]).unwrap();
+        // Sanity check config defaults.
+        let config = setup_for_tests(None, &["run"]);
 
-            assert_eq!(
-                config.instance.peers(),
-                vec![IprotoAddress::default()]
-            );
-            assert_eq!(config.instance.name(), None);
-            assert_eq!(config.instance.iproto_listen().to_host_port(), IprotoAddress::default_host_port());
-            assert_eq!(config.instance.iproto_advertise().to_host_port(), IprotoAddress::default_host_port());
-            assert_eq!(config.instance.log_level(), SayLevel::Info);
-            assert!(config.instance.failure_domain().data.is_empty());
-        }
+        assert_eq!(config.instance.peers(), vec![IprotoAddress::default()]);
+        assert_eq!(config.instance.name(), None);
+        assert_eq!(
+            config.instance.iproto_listen().to_host_port(),
+            IprotoAddress::default_host_port()
+        );
+        assert_eq!(
+            config.instance.iproto_advertise().to_host_port(),
+            IprotoAddress::default_host_port()
+        );
+        assert_eq!(config.instance.log_level(), SayLevel::Info);
+        assert!(config.instance.failure_domain().data.is_empty());
+    }
 
-        //
+    #[test]
+    fn precedence_instance_name() {
+        let _guard = protect_env();
         // Precedence: command line > env > config
-        //
-        {
-            let yaml = r###"
+
+        let yaml = r###"
 instance:
     name: I-CONFIG
 "###;
 
-            // only config
-            let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+        // only config
+        let config = setup_for_tests(yaml, &["run"]);
 
-            assert_eq!(config.instance.name().unwrap(), "I-CONFIG");
+        assert_eq!(config.instance.name().unwrap(), "I-CONFIG");
+
+        // PICODATA_CONFIG_PARAMETERS > config
+        std::env::set_var(
+            "PICODATA_CONFIG_PARAMETERS",
+            "instance.name=I-ENV-CONF-PARAM",
+        );
+        let config = setup_for_tests(yaml, &["run"]);
 
-            // PICODATA_CONFIG_PARAMETERS > config
-            std::env::set_var("PICODATA_CONFIG_PARAMETERS", "instance.name=I-ENV-CONF-PARAM");
-            let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+        assert_eq!(config.instance.name().unwrap(), "I-ENV-CONF-PARAM");
 
-            assert_eq!(config.instance.name().unwrap(), "I-ENV-CONF-PARAM");
+        // other env > PICODATA_CONFIG_PARAMETERS
+        std::env::set_var("PICODATA_INSTANCE_NAME", "I-ENVIRON");
+        let config = setup_for_tests(yaml, &["run"]);
 
-            // other env > PICODATA_CONFIG_PARAMETERS
-            std::env::set_var("PICODATA_INSTANCE_NAME", "I-ENVIRON");
-            let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+        assert_eq!(config.instance.name().unwrap(), "I-ENVIRON");
 
-            assert_eq!(config.instance.name().unwrap(), "I-ENVIRON");
+        // command line > env
+        let config = setup_for_tests(yaml, &["run", "--instance-name=I-COMMANDLINE"]);
 
-            // command line > env
-            let config = setup_for_tests(Some(yaml), &["run", "--instance-name=I-COMMANDLINE"]).unwrap();
+        assert_eq!(config.instance.name().unwrap(), "I-COMMANDLINE");
 
-            assert_eq!(config.instance.name().unwrap(), "I-COMMANDLINE");
+        // -c PARAMETER=VALUE > other command line
+        let config = setup_for_tests(
+            yaml,
+            &[
+                "run",
+                "-c",
+                "instance.name=I-CLI-CONF-PARAM",
+                "--instance-name=I-COMMANDLINE",
+            ],
+        );
 
-            // -c PARAMETER=VALUE > other command line
-            let config = setup_for_tests(Some(yaml), &["run", "-c", "instance.name=I-CLI-CONF-PARAM", "--instance-name=I-COMMANDLINE"]).unwrap();
+        assert_eq!(config.instance.name().unwrap(), "I-CLI-CONF-PARAM");
+    }
 
-            assert_eq!(config.instance.name().unwrap(), "I-CLI-CONF-PARAM");
+    fn check_peers(config: &PicodataConfig, peers: &[(Option<&'static str>, &'static str, u16)]) {
+        let config_peers = config
+            .instance
+            .peer
+            .as_ref()
+            .expect("test configs expected to set `peer`");
+        for (config_peer, peer) in iter::zip(config_peers, peers) {
+            let host = config_peer.host();
+            let port = config_peer.port();
+            let user = config_peer.user.as_deref();
+            assert_eq!((user, host.as_str(), port), *peer);
         }
+    }
 
-        //
-        // peer parsing
-        //
-        {
-            // empty config means default
-            let yaml = r###"
+    #[test]
+    fn spaces_dont_parse_1() {
+        let _guard = protect_env();
+
+        setup_for_tests_invalid(
+            r###"
 instance:
-    peer:
-"###;
-            let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+    iproto_listen:  kevin  spacey:13
+"###,
+            &["run"],
+        );
 
-            assert_eq!(
-                config.instance.peers(),
-                vec![IprotoAddress::default()]
-            );
+        setup_for_tests_invalid(
+            r###"
+instance:
+    iproto_listen:  kevin-spacey :13
+"###,
+            &["run"],
+        );
 
-            // only config
-            let yaml = r###"
+        setup_for_tests_invalid(
+            r###"
 instance:
-    peer:
-        - bobbert:420
-        - tomathan:69
-"###;
-            let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+    iproto_listen:  kevin-spacey: 13
+"###,
+            &["run"],
+        );
 
-            assert_eq!(
-                config.instance.peers(),
-                vec![
-                    IprotoAddress {
-                        user: None,
-                        host: "bobbert".into(),
-                        port: "420".into(),
-                    },
-                    IprotoAddress {
-                        user: None,
-                        host: "tomathan".into(),
-                        port: "69".into(),
-                    }
-                ]
-            );
+        setup_for_tests_invalid(
+            r###"
+instance:
+    iproto_listen:          kevin-spacey: 13
+"###,
+            &["run"],
+        );
+
+        setup_for_tests_invalid(
+            r###"
+instance:
+    iproto_listen:  kevin-spacey: 13   # <- spaces at the end of line
+"###,
+            &["run"],
+        );
+    }
 
-            // env > config
-            std::env::set_var("PICODATA_PEER", "oops there's a space over here -> <-:13,             maybe we should at least strip these:37");
-            let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+    #[test]
+    fn spaces_dont_parse_2() {
+        let _guard = protect_env();
 
-            assert_eq!(
-                config.instance.peers(),
-                vec![
-                    IprotoAddress {
-                        user: None,
-                        host: "oops there's a space over here -> <-".into(),
-                        port: "13".into(),
-                    },
-                    IprotoAddress {
-                        user: None,
-                        host: "             maybe we should at least strip these".into(),
-                        port: "37".into(),
-                    }
-                ]
-            );
+        std::env::set_var("PICODATA_PEER", "kevin spacey:13");
+        setup_for_tests_invalid("", &["run"]);
 
-            // command line > env
-            let config = setup_for_tests(Some(yaml), &["run",
-                "--peer", "one:1",
-                "--peer", "two:2,    <- same problem here,:3,4"
-            ]).unwrap();
+        std::env::set_var("PICODATA_PEER", "looney:13,       toons:37");
+        setup_for_tests_invalid("", &["run"]);
+    }
 
-            assert_eq!(
-                config.instance.peers(),
-                vec![
-                    IprotoAddress {
-                        user: None,
-                        host: "one".into(),
-                        port: "1".into(),
-                    },
-                    IprotoAddress {
-                        user: None,
-                        host: "two".into(),
-                        port: "2".into(),
-                    },
-                    IprotoAddress {
-                        user: None,
-                        host: "    <- same problem here".into(),
-                        port: "3301".into(),
-                    },
-                    IprotoAddress {
-                        user: None,
-                        host: "127.0.0.1".into(),
-                        port: "3".into(),
-                    },
-                    IprotoAddress {
-                        user: None,
-                        host: "4".into(),
-                        port: "3301".into(),
-                    }
-                ]
-            );
+    #[test]
+    fn spaces_dont_parse_3() {
+        let _guard = protect_env();
 
-            // --config-parameter > --peer
-            let config = setup_for_tests(Some(yaml), &["run",
-                "-c", "instance.peer=[  host:123  , ghost :321, гост:666]",
-            ]).unwrap();
+        setup_for_tests_invalid(
+            "",
+            &[
+                "run",
+                "--peer",
+                "one:1",
+                "--peer",
+                "two:2,    <- same problem here,:3,4",
+            ],
+        );
 
-            assert_eq!(
-                config.instance.peers(),
-                vec![
-                    IprotoAddress {
-                        user: None,
-                        host: "host".into(),
-                        port: "123".into(),
-                    },
-                    IprotoAddress {
-                        user: None,
-                        host: "ghost ".into(),
-                        port: "321".into(),
-                    },
-                    IprotoAddress {
-                        user: None,
-                        host: "гост".into(),
-                        port: "666".into(),
-                    },
-                ]
+        setup_for_tests_invalid(
+            "",
+            &["run", "-c", "instance.peer=[  host:123  , ghost :321]"],
+        );
+    }
+
+    #[test]
+    fn non_ascii_hosts_dont_parse() {
+        let _guard = protect_env();
+
+        let invalid_hosts = &[
+            "bob#bert:420",
+            "гост:666",
+            "naïve",
+            "i❤️NY",
+            "-woosh",
+            "woosh-",
+        ];
+
+        for host in invalid_hosts {
+            let invalid_yaml = format!(
+                r#"
+instance:
+    peer: {host}
+        "#
+            );
+            setup_for_tests_invalid(invalid_yaml.as_str(), &["run"]);
+        }
+
+        for host in invalid_hosts {
+            setup_for_tests_invalid(
+                "",
+                &["run", "-c", format!("instance.peer=[{host}]").as_str()],
             );
         }
 
+        for host in invalid_hosts {
+            std::env::set_var("PICODATA_PEER", host);
+            setup_for_tests_invalid("", &["run"]);
+        }
+    }
+
+    #[rustfmt::skip]
+    #[test]
+    fn peer_parsing() {
+        let _guard = protect_env();
+
+        // empty config means default
+        let yaml = r###"
+instance:
+    peer:
+"###;
+        let config = setup_for_tests(Some(yaml), &["run"]);
+        assert_eq!(&config.instance.peers(), &[IprotoAddress::default()]);
+
+        // only config
+        let yaml = r###"
+instance:
+    peer:
+        - bobbert:420
+        - josh@tomathan:69
+        - tom.ath.an:65535
+        - ma-ce.windows64:1331
+"###;
+        let config = setup_for_tests(yaml, &["run"]);
+        check_peers(&config, &[
+            (None, "bobbert", 420),
+            (Some("josh"), "tomathan", 69),
+            (None, "tom.ath.an", u16::MAX),
+            (None, "ma-ce.windows64", 1331),
+        ]);
+
+        // env > config
+        std::env::set_var("PICODATA_PEER", "catbert:13,dilbe.rt:37,boss@localhost");
+        let config = setup_for_tests(yaml, &["run"]);
+        check_peers(&config, &[
+            (None, "catbert", 13),
+            (None, "dilbe.rt", 37),
+            (Some("boss"), "localhost", IPROTO_PORT),
+        ]);
+
+        // command line > env
+        let config = setup_for_tests(yaml, &["run",
+            "--peer", "one:1",
+            "--peer", "two:2,here"
+        ]);
+        check_peers(&config, &[
+            (None, "one", 1),
+            (None, "two", 2),
+            (None, "here", IPROTO_PORT),
+        ]);
+
+        // --config-parameter > --peer
+        let config = setup_for_tests(yaml, &["run",
+            "-c", "instance.peer=[  ost:123,syd  ]",
+        ]);
+        check_peers(&config, &[
+            (None, "ost", 123),
+            (None, "syd", IPROTO_PORT),
+        ]);
+    }
+
+    #[rustfmt::skip]
+    #[test]
+    fn parameter_source_precedence() {
+        let _guard = protect_env();
+
         //
         // Advertise = listen unless specified explicitly
         //
             {
             std::env::set_var("PICODATA_LISTEN", "L-ENVIRON");
-            let config = setup_for_tests(Some(""), &["run"]).unwrap();
+            let config = setup_for_tests(Some(""), &["run"]);
 
             assert_eq!(config.instance.iproto_listen().to_host_port(), "L-ENVIRON:3301");
             assert_eq!(config.instance.iproto_advertise().to_host_port(), "L-ENVIRON:3301");
@@ -2384,12 +2491,12 @@ instance:
 instance:
     iproto_advertise: A-CONFIG
 "###;
-            let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+            let config = setup_for_tests(Some(yaml), &["run"]);
 
             assert_eq!(config.instance.iproto_listen().to_host_port(), "L-ENVIRON:3301");
             assert_eq!(config.instance.iproto_advertise().to_host_port(), "A-CONFIG:3301");
 
-            let config = setup_for_tests(Some(yaml), &["run", "-l", "L-COMMANDLINE"]).unwrap();
+            let config = setup_for_tests(Some(yaml), &["run", "-l", "L-COMMANDLINE"]);
 
             assert_eq!(config.instance.iproto_listen().to_host_port(), "L-COMMANDLINE:3301");
             assert_eq!(config.instance.iproto_advertise().to_host_port(), "A-CONFIG:3301");
@@ -2408,7 +2515,7 @@ instance:
         kconf2: vconf2
         kconf2: vconf2-replaced
 "###;
-            let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+            let config = setup_for_tests(Some(yaml), &["run"]);
             assert_eq!(
                 config.instance.failure_domain(),
                 FailureDomain::from([("KCONF1", "VCONF1"), ("KCONF2", "VCONF2-REPLACED")])
@@ -2416,14 +2523,14 @@ instance:
 
             // environment
             std::env::set_var("PICODATA_FAILURE_DOMAIN", "kenv1=venv1,kenv2=venv2,kenv2=venv2-replaced");
-            let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+            let config = setup_for_tests(Some(yaml), &["run"]);
             assert_eq!(
                 config.instance.failure_domain(),
                 FailureDomain::from([("KENV1", "VENV1"), ("KENV2", "VENV2-REPLACED")])
             );
 
             // command line
-            let config = setup_for_tests(Some(yaml), &["run", "--failure-domain", "karg1=varg1,karg1=varg1-replaced"]).unwrap();
+            let config = setup_for_tests(Some(yaml), &["run", "--failure-domain", "karg1=varg1,karg1=varg1-replaced"]);
             assert_eq!(
                 config.instance.failure_domain(),
                 FailureDomain::from([("KARG1", "VARG1-REPLACED")])
@@ -2433,7 +2540,7 @@ instance:
             let config = setup_for_tests(Some(yaml), &["run",
                 "--failure-domain", "foo=1",
                 "--failure-domain", "bar=2,baz=3"
-            ]).unwrap();
+            ]);
             assert_eq!(
                 config.instance.failure_domain(),
                 FailureDomain::from([
@@ -2446,7 +2553,7 @@ instance:
             // --config-parameter
             let config = setup_for_tests(Some(yaml), &["run",
                 "-c", "instance.failure_domain={foo: '11', bar: '22'}"
-            ]).unwrap();
+            ]);
             assert_eq!(
                 config.instance.failure_domain(),
                 FailureDomain::from([
@@ -2471,11 +2578,11 @@ instance:
             let config = setup_for_tests(Some(yaml), &["run",
                 "-c", "  instance.log .level =debug  ",
                 "--config-parameter", "instance. memtx . memory=  999",
-            ]).unwrap();
+            ]);
             assert_eq!(config.instance.tier.unwrap(), "ABC");
             assert_eq!(config.cluster.name.unwrap(), "DEF");
             assert_eq!(config.instance.log.level.unwrap(), args::LogLevel::Debug);
-            assert_eq!(config.instance.memtx.memory.unwrap().to_string(), String::from("999B"));
+            assert_eq!(config.instance.memtx.memory.unwrap().to_string(), "999B");
             assert_eq!(config.instance.audit.unwrap(), "audit.txt");
             assert_eq!(config.instance.instance_dir.unwrap(), PathBuf::from("."));
 
@@ -2484,10 +2591,13 @@ instance:
             //
             let yaml = r###"
 "###;
-            let e = setup_for_tests(Some(yaml), &["run",
+            let e = setup_for_tests_invalid(Some(yaml), &["run",
                 "-c", "  instance.hoobabooba = malabar  ",
-            ]).unwrap_err();
-            assert!(dbg!(e.to_string()).starts_with("invalid configuration: instance: unknown field `hoobabooba`, expected one of"));
+            ]).to_string();
+            assert!(
+                e.starts_with("invalid configuration: instance: unknown field `hoobabooba`, expected one of"),
+                "Received error: {e}"
+            );
 
             let yaml = r###"
 "###;
@@ -2496,8 +2606,11 @@ instance:
                 "  cluster.name=DEF  ;
                   cluster.asdfasdfbasdfbasd = "
             );
-            let e = setup_for_tests(Some(yaml), &["run"]).unwrap_err();
-            assert!(dbg!(e.to_string()).starts_with("invalid configuration: cluster: unknown field `asdfasdfbasdfbasd`, expected one of"));
+            let e = setup_for_tests_invalid(Some(yaml), &["run"]).to_string();
+            assert!(
+                e.starts_with("invalid configuration: cluster: unknown field `asdfasdfbasdfbasd`, expected one of"),
+                "Received error: {e}"
+            );
         }
     }
 
@@ -2508,7 +2621,7 @@ instance:
         let yaml = r###"
 instance:
 "###;
-        let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+        let config = setup_for_tests(Some(yaml), &["run"]);
         let pg = config.instance.pg;
         // pg section wasn't specified, but it should be enabled by default
         assert_eq!(
@@ -2522,7 +2635,7 @@ instance:
         listen: "127.0.0.1:5432"
         ssl: true
 "###;
-        let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+        let config = setup_for_tests(Some(yaml), &["run"]);
         let pg = config.instance.pg;
         assert_eq!(&pg.listen().to_host_port(), "127.0.0.1:5432");
         assert!(pg.ssl());
@@ -2533,7 +2646,7 @@ instance:
     pg:
         listen: "127.0.0.1:5432"
 "###;
-        let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+        let config = setup_for_tests(Some(yaml), &["run"]);
         let pg = config.instance.pg;
         assert_eq!(
             pg.listen(),
@@ -2542,8 +2655,7 @@ instance:
         assert!(!pg.ssl());
 
         // test config with -c option
-        let config =
-            setup_for_tests(None, &["run", "-c", "instance.pg.listen=127.0.0.1:5432"]).unwrap();
+        let config = setup_for_tests(None, &["run", "-c", "instance.pg.listen=127.0.0.1:5432"]);
         let pg = config.instance.pg;
         assert_eq!(
             pg.listen(),
@@ -2552,7 +2664,7 @@ instance:
         assert!(!pg.ssl());
 
         // test config from run args
-        let config = setup_for_tests(None, &["run", "--pg-listen", "127.0.0.1:5432"]).unwrap();
+        let config = setup_for_tests(None, &["run", "--pg-listen", "127.0.0.1:5432"]);
         let pg = config.instance.pg;
         assert_eq!(
             pg.listen(),
@@ -2562,7 +2674,7 @@ instance:
 
         // test config from env
         std::env::set_var("PICODATA_PG_LISTEN", "127.0.0.1:1234");
-        let config = setup_for_tests(None, &["run"]).unwrap();
+        let config = setup_for_tests(None, &["run"]);
         let pg = config.instance.pg;
         assert_eq!(
             pg.listen(),
@@ -2621,7 +2733,7 @@ instance:
 instance:
         listen: localhost:3301
 "###;
-        let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+        let config = setup_for_tests(Some(yaml), &["run"]);
         assert_eq!(
             config.instance.iproto_listen().to_host_port(),
             "localhost:3301"
@@ -2633,17 +2745,17 @@ instance:
         listen: localhost:3302
         iproto_listen: localhost:3301
 "###;
-        let config = setup_for_tests(Some(yaml), &["run"]);
+        let config = setup_for_tests_invalid(Some(yaml), &["run"]);
 
-        assert_eq!(config.unwrap_err().to_string(), "invalid configuration: instance.listen is deprecated, use instance.iproto_listen instead (cannot use both at the same time)");
+        assert_eq!(config.to_string(), "invalid configuration: instance.listen is deprecated, use instance.iproto_listen instead (cannot use both at the same time)");
 
         let yaml = r###"
 instance:
         iproto_listen: localhost:3302
 "###;
-        let config = setup_for_tests(Some(yaml), &["run", "--listen", "localhost:3303"]);
+        let config = setup_for_tests_invalid(Some(yaml), &["run", "--listen", "localhost:3303"]);
 
-        assert_eq!(config.unwrap_err().to_string(), "invalid configuration: instance.listen is deprecated, use instance.iproto_listen instead (cannot use both at the same time)");
+        assert_eq!(config.to_string(), "invalid configuration: instance.listen is deprecated, use instance.iproto_listen instead (cannot use both at the same time)");
     }
 
     #[test]
@@ -2655,7 +2767,7 @@ instance:
 instance:
         advertise_address: localhost:3301
 "###;
-        let config = setup_for_tests(Some(yaml), &["run"]).unwrap();
+        let config = setup_for_tests(Some(yaml), &["run"]);
         assert_eq!(
             config.instance.iproto_advertise().to_host_port(),
             "localhost:3301"
@@ -2667,15 +2779,15 @@ instance:
         advertise_address: localhost:3302
         iproto_advertise: localhost:3301
 "###;
-        let config = setup_for_tests(Some(yaml), &["run"]);
-        assert_eq!(config.unwrap_err().to_string(), "invalid configuration: instance.advertise_address is deprecated, use instance.iproto_advertise instead (cannot use both at the same time)");
+        let config = setup_for_tests_invalid(Some(yaml), &["run"]);
+        assert_eq!(config.to_string(), "invalid configuration: instance.advertise_address is deprecated, use instance.iproto_advertise instead (cannot use both at the same time)");
 
         // can't use both options
         let yaml = r###"
 instance:
         iproto_advertise: localhost:3302
 "###;
-        let config = setup_for_tests(Some(yaml), &["run", "--advertise", "localhost:3303"]);
-        assert_eq!(config.unwrap_err().to_string(), "invalid configuration: instance.advertise_address is deprecated, use instance.iproto_advertise instead (cannot use both at the same time)");
+        let config = setup_for_tests_invalid(Some(yaml), &["run", "--advertise", "localhost:3303"]);
+        assert_eq!(config.to_string(), "invalid configuration: instance.advertise_address is deprecated, use instance.iproto_advertise instead (cannot use both at the same time)");
     }
 }
diff --git a/src/introspection.rs b/src/introspection.rs
index 2750dec91ba5e7c92c48e98bbdfc852afd6a1688..e42cadf257a6f07bae508879832bbef2744306f0 100644
--- a/src/introspection.rs
+++ b/src/introspection.rs
@@ -338,6 +338,7 @@ pub enum IntrospectionError {
     #[error("incorrect value for field '{field}': {error}")]
     ConvertToFieldError {
         field: String,
+        #[source]
         error: Box<dyn std::error::Error>,
     },
 
@@ -357,7 +358,11 @@ pub enum IntrospectionError {
     },
 
     #[error("failed converting '{field}' to a msgpack value: {details}")]
-    ToRmpvValue { field: String, details: Error },
+    ToRmpvValue {
+        field: String,
+        #[source]
+        details: Error,
+    },
 }
 
 impl IntrospectionError {
diff --git a/src/lib.rs b/src/lib.rs
index f2f5582a74644bffecb69442b68530a92a5bec44..8de6850ed625cd61e5d8306d35cb792df41a3a6d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -201,8 +201,8 @@ fn preload_http() {
     preload!("http.mime_types", "http/mime_types.lua");
 }
 
-fn start_http_server(HttpAddress { host, port, .. }: &HttpAddress) {
-    tlog!(Info, "starting http server at {host}:{port}");
+fn start_http_server(addr: &HttpAddress) {
+    tlog!(Info, "starting http server at {addr}");
     let lua = ::tarantool::lua_state();
     lua.exec_with(
         r#"
@@ -211,7 +211,7 @@ fn start_http_server(HttpAddress { host, port, .. }: &HttpAddress) {
         httpd:start();
         _G.pico.httpd = httpd
         "#,
-        (host, port),
+        (addr.host(), addr.port()),
     )
     .expect("failed to start http server");
     lua.exec_with(
diff --git a/src/pgproto.rs b/src/pgproto.rs
index c14a3ce05cbddcfc5c8e72611597080791d1c17f..4528185980b54376bd8225b078499d016d9abf20 100644
--- a/src/pgproto.rs
+++ b/src/pgproto.rs
@@ -130,10 +130,6 @@ impl Context {
         storage: &'static Clusterwide,
     ) -> Result<Self, Error> {
         let listen = config.listen();
-        let host = listen.host.as_str();
-        let port = listen.port.parse::<u16>().map_err(|_| {
-            Error::invalid_configuration(format!("bad postgres port {}", listen.port))
-        })?;
 
         let tls_acceptor = config
             .ssl()
@@ -142,9 +138,8 @@ impl Context {
             .map_err(Error::invalid_configuration)?
             .inspect(|tls| tlog!(Info, "configured {} for pgproto", tls.kind()));
 
-        let addr = (host, port);
-        tlog!(Info, "starting postgres server at {:?}...", addr);
-        let server = server::new_listener(addr)?;
+        tlog!(Info, "starting postgres server at {}...", listen);
+        let server = server::new_listener(listen)?;
 
         Ok(Self {
             server,
diff --git a/src/pgproto/server.rs b/src/pgproto/server.rs
index dac9d682edc06469e8ca5c5c61c5cd42093ad1e9..52dc90b648fbe29c74bb322ef411ff648d2fe54c 100644
--- a/src/pgproto/server.rs
+++ b/src/pgproto/server.rs
@@ -2,10 +2,10 @@ use crate::tlog;
 use std::io;
 use tarantool::coio::CoIOListener;
 
-pub fn new_listener(addr: (&str, u16)) -> tarantool::Result<CoIOListener> {
+pub fn new_listener<A: std::net::ToSocketAddrs>(addr: A) -> tarantool::Result<CoIOListener> {
     let mut socket = None;
     let mut f = |_| {
-        let wrapped = std::net::TcpListener::bind(addr);
+        let wrapped = std::net::TcpListener::bind(&addr);
         tlog!(Debug, "PG socket bind result: {wrapped:?}");
         socket.replace(wrapped);
         0
diff --git a/src/util.rs b/src/util.rs
index 722759c367a19e276e915b802ce8d3e67fc94770..93f7d200572c45a983478cbdbd0802b5d6c49d43 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -4,6 +4,7 @@ use std::{
     any::{Any, TypeId},
     cell::Cell,
     io::{BufRead as _, BufReader, Write as _},
+    iter,
     mem::replace,
     panic::Location,
     path::Path,
@@ -1080,6 +1081,18 @@ pub fn edit_distance(lhs: &str, rhs: &str) -> usize {
     current_row[current_row.len() - 1]
 }
 
+#[track_caller]
+pub(crate) fn unwrap_error_trace<T, R>(err: T) -> R
+where
+    T: std::error::Error + 'static,
+{
+    eprintln!("{err}");
+    for source in iter::successors(Some(&err as &dyn std::error::Error), |e| e.source()) {
+        eprintln!("\tcaused by: {source}");
+    }
+    panic!("called unwrap on {err:#?}");
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 /// tests
 #[cfg(test)]
diff --git a/test/int/test_cli_connect.py b/test/int/test_cli_connect.py
index ac3f2fc22ea37bf55b1b2d9eba72b63136e97392..2b493301fa2409a6a2b874238da16ffb3d3ae3b9 100644
--- a/test/int/test_cli_connect.py
+++ b/test/int/test_cli_connect.py
@@ -1,5 +1,6 @@
 import pexpect  # type: ignore
 import pytest
+import re
 import sys
 from conftest import CLI_TIMEOUT, Cluster, Instance, eprint
 
@@ -258,7 +259,7 @@ def test_connect_auth_type_ldap(cluster: Cluster, ldap_server: LdapServer):
 def test_connect_auth_type_unknown(binary_path_fixt: str):
     cli = pexpect.spawn(
         command=binary_path_fixt,
-        args=["connect", ":0", "-u", "testuser", "-a", "deadbeef"],
+        args=["connect", "localhost:0", "-u", "testuser", "-a", "deadbeef"],
         env={"NO_COLOR": "1"},
         encoding="utf-8",
         timeout=CLI_TIMEOUT,
@@ -419,7 +420,7 @@ def test_connect_unix_ok_via_default_sock(cluster: Cluster):
 def test_connect_with_empty_password_path(binary_path_fixt: str):
     cli = pexpect.spawn(
         command=binary_path_fixt,
-        args=["connect", ":3301", "--password-file", "", "-u", "trash"],
+        args=["connect", "localhost:3301", "--password-file", "", "-u", "trash"],
         env={"NO_COLOR": "1"},
         encoding="utf-8",
         timeout=CLI_TIMEOUT,
@@ -438,7 +439,7 @@ def test_connect_with_wrong_password_path(binary_path_fixt: str):
         command=binary_path_fixt,
         args=[
             "connect",
-            ":3301",
+            "localhost:3301",
             "--password-file",
             "/not/existing/path",
             "-u",
@@ -556,29 +557,24 @@ def test_connect_with_incorrect_url(cluster: Cluster):
         cli.logfile = sys.stdout
         return cli
 
+    # Allow the possibility of coloured and multiline errors.
+    connect_error = re.compile(r"^.*error.*:.*")
+
     # GL685
     cli = connect_to("unix:/tmp/sock")
-    cli.expect_exact(
-        "Error while parsing instance port '/tmp/sock': invalid digit found in string"
-    )
+    cli.expect(connect_error)
     cli.expect_exact(pexpect.EOF)
 
     cli = connect_to("trash:not_a_port")
-    cli.expect_exact(
-        "Error while parsing instance port 'not_a_port': invalid digit found in string"
-    )
+    cli.expect(connect_error)
     cli.expect_exact(pexpect.EOF)
 
     cli = connect_to("random:999999999")
-    cli.expect_exact(
-        "Error while parsing instance port '999999999': number too large to fit in target type"
-    )
+    cli.expect(connect_error)
     cli.expect_exact(pexpect.EOF)
 
     cli = connect_to("random:-1")
-    cli.expect_exact(
-        "Error while parsing instance port '-1': invalid digit found in string"
-    )
+    cli.expect(connect_error)
     cli.expect_exact(pexpect.EOF)
 
 
@@ -613,7 +609,7 @@ def test_connect_timeout(cluster: Cluster):
     # * many others that depend on your/CI network settings and
     #   which we don't want to list here
 
-    cli = connect_to("100")
+    cli = connect_to("invalid")
     cli.expect_exact("Connection Error. Try to reconnect")
     cli.expect_exact(pexpect.EOF)
 
@@ -621,11 +617,11 @@ def test_connect_timeout(cluster: Cluster):
     cli.expect_exact("Connection Error. Try to reconnect")
     cli.expect_exact(pexpect.EOF)
 
-    cli = connect_to("1000010002")
+    cli = connect_to("test")
     cli.expect_exact("Connection Error. Try to reconnect")
     cli.expect_exact(pexpect.EOF)
 
-    cli = connect_to("1000010002", timeout=CLI_TIMEOUT)
+    cli = connect_to("test", timeout=CLI_TIMEOUT)
     cli.expect_exact("Connection Error. Try to reconnect")
     cli.expect_exact(pexpect.EOF)
 
diff --git a/test/int/test_expelling.py b/test/int/test_expelling.py
index 5df330be34d5bd9e93afe5d15ebd23cda396bc19..90726ab683245d244d6485d8a624cfb330d63fb0 100644
--- a/test/int/test_expelling.py
+++ b/test/int/test_expelling.py
@@ -172,7 +172,7 @@ def test_expel_timeout(cluster: Cluster):
             "expel",
             "random_instance_name",
             f"--timeout={CLI_TIMEOUT}",
-            "--peer=10001",
+            "--peer=invalid",
         ],
         encoding="utf-8",
         timeout=CLI_TIMEOUT,
@@ -182,14 +182,8 @@ def test_expel_timeout(cluster: Cluster):
     cli.expect_exact("Enter password for admin:")
     cli.sendline("wrong_password")
 
-    if sys.platform == "darwin":
-        cli.expect_exact(
-            "CRITICAL: failed to connect to address '10001:3301': "
-            "No route to host (os error 65)"
-        )
-    else:
-        cli.expect_exact("CRITICAL: connect timeout")
-
+    # Allow the possibility of coloured output.
+    cli.expect(r".*CRITICAL.*:.*")
     cli.expect_exact(pexpect.EOF)
 
 
diff --git a/test/int/test_uninitialized.py b/test/int/test_uninitialized.py
index 6f3cb120119046fef810f09e6264350bc6de2316..861b2c859848c9071b1970139ce511ee59f0a48f 100644
--- a/test/int/test_uninitialized.py
+++ b/test/int/test_uninitialized.py
@@ -17,7 +17,7 @@ def uninitialized_instance(cluster: Cluster) -> Generator[Instance, None, None]:
     """Returns a running instance that is stuck in discovery phase."""
 
     # Connecting TCP/0 always results in "Connection refused"
-    instance = cluster.add_instance(peers=[":0"], wait_online=False)
+    instance = cluster.add_instance(peers=["localhost:0"], wait_online=False)
     instance.start()
 
     def check_running(instance):