diff --git a/bootserver/Makefile b/bootserver/Makefile index f72f64d..cc6330e 100644 --- a/bootserver/Makefile +++ b/bootserver/Makefile @@ -3,6 +3,9 @@ build: go build -o build/ +buildarm: + env GOOS=linux GOARCH=arm64 go build -o build/bootserver_arm + test: go test -v ./... diff --git a/bootserver/bootoption/bootoption.go b/bootserver/bootoption/bootoption.go index f8fae79..d4ef016 100644 --- a/bootserver/bootoption/bootoption.go +++ b/bootserver/bootoption/bootoption.go @@ -1,5 +1,7 @@ package bootoption +import "github.com/google/uuid" + type EFIApp struct { Name string `json:"name"` Description string `json:"description"` @@ -7,6 +9,7 @@ type EFIApp struct { } type Client struct { + ID uuid.UUID IP string `json:"ip"` Name string `json:"name"` Options []EFIApp `json:"options"` diff --git a/bootserver/bootprotocol/bootprotocol.go b/bootserver/bootprotocol/bootprotocol.go index 5432105..ea028a1 100644 --- a/bootserver/bootprotocol/bootprotocol.go +++ b/bootserver/bootprotocol/bootprotocol.go @@ -64,6 +64,7 @@ type Message interface { encoding.BinaryMarshaler Action() Action ID() uuid.UUID + String() string } type requestMessage struct { @@ -103,6 +104,10 @@ func (rm *requestMessage) ID() uuid.UUID { return rm.id } +func (rm *requestMessage) String() string { + return fmt.Sprintf("%s from %s", ActionRequest.String(), rm.ID().String()) +} + type acceptMessage struct { id uuid.UUID efiApp string @@ -151,6 +156,10 @@ func (am *acceptMessage) ID() uuid.UUID { return am.id } +func (am *acceptMessage) String() string { + return fmt.Sprintf("%s from %s, app %s", ActionAccept.String(), am.ID().String(), am.efiApp) +} + type denyMessage struct { id uuid.UUID reason string @@ -199,6 +208,10 @@ func (dm *denyMessage) ID() uuid.UUID { return dm.id } +func (dm *denyMessage) String() string { + return fmt.Sprintf("%s from %s, reason %q", ActionDeny.String(), dm.ID().String(), dm.reason) +} + func MessageFromBytes(dat []byte) (Message, error) { rawAction, content, found := bytes.Cut(dat, spaceByte) if !found { diff --git a/bootserver/config/config.go b/bootserver/config/config.go index 01ab09e..c0a9f7f 100644 --- a/bootserver/config/config.go +++ b/bootserver/config/config.go @@ -48,14 +48,22 @@ type jsonConf struct { Mode string `json:"mode"` SockPath string `json:"sock"` } `json:"server"` + BootProvider struct { + Iface string `json:"interface"` + Port int `json:"port"` + McastGroup string `json:"multicast_group"` + } `json:"boot_provider"` } type AppConfig struct { - LogLevel logrus.Level - ServerMode ListeningMode - Host string - Port int - SockPath string + LogLevel logrus.Level + ServerMode ListeningMode + Host string + Port int + SockPath string + UPDMcastGroup string + UDPPort int + UDPIface string } func parseLevel(lvlStr string) logrus.Level { @@ -82,14 +90,20 @@ func (ac *AppConfig) UnmarshalJSON(data []byte) error { ac.SockPath = jsonConf.Server.SockPath ac.Host = jsonConf.Server.Host ac.Port = jsonConf.Server.Port + ac.UPDMcastGroup = jsonConf.BootProvider.McastGroup + ac.UDPIface = jsonConf.BootProvider.Iface + ac.UDPPort = jsonConf.BootProvider.Port return nil } var defaultConfig AppConfig = AppConfig{ - LogLevel: logrus.InfoLevel, - ServerMode: ModeNet, - Host: "0.0.0.0", - Port: 5000, + LogLevel: logrus.InfoLevel, + ServerMode: ModeNet, + Host: "0.0.0.0", + Port: 5000, + UPDMcastGroup: "ff02::abcd:1234", + UDPPort: 42, + UDPIface: "eth0", } func New(filepath string) (*AppConfig, error) { diff --git a/bootserver/controllers/client/client.go b/bootserver/controllers/client/client.go index 350eef9..90938bd 100644 --- a/bootserver/controllers/client/client.go +++ b/bootserver/controllers/client/client.go @@ -1,85 +1,85 @@ package client -import ( - "encoding/json" - "fmt" - "io" - "net" - "net/http" +// import ( +// "encoding/json" +// "fmt" +// "io" +// "net" +// "net/http" - "git.faercol.me/faercol/http-boot-server/bootserver/helpers" - "git.faercol.me/faercol/http-boot-server/bootserver/services" - "github.com/sirupsen/logrus" -) +// "git.faercol.me/faercol/http-boot-server/bootserver/helpers" +// "git.faercol.me/faercol/http-boot-server/bootserver/services" +// "github.com/sirupsen/logrus" +// ) -const BootRoute = "/boot" +// const BootRoute = "/boot" -type BootController struct { - clientService *services.ClientHandlerService - l *logrus.Logger -} +// type BootController struct { +// clientService *services.ClientHandlerService +// l *logrus.Logger +// } -func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController { - return &BootController{ - clientService: service, - l: logger, - } -} +// func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController { +// return &BootController{ +// clientService: service, +// l: logger, +// } +// } -func (bc *BootController) getBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, []byte, error) { - bootOption, err := bc.clientService.GetClientSelectedBootOption(clientIP) - if err != nil { - return http.StatusInternalServerError, nil, fmt.Errorf("failed to get boot option: %w", err) - } +// func (bc *BootController) getBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, []byte, error) { +// bootOption, err := bc.clientService.GetClientSelectedBootOption(clientIP) +// if err != nil { +// return http.StatusInternalServerError, nil, fmt.Errorf("failed to get boot option: %w", err) +// } - dat, err := json.Marshal(bootOption) - if err != nil { - return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize body") - } +// dat, err := json.Marshal(bootOption) +// if err != nil { +// return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize body") +// } - w.Header().Add("Content-Type", "application/json") - return http.StatusOK, dat, nil -} +// w.Header().Add("Content-Type", "application/json") +// return http.StatusOK, dat, nil +// } -func (bc *BootController) setBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, error) { - dat, err := io.ReadAll(r.Body) - if err != nil { - return http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err) - } - var option string - if err := json.Unmarshal(dat, &option); err != nil { - return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err) - } +// func (bc *BootController) setBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, error) { +// dat, err := io.ReadAll(r.Body) +// if err != nil { +// return http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err) +// } +// var option string +// if err := json.Unmarshal(dat, &option); err != nil { +// return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err) +// } - if err := bc.clientService.SetClientBootOption(clientIP, option); err != nil { - return http.StatusInternalServerError, fmt.Errorf("failed to set boot option for client: %w", err) - } +// if err := bc.clientService.SetClientBootOption(clientIP, option); err != nil { +// return http.StatusInternalServerError, fmt.Errorf("failed to set boot option for client: %w", err) +// } - return http.StatusAccepted, nil -} +// return http.StatusAccepted, nil +// } -func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) { - clientIP, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - bc.l.Errorf("Failed to read remote IP: %s", err.Error()) - helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, bc.l) - return - } +// func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// clientIP, _, err := net.SplitHostPort(r.RemoteAddr) +// if err != nil { +// bc.l.Errorf("Failed to read remote IP: %s", err.Error()) +// helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, bc.l) +// return +// } - var returncode int - var content []byte +// var returncode int +// var content []byte - switch r.Method { - case http.MethodGet: - returncode, content, err = bc.getBootOption(clientIP, w, r) - case http.MethodPut: - returncode, err = bc.setBootOption(clientIP, w, r) - default: - returncode = http.StatusMethodNotAllowed - } +// switch r.Method { +// case http.MethodGet: +// returncode, content, err = bc.getBootOption(clientIP, w, r) +// case http.MethodPut: +// returncode, err = bc.setBootOption(clientIP, w, r) +// default: +// returncode = http.StatusMethodNotAllowed +// } - if err != nil { - bc.l.Errorf("An error occured while handling boot request: %q", err.Error()) - } - helpers.HandleResponse(w, r, returncode, content, bc.l) -} +// if err != nil { +// bc.l.Errorf("An error occured while handling boot request: %q", err.Error()) +// } +// helpers.HandleResponse(w, r, returncode, content, bc.l) +// } diff --git a/bootserver/main.go b/bootserver/main.go index 5cf6fab..c6dbf25 100644 --- a/bootserver/main.go +++ b/bootserver/main.go @@ -10,6 +10,7 @@ import ( "git.faercol.me/faercol/http-boot-server/bootserver/config" "git.faercol.me/faercol/http-boot-server/bootserver/logger" "git.faercol.me/faercol/http-boot-server/bootserver/server" + "git.faercol.me/faercol/http-boot-server/bootserver/udplistener" ) const stopTimeout = 10 * time.Second @@ -47,7 +48,17 @@ func main() { logger.L.Fatalf("Failed to initialize server: %s", err.Error()) } + logger.L.Info("Initializing UDP listener") + listener, err := udplistener.New(conf.UDPIface, conf.UPDMcastGroup, conf.UDPPort, logger.L) + if err != nil { + logger.L.Fatalf("Failed to initialize UDP listener: %s", err.Error()) + } + if err := listener.Init(); err != nil { + logger.L.Fatalf("Failed to start UDP listener: %s", err.Error()) + } + go s.Run(mainCtx) + go listener.Run(mainCtx) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) diff --git a/bootserver/server/server.go b/bootserver/server/server.go index f50a35a..720f8a4 100644 --- a/bootserver/server/server.go +++ b/bootserver/server/server.go @@ -66,7 +66,7 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) { } service := services.NewClientHandlerService() controllers := map[string]http.Handler{ - client.BootRoute: middlewares.WithLogger(client.NewBootController(logger, service), logger), + // client.BootRoute: middlewares.WithLogger(client.NewBootController(logger, service), logger), client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger), } diff --git a/bootserver/services/services.go b/bootserver/services/services.go index a09d35a..ac74d5b 100644 --- a/bootserver/services/services.go +++ b/bootserver/services/services.go @@ -4,6 +4,7 @@ import ( "errors" "git.faercol.me/faercol/http-boot-server/bootserver/bootoption" + "github.com/google/uuid" ) var ErrUnknownClient = errors.New("unknown client") @@ -11,20 +12,20 @@ var ErrUnselectedBootOption = errors.New("unselected boot option") var ErrUnknownBootOption = errors.New("unknown boot option") type ClientHandlerService struct { - clients map[string]*bootoption.Client + clients map[uuid.UUID]*bootoption.Client } func NewClientHandlerService() *ClientHandlerService { return &ClientHandlerService{ - clients: make(map[string]*bootoption.Client), + clients: make(map[uuid.UUID]*bootoption.Client), } } func (chs *ClientHandlerService) AddClient(client *bootoption.Client) { - chs.clients[client.IP] = client + chs.clients[client.ID] = client } -func (chs *ClientHandlerService) GetClientSelectedBootOption(client string) (*bootoption.EFIApp, error) { +func (chs *ClientHandlerService) GetClientSelectedBootOption(client uuid.UUID) (*bootoption.EFIApp, error) { clientDetails, ok := chs.clients[client] if !ok { return nil, ErrUnknownClient @@ -42,7 +43,7 @@ func (chs *ClientHandlerService) GetClientSelectedBootOption(client string) (*bo return nil, ErrUnknownBootOption } -func (chs *ClientHandlerService) SetClientBootOption(client, option string) error { +func (chs *ClientHandlerService) SetClientBootOption(client uuid.UUID, option string) error { clientDetails, ok := chs.clients[client] if !ok { return ErrUnknownClient diff --git a/bootserver/udplistener/udplistener.go b/bootserver/udplistener/udplistener.go new file mode 100644 index 0000000..e33c9b8 --- /dev/null +++ b/bootserver/udplistener/udplistener.go @@ -0,0 +1,117 @@ +package udplistener + +import ( + "bytes" + "context" + "fmt" + "net" + + "git.faercol.me/faercol/http-boot-server/bootserver/bootprotocol" + "git.faercol.me/faercol/http-boot-server/bootserver/services" + "github.com/sirupsen/logrus" +) + +const bufferLength = 2048 + +type udpMessage struct { + sourceAddr *net.UDPAddr + message bootprotocol.Message +} + +type UDPListener struct { + addr *net.UDPAddr + iface *net.Interface + l *net.UDPConn + log *logrus.Logger + ctx context.Context + service *services.ClientHandlerService + cancel context.CancelFunc +} + +func New(ifaceName, multicastGroup string, port int, log *logrus.Logger) (*UDPListener, error) { + addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("[%s]:%d", multicastGroup, port)) + if err != nil { + return nil, fmt.Errorf("failed to resolve UDP address: %w", err) + } + + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return nil, fmt.Errorf("failed to resolve interface name %s: %w", ifaceName, err) + } + + return &UDPListener{ + addr: addr, + iface: iface, + ctx: context.TODO(), + log: log, + }, nil +} + +func (l *UDPListener) Init() error { + l.log.Debugf("Creating listener on address %s, iface %s", l.addr.String(), l.iface.Name) + listener, err := net.ListenMulticastUDP("udp", l.iface, l.addr) + if err != nil { + return fmt.Errorf("failed to init listener: %w", err) + } + l.l = listener + return nil +} + +func (l *UDPListener) listen() (*udpMessage, error) { + buffer := make([]byte, bufferLength) + n, source, err := l.l.ReadFromUDP(buffer) + if err != nil { + return nil, fmt.Errorf("failed to read UDP packet: %w", err) + } + if n > bufferLength { + return nil, fmt.Errorf("UDP packet too big (%d/%d)", n, bufferLength) + } + + parsedMsg, err := bootprotocol.MessageFromBytes(bytes.Trim(buffer, "\x00")) + if err != nil { + return nil, fmt.Errorf("failed to parse message: %w", err) + } + + return &udpMessage{sourceAddr: source, message: parsedMsg}, nil +} + +func (l *UDPListener) mainLoop() { + msgChan := make(chan *udpMessage, 10) + errChan := make(chan error, 10) + + for { + + go func() { + msg, err := l.listen() + if err != nil { + errChan <- fmt.Errorf("error while listening to UDP packets: %w", err) + } else { + msgChan <- msg + } + }() + + l.log.Debug("Waiting for packets") + + select { + case <-l.ctx.Done(): + if err := l.l.Close(); err != nil { + l.log.Errorf("Error closing UDP listener: %s", err.Error()) + } + return + case err := <-errChan: + l.log.Error(err) + case msg := <-msgChan: + l.log.Infof("Request from %s: %q", msg.sourceAddr.String(), msg.message.String()) + } + + } +} + +func (l *UDPListener) Run(ctx context.Context) { + l.ctx, l.cancel = context.WithCancel(ctx) + l.mainLoop() +} + +func (l *UDPListener) Cancel() { + l.cancel() +}