func umountCmd(args ...string) { cmd := exec.Command("/sbin/umount", args...) gologit.Debugln(append([]string{"/sbin/umount"}, args...)) out, err := cmd.CombinedOutput() for _, line := range strings.Split(string(out), "\n") { if line != "" { gologit.Debugln(line) } } if err != nil { // some mounts are not present, so just fail // do not log exit status 1 unless debug logging gologit.Debugf("%s\n", err) } }
// B64DecodeURL ensures the url is properly verified via HMAC, and then // unencodes the url, returning the url (if valid) and whether the // HMAC was verified. func B64DecodeURL(hmackey []byte, encdig string, encURL string) (string, bool) { urlBytes, err := b64decode(encURL) if err != nil { gologit.Debugln("Bad B64 Decode of URL", encURL) return "", false } macBytes, err := b64decode(encdig) if err != nil { gologit.Debugln("Bad B64 Decode of MAC", encURL) return "", false } if ok := validateURL(&hmackey, &macBytes, &urlBytes); !ok { return "", false } return string(urlBytes), true }
// HexDecodeURL ensures the url is properly verified via HMAC, and then // unencodes the url, returning the url (if valid) and whether the // HMAC was verified. func HexDecodeURL(hmackey []byte, hexdig string, hexURL string) (string, bool) { urlBytes, err := hex.DecodeString(hexURL) if err != nil { gologit.Debugln("Bad Hex Decode of URL", hexURL) return "", false } macBytes, err := hex.DecodeString(hexdig) if err != nil { gologit.Debugln("Bad Hex Decode of MAC", hexURL) return "", false } if ok := validateURL(&hmackey, &macBytes, &urlBytes); !ok { return "", false } return string(urlBytes), true }
func stopCmdRun(cmd *cobra.Command, args []string) { // requires root if !core.IsRoot() { gologit.Fatalf("Must be root to stop\n") } jail, err := core.FindJail(args[0]) if err != nil { gologit.Fatalf("No jail found by '%s'\n", args[0]) } if !jail.IsRunning() { gologit.Fatalf("Jail is not running!\n") } // create file f, err := os.OpenFile(jail.GetLogPath(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) if err != nil { gologit.Fatal(err) } defer f.Close() fmt.Printf("* Stopping %s (%s)\n", jail.HostUUID, jail.Tag) fmt.Printf(" + Removing jail process\n") file, err := ioutil.TempFile(os.TempDir(), "rollcage.") defer os.Remove(file.Name()) jailConfig := jail.JailConfig() gologit.Debugln(jailConfig) file.WriteString(jailConfig) file.Close() excmd := exec.Command( "/usr/sbin/jail", "-f", file.Name(), "-r", fmt.Sprintf("ioc-%s", jail.HostUUID)) excmd.Stdout = f excmd.Stderr = f err = excmd.Run() if err != nil { gologit.Fatal(err) } // mostly for safety... fmt.Printf(" + Tearing down mounts\n") umountCmd("-afvF", path.Join(jail.Mountpoint, "fstab")) umountCmd(path.Join(jail.Mountpoint, "root/dev/fd")) umountCmd(path.Join(jail.Mountpoint, "root/dev")) umountCmd(path.Join(jail.Mountpoint, "root/proc")) // TODO: basejail here? // TODO: rctl stuff here... }
// DecodeURL ensures the url is properly verified via HMAC, and then // unencodes the url, returning the url (if valid) and whether the // HMAC was verified. Tries to HexDecode the url, then B64Decode if that fails. func DecodeURL(hmackey []byte, encdig string, encURL string) (string, bool) { var decoder func([]byte, string, string) (string, bool) if len(encdig) == 40 { decoder = HexDecodeURL } else { decoder = B64DecodeURL } urlBytes, ok := decoder(hmackey, encdig, encURL) if !ok { gologit.Debugln("Bad Decode of URL", encURL) return "", false } return string(urlBytes), true }
func stopCmdRun(cmd *cobra.Command, args []string) { // requires root if !core.IsRoot() { gologit.Fatalf("Must be root to stop\n") } jail, err := core.FindJail(args[0]) if err != nil { gologit.Fatalf("No jail found by '%s'\n", args[0]) } if !jail.IsRunning() { gologit.Fatalf("Jail is not running!\n") } propertyList := []string{ "mountpoint", "org.freebsd.iocage:type", "org.freebsd.iocage:tag", "org.freebsd.iocage:prestop", "org.freebsd.iocage:exec_stop", "org.freebsd.iocage:poststop", "org.freebsd.iocage:vnet", "org.freebsd.iocage:ip4", } lines := core.SplitOutput(core.ZFSMust( fmt.Errorf("Error listing properties"), "list", "-H", "-o", strings.Join(propertyList, ","), jail.Path)) if len(lines) < 1 { gologit.Fatalf("No output from property fetch\n") } prop_mountpoint := removeDash(lines[0][0]) //prop_type := removeDash(lines[0][1]) prop_tag := removeDash(lines[0][2]) prop_prestop := removeDash(lines[0][3]) prop_exec_stop := removeDash(lines[0][4]) prop_poststop := removeDash(lines[0][5]) prop_vnet := removeDash(lines[0][6]) prop_ip4 := removeDash(lines[0][7]) // set a default path environ := []string{ "PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin", } fmt.Printf("* Stopping %s (%s)\n", jail.HostUUID, prop_tag) if prop_prestop != "" { fmt.Printf(" + Running pre-stop\n") preStop := core.SplitFieldsQuoteSafe(prop_prestop) excmd := exec.Command(preStop[0], preStop[1:]...) excmd.Env = environ err := excmd.Run() if err != nil { gologit.Printf("%s\n", err) } } fmt.Printf(" + Stopping services\n") jexec := []string{"/usr/sbin/jexec"} jexec = append(jexec, fmt.Sprintf("ioc-%s", jail.HostUUID)) jexec = append(jexec, core.SplitFieldsQuoteSafe(prop_exec_stop)...) out, err := exec.Command(jexec[0], jexec[1:]...).CombinedOutput() gologit.Debugln(string(out)) if err != nil { gologit.Printf("%s\n", err) } if prop_vnet == "on" { fmt.Printf(" + Tearing down VNET\n") // stop VNET networking } else if prop_ip4 != "inherit" { // stop standard networking (legacy?) lines := core.SplitOutput(core.ZFSMust( fmt.Errorf("Error listing jails"), "list", "-H", "-o", "org.freebsd.iocage:ip4_addr,org.freebsd.iocage:ip6_addr", jail.Path)) for _, ip_config := range lines[0] { if ip_config == "none" { continue } for _, addr := range strings.Split(ip_config, ",") { item := strings.Split(addr, "|") gologit.Debugln("/sbin/ifconfig", item[0], item[1], "-alias") out, err := exec.Command("/sbin/ifconfig", item[0], item[1], "-alias").CombinedOutput() gologit.Debugln(string(out)) if err != nil { gologit.Printf("%s\n", err) } } } } fmt.Printf(" + Removing jail process\n") jrexec := []string{"/usr/sbin/jail", "-r", fmt.Sprintf("ioc-%s", jail.HostUUID)} out, err = exec.Command(jrexec[0], jrexec[1:]...).CombinedOutput() if err != nil { gologit.Printf("%s\n", err) } if prop_poststop != "" { fmt.Printf(" + Running post-stop\n") postStop := core.SplitFieldsQuoteSafe(prop_poststop) excmd := exec.Command(postStop[0], postStop[1:]...) excmd.Env = environ err := excmd.Run() if err != nil { gologit.Printf("%s\n", err) } } fmt.Printf(" + Tearing down mounts\n") umountCmd("-afvF", path.Join(prop_mountpoint, "fstab")) umountCmd(path.Join(prop_mountpoint, "root/dev/fd")) umountCmd(path.Join(prop_mountpoint, "root/dev")) umountCmd(path.Join(prop_mountpoint, "root/proc")) // TODO: basejail here? // TODO: rctl stuff here... }
func startCmdRun(cmd *cobra.Command, args []string) { // requires root if !core.IsRoot() { gologit.Fatalf("Must be root to stop\n") } jail, err := core.FindJail(args[0]) if err != nil { gologit.Fatalf("No jail found by '%s'\n", args[0]) } if jail.IsRunning() { gologit.Fatalf("Jail is already running!\n") } props := jail.GetProperties() // set a default path environ := []string{ "PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin", } fmt.Printf("* Starting %s (%s)\n", jail.HostUUID, jail.Tag) // mount procfs if props.GetIOC("mount_procfs") == "1" { fmt.Printf(" + mounting procfs\n") procpath := path.Join(jail.Mountpoint, "root/proc") excmd := exec.Command("/sbin/mount", "-t", "procfs", "proc", procpath) excmd.Env = environ err := excmd.Run() if err != nil { gologit.Printf("%s\n", err) } } // prepare jail zfs dataset if enabled if props.GetIOC("jail_zfs") == "on" { fmt.Printf(" + jailing zfs dataset\n") setprops := core.ZFSProperties{ "org.freebsd.iocage:allow_mount": "1", "org.freebsd.iocage:allow_mount_zfs": "1", "org.freebsd.iocage:enforce_statfs": "1", } jail.SetProperties(setprops) core.ZFSMust( fmt.Errorf("Error setting property"), "set", "jailed=on", path.Join(core.GetZFSRootPath(), props.GetIOC("jail_zfs_dataset"))) } // copy resolv conf err = core.CopyFile( "/etc/resolv.conf", path.Join(jail.Mountpoint, "root/etc/resolv.conf")) if err != nil { gologit.Printf("%s\n", err) } // create log file logfile, err := os.OpenFile(jail.GetLogPath(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) if err != nil { gologit.Fatal(err) } defer logfile.Close() file, err := ioutil.TempFile(os.TempDir(), "rollcage.") defer os.Remove(file.Name()) jailConfig := jail.JailConfig() gologit.Debugln(jailConfig) file.WriteString(jailConfig) file.Close() excmd := exec.Command( "/usr/sbin/jail", "-f", file.Name(), "-c", fmt.Sprintf("ioc-%s", jail.HostUUID)) excmd.Stdout = logfile excmd.Stderr = logfile err = excmd.Run() if err != nil { gologit.Fatal(err) } // rctl_limits? // cpuset? // jail zfs if props.GetIOC("jail_zfs") == "on" { core.ZFSMust( fmt.Errorf("Error setting property"), "jail", fmt.Sprintf("ioc-%s", jail.HostUUID), path.Join(core.GetZFSRootPath(), props.GetIOC("jail_zfs_dataset"))) out, err := exec.Command( "/usr/sbin/jexec", fmt.Sprintf("ioc-%s", jail.HostUUID), "zfs", "mount", "-a").CombinedOutput() gologit.Debugln(string(out)) if err != nil { gologit.Printf("%s\n", err) } } // set last_started property t := time.Now() core.ZFSMust( fmt.Errorf("Error setting property"), "set", fmt.Sprintf( "org.freebsd.iocage:last_started=%s", t.Format("2006-01-02_15:04:05")), jail.Path) }
// ServerHTTP handles the client request, validates the request is validly // HMAC signed, filters based on the Allow list, and then proxies // valid requests to the desired endpoint. Responses are filtered for // proper image content types. func (p *Proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { gologit.Debugln("Request:", req.URL) if p.metrics != nil { go p.metrics.AddServed() } if p.config.DisableKeepAlivesFE { w.Header().Set("Connection", "close") } if req.Header.Get("Via") == p.config.ServerName { http.Error(w, "Request loop failure", http.StatusNotFound) return } // split path and get components components := strings.Split(req.URL.Path, "/") if len(components) < 3 { http.Error(w, "Malformed request path", http.StatusNotFound) return } sigHash, encodedURL := components[1], components[2] sURL, ok := encoding.DecodeURL(p.config.HMACKey, sigHash, encodedURL) if !ok { http.Error(w, "Bad Signature", http.StatusForbidden) return } gologit.Debugln("URL:", sURL) gologit.Debugln("Client request:", req) u, err := url.Parse(sURL) if err != nil { gologit.Debugln("url parse error:", err) http.Error(w, "Bad url", http.StatusBadRequest) return } u.Host = strings.ToLower(u.Host) if u.Host == "" || localhostRegex.MatchString(u.Host) { http.Error(w, "Bad url host", http.StatusNotFound) return } // if allowList is set, require match matchFound := true if len(p.allowList) > 0 { matchFound = false for _, rgx := range p.allowList { if rgx.MatchString(u.Host) { matchFound = true } } } if !matchFound { http.Error(w, "Allowlist host failure", http.StatusNotFound) return } // filter out rfc1918 hosts ip := net.ParseIP(u.Host) if ip != nil { if addr1918PrefixRegex.MatchString(ip.String()) { http.Error(w, "Denylist host failure", http.StatusNotFound) return } } nreq, err := http.NewRequest(req.Method, sURL, nil) if err != nil { gologit.Debugln("Could not create NewRequest", err) http.Error(w, "Error Fetching Resource", http.StatusBadGateway) return } // filter headers p.copyHeader(&nreq.Header, &req.Header, &ValidReqHeaders) if req.Header.Get("X-Forwarded-For") == "" { host, _, err := net.SplitHostPort(req.RemoteAddr) if err == nil && !addr1918PrefixRegex.MatchString(host) { nreq.Header.Add("X-Forwarded-For", host) } } // add an accept header if the client didn't send one if nreq.Header.Get("Accept") == "" { nreq.Header.Add("Accept", "image/*") } nreq.Header.Add("User-Agent", p.config.ServerName) nreq.Header.Add("Via", p.config.ServerName) gologit.Debugln("Built outgoing request:", nreq) resp, err := p.client.Do(nreq) if err != nil { gologit.Debugln("Could not connect to endpoint", err) // this is a bit janky, but better than peeling off the // 3 layers of wrapped errors and trying to get to net.OpErr and // still having to rely on string comparison to find out if it is // a net.errClosing or not. errString := err.Error() if strings.Contains(errString, "timeout") { http.Error(w, "Error Fetching Resource", http.StatusGatewayTimeout) } else if strings.Contains(errString, "use of closed") { http.Error(w, "Error Fetching Resource", http.StatusBadGateway) } else { // some other error. call it a not found (camo compliant) http.Error(w, "Error Fetching Resource", http.StatusNotFound) } return } defer resp.Body.Close() gologit.Debugln("Response from upstream:", resp) // check for too large a response if resp.ContentLength > p.config.MaxSize { gologit.Debugln("Content length exceeded", sURL) http.Error(w, "Content length exceeded", http.StatusNotFound) return } switch resp.StatusCode { case 200: // check content type if !strings.HasPrefix(resp.Header.Get("Content-Type"), "image/") { gologit.Debugln("Non-Image content-type returned", u) http.Error(w, "Non-Image content-type returned", http.StatusBadRequest) return } case 300: gologit.Debugln("Multiple choices not supported") http.Error(w, "Multiple choices not supported", http.StatusNotFound) return case 301, 302, 303, 307: // if we get a redirect here, we either disabled following, // or followed until max depth and still got one (redirect loop) http.Error(w, "Not Found", http.StatusNotFound) return case 304: h := w.Header() p.copyHeader(&h, &resp.Header, &ValidRespHeaders) w.WriteHeader(304) return case 404: http.Error(w, "Not Found", http.StatusNotFound) return case 500, 502, 503, 504: // upstream errors should probably just 502. client can try later. http.Error(w, "Error Fetching Resource", http.StatusBadGateway) return default: http.Error(w, "Not Found", http.StatusNotFound) return } h := w.Header() p.copyHeader(&h, &resp.Header, &ValidRespHeaders) w.WriteHeader(resp.StatusCode) // since this uses io.Copy from the respBody, it is streaming // from the request to the response. This means it will nearly // always end up with a chunked response. bW, err := io.Copy(w, resp.Body) if err != nil { if opErr, ok := err.(*net.OpError); ok { switch opErr.Err { case syscall.EPIPE, syscall.ECONNRESET: // broken pipe - endpoint terminated the conn // connection reset by peer - endpoint terminated the conn // log as debug only. gologit.Debugln("OpError writing response:", err) default: // log anything else normally gologit.Println("OpError writing response:", err) } } else { // unknown error and not an OpError. gologit.Println("Error writing response:", err) } return } if p.metrics != nil { go p.metrics.AddBytes(bW) } gologit.Debugln("Response to client:", w) }
func updateCmdRun(cmd *cobra.Command, args []string) { // requires root if !core.IsRoot() { gologit.Fatalf("Must be root to snapremove\n") } jail, err := core.FindJail(args[0]) if err != nil { gologit.Fatalf("No jail found by '%s'\n", args[0]) } zfsArgs := []string{ "get", "-Ho", "value", "org.freebsd.iocage:release,mountpoint", jail.Path} out := strings.Split( core.ZFSMust(fmt.Errorf("Error getting properties"), zfsArgs...), "\n") release := out[0] mountpoint := out[1] resolvconf := path.Join(mountpoint, "root/etc/resolv.conf") if _, err := os.Stat(resolvconf); os.IsNotExist(err) { data, err := ioutil.ReadFile("/etc/resolv.conf") if err != nil { gologit.Fatalln("/etc/resolv.conf not present or not readable") } err = ioutil.WriteFile(resolvconf, data, 0755) if err != nil { gologit.Fatalf("Could not copy contents to '%s'\n", resolvconf) } } fmt.Println("* Creating back out snapshot") snappath := fmt.Sprintf( "%s/root@%s", jail.Path, fmt.Sprintf( "ioc-update-%s", time.Now().Format("2006-01-02_15:04:05"))) core.ZFSMust( fmt.Errorf("Error taking snapshot"), "snapshot", snappath) devroot := path.Join(mountpoint, "root/dev") ecmd := exec.Command("/sbin/mount", "-t", "devfs", "devfs", devroot) gologit.Debugln(ecmd.Args) eout, err := ecmd.CombinedOutput() if err != nil { gologit.Fatalf("Error mounting devfs: %s\n", err) } gologit.Debugln(string(eout)) defer func() { ecmd := exec.Command("/sbin/umount", devroot) gologit.Debugln(ecmd.Args) err := ecmd.Run() if err != nil { gologit.Fatalf("Error unmounting devfs: %s\n", err) } }() fmt.Println("* Updating jail...") root := path.Join(mountpoint, "root") ecmd = exec.Command("/usr/sbin/chroot", root, "/usr/sbin/freebsd-update", "--not-running-from-cron", "fetch", "install") ecmd.Env = []string{ "PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin", fmt.Sprintf("UNAME_r=%s", release), "PAGER=/bin/cat", } gologit.Debugln(ecmd.Args) ecmd.Stdout = os.Stdout ecmd.Stderr = os.Stderr ecmd.Run() fmt.Println("* update finished") fmt.Println(" Once verified, don't forget to remove the snapshot!") }
func releaseUpdateCmdRun(cmd *cobra.Command, args []string) { // requires root if !core.IsRoot() { gologit.Fatalf("Must be root to update\n") } release, err := core.FindRelease(args[0]) if err != nil { gologit.Fatalf("Release '%s' not found!\n", args[0]) } mountpoint := release.Mountpoint resolvconf := path.Join(mountpoint, "root/etc/resolv.conf") if _, err := os.Stat(resolvconf); os.IsNotExist(err) { data, err := ioutil.ReadFile("/etc/resolv.conf") if err != nil { gologit.Fatalln("/etc/resolv.conf not present or not readable") } err = ioutil.WriteFile(resolvconf, data, 0755) if err != nil { gologit.Fatalf("Could not copy contents to '%s'\n", resolvconf) } } devroot := path.Join(mountpoint, "root/dev") ecmd := exec.Command("/sbin/mount", "-t", "devfs", "devfs", devroot) gologit.Debugln(ecmd.Args) eout, err := ecmd.CombinedOutput() if err != nil { gologit.Fatalf("Error mounting devfs: %s\n", err) } gologit.Debugln(string(eout)) defer func() { ecmd := exec.Command("/sbin/umount", devroot) gologit.Debugln(ecmd.Args) err := ecmd.Run() if err != nil { gologit.Fatalf("Error unmounting devfs: %s\n", err) } }() fmt.Println("* Updating release...") root := path.Join(mountpoint, "root") exargs := []string{root, "/usr/sbin/freebsd-update"} if release.Name != "9.3-RELEASE" && release.Name != "10.1-RELEASE" { exargs = append(exargs, "--not-running-from-cron") } exargs = append(exargs, "fetch", "install") ecmd = exec.Command("/usr/sbin/chroot", exargs...) unamer := release.Name if release.Patchlevel != "" { unamer = release.Patchlevel } ecmd.Env = []string{ "PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin", fmt.Sprintf("UNAME_r=%s", unamer), "PAGER=/bin/cat", } gologit.Debugln(ecmd.Args) ecmd.Stdout = os.Stdout ecmd.Stderr = os.Stderr ecmd.Stdin = os.Stdin ecmd.Run() fmt.Println("* update finished") }