diff --git a/config.go b/config.go index 6cc7a29..23d4c84 100644 --- a/config.go +++ b/config.go @@ -6,6 +6,7 @@ import ( "os" "sync" + "github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/network" "github.com/jetkvm/kvm/internal/usbgadget" ) @@ -123,6 +124,7 @@ var defaultConfig = &Config{ Keyboard: true, MassStorage: true, }, + NetworkConfig: &network.NetworkConfig{}, DefaultLogLevel: "INFO", } @@ -172,7 +174,7 @@ func LoadConfig() { config = &loadedConfig - rootLogger.UpdateLogLevel() + logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel) logger.Info().Str("path", configPath).Msg("config loaded") } diff --git a/hw.go b/hw.go index 21bffad..20d88eb 100644 --- a/hw.go +++ b/hw.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "regexp" + "strings" "sync" "time" ) @@ -51,6 +52,15 @@ func GetDeviceID() string { return deviceID } +func GetDefaultHostname() string { + deviceId := GetDeviceID() + if deviceId == "unknown_device_id" { + return "jetkvm" + } + + return fmt.Sprintf("jetkvm-%s", strings.ToLower(deviceId)) +} + func runWatchdog() { file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0) if err != nil { diff --git a/internal/mdns/mdns.go b/internal/mdns/mdns.go new file mode 100644 index 0000000..4899180 --- /dev/null +++ b/internal/mdns/mdns.go @@ -0,0 +1,154 @@ +package mdns + +import ( + "fmt" + "net" + "reflect" + "strings" + "sync" + + "github.com/jetkvm/kvm/internal/logging" + pion_mdns "github.com/pion/mdns/v2" + "github.com/rs/zerolog" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +type MDNS struct { + conn *pion_mdns.Conn + lock sync.Mutex + l *zerolog.Logger + + localNames []string + listenOptions *MDNSListenOptions +} + +type MDNSListenOptions struct { + IPv4 bool + IPv6 bool +} + +type MDNSOptions struct { + Logger *zerolog.Logger + LocalNames []string + ListenOptions *MDNSListenOptions +} + +const ( + DefaultAddressIPv4 = pion_mdns.DefaultAddressIPv4 + DefaultAddressIPv6 = pion_mdns.DefaultAddressIPv6 +) + +func NewMDNS(opts *MDNSOptions) (*MDNS, error) { + if opts.Logger == nil { + opts.Logger = logging.GetDefaultLogger() + } + + return &MDNS{ + l: opts.Logger, + lock: sync.Mutex{}, + localNames: opts.LocalNames, + listenOptions: opts.ListenOptions, + }, nil +} + +func (m *MDNS) start(allowRestart bool) error { + m.lock.Lock() + defer m.lock.Unlock() + + if m.conn != nil { + if !allowRestart { + return fmt.Errorf("mDNS server already running") + } + + m.conn.Close() + } + + addr4, err := net.ResolveUDPAddr("udp4", DefaultAddressIPv4) + if err != nil { + return err + } + + addr6, err := net.ResolveUDPAddr("udp6", DefaultAddressIPv6) + if err != nil { + return err + } + + l4, err := net.ListenUDP("udp4", addr4) + if err != nil { + return err + } + + l6, err := net.ListenUDP("udp6", addr6) + if err != nil { + return err + } + + scopeLogger := m.l.With().Interface("local_names", m.localNames).Logger() + + newLocalNames := make([]string, len(m.localNames)) + for i, name := range m.localNames { + newLocalNames[i] = strings.TrimRight(strings.ToLower(name), ".") + if !strings.HasSuffix(newLocalNames[i], ".local") { + newLocalNames[i] = newLocalNames[i] + ".local" + } + } + + mDNSConn, err := pion_mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &pion_mdns.Config{ + LocalNames: newLocalNames, + LoggerFactory: logging.GetPionDefaultLoggerFactory(), + }) + + if err != nil { + scopeLogger.Warn().Err(err).Msg("failed to start mDNS server") + return err + } + + m.conn = mDNSConn + scopeLogger.Info().Msg("mDNS server started") + + return nil +} + +func (m *MDNS) Start() error { + return m.start(false) +} + +func (m *MDNS) Restart() error { + return m.start(true) +} + +func (m *MDNS) Stop() error { + m.lock.Lock() + defer m.lock.Unlock() + + if m.conn == nil { + return nil + } + + return m.conn.Close() +} + +func (m *MDNS) SetLocalNames(localNames []string, always bool) error { + if reflect.DeepEqual(m.localNames, localNames) && !always { + return nil + } + + m.localNames = localNames + m.Restart() + + return nil +} + +func (m *MDNS) SetListenOptions(listenOptions *MDNSListenOptions) error { + if m.listenOptions != nil && + m.listenOptions.IPv4 == listenOptions.IPv4 && + m.listenOptions.IPv6 == listenOptions.IPv6 { + return nil + } + + m.listenOptions = listenOptions + m.Restart() + + return nil +} diff --git a/internal/mdns/utils.go b/internal/mdns/utils.go new file mode 100644 index 0000000..7565eee --- /dev/null +++ b/internal/mdns/utils.go @@ -0,0 +1 @@ +package mdns diff --git a/internal/network/config.go b/internal/network/config.go index 1cfe9bb..1629665 100644 --- a/internal/network/config.go +++ b/internal/network/config.go @@ -1,8 +1,11 @@ package network import ( + "fmt" "net" "time" + + "golang.org/x/net/idna" ) type IPv6Address struct { @@ -33,8 +36,51 @@ type NetworkConfig struct { DNS []string `json:"dns" validate_type:"ipv6"` } `json:"ipv6_static,omitempty" required_if:"ipv6_mode,static"` - LLDPMode string `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` - LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` - MDNSMode string `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` - TimeSyncMode string `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` + LLDPMode string `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` + LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` + MDNSMode string `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` + TimeSyncMode string `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` + TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"` + TimeSyncDisableFallback bool `json:"time_sync_disable_fallback,omitempty" default:"false"` + TimeSyncParallel int `json:"time_sync_parallel,omitempty" default:"4"` +} + +func (s *NetworkInterfaceState) GetHostname() string { + hostname := ToValidHostname(s.config.Hostname) + + if hostname == "" { + return s.defaultHostname + } + + return hostname +} + +func ToValidDomain(domain string) string { + ascii, err := idna.Lookup.ToASCII(domain) + if err != nil { + return "" + } + + return ascii +} + +func (s *NetworkInterfaceState) GetDomain() string { + domain := ToValidDomain(s.config.Domain) + + if domain == "" { + lease := s.dhcpClient.GetLease() + if lease != nil && lease.Domain != "" { + domain = ToValidDomain(lease.Domain) + } + } + + if domain == "" { + return "local" + } + + return domain +} + +func (s *NetworkInterfaceState) GetFQDN() string { + return fmt.Sprintf("%s.%s", s.GetHostname(), s.GetDomain()) } diff --git a/internal/network/hostname.go b/internal/network/hostname.go new file mode 100644 index 0000000..8d9286f --- /dev/null +++ b/internal/network/hostname.go @@ -0,0 +1,124 @@ +package network + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + "sync" + + "golang.org/x/net/idna" +) + +const ( + hostnamePath = "/etc/hostname" + hostsPath = "/etc/hosts" +) + +var ( + hostnameLock sync.Mutex = sync.Mutex{} +) + +func updateEtcHosts(hostname string, fqdn string) error { + // update /etc/hosts + hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive) + if err != nil { + return fmt.Errorf("failed to open %s: %w", hostsPath, err) + } + defer hostsFile.Close() + + // read all lines + hostsFile.Seek(0, io.SeekStart) + lines, err := io.ReadAll(hostsFile) + if err != nil { + return fmt.Errorf("failed to read %s: %w", hostsPath, err) + } + + newLines := []string{} + hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn) + hostLineExists := false + + for _, line := range strings.Split(string(lines), "\n") { + if strings.HasPrefix(line, "127.0.1.1") { + hostLineExists = true + line = hostLine + } + newLines = append(newLines, line) + } + + if !hostLineExists { + newLines = append(newLines, hostLine) + } + + hostsFile.Truncate(0) + hostsFile.Seek(0, io.SeekStart) + hostsFile.Write([]byte(strings.Join(newLines, "\n"))) + + return nil +} + +func ToValidHostname(hostname string) string { + ascii, err := idna.Lookup.ToASCII(hostname) + if err != nil { + return "" + } + return ascii +} + +func SetHostname(hostname string, fqdn string) error { + hostnameLock.Lock() + defer hostnameLock.Unlock() + + hostname = ToValidHostname(strings.TrimSpace(hostname)) + fqdn = ToValidHostname(strings.TrimSpace(fqdn)) + + if hostname == "" { + return fmt.Errorf("invalid hostname: %s", hostname) + } + + if fqdn == "" { + fqdn = hostname + } + + // update /etc/hostname + os.WriteFile(hostnamePath, []byte(hostname), 0644) + + // update /etc/hosts + if err := updateEtcHosts(hostname, fqdn); err != nil { + return fmt.Errorf("failed to update /etc/hosts: %w", err) + } + + // run hostname + if err := exec.Command("hostname", "-F", hostnamePath).Run(); err != nil { + return fmt.Errorf("failed to run hostname: %w", err) + } + + return nil +} + +func (s *NetworkInterfaceState) setHostnameIfNotSame() error { + hostname := s.GetHostname() + currentHostname, _ := os.Hostname() + + fqdn := fmt.Sprintf("%s.%s", hostname, s.GetDomain()) + + if currentHostname == hostname && s.currentFqdn == fqdn && s.currentHostname == hostname { + return nil + } + + scopedLogger := s.l.With().Str("hostname", hostname).Str("fqdn", fqdn).Logger() + + err := SetHostname(hostname, fqdn) + if err != nil { + scopedLogger.Error().Err(err).Msg("failed to set hostname") + return err + } + + s.currentHostname = hostname + s.currentFqdn = fqdn + + scopedLogger.Info().Msg("hostname set") + + return nil +} diff --git a/internal/network/netif.go b/internal/network/netif.go index 8e3370a..11cb6bc 100644 --- a/internal/network/netif.go +++ b/internal/network/netif.go @@ -1,6 +1,7 @@ package network import ( + "fmt" "net" "sync" "time" @@ -29,6 +30,10 @@ type NetworkInterfaceState struct { config *NetworkConfig dhcpClient *udhcpc.DHCPClient + defaultHostname string + currentHostname string + currentFqdn string + onStateChange func(state *NetworkInterfaceState) onInitialCheck func(state *NetworkInterfaceState) @@ -39,21 +44,31 @@ type NetworkInterfaceOptions struct { InterfaceName string DhcpPidFile string Logger *zerolog.Logger + DefaultHostname string OnStateChange func(state *NetworkInterfaceState) OnInitialCheck func(state *NetworkInterfaceState) OnDhcpLeaseChange func(lease *udhcpc.Lease) NetworkConfig *NetworkConfig } -func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) *NetworkInterfaceState { +func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceState, error) { + if opts.NetworkConfig == nil { + return nil, fmt.Errorf("NetworkConfig can not be nil") + } + + if opts.DefaultHostname == "" { + opts.DefaultHostname = "jetkvm" + } + l := opts.Logger s := &NetworkInterfaceState{ - interfaceName: opts.InterfaceName, - stateLock: sync.Mutex{}, - l: l, - onStateChange: opts.OnStateChange, - onInitialCheck: opts.OnInitialCheck, - config: opts.NetworkConfig, + interfaceName: opts.InterfaceName, + defaultHostname: opts.DefaultHostname, + stateLock: sync.Mutex{}, + l: l, + onStateChange: opts.OnStateChange, + onInitialCheck: opts.OnInitialCheck, + config: opts.NetworkConfig, } // create the dhcp client @@ -68,13 +83,15 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) *NetworkInterfaceSt return } + s.setHostnameIfNotSame() + opts.OnDhcpLeaseChange(lease) }, }) s.dhcpClient = dhcpClient - return s + return s, nil } func (s *NetworkInterfaceState) IsUp() bool { @@ -277,6 +294,12 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) { if initialCheck { s.checked = true changed = false + if dhcpTargetState == DhcpTargetStateRenew { + // it's the initial check, we'll start the DHCP client + // dhcpTargetState = DhcpTargetStateStart + // TODO: manage DHCP client start/stop + dhcpTargetState = DhcpTargetStateDoNothing + } } if initialCheck { @@ -326,6 +349,8 @@ func (s *NetworkInterfaceState) Run() error { return err } + _ = s.setHostnameIfNotSame() + // run the dhcp client go s.dhcpClient.Run() // nolint:errcheck diff --git a/internal/network/rpc.go b/internal/network/rpc.go index afdcbc0..0d6361a 100644 --- a/internal/network/rpc.go +++ b/internal/network/rpc.go @@ -39,14 +39,18 @@ type RpcNetworkSettings struct { func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState { ipv6Addresses := make([]RpcIPv6Address, 0) - for _, addr := range s.ipv6Addresses { - ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{ - Address: addr.Prefix.String(), - ValidLifetime: addr.ValidLifetime, - PreferredLifetime: addr.PreferredLifetime, - Scope: addr.Scope, - }) + + if s.ipv6Addresses != nil { + for _, addr := range s.ipv6Addresses { + ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{ + Address: addr.Prefix.String(), + ValidLifetime: addr.ValidLifetime, + PreferredLifetime: addr.PreferredLifetime, + Scope: addr.Scope, + }) + } } + return RpcNetworkState{ InterfaceName: s.interfaceName, MacAddress: s.macAddr.String(), @@ -60,6 +64,10 @@ func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState { } func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings { + if s.config == nil { + return RpcNetworkSettings{} + } + return RpcNetworkSettings{ Hostname: null.StringFrom(s.config.Hostname), Domain: null.StringFrom(s.config.Domain), diff --git a/jsonrpc.go b/jsonrpc.go index 9dd365f..00df3cd 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -960,10 +960,10 @@ var rpcHandlers = map[string]RPCHandler{ "getDeviceID": {Func: rpcGetDeviceID}, "deregisterDevice": {Func: rpcDeregisterDevice}, "getCloudState": {Func: rpcGetCloudState}, - "getNetworkState": {Func: networkState.RpcGetNetworkState}, - "getNetworkSettings": {Func: networkState.RpcGetNetworkSettings}, - "setNetworkSettings": {Func: networkState.RpcSetNetworkSettings, Params: []string{"settings"}}, - "renewDHCPLease": {Func: networkState.RpcRenewDHCPLease}, + "getNetworkState": {Func: rpcGetNetworkState}, + "getNetworkSettings": {Func: rpcGetNetworkSettings}, + "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, + "renewDHCPLease": {Func: rpcRenewDHCPLease}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, diff --git a/main.go b/main.go index 73a4702..25fbb3a 100644 --- a/main.go +++ b/main.go @@ -43,12 +43,26 @@ func Main() { Int("ca_certs_loaded", len(rootcerts.Certs())). Msg("loaded Root CA certificates") - initNetwork() - initTimeSync() + // Initialize network + if err := initNetwork(); err != nil { + logger.Error().Err(err).Msg("failed to initialize network") + os.Exit(1) + } + // Initialize time sync + initTimeSync() timeSync.Start() + // Initialize mDNS + if err := initMdns(); err != nil { + logger.Error().Err(err).Msg("failed to initialize mDNS") + os.Exit(1) + } + + // Initialize native ctrl socket server StartNativeCtrlSocketServer() + + // Initialize native video socket server StartNativeVideoSocketServer() initPrometheus() diff --git a/mdns.go b/mdns.go index 309709e..dd9eaf8 100644 --- a/mdns.go +++ b/mdns.go @@ -1,60 +1,33 @@ package kvm import ( - "net" - - "github.com/pion/mdns/v2" - "golang.org/x/net/ipv4" - "golang.org/x/net/ipv6" + "github.com/jetkvm/kvm/internal/mdns" ) -var mDNSConn *mdns.Conn +var mDNS *mdns.MDNS -func startMDNS() error { - // If server was previously running, stop it - if mDNSConn != nil { - logger.Info().Msg("stopping mDNS server") - err := mDNSConn.Close() - if err != nil { - logger.Warn().Err(err).Msg("failed to stop mDNS server") - } - } - - // Start a new server - hostname := "jetkvm.local" - - scopedLogger := logger.With().Str("hostname", hostname).Logger() - scopedLogger.Info().Msg("starting mDNS server") - - addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) - if err != nil { - return err - } - - addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) - if err != nil { - return err - } - - l4, err := net.ListenUDP("udp4", addr4) - if err != nil { - return err - } - - l6, err := net.ListenUDP("udp6", addr6) - if err != nil { - return err - } - - mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{ - LocalNames: []string{hostname}, //TODO: make it configurable - LoggerFactory: defaultLoggerFactory, +func initMdns() error { + m, err := mdns.NewMDNS(&mdns.MDNSOptions{ + Logger: logger, + LocalNames: []string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, + ListenOptions: &mdns.MDNSListenOptions{ + IPv4: true, + IPv6: true, + }, }) if err != nil { - scopedLogger.Warn().Err(err).Msg("failed to start mDNS server") - mDNSConn = nil return err } - //defer server.Close() + + err = m.Start() + if err != nil { + return err + } + + mDNS = m + return nil } diff --git a/network.go b/network.go index 4e1d42b..75947ce 100644 --- a/network.go +++ b/network.go @@ -1,7 +1,7 @@ package kvm import ( - "os" + "fmt" "github.com/jetkvm/kvm/internal/network" "github.com/jetkvm/kvm/internal/udhcpc" @@ -15,27 +15,72 @@ var ( networkState *network.NetworkInterfaceState ) -func initNetwork() { +func networkStateChanged() { + // do not block the main thread + go waitCtrlAndRequestDisplayUpdate(true) + + // always restart mDNS when the network state changes + if mDNS != nil { + mDNS.SetLocalNames([]string{ + networkState.GetHostname(), + networkState.GetFQDN(), + }, true) + } +} + +func initNetwork() error { ensureConfigLoaded() - networkState = network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{ - InterfaceName: NetIfName, - NetworkConfig: config.NetworkConfig, - Logger: networkLogger, + state, err := network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{ + DefaultHostname: GetDefaultHostname(), + InterfaceName: NetIfName, + NetworkConfig: config.NetworkConfig, + Logger: networkLogger, OnStateChange: func(state *network.NetworkInterfaceState) { - waitCtrlAndRequestDisplayUpdate(true) + networkStateChanged() }, OnInitialCheck: func(state *network.NetworkInterfaceState) { - waitCtrlAndRequestDisplayUpdate(true) + networkStateChanged() }, OnDhcpLeaseChange: func(lease *udhcpc.Lease) { - waitCtrlAndRequestDisplayUpdate(true) + networkStateChanged() + + if currentSession == nil { + return + } + + writeJSONRPCEvent("networkState", networkState.RpcGetNetworkState(), currentSession) }, }) - err := networkState.Run() - if err != nil { - networkLogger.Error().Err(err).Msg("failed to run network state") - os.Exit(1) + if state == nil { + if err == nil { + return fmt.Errorf("failed to create NetworkInterfaceState") + } + return err } + + if err := state.Run(); err != nil { + return err + } + + networkState = state + + return nil +} + +func rpcGetNetworkState() network.RpcNetworkState { + return networkState.RpcGetNetworkState() +} + +func rpcGetNetworkSettings() network.RpcNetworkSettings { + return networkState.RpcGetNetworkSettings() +} + +func rpcSetNetworkSettings(settings network.RpcNetworkSettings) error { + return networkState.RpcSetNetworkSettings(settings) +} + +func rpcRenewDHCPLease() error { + return networkState.RpcRenewDHCPLease() } diff --git a/webrtc.go b/webrtc.go index 5324b23..f6c8529 100644 --- a/webrtc.go +++ b/webrtc.go @@ -10,6 +10,7 @@ import ( "github.com/coder/websocket" "github.com/coder/websocket/wsjson" "github.com/gin-gonic/gin" + "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4" "github.com/rs/zerolog" ) @@ -68,7 +69,7 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) { func newSession(config SessionConfig) (*Session, error) { webrtcSettingEngine := webrtc.SettingEngine{ - LoggerFactory: defaultLoggerFactory, + LoggerFactory: logging.GetPionDefaultLoggerFactory(), } iceServer := webrtc.ICEServer{}