Add udp listener
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Melora Hugues 2023-07-29 21:23:36 +02:00
parent 576c78e6dd
commit 0b6bbce7d5
9 changed files with 246 additions and 84 deletions

View file

@ -3,6 +3,9 @@
build: build:
go build -o build/ go build -o build/
buildarm:
env GOOS=linux GOARCH=arm64 go build -o build/bootserver_arm
test: test:
go test -v ./... go test -v ./...

View file

@ -1,5 +1,7 @@
package bootoption package bootoption
import "github.com/google/uuid"
type EFIApp struct { type EFIApp struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
@ -7,6 +9,7 @@ type EFIApp struct {
} }
type Client struct { type Client struct {
ID uuid.UUID
IP string `json:"ip"` IP string `json:"ip"`
Name string `json:"name"` Name string `json:"name"`
Options []EFIApp `json:"options"` Options []EFIApp `json:"options"`

View file

@ -64,6 +64,7 @@ type Message interface {
encoding.BinaryMarshaler encoding.BinaryMarshaler
Action() Action Action() Action
ID() uuid.UUID ID() uuid.UUID
String() string
} }
type requestMessage struct { type requestMessage struct {
@ -103,6 +104,10 @@ func (rm *requestMessage) ID() uuid.UUID {
return rm.id return rm.id
} }
func (rm *requestMessage) String() string {
return fmt.Sprintf("%s from %s", ActionRequest.String(), rm.ID().String())
}
type acceptMessage struct { type acceptMessage struct {
id uuid.UUID id uuid.UUID
efiApp string efiApp string
@ -151,6 +156,10 @@ func (am *acceptMessage) ID() uuid.UUID {
return am.id 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 { type denyMessage struct {
id uuid.UUID id uuid.UUID
reason string reason string
@ -199,6 +208,10 @@ func (dm *denyMessage) ID() uuid.UUID {
return dm.id 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) { func MessageFromBytes(dat []byte) (Message, error) {
rawAction, content, found := bytes.Cut(dat, spaceByte) rawAction, content, found := bytes.Cut(dat, spaceByte)
if !found { if !found {

View file

@ -48,14 +48,22 @@ type jsonConf struct {
Mode string `json:"mode"` Mode string `json:"mode"`
SockPath string `json:"sock"` SockPath string `json:"sock"`
} `json:"server"` } `json:"server"`
BootProvider struct {
Iface string `json:"interface"`
Port int `json:"port"`
McastGroup string `json:"multicast_group"`
} `json:"boot_provider"`
} }
type AppConfig struct { type AppConfig struct {
LogLevel logrus.Level LogLevel logrus.Level
ServerMode ListeningMode ServerMode ListeningMode
Host string Host string
Port int Port int
SockPath string SockPath string
UPDMcastGroup string
UDPPort int
UDPIface string
} }
func parseLevel(lvlStr string) logrus.Level { func parseLevel(lvlStr string) logrus.Level {
@ -82,14 +90,20 @@ func (ac *AppConfig) UnmarshalJSON(data []byte) error {
ac.SockPath = jsonConf.Server.SockPath ac.SockPath = jsonConf.Server.SockPath
ac.Host = jsonConf.Server.Host ac.Host = jsonConf.Server.Host
ac.Port = jsonConf.Server.Port ac.Port = jsonConf.Server.Port
ac.UPDMcastGroup = jsonConf.BootProvider.McastGroup
ac.UDPIface = jsonConf.BootProvider.Iface
ac.UDPPort = jsonConf.BootProvider.Port
return nil return nil
} }
var defaultConfig AppConfig = AppConfig{ var defaultConfig AppConfig = AppConfig{
LogLevel: logrus.InfoLevel, LogLevel: logrus.InfoLevel,
ServerMode: ModeNet, ServerMode: ModeNet,
Host: "0.0.0.0", Host: "0.0.0.0",
Port: 5000, Port: 5000,
UPDMcastGroup: "ff02::abcd:1234",
UDPPort: 42,
UDPIface: "eth0",
} }
func New(filepath string) (*AppConfig, error) { func New(filepath string) (*AppConfig, error) {

View file

@ -1,85 +1,85 @@
package client package client
import ( // import (
"encoding/json" // "encoding/json"
"fmt" // "fmt"
"io" // "io"
"net" // "net"
"net/http" // "net/http"
"git.faercol.me/faercol/http-boot-server/bootserver/helpers" // "git.faercol.me/faercol/http-boot-server/bootserver/helpers"
"git.faercol.me/faercol/http-boot-server/bootserver/services" // "git.faercol.me/faercol/http-boot-server/bootserver/services"
"github.com/sirupsen/logrus" // "github.com/sirupsen/logrus"
) // )
const BootRoute = "/boot" // const BootRoute = "/boot"
type BootController struct { // type BootController struct {
clientService *services.ClientHandlerService // clientService *services.ClientHandlerService
l *logrus.Logger // l *logrus.Logger
} // }
func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController { // func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController {
return &BootController{ // return &BootController{
clientService: service, // clientService: service,
l: logger, // l: logger,
} // }
} // }
func (bc *BootController) getBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, []byte, error) { // func (bc *BootController) getBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, []byte, error) {
bootOption, err := bc.clientService.GetClientSelectedBootOption(clientIP) // bootOption, err := bc.clientService.GetClientSelectedBootOption(clientIP)
if err != nil { // if err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("failed to get boot option: %w", err) // return http.StatusInternalServerError, nil, fmt.Errorf("failed to get boot option: %w", err)
} // }
dat, err := json.Marshal(bootOption) // dat, err := json.Marshal(bootOption)
if err != nil { // if err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize body") // return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize body")
} // }
w.Header().Add("Content-Type", "application/json") // w.Header().Add("Content-Type", "application/json")
return http.StatusOK, dat, nil // return http.StatusOK, dat, nil
} // }
func (bc *BootController) setBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, error) { // func (bc *BootController) setBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, error) {
dat, err := io.ReadAll(r.Body) // dat, err := io.ReadAll(r.Body)
if err != nil { // if err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err) // return http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err)
} // }
var option string // var option string
if err := json.Unmarshal(dat, &option); err != nil { // if err := json.Unmarshal(dat, &option); err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err) // return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err)
} // }
if err := bc.clientService.SetClientBootOption(clientIP, option); err != nil { // 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.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) { // func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
clientIP, _, err := net.SplitHostPort(r.RemoteAddr) // clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil { // if err != nil {
bc.l.Errorf("Failed to read remote IP: %s", err.Error()) // bc.l.Errorf("Failed to read remote IP: %s", err.Error())
helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, bc.l) // helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, bc.l)
return // return
} // }
var returncode int // var returncode int
var content []byte // var content []byte
switch r.Method { // switch r.Method {
case http.MethodGet: // case http.MethodGet:
returncode, content, err = bc.getBootOption(clientIP, w, r) // returncode, content, err = bc.getBootOption(clientIP, w, r)
case http.MethodPut: // case http.MethodPut:
returncode, err = bc.setBootOption(clientIP, w, r) // returncode, err = bc.setBootOption(clientIP, w, r)
default: // default:
returncode = http.StatusMethodNotAllowed // returncode = http.StatusMethodNotAllowed
} // }
if err != nil { // if err != nil {
bc.l.Errorf("An error occured while handling boot request: %q", err.Error()) // bc.l.Errorf("An error occured while handling boot request: %q", err.Error())
} // }
helpers.HandleResponse(w, r, returncode, content, bc.l) // helpers.HandleResponse(w, r, returncode, content, bc.l)
} // }

View file

@ -10,6 +10,7 @@ import (
"git.faercol.me/faercol/http-boot-server/bootserver/config" "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/logger"
"git.faercol.me/faercol/http-boot-server/bootserver/server" "git.faercol.me/faercol/http-boot-server/bootserver/server"
"git.faercol.me/faercol/http-boot-server/bootserver/udplistener"
) )
const stopTimeout = 10 * time.Second const stopTimeout = 10 * time.Second
@ -47,7 +48,17 @@ func main() {
logger.L.Fatalf("Failed to initialize server: %s", err.Error()) 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 s.Run(mainCtx)
go listener.Run(mainCtx)
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt) signal.Notify(c, os.Interrupt)

View file

@ -66,7 +66,7 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
} }
service := services.NewClientHandlerService() service := services.NewClientHandlerService()
controllers := map[string]http.Handler{ 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), client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger),
} }

View file

@ -4,6 +4,7 @@ import (
"errors" "errors"
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption" "git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
"github.com/google/uuid"
) )
var ErrUnknownClient = errors.New("unknown client") var ErrUnknownClient = errors.New("unknown client")
@ -11,20 +12,20 @@ var ErrUnselectedBootOption = errors.New("unselected boot option")
var ErrUnknownBootOption = errors.New("unknown boot option") var ErrUnknownBootOption = errors.New("unknown boot option")
type ClientHandlerService struct { type ClientHandlerService struct {
clients map[string]*bootoption.Client clients map[uuid.UUID]*bootoption.Client
} }
func NewClientHandlerService() *ClientHandlerService { func NewClientHandlerService() *ClientHandlerService {
return &ClientHandlerService{ return &ClientHandlerService{
clients: make(map[string]*bootoption.Client), clients: make(map[uuid.UUID]*bootoption.Client),
} }
} }
func (chs *ClientHandlerService) AddClient(client *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] clientDetails, ok := chs.clients[client]
if !ok { if !ok {
return nil, ErrUnknownClient return nil, ErrUnknownClient
@ -42,7 +43,7 @@ func (chs *ClientHandlerService) GetClientSelectedBootOption(client string) (*bo
return nil, ErrUnknownBootOption 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] clientDetails, ok := chs.clients[client]
if !ok { if !ok {
return ErrUnknownClient return ErrUnknownClient

View file

@ -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()
}