feat(monitors): expose a configurable control socket for each monitor - #850
feat(monitors): expose a configurable control socket for each monitor#850Anamika1608 wants to merge 10 commits into
Conversation
✅ Deploy Preview for urunc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
48afaad to
3e02d31
Compare
cmainas
left a comment
There was a problem hiding this comment.
Hello @Anamika1608 ,
thank you for the PR. I have added some comments in the code. Also, some generic comments:
- There is still the case where an admin configures the urunc to run the monitors without any socket. This might be for security reasons or because they do not require it. Therefore, if the socket path is not in the configuration, then there should be no socket (no default value).
- We need to cleanup the socket path, because in case of a container restart (e.g. pod) the monitor might fail to use the same path.
|
can you take a look at the new commits i have pushed, done all three changes as you said. @cmainas |
cmainas
left a comment
There was a problem hiding this comment.
Hello @Anamika1608 ,
thank you for the updates. I have added some more comments. One thing that we need to have in mind is that the socket path can be anywhere, but it should still be in an accessible places for any user and more importantly do not collide with any other file.
|
hi @cmainas, thanks for the review. i made all the changes. here is each one. 1. UsesControlSocket is now a method on the VMM interface. 2. The socket directory now uses 0o700 (owner only). 3. urunc removes the socket in Delete, not in Exec. 4. Firecracker comment. 5. Docs. your main note (accessible for any user, and no collision). Inside one filesystem there is also no overwrite. If a folder in the path is already a file, the make-directory step fails. If a file already sits at the socket path, the monitor fails to bind. In both cases urunc keeps the file that is there. For "accessible for any user": urunc makes the folder as the monitor's user. A folder that is open to all users, such as one thing i want to ask. tested live (aarch64, real containerd and nerdctl):
|
Expose each monitor's control socket in the normal boot flow, so the runtime can keep talking to the VMM after the guest starts. Every monitor boots exactly as before; the only change is that its control socket stays open and reachable: - Firecracker launches with --api-sock instead of --no-api, keeping --config-file so the guest still boots from the config file. - QEMU exposes a QMP Unix socket in server mode, configured not to wait for a client before booting, alongside the disabled human monitor. - Cloud Hypervisor exposes its REST API socket (--api-socket). The socket location is configurable through a new socket_path option under a monitor's configuration, wired through MonitorConfig, ExecArgs and the state.json annotation passthrough, with a per-container default of /tmp/<id>.sock behind a DefaultSocketDir constant and a shared resolveSocketPath helper. After changeRoot, urunc creates the socket path's directory inside the monitor rootfs, so any custom path works; it fails only if the location is invalid, such as a file already existing at one of the path's components. Extend the QEMU BuildExecCmd tests to cover the new argument and document the socket_path option. Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
Remove the default /tmp/<id>.sock socket path. A monitor now gets a control socket only when socket_path is set in config; with no socket_path it launches with no control socket, exactly like upstream. Firecracker restores --no-api in that case. Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
Move the control socket directory creation from between changeRoot and setupUser to right after setupUser, so it runs as the monitor's user and a non-root monitor can create and use it. Also remove a stale socket left at the same path by a previous instance before the monitor binds, so a restart reusing the same socket_path does not fail to bind. Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
The QEMU QMP flag string "server,nowait" trips cspell in qemu.go and qemu_test.go. Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
Address the review on the control-socket configuration: - Make UsesControlSocket a method on the VMM interface instead of a switch over the monitor type in vmm.go. Every monitor now declares whether it exposes a control socket, so a new monitor cannot be added without deciding this (the switch could be silently forgotten). Qemu, Firecracker and Cloud Hypervisor return true; hvt, spt and hedge return false. - Create the control socket directory with 0o700 instead of 0o755. The directory is owned by the monitor's user and only the monitor needs it. - Move the stale socket removal out of Exec into Delete, where the rest of the container teardown happens. Delete removes the socket at its real path inside the monitor rootfs, and only when it is actually a socket, so a misconfigured socket_path over a regular file is never deleted. Removing the unlink from Exec also means the monitor now fails to bind on a pre-existing file instead of urunc deleting it. - Move the Firecracker two-mode comment out of the FIXME block to right before the if/else and trim it. Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
Drop the claim that socket_path can be anywhere and use third person (remove "for you"). Document that urunc creates the directory after it drops privileges to the monitor's user, so the parent must be writable by that user and the path must not sit over an existing file. Note that urunc removes the socket on delete, so a restart reusing the same path does not find a stale socket. Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
4e0bebb to
cd63c77
Compare
|
Hello @Anamika1608 , thank you for the update.
In the case of block/initrd/no rootfs,
As far as I concern, a docker / nerdctl stop will result to delete. I am not sure if there is any case where a stop does not go over delete. Maybe I am missing something. |
|
hi @cmainas, thanks. both points are right. i checked each in the VM. here is what i found. 1. collision with a file in the image rootfs. You are correct, and it is worse. i tested QEMU directly. When the QMP socket path already holds a regular file, QEMU does not fail. It tries to delete the file first, then bind. i saw this when the file was root-owned and QEMU ran as a normal user: So QEMU calls 2. does a stop go through delete? i tested this on containerd + nerdctl. A stop does NOT call urunc delete. i watched urunc's state directory
So delete runs on So delete-only cleanup does not cover a stop/start when proposed fix (covers both, and does not depend on the monitor). Before the monitor starts, when
i keep the delete-time cleanup too, for the normal Does this work for you? If yes, i will add the check before launch and update the docs. |
|
Hello @Anamika1608 , the container start process should not perform any cleanup. The environment should be clean enough to not create issues. Therefore, if delete operation does not cover all cases (nerdctl), then kill will. However, this will be tricky. In the future, we will change kill to perform a API call to the socket, therefore we need the socket. So, we should remove it after we perform the respective API call. |
A docker/nerdctl stop goes through urunc kill with SIGTERM and never runs delete, so the control socket was left behind until the next delete. Remove it on a terminating signal (SIGTERM or SIGKILL) as well, while the monitor is still alive and its socket is reachable at its real path inside the monitor rootfs. Factor the removal into a shared helper used by both the kill path (Signal) and Delete: it resolves the monitor rootfs, and removes the file only when it is an actual socket, so a misconfigured socket_path over a regular file is never deleted. The start path performs no cleanup. The removal in Signal is best-effort and never blocks the kill. Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
afcb148 to
0ddb627
Compare
Trim the multi-line comments added for the control socket work down to one or two short sentences. No code change. Signed-off-by: Anamika Aggarwal <anamikaagg18@gmail.com>
|
hi @cmainas , check the changes now? all good to go? |
cmainas
left a comment
There was a problem hiding this comment.
Hello @Anamika1608 , thank you for the changes. I have some more comments:
UsesControlSocketshould be renamed toSupportsControlSocketto match the other similar methods.- nit: please avoid using comments like "this function is called form", because they will become stale as soon as the function gets called form another place.
| // UsesControlSocket reports whether the monitor exposes a control socket | ||
| // (set through socket_path). |
There was a problem hiding this comment.
nit: unnecessary comment. Name is self-explanatory.
| if rmErr := u.removeControlSocket(vmm, vmmType); rmErr != nil { | ||
| uniklog.Warnf("failed to remove control socket: %v", rmErr) | ||
| } | ||
| } |
There was a problem hiding this comment.
The removal f the socket must take place after the monitor process receives the signal.
| // isLethalSignal reports whether the signal stops the container. Only SIGKILL | ||
| // and SIGTERM count (the signals a stop sends). | ||
| func isLethalSignal(signal unix.Signal) bool { | ||
| return signal == unix.SIGKILL || signal == unix.SIGTERM | ||
| } |
There was a problem hiding this comment.
unnecessary function, please inline.
| // removeControlSocket deletes the monitor's control socket, if one is set. It | ||
| // skips a missing path and never deletes a non-socket file. Signal (on a | ||
| // lethal signal) and Delete both use it. | ||
| func (u *Unikontainer) removeControlSocket(vmm types.VMM, vmmType string) error { |
There was a problem hiding this comment.
We can reduce the arguments in this function by simply moving the first logic in the caller. The caller already has a vmm instance.
| @@ -50,6 +50,12 @@ func (ch *CloudHypervisor) UsesKVM() bool { | |||
| } | |||
|
|
|||
| // SupportsSharedfs returns true as Cloud Hypervisor supports virtiofs | |||
There was a problem hiding this comment.
This comment should be moved over SupportsSharedfs
| @@ -792,13 +804,65 @@ func setupUser(user specs.User) error { | |||
| } | |||
|
|
|||
| // Signal sends a specified signal to container's init. | |||
There was a problem hiding this comment.
This comment should be above Signal function.
| if socketPath == "" || !vmm.UsesControlSocket() { | ||
| return nil | ||
| } | ||
| sockRealPath := filepath.Join(u.monitorRootfs(), socketPath) |
Description
Expose each monitor's control socket in the normal (config-file / CLI) boot flow, so the runtime can talk to the VMM after the guest starts (graceful shutdown now, snapshots later). Each monitor still boots exactly as before — this only keeps its control socket open and reachable:
--api-sockwhile keeping--config-file, so the guest still boots from the config file and the socket stays open (drops only--no-api).-qmp unix:<path>,server,nowaitalongside the disabled human monitor.--api-socket path=<path>.The socket path is configurable via a
socket_pathmonitor field, shared across all monitors through a commonResolveSocketPathhelper, defaulting to a per-container/tmp/<id>.sock. AfterchangeRoot, urunc creates the socket path's directory inside the monitor rootfs so a custom path works; it fails only if the location is invalid (a file already exists on the path).Related issues
How was this tested?
go build ./...,go test ./pkg/... ./internal/...,gofmt,make lint, and cspell pass; the QEMUBuildExecCmdtests cover the new-qmpargument (Ubuntu 24.04 aarch64 VM, KVM).socket_path; QEMU (chttp-qemu-linux-aarch64) boots, the guest serves HTTP 200, launches with-qmp unix:/tmp/qemu.sock,server,nowait, and the QMP socket answersquery-statuswithrunning.LLM usage
claude code (opus 4.8) for the understanding of codebase, approach decisions and reviews
Checklist
make lint).make test_ctr,make test_nerdctl,make test_docker,make test_crictl).