diff --git a/changelogs/unreleased/gh-8927-socketpair.md b/changelogs/unreleased/gh-8927-socketpair.md
new file mode 100644
index 0000000000000000000000000000000000000000..66f09d53b81989646ce7f8fae7671531bca066e9
--- /dev/null
+++ b/changelogs/unreleased/gh-8927-socketpair.md
@@ -0,0 +1,4 @@
+## feature/lua/socket
+
+* Introduced new socket functions `socket.socketpair`, `socket.from_fd`, and
+  `socket:detach` (gh-8927).
diff --git a/src/lua/socket.lua b/src/lua/socket.lua
index 5aac3f662fb258e33e8a82908660acd788c84845..28581480497bde2d590a782e3f2de014239e099b 100644
--- a/src/lua/socket.lua
+++ b/src/lua/socket.lua
@@ -32,6 +32,7 @@ ffi.cdef[[
     ssize_t read(int fd, void *buf, size_t count);
     int listen(int fd, int backlog);
     int socket(int domain, int type, int protocol);
+    int socketpair(int domain, int type, int protocol, int sv[2]);
     int coio_close(int s);
     int shutdown(int s, int how);
     ssize_t send(int sockfd, const void *buf, size_t len, int flags);
@@ -933,7 +934,7 @@ local function socket_sendto(self, host, port, octets, flags)
     return tonumber(res)
 end
 
-local function socket_new(domain, stype, proto)
+local function check_socket_args(domain, stype, proto)
     local idomain = get_ivalue(internal.DOMAIN, domain)
     if idomain == nil then
         boxerrno(boxerrno.EINVAL)
@@ -951,6 +952,14 @@ local function socket_new(domain, stype, proto)
         return nil
     end
 
+    return idomain, itype, iproto
+end
+
+local function socket_new(domain, stype, proto)
+    local idomain, itype, iproto = check_socket_args(domain, stype, proto)
+    if idomain == nil then
+        return nil
+    end
     local fd = ffi.C.socket(idomain, itype, iproto)
     if fd >= 0 then
         local socket = make_socket(fd, itype)
@@ -962,6 +971,25 @@ local function socket_new(domain, stype, proto)
     end
 end
 
+local function socket_socketpair(domain, stype, proto)
+    local idomain, itype, iproto = check_socket_args(domain, stype, proto)
+    if idomain == nil then
+        return nil
+    end
+    local sv = ffi.new('int[2]')
+    if ffi.C.socketpair(idomain, itype, iproto, sv) ~= 0 then
+        return nil
+    end
+    local s1 = make_socket(sv[0], itype)
+    local s2 = make_socket(sv[1], itype)
+    if not s1:nonblock(true) or not s2:nonblock(true) then
+        s1:close()
+        s2:close()
+        return nil
+    end
+    return s1, s2
+end
+
 local function socket_from_fd(fd)
     if type(fd) ~= 'number' then
         error('fd must be a number')
@@ -1622,6 +1650,7 @@ end
 
 return setmetatable({
     from_fd = socket_from_fd;
+    socketpair = socket_socketpair;
     getaddrinfo = getaddrinfo,
     tcp_connect = tcp_connect,
     tcp_server = tcp_server,
diff --git a/test/app-luatest/socket_test.lua b/test/app-luatest/socket_test.lua
index 92374ff8d4b6b8292ae8da00c6db8baab04f9057..db099b415bed313478b6a4fede3275a3c1f7cf0a 100644
--- a/test/app-luatest/socket_test.lua
+++ b/test/app-luatest/socket_test.lua
@@ -1,3 +1,4 @@
+local errno = require('errno')
 local socket = require('socket')
 local t = require('luatest')
 
@@ -58,3 +59,26 @@ g.test_detach = function()
     t.assert_is_not(s2:name(), nil)
     t.assert(s2:close())
 end
+
+g.test_socketpair = function()
+    t.assert_is(socket.socketpair(), nil)
+    t.assert_equals(errno(), errno.EINVAL)
+    t.assert_is(socket.socketpair('foo'), nil)
+    t.assert_equals(errno(), errno.EINVAL)
+    t.assert_is(socket.socketpair('AF_UNIX', 'bar'), nil)
+    t.assert_equals(errno(), errno.EINVAL)
+    t.assert_is(socket.socketpair('AF_UNIX', 'SOCK_STREAM', 'baz'), nil)
+    t.assert_equals(errno(), errno.EPROTOTYPE)
+    t.assert_is(socket.socketpair('AF_INET', 'SOCK_STREAM', 0), nil)
+    t.assert_equals(errno(), errno.EOPNOTSUPP)
+
+    local s1, s2 = socket.socketpair('AF_UNIX', 'SOCK_STREAM', 0)
+    t.assert(s1)
+    t.assert(s2)
+    t.assert(s1:nonblock())
+    t.assert(s2:nonblock())
+    t.assert_equals(s1:send('foo'), 3)
+    t.assert_equals(s2:recv(), 'foo')
+    t.assert(s1:close())
+    t.assert(s2:close())
+end