Skip to content

[draft] feat(usbgadget): share JetKVM's internet via USB ethernet #459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 38 additions & 23 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,28 +75,29 @@ func (m *KeyboardMacro) Validate() error {
}

type Config struct {
CloudURL string `json:"cloud_url"`
CloudAppURL string `json:"cloud_app_url"`
CloudToken string `json:"cloud_token"`
GoogleIdentity string `json:"google_identity"`
JigglerEnabled bool `json:"jiggler_enabled"`
AutoUpdateEnabled bool `json:"auto_update_enabled"`
IncludePreRelease bool `json:"include_pre_release"`
HashedPassword string `json:"hashed_password"`
LocalAuthToken string `json:"local_auth_token"`
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
EdidString string `json:"hdmi_edid_string"`
ActiveExtension string `json:"active_extension"`
DisplayMaxBrightness int `json:"display_max_brightness"`
DisplayDimAfterSec int `json:"display_dim_after_sec"`
DisplayOffAfterSec int `json:"display_off_after_sec"`
TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", ""
UsbConfig *usbgadget.Config `json:"usb_config"`
UsbDevices *usbgadget.Devices `json:"usb_devices"`
NetworkConfig *network.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"`
CloudURL string `json:"cloud_url"`
CloudAppURL string `json:"cloud_app_url"`
CloudToken string `json:"cloud_token"`
GoogleIdentity string `json:"google_identity"`
JigglerEnabled bool `json:"jiggler_enabled"`
AutoUpdateEnabled bool `json:"auto_update_enabled"`
IncludePreRelease bool `json:"include_pre_release"`
HashedPassword string `json:"hashed_password"`
LocalAuthToken string `json:"local_auth_token"`
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
EdidString string `json:"hdmi_edid_string"`
ActiveExtension string `json:"active_extension"`
DisplayMaxBrightness int `json:"display_max_brightness"`
DisplayDimAfterSec int `json:"display_dim_after_sec"`
DisplayOffAfterSec int `json:"display_off_after_sec"`
TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", ""
UsbConfig *usbgadget.Config `json:"usb_config"`
UsbDevices *usbgadget.Devices `json:"usb_devices"`
NetworkConfig *network.NetworkConfig `json:"network_config"`
UsbNetworkConfig *network.UsbNetworkConfig `json:"usb_network_config"`
DefaultLogLevel string `json:"default_log_level"`
}

const configPath = "/userdata/kvm_config.json"
Expand All @@ -123,8 +124,18 @@ var defaultConfig = &Config{
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
EthernetEcm: false,
EthernetEem: false,
EthernetNcm: false,
EthernetRndis: false,
},
NetworkConfig: &network.NetworkConfig{
NatEnable: false,
},
UsbNetworkConfig: &network.UsbNetworkConfig{
IPv4Addr: "172.16.55.1/24",
IPv4Network: "172.16.55.0/24",
},
NetworkConfig: &network.NetworkConfig{},
DefaultLogLevel: "INFO",
}

Expand Down Expand Up @@ -172,6 +183,10 @@ func LoadConfig() {
loadedConfig.NetworkConfig = defaultConfig.NetworkConfig
}

if loadedConfig.UsbNetworkConfig == nil {
loadedConfig.UsbNetworkConfig = defaultConfig.UsbNetworkConfig
}

config = &loadedConfig

logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel)
Expand Down
7 changes: 7 additions & 0 deletions internal/network/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ type NetworkConfig struct {
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`

NatEnable bool `json:"nat_enable,omitempty" default:"false" required:"true"`
}

type UsbNetworkConfig struct {
IPv4Addr string `json:"ipv4_addr,omitempty" validate_type:"ipv4" default:"172.16.55.1/24" required:"true"`
IPv4Network string `json:"ipv4_network,omitempty" validate_type:"ipv4" default:"172.16.55.0/24" required:"true"`
}

func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
Expand Down
78 changes: 78 additions & 0 deletions internal/network/nat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package network

import (
"fmt"
"os"
"os/exec"
)

const (
procIpv4ForwardPath = "/proc/sys/net/ipv4/ip_forward"
)

func (s *NetworkInterfaceState) UsbNetworkConfig() *UsbNetworkConfig {
return s.usbNetConfig
}

func (s *NetworkInterfaceState) reconfigureNat(wantNat bool, sourceAddr string) error {
scopedLogger := s.l.With().Str("iface", s.interfaceName).Logger()

if !wantNat {
if s.natEnabled {
scopedLogger.Info().Msg("disabling NAT")
err := disableNat()
if err != nil {
s.l.Error().Err(err).Msg("failed to disable NAT")
}
}
return nil
}

if wantNat && s.IsOnline() {
scopedLogger.Info().Msg("enabling NAT")
err := enableNat(sourceAddr, s.interfaceName, s.IPv4String())
if err != nil {
s.l.Error().Err(err).Msg("failed to enable NAT")
}
s.natEnabled = true
return nil
}

return nil
}

func enableNat(sourceAddr string, oIfName string, snatToAddr string) error {
if err := os.WriteFile(procIpv4ForwardPath, []byte("1"), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", procIpv4ForwardPath, err)
}

if err := exec.Command("nft", "add table nat").Run(); err != nil {
return fmt.Errorf("failed to add table nat: %w", err)
}

if err := exec.Command("nft", "flush table nat").Run(); err != nil {
return fmt.Errorf("failed to flush table nat: %w", err)
}

if err := exec.Command("nft", "add chain nat postrouting { type nat hook postrouting priority 100 ; }").Run(); err != nil {
return fmt.Errorf("failed to add chain nat: %w", err)
}

if err := exec.Command("nft", "add rule nat postrouting ip saddr", sourceAddr, "oif", oIfName, "snat to", snatToAddr).Run(); err != nil {
return fmt.Errorf("failed to add postrouting rule: %w", err)
}

return nil
}

func disableNat() error {
if err := os.WriteFile(procIpv4ForwardPath, []byte("0"), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", procIpv4ForwardPath, err)
}

if err := exec.Command("nft", "delete table nat").Run(); err != nil {
return fmt.Errorf("failed to run nft: %w", err)
}

return nil
}
28 changes: 20 additions & 8 deletions internal/network/netif.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ type NetworkInterfaceState struct {
l *zerolog.Logger
stateLock sync.Mutex

config *NetworkConfig
dhcpClient *udhcpc.DHCPClient
config *NetworkConfig
usbNetConfig *UsbNetworkConfig
dhcpClient *udhcpc.DHCPClient

defaultHostname string
currentHostname string
Expand All @@ -37,6 +38,8 @@ type NetworkInterfaceState struct {
onInitialCheck func(state *NetworkInterfaceState)
cbConfigChange func(config *NetworkConfig)

natEnabled bool

checked bool
}

Expand All @@ -50,6 +53,7 @@ type NetworkInterfaceOptions struct {
OnDhcpLeaseChange func(lease *udhcpc.Lease)
OnConfigChange func(config *NetworkConfig)
NetworkConfig *NetworkConfig
UsbNetworkConfig *UsbNetworkConfig
}

func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceState, error) {
Expand All @@ -72,10 +76,17 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
defaultHostname: opts.DefaultHostname,
stateLock: sync.Mutex{},
l: l,
onStateChange: opts.OnStateChange,
onInitialCheck: opts.OnInitialCheck,
cbConfigChange: opts.OnConfigChange,
config: opts.NetworkConfig,
onStateChange: func(s *NetworkInterfaceState) {
s.reconfigureNat(opts.NetworkConfig.NatEnable, opts.UsbNetworkConfig.IPv4Network)
opts.OnStateChange(s)
},
onInitialCheck: func(s *NetworkInterfaceState) {
s.reconfigureNat(opts.NetworkConfig.NatEnable, opts.UsbNetworkConfig.IPv4Network)
opts.OnInitialCheck(s)
},
cbConfigChange: opts.OnConfigChange,
config: opts.NetworkConfig,
usbNetConfig: opts.UsbNetworkConfig,
}

// create the dhcp client
Expand Down Expand Up @@ -174,11 +185,12 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
}

if changed {
scopedLogger := s.l.With().Str("iface", s.interfaceName).Logger()
if interfaceGoingUp {
s.l.Info().Msg("interface state transitioned to up")
scopedLogger.Info().Msg("interface state transitioned to up")
dhcpTargetState = DhcpTargetStateRenew
} else if interfaceGoingDown {
s.l.Info().Msg("interface state transitioned to down")
scopedLogger.Info().Msg("interface state transitioned to down")
}
}

Expand Down
13 changes: 13 additions & 0 deletions internal/usbgadget/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
// mass storage
"mass_storage_base": massStorageBaseConfig,
"mass_storage_lun0": massStorageLun0Config,
// ethernet
"ethernet_ecm": ethernetEcmConfig,
"ethernet_eem": ethernetEemConfig,
"ethernet_ncm": ethernetNcmConfig,
"ethernet_rndis": ethernetRndisConfig,
}

func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool {
Expand All @@ -77,6 +82,14 @@ func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool {
return u.enabledDevices.MassStorage
case "mass_storage_lun0":
return u.enabledDevices.MassStorage
case "ethernet_ecm":
return u.enabledDevices.EthernetEcm
case "ethernet_eem":
return u.enabledDevices.EthernetEem
case "ethernet_ncm":
return u.enabledDevices.EthernetNcm
case "ethernet_rndis":
return u.enabledDevices.EthernetRndis
default:
return true
}
Expand Down
60 changes: 60 additions & 0 deletions internal/usbgadget/ethernet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package usbgadget

const (
usbEthernetDevice = "usb0"
)

// Ethernet Control Model (ECM)
var ethernetEcmConfig = gadgetConfigItem{
order: 4000,
path: []string{"functions", "ecm.usb0"},
configPath: []string{"ecm.usb0"},
attrs: gadgetAttributes{
"host_addr": "", // MAC address of target host (randomly select)
"dev_addr": "", // MAC address of JetKVM (randomly select)
},
}

// Ethernet Emulation Model (EEM)
var ethernetEemConfig = gadgetConfigItem{
order: 4001,
path: []string{"functions", "eem.usb0"},
configPath: []string{"eem.usb0"},
attrs: gadgetAttributes{
"host_addr": "", // MAC address of target host (randomly select)
"dev_addr": "", // MAC address of JetKVM (randomly select)
},
}

// Network Control Model (NCM)
var ethernetNcmConfig = gadgetConfigItem{
order: 4001,
path: []string{"functions", "ncm.usb0"},
configPath: []string{"ncm.usb0"},
attrs: gadgetAttributes{
"host_addr": "", // MAC address of target host (randomly select)
"dev_addr": "", // MAC address of JetKVM (randomly select)
},
}

// Remote Network Driver Interface Specification (RNDIS)
var ethernetRndisConfig = gadgetConfigItem{
order: 4001,
path: []string{"functions", "rndis.usb0"},
configPath: []string{"rndis.usb0"},
attrs: gadgetAttributes{
"host_addr": "", // MAC address of target host (randomly select)
"dev_addr": "", // MAC address of JetKVM (randomly select)
},
}

func (u *UsbGadget) UsbEthernetEnabled() bool {
return u.isGadgetConfigItemEnabled("ecm") ||
u.isGadgetConfigItemEnabled("eem") ||
u.isGadgetConfigItemEnabled("ncm") ||
u.isGadgetConfigItemEnabled("rndis")
}

func (u *UsbGadget) UsbEthernetDevice() string {
return usbEthernetDevice
}
8 changes: 8 additions & 0 deletions internal/usbgadget/usbgadget.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ type Devices struct {
RelativeMouse bool `json:"relative_mouse"`
Keyboard bool `json:"keyboard"`
MassStorage bool `json:"mass_storage"`
EthernetEcm bool `json:"ethernet_ecm"`
EthernetEem bool `json:"ethernet_eem"`
EthernetNcm bool `json:"ethernet_ncm"`
EthernetRndis bool `json:"ethernet_rndis"`
}

// Config is a struct that represents the customizations for a USB gadget.
Expand All @@ -36,6 +40,10 @@ var defaultUsbGadgetDevices = Devices{
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
EthernetEcm: false,
EthernetEem: false,
EthernetNcm: false,
EthernetRndis: false,
}

// UsbGadget is a struct that represents a USB gadget.
Expand Down
9 changes: 5 additions & 4 deletions network.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ func initNetwork() error {
ensureConfigLoaded()

state, err := network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{
DefaultHostname: GetDefaultHostname(),
InterfaceName: NetIfName,
NetworkConfig: config.NetworkConfig,
Logger: networkLogger,
DefaultHostname: GetDefaultHostname(),
InterfaceName: NetIfName,
NetworkConfig: config.NetworkConfig,
UsbNetworkConfig: config.UsbNetworkConfig,
Logger: networkLogger,
OnStateChange: func(state *network.NetworkInterfaceState) {
networkStateChanged()
},
Expand Down
5 changes: 5 additions & 0 deletions usb.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ func initUsbGadget() {
usbLogger,
)

err := initUsbEthernet(gadget)
if err != nil {
usbLogger.Err(err).Msg("Failed to initialize USB Ethernet")
}

go func() {
for {
checkUSBState()
Expand Down
Loading