network enhanecment / refactor (#361)

* chore(network): improve connectivity check

* refactor(network): rewrite network and timesync component

* feat(display): show cloud connection status

* chore: change logging verbosity

* chore(websecure): update log message

* fix(ota): validate root certificate when downloading update

* feat(ui): add network settings tab

* fix(display): cloud connecting animation

* fix: golintci issues

* feat: add network settings tab

* feat(timesync): query servers in parallel

* refactor(network): move to internal/network package

* feat(timesync): add metrics

* refactor(log): move log to internal/logging package

* refactor(mdms): move mdns to internal/mdns package

* feat(developer): add pprof endpoint

* feat(logging): add a simple logging streaming endpoint

* fix(mdns): do not start mdns until network is up

* feat(network): allow users to update network settings from ui

* fix(network): handle errors when net.IPAddr is nil

* fix(mdns): scopedLogger SIGSEGV

* fix(dhcp): watch directory instead of file to catch fsnotify.Create event

* refactor(nbd): move platform-specific code to different files

* refactor(native): move platform-specific code to different files

* chore: fix linter issues

* chore(dev_deploy): allow to override PION_LOG_TRACE
This commit is contained in:
Aveline
2025-04-16 01:39:23 +02:00
committed by GitHub
parent 2b2a14204d
commit 189b84380b
71 changed files with 4938 additions and 825 deletions

View File

@@ -0,0 +1,12 @@
package udhcpc
func (u *DHCPClient) GetNtpServers() []string {
if u.lease == nil {
return nil
}
servers := make([]string, len(u.lease.NTPServers))
for i, server := range u.lease.NTPServers {
servers[i] = server.String()
}
return servers
}

186
internal/udhcpc/parser.go Normal file
View File

@@ -0,0 +1,186 @@
package udhcpc
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"reflect"
"strconv"
"strings"
"time"
)
type Lease struct {
// from https://udhcp.busybox.net/README.udhcpc
IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP
Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask
Broadcast net.IP `env:"broadcast" json:"broadcast"` // The broadcast address for this network
TTL int `env:"ipttl" json:"ttl,omitempty"` // The TTL to use for this network
MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU to use for this network
HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname
Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network
BootPNextServer net.IP `env:"siaddr" json:"bootp_next_server,omitempty"` // The bootp next server option
BootPServerName string `env:"sname" json:"bootp_server_name,omitempty"` // The bootp server name option
BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option
Timezone string `env:"timezone" json:"timezone,omitempty"` // Offset in seconds from UTC
Routers []net.IP `env:"router" json:"routers,omitempty"` // A list of routers
DNS []net.IP `env:"dns" json:"dns_servers,omitempty"` // A list of DNS servers
NTPServers []net.IP `env:"ntpsrv" json:"ntp_servers,omitempty"` // A list of NTP servers
LPRServers []net.IP `env:"lprsvr" json:"lpr_servers,omitempty"` // A list of LPR servers
TimeServers []net.IP `env:"timesvr" json:"_time_servers,omitempty"` // A list of time servers (obsolete)
IEN116NameServers []net.IP `env:"namesvr" json:"_name_servers,omitempty"` // A list of IEN 116 name servers (obsolete)
LogServers []net.IP `env:"logsvr" json:"_log_servers,omitempty"` // A list of MIT-LCS UDP log servers (obsolete)
CookieServers []net.IP `env:"cookiesvr" json:"_cookie_servers,omitempty"` // A list of RFC 865 cookie servers (obsolete)
WINSServers []net.IP `env:"wins" json:"_wins_servers,omitempty"` // A list of WINS servers
SwapServer net.IP `env:"swapsvr" json:"_swap_server,omitempty"` // The IP address of the client's swap server
BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile
RootPath string `env:"rootpath" json:"root_path,omitempty"` // The path name of the client's root disk
LeaseTime time.Duration `env:"lease" json:"lease,omitempty"` // The lease time, in seconds
DHCPType string `env:"dhcptype" json:"dhcp_type,omitempty"` // DHCP message type (safely ignored)
ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server
Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK
TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name
BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file name
Uptime time.Duration `env:"uptime" json:"uptime,omitempty"` // The uptime of the device when the lease was obtained, in seconds
LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease
isEmpty map[string]bool
}
func (l *Lease) setIsEmpty(m map[string]bool) {
l.isEmpty = m
}
func (l *Lease) IsEmpty(key string) bool {
return l.isEmpty[key]
}
func (l *Lease) ToJSON() string {
json, err := json.Marshal(l)
if err != nil {
return ""
}
return string(json)
}
func (l *Lease) SetLeaseExpiry() (time.Time, error) {
if l.Uptime == 0 || l.LeaseTime == 0 {
return time.Time{}, fmt.Errorf("uptime or lease time isn't set")
}
// get the uptime of the device
file, err := os.Open("/proc/uptime")
if err != nil {
return time.Time{}, fmt.Errorf("failed to open uptime file: %w", err)
}
defer file.Close()
var uptime time.Duration
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
parts := strings.Split(text, " ")
uptime, err = time.ParseDuration(parts[0] + "s")
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse uptime: %w", err)
}
}
relativeLeaseRemaining := (l.Uptime + l.LeaseTime) - uptime
leaseExpiry := time.Now().Add(relativeLeaseRemaining)
l.LeaseExpiry = &leaseExpiry
return leaseExpiry, nil
}
func UnmarshalDHCPCLease(lease *Lease, str string) error {
// parse the lease file as a map
data := make(map[string]string)
for _, line := range strings.Split(str, "\n") {
line = strings.TrimSpace(line)
// skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
data[key] = value
}
// now iterate over the lease struct and set the values
leaseType := reflect.TypeOf(lease).Elem()
leaseValue := reflect.ValueOf(lease).Elem()
valuesParsed := make(map[string]bool)
for i := 0; i < leaseType.NumField(); i++ {
field := leaseValue.Field(i)
// get the env tag
key := leaseType.Field(i).Tag.Get("env")
if key == "" {
continue
}
valuesParsed[key] = false
// get the value from the data map
value, ok := data[key]
if !ok || value == "" {
continue
}
switch field.Interface().(type) {
case string:
field.SetString(value)
case int:
val, err := strconv.Atoi(value)
if err != nil {
continue
}
field.SetInt(int64(val))
case time.Duration:
val, err := time.ParseDuration(value + "s")
if err != nil {
continue
}
field.Set(reflect.ValueOf(val))
case net.IP:
ip := net.ParseIP(value)
if ip == nil {
continue
}
field.Set(reflect.ValueOf(ip))
case []net.IP:
val := make([]net.IP, 0)
for _, ipStr := range strings.Fields(value) {
ip := net.ParseIP(ipStr)
if ip == nil {
continue
}
val = append(val, ip)
}
field.Set(reflect.ValueOf(val))
default:
return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
}
valuesParsed[key] = true
}
lease.setIsEmpty(valuesParsed)
return nil
}

View File

@@ -0,0 +1,74 @@
package udhcpc
import (
"testing"
"time"
)
func TestUnmarshalDHCPCLease(t *testing.T) {
lease := &Lease{}
err := UnmarshalDHCPCLease(lease, `
# generated @ Mon Jan 4 19:31:53 UTC 2021
# 19:31:53 up 0 min, 0 users, load average: 0.72, 0.14, 0.04
# the date might be inaccurate if the clock is not set
ip=192.168.0.240
siaddr=192.168.0.1
sname=
boot_file=
subnet=255.255.255.0
timezone=
router=192.168.0.1
timesvr=
namesvr=
dns=172.19.53.2
logsvr=
cookiesvr=
lprsvr=
hostname=
bootsize=
domain=
swapsvr=
rootpath=
ipttl=
mtu=
broadcast=
ntpsrv=162.159.200.123
wins=
lease=172800
dhcptype=
serverid=192.168.0.1
message=
tftp=
bootfile=
`)
if lease.IPAddress.String() != "192.168.0.240" {
t.Fatalf("expected ip to be 192.168.0.240, got %s", lease.IPAddress.String())
}
if lease.Netmask.String() != "255.255.255.0" {
t.Fatalf("expected netmask to be 255.255.255.0, got %s", lease.Netmask.String())
}
if len(lease.Routers) != 1 {
t.Fatalf("expected 1 router, got %d", len(lease.Routers))
}
if lease.Routers[0].String() != "192.168.0.1" {
t.Fatalf("expected router to be 192.168.0.1, got %s", lease.Routers[0].String())
}
if len(lease.NTPServers) != 1 {
t.Fatalf("expected 1 timeserver, got %d", len(lease.NTPServers))
}
if lease.NTPServers[0].String() != "162.159.200.123" {
t.Fatalf("expected timeserver to be 162.159.200.123, got %s", lease.NTPServers[0].String())
}
if len(lease.DNS) != 1 {
t.Fatalf("expected 1 dns, got %d", len(lease.DNS))
}
if lease.DNS[0].String() != "172.19.53.2" {
t.Fatalf("expected dns to be 172.19.53.2, got %s", lease.DNS[0].String())
}
if lease.LeaseTime != 172800*time.Second {
t.Fatalf("expected lease time to be 172800 seconds, got %d", lease.LeaseTime)
}
if err != nil {
t.Fatal(err)
}
}

212
internal/udhcpc/proc.go Normal file
View File

@@ -0,0 +1,212 @@
package udhcpc
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
)
func readFileNoStat(filename string) ([]byte, error) {
const maxBufferSize = 1024 * 1024
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
reader := io.LimitReader(f, maxBufferSize)
return io.ReadAll(reader)
}
func toCmdline(path string) ([]string, error) {
data, err := readFileNoStat(path)
if err != nil {
return nil, err
}
if len(data) < 1 {
return []string{}, nil
}
return strings.Split(string(bytes.TrimRight(data, "\x00")), "\x00"), nil
}
func (p *DHCPClient) findUdhcpcProcess() (int, error) {
// read procfs for udhcpc processes
// we do not use procfs.AllProcs() because we want to avoid the overhead of reading the entire procfs
processes, err := os.ReadDir("/proc")
if err != nil {
return 0, err
}
// iterate over the processes
for _, d := range processes {
// check if file is numeric
pid, err := strconv.Atoi(d.Name())
if err != nil {
continue
}
// check if it's a directory
if !d.IsDir() {
continue
}
cmdline, err := toCmdline(filepath.Join("/proc", d.Name(), "cmdline"))
if err != nil {
continue
}
if len(cmdline) < 1 {
continue
}
if cmdline[0] != "udhcpc" {
continue
}
cmdlineText := strings.Join(cmdline, " ")
// check if it's a udhcpc process
if strings.Contains(cmdlineText, fmt.Sprintf("-i %s", p.InterfaceName)) {
p.logger.Debug().
Str("pid", d.Name()).
Interface("cmdline", cmdline).
Msg("found udhcpc process")
return pid, nil
}
}
return 0, errors.New("udhcpc process not found")
}
func (c *DHCPClient) getProcessPid() (int, error) {
var pid int
if c.pidFile != "" {
// try to read the pid file
pidHandle, err := os.ReadFile(c.pidFile)
if err != nil {
c.logger.Warn().Err(err).
Str("pidFile", c.pidFile).Msg("failed to read udhcpc pid file")
}
// if it exists, try to read the pid
if pidHandle != nil {
pidFromFile, err := strconv.Atoi(string(pidHandle))
if err != nil {
c.logger.Warn().Err(err).
Str("pidFile", c.pidFile).Msg("failed to convert pid file to int")
}
pid = pidFromFile
}
}
// if the pid is 0, try to find the pid using procfs
if pid == 0 {
newPid, err := c.findUdhcpcProcess()
if err != nil {
return 0, err
}
pid = newPid
}
return pid, nil
}
func (c *DHCPClient) getProcess() *os.Process {
pid, err := c.getProcessPid()
if err != nil {
return nil
}
process, err := os.FindProcess(pid)
if err != nil {
c.logger.Warn().Err(err).
Int("pid", pid).Msg("failed to find process")
return nil
}
return process
}
func (c *DHCPClient) GetProcess() *os.Process {
if c.process == nil {
process := c.getProcess()
if process == nil {
return nil
}
c.process = process
}
err := c.process.Signal(syscall.Signal(0))
if err != nil && errors.Is(err, os.ErrProcessDone) {
oldPid := c.process.Pid
c.process = nil
c.process = c.getProcess()
if c.process == nil {
c.logger.Error().Msg("failed to find new udhcpc process")
return nil
}
c.logger.Warn().
Int("oldPid", oldPid).
Int("newPid", c.process.Pid).
Msg("udhcpc process pid changed")
} else if err != nil {
c.logger.Warn().Err(err).
Int("pid", c.process.Pid).Msg("udhcpc process is not running")
}
return c.process
}
func (c *DHCPClient) KillProcess() error {
process := c.GetProcess()
if process == nil {
return nil
}
return process.Kill()
}
func (c *DHCPClient) ReleaseProcess() error {
process := c.GetProcess()
if process == nil {
return nil
}
return process.Release()
}
func (c *DHCPClient) signalProcess(sig syscall.Signal) error {
process := c.GetProcess()
if process == nil {
return nil
}
s := process.Signal(sig)
if s != nil {
c.logger.Warn().Err(s).
Int("pid", process.Pid).
Str("signal", sig.String()).
Msg("failed to signal udhcpc process")
return s
}
return nil
}
func (c *DHCPClient) Renew() error {
return c.signalProcess(syscall.SIGUSR1)
}
func (c *DHCPClient) Release() error {
return c.signalProcess(syscall.SIGUSR2)
}

191
internal/udhcpc/udhcpc.go Normal file
View File

@@ -0,0 +1,191 @@
package udhcpc
import (
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/fsnotify/fsnotify"
"github.com/rs/zerolog"
)
const (
DHCPLeaseFile = "/run/udhcpc.%s.info"
DHCPPidFile = "/run/udhcpc.%s.pid"
)
type DHCPClient struct {
InterfaceName string
leaseFile string
pidFile string
lease *Lease
logger *zerolog.Logger
process *os.Process
onLeaseChange func(lease *Lease)
}
type DHCPClientOptions struct {
InterfaceName string
PidFile string
Logger *zerolog.Logger
OnLeaseChange func(lease *Lease)
}
var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
func NewDHCPClient(options *DHCPClientOptions) *DHCPClient {
if options.Logger == nil {
options.Logger = &defaultLogger
}
l := options.Logger.With().Str("interface", options.InterfaceName).Logger()
return &DHCPClient{
InterfaceName: options.InterfaceName,
logger: &l,
leaseFile: fmt.Sprintf(DHCPLeaseFile, options.InterfaceName),
pidFile: options.PidFile,
onLeaseChange: options.OnLeaseChange,
}
}
func (c *DHCPClient) getWatchPaths() []string {
watchPaths := make(map[string]interface{})
watchPaths[filepath.Dir(c.leaseFile)] = nil
if c.pidFile != "" {
watchPaths[filepath.Dir(c.pidFile)] = nil
}
paths := make([]string, 0)
for path := range watchPaths {
paths = append(paths, path)
}
return paths
}
// Run starts the DHCP client and watches the lease file for changes.
// this isn't a blocking call, and the lease file is reloaded when a change is detected.
func (c *DHCPClient) Run() error {
err := c.loadLeaseFile()
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
defer watcher.Close()
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
continue
}
if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
continue
}
if event.Name == c.leaseFile {
c.logger.Debug().
Str("event", event.Op.String()).
Str("path", event.Name).
Msg("udhcpc lease file updated, reloading lease")
_ = c.loadLeaseFile()
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
c.logger.Error().Err(err).Msg("error watching lease file")
}
}
}()
for _, path := range c.getWatchPaths() {
err = watcher.Add(path)
if err != nil {
c.logger.Error().
Err(err).
Str("path", path).
Msg("failed to watch directory")
return err
}
}
// TODO: update udhcpc pid file
// we'll comment this out for now because the pid might change
// process := c.GetProcess()
// if process == nil {
// c.logger.Error().Msg("udhcpc process not found")
// }
// block the goroutine until the lease file is updated
<-make(chan struct{})
return nil
}
func (c *DHCPClient) loadLeaseFile() error {
file, err := os.ReadFile(c.leaseFile)
if err != nil {
return err
}
data := string(file)
if data == "" {
c.logger.Debug().Msg("udhcpc lease file is empty")
return nil
}
lease := &Lease{}
err = UnmarshalDHCPCLease(lease, string(file))
if err != nil {
return err
}
isFirstLoad := c.lease == nil
c.lease = lease
if lease.IPAddress == nil {
c.logger.Info().
Interface("lease", lease).
Str("data", string(file)).
Msg("udhcpc lease cleared")
return nil
}
msg := "udhcpc lease updated"
if isFirstLoad {
msg = "udhcpc lease loaded"
}
leaseExpiry, err := lease.SetLeaseExpiry()
if err != nil {
c.logger.Error().Err(err).Msg("failed to get dhcp lease expiry")
} else {
expiresIn := time.Until(leaseExpiry)
c.logger.Info().
Interface("expiry", leaseExpiry).
Str("expiresIn", expiresIn.String()).
Msg("current dhcp lease expiry time calculated")
}
c.onLeaseChange(lease)
c.logger.Info().
Str("ip", lease.IPAddress.String()).
Str("leaseTime", lease.LeaseTime.String()).
Interface("data", lease).
Msg(msg)
return nil
}
func (c *DHCPClient) GetLease() *Lease {
return c.lease
}