9c06b898c1
This is more user-friendly compared to the raw SHA256 checksums. Those can still be obtained by querying the status via JSON. fixes https://github.com/gokrazy/gokrazy/issues/343
531 lines
14 KiB
Go
531 lines
14 KiB
Go
// Boot and Supervise are called by the auto-generated init
|
|
// program. They are provided in case you need to implement a custom
|
|
// init program.
|
|
//
|
|
// PrivateInterfaceAddrs is useful for init and other processes.
|
|
//
|
|
// DontStartOnBoot and WaitForClock are useful for non-init processes.
|
|
package gokrazy
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/mdlayher/watchdog"
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/gokrazy/gokrazy/internal/iface"
|
|
"github.com/gokrazy/internal/rootdev"
|
|
)
|
|
|
|
var (
|
|
buildTimestamp = "uninitialized"
|
|
httpPassword string // or empty to only permit unix socket access
|
|
hostname string
|
|
tlsConfig *tls.Config
|
|
useTLS bool
|
|
)
|
|
|
|
func configureLoopback() error {
|
|
cs, err := iface.NewConfigSocket("lo")
|
|
if err != nil {
|
|
return fmt.Errorf("config socket: %v", err)
|
|
}
|
|
defer cs.Close()
|
|
|
|
if err := cs.Up(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := cs.SetAddress(net.IP([]byte{127, 0, 0, 1})); err != nil {
|
|
return err
|
|
}
|
|
|
|
return cs.SetNetmask(net.IPMask([]byte{255, 0, 0, 0}))
|
|
}
|
|
|
|
// runWatchdog periodically pings the hardware watchdog.
|
|
func runWatchdog() {
|
|
d, err := watchdog.Open()
|
|
if err != nil {
|
|
log.Printf("disabling hardware watchdog, as it could not be opened: %v", err)
|
|
return
|
|
}
|
|
defer d.Close()
|
|
|
|
var timeout string
|
|
if t, err := d.Timeout(); err != nil {
|
|
// Assume the device cannot report the watchdog timeout.
|
|
timeout = "unknown"
|
|
} else {
|
|
timeout = t.String()
|
|
}
|
|
|
|
log.Printf("found hardware watchdog %q with timeout %s, pinging...", d.Identity, timeout)
|
|
|
|
for {
|
|
if err := d.Ping(); err != nil {
|
|
log.Printf("hardware watchdog ping failed: %v", err)
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
|
|
func setupTLS() error {
|
|
if _, err := os.Stat("/etc/ssl/gokrazy-web.pem"); os.IsNotExist(err) {
|
|
return nil // Nothing to set up
|
|
}
|
|
cert, err := tls.LoadX509KeyPair("/etc/ssl/gokrazy-web.pem", "/etc/ssl/gokrazy-web.key.pem")
|
|
if err != nil {
|
|
return fmt.Errorf("failed loading certificate: %v", err)
|
|
}
|
|
useTLS = true
|
|
tlsConfig = &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
MinVersion: tls.VersionTLS12,
|
|
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
|
PreferServerCipherSuites: true,
|
|
CipherSuites: []uint16{
|
|
// required for http/2
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
// See https://cipherlist.eu/
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
|
},
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readConfigFile reads configuration files from /perm /etc or / and returns trimmed content as string
|
|
func readConfigFile(fileName string) (string, error) {
|
|
str, err := ioutil.ReadFile("/perm/" + fileName)
|
|
if err != nil {
|
|
str, err = ioutil.ReadFile("/etc/" + fileName)
|
|
}
|
|
if err != nil && os.IsNotExist(err) {
|
|
str, err = ioutil.ReadFile("/" + fileName)
|
|
}
|
|
|
|
return strings.TrimSpace(string(str)), err
|
|
}
|
|
|
|
// readPortFromConfigFile reads port from config file
|
|
func readPortFromConfigFile(fileName, defaultPort string) string {
|
|
port, err := readConfigFile(fileName)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
log.Printf("reading %s failed: %v", fileName, err)
|
|
}
|
|
return defaultPort
|
|
}
|
|
|
|
if _, err := strconv.Atoi(port); err != nil {
|
|
log.Printf("invalid port in %s: %v", fileName, err)
|
|
return defaultPort
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func switchToInactiveRoot() error {
|
|
// mount the inactive root partition on a temporary mountpoint
|
|
tmpmnt := filepath.Join(os.TempDir(), "mnt")
|
|
if err := os.MkdirAll(tmpmnt, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := syscall.Mount(rootdev.Partition(rootdev.InactiveRootPartition()), tmpmnt, "squashfs", syscall.MS_RDONLY, ""); err != nil {
|
|
return fmt.Errorf("mount inactive root: %v", err)
|
|
}
|
|
|
|
if err := os.Chdir(tmpmnt); err != nil {
|
|
return fmt.Errorf("chdir: %v", err)
|
|
}
|
|
|
|
if err := syscall.Mount(".", "/", "", syscall.MS_MOVE, ""); err != nil {
|
|
return fmt.Errorf("mount . /: %v", err)
|
|
}
|
|
|
|
if err := syscall.Chroot("."); err != nil {
|
|
return fmt.Errorf("chroot .: %v", err)
|
|
}
|
|
|
|
if err := os.Chdir("/"); err != nil {
|
|
return fmt.Errorf("chdir: %v", err)
|
|
}
|
|
|
|
if err := syscall.Exec("/gokrazy/init", []string{"/gokrazy/init"}, os.Environ()); err != nil {
|
|
return fmt.Errorf("exec(init): %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func maybeSwitchToInactive() {
|
|
cmdline, err := readCmdline()
|
|
if err != nil {
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
log.Printf("cmdline: %s", string(cmdline))
|
|
|
|
if !strings.Contains(string(cmdline), "gokrazy.try_boot_inactive=1") {
|
|
return
|
|
}
|
|
log.Printf("switching to inactive root partition")
|
|
|
|
// update the cmdline to have the device boot into the old environment next time
|
|
if err := modifyCmdline(func(b []byte) []byte {
|
|
return bytes.ReplaceAll(b,
|
|
[]byte("gokrazy.try_boot_inactive=1"),
|
|
[]byte("gokrazy.switch_on_boot=1"))
|
|
}); err != nil {
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
// switch to the inactive root partition instead
|
|
if err := switchToInactiveRoot(); err != nil {
|
|
log.Print(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Boot configures basic system settings. More specifically, it:
|
|
//
|
|
// - mounts /dev, /tmp, /proc, /sys and /perm file systems
|
|
// - mounts and populate /etc tmpfs overlay
|
|
// - sets hostname from the /etc/hostname file
|
|
// - sets HTTP password from the gokr-pw.txt file
|
|
// - configures the loopback network interface
|
|
//
|
|
// Boot should always be called to transition the machine into a
|
|
// useful state, even in custom init process implementations or
|
|
// single-process applications.
|
|
//
|
|
// userBuildTimestamp will be exposed on the HTTP status handlers that
|
|
// are set up by Supervise.
|
|
func Boot(userBuildTimestamp string) error {
|
|
// TODO: think about whether the watchdog needs a different setup during the
|
|
// update process
|
|
go runWatchdog()
|
|
|
|
buildTimestamp = userBuildTimestamp
|
|
|
|
if err := mountfs(); err != nil {
|
|
return err
|
|
}
|
|
|
|
maybeSwitchToInactive()
|
|
|
|
if err := loadModules(); err != nil {
|
|
log.Printf("loading modules: %v", err)
|
|
}
|
|
|
|
hostnameb, err := ioutil.ReadFile("/etc/hostname")
|
|
if err != nil && os.IsNotExist(err) {
|
|
hostnameb, err = ioutil.ReadFile("/hostname")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := unix.Sethostname(hostnameb); err != nil {
|
|
return err
|
|
}
|
|
hostname = string(hostnameb)
|
|
|
|
if _, err := os.Stat("/etc/gokr-pw.txt"); os.IsNotExist(err) {
|
|
// This instance was built with the NoPassword option
|
|
// (because there is no password file in the root fs),
|
|
// so do not read a password from any location.
|
|
} else {
|
|
httpPassword, err = readConfigFile("gokr-pw.txt")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := configureLoopback(); err != nil {
|
|
return err
|
|
}
|
|
|
|
initRemoteSyslog()
|
|
if err := setupTLS(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// create /dev/serial0 symlink in /dev, which was mounted by mountfs() earlier
|
|
if err := createSerialSymlink(); err != nil {
|
|
log.Printf("creating serial symlink failed: %v", err)
|
|
}
|
|
|
|
// create a /dev/ttyAMA0 symlink to /dev/ttyS0 so that software which does
|
|
// not yet use /dev/serial0 keeps working without changes.
|
|
if err := os.Symlink("/dev/ttyS0", "/dev/ttyAMA0"); err != nil && !os.IsExist(err) {
|
|
log.Printf("creating compatibility symlink /dev/ttyAMA0 -> /dev/ttyS0 failed: %v", err)
|
|
}
|
|
|
|
if err := pollPowerButtons(); err != nil {
|
|
log.Printf("polling power buttons: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createSerialSymlink() error {
|
|
uart0, uart0err := ioutil.ReadFile("/proc/device-tree/aliases/uart0")
|
|
uart1, uart1err := ioutil.ReadFile("/proc/device-tree/aliases/uart1")
|
|
serial0, serial0err := ioutil.ReadFile("/proc/device-tree/aliases/serial0")
|
|
if uart0err == nil && serial0err == nil && bytes.Equal(uart0, serial0) {
|
|
// old kernel (before https://github.com/gokrazy/gokrazy/issues/49)
|
|
return os.Symlink("/dev/ttyAMA0", "/dev/serial0")
|
|
} else if os.IsNotExist(uart1err) ||
|
|
(uart1err == nil && serial0err == nil && bytes.Equal(uart1, serial0)) {
|
|
// new kernel (after https://github.com/gokrazy/gokrazy/issues/49)
|
|
return os.Symlink("/dev/ttyS0", "/dev/serial0")
|
|
}
|
|
return fmt.Errorf("unexpected device tree state: uart0=%s, %v / uart1=%s, %v / serial0=%s, %v", string(uart0), uart0err, string(uart1), uart1err, string(serial0), serial0err)
|
|
}
|
|
|
|
func updateListenerPairs(httpPort, httpsPort string, useTLS bool, tlsConfig *tls.Config) error {
|
|
if err := updateListeners(httpPort, httpsPort, useTLS, nil); err != nil {
|
|
return err
|
|
}
|
|
if useTLS {
|
|
if err := updateListeners(httpsPort, "", useTLS, tlsConfig); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// By default, stdio is open on /dev/console which cannot be a controlling tty
|
|
// so set up stdio on a normal tty (e.g. tty1, ttyS0, ttyAMA0)
|
|
// See https://busybox.net/FAQ.html#job_control
|
|
func runWithCtty(shell string) error {
|
|
// find TTY device connected to /dev/console
|
|
// https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-tty
|
|
buf, err := ioutil.ReadFile("/sys/class/tty/console/active")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
devs := strings.Split(strings.TrimSpace(string(buf)), " ")
|
|
ttyDev := "/dev/" + devs[len(devs)-1]
|
|
|
|
stdin, err := os.OpenFile(ttyDev, os.O_RDWR, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("stdin: %v", err)
|
|
}
|
|
defer stdin.Close()
|
|
|
|
stdout, err := os.OpenFile(ttyDev, os.O_RDWR, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("stdout: %v", err)
|
|
}
|
|
defer stdout.Close()
|
|
|
|
stderr, err := os.OpenFile(ttyDev, os.O_RDWR, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("stderr: %v", err)
|
|
}
|
|
defer stderr.Close()
|
|
|
|
sh := exec.Command(shell)
|
|
sh.Stdin = stdin
|
|
sh.Stdout = stdout
|
|
sh.Stderr = stderr
|
|
sh.SysProcAttr = &syscall.SysProcAttr{
|
|
Setsid: true,
|
|
Setctty: true,
|
|
Ctty: 0,
|
|
}
|
|
|
|
return sh.Run()
|
|
}
|
|
|
|
func tryStartShell() error {
|
|
var lastErr error
|
|
for _, shell := range []string{
|
|
"/tmp/serial-busybox/ash",
|
|
"/perm/sh",
|
|
} {
|
|
_, err := os.Stat(shell)
|
|
lastErr = err
|
|
if err != nil {
|
|
continue
|
|
}
|
|
log.Printf("starting shell %s upon input on serial console", shell)
|
|
|
|
if err := runWithCtty(shell); err != nil {
|
|
log.Printf("runWithCtty: %v", err)
|
|
lastErr = err
|
|
}
|
|
return nil
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
// Service is a gokrazy service.
|
|
type Service struct {
|
|
// TODO: refactor the fields of service into this type, but
|
|
// visibility-restricted.
|
|
s *service
|
|
}
|
|
|
|
func newService(cmd *exec.Cmd, stopped, waitForClock bool) *Service {
|
|
s := &service{
|
|
cmd: cmd,
|
|
stopped: stopped,
|
|
waitForClock: waitForClock,
|
|
}
|
|
|
|
s.state = NewProcessState()
|
|
|
|
tag := filepath.Base(s.Cmd().Path)
|
|
s.Stdout = newLogWriter(tag)
|
|
s.Stderr = newLogWriter(tag)
|
|
|
|
return &Service{s}
|
|
}
|
|
|
|
// NewService constructs a new gokrazy service from the specified command.
|
|
func NewService(cmd *exec.Cmd) *Service {
|
|
return newService(cmd, false, false)
|
|
}
|
|
|
|
// NewStoppedService is like NewService, but the created gokrazy service will
|
|
// not be supervised, i.e. remain stopped on boot.
|
|
func NewStoppedService(cmd *exec.Cmd) *Service {
|
|
return newService(cmd, true, false)
|
|
}
|
|
|
|
// NewWaitForClockService is like NewService, but the created gokrazy service
|
|
// will wait for clock to be synchronized, i.e. blocked till the clock is accurate.
|
|
func NewWaitForClockService(cmd *exec.Cmd) *Service {
|
|
return newService(cmd, false, true)
|
|
}
|
|
|
|
// Supervise runs SuperviseServices, creating services from commands.
|
|
//
|
|
// Deprecated: newer versions of gokr-packer run gokrazy.SuperviseServices()
|
|
// instead
|
|
func Supervise(commands []*exec.Cmd) error {
|
|
services := make([]*Service, len(commands))
|
|
for idx, cmd := range commands {
|
|
services[idx] = &Service{&service{cmd: cmd}}
|
|
}
|
|
return SuperviseServices(services)
|
|
}
|
|
|
|
// SuperviseServices continuously restarts the processes specified in services
|
|
// unless they run DontStartOnBoot.
|
|
//
|
|
// Password-protected HTTP handlers are installed, allowing to inspect
|
|
// the supervised services and update the gokrazy installation over
|
|
// the network.
|
|
//
|
|
// HTTP is served on PrivateInterfaceAddrs(). New IP addresses will be
|
|
// picked up upon receiving SIGHUP.
|
|
func SuperviseServices(services []*Service) error {
|
|
unwrapped := make([]*service, len(services))
|
|
for idx, svc := range services {
|
|
unwrapped[idx] = svc.s
|
|
}
|
|
|
|
var err error
|
|
lastInstalledEepromVersion, err = readEEPROMVersions(Model())
|
|
if err != nil {
|
|
log.Printf("getting EEPROM version: %v", err)
|
|
}
|
|
|
|
initStatus()
|
|
setupDeviceSpecifics()
|
|
|
|
if err := initUpdate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
httpPort := readPortFromConfigFile("http-port.txt", "80")
|
|
httpsPort := readPortFromConfigFile("https-port.txt", "443")
|
|
|
|
if err := updateListenerPairs(httpPort, httpsPort, useTLS, tlsConfig); err != nil {
|
|
return fmt.Errorf("updating listeners: %v", err)
|
|
}
|
|
|
|
if nl, err := listenNetlink(); err == nil {
|
|
go func() {
|
|
for {
|
|
msgs, err := nl.ReadMsgs()
|
|
if err != nil {
|
|
log.Printf("netlink.ReadMsgs: %v", err)
|
|
return
|
|
}
|
|
|
|
for _, m := range msgs {
|
|
if m.Header.Type != syscall.RTM_NEWADDR &&
|
|
m.Header.Type != syscall.RTM_DELADDR {
|
|
continue
|
|
}
|
|
if err := updateListenerPairs(httpPort, httpsPort, useTLS, tlsConfig); err != nil {
|
|
log.Printf("updating listeners: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
} else {
|
|
log.Printf("cannot listen for new IP addresses: %v", err)
|
|
}
|
|
|
|
listenForSignals(func() {
|
|
if err := updateListenerPairs(httpPort, httpsPort, useTLS, tlsConfig); err != nil {
|
|
log.Printf("updating listeners: %v", err)
|
|
}
|
|
})
|
|
|
|
// Only supervise services after the SIGHUP handler is set up, otherwise a
|
|
// particularly fast dhcp client (e.g. when running in qemu) might send
|
|
// SIGHUP before the signal handler is set up, thereby killing init and
|
|
// panic the system!
|
|
superviseServices(unwrapped)
|
|
|
|
go func() {
|
|
buf := make([]byte, 1)
|
|
for {
|
|
if _, err := os.Stdin.Read(buf); err != nil {
|
|
log.Printf("read(stdin): %v", err)
|
|
break
|
|
}
|
|
|
|
if err := tryStartShell(); err != nil {
|
|
log.Printf("could not start shell: %v", err)
|
|
if err := updateListenerPairs(httpPort, httpsPort, useTLS, tlsConfig); err != nil {
|
|
log.Printf("updating listeners: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// DontStartOnBoot informs the gokrazy init process to not supervise
|
|
// the process and exits. The user can manually start supervision,
|
|
// which turns DontStartOnBoot into a no-op.
|
|
func DontStartOnBoot() {
|
|
if os.Getenv("GOKRAZY_FIRST_START") == "1" {
|
|
os.Exit(125)
|
|
}
|
|
}
|