354 lines
8.5 KiB
Go
354 lines
8.5 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 (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/mdlayher/watchdog"
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/gokrazy/gokrazy/internal/iface"
|
|
)
|
|
|
|
var (
|
|
buildTimestamp = "uninitialized"
|
|
httpPassword string
|
|
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
|
|
}
|
|
|
|
// 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 {
|
|
go runWatchdog()
|
|
|
|
buildTimestamp = userBuildTimestamp
|
|
|
|
if err := mountfs(); err != nil {
|
|
return 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)
|
|
|
|
pw, err := readConfigFile("gokr-pw.txt")
|
|
if err != nil {
|
|
return fmt.Errorf("could read neither /perm/gokr-pw.txt, nor /etc/gokr-pw.txt, nor /gokr-pw.txt: %v", err)
|
|
}
|
|
httpPassword = pw
|
|
|
|
if err := configureLoopback(); err != nil {
|
|
return err
|
|
}
|
|
|
|
initRemoteSyslog()
|
|
if err := setupTLS(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
sh := exec.Command(shell)
|
|
sh.Stdin = os.Stdin
|
|
sh.Stdout = os.Stdout
|
|
sh.Stderr = os.Stderr
|
|
if err := sh.Run(); err != nil {
|
|
log.Printf("sh: %v", err)
|
|
lastErr = err
|
|
}
|
|
return nil
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
var add []*service
|
|
|
|
// Supervise continuously restarts the processes specified in commands
|
|
// 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 Supervise(commands []*exec.Cmd) error {
|
|
services := make([]*service, len(commands))
|
|
for idx, cmd := range commands {
|
|
services[idx] = &service{cmd: cmd}
|
|
if cmd.Path == "/user/backupd" {
|
|
add = append(add, services[idx])
|
|
}
|
|
}
|
|
superviseServices(services)
|
|
|
|
initStatus(services)
|
|
|
|
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
|
|
}
|
|
for _, v := range add {
|
|
v.Signal(unix.SIGUSR1)
|
|
}
|
|
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)
|
|
}
|
|
|
|
go func() {
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, unix.SIGHUP)
|
|
|
|
for range c {
|
|
if err := updateListenerPairs(httpPort, httpsPort, useTLS, tlsConfig); err != nil {
|
|
log.Printf("updating listeners: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, unix.SIGTERM)
|
|
|
|
for range c {
|
|
killSupervisedServices()
|
|
os.Exit(0)
|
|
}
|
|
}()
|
|
|
|
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
|
|
}
|
|
|
|
// WaitForClock returns once the system clock appears to have been
|
|
// set. Assumes that the system boots with a clock value of January 1,
|
|
// 1970 UTC (UNIX epoch), as is the case on the Raspberry Pi 3.
|
|
func WaitForClock() {
|
|
epochPlus1Year := time.Unix(60*60*24*365, 0)
|
|
for {
|
|
if time.Now().After(epochPlus1Year) {
|
|
return
|
|
}
|
|
// Sleeps for 1 real second, regardless of wall-clock time.
|
|
// See https://github.com/golang/proposal/blob/master/design/12914-monotonic.md
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|