|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | + |
| 3 | +package internal |
| 4 | + |
| 5 | +import ( |
| 6 | + "archive/tar" |
| 7 | + "bytes" |
| 8 | + "encoding/json" |
| 9 | + "fmt" |
| 10 | + "io" |
| 11 | + "os" |
| 12 | + "strconv" |
| 13 | + |
| 14 | + "github.com/checkpoint-restore/go-criu/v8/crit" |
| 15 | + "github.com/checkpoint-restore/go-criu/v8/crit/images/fdinfo" |
| 16 | +) |
| 17 | + |
| 18 | +// TCP_LISTEN state value from the Linux kernel |
| 19 | +const tcpListenState = 10 |
| 20 | + |
| 21 | +// remapFilesImg decodes a CRIU files.img binary image, remaps the source port |
| 22 | +// of any TCP listen socket matching oldPort to newPort, and re-encodes the image. |
| 23 | +func remapFilesImg(hdr *tar.Header, content io.Reader, oldPort, newPort uint32) (*tar.Header, []byte, error) { |
| 24 | + // crit.New requires *os.File, so write the tar entry content to a temp file |
| 25 | + tmpIn, err := os.CreateTemp("", "files-img-in-*.img") |
| 26 | + if err != nil { |
| 27 | + return nil, nil, fmt.Errorf("creating temp input file: %w", err) |
| 28 | + } |
| 29 | + defer os.Remove(tmpIn.Name()) |
| 30 | + defer tmpIn.Close() |
| 31 | + |
| 32 | + if _, err := io.Copy(tmpIn, content); err != nil { |
| 33 | + return nil, nil, fmt.Errorf("writing to temp file: %w", err) |
| 34 | + } |
| 35 | + if _, err := tmpIn.Seek(0, 0); err != nil { |
| 36 | + return nil, nil, fmt.Errorf("seeking temp file: %w", err) |
| 37 | + } |
| 38 | + |
| 39 | + // Decode the binary image |
| 40 | + c := crit.New(tmpIn, nil, "", false, false) |
| 41 | + img, err := c.Decode(&fdinfo.FileEntry{}) |
| 42 | + if err != nil { |
| 43 | + return nil, nil, fmt.Errorf("decoding files.img: %w", err) |
| 44 | + } |
| 45 | + |
| 46 | + // Walk every entry looking for TCP listen sockets on the old port |
| 47 | + remapped := 0 |
| 48 | + for _, entry := range img.Entries { |
| 49 | + fileEntry, ok := entry.Message.(*fdinfo.FileEntry) |
| 50 | + if !ok { |
| 51 | + continue |
| 52 | + } |
| 53 | + if fileEntry.GetType() != fdinfo.FdTypes_INETSK { |
| 54 | + continue |
| 55 | + } |
| 56 | + isk := fileEntry.GetIsk() |
| 57 | + if isk == nil { |
| 58 | + continue |
| 59 | + } |
| 60 | + if isk.GetState() == tcpListenState && isk.GetSrcPort() == oldPort { |
| 61 | + np := newPort |
| 62 | + isk.SrcPort = &np |
| 63 | + remapped++ |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + if remapped == 0 { |
| 68 | + return nil, nil, fmt.Errorf("no TCP listen sockets found with source port %d", oldPort) |
| 69 | + } |
| 70 | + |
| 71 | + // Encode the modified image to another temp file |
| 72 | + tmpOut, err := os.CreateTemp("", "files-img-out-*.img") |
| 73 | + if err != nil { |
| 74 | + return nil, nil, fmt.Errorf("creating temp output file: %w", err) |
| 75 | + } |
| 76 | + defer os.Remove(tmpOut.Name()) |
| 77 | + defer tmpOut.Close() |
| 78 | + |
| 79 | + cOut := crit.New(nil, tmpOut, "", false, false) |
| 80 | + if err := cOut.Encode(img); err != nil { |
| 81 | + return nil, nil, fmt.Errorf("encoding files.img: %w", err) |
| 82 | + } |
| 83 | + |
| 84 | + // Read the re-encoded bytes |
| 85 | + if _, err := tmpOut.Seek(0, 0); err != nil { |
| 86 | + return nil, nil, fmt.Errorf("seeking output file: %w", err) |
| 87 | + } |
| 88 | + var buf bytes.Buffer |
| 89 | + if _, err := io.Copy(&buf, tmpOut); err != nil { |
| 90 | + return nil, nil, fmt.Errorf("reading output file: %w", err) |
| 91 | + } |
| 92 | + |
| 93 | + // Update the tar header to reflect the new size |
| 94 | + hdr.Size = int64(buf.Len()) |
| 95 | + return hdr, buf.Bytes(), nil |
| 96 | +} |
| 97 | + |
| 98 | +// remapConfigDump modifies the container runtime config JSON to update: |
| 99 | +// - Port mappings (Podman/CRI-O: newPortMappings[].container_port) |
| 100 | +// - PORT environment variable in any nested env arrays |
| 101 | +func remapConfigDump(hdr *tar.Header, content io.Reader, oldPort, newPort string) (*tar.Header, []byte, error) { |
| 102 | + data, err := io.ReadAll(content) |
| 103 | + if err != nil { |
| 104 | + return nil, nil, fmt.Errorf("reading config.dump: %w", err) |
| 105 | + } |
| 106 | + |
| 107 | + // Parse into a generic map to preserve all fields |
| 108 | + var config map[string]any |
| 109 | + if err := json.Unmarshal(data, &config); err != nil { |
| 110 | + return nil, nil, fmt.Errorf("parsing config.dump JSON: %w", err) |
| 111 | + } |
| 112 | + |
| 113 | + // Update port mappings - search for any array of objects containing |
| 114 | + // "container_port" (covers Podman's "newPortMappings" and |
| 115 | + // CRI-O's "portMappings" or any similar structure) |
| 116 | + remapPortMappings(config, oldPort, newPort) |
| 117 | + |
| 118 | + // Update PORT env var in any nested env arrays |
| 119 | + remapEnvRecursive(config, oldPort, newPort) |
| 120 | + |
| 121 | + output, err := json.Marshal(config) |
| 122 | + if err != nil { |
| 123 | + return nil, nil, fmt.Errorf("marshaling config.dump: %w", err) |
| 124 | + } |
| 125 | + |
| 126 | + hdr.Size = int64(len(output)) |
| 127 | + return hdr, output, nil |
| 128 | +} |
| 129 | + |
| 130 | +// remapSpecDump modifies the OCI runtime spec JSON to update the PORT env var. |
| 131 | +func remapSpecDump(hdr *tar.Header, content io.Reader, oldPort, newPort string) (*tar.Header, []byte, error) { |
| 132 | + data, err := io.ReadAll(content) |
| 133 | + if err != nil { |
| 134 | + return nil, nil, fmt.Errorf("reading spec.dump: %w", err) |
| 135 | + } |
| 136 | + |
| 137 | + var spec map[string]any |
| 138 | + if err := json.Unmarshal(data, &spec); err != nil { |
| 139 | + return nil, nil, fmt.Errorf("parsing spec.dump JSON: %w", err) |
| 140 | + } |
| 141 | + |
| 142 | + // The env array lives under spec.process.env |
| 143 | + if process, ok := spec["process"].(map[string]any); ok { |
| 144 | + if envSlice, ok := process["env"].([]any); ok { |
| 145 | + process["env"] = remapEnvSlice(envSlice, oldPort, newPort) |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + output, err := json.Marshal(spec) |
| 150 | + if err != nil { |
| 151 | + return nil, nil, fmt.Errorf("marshaling spec.dump: %w", err) |
| 152 | + } |
| 153 | + |
| 154 | + hdr.Size = int64(len(output)) |
| 155 | + return hdr, output, nil |
| 156 | +} |
| 157 | + |
| 158 | +// remapPortMappings recursively searches a JSON structure for arrays of objects |
| 159 | +// containing a "container_port" key, and remaps matching ports. |
| 160 | +// This covers Podman ("newPortMappings"), CRI-O ("portMappings"), and any |
| 161 | +// future runtime that uses a similar convention. |
| 162 | +func remapPortMappings(obj any, oldPort, newPort string) { |
| 163 | + switch v := obj.(type) { |
| 164 | + case map[string]any: |
| 165 | + for _, val := range v { |
| 166 | + remapPortMappings(val, oldPort, newPort) |
| 167 | + } |
| 168 | + case []any: |
| 169 | + for _, item := range v { |
| 170 | + if mapping, ok := item.(map[string]any); ok { |
| 171 | + if cp, ok := mapping["container_port"].(float64); ok && strconv.FormatFloat(cp, 'f', -1, 64) == oldPort { |
| 172 | + newPortNum, _ := strconv.ParseFloat(newPort, 64) |
| 173 | + mapping["container_port"] = newPortNum |
| 174 | + } |
| 175 | + } |
| 176 | + remapPortMappings(item, oldPort, newPort) |
| 177 | + } |
| 178 | + } |
| 179 | +} |
| 180 | + |
| 181 | +// remapEnvRecursive walks a JSON structure looking for any "env" key |
| 182 | +// whose value is an array of strings, and replaces PORT=oldPort with PORT=newPort. |
| 183 | +// This handles any runtime's config layout without hardcoding paths. |
| 184 | +func remapEnvRecursive(obj any, oldPort, newPort string) { |
| 185 | + m, ok := obj.(map[string]any) |
| 186 | + if !ok { |
| 187 | + return |
| 188 | + } |
| 189 | + for key, val := range m { |
| 190 | + if key == "env" { |
| 191 | + if envSlice, ok := val.([]any); ok { |
| 192 | + m["env"] = remapEnvSlice(envSlice, oldPort, newPort) |
| 193 | + } |
| 194 | + } else { |
| 195 | + switch child := val.(type) { |
| 196 | + case map[string]any: |
| 197 | + remapEnvRecursive(child, oldPort, newPort) |
| 198 | + case []any: |
| 199 | + for _, item := range child { |
| 200 | + remapEnvRecursive(item, oldPort, newPort) |
| 201 | + } |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | +} |
| 206 | + |
| 207 | +// remapEnvSlice replaces PORT=oldPort with PORT=newPort in an env slice. |
| 208 | +func remapEnvSlice(envSlice []any, oldPort, newPort string) []any { |
| 209 | + target := "PORT=" + oldPort |
| 210 | + replacement := "PORT=" + newPort |
| 211 | + for i, v := range envSlice { |
| 212 | + s, ok := v.(string) |
| 213 | + if !ok { |
| 214 | + continue |
| 215 | + } |
| 216 | + if s == target { |
| 217 | + envSlice[i] = replacement |
| 218 | + } |
| 219 | + } |
| 220 | + return envSlice |
| 221 | +} |
0 commit comments