diff --git a/src/box/iproto.cc b/src/box/iproto.cc
index 814ad86818fe5fb7feafd740cf12a17739138947..08f1a0b42414196ee04fec413c7a01477e597d34 100644
--- a/src/box/iproto.cc
+++ b/src/box/iproto.cc
@@ -483,7 +483,7 @@ iproto_enqueue_batch(struct iproto_connection *con, struct ibuf *in)
 		 * stay in sync.
 		 */
 		if (ireq->header.type >= IPROTO_SELECT &&
-		    ireq->header.type <= IPROTO_AUTH) {
+		    ireq->header.type <= IPROTO_EVAL) {
 			/* Pre-parse request before putting it into the queue */
 			if (ireq->header.bodycnt == 0) {
 				tnt_raise(ClientError, ER_INVALID_MSGPACK,
@@ -661,16 +661,24 @@ iproto_process(struct iproto_request *ireq)
 		case IPROTO_REPLACE:
 		case IPROTO_UPDATE:
 		case IPROTO_DELETE:
+			assert(ireq->request.type == ireq->header.type);
 			struct iproto_port port;
 			iproto_port_init(&port, out, ireq->header.sync);
 			box_process(&ireq->request, (struct port *) &port);
 			break;
 		case IPROTO_CALL:
+			assert(ireq->request.type == ireq->header.type);
 			stat_collect(stat_base, ireq->request.type, 1);
 			box_lua_call(&ireq->request, &iobuf->out);
 			break;
+		case IPROTO_EVAL:
+			assert(ireq->request.type == ireq->header.type);
+			stat_collect(stat_base, ireq->request.type, 1);
+			box_lua_eval(&ireq->request, &iobuf->out);
+			break;
 		case IPROTO_AUTH:
 		{
+			assert(ireq->request.type == ireq->header.type);
 			const char *user = ireq->request.key;
 			uint32_t len = mp_decode_strl(&user);
 			authenticate(user, len, ireq->request.tuple,
diff --git a/src/box/iproto_constants.c b/src/box/iproto_constants.c
index 230454326d9df8f0866228558ca60f4fa1023d20..7497fec094801a9608ca3d2dbe4ce51b1511eee1 100644
--- a/src/box/iproto_constants.c
+++ b/src/box/iproto_constants.c
@@ -94,11 +94,12 @@ const char *iproto_type_strs[] =
 	"UPDATE",
 	"DELETE",
 	"CALL",
-	"AUTH"
+	"AUTH",
+	"EVAL"
 };
 
 #define bit(c) (1ULL<<IPROTO_##c)
-const uint64_t iproto_body_key_map[IPROTO_AUTH + 1] = {
+const uint64_t iproto_body_key_map[IPROTO_EVAL + 1] = {
 	0,                                                     /* unused */
 	bit(SPACE_ID) | bit(LIMIT) | bit(KEY),                 /* SELECT */
 	bit(SPACE_ID) | bit(TUPLE),                            /* INSERT */
@@ -106,7 +107,8 @@ const uint64_t iproto_body_key_map[IPROTO_AUTH + 1] = {
 	bit(SPACE_ID) | bit(KEY) | bit(TUPLE),                 /* UPDATE */
 	bit(SPACE_ID) | bit(KEY),                              /* DELETE */
 	bit(FUNCTION_NAME) | bit(TUPLE),                       /* CALL */
-	bit(USER_NAME) | bit(TUPLE)                            /* AUTH */
+	bit(USER_NAME) | bit(TUPLE),                           /* AUTH */
+	bit(FUNCTION_NAME) | bit(TUPLE),                       /* EVAL */
 };
 #undef bit
 
diff --git a/src/box/iproto_constants.h b/src/box/iproto_constants.h
index 917dbe62d72b0f3bfb5862e210f98dd2c7592093..55572cac8c78d21033cc5163440c003b7decf613 100644
--- a/src/box/iproto_constants.h
+++ b/src/box/iproto_constants.h
@@ -118,8 +118,9 @@ enum iproto_type {
 	IPROTO_DELETE = 5,
 	IPROTO_TYPE_DML_MAX = IPROTO_DELETE + 1,
 	IPROTO_CALL = 6,
-	IPROTO_TYPE_STAT_MAX = IPROTO_CALL + 1,
 	IPROTO_AUTH = 7,
+	IPROTO_EVAL = 8,
+	IPROTO_TYPE_STAT_MAX = IPROTO_EVAL + 1,
 	/* admin command codes */
 	IPROTO_PING = 64,
 	IPROTO_JOIN = 65,
@@ -138,7 +139,7 @@ extern const uint64_t iproto_body_key_map[];
 static inline const char *
 iproto_type_name(uint32_t type)
 {
-	if (type >= IPROTO_TYPE_DML_MAX)
+	if (type >= IPROTO_TYPE_STAT_MAX)
 		return "unknown";
 	return iproto_type_strs[type];
 }
diff --git a/src/box/lua/call.cc b/src/box/lua/call.cc
index eb661774c1ad8e9c71f2948c7cf36df5dc6cac72..89cf3dbd57fcd0a214a42740cfcfe19bddf87778 100644
--- a/src/box/lua/call.cc
+++ b/src/box/lua/call.cc
@@ -590,6 +590,61 @@ box_lua_call(struct request *request, struct obuf *out)
 	}
 }
 
+static inline void
+execute_eval(lua_State *L, struct request *request, struct obuf *out)
+{
+	/* Check permissions */
+	struct credentials *credentials = current_user();
+	if (!(credentials->universal_access & PRIV_X)) {
+		/* Access violation, report error. */
+		struct user *user = user_cache_find(credentials->uid);
+		tnt_raise(ClientError, ER_FUNCTION_ACCESS_DENIED,
+			  priv_name(PRIV_X), user->name, "eval");
+	}
+
+	/* Compile expression */
+	const char *expr = request->key;
+	uint32_t expr_len = mp_decode_strl(&expr);
+	if (luaL_loadbuffer(L, expr, expr_len, "=eval"))
+		tnt_raise(LuajitError, L);
+
+	/* Unpack arguments */
+	const char *args = request->tuple;
+	uint32_t arg_count = mp_decode_array(&args);
+	luaL_checkstack(L, arg_count, "call: out of stack");
+	for (uint32_t i = 0; i < arg_count; i++) {
+		luamp_decode(L, luaL_msgpack_default, &args);
+	}
+
+	/* Call compiled code */
+	lua_call(L, arg_count, LUA_MULTRET);
+
+	/* Send results of the called procedure to the client. */
+	struct obuf_svp svp = iproto_prepare_select(out);
+	int nrets = lua_gettop(L);
+	for (int k = 1; k <= nrets; ++k) {
+		luamp_encode(L, luaL_msgpack_default, out, k);
+	}
+	iproto_reply_select(out, &svp, request->header->sync, nrets);
+}
+
+void
+box_lua_eval(struct request *request, struct obuf *out)
+{
+	lua_State *L = NULL;
+	try {
+		L = lua_newthread(tarantool_L);
+		LuarefGuard coro_ref(tarantool_L);
+		execute_eval(L, request, out);
+	} catch (Exception *e) {
+		/* Let all well-behaved exceptions pass through. */
+		throw;
+	} catch (...) {
+		/* Convert Lua error to a Tarantool exception. */
+		tnt_raise(LuajitError, L != NULL ? L : tarantool_L);
+	}
+}
+
 static int
 lbox_snapshot(struct lua_State *L)
 {
diff --git a/src/box/lua/call.h b/src/box/lua/call.h
index bbb1a41f5a10613d5554b66a216ad2de24285996..2562554c52e83c295ecefbf8eddefc34f58af767 100644
--- a/src/box/lua/call.h
+++ b/src/box/lua/call.h
@@ -41,6 +41,9 @@ struct port;
 void
 box_lua_call(struct request *request, struct obuf *out);
 
+void
+box_lua_eval(struct request *request, struct obuf *out);
+
 extern "C" {
 struct port_ffi
 {
diff --git a/src/lua/box_net_box.lua b/src/lua/box_net_box.lua
index 0a7218ae26cff3ec7d915bce1693a947a8aed566..5fcb5c35391e4e1a2548c402cb80f558add88a5c 100644
--- a/src/lua/box_net_box.lua
+++ b/src/lua/box_net_box.lua
@@ -19,6 +19,7 @@ local UPDATE            = 4
 local DELETE            = 5
 local CALL              = 6
 local AUTH              = 7
+local EVAL              = 8
 local PING              = 64
 local ERROR_TYPE        = 65536
 
@@ -122,6 +123,17 @@ local proto = {
         )
     end,
 
+    -- lua eval
+    eval = function(sync, expr, args)
+        if args == nil then
+            args = {}
+        end
+        return request(
+            { [SYNC] = sync, [TYPE] = EVAL  },
+            { [FUNCTION_NAME] = expr, [TUPLE] = args }
+        )
+    end,
+
     -- insert
     insert = function(sync, spaceno, tuple)
         return request(
@@ -472,6 +484,24 @@ local remote_methods = {
         return res.body[DATA]
     end,
 
+    eval    = function(self, expr, ...)
+        if type(self) ~= 'table' then
+            box.error(box.error.PROC_LUA, "usage: remote:eval(expr, ...)")
+        end
+
+        expr = tostring(expr)
+        local res = self:_request('eval', true, expr, {...})
+        local data = res.body[DATA]
+        local data_len = #data
+        if data_len == 1 then
+            return data[1]
+        elseif data_len == 0 then
+            return
+        else
+            return unpack(data)
+        end
+    end,
+
     is_connected = function(self)
         if self.state == 'active' then
             return true
@@ -1013,10 +1043,10 @@ local remote_methods = {
         if raise == nil then
             raise = true
         end
-        return self:_request_raw(CONSOLE_FAKESYNC, request_body, raise)
+        return self:_request_raw('call', CONSOLE_FAKESYNC, request_body, raise)
     end,
 
-    _request_raw = function(self, sync, request, raise)
+    _request_raw = function(self, name, sync, request, raise)
 
         local fid = fiber.id()
         if self.timeouts[fid] == nil then
@@ -1058,7 +1088,7 @@ local remote_methods = {
         end
 
         if response.body[DATA] ~= nil then
-            if rawget(box, 'tuple') ~= nil then
+            if rawget(box, 'tuple') ~= nil and name ~= 'eval' then
                 for i, v in pairs(response.body[DATA]) do
                     response.body[DATA][i] =
                         box.tuple.new(response.body[DATA][i])
@@ -1074,7 +1104,7 @@ local remote_methods = {
     _request_internal = function(self, name, raise, ...)
         local sync = self.proto:sync()
         local request = self.proto[name](sync, ...)
-        return self:_request_raw(sync, request, raise)
+        return self:_request_raw(name, sync, request, raise)
     end,
 
     -- private (low level) methods
@@ -1135,6 +1165,16 @@ remote.self = {
         end
         return result
     end,
+    eval = function(_box, expr, ...)
+        local proc, errmsg = loadstring(expr)
+        if not proc then
+            proc, errmsg = loadstring("return "..expr)
+        end
+        if not proc then
+            box.error(box.error.PROC_LUA, errmsg)
+        end
+        return proc(...)
+    end,
     console = false
 }
 
diff --git a/src/lua/console.lua b/src/lua/console.lua
index 7a591139111ecb596d5a780cc007e187bb32b9db..c1f19bd340479aac0ffddfe15b053871ca70a457 100644
--- a/src/lua/console.lua
+++ b/src/lua/console.lua
@@ -82,7 +82,7 @@ local function remote_eval(self, line)
     --
     -- call remote 'console.eval' function using 'dostring' and return result
     --
-    local status, res = pcall(self.remote.call, self.remote, "dostring",
+    local status, res = pcall(self.remote.eval, self.remote,
         "return require('console').eval(...)", line)
     if not status then
         -- remote request failed
diff --git a/test/box/box.net.box.result b/test/box/box.net.box.result
index 448a5644ed972235e3ca459023019b4bf34e0857..25a221beb9abd4ee0b7e79fd51e8048ffad57ec0 100644
--- a/test/box/box.net.box.result
+++ b/test/box/box.net.box.result
@@ -10,9 +10,6 @@ log = require 'log'
 msgpack = require 'msgpack'
 ---
 ...
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
----
-...
 LISTEN = require('uri').parse(box.cfg.listen)
 ---
 ...
@@ -54,6 +51,7 @@ cn:ping()
 ---
 - true
 ...
+-- check permissions
 cn:call('unexists_procedure')
 ---
 - error: Procedure 'unexists_procedure' is not defined
@@ -63,6 +61,27 @@ function test_foo(a,b,c) return { {{ [a] = 1 }}, {{ [b] = 2 }}, c } end
 ...
 cn:call('test_foo', 'a', 'b', 'c')
 ---
+- error: Execute access denied for user 'guest' to function 'test_foo'
+...
+cn:eval('return 2+2')
+---
+- error: Execute access denied for user 'guest' to function 'eval'
+...
+box.schema.user.grant('guest','execute','universe')
+---
+...
+cn:close()
+---
+...
+cn = remote:new(box.cfg.listen)
+---
+...
+cn:call('unexists_procedure')
+---
+- error: Procedure 'unexists_procedure' is not defined
+...
+cn:call('test_foo', 'a', 'b', 'c')
+---
 - - [{'a': 1}]
   - [{'b': 2}]
   - ['c']
@@ -71,6 +90,60 @@ cn:call(nil, 'a', 'b', 'c')
 ---
 - error: Procedure 'nil' is not defined
 ...
+cn:eval('return 2+2')
+---
+- 4
+...
+cn:eval('return 1, 2, 3')
+---
+- 1
+- 2
+- 3
+...
+cn:eval('return ...', 1, 2, 3)
+---
+- 1
+- 2
+- 3
+...
+cn:eval('return { k = "v1" }, true, {  xx = 10, yy = 15 }, nil')
+---
+- {k: v1}
+- true
+- {yy: 15, xx: 10}
+- null
+...
+cn:eval('return nil')
+---
+- null
+...
+cn:eval('return')
+---
+...
+cn:eval('error("exception")')
+---
+- error: 'eval:1: exception'
+...
+cn:eval('box.error(0)')
+---
+- error: Unknown error
+...
+cn:eval('!invalid expression')
+---
+- error: 'eval:1: unexpected symbol near ''!'''
+...
+box.schema.user.revoke('guest','execute','universe')
+---
+...
+box.schema.user.grant('guest','read,write,execute','universe')
+---
+...
+cn:close()
+---
+...
+cn = remote:new(box.cfg.listen)
+---
+...
 cn:_select(space.id, space.index.primary.id, 123)
 ---
 - []
@@ -141,7 +214,7 @@ cn.space.net_box_test_space:insert{234, 1,2,3}
 ...
 cn.space.net_box_test_space.insert{234, 1,2,3}
 ---
-- error: 'builtin/net.box.lua:226: Use space:method(...) instead space.method(...)'
+- error: 'builtin/net.box.lua:238: Use space:method(...) instead space.method(...)'
 ...
 cn.space.net_box_test_space:replace{354, 1,2,3}
 ---
@@ -737,7 +810,7 @@ require('fiber').create(
 ---
 - status: suspended
   name: lua
-  id: 151
+  id: 157
 ...
 while true do
    local line = file_log:read(2048)
diff --git a/test/box/box.net.box.test.lua b/test/box/box.net.box.test.lua
index 2839f9eb834b96bce96fbfa101a55f3eb3f6dfb9..526e23586e25de3f30a02687624100782f3842e5 100644
--- a/test/box/box.net.box.test.lua
+++ b/test/box/box.net.box.test.lua
@@ -3,7 +3,6 @@ fiber = require 'fiber'
 log = require 'log'
 msgpack = require 'msgpack'
 
-box.schema.user.grant('guest', 'read,write,execute', 'universe')
 LISTEN = require('uri').parse(box.cfg.listen)
 space = box.schema.create_space('net_box_test_space')
 index = space:create_index('primary', { type = 'tree' })
@@ -23,12 +22,33 @@ log.info("ping is done")
 cn:ping()
 
 
+-- check permissions
 cn:call('unexists_procedure')
-
 function test_foo(a,b,c) return { {{ [a] = 1 }}, {{ [b] = 2 }}, c } end
+cn:call('test_foo', 'a', 'b', 'c')
+cn:eval('return 2+2')
+
+box.schema.user.grant('guest','execute','universe')
+cn:close()
+cn = remote:new(box.cfg.listen)
 
+cn:call('unexists_procedure')
 cn:call('test_foo', 'a', 'b', 'c')
 cn:call(nil, 'a', 'b', 'c')
+cn:eval('return 2+2')
+cn:eval('return 1, 2, 3')
+cn:eval('return ...', 1, 2, 3)
+cn:eval('return { k = "v1" }, true, {  xx = 10, yy = 15 }, nil')
+cn:eval('return nil')
+cn:eval('return')
+cn:eval('error("exception")')
+cn:eval('box.error(0)')
+cn:eval('!invalid expression')
+
+box.schema.user.revoke('guest','execute','universe')
+box.schema.user.grant('guest','read,write,execute','universe')
+cn:close()
+cn = remote:new(box.cfg.listen)
 
 cn:_select(space.id, space.index.primary.id, 123)
 space:insert{123, 345}
diff --git a/test/box/call.result b/test/box/call.result
index b23515307bdc5e832ec3a19ea029e2f93f3ea62a..9e606bc540a08514ba7eed7c25a3bdb42aae436b 100644
--- a/test/box/call.result
+++ b/test/box/call.result
@@ -133,7 +133,7 @@ function f3() return {'hello', {'world'}} end
 ...
 call f3()
 ---
-- ['hello', ('world',)]
+- ['hello', ['world']]
 ...
 function f3() return 'hello', {{'world'}, {'canada'}} end
 ---
@@ -141,7 +141,7 @@ function f3() return 'hello', {{'world'}, {'canada'}} end
 call f3()
 ---
 - ['hello']
-- [('world',), ('canada',)]
+- [['world'], ['canada']]
 ...
 function f3() return {}, '123', {{}, {}} end
 ---
@@ -150,14 +150,14 @@ call f3()
 ---
 - []
 - ['123']
-- [(), ()]
+- [[], []]
 ...
 function f3() return { {{'hello'}} } end
 ---
 ...
 call f3()
 ---
-- [('hello',)]
+- [['hello']]
 ...
 function f3() return { box.tuple.new('hello'), {'world'} } end
 ---
@@ -375,3 +375,42 @@ space:drop()
 box.schema.user.drop('test')
 ---
 ...
+eval (return 2+2)()
+---
+[4]
+
+eval (return 1, 2, 3)()
+---
+[1, 2, 3]
+
+eval (return ...)(1,2,3)
+---
+[1, 2, 3]
+
+eval (return { k = "v1" }, true, {  xx = 10, yy = 15 }, nil)()
+---
+- {k: v1}
+- true
+- {xx: 10, yy: 15}
+- null
+
+eval (return nil)()
+---
+[null]
+
+eval (return)()
+---
+[]
+
+eval (error("exception"))()
+---
+error: {code: ER_PROC_LUA, reason: 'eval:1: exception'}
+
+eval (box.error(0))()
+---
+error: {code: ER_OK, reason: Unknown error}
+
+eval (!invalid expression)()
+---
+error: {code: ER_PROC_LUA, reason: 'eval:1: unexpected symbol near ''!'''}
+
diff --git a/test/box/call.test.py b/test/box/call.test.py
index d1681f28ea299c8e617b9086e4f74bbef3cf2420..cb8710bf0046ca1b6f0b7b8ab6806c94a59584fa 100644
--- a/test/box/call.test.py
+++ b/test/box/call.test.py
@@ -123,5 +123,20 @@ sql("call space:delete(4)")
 admin("space:drop()")
 admin("box.schema.user.drop('test')")
 
+def lua_eval(name, *args):
+    print 'eval (%s)(%s)' % (name, ','.join([ str(arg) for arg in args]))
+    print '---'
+    print sql.py_con.eval(name, *args)
+
+lua_eval('return 2+2')
+lua_eval('return 1, 2, 3')
+lua_eval('return ...', 1, 2, 3)
+lua_eval('return { k = "v1" }, true, {  xx = 10, yy = 15 }, nil')
+lua_eval('return nil')
+lua_eval('return')
+lua_eval('error("exception")')
+lua_eval('box.error(0)')
+lua_eval('!invalid expression')
+
 # Re-connect after removing user
 sql.py_con.close()
diff --git a/test/box/iproto.result b/test/box/iproto.result
index fa577523ee906d71b657bd6fb7875cc39808a46c..a9988eca43c2b9e523e69fd06725f1f9e6e861a5 100644
--- a/test/box/iproto.result
+++ b/test/box/iproto.result
@@ -82,30 +82,22 @@ index = space:create_index('primary', { type = 'hash' })
 box.schema.user.grant('guest', 'read,write,execute', 'space', 'test')
 ---
 ...
----
-- [1, 'baobab']
-...
----
-- [2, 'obbaba']
-...
----
-- [1, 'baobab']
-...
----
-- [3, 'occama']
-...
----
-- [2, 'obbaba']
-...
----
-- [4, 'ockham']
-...
----
-- [1, 'baobab']
-...
----
-- [2, 'obbaba']
-...
+- [1, baobab]
+
+- [2, obbaba]
+
+- [1, baobab]
+
+- [3, occama]
+
+- [2, obbaba]
+
+- [4, ockham]
+
+- [1, baobab]
+
+- [2, obbaba]
+
 space:drop()
 ---
 ...
diff --git a/test/box/misc.result b/test/box/misc.result
index fd4be8cb229ab86d67ac48a1041754790c504888..dcf5c0415461fa6c1101d3fe404e30a05d2561e4 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -85,9 +85,11 @@ end;
 t;
 ---
 - - DELETE
+  - EVAL
   - SELECT
   - REPLACE
   - INSERT
+  - AUTH
   - CALL
   - UPDATE
   - total
diff --git a/test/lib/tarantool-python b/test/lib/tarantool-python
index b2bcd37691d3f4664bd34646078b02709fa102cd..c9f2bd36736a8198920b35af072cdf12cbac8dec 160000
--- a/test/lib/tarantool-python
+++ b/test/lib/tarantool-python
@@ -1 +1 @@
-Subproject commit b2bcd37691d3f4664bd34646078b02709fa102cd
+Subproject commit c9f2bd36736a8198920b35af072cdf12cbac8dec