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