Compare commits

1 Commits

Author SHA1 Message Date
e759f13d97 mnis: Add client mode to coexist with MNIS
Adds ClientMode to UDPConfig which binds ephemeral local ports
instead of the well-known service ports (4005/4007/4001). This lets
mototrbod run alongside MNIS — outbound packets still target the
radio's well-known port, and responses arrive on the ephemeral
socket. Defaults to true (mnis_client = true) so the service works
out of the box on hosts where MNIS is already running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 00:38:28 -05:00
3 changed files with 27 additions and 7 deletions

View File

@@ -73,6 +73,7 @@ type config struct {
RESTPlainHTTP bool `mapstructure:"rest_plain_http"`
GRPCPlain bool `mapstructure:"grpc_plain"`
MNISClient bool `mapstructure:"mnis_client"`
TLS struct {
Cert string `mapstructure:"cert"`
@@ -141,6 +142,7 @@ func registerDaemonFlags(cmd *cobra.Command) {
f.String("rest-addr", ":8443", "REST listen address")
f.Bool("rest-plain-http", false, "Serve REST over plain HTTP (loopback only)")
f.Bool("grpc-plain", false, "Serve gRPC without TLS (DEV ONLY, no auth)")
f.Bool("mnis-client", true, "Use ephemeral ports (coexist with MNIS); false binds the well-known ports directly")
f.String("tls-cert", "", "Server TLS certificate (PEM)")
f.String("tls-key", "", "Server TLS key (PEM)")
f.String("tls-client-ca", "", "PEM bundle of CAs allowed to issue client certs")
@@ -158,6 +160,7 @@ var flagToKey = map[string]string{
"rest-addr": "rest_addr",
"rest-plain-http": "rest_plain_http",
"grpc-plain": "grpc_plain",
"mnis-client": "mnis_client",
"tls-cert": "tls.cert",
"tls-key": "tls.key",
"tls-client-ca": "tls.client_ca",
@@ -180,6 +183,7 @@ func loadConfig(v *viper.Viper, cmd *cobra.Command, cfgFile string) error {
v.SetDefault("rest_addr", ":8443")
v.SetDefault("rest_plain_http", false)
v.SetDefault("grpc_plain", false)
v.SetDefault("mnis_client", true)
v.SetDefault("ntfy.url", "https://ntfy.sh")
used, err := cfgloader.Load(v, cfgloader.Options{AppName: appName, Explicit: cfgFile})
@@ -210,14 +214,15 @@ func runWithConfig(ctx context.Context, cfg config) error {
return fmt.Errorf("invalid bind_ip %q", cfg.BindIP)
}
tr, err := mnis.NewUDP(mnis.UDPConfig{
BindIP: bindIP,
Ports: []int{tms.Port, ars.Port, lrrp.Port},
BindIP: bindIP,
Ports: []int{tms.Port, ars.Port, lrrp.Port},
ClientMode: cfg.MNISClient,
})
if err != nil {
return fmt.Errorf("mnis: %w", err)
}
defer tr.Close()
log.Info("mnis transport bound", "ip", bindIP, "ports", []int{tms.Port, ars.Port, lrrp.Port})
log.Info("mnis transport ready", "ip", bindIP, "ports", []int{tms.Port, ars.Port, lrrp.Port}, "client_mode", cfg.MNISClient)
// Contacts.
store, err := contacts.Open(cfg.DataDir)

View File

@@ -7,7 +7,12 @@
# /etc/mototrbo/mototrbod.toml
# Override with --config. Env vars (MOTOTRBO_*) and --flags override file.
bind_ip = "127.0.0.1"
bind_ip = "127.0.0.1"
# Client mode (default true): use ephemeral ports so mototrbod can
# coexist with MNIS. Set to false only if MNIS is not running and
# mototrbod should bind ports 4005/4007/4001 directly.
mnis_client = true
# data_dir defaults to %PROGRAMDATA%\mototrbo\data on Windows and
# ./var/badger elsewhere. Set explicitly to override.
data_dir = "./var/badger"

View File

@@ -66,6 +66,12 @@ type UDPConfig struct {
// allowed for any port, but inbound traffic is only delivered for
// ports that have a bound socket.
Ports []int
// ClientMode, when true, binds ephemeral local ports instead of
// the well-known service ports. This lets mototrbod coexist with
// MNIS (which already owns ports 4005/4007/4001). Outbound
// packets are still addressed to the well-known port on the
// radio's tunnel IP; replies arrive on the ephemeral socket.
ClientMode bool
}
// udpTransport is a Transport that uses one net.UDPConn per service port.
@@ -97,12 +103,16 @@ func NewUDP(cfg UDPConfig) (Transport, error) {
}
for _, p := range cfg.Ports {
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: bind, Port: p})
localPort := p
if cfg.ClientMode {
localPort = 0 // ephemeral — let the OS pick
}
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: bind, Port: localPort})
if err != nil {
t.closeAllConns()
return nil, fmt.Errorf("listen %s:%d: %w", bind, p, err)
return nil, fmt.Errorf("listen %s:%d: %w", bind, localPort, err)
}
t.conns[p] = conn
t.conns[p] = conn // keyed by well-known port, not local port
t.wg.Add(1)
go t.readLoop(p, conn)
}