empeira 0.1.0.pre.alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +661 -0
  3. data/README.md +150 -0
  4. data/bin/empeira +18 -0
  5. data/config/defaults.yaml +229 -0
  6. data/docs/addons.md +331 -0
  7. data/docs/architecture.md +277 -0
  8. data/docs/configuration.md +1204 -0
  9. data/docs/development.md +472 -0
  10. data/docs/hiera-and-eyaml.md +361 -0
  11. data/docs/installation.md +280 -0
  12. data/docs/networking.md +362 -0
  13. data/docs/nodes.md +642 -0
  14. data/docs/proxy.md +136 -0
  15. data/docs/troubleshooting.md +361 -0
  16. data/lib/empeira/addons/environment.rb +82 -0
  17. data/lib/empeira/addons/error.rb +14 -0
  18. data/lib/empeira/addons/installation.rb +119 -0
  19. data/lib/empeira/addons/manifest.rb +112 -0
  20. data/lib/empeira/addons/service.rb +50 -0
  21. data/lib/empeira/addons/storage.rb +94 -0
  22. data/lib/empeira/agent/acquisition.rb +120 -0
  23. data/lib/empeira/agent/artifact.rb +41 -0
  24. data/lib/empeira/agent/authentication.rb +122 -0
  25. data/lib/empeira/agent/cache.rb +122 -0
  26. data/lib/empeira/agent/cache_storage.rb +82 -0
  27. data/lib/empeira/agent/download.rb +84 -0
  28. data/lib/empeira/agent/native_commands.rb +84 -0
  29. data/lib/empeira/agent/release_sources.rb +64 -0
  30. data/lib/empeira/agent/target.rb +66 -0
  31. data/lib/empeira/agent/versions.rb +29 -0
  32. data/lib/empeira/application.rb +86 -0
  33. data/lib/empeira/build.json +5 -0
  34. data/lib/empeira/build_info.rb +46 -0
  35. data/lib/empeira/cli/addons.rb +45 -0
  36. data/lib/empeira/cli/base.rb +73 -0
  37. data/lib/empeira/cli/completion.rb +42 -0
  38. data/lib/empeira/cli/config.rb +22 -0
  39. data/lib/empeira/cli/main.rb +114 -0
  40. data/lib/empeira/cli/node.rb +87 -0
  41. data/lib/empeira/cli/progress.rb +169 -0
  42. data/lib/empeira/cli/proxy.rb +36 -0
  43. data/lib/empeira/cli/update.rb +23 -0
  44. data/lib/empeira/completion/addons.rb +93 -0
  45. data/lib/empeira/completion/bash.rb +62 -0
  46. data/lib/empeira/completion/catalog.rb +170 -0
  47. data/lib/empeira/configuration/additional_services.rb +74 -0
  48. data/lib/empeira/configuration/addons.rb +85 -0
  49. data/lib/empeira/configuration/agent_schema.rb +163 -0
  50. data/lib/empeira/configuration/agent_values.rb +54 -0
  51. data/lib/empeira/configuration/bootstrap_guests.rb +42 -0
  52. data/lib/empeira/configuration/command_mocks.rb +65 -0
  53. data/lib/empeira/configuration/display.rb +20 -0
  54. data/lib/empeira/configuration/dns_rewrites.rb +58 -0
  55. data/lib/empeira/configuration/document.rb +67 -0
  56. data/lib/empeira/configuration/eyaml.rb +46 -0
  57. data/lib/empeira/configuration/hiera_modulepath.rb +40 -0
  58. data/lib/empeira/configuration/hiera_mount_schema.rb +62 -0
  59. data/lib/empeira/configuration/hiera_mounts.rb +83 -0
  60. data/lib/empeira/configuration/image_schema.rb +68 -0
  61. data/lib/empeira/configuration/loader.rb +70 -0
  62. data/lib/empeira/configuration/merge.rb +48 -0
  63. data/lib/empeira/configuration/network_redirects.rb +73 -0
  64. data/lib/empeira/configuration/node_images.rb +64 -0
  65. data/lib/empeira/configuration/schema.rb +215 -0
  66. data/lib/empeira/configuration/server_mounts.rb +106 -0
  67. data/lib/empeira/configuration/service_configuration.rb +40 -0
  68. data/lib/empeira/configuration/ssh_preferences.rb +83 -0
  69. data/lib/empeira/configuration/user.rb +46 -0
  70. data/lib/empeira/configuration/vm_interfaces.rb +147 -0
  71. data/lib/empeira/control_plane/additional_configurations.rb +106 -0
  72. data/lib/empeira/control_plane/browser.rb +73 -0
  73. data/lib/empeira/control_plane/controller.rb +763 -0
  74. data/lib/empeira/control_plane/discovery.rb +69 -0
  75. data/lib/empeira/control_plane/environment.rb +101 -0
  76. data/lib/empeira/control_plane/files.rb +263 -0
  77. data/lib/empeira/control_plane/health.rb +104 -0
  78. data/lib/empeira/control_plane/plan.rb +356 -0
  79. data/lib/empeira/control_plane/storage.rb +48 -0
  80. data/lib/empeira/errors.rb +9 -0
  81. data/lib/empeira/execution/console.rb +45 -0
  82. data/lib/empeira/execution/diagnostics.rb +210 -0
  83. data/lib/empeira/execution/launch_error.rb +14 -0
  84. data/lib/empeira/execution/result.rb +11 -0
  85. data/lib/empeira/execution/runner.rb +235 -0
  86. data/lib/empeira/execution/terminal.rb +35 -0
  87. data/lib/empeira/hostname_pattern.rb +14 -0
  88. data/lib/empeira/images/configuration.rb +44 -0
  89. data/lib/empeira/images/identity.rb +30 -0
  90. data/lib/empeira/images/recipe.rb +22 -0
  91. data/lib/empeira/images/reference.rb +43 -0
  92. data/lib/empeira/images/source.rb +19 -0
  93. data/lib/empeira/immutable.rb +19 -0
  94. data/lib/empeira/infrastructure/cleanup.rb +328 -0
  95. data/lib/empeira/infrastructure/definition.rb +59 -0
  96. data/lib/empeira/infrastructure/owned_files.rb +63 -0
  97. data/lib/empeira/infrastructure/recovery_inventory.rb +53 -0
  98. data/lib/empeira/infrastructure/service.rb +237 -0
  99. data/lib/empeira/infrastructure/status.rb +73 -0
  100. data/lib/empeira/infrastructure/store.rb +274 -0
  101. data/lib/empeira/modules/events.rb +67 -0
  102. data/lib/empeira/modules/git_safety.rb +60 -0
  103. data/lib/empeira/modules/host_git.rb +91 -0
  104. data/lib/empeira/modules/image.rb +27 -0
  105. data/lib/empeira/modules/installer.rb +67 -0
  106. data/lib/empeira/modules/request.rb +68 -0
  107. data/lib/empeira/modules/state.rb +87 -0
  108. data/lib/empeira/modules/storage.rb +35 -0
  109. data/lib/empeira/network/bootstrap_proxy.rb +157 -0
  110. data/lib/empeira/network/definition.rb +61 -0
  111. data/lib/empeira/network/direct_egress.rb +133 -0
  112. data/lib/empeira/network/egress.rb +27 -0
  113. data/lib/empeira/network/gateway.rb +79 -0
  114. data/lib/empeira/network/host_policy.rb +53 -0
  115. data/lib/empeira/network/naming.rb +28 -0
  116. data/lib/empeira/network/peer/adapter_image.rb +32 -0
  117. data/lib/empeira/network/peer/allocation.rb +45 -0
  118. data/lib/empeira/network/peer/attachment.rb +27 -0
  119. data/lib/empeira/network/peer/backend.rb +108 -0
  120. data/lib/empeira/network/peer/channel.rb +133 -0
  121. data/lib/empeira/network/peer/channel_server.rb +71 -0
  122. data/lib/empeira/network/peer/docker_adapter.rb +165 -0
  123. data/lib/empeira/network/peer/helper_security.rb +40 -0
  124. data/lib/empeira/network/peer/layout.rb +55 -0
  125. data/lib/empeira/network/peer/linux_podman.rb +114 -0
  126. data/lib/empeira/network/peer/podman_machine.rb +112 -0
  127. data/lib/empeira/network/policy.rb +39 -0
  128. data/lib/empeira/network/proxy_bindings.rb +52 -0
  129. data/lib/empeira/network/proxy_policy.rb +64 -0
  130. data/lib/empeira/network/redirects.rb +89 -0
  131. data/lib/empeira/network/resource.rb +50 -0
  132. data/lib/empeira/node/agent_installer.rb +31 -0
  133. data/lib/empeira/node/agent_package.rb +125 -0
  134. data/lib/empeira/node/agent_repository.rb +160 -0
  135. data/lib/empeira/node/apt_configuration.rb +85 -0
  136. data/lib/empeira/node/bootstrap.rb +17 -0
  137. data/lib/empeira/node/certificates.rb +98 -0
  138. data/lib/empeira/node/command_mock.rb +38 -0
  139. data/lib/empeira/node/command_mocks.rb +114 -0
  140. data/lib/empeira/node/container.rb +134 -0
  141. data/lib/empeira/node/container_agent_bootstrap.rb +43 -0
  142. data/lib/empeira/node/container_bootstrap.rb +52 -0
  143. data/lib/empeira/node/container_guest.rb +42 -0
  144. data/lib/empeira/node/container_lifecycle.rb +145 -0
  145. data/lib/empeira/node/container_preparation.rb +144 -0
  146. data/lib/empeira/node/container_state.rb +129 -0
  147. data/lib/empeira/node/definition.rb +20 -0
  148. data/lib/empeira/node/diagnostic_command.rb +19 -0
  149. data/lib/empeira/node/dnf_agent_repository.rb +202 -0
  150. data/lib/empeira/node/external_fact.rb +21 -0
  151. data/lib/empeira/node/image.rb +38 -0
  152. data/lib/empeira/node/interactive_tools.rb +33 -0
  153. data/lib/empeira/node/interface.rb +18 -0
  154. data/lib/empeira/node/inventory.rb +143 -0
  155. data/lib/empeira/node/package_bootstrap.rb +129 -0
  156. data/lib/empeira/node/package_proxy.rb +102 -0
  157. data/lib/empeira/node/package_sources.rb +19 -0
  158. data/lib/empeira/node/puppet_command.rb +12 -0
  159. data/lib/empeira/node/rpm_configuration.rb +62 -0
  160. data/lib/empeira/node/run_request.rb +52 -0
  161. data/lib/empeira/node/runtime_proxy.rb +86 -0
  162. data/lib/empeira/node/service.rb +72 -0
  163. data/lib/empeira/node/ssh_client.rb +25 -0
  164. data/lib/empeira/node/ssh_credentials.rb +78 -0
  165. data/lib/empeira/node/ssh_endpoint.rb +58 -0
  166. data/lib/empeira/node/ssh_identity.rb +37 -0
  167. data/lib/empeira/node/user_ssh.rb +60 -0
  168. data/lib/empeira/node/vm.rb +89 -0
  169. data/lib/empeira/node/vm_bootstrap.rb +53 -0
  170. data/lib/empeira/node/vm_lifecycle.rb +212 -0
  171. data/lib/empeira/node/vm_preflight.rb +37 -0
  172. data/lib/empeira/node/vm_preparation.rb +161 -0
  173. data/lib/empeira/node/vm_state.rb +161 -0
  174. data/lib/empeira/platform/facts.rb +66 -0
  175. data/lib/empeira/platform/kvm.rb +91 -0
  176. data/lib/empeira/platform/locations.rb +51 -0
  177. data/lib/empeira/platform/resolvers.rb +85 -0
  178. data/lib/empeira/platform/routes.rb +50 -0
  179. data/lib/empeira/progress.rb +70 -0
  180. data/lib/empeira/project_root.rb +47 -0
  181. data/lib/empeira/providers/lifecycle.rb +96 -0
  182. data/lib/empeira/providers/registry.rb +21 -0
  183. data/lib/empeira/providers/resource.rb +32 -0
  184. data/lib/empeira/providers.rb +55 -0
  185. data/lib/empeira/runtime/agent_helper.rb +34 -0
  186. data/lib/empeira/runtime/container.rb +184 -0
  187. data/lib/empeira/runtime/diagnostic_execution.rb +52 -0
  188. data/lib/empeira/runtime/docker.rb +87 -0
  189. data/lib/empeira/runtime/image_metadata.rb +83 -0
  190. data/lib/empeira/runtime/interface.rb +8 -0
  191. data/lib/empeira/runtime/mount_probe.rb +57 -0
  192. data/lib/empeira/runtime/podman.rb +122 -0
  193. data/lib/empeira/runtime/recipe_build.rb +103 -0
  194. data/lib/empeira/runtime/registry_diagnostics.rb +23 -0
  195. data/lib/empeira/runtime/service_arguments.rb +55 -0
  196. data/lib/empeira/runtime/service_images.rb +118 -0
  197. data/lib/empeira/runtime/service_inspection.rb +159 -0
  198. data/lib/empeira/runtime/services.rb +120 -0
  199. data/lib/empeira/runtime/update_helper.rb +97 -0
  200. data/lib/empeira/runtime/workspace_network.rb +69 -0
  201. data/lib/empeira/server/environment_configuration.rb +53 -0
  202. data/lib/empeira/server/http.rb +38 -0
  203. data/lib/empeira/server/puppetdb_runtime.rb +74 -0
  204. data/lib/empeira/server/relay_certificate.rb +76 -0
  205. data/lib/empeira/server/runtime.rb +90 -0
  206. data/lib/empeira/services/definition.rb +42 -0
  207. data/lib/empeira/updates/images.rb +104 -0
  208. data/lib/empeira/updates/modules.rb +45 -0
  209. data/lib/empeira/updates/service.rb +45 -0
  210. data/lib/empeira/version_requirement.rb +23 -0
  211. data/lib/empeira/vm/agent.rb +66 -0
  212. data/lib/empeira/vm/bootstrap_requirements.rb +69 -0
  213. data/lib/empeira/vm/certificate_runtime.rb +30 -0
  214. data/lib/empeira/vm/cloud_init.rb +171 -0
  215. data/lib/empeira/vm/disk.rb +136 -0
  216. data/lib/empeira/vm/guest.rb +123 -0
  217. data/lib/empeira/vm/guest_channel.rb +110 -0
  218. data/lib/empeira/vm/guest_network.rb +94 -0
  219. data/lib/empeira/vm/guest_output.rb +52 -0
  220. data/lib/empeira/vm/guest_shell.rb +79 -0
  221. data/lib/empeira/vm/guest_upload.rb +43 -0
  222. data/lib/empeira/vm/image_cache.rb +77 -0
  223. data/lib/empeira/vm/image_source.rb +91 -0
  224. data/lib/empeira/vm/interface.rb +8 -0
  225. data/lib/empeira/vm/interface_ownership.rb +124 -0
  226. data/lib/empeira/vm/interface_routes.rb +101 -0
  227. data/lib/empeira/vm/interfaces.rb +240 -0
  228. data/lib/empeira/vm/management.rb +73 -0
  229. data/lib/empeira/vm/prerequisites.rb +42 -0
  230. data/lib/empeira/vm/qemu.rb +83 -0
  231. data/lib/empeira/vm/qemu_runtime.rb +404 -0
  232. data/lib/empeira/vm/removal.rb +30 -0
  233. data/lib/empeira/vm/root_disk.rb +92 -0
  234. data/lib/empeira/vm/session_lock.rb +56 -0
  235. data/lib/empeira/vm/system_ssh.rb +29 -0
  236. data/lib/empeira/workspace.rb +42 -0
  237. data/lib/empeira.rb +209 -0
  238. data/resources/browser/relay.rb +22 -0
  239. data/resources/gateway/Containerfile +8 -0
  240. data/resources/gateway/bridge.rb +66 -0
  241. data/resources/gateway/configure.rb +323 -0
  242. data/resources/modules/Containerfile +15 -0
  243. data/resources/modules/Gemfile +5 -0
  244. data/resources/modules/Gemfile.lock +105 -0
  245. data/resources/modules/worker.rb +161 -0
  246. data/resources/network/adapter/Containerfile +13 -0
  247. data/resources/network/adapter/bridge_linux.go +51 -0
  248. data/resources/network/adapter/framing.go +45 -0
  249. data/resources/network/adapter/framing_test.go +53 -0
  250. data/resources/network/adapter/go.mod +3 -0
  251. data/resources/network/adapter/main_linux.go +273 -0
  252. data/resources/network/channel.rb +18 -0
  253. data/resources/network/ssh_connect.rb +32 -0
  254. data/resources/nodes/apt_sources.rb +66 -0
  255. data/resources/nodes/debian/Containerfile +10 -0
  256. data/resources/nodes/diagnostic_exec.rb +109 -0
  257. data/resources/nodes/interactive_tools.rb +139 -0
  258. data/resources/nodes/managed_file.rb +91 -0
  259. data/resources/nodes/management/guest_agent.py +415 -0
  260. data/resources/nodes/management/setup.sh +20 -0
  261. data/resources/nodes/management/shell_rc.sh +12 -0
  262. data/resources/nodes/management/system_account.sh +24 -0
  263. data/resources/nodes/management/system_home.sh +15 -0
  264. data/resources/nodes/rpm/Containerfile +10 -0
  265. data/resources/nodes/runtime_proxy.rb +244 -0
  266. data/resources/nodes/ssh/proxy.rb +16 -0
  267. data/resources/nodes/ssh/sshd_config +15 -0
  268. data/resources/nodes/ssh/start +6 -0
  269. data/resources/proxy/Containerfile +10 -0
  270. data/resources/relay/Containerfile +5 -0
  271. data/resources/relay/puppetdb.rb +81 -0
  272. data/resources/relay/server_environment.rb +10 -0
  273. data/resources/relay/server_start.sh +41 -0
  274. metadata +326 -0
@@ -0,0 +1,273 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Linux endpoint for an authenticated runtime/SSH binary exec channel.
3
+ package main
4
+
5
+ import (
6
+ "crypto/sha256"
7
+ "fmt"
8
+ "io"
9
+ "net"
10
+ "os"
11
+ "os/signal"
12
+ "path/filepath"
13
+ "strconv"
14
+ "strings"
15
+ "syscall"
16
+ "time"
17
+ "unsafe"
18
+ )
19
+
20
+ func ioctl(fd int, operation uintptr, value unsafe.Pointer) error {
21
+ _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), operation, uintptr(value))
22
+ if errno != 0 {
23
+ return errno
24
+ }
25
+ return nil
26
+ }
27
+
28
+ func ifreq(name string) ([40]byte, error) {
29
+ var request [40]byte
30
+ if len(name) == 0 || len(name) > 15 {
31
+ return request, fmt.Errorf("invalid interface name")
32
+ }
33
+ copy(request[:16], name)
34
+ return request, nil
35
+ }
36
+
37
+ func tap(name, bridge string) (*os.File, error) {
38
+ request, err := ifreq(name)
39
+ if err != nil {
40
+ return nil, err
41
+ }
42
+ f, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0)
43
+ if err != nil {
44
+ return nil, err
45
+ }
46
+ // TUN_EXCL prevents adopting an existing interface. No persistence: close removes it.
47
+ *(*uint16)(unsafe.Pointer(&request[16])) = 0x0002 | 0x1000 | 0x8000
48
+ if err = ioctl(int(f.Fd()), 0x400454ca, unsafe.Pointer(&request[0])); err != nil {
49
+ f.Close()
50
+ return nil, err
51
+ }
52
+ if err = attach(name, bridge); err != nil {
53
+ f.Close()
54
+ return nil, err
55
+ }
56
+ return f, nil
57
+ }
58
+
59
+ func attach(name, bridge string) error {
60
+ nic, err := net.InterfaceByName(name)
61
+ if err != nil {
62
+ return err
63
+ }
64
+ request, err := ifreq(bridge)
65
+ if err != nil {
66
+ return err
67
+ }
68
+ fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
69
+ if err != nil {
70
+ return err
71
+ }
72
+ defer syscall.Close(fd)
73
+ *(*int32)(unsafe.Pointer(&request[16])) = int32(nic.Index)
74
+ if err := ioctl(fd, 0x89a2, unsafe.Pointer(&request[0])); err != nil {
75
+ return err
76
+ } // SIOCBRADDIF
77
+ request, _ = ifreq(name)
78
+ *(*uint16)(unsafe.Pointer(&request[16])) = syscall.IFF_UP
79
+ return ioctl(fd, syscall.SIOCSIFFLAGS, unsafe.Pointer(&request[0]))
80
+ }
81
+
82
+ // Docker's endpoint owns only its container network namespace. A local bridge
83
+ // joins its veth to the TAP; the TAP kernel path completes checksum/GSO work
84
+ // before frames enter the QEMU stream. No IP/application parsing is necessary.
85
+ func localBridge(name, ethernet string) error {
86
+ if _, err := net.InterfaceByName(name); err == nil {
87
+ master, err := os.Readlink("/sys/class/net/" + ethernet + "/master")
88
+ if err != nil || filepath.Base(master) != name {
89
+ return fmt.Errorf("unexpected existing adapter bridge")
90
+ }
91
+ return nil
92
+ }
93
+
94
+ req, err := ifreq(name)
95
+ if err != nil {
96
+ return err
97
+ }
98
+ fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
99
+ if err != nil {
100
+ return err
101
+ }
102
+ defer syscall.Close(fd)
103
+ if err = ioctl(fd, 0x89a0, unsafe.Pointer(&req[0])); err != nil {
104
+ return err
105
+ }
106
+ *(*uint16)(unsafe.Pointer(&req[16])) = syscall.IFF_UP
107
+ if err = ioctl(fd, syscall.SIOCSIFFLAGS, unsafe.Pointer(&req[0])); err != nil {
108
+ return err
109
+ }
110
+ if err = attach(ethernet, name); err != nil {
111
+ return err
112
+ }
113
+ if err = disableUplinkLearning(ethernet); err != nil {
114
+ return err
115
+ }
116
+ // Keep the runtime-assigned IPv4 as a local peer endpoint on the bridge.
117
+ // Forwarding stays disabled; SSH needs no host route or external publication.
118
+ address, err := ifreq(ethernet)
119
+ if err != nil {
120
+ return err
121
+ }
122
+ if err = ioctl(fd, syscall.SIOCGIFADDR, unsafe.Pointer(&address[0])); err != nil {
123
+ return err
124
+ }
125
+ mask, err := ifreq(ethernet)
126
+ if err != nil {
127
+ return err
128
+ }
129
+ if err = ioctl(fd, syscall.SIOCGIFNETMASK, unsafe.Pointer(&mask[0])); err != nil {
130
+ return err
131
+ }
132
+ req, err = ifreq(ethernet)
133
+ if err != nil {
134
+ return err
135
+ }
136
+ *(*uint16)(unsafe.Pointer(&req[16])) = syscall.AF_INET
137
+ if err = ioctl(fd, syscall.SIOCSIFADDR, unsafe.Pointer(&req[0])); err != nil {
138
+ return err
139
+ }
140
+ bridgeAddress, err := ifreq(name)
141
+ if err != nil {
142
+ return err
143
+ }
144
+ copy(bridgeAddress[16:32], address[16:32])
145
+ if err = ioctl(fd, syscall.SIOCSIFADDR, unsafe.Pointer(&bridgeAddress[0])); err != nil {
146
+ return err
147
+ }
148
+ copy(bridgeAddress[16:32], mask[16:32])
149
+ return ioctl(fd, syscall.SIOCSIFNETMASK, unsafe.Pointer(&bridgeAddress[0]))
150
+ }
151
+
152
+ func bridgeFrames(read func([]byte) (int, error), write func([]byte) error) error {
153
+ done := make(chan error, 2)
154
+ go func() {
155
+ for {
156
+ frame, err := readFrame(os.Stdin)
157
+
158
+ if err == nil {
159
+ err = write(frame)
160
+ }
161
+ if err != nil {
162
+ done <- err
163
+ return
164
+ }
165
+ }
166
+ }()
167
+ go func() {
168
+ buf := make([]byte, maxFrame)
169
+ for {
170
+ n, err := read(buf)
171
+
172
+ if err == nil {
173
+ err = writeFrame(os.Stdout, buf[:n])
174
+ }
175
+ if err != nil {
176
+ done <- err
177
+ return
178
+ }
179
+ }
180
+ }()
181
+ return <-done
182
+ }
183
+
184
+ func run() error {
185
+ if len(os.Args) == 5 && os.Args[1] == "connect" {
186
+ address := net.ParseIP(os.Args[2])
187
+ port, err := strconv.Atoi(os.Args[3])
188
+ source := net.ParseIP(os.Args[4])
189
+ if source == nil || source.To4() == nil || !source.IsPrivate() || address == nil || address.To4() == nil || !address.IsPrivate() || err != nil || port < 1 || port > 65535 {
190
+ return fmt.Errorf("invalid private SSH destination")
191
+ }
192
+ dialer := net.Dialer{Timeout: 5 * time.Second, LocalAddr: &net.TCPAddr{IP: source}}
193
+ connection, err := dialer.Dial("tcp4", net.JoinHostPort(address.String(), os.Args[3]))
194
+ if err != nil {
195
+ return err
196
+ }
197
+ defer connection.Close()
198
+ go func() {
199
+ _, err := io.Copy(connection, os.Stdin)
200
+ if err != nil {
201
+ connection.Close()
202
+ } else {
203
+ connection.(*net.TCPConn).CloseWrite()
204
+ }
205
+ }()
206
+ _, err = io.Copy(os.Stdout, connection)
207
+ return err
208
+ }
209
+ if len(os.Args) == 2 && os.Args[1] == "capabilities" {
210
+ data, err := os.ReadFile("/proc/self/status")
211
+ if err != nil {
212
+ return err
213
+ }
214
+ for _, line := range strings.Split(string(data), "\n") {
215
+ if strings.HasPrefix(line, "CapEff:") {
216
+ fmt.Println(strings.TrimSpace(strings.TrimPrefix(line, "CapEff:")))
217
+ return nil
218
+ }
219
+ }
220
+ return fmt.Errorf("effective capabilities unavailable")
221
+ }
222
+
223
+ if len(os.Args) == 2 && os.Args[1] == "hold" {
224
+ signals := make(chan os.Signal, 1)
225
+ signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
226
+ <-signals
227
+ return nil
228
+ }
229
+ if len(os.Args) == 2 && os.Args[1] == "sha256" {
230
+ data, err := os.ReadFile("/proc/self/exe")
231
+ if err != nil {
232
+ return err
233
+ }
234
+ fmt.Printf("%x\n", sha256.Sum256(data))
235
+ return nil
236
+ }
237
+
238
+ if len(os.Args) != 4 {
239
+ return fmt.Errorf("usage: adapter tap NAME BRIDGE | bridge NAME INTERFACE")
240
+ }
241
+ if os.Args[1] == "bridge" {
242
+ return localBridge(os.Args[2], os.Args[3])
243
+ }
244
+ if os.Args[1] != "tap" {
245
+ return fmt.Errorf("unsupported mode")
246
+ }
247
+ bridge := os.Args[3]
248
+ f, err := tap(os.Args[2], bridge)
249
+ if err != nil {
250
+ return err
251
+ }
252
+ defer f.Close()
253
+ fd := int(f.Fd())
254
+ if err = syscall.SetNonblock(fd, false); err != nil {
255
+ return err
256
+ }
257
+ // Opened before TUNSETIFF, the descriptor need not have joined Go's netpoller.
258
+ // Use blocking Linux I/O consistently; EOF on the exec channel ends the process.
259
+ return bridgeFrames(func(b []byte) (int, error) { return syscall.Read(fd, b) }, func(b []byte) error {
260
+ n, e := syscall.Write(fd, b)
261
+ if e == nil && n != len(b) {
262
+ e = io.ErrShortWrite
263
+ }
264
+ return e
265
+ })
266
+ }
267
+
268
+ func main() {
269
+ if err := run(); err != nil && err != io.EOF {
270
+ fmt.Fprintln(os.Stderr, err)
271
+ os.Exit(1)
272
+ }
273
+ }
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: AGPL-3.0-only
4
+
5
+ require_relative '../../lib/empeira'
6
+
7
+ path = Pathname(ARGV.fetch(0))
8
+ metadata = path.lstat
9
+ unless metadata.file? && metadata.uid == Process.uid && metadata.mode.nobits?(0o077)
10
+ abort 'Unsafe peer channel configuration'
11
+ end
12
+ Process.daemon(false, false)
13
+ begin
14
+ Empeira::Network::Peer::ChannelServer.new(path).run
15
+ rescue StandardError => e
16
+ File.open(path.dirname.join('channel.log'), 'a', 0o600) { |log| log.puts("#{e.class}: #{e.message}") }
17
+ exit 1
18
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: AGPL-3.0-only
4
+ # SSH byte tunnel inside the rootless runtime namespace.
5
+ require 'socket'
6
+ require 'ipaddr'
7
+
8
+ address, value = ARGV.size == 1 ? ['127.0.0.1', ARGV.fetch(0)] : ARGV
9
+ ip = IPAddr.new(address)
10
+ raise ArgumentError, 'Invalid SSH destination' unless ip.ipv4? && (ip.private? || address == '127.0.0.1')
11
+
12
+ port = Integer(value, 10)
13
+ raise ArgumentError, 'Invalid SSH port' unless port.between?(1, 65_535)
14
+
15
+ begin
16
+ Socket.tcp(address, port, connect_timeout: 5) do |socket|
17
+ request = Thread.new do
18
+ IO.copy_stream($stdin, socket)
19
+ socket.close_write
20
+ rescue IOError, SystemCallError
21
+ nil
22
+ end
23
+ begin
24
+ IO.copy_stream(socket, $stdout)
25
+ ensure
26
+ request.kill.join
27
+ end
28
+ end
29
+ rescue IOError, SystemCallError => e
30
+ warn "System SSH connection failed: #{e.message}"
31
+ exit 1
32
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'uri'
5
+
6
+ module Empeira
7
+ # Read-only bootstrap check. Repository management after bootstrap belongs to Puppet.
8
+ class AptSources
9
+ def initialize(root = '/')
10
+ @root = Pathname(root)
11
+ end
12
+
13
+ def valid?
14
+ release = @root.join('etc/os-release').read
15
+ return true unless release.match?(/^ID=["']?(?:ubuntu|debian)["']?$/)
16
+
17
+ apt = @root.join('etc/apt')
18
+ lists = [apt.join('sources.list'), *apt.glob('sources.list.d/*.list')]
19
+ lists.any? { |path| path.file? && legacy?(path.read) } ||
20
+ apt.glob('sources.list.d/*.sources').any? { |path| deb822?(path.read) }
21
+ end
22
+
23
+ private
24
+
25
+ def uri?(value)
26
+ uri = URI.parse(value)
27
+ %w[http https file].include?(uri.scheme) && (uri.scheme == 'file' || !uri.host.to_s.empty?)
28
+ rescue URI::InvalidURIError
29
+ false
30
+ end
31
+
32
+ def legacy?(content)
33
+ content.lines.any? do |line|
34
+ fields = line.sub(/#.*/, '').strip.sub(/\Adeb\s+\[[^\]]*\]\s+/, 'deb ').split
35
+ fields.first == 'deb' && fields.length >= 4 && uri?(fields[1]) && fields.drop(3).include?('main')
36
+ end
37
+ end
38
+
39
+ def deb822?(content)
40
+ content.gsub(/^#.*\n?/, '').gsub(/\n[ \t]+/, ' ').split(/\n\s*\n/).any? do |paragraph|
41
+ active?(fields(paragraph))
42
+ end
43
+ end
44
+
45
+ def fields(paragraph)
46
+ paragraph.lines.filter_map do |line|
47
+ key, value = line.split(':', 2)
48
+ [key.downcase, value.strip] if value
49
+ end.to_h
50
+ end
51
+
52
+ def active?(fields)
53
+ fields.fetch('enabled', 'yes').downcase != 'no' && fields.fetch('types', '').split.include?('deb') &&
54
+ fields.fetch('uris', '').split.any? { |value| uri?(value) } &&
55
+ !fields.fetch('suites', '').empty? && fields.fetch('components', '').split.include?('main')
56
+ end
57
+ end
58
+ end
59
+
60
+ if $PROGRAM_NAME == __FILE__
61
+ begin
62
+ exit(Empeira::AptSources.new.valid? ? 0 : 1)
63
+ rescue StandardError
64
+ exit 1
65
+ end
66
+ end
@@ -0,0 +1,10 @@
1
+ ARG EMPEIRA_BASE_NODE=@BASE_IMAGE@
2
+ FROM ${EMPEIRA_BASE_NODE}
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates openssh-server curl systemd systemd-sysv dbus iproute2 iputils-ping dnsutils procps sudo plocate
5
+ COPY node-start /usr/local/libexec/empeira-node-start
6
+ COPY node-connect /usr/local/libexec/empeira-ssh-connect.rb
7
+ COPY sshd_config /etc/ssh/sshd_config
8
+ RUN chmod 0755 /usr/local/libexec/empeira-node-start && rm -f /etc/ssh/ssh_host_* && systemctl enable ssh.service
9
+ ENTRYPOINT ["/usr/local/libexec/empeira-node-start"]
10
+ CMD ["/usr/sbin/sshd", "-D", "-e"]
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: AGPL-3.0-only
4
+ # Guest-only supervisor, invoked by an owned native container exec as numeric UID 0.
5
+ require 'json'
6
+ require 'timeout'
7
+
8
+ module EmpeiraDiagnosticExec
9
+ ROOT = '/run/empeira-exec'
10
+
11
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- Both root and session directory must remain private numeric-root paths.
12
+ def self.directory(token, create: false)
13
+ raise 'Invalid diagnostic identity' unless token.match?(/\A[0-9a-f]{32}\z/)
14
+
15
+ path = File.join(ROOT, token)
16
+ [ROOT, path].each do |entry|
17
+ if create
18
+ begin
19
+ Dir.mkdir(entry, 0o700)
20
+ rescue Errno::EEXIST
21
+ raise 'Existing diagnostic session' if entry == path
22
+ end
23
+ end
24
+ metadata = File.lstat(entry)
25
+ unless metadata.directory? && metadata.uid.zero? && metadata.mode.nobits?(0o077)
26
+ raise 'Unsafe diagnostic directory'
27
+ end
28
+ end
29
+ path
30
+ end
31
+
32
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- Scan only the pinned owned process session before TERM/KILL.
33
+ def self.cleanup_groups
34
+ groups = Dir.glob('/proc/[0-9]*/stat').filter_map do |path|
35
+ fields = File.read(path).rpartition(') ').last.split
36
+ Integer(fields[2]) if Integer(fields[3]) == Process.pid && Integer(fields[2]) != Process.getpgrp
37
+ rescue Errno::ENOENT, Errno::ESRCH
38
+ nil
39
+ end.uniq
40
+ %w[TERM KILL].each do |signal|
41
+ groups.each do |group|
42
+ Process.kill(signal, -group)
43
+ rescue Errno::ESRCH
44
+ nil
45
+ end
46
+ sleep 0.1 if signal == 'TERM' && !groups.empty?
47
+ end
48
+ end
49
+
50
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- Completion is published only after interruption, timeout and owned-group cleanup.
51
+ def self.run(token, duration, arguments)
52
+ raise 'Diagnostic supervisor requires UID 0' unless Process.uid.zero?
53
+
54
+ path = directory(token, create: true)
55
+ begin
56
+ Process.setsid
57
+ rescue Errno::EPERM
58
+ raise unless Process.getsid == Process.pid
59
+ end
60
+ owner = Thread.current
61
+ monitor = Thread.new do
62
+ $stdin.read(1)
63
+ owner.raise Interrupt
64
+ end
65
+ environment = { 'PATH' => '/opt/puppetlabs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' }
66
+ pid = Process.spawn(environment, [arguments.first, arguments.first], *arguments.drop(1),
67
+ in: File::NULL, pgroup: true)
68
+ status = Timeout.timeout(duration == 'none' ? nil : Float(duration)) { Process.wait2(pid).last }
69
+ result = { 'status' => status.exitstatus || (128 + status.termsig), 'timed_out' => false }
70
+ rescue Interrupt
71
+ result = { 'status' => 130, 'timed_out' => false }
72
+ rescue Timeout::Error
73
+ result = { 'status' => nil, 'timed_out' => true }
74
+ rescue SystemCallError => e
75
+ warn "Guest command could not start (#{e.class.name})"
76
+ result = { 'status' => e.is_a?(Errno::ENOENT) ? 127 : 126, 'timed_out' => false }
77
+ ensure
78
+ monitor&.kill&.join
79
+ cleanup_groups if path && Process.getsid == Process.pid
80
+ begin
81
+ Process.wait(pid) if pid
82
+ rescue Errno::ECHILD
83
+ nil
84
+ end
85
+ if result && path
86
+ File.open(File.join(path, 'status'), File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
87
+ file.write(JSON.generate(result))
88
+ end
89
+ end
90
+ end
91
+
92
+ def self.completion(token)
93
+ path = directory(token)
94
+ file = File.join(path, 'status')
95
+ metadata = File.lstat(file)
96
+ raise 'Unsafe diagnostic completion' unless metadata.file? && metadata.uid.zero? && metadata.mode.nobits?(0o077)
97
+
98
+ print File.read(file, 1024)
99
+ File.unlink(file)
100
+ Dir.rmdir(path)
101
+ end
102
+ end
103
+
104
+ mode, token, duration, *arguments = ARGV
105
+ if mode == 'read'
106
+ EmpeiraDiagnosticExec.completion(token)
107
+ else
108
+ EmpeiraDiagnosticExec.run(token, duration, arguments)
109
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'pathname'
5
+ require 'rbconfig'
6
+ require 'shellwords'
7
+ require 'tempfile'
8
+ require 'fileutils'
9
+
10
+ module Empeira
11
+ class InteractiveToolsGuest
12
+ PROFILE = '/etc/profile.d/90-empeira-tools.sh'
13
+ BEGIN_MARKER = "# BEGIN EMPEIRA INTERACTIVE TOOLS\n"
14
+ END_MARKER = "# END EMPEIRA INTERACTIVE TOOLS\n"
15
+
16
+ def initialize(request, root: Pathname('/'))
17
+ @root = Pathname(root)
18
+ @candidates = [Pathname(request.fetch('puppet')).dirname.to_s, RbConfig::CONFIG.fetch('bindir')].uniq
19
+ end
20
+
21
+ def reconcile
22
+ directory = tool_directory
23
+ changed = profile(directory)
24
+ global_initializers.each { |target| changed = bash(target) || changed }
25
+ { 'changed' => changed, 'directory' => directory }
26
+ end
27
+
28
+ private
29
+
30
+ def tool_directory
31
+ directory = @candidates.find do |candidate|
32
+ %w[puppet facter].all? { |command| path(candidate).join(command).executable? }
33
+ end
34
+ directory || raise('Cannot locate installed puppet and facter tools together')
35
+ end
36
+
37
+ def global_initializers
38
+ initializers = %w[/etc/bash.bashrc /etc/bashrc].map { |value| path(value) }.select(&:exist?)
39
+ raise 'No supported global Bash initializer; inspect the image shell setup' if initializers.empty?
40
+
41
+ initializers
42
+ end
43
+
44
+ def path(value)
45
+ @root.join(value.delete_prefix('/'))
46
+ end
47
+
48
+ def read(target)
49
+ target.parent.ascend do |parent|
50
+ raise 'Unsafe interactive startup directory' if parent.symlink?
51
+ end
52
+ stat = target.lstat
53
+ unless stat.file? && stat.nlink == 1 && stat.uid == Process.euid && stat.mode.nobits?(0o022)
54
+ raise 'Unsafe or unowned interactive startup file'
55
+ end
56
+
57
+ target.binread
58
+ rescue Errno::ENOENT
59
+ nil
60
+ end
61
+
62
+ def write(target, content, mode: nil)
63
+ FileUtils.mkdir_p(target.parent, mode: 0o755)
64
+ mode ||= target.exist? ? target.stat.mode & 0o777 : 0o644
65
+ Tempfile.create('.empeira-tools-', target.parent) do |file|
66
+ file.chmod(mode)
67
+ file.write(content)
68
+ file.flush
69
+ file.fsync
70
+ File.rename(file.path, target)
71
+ end
72
+ end
73
+
74
+ def profile_content(directory)
75
+ <<~SH
76
+ # Managed by Empeira: installed agent tools, interactive shells only.
77
+ case $- in
78
+ *i*)
79
+ empeira_tool_bin=#{Shellwords.escape(path(directory).to_s)}
80
+ case ":${PATH-}:" in
81
+ *":${empeira_tool_bin}:"*) ;;
82
+ *) PATH="${empeira_tool_bin}${PATH:+:$PATH}"; export PATH ;;
83
+ esac
84
+ unset empeira_tool_bin
85
+ ;;
86
+ esac
87
+ SH
88
+ end
89
+
90
+ # rubocop:disable-next Naming/PredicateMethod -- Reconciliation writes an owned profile fragment.
91
+ def profile(directory)
92
+ target = path(PROFILE)
93
+ current = read(target)
94
+ if current && @candidates.none? { |candidate| current == profile_content(candidate) }
95
+ raise 'Foreign or modified /etc/profile.d/90-empeira-tools.sh; refusing to overwrite it'
96
+ end
97
+
98
+ content = profile_content(directory)
99
+ return false if current == content && (target.stat.mode & 0o777) == 0o644
100
+
101
+ write(target, content, mode: 0o644)
102
+ true
103
+ end
104
+
105
+ def hook
106
+ <<~SH
107
+ #{BEGIN_MARKER.chomp}
108
+ case $- in
109
+ *i*) . #{Shellwords.escape(path(PROFILE).to_s)} ;;
110
+ esac
111
+ #{END_MARKER.chomp}
112
+ SH
113
+ end
114
+
115
+ # rubocop:disable-next Naming/PredicateMethod -- Preserve every foreign line in the global Bash initializer.
116
+ def bash(target)
117
+ current = read(target)
118
+ if current.include?(BEGIN_MARKER) || current.include?(END_MARKER)
119
+ unless current.scan(BEGIN_MARKER).size == 1 && current.scan(END_MARKER).size == 1 && current.include?(hook)
120
+ raise 'Modified Empeira global Bash hook; refusing to overwrite it'
121
+ end
122
+
123
+ return false
124
+ end
125
+
126
+ write(target, "#{hook}#{current}")
127
+ true
128
+ end
129
+ end
130
+ end
131
+
132
+ if $PROGRAM_NAME == __FILE__
133
+ begin
134
+ puts JSON.generate(Empeira::InteractiveToolsGuest.new(JSON.parse(ARGV.fetch(0))).reconcile)
135
+ rescue StandardError => e
136
+ warn e.message
137
+ exit 1
138
+ end
139
+ end