1
+#!/usr/bin/env python3
2
+#
3
+# Copyright (c) 2025 Software Freedom Conservancy, Inc.
4
+#
5
+# Author: Yodel Eldar <yodel.eldar@yodel.dev>
6
+#
7
+# SPDX-License-Identifier: GPL-2.0-or-later
8
+"""
9
+Test vhost-user-bridge (vubr) functionality:
10
+
11
+ 1) Run vhost-user-bridge on the host.
12
+ 2) Launch a guest VM:
13
+ a) Instantiate a unix domain socket to the vubr-created path
14
+ b) Instantiate a vhost-user backend on top of that socket
15
+ c) Map a virtio-net-pci device to the vhost-user backend
16
+ d) Instantiate a UDP socket backend
17
+ e) Instantiate a user-mode net backend
18
+ i) Forward an ephemeral port to port 8080 in-guest with hostfwd=
19
+ ii) Expose a generated scratch file to the guest with tftp=
20
+ f) Hub the UDP and user-mode backends.
21
+ 3) Invoke tftp in the guest to download exported scratch file from the host.
22
+ 4) Serve a file to the host via http server in the guest.
23
+"""
24
+
25
+import os
26
+import shutil
27
+import subprocess
28
+from qemu_test import Asset, LinuxKernelTest, which
29
+from qemu_test import exec_command_and_wait_for_pattern
30
+from qemu_test import is_readable_executable_file
31
+from qemu_test import wait_for_console_pattern
32
+from qemu_test.ports import Ports
33
+
34
+class VhostUserBridge(LinuxKernelTest):
35
+
36
+ ASSET_KERNEL_INITRAMFS = Asset(
37
+ "https://github.com/yodel/vhost-user-bridge-test/raw/refs/heads/main/bzImage",
38
+ "8860d7aa59434f483542cdf25b42eacae0d4d4aa7ec923af9589d1ad4703d42b")
39
+
40
+ HOST_UUID = "ba4c2e39-627f-487d-ae3b-93cc5d783eb8"
41
+ HOST_UUID_HSUM = \
42
+ "d2932e34bf6c17b33e7325140b691e27c191d9ac4dfa550f68c09506facb09b9"
43
+
44
+ GUEST_UUID = "143d2b21-fdf0-4c5e-a9ef-f35ebbac8945"
45
+ GUEST_UUID_HSUM = \
46
+ "14b64203f5cf2afe520f8be0fdfe630aafc1e85d1301f55a0d1681e68881f3a2"
47
+
48
+ def configure_vm(self, ud_socket_path, lport, rport, hostfwd_port, tftpdir):
49
+ self.require_accelerator("kvm")
50
+ self.require_netdev("vhost-user")
51
+ self.require_netdev("socket")
52
+ self.require_netdev("hubport")
53
+ self.require_netdev("user")
54
+ self.require_device("virtio-net-pci")
55
+ self.set_machine("q35")
56
+ self.vm.add_args(
57
+ "-cpu", "host",
58
+ "-accel", "kvm",
59
+ "-append", "printk.time=0 console=ttyS0",
60
+ "-smp", "2",
61
+ "-m", "128M",
62
+ "-object", "memory-backend-memfd,id=mem0,"
63
+ "size=128M,share=on,prealloc=on",
64
+ "-numa", "node,memdev=mem0",
65
+ "-chardev", f"socket,id=char0,path={ud_socket_path}",
66
+ "-netdev", "vhost-user,id=vhost0,chardev=char0,vhostforce=on",
67
+ "-device", "virtio-net-pci,netdev=vhost0",
68
+ "-netdev", f"socket,id=udp0,udp=localhost:{lport},"
69
+ f"localaddr=localhost:{rport}",
70
+ "-netdev", "hubport,id=hub0,hubid=0,netdev=udp0",
71
+ "-netdev", f"user,id=user0,tftp={tftpdir},"
72
+ f"hostfwd=tcp:127.0.0.1:{hostfwd_port}-:8080",
73
+ "-netdev", "hubport,id=hub1,hubid=0,netdev=user0"
74
+ )
75
+
76
+ def assemble_vubr_args(self, vubr_path, ud_socket_path, lport, rport):
77
+ vubr_args = []
78
+
79
+ if (stdbuf_path := which("stdbuf")) is None:
80
+ self.log.info("Could not find stdbuf: vhost-user-bridge "
81
+ "log lines may appear out of order")
82
+ else:
83
+ vubr_args += [stdbuf_path, "-o0", "-e0"]
84
+
85
+ vubr_args += [vubr_path, "-u", f"{ud_socket_path}",
86
+ "-l", f"127.0.0.1:{lport}", "-r", f"127.0.0.1:{rport}"]
87
+
88
+ return vubr_args
89
+
90
+ def test_vhost_user_bridge(self):
91
+ prompt = "~ # "
92
+ host_uuid_filename = "vubr-test-uuid.txt"
93
+ guest_uuid_path = "/tmp/uuid.txt"
94
+ kernel_path = self.ASSET_KERNEL_INITRAMFS.fetch()
95
+
96
+ vubr_path = self.build_file("contrib", "vhost-user-bridge",
97
+ "vhost-user-bridge")
98
+ if not is_readable_executable_file(vubr_path):
99
+ self.skipTest("Could not find a readable and executable "
100
+ "vhost-user-bridge")
101
+
102
+ vubr_log_path = self.log_file("vhost-user-bridge.log")
103
+ self.log.info("For the vhost-user-bridge application log,"
104
+ f" see: {vubr_log_path}")
105
+
106
+ sock_dir = self.socket_dir()
107
+ ud_socket_path = os.path.join(sock_dir.name, "vubr-test.sock")
108
+
109
+ tftpdir = self.scratch_file("tftp")
110
+ shutil.rmtree(tftpdir, ignore_errors=True)
111
+ os.mkdir(tftpdir)
112
+ host_uuid_path = self.scratch_file("tftp", host_uuid_filename)
113
+ with open(host_uuid_path, "w", encoding="utf-8") as host_uuid_file:
114
+ host_uuid_file.write(self.HOST_UUID)
115
+
116
+ with Ports() as ports:
117
+ # pylint: disable=unbalanced-tuple-unpacking
118
+ lport, rport, hostfwd_port = ports.find_free_ports(3)
119
+
120
+ self.configure_vm(ud_socket_path, lport, rport, hostfwd_port,
121
+ tftpdir)
122
+
123
+ vubr_args = self.assemble_vubr_args(vubr_path, ud_socket_path,
124
+ lport, rport)
125
+
126
+ with open(vubr_log_path, "w", encoding="utf-8") as vubr_log, \
127
+ subprocess.Popen(vubr_args, stdin=subprocess.DEVNULL,
128
+ stdout=vubr_log,
129
+ stderr=subprocess.STDOUT) as vubr_proc:
130
+ self.launch_kernel(kernel_path, wait_for=prompt)
131
+
132
+ exec_command_and_wait_for_pattern(self,
133
+ f"tftp -g -r {host_uuid_filename} 10.0.2.2 ; "
134
+ f"sha256sum {host_uuid_filename}", self.HOST_UUID_HSUM)
135
+ wait_for_console_pattern(self, prompt)
136
+
137
+ exec_command_and_wait_for_pattern(self,
138
+ f"echo -n '{self.GUEST_UUID}' > {guest_uuid_path}", prompt)
139
+ self.check_http_download(guest_uuid_path, self.GUEST_UUID_HSUM)
140
+ wait_for_console_pattern(self, prompt)
141
+
142
+ self.vm.shutdown()
143
+ vubr_proc.terminate()
144
+ vubr_proc.wait()
145
+
146
+if __name__ == '__main__':
147
+ LinuxKernelTest.main()