blob: 9af48724f6f688dd427e0a6515343cb4ee79e86b [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brun7922d412023-02-21 20:47:39 +01004// takeover is a self-contained executable which when executed loads the BMaaS
5// agent via kexec. It is intended to be called over SSH, given a binary
6// TakeoverInit message over standard input and (if all preparation work
7// completed successfully) will respond with a TakeoverResponse on standard
8// output. At that point the new kernel and agent initramfs are fully staged
9// by the current kernel.
10// The second stage which is also part of this binary, selected by an
11// environment variable, is then executed in detached mode and the main
12// takeover binary called over SSH terminates.
13// The second stage waits for 5 seconds for the main binary to exit, the SSH
14// session to be torn down and various other things before issuing the final
15// non-returning syscall which jumps into the new kernel.
16
17package main
18
19import (
20 "bytes"
21 "crypto/ed25519"
22 "crypto/rand"
23 _ "embed"
24 "errors"
25 "fmt"
26 "io"
27 "log"
28 "os"
29 "os/exec"
30 "time"
31
32 "github.com/cavaliergopher/cpio"
Lorenz Brun62f1d362023-11-14 16:18:24 +010033 "github.com/klauspost/compress/zstd"
Lorenz Brun7922d412023-02-21 20:47:39 +010034 "golang.org/x/sys/unix"
35 "google.golang.org/protobuf/proto"
36
37 "source.monogon.dev/cloud/agent/api"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020038 "source.monogon.dev/osbase/bootparam"
39 "source.monogon.dev/osbase/kexec"
Tim Windelschmidt10ef8f92024-08-13 15:35:10 +020040 netdump "source.monogon.dev/osbase/net/dump"
41 netapi "source.monogon.dev/osbase/net/proto"
Lorenz Brun7922d412023-02-21 20:47:39 +010042)
43
44//go:embed third_party/linux/bzImage
45var kernel []byte
46
Tim Windelschmidt65bf3112024-04-08 21:32:14 +020047//go:embed third_party/ucode.cpio
Lorenz Brun7922d412023-02-21 20:47:39 +010048var ucode []byte
49
Tim Windelschmidt79ffbbe2024-02-22 19:15:51 +010050//go:embed initramfs.cpio.zst
Lorenz Brun7922d412023-02-21 20:47:39 +010051var initramfs []byte
52
53// newMemfile creates a new file which is not located on a specific filesystem,
54// but is instead backed by anonymous memory.
55func newMemfile(name string, flags int) (*os.File, error) {
56 fd, err := unix.MemfdCreate(name, flags)
57 if err != nil {
58 return nil, fmt.Errorf("memfd_create failed: %w", err)
59 }
60 return os.NewFile(uintptr(fd), name), nil
61}
62
63func setupTakeover() (*api.TakeoverSuccess, error) {
64 // Read init specification from stdin.
65 initRaw, err := io.ReadAll(os.Stdin)
66 if err != nil {
67 return nil, fmt.Errorf("failed to read TakeoverInit message from stdin: %w", err)
68 }
69 var takeoverInit api.TakeoverInit
70 if err := proto.Unmarshal(initRaw, &takeoverInit); err != nil {
71 return nil, fmt.Errorf("failed to parse TakeoverInit messag from stdin: %w", err)
72 }
73
74 // Sanity check for empty TakeoverInit messages
75 if takeoverInit.BmaasEndpoint == "" {
76 return nil, errors.New("BMaaS endpoint is empty, check that a proper TakeoverInit message has been provided")
77 }
78
79 // Load data from embedded files into memfiles as the kexec load syscall
80 // requires file descriptors.
81 kernelFile, err := newMemfile("kernel", 0)
82 if err != nil {
83 return nil, fmt.Errorf("failed to create kernel memfile: %w", err)
84 }
85 initramfsFile, err := newMemfile("initramfs", 0)
86 if err != nil {
87 return nil, fmt.Errorf("failed to create initramfs memfile: %w", err)
88 }
89 if _, err := kernelFile.ReadFrom(bytes.NewReader(kernel)); err != nil {
90 return nil, fmt.Errorf("failed to read kernel into memory-backed file: %w", err)
91 }
92 if _, err := initramfsFile.ReadFrom(bytes.NewReader(ucode)); err != nil {
93 return nil, fmt.Errorf("failed to read ucode into memory-backed file: %w", err)
94 }
95 if _, err := initramfsFile.ReadFrom(bytes.NewReader(initramfs)); err != nil {
96 return nil, fmt.Errorf("failed to read initramfs into memory-backed file: %w", err)
97 }
98
99 // Dump the current network configuration
100 netconf, warnings, err := netdump.Dump()
101 if err != nil {
102 return nil, fmt.Errorf("failed to dump network configuration: %w", err)
103 }
104
Lorenz Brund0be3712023-04-11 13:22:25 +0200105 if len(netconf.Nameserver) == 0 {
106 netconf.Nameserver = []*netapi.Nameserver{{
107 Ip: "8.8.8.8",
108 }, {
109 Ip: "1.1.1.1",
110 }}
111 }
112
Lorenz Brun7922d412023-02-21 20:47:39 +0100113 // Generate agent private key
114 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
115 if err != nil {
116 return nil, fmt.Errorf("unable to generate Ed25519 key: %w", err)
117 }
118
119 agentInit := api.AgentInit{
120 TakeoverInit: &takeoverInit,
121 PrivateKey: privKey,
122 NetworkConfig: netconf,
123 }
124 agentInitRaw, err := proto.Marshal(&agentInit)
125 if err != nil {
Tim Windelschmidt327cdba2024-05-21 13:51:32 +0200126 return nil, fmt.Errorf("unable to marshal AgentInit message: %w", err)
Lorenz Brun7922d412023-02-21 20:47:39 +0100127 }
128
129 // Append AgentInit spec to initramfs
Lorenz Brun62f1d362023-11-14 16:18:24 +0100130 compressedW, err := zstd.NewWriter(initramfsFile, zstd.WithEncoderLevel(1))
131 if err != nil {
132 return nil, fmt.Errorf("while creating zstd writer: %w", err)
133 }
134 cpioW := cpio.NewWriter(compressedW)
Lorenz Brun7922d412023-02-21 20:47:39 +0100135 cpioW.WriteHeader(&cpio.Header{
136 Name: "/init.pb",
137 Size: int64(len(agentInitRaw)),
138 Mode: cpio.TypeReg | 0o644,
139 })
140 cpioW.Write(agentInitRaw)
141 cpioW.Close()
Lorenz Brun62f1d362023-11-14 16:18:24 +0100142 compressedW.Close()
Lorenz Brun7922d412023-02-21 20:47:39 +0100143
144 agentParams := bootparam.Params{
145 bootparam.Param{Param: "quiet"},
146 bootparam.Param{Param: "init", Value: "/init"},
Lorenz Brun7922d412023-02-21 20:47:39 +0100147 }
148
Lorenz Brun5d503b32023-04-11 13:20:23 +0200149 var customConsoles bool
Lorenz Brun7922d412023-02-21 20:47:39 +0100150 cmdline, err := os.ReadFile("/proc/cmdline")
151 if err != nil {
152 warnings = append(warnings, fmt.Errorf("unable to read current kernel command line: %w", err))
153 } else {
154 params, _, err := bootparam.Unmarshal(string(cmdline))
155 // If the existing command line is well-formed, add all existing console
156 // parameters to the console for the agent
157 if err == nil {
158 for _, p := range params {
159 if p.Param == "console" {
160 agentParams = append(agentParams, p)
Lorenz Brun5d503b32023-04-11 13:20:23 +0200161 customConsoles = true
Lorenz Brun7922d412023-02-21 20:47:39 +0100162 }
163 }
164 }
165 }
Lorenz Brun5d503b32023-04-11 13:20:23 +0200166 if !customConsoles {
167 // Add the "default" console on x86
168 agentParams = append(agentParams, bootparam.Param{Param: "console", Value: "ttyS0,115200"})
169 }
Lorenz Brun7922d412023-02-21 20:47:39 +0100170 agentCmdline, err := bootparam.Marshal(agentParams, "")
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200171 if err != nil {
172 return nil, fmt.Errorf("failed to marshal agent params: %w", err)
173 }
Lorenz Brun7922d412023-02-21 20:47:39 +0100174 // Stage agent payload into kernel memory
175 if err := kexec.FileLoad(kernelFile, initramfsFile, agentCmdline); err != nil {
176 return nil, fmt.Errorf("failed to load kexec payload: %w", err)
177 }
178 var warningsStrs []string
179 for _, w := range warnings {
180 warningsStrs = append(warningsStrs, w.Error())
181 }
182 return &api.TakeoverSuccess{
183 InitMessage: &takeoverInit,
184 Key: pubKey,
185 Warning: warningsStrs,
186 }, nil
187}
188
189// Environment variable which tells the takeover binary to run the second stage
190const detachedLaunchEnv = "TAKEOVER_DETACHED_LAUNCH"
191
192func main() {
193 // Check if the second stage should be executed
194 if os.Getenv(detachedLaunchEnv) == "1" {
195 // Wait 5 seconds for data to be sent, connections to be closed and
196 // syncs to be executed
197 time.Sleep(5 * time.Second)
198 // Perform kexec, this will not return unless it fails
199 err := unix.Reboot(unix.LINUX_REBOOT_CMD_KEXEC)
Tim Windelschmidt5e460a92024-04-11 01:33:09 +0200200 var msg = "takeover: reboot succeeded, but we're still runing??"
Lorenz Brun7922d412023-02-21 20:47:39 +0100201 if err != nil {
202 msg = err.Error()
203 }
204 // We have no standard output/error anymore, if this fails it's
205 // just borked. Attempt to dump the error into kmesg for manual
206 // debugging.
207 kmsg, err := os.OpenFile("/dev/kmsg", os.O_WRONLY, 0)
208 if err != nil {
209 os.Exit(2)
210 }
211 kmsg.WriteString(msg)
212 kmsg.Close()
213 os.Exit(1)
214 }
215
216 var takeoverResp api.TakeoverResponse
217 res, err := setupTakeover()
218 if err != nil {
219 takeoverResp.Result = &api.TakeoverResponse_Error{Error: &api.TakeoverError{
220 Message: err.Error(),
221 }}
222 } else {
223 takeoverResp.Result = &api.TakeoverResponse_Success{Success: res}
224 }
225 // Respond to stdout
226 takeoverRespRaw, err := proto.Marshal(&takeoverResp)
227 if err != nil {
228 log.Fatalf("failed to marshal response: %v", err)
229 }
230 if _, err := os.Stdout.Write(takeoverRespRaw); err != nil {
231 log.Fatalf("failed to write response to stdout: %v", err)
232 }
233 // Close stdout, we're done responding
234 os.Stdout.Close()
235
236 // Start second stage which waits for 5 seconds while performing
237 // final cleanup.
238 detachedCmd := exec.Command("/proc/self/exe")
239 detachedCmd.Env = []string{detachedLaunchEnv + "=1"}
240 if err := detachedCmd.Start(); err != nil {
241 log.Fatalf("failed to launch final stage: %v", err)
242 }
243 // Release the second stage so that the first stage can cleanly terminate.
244 if err := detachedCmd.Process.Release(); err != nil {
245 log.Fatalf("error releasing final stage process: %v", err)
246 }
247}