Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

replace vsock helper server with socat and related changes #4125

Merged
merged 8 commits into from
Sep 22, 2023
27 changes: 19 additions & 8 deletions tests/framework/utils_vsock.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,29 @@ def make_blob(dst_dir, size=BLOB_SIZE):
return blob_path, blob_hash.hexdigest()


def check_host_connections(vm, uds_path, blob_path, blob_hash):
def start_guest_echo_server(vm):
"""Start a vsock echo server in the microVM.

Returns a UDS path to connect to the server.
"""
cmd = f"nohup socat VSOCK-LISTEN:{ECHO_SERVER_PORT},backlog=128,reuseaddr,fork EXEC:'/bin/cat' > /dev/null 2>&1 &"
ecode, _, stderr = vm.ssh.run(cmd)
assert ecode == 0, stderr

# Give the server time to initialise
time.sleep(1)

return os.path.join(vm.jailer.chroot_path(), VSOCK_UDS_PATH)


def check_host_connections(uds_path, blob_path, blob_hash):
"""Test host-initiated connections.

This will start a daemonized echo server on the guest VM, and then spawn
`TEST_CONNECTION_COUNT` `HostEchoWorker` threads.
This will spawn `TEST_CONNECTION_COUNT` `HostEchoWorker` threads.
After the workers are done transferring the data read from `blob_path`,
the hashes they computed for the data echoed back by the server are
checked against `blob_hash`.
"""
cmd = "/tmp/vsock_helper echosrv -d {}".format(ECHO_SERVER_PORT)
ecode, _, _ = vm.ssh.run(cmd)
assert ecode == 0

workers = []
for _ in range(TEST_CONNECTION_COUNT):
Expand Down Expand Up @@ -226,5 +237,5 @@ def check_vsock_device(vm, bin_vsock_path, test_fc_session_root_path, ssh_connec
check_guest_connections(vm, path, vm_blob_path, blob_hash)

# Test vsock host-initiated connections.
path = os.path.join(vm.jailer.chroot_path(), VSOCK_UDS_PATH)
check_host_connections(vm, path, blob_path, blob_hash)
path = start_guest_echo_server(vm)
check_host_connections(path, blob_path, blob_hash)
105 changes: 4 additions & 101 deletions tests/host_tools/vsock_helper.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

// This is a vsock helper tool, used by the Firecracker integration tests,
// to - well - test the virtio vsock device. It can be used to:
// 1. Run a forking echo server, that echoes back any data received from
// a client; and
// 2. Run a vsock echo client, that reads data from STDIN, sends it to an
// echo server, then forwards the server's reply to STDOUT.
// to - well - test the virtio vsock device. It can be used to
// run a vsock echo client, that reads data from STDIN, sends it to an
// echo server, then forwards the server's reply to STDOUT.

#include <sys/socket.h>
#include <sys/types.h>
Expand All @@ -27,12 +25,7 @@


int print_usage() {
fprintf(stderr, "Usage: ./vsock-helper {echosrv [-d] <port> | echo <cid> <port>}\n");
fprintf(stderr, "\n");
fprintf(stderr, " echosrv start a vsock echo server. The server will accept\n");
fprintf(stderr, " any incoming connection, and echo back any data\n");
fprintf(stderr, " received on it.\n");
fprintf(stderr, " -d can be used to daemonize the server.\n");
fprintf(stderr, "Usage: ./vsock-helper echo <cid> <port>\n");
fprintf(stderr, "\n");
fprintf(stderr, " echo connect to an echo server, listening on CID:port.\n");
fprintf(stderr, " STDIN will be piped through to the echo server, and\n");
Expand Down Expand Up @@ -60,66 +53,6 @@ int xfer(int src_fd, int dst_fd) {
}


int run_echosrv(uint32_t port) {

struct sockaddr_vm vsock_addr = {
.svm_family = AF_VSOCK,
.svm_port = port,
.svm_cid = VMADDR_CID_ANY
};

int srv_sock = socket(AF_VSOCK, SOCK_STREAM, 0);
if (srv_sock < 0) {
perror("socket()");
return -1;
}

int res = bind(srv_sock, (struct sockaddr*)&vsock_addr, sizeof(struct sockaddr_vm));
if (res) {
perror("bind()");
return -1;
}

res = listen(srv_sock, SERVER_ACCEPT_BACKLOG);
if (res) {
perror("listen()");
return -1;
}

for (;;) {
struct sockaddr cl_addr;
socklen_t sockaddr_len = sizeof(cl_addr);
int cl_sock = accept(srv_sock, &cl_addr, &sockaddr_len);
if (cl_sock < 0) {
perror("accept()");
continue;
}

int pid = fork();
if (pid < 0) {
perror("fork()");
close(cl_sock);
continue;
}

if (!pid) {
int res;
do {
res = xfer(cl_sock, cl_sock);
} while (res > 0);
return res >= 0 ? 0 : -1;
}

close(cl_sock);
int cstatus;
waitpid(-1, &cstatus, WNOHANG);
printf("New client forked...\n");
}

return 0;
}


int run_echo(uint32_t cid, uint32_t port) {

int sock = socket(AF_VSOCK, SOCK_STREAM, 0);
Expand Down Expand Up @@ -161,36 +94,6 @@ int main(int argc, char **argv) {
return print_usage();
}

if (strcmp(argv[1], "echosrv") == 0) {
uint32_t port;
if (strcmp(argv[2], "-d") == 0) {
if (argc < 4) {
return print_usage();
}
port = atoi(argv[3]);
if (!port) {
return print_usage();
}
int pid = fork();
if (pid < 0) return -1;
if (pid) {
printf("Forked vsock echo daemon listening on port %d\n", port);
return 0;
}
setsid();
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
}
else {
port = atoi(argv[2]);
if (!port) {
return print_usage();
}
}
return run_echosrv(port);
}

if (strcmp(argv[1], "echo") == 0) {
if (argc != 4) {
return print_usage();
Expand Down
5 changes: 3 additions & 2 deletions tests/integration_tests/functional/test_snapshot_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
check_host_connections,
make_blob,
make_host_port_path,
start_guest_echo_server,
)


Expand Down Expand Up @@ -93,8 +94,8 @@ def test_5_snapshots(
)
check_guest_connections(microvm, path, vm_blob_path, blob_hash)
# Test vsock host-initiated connections.
path = os.path.join(microvm.jailer.chroot_path(), VSOCK_UDS_PATH)
check_host_connections(microvm, path, blob_path, blob_hash)
path = start_guest_echo_server(microvm)
check_host_connections(path, blob_path, blob_hash)

# Check that the root device is not corrupted.
check_filesystem(microvm.ssh, "squashfs", "/dev/vda")
Expand Down
53 changes: 24 additions & 29 deletions tests/integration_tests/functional/test_vsock.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

In order to test the vsock device connection state machine, these tests will:
- Generate a 20MiB random data blob;
- Use `host_tools/vsock_helper.c` to start a listening echo server inside the
guest VM;
- Use `socat` to start a listening echo server inside the guest VM;
- Run 50, concurrent, host-initiated connections, each transfering the random
blob to and from the guest echo server;
- For every connection, check that the data received back from the echo server
Expand All @@ -27,6 +26,7 @@
check_vsock_device,
make_blob,
make_host_port_path,
start_guest_echo_server,
)

NEGATIVE_TEST_CONNECTION_COUNT = 100
Expand All @@ -51,17 +51,15 @@ def test_vsock(test_microvm_with_api, bin_vsock_path, test_fc_session_root_path)
check_vsock_device(vm, bin_vsock_path, test_fc_session_root_path, vm.ssh)


def negative_test_host_connections(vm, uds_path, blob_path, blob_hash):
def negative_test_host_connections(vm, blob_path, blob_hash):
"""Negative test for host-initiated connections.

This will start a daemonized echo server on the guest VM, and then spawn
`NEGATIVE_TEST_CONNECTION_COUNT` `HostEchoWorker` threads.
Closes the UDS sockets while data is in flight.
"""

cmd = "/tmp/vsock_helper echosrv -d {}".format(ECHO_SERVER_PORT)
ecode, _, _ = vm.ssh.run(cmd)
assert ecode == 0
uds_path = start_guest_echo_server(vm)

workers = []
for _ in range(NEGATIVE_TEST_CONNECTION_COUNT):
Expand All @@ -78,9 +76,22 @@ def negative_test_host_connections(vm, uds_path, blob_path, blob_hash):
# Should fail if Firecracker exited from SIGPIPE handler.
assert ecode == 0

metrics = vm.flush_metrics()
# Validate that at least 1 `SIGPIPE` signal was received.
# Since we are reusing the existing echo server which triggers
# reads/writes on the UDS backend connections, these might be closed
# before a read() or a write() is about to be performed by the emulation.
# The test uses 100 connections it is enough to close at least one
# before write().
#
# If this ever fails due to 100 closes before read() we must
# add extra tooling that will trigger only writes().
assert metrics["signals"]["sigpipe"] > 0

# Validate vsock emulation still accepts connections and works
# as expected.
check_host_connections(vm, uds_path, blob_path, blob_hash)
# as expected. Use the default blob size to speed up the test.
blob_path, blob_hash = make_blob(os.path.dirname(blob_path))
check_host_connections(uds_path, blob_path, blob_hash)


def test_vsock_epipe(test_microvm_with_api, bin_vsock_path, test_fc_session_root_path):
Expand All @@ -102,22 +113,9 @@ def test_vsock_epipe(test_microvm_with_api, bin_vsock_path, test_fc_session_root
# Guest-initiated connections (echo workers) will use this blob.
_copy_vsock_data_to_guest(vm.ssh, blob_path, vm_blob_path, bin_vsock_path)

path = os.path.join(vm.jailer.chroot_path(), VSOCK_UDS_PATH)
# Negative test for host-initiated connections that
# are closed with in flight data.
negative_test_host_connections(vm, path, blob_path, blob_hash)

metrics = vm.flush_metrics()
# Validate that at least 1 `SIGPIPE` signal was received.
# Since we are reusing the existing echo server which triggers
# reads/writes on the UDS backend connections, these might be closed
# before a read() or a write() is about to be performed by the emulation.
# The test uses 100 connections it is enough to close at least one
# before write().
#
# If this ever fails due to 100 closes before read() we must
# add extra tooling that will trigger only writes().
assert metrics["signals"]["sigpipe"] > 0
negative_test_host_connections(vm, blob_path, blob_hash)


def test_vsock_transport_reset(
Expand Down Expand Up @@ -152,10 +150,7 @@ def test_vsock_transport_reset(
_copy_vsock_data_to_guest(test_vm.ssh, blob_path, vm_blob_path, bin_vsock_path)

# Start guest echo server.
path = os.path.join(test_vm.jailer.chroot_path(), VSOCK_UDS_PATH)
cmd = f"/tmp/vsock_helper echosrv -d {ECHO_SERVER_PORT}"
ecode, _, _ = test_vm.ssh.run(cmd)
assert ecode == 0
path = start_guest_echo_server(test_vm)

# Start host workers that connect to the guest server.
workers = []
Expand All @@ -176,8 +171,8 @@ def test_vsock_transport_reset(
# Whatever we send to the server, it should return the same
# value.
buf = bytearray("TEST\n".encode("utf-8"))
worker.sock.send(buf)
try:
worker.sock.send(buf)
# Arbitrary timeout, we set this so the socket won't block as
# it shouldn't receive anything.
worker.sock.settimeout(0.25)
Expand All @@ -187,7 +182,7 @@ def test_vsock_transport_reset(
assert False, "Connection not closed: response recieved '{}'".format(
response.decode("utf-8")
)
except SocketTimeout:
except (SocketTimeout, ConnectionResetError, BrokenPipeError):
assert True

# Terminate VM.
Expand All @@ -206,4 +201,4 @@ def test_vsock_transport_reset(

# Test host-initiated connections.
path = os.path.join(vm2.jailer.chroot_path(), VSOCK_UDS_PATH)
check_host_connections(vm2, path, blob_path, blob_hash)
check_host_connections(path, blob_path, blob_hash)