Files
Michael Stapelberg 267d2ac640 wait: implement WaitFor("net-online")
tested in a VM:

2025/02/23 10:42:10 waiting for net-online
2025/02/23 10:42:10 waiting for net-online: lookup(gokrazy.org): lookup gokrazy.org on [::1]:53: read udp [::1]:56467->[::1]:53: read: connection refused
2025/02/23 10:42:11 waiting for net-online: lookup(gokrazy.org): lookup gokrazy.org on [::1]:53: read udp [::1]:36567->[::1]:53: read: connection refused
2025/02/23 10:42:12 waiting for net-online: lookup(gokrazy.org): lookup gokrazy.org on [::1]:53: read udp [::1]:36773->[::1]:53: read: connection refused
2025/02/23 10:42:13 waiting for net-online: lookup(gokrazy.org): lookup gokrazy.org on [::1]:53: read udp [::1]:34451->[::1]:53: read: connection refused
2025/02/23 10:42:14 waiting for net-online: lookup(gokrazy.org): lookup gokrazy.org on [::1]:53: read udp [::1]:51114->[::1]:53: read: connection refused
2025/02/23 10:42:15 done waiting after 5.030385208s
2025-02-23 10:42:56 +01:00

145 lines
4.0 KiB
Go

package gokrazy
import (
"context"
"errors"
"fmt"
"log"
"net"
"time"
"github.com/vishvananda/netlink"
)
// WaitFor allows waiting for one or more conditions to be true.
// The currently defined conditions are:
//
// - clock: Wait for the system clock to be set, signaling
// that devices like the Raspberry Pi (which loses the time
// when power is lost and boots with the clock set to
// January 1, 1970 UTC (UNIX epoch)) have obtained the
// current time via NTP. On machines that boot with a
// correct time, such as most PCs, this is a no-op.
// - net-route: Wait for a default route to be installed,
// signaling that local network connectivity is available
// (typically via DHCP, or statically configured). Note
// that while the local network may be up, internet connectivity
// might not be available (see net-online), for example
// right after a power outage.
// - net-online: wait for a connectivity check to succeed,
// specifically resolving the gokrazy.org domain name
func WaitFor(things ...string) {
if len(things) == 0 {
return // skip log messages for no-op waits
}
start := time.Now()
// Wait in sequence (but we might wait concurrently in the future).
for _, thing := range things {
switch thing {
case "clock":
log.Printf("waiting for clock")
waitForClock()
case "net-route":
log.Printf("waiting for net-route")
waitForNetRoute()
case "net-online":
log.Printf("waiting for net-online")
waitForNetOnline()
default:
panic(fmt.Sprintf("BUG: gokrazy.WaitFor(%q) unknown", thing))
}
}
log.Printf("done waiting after %v", time.Since(start))
}
// WaitForClock returns once the system clock appears to have been set.
//
// New usages should prefer directly calling WaitFor("clock") instead.
func WaitForClock() {
WaitFor("clock")
}
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)
}
}
var errNoDefaultRoute = errors.New("no default route found")
func waitForNetRoute1() error {
nl, err := netlink.NewHandle(netlink.FAMILY_V4)
if err != nil {
return fmt.Errorf("netlink.New: %v", err)
}
defer nl.Delete()
// This is the address of k.root-servers.net,
// but it does not matter; we just need an IP address
// that will not be null-routed explicitly, so avoid
// public DNS server addresses in case any filtering
// software blocks these.
arbitraryIP := net.ParseIP("193.0.14.129")
routes, err := nl.RouteGet(arbitraryIP)
if err != nil {
return fmt.Errorf("RouteGet(%v): %v", arbitraryIP, err)
}
// We are looking for a route with a gateway, e.g.:
// [{Ifindex: 3 Dst: 193.0.14.129/32 Src: 10.0.0.135 Gw: 10.0.0.1 Flags: [] Table: 254}]
for _, route := range routes {
if route.Gw.IsUnspecified() {
continue
}
return nil // stop waiting, route with gateway found
}
return errNoDefaultRoute
}
func waitForNetRoute() {
for {
if err := waitForNetRoute1(); err != nil {
log.Printf("waiting for net-route: %v", err)
time.Sleep(1 * time.Second)
continue
}
break // no error, wait succeeded
}
}
func waitForNetOnline1() error {
// Cap the time for each resolution attempt to 5s.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var resolver net.Resolver
// If the gokrazy.org domain name is no longer resolveable,
// the entire project is truly defunct and over.
addrs, err := resolver.LookupIPAddr(ctx, "gokrazy.org")
if err != nil {
return fmt.Errorf("lookup(gokrazy.org): %v", err)
}
if len(addrs) == 0 {
return fmt.Errorf("lookup(gokrazy.org) unexpectedly returned no addresses")
}
return nil // connectivity check succeeded
}
func waitForNetOnline() {
for {
if err := waitForNetOnline1(); err != nil {
log.Printf("waiting for net-online: %v", err)
time.Sleep(1 * time.Second)
continue
}
break // no error, wait succeeded
}
}