From 1caf0d193c50ed031486639b2e7717efe89c0a19 Mon Sep 17 00:00:00 2001 From: Stefano Pagnottelli Date: Tue, 14 Jan 2025 15:35:03 +0100 Subject: [PATCH 1/2] wsproxy: First release --- cmd/wsproxy/http_server.go | 42 ++++++ cmd/wsproxy/main.go | 244 +++++++++++++++++++++++++++++++++++ cmd/wsproxy/tak_ws/tak_ws.go | 195 ++++++++++++++++++++++++++++ cmd/wsproxy/tcp_handler.go | 38 ++++++ goatak_wsproxy.yml | 18 +++ 5 files changed, 537 insertions(+) create mode 100644 cmd/wsproxy/http_server.go create mode 100644 cmd/wsproxy/main.go create mode 100644 cmd/wsproxy/tak_ws/tak_ws.go create mode 100644 cmd/wsproxy/tcp_handler.go create mode 100644 goatak_wsproxy.yml diff --git a/cmd/wsproxy/http_server.go b/cmd/wsproxy/http_server.go new file mode 100644 index 0000000..045d18f --- /dev/null +++ b/cmd/wsproxy/http_server.go @@ -0,0 +1,42 @@ +package main + +import ( + "github.com/kdudkov/goatak/cmd/wsproxy/tak_ws" + "github.com/kdudkov/goatak/pkg/log" + + "github.com/gofiber/contrib/websocket" + "github.com/gofiber/fiber/v2" +) + +func NewHttp(app *App) *fiber.App { + srv := fiber.New(fiber.Config{EnablePrintRoutes: false, DisableStartupMessage: true}) + srv.Use(log.NewFiberLogger(nil)) + srv.Get("/", getIndexHandler(app)) + srv.Get("/takproto/1", getTakWsHandler(app)) + return srv +} + +func getIndexHandler(_ *App) fiber.Handler { + return func(ctx *fiber.Ctx) error { + data := fiber.Map{ + "js": []string{"util.js", "map.js"}, + } + + return ctx.Render("templates/map", data, "templates/header") + } +} + +func getTakWsHandler(app *App) fiber.Handler { + return websocket.New(func(ws *websocket.Conn) { + defer ws.Close() + + app.logger.Info("WS connection from " + ws.RemoteAddr().String()) + name := "ws:" + ws.RemoteAddr().String() + w := tak_ws.New(name, nil, ws, app.ProcessCotFromWSClient) + + app.AddClientHandler(w) + w.Listen() + app.logger.Info("ws disconnected") + app.RemoveClientHandler(w.GetName()) + }) +} diff --git a/cmd/wsproxy/main.go b/cmd/wsproxy/main.go new file mode 100644 index 0000000..aa3660c --- /dev/null +++ b/cmd/wsproxy/main.go @@ -0,0 +1,244 @@ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "log/slog" + "os" + "os/signal" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/kdudkov/goatak/internal/client" + "github.com/kdudkov/goatak/pkg/cot" + "github.com/kdudkov/goatak/pkg/log" + "github.com/kdudkov/goatak/pkg/tlsutil" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" +) + +type App struct { + webPort int + webAddress string + dialTimeout time.Duration + host string + tcpPort string + logger *slog.Logger + tls bool + tlsStrict bool + tlsCert *tls.Certificate + cas *x509.CertPool + connected uint32 + serverClient *client.ConnClientHandler + wsClients map[string]client.ClientHandler +} + +func (app *App) SetConnected(connected bool) { + if connected { + atomic.StoreUint32(&app.connected, 1) + } else { + atomic.StoreUint32(&app.connected, 0) + } +} + +func (app *App) ProcessCotFromWSClient(msg *cot.CotMessage) { + if msg != nil { + if app.connected == 1 { + app.serverClient.SendMsg(msg) + } else { + app.logger.Info("not connected to TAK server, drop message", slog.Any("msg", msg)) + } + } +} + +func (app *App) ProcessCotFromTAKServer(msg *cot.CotMessage) { + app.logger.Info("process cot from server", slog.Any("msg", msg)) + + if len(app.wsClients) == 0 { + app.logger.Info("no websocket clients connected, drop message", slog.Any("msg", msg)) + return + } + + for _, ch := range app.wsClients { + ch.SendMsg(msg) + } +} + +func (app *App) ProcessRemoveFromTAKServer(ch client.ClientHandler) { + app.logger.Info("process remove from server") + app.SetConnected(false) + //wg.Done() + //cancel1() + app.logger.Info("disconnected") +} + +func (app *App) AddClientHandler(ch client.ClientHandler) { + app.wsClients[ch.GetName()] = ch +} + +func (app *App) RemoveClientHandler(name string) { + delete(app.wsClients, name) +} + +func (app *App) getTLSConfig(strict bool) *tls.Config { + conf := &tls.Config{ //nolint:exhaustruct + Certificates: []tls.Certificate{*app.tlsCert}, + RootCAs: app.cas, + ClientCAs: app.cas, + } + + if !strict { + conf.InsecureSkipVerify = true + } + + return conf +} + +func (app *App) Init() { +} + +// Run start client connection to the configured server. It loops until the context is canceled by signal or other means. +// Until running it will try to reconnect if the connection is lost. +func (app *App) Run(ctx context.Context) { + if app.webPort >= 0 { + go func() { + addr := fmt.Sprintf("%s:%d", app.webAddress, app.webPort) + app.logger.Info("listening " + addr) + + http := NewHttp(app) + err := http.Listen(addr) + if err != nil { + panic(err) + } + + }() + } + + for ctx.Err() == nil { + // Dial the server and connect to it. + conn, err := app.connect() + if err != nil { + app.logger.Error("connect error", slog.Any("error", err)) + time.Sleep(time.Second * 5) + + continue + } + + app.SetConnected(true) + app.logger.Info("connected") + app.logger.Info(fmt.Sprintf("conn: %+v", conn.RemoteAddr())) + + wg := new(sync.WaitGroup) + wg.Add(1) + + //_, cancel1 := context.WithCancel(ctx) + app.serverClient = client.NewConnClientHandler(fmt.Sprintf("%s:%s", app.host, app.tcpPort), conn, &client.HandlerConfig{ + MessageCb: app.ProcessCotFromTAKServer, + RemoveCb: func(ch client.ClientHandler) { + app.SetConnected(false) + wg.Done() + //cancel1() + app.logger.Info("disconnected") + }, + IsClient: true, + UID: "FIXME:UID:00001", + }) + + go app.serverClient.Start() + wg.Wait() + } +} + +func NewApp(connectStr string) *App { + logger := slog.Default() + parts := strings.Split(connectStr, ":") + + if len(parts) != 3 { + logger.Error("invalid connect string: " + connectStr) + + return nil + } + + var tlsConn bool + + switch parts[2] { + case "tcp": + tlsConn = false + case "ssl": + tlsConn = true + default: + logger.Error("invalid connect string " + connectStr) + + return nil + } + + return &App{ + logger: logger, + host: parts[0], + tcpPort: parts[1], + tls: tlsConn, + dialTimeout: time.Second * 5, + wsClients: make(map[string]client.ClientHandler), + } +} + +func main() { + conf := flag.String("config", "goatak_wsproxy.yml", "name of config file") + debug := flag.Bool("debug", false, "debug") + flag.Parse() + + k := koanf.New(".") + k.Set("server_address", "204.48.30.216:8087:tcp") + k.Set("web_address", "0.0.0.0") + k.Set("web_port", 8088) + k.Set("ssl.password", "atakatak") + k.Set("ssl.strict", false) + + if err := k.Load(file.Provider(*conf), yaml.Parser()); err != nil { + fmt.Printf("error loading config: %s", err.Error()) + return + } + + var h slog.Handler + if *debug { + h = log.NewHandler(&slog.HandlerOptions{Level: slog.LevelDebug}) + } else { + h = log.NewHandler(&slog.HandlerOptions{Level: slog.LevelInfo}) + } + + slog.SetDefault(slog.New(h)) + + app := NewApp(k.String("server_address")) + app.webPort = k.Int("web_port") + app.webAddress = k.String("web_address") + ctx, cancel := context.WithCancel(context.Background()) + + if app.tls { + app.logger.Info("loading cert from file " + k.String("ssl.cert")) + cert, cas, err := client.LoadP12(k.String("ssl.cert"), k.String("ssl.password")) + if err != nil { + app.logger.Error("error while loading cert: " + err.Error()) + return + } + + tlsutil.LogCert(app.logger, "loaded cert", cert.Leaf) + app.tlsCert = cert + app.cas = tlsutil.MakeCertPool(cas...) + } + + app.Init() + go app.Run(ctx) + + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + <-c + + cancel() +} diff --git a/cmd/wsproxy/tak_ws/tak_ws.go b/cmd/wsproxy/tak_ws/tak_ws.go new file mode 100644 index 0000000..78f7197 --- /dev/null +++ b/cmd/wsproxy/tak_ws/tak_ws.go @@ -0,0 +1,195 @@ +package tak_ws + +import ( + "bufio" + "bytes" + "encoding/hex" + "fmt" + "log" + "log/slog" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/contrib/websocket" + + "github.com/kdudkov/goatak/internal/model" + "github.com/kdudkov/goatak/pkg/cot" + "github.com/kdudkov/goatak/pkg/cotproto" +) + +type MessageCb func(msg *cot.CotMessage) + +type WsClientHandler struct { + log *slog.Logger + name string + user *model.User + ws *websocket.Conn + ch chan []byte + uids sync.Map + active int32 + messageCb MessageCb +} + +func (w *WsClientHandler) GetName() string { + return w.name +} + +func (w *WsClientHandler) GetUser() *model.User { + return w.user +} + +func (w *WsClientHandler) GetSerial() string { + return "" +} + +func (w *WsClientHandler) CanSeeScope(scope string) bool { + return true +} + +func (w *WsClientHandler) GetVersion() int32 { + return 0 +} + +func (w *WsClientHandler) GetUids() map[string]string { + res := make(map[string]string) + + w.uids.Range(func(key, value any) bool { + res[key.(string)] = value.(string) + + return true + }) + + return res +} + +func (w *WsClientHandler) HasUID(uid string) bool { + _, ok := w.uids.Load(uid) + + return ok +} + +func (w *WsClientHandler) GetLastSeen() *time.Time { + return nil +} + +func New(name string, user *model.User, ws *websocket.Conn, mc MessageCb) *WsClientHandler { + return &WsClientHandler{ + log: slog.Default().With("logger", "tak_ws", "name", name, "user", user), + name: name, + user: user, + ws: ws, + uids: sync.Map{}, + ch: make(chan []byte, 10), + active: 1, + messageCb: mc, + } +} + +func (w *WsClientHandler) SendMsg(msg *cot.CotMessage) error { + return w.SendCot(msg.GetTakMessage()) +} + +func (w *WsClientHandler) SendCot(msg *cotproto.TakMessage) error { + dat, err := cot.MakeProtoPacket(msg) + if err != nil { + return err + } + if w.tryAddPacket(dat) { + return nil + } + + return fmt.Errorf("client is off") +} + +func (w *WsClientHandler) tryAddPacket(msg []byte) bool { + if !w.IsActive() { + return false + } + select { + case w.ch <- msg: + default: + } + + return true +} + +func (w *WsClientHandler) IsActive() bool { + return atomic.LoadInt32(&w.active) == 1 +} + +func (w *WsClientHandler) writer() { + for b := range w.ch { + log.Println(hex.EncodeToString(b)) + if err := w.ws.WriteMessage(websocket.BinaryMessage, b); err != nil { + w.log.Error("send error", slog.Any("error", err)) + w.Stop() + + break + } + } +} + +func (w *WsClientHandler) reader() { + defer w.Stop() + + for { + mt, b, err := w.ws.ReadMessage() + + if err != nil { + w.log.Error("read error", slog.Any("error", err)) + return + } + + if mt != websocket.BinaryMessage { + continue + } + + if err = w.parse(b); err != nil { + w.log.Error("parse", slog.Any("error", err)) + } + } +} + +func (w *WsClientHandler) Stop() { + if atomic.CompareAndSwapInt32(&w.active, 1, 0) { + close(w.ch) + _ = w.ws.Close() + } +} + +func (w *WsClientHandler) Listen() { + go w.writer() + w.reader() + w.log.Info("stop listening") +} + +func (w *WsClientHandler) parse(b []byte) error { + msg, err := cot.ReadProto(bufio.NewReader(bytes.NewReader(b))) + if err != nil { + return fmt.Errorf("read error %w", err) + } + + cotmsg, err := cot.CotFromProto(msg, w.name, w.GetUser().GetScope()) + if err != nil { + return fmt.Errorf("convert error %w", err) + } + + if cotmsg.IsContact() { + uid := msg.GetCotEvent().GetUid() + uid = strings.TrimSuffix(uid, "-ping") + + w.uids.Store(uid, cotmsg.GetCallsign()) + } + + // remove contact + if cotmsg.GetType() == "t-x-d-d" && cotmsg.GetDetail() != nil && cotmsg.GetDetail().Has("link") { + uid := cotmsg.GetDetail().GetFirst("link").GetAttr("uid") + w.uids.Delete(uid) + } + + w.messageCb(cotmsg) + + return nil +} diff --git a/cmd/wsproxy/tcp_handler.go b/cmd/wsproxy/tcp_handler.go new file mode 100644 index 0000000..356a78c --- /dev/null +++ b/cmd/wsproxy/tcp_handler.go @@ -0,0 +1,38 @@ +package main + +import ( + "crypto/tls" + "fmt" + "net" + + "github.com/kdudkov/goatak/pkg/tlsutil" +) + +func (app *App) connect() (net.Conn, error) { + addr := fmt.Sprintf("%s:%s", app.host, app.tcpPort) + if app.tls { + app.logger.Info(fmt.Sprintf("connecting with SSL to %s...", addr)) + + conn, err := tls.Dial("tcp", addr, app.getTLSConfig(app.tlsStrict)) + if err != nil { + return nil, err + } + + app.logger.Debug("handshake...") + + if err := conn.Handshake(); err != nil { + return conn, err + } + + cs := conn.ConnectionState() + + app.logger.Info(fmt.Sprintf("Handshake complete: %t", cs.HandshakeComplete)) + app.logger.Info(fmt.Sprintf("version: %d", cs.Version)) + tlsutil.LogCerts(app.logger, cs.PeerCertificates...) + + return conn, nil + } + app.logger.Info(fmt.Sprintf("connecting to %s...", addr)) + + return net.DialTimeout("tcp", addr, app.dialTimeout) +} diff --git a/goatak_wsproxy.yml b/goatak_wsproxy.yml new file mode 100644 index 0000000..17ae3ab --- /dev/null +++ b/goatak_wsproxy.yml @@ -0,0 +1,18 @@ +--- +# server address to connect to +server_address: 127.0.0.1:8999:tcp + +# local port for web server +web_port: 8088 + +# local address for web server +web_address: 0.0.0.0 + +ssl: + cert: "servercertificate.p12" + # password for certificate file + password: atakatak + # login for cert enrollment. If not empty, client does cert enrollment + enroll_user: "" + # password for cert enrollment + enroll_password: "" From beb3ffe6e2b16e2e607177c45fd839338f4197ae Mon Sep 17 00:00:00 2001 From: Stefano Pagnottelli Date: Thu, 16 Jan 2025 06:41:31 +0100 Subject: [PATCH 2/2] wsproxy: added multicast receiver --- cmd/wsproxy/main.go | 47 +++++++++- cmd/wsproxy/tak_ws/tak_ws.go | 3 - cmd/wsproxy/udpserver.go | 173 +++++++++++++++++++++++++++++++++++ goatak_wsproxy.yml | 6 +- 4 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 cmd/wsproxy/udpserver.go diff --git a/cmd/wsproxy/main.go b/cmd/wsproxy/main.go index aa3660c..a5ebeeb 100644 --- a/cmd/wsproxy/main.go +++ b/cmd/wsproxy/main.go @@ -27,6 +27,8 @@ import ( type App struct { webPort int webAddress string + mcastPort int + mcastAddress string dialTimeout time.Duration host string tcpPort string @@ -38,6 +40,7 @@ type App struct { connected uint32 serverClient *client.ConnClientHandler wsClients map[string]client.ClientHandler + mcastHandler *UdpClientHandler } func (app *App) SetConnected(connected bool) { @@ -48,19 +51,38 @@ func (app *App) SetConnected(connected bool) { } } +// ProcessCotFromWSClient processes COT messages from websocket clients and forwards them to the server connection or multicast connection func (app *App) ProcessCotFromWSClient(msg *cot.CotMessage) { if msg != nil { + sent := false if app.connected == 1 { app.serverClient.SendMsg(msg) - } else { - app.logger.Info("not connected to TAK server, drop message", slog.Any("msg", msg)) + sent = true + } + if app.mcastHandler.IsActive() { + app.mcastHandler.SendMsg(msg) + sent = true + } + if !sent { + app.logger.Info("not connected to server or multicast, drop message", slog.Any("msg", msg)) } } } -func (app *App) ProcessCotFromTAKServer(msg *cot.CotMessage) { - app.logger.Info("process cot from server", slog.Any("msg", msg)) +// ProcessCotFromMcast processes COT messages from multicast and forwards them to the websocket connected clients +func (app *App) ProcessCotFromMcast(msg *cot.CotMessage) { + if len(app.wsClients) == 0 { + app.logger.Info("no websocket clients connected, drop message", slog.Any("msg", msg)) + return + } + for _, ch := range app.wsClients { + ch.SendMsg(msg) + } +} + +// ProcessCotFromTAKServer processes COT messages from the TAK server and forwards them to the websocket connected clients +func (app *App) ProcessCotFromTAKServer(msg *cot.CotMessage) { if len(app.wsClients) == 0 { app.logger.Info("no websocket clients connected, drop message", slog.Any("msg", msg)) return @@ -121,6 +143,19 @@ func (app *App) Run(ctx context.Context) { }() } + if app.mcastPort > 0 { + go func() { + addr := fmt.Sprintf("%s:%d", app.mcastAddress, app.mcastPort) + app.logger.Info("listening multicast " + addr) + + app.mcastHandler = NewUdpClientHandler(app.logger, app.ProcessCotFromMcast) + err := app.mcastHandler.Listen(addr) + if err != nil { + panic(err) + } + }() + } + for ctx.Err() == nil { // Dial the server and connect to it. conn, err := app.connect() @@ -198,6 +233,8 @@ func main() { k.Set("server_address", "204.48.30.216:8087:tcp") k.Set("web_address", "0.0.0.0") k.Set("web_port", 8088) + k.Set("mcast_address", "239.2.3.1") + k.Set("mcast_port", 6969) k.Set("ssl.password", "atakatak") k.Set("ssl.strict", false) @@ -218,6 +255,8 @@ func main() { app := NewApp(k.String("server_address")) app.webPort = k.Int("web_port") app.webAddress = k.String("web_address") + app.mcastPort = k.Int("mcast_port") + app.mcastAddress = k.String("mcast_address") ctx, cancel := context.WithCancel(context.Background()) if app.tls { diff --git a/cmd/wsproxy/tak_ws/tak_ws.go b/cmd/wsproxy/tak_ws/tak_ws.go index 78f7197..5a70d08 100644 --- a/cmd/wsproxy/tak_ws/tak_ws.go +++ b/cmd/wsproxy/tak_ws/tak_ws.go @@ -3,9 +3,7 @@ package tak_ws import ( "bufio" "bytes" - "encoding/hex" "fmt" - "log" "log/slog" "strings" "sync" @@ -121,7 +119,6 @@ func (w *WsClientHandler) IsActive() bool { func (w *WsClientHandler) writer() { for b := range w.ch { - log.Println(hex.EncodeToString(b)) if err := w.ws.WriteMessage(websocket.BinaryMessage, b); err != nil { w.log.Error("send error", slog.Any("error", err)) w.Stop() diff --git a/cmd/wsproxy/udpserver.go b/cmd/wsproxy/udpserver.go new file mode 100644 index 0000000..1d4eba6 --- /dev/null +++ b/cmd/wsproxy/udpserver.go @@ -0,0 +1,173 @@ +package main + +import ( + "encoding/xml" + "fmt" + "log/slog" + "net" + + "google.golang.org/protobuf/proto" + + "github.com/kdudkov/goatak/pkg/cot" + "github.com/kdudkov/goatak/pkg/cotproto" +) + +const ( + magicByte = 0xbf + scopeBroadcast = "broadcast" +) + +type MessageCb func(msg *cot.CotMessage) + +type UdpClientHandler struct { + conn *net.UDPConn + addr *net.UDPAddr + logger *slog.Logger + messageCb MessageCb + ch chan []byte + active bool +} + +func (c *UdpClientHandler) IsActive() bool { + return c.active +} + +func (c *UdpClientHandler) Listen(addrstr string) error { + c.logger.Info("listening UDP at " + addrstr) + + var err error + c.addr, err = net.ResolveUDPAddr("udp", addrstr) + if err != nil { + c.logger.Error("error", slog.Any("error", err)) + return err + } + + c.conn, err = net.ListenMulticastUDP("udp", nil, c.addr) + if err != nil { + c.logger.Error("error", slog.Any("error", err)) + return err + } + + c.conn.SetReadBuffer(65535) + c.active = true + + go c.writer() + c.reader() + + return nil +} + +func (c *UdpClientHandler) SendMsg(msg *cot.CotMessage) error { + return c.SendCot(msg.GetTakMessage()) +} + +func (c *UdpClientHandler) SendCot(msg *cotproto.TakMessage) error { + dat, err := cot.MakeProtoPacket(msg) + if err != nil { + return err + } + if c.tryAddPacket(dat) { + return nil + } + + return fmt.Errorf("client is off") +} + +func (c *UdpClientHandler) tryAddPacket(msg []byte) bool { + select { + case c.ch <- msg: + default: + } + return true +} + +func (c *UdpClientHandler) writer() { + for b := range c.ch { + _, _, err := c.conn.WriteMsgUDP(b, []byte{}, c.addr) + if err != nil { + c.logger.Error("send error", slog.Any("error", err)) + c.Stop() + break + } + } +} + +func (c *UdpClientHandler) reader() { + defer c.Stop() + + buf := make([]byte, 65535) + for { + n, _, err := c.conn.ReadFromUDP(buf) + if err != nil { + c.logger.Error("read error", slog.Any("error", err)) + continue + } + + if n < 4 { + continue + } + + if buf[0] == magicByte && buf[2] == magicByte { + if buf[1] == 1 { + msg := new(cotproto.TakMessage) + + err = proto.Unmarshal(buf[3:n], msg) + if err != nil { + c.logger.Error("protobuf decode error", slog.Any("error", err)) + + continue + } + + cot, err := cot.CotFromProto(msg, "", cot.BroadcastScope) + if err != nil { + c.logger.Error("protobuf detail extract error", slog.Any("error", err)) + continue + } + + c.messageCb(cot) + } else { + ev := &cot.Event{} + + err = xml.Unmarshal(buf[3:n], ev) + if err != nil { + c.logger.Error("xml decode error", slog.Any("error", err)) + continue + } + + cot, err := cot.EventToProtoExt(ev, "", scopeBroadcast) + if err != nil { + c.logger.Error("error", slog.Any("error", err)) + } + + c.messageCb(cot) + } + } else { + ev := &cot.Event{} + if err := xml.Unmarshal(buf[:n], ev); err != nil { + c.logger.Error("decode error", slog.Any("error", err)) + continue + } + + cot, err := cot.EventToProtoExt(ev, "", scopeBroadcast) + if err != nil { + c.logger.Error("error", slog.Any("error", err)) + } + + c.messageCb(cot) + } + } +} + +func (c *UdpClientHandler) Stop() { + if !c.active { + return + } + + c.active = false + close(c.ch) + _ = c.conn.Close() +} + +func NewUdpClientHandler(logger *slog.Logger, messageCb MessageCb) *UdpClientHandler { + return &UdpClientHandler{logger: logger, messageCb: messageCb, ch: make(chan []byte, 10)} +} diff --git a/goatak_wsproxy.yml b/goatak_wsproxy.yml index 17ae3ab..3fe64d0 100644 --- a/goatak_wsproxy.yml +++ b/goatak_wsproxy.yml @@ -4,10 +4,14 @@ server_address: 127.0.0.1:8999:tcp # local port for web server web_port: 8088 - # local address for web server web_address: 0.0.0.0 +# multicast address +mcast_address: 239.2.3.1 +# multicast port +mcast_port: 6969 + ssl: cert: "servercertificate.p12" # password for certificate file