| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "net" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strconv" |
| 8 | "strings" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/instancectl" |
| 11 | "github.com/carlosframework/platform/internal/provision" |
| 12 | ) |
| 13 | |
| 14 | func claimBoundUnitDefinition(s claimUnitShape) ([]byte, error) { |
| 15 | if !s.valid() || !claimHexDigest(s.Program) { |
| 16 | return nil, os.ErrPermission |
| 17 | } |
| 18 | template := claimBoundTemplate |
| 19 | body := strings.ReplaceAll(strings.ReplaceAll(string(template), "%i", instancectl.Escape(provision.InstanceID(s.Host))), "%I", provision.InstanceID(s.Host)) |
| 20 | // With no live root gate, even a direct systemctl restart must execute no |
| 21 | // tenant code. The temporary reservation drop-in replaces this command. |
| 22 | body = strings.Replace(body, "ExecStart="+instancectl.SystemBinary+" instance-exec run "+provision.InstanceID(s.Host), "ExecStart=/usr/bin/false", 1) |
| 23 | if s.Socket != "" && s.Socket != provision.NestedSocket(s.Host) { |
| 24 | if !claimFlatSocket(s) { |
| 25 | return nil, os.ErrPermission |
| 26 | } |
| 27 | body = strings.Replace(body, "TemporaryFileSystem=/run/carlos:ro\n", "", 1) |
| 28 | body += "\n[Service]\nBindPaths=" + provision.InstanceRunDir(s.Host) + ":/run/carlos\nReadWritePaths=/run/carlos\n" |
| 29 | } |
| 30 | if s.Addr != "" { |
| 31 | host, port, err := net.SplitHostPort(s.Addr) |
| 32 | n, e := strconv.Atoi(port) |
| 33 | if err != nil || e != nil || n < 1 || n > 65535 { |
| 34 | return nil, os.ErrPermission |
| 35 | } |
| 36 | family, bits := "ipv4", "/32" |
| 37 | if ip4 := net.ParseIP(host).To4(); ip4 != nil { |
| 38 | host = ip4.String() |
| 39 | } else { |
| 40 | family, bits = "ipv6", "/128" |
| 41 | } |
| 42 | body += "\n[Service]\nIPAddressAllow=" + host + bits + "\nSocketBindDeny=any\nSocketBindAllow=" + family + ":tcp:" + port + "\n" |
| 43 | } |
| 44 | return []byte(body), nil |
| 45 | } |
| 46 | |
| 47 | func claimFlatSocket(s claimUnitShape) bool { |
| 48 | if filepath.Dir(s.Socket) != provision.SocketDir || !strings.HasSuffix(s.Socket, ".sock") { |
| 49 | return false |
| 50 | } |
| 51 | for _, r := range filepath.Base(s.Socket) { |
| 52 | if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.') { |
| 53 | return false |
| 54 | } |
| 55 | } |
| 56 | return true |
| 57 | } |
| 58 | |
| 59 | func claimSocketTarget(s claimUnitShape) string { |
| 60 | if claimFlatSocket(s) { |
| 61 | return filepath.Join(provision.InstanceRunDir(s.Host), filepath.Base(s.Socket)) |
| 62 | } |
| 63 | return s.Socket |
| 64 | } |
| 65 | |