Merge pull request #48 from ipnet-mesh/feature/mqtt-tls

Added support for MQTT TLS
This commit is contained in:
JingleManSweep
2025-12-07 21:16:13 +00:00
committed by GitHub
12 changed files with 95 additions and 0 deletions

View File

@@ -52,6 +52,11 @@ MQTT_USERNAME=
MQTT_PASSWORD=
MQTT_PREFIX=meshcore
# Enable TLS/SSL for MQTT connection (default: false)
# When enabled, uses TLS with system CA certificates (e.g., for Let's Encrypt)
# Set to true for secure MQTT connections (port 8883)
MQTT_TLS=false
# External port mappings for local MQTT broker (--profile mqtt only)
MQTT_EXTERNAL_PORT=1883
MQTT_WS_PORT=9001

View File

@@ -47,6 +47,7 @@ services:
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
- MQTT_TLS=${MQTT_TLS:-false}
- SERIAL_PORT=${SERIAL_PORT:-/dev/ttyUSB0}
- SERIAL_BAUD=${SERIAL_BAUD:-115200}
- NODE_ADDRESS=${NODE_ADDRESS:-}
@@ -81,6 +82,7 @@ services:
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
- MQTT_TLS=${MQTT_TLS:-false}
- SERIAL_PORT=${SERIAL_PORT_SENDER:-/dev/ttyUSB1}
- SERIAL_BAUD=${SERIAL_BAUD:-115200}
- NODE_ADDRESS=${NODE_ADDRESS_SENDER:-}
@@ -112,6 +114,7 @@ services:
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
- MQTT_TLS=${MQTT_TLS:-false}
- MOCK_DEVICE=true
- NODE_ADDRESS=${NODE_ADDRESS:-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef}
command: ["interface", "receiver", "--mock"]
@@ -145,6 +148,7 @@ services:
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
- MQTT_TLS=${MQTT_TLS:-false}
- DATA_HOME=/data
- SEED_HOME=/seed
# Explicitly unset to use DATA_HOME-based default path
@@ -203,6 +207,7 @@ services:
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_PREFIX=${MQTT_PREFIX:-meshcore}
- MQTT_TLS=${MQTT_TLS:-false}
- DATA_HOME=/data
# Explicitly unset to use DATA_HOME-based default path
- DATABASE_URL=

View File

@@ -52,6 +52,7 @@ def create_app(
mqtt_host: str = "localhost",
mqtt_port: int = 1883,
mqtt_prefix: str = "meshcore",
mqtt_tls: bool = False,
cors_origins: list[str] | None = None,
) -> FastAPI:
"""Create and configure the FastAPI application.
@@ -63,6 +64,7 @@ def create_app(
mqtt_host: MQTT broker host
mqtt_port: MQTT broker port
mqtt_prefix: MQTT topic prefix
mqtt_tls: Enable TLS/SSL for MQTT connection
cors_origins: Allowed CORS origins
Returns:
@@ -85,6 +87,7 @@ def create_app(
app.state.mqtt_host = mqtt_host
app.state.mqtt_port = mqtt_port
app.state.mqtt_prefix = mqtt_prefix
app.state.mqtt_tls = mqtt_tls
# Configure CORS
if cors_origins is None:

View File

@@ -67,6 +67,13 @@ import click
envvar="MQTT_TOPIC_PREFIX",
help="MQTT topic prefix",
)
@click.option(
"--mqtt-tls",
is_flag=True,
default=False,
envvar="MQTT_TLS",
help="Enable TLS/SSL for MQTT connection",
)
@click.option(
"--cors-origins",
type=str,
@@ -92,6 +99,7 @@ def api(
mqtt_host: str,
mqtt_port: int,
mqtt_prefix: str,
mqtt_tls: bool,
cors_origins: str | None,
reload: bool,
) -> None:
@@ -171,6 +179,7 @@ def api(
mqtt_host=mqtt_host,
mqtt_port=mqtt_port,
mqtt_prefix=mqtt_prefix,
mqtt_tls=mqtt_tls,
cors_origins=origins_list,
)

View File

@@ -57,6 +57,7 @@ def get_mqtt_client(request: Request) -> MQTTClient:
mqtt_host = getattr(request.app.state, "mqtt_host", "localhost")
mqtt_port = getattr(request.app.state, "mqtt_port", 1883)
mqtt_prefix = getattr(request.app.state, "mqtt_prefix", "meshcore")
mqtt_tls = getattr(request.app.state, "mqtt_tls", False)
# Use unique client ID to allow multiple API instances
unique_id = uuid.uuid4().hex[:8]
@@ -65,6 +66,7 @@ def get_mqtt_client(request: Request) -> MQTTClient:
port=mqtt_port,
prefix=mqtt_prefix,
client_id=f"meshcore-api-{unique_id}",
tls=mqtt_tls,
)
client = MQTTClient(config)

View File

@@ -47,6 +47,13 @@ if TYPE_CHECKING:
envvar="MQTT_PREFIX",
help="MQTT topic prefix",
)
@click.option(
"--mqtt-tls",
is_flag=True,
default=False,
envvar="MQTT_TLS",
help="Enable TLS/SSL for MQTT connection",
)
@click.option(
"--data-home",
type=str,
@@ -82,6 +89,7 @@ def collector(
mqtt_username: str | None,
mqtt_password: str | None,
prefix: str,
mqtt_tls: bool,
data_home: str | None,
seed_home: str | None,
database_url: str | None,
@@ -125,6 +133,7 @@ def collector(
ctx.obj["mqtt_username"] = mqtt_username
ctx.obj["mqtt_password"] = mqtt_password
ctx.obj["prefix"] = prefix
ctx.obj["mqtt_tls"] = mqtt_tls
ctx.obj["data_home"] = data_home or settings.data_home
ctx.obj["seed_home"] = settings.effective_seed_home
ctx.obj["database_url"] = effective_db_url
@@ -139,6 +148,7 @@ def collector(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
prefix=prefix,
mqtt_tls=mqtt_tls,
database_url=effective_db_url,
log_level=log_level,
data_home=data_home or settings.data_home,
@@ -152,6 +162,7 @@ def _run_collector_service(
mqtt_username: str | None,
mqtt_password: str | None,
prefix: str,
mqtt_tls: bool,
database_url: str,
log_level: str,
data_home: str,
@@ -256,6 +267,7 @@ def _run_collector_service(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
mqtt_prefix=prefix,
mqtt_tls=mqtt_tls,
database_url=database_url,
webhook_dispatcher=webhook_dispatcher,
cleanup_enabled=settings.data_retention_enabled,
@@ -279,6 +291,7 @@ def run_cmd(ctx: click.Context) -> None:
mqtt_username=ctx.obj["mqtt_username"],
mqtt_password=ctx.obj["mqtt_password"],
prefix=ctx.obj["prefix"],
mqtt_tls=ctx.obj["mqtt_tls"],
database_url=ctx.obj["database_url"],
log_level=ctx.obj["log_level"],
data_home=ctx.obj["data_home"],

View File

@@ -428,6 +428,7 @@ def create_subscriber(
mqtt_username: Optional[str] = None,
mqtt_password: Optional[str] = None,
mqtt_prefix: str = "meshcore",
mqtt_tls: bool = False,
database_url: str = "sqlite:///./meshcore.db",
webhook_dispatcher: Optional["WebhookDispatcher"] = None,
cleanup_enabled: bool = False,
@@ -444,6 +445,7 @@ def create_subscriber(
mqtt_username: MQTT username
mqtt_password: MQTT password
mqtt_prefix: MQTT topic prefix
mqtt_tls: Enable TLS/SSL for MQTT connection
database_url: Database connection URL
webhook_dispatcher: Optional webhook dispatcher for event forwarding
cleanup_enabled: Enable automatic event data cleanup
@@ -464,6 +466,7 @@ def create_subscriber(
password=mqtt_password,
prefix=mqtt_prefix,
client_id=f"meshcore-collector-{unique_id}",
tls=mqtt_tls,
)
mqtt_client = MQTTClient(mqtt_config)
@@ -496,6 +499,7 @@ def run_collector(
mqtt_username: Optional[str] = None,
mqtt_password: Optional[str] = None,
mqtt_prefix: str = "meshcore",
mqtt_tls: bool = False,
database_url: str = "sqlite:///./meshcore.db",
webhook_dispatcher: Optional["WebhookDispatcher"] = None,
cleanup_enabled: bool = False,
@@ -512,6 +516,7 @@ def run_collector(
mqtt_username: MQTT username
mqtt_password: MQTT password
mqtt_prefix: MQTT topic prefix
mqtt_tls: Enable TLS/SSL for MQTT connection
database_url: Database connection URL
webhook_dispatcher: Optional webhook dispatcher for event forwarding
cleanup_enabled: Enable automatic event data cleanup
@@ -526,6 +531,7 @@ def run_collector(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
mqtt_prefix=mqtt_prefix,
mqtt_tls=mqtt_tls,
database_url=database_url,
webhook_dispatcher=webhook_dispatcher,
cleanup_enabled=cleanup_enabled,

View File

@@ -52,6 +52,9 @@ class CommonSettings(BaseSettings):
default=None, description="MQTT password (optional)"
)
mqtt_prefix: str = Field(default="meshcore", description="MQTT topic prefix")
mqtt_tls: bool = Field(
default=False, description="Enable TLS/SSL for MQTT connection"
)
class InterfaceSettings(CommonSettings):

View File

@@ -23,6 +23,7 @@ class MQTTConfig:
client_id: Optional[str] = None
keepalive: int = 60
clean_session: bool = True
tls: bool = False
class TopicBuilder:
@@ -131,6 +132,11 @@ class MQTTClient:
self._connected = False
self._message_handlers: dict[str, list[MessageHandler]] = {}
# Set up TLS if enabled
if config.tls:
self._client.tls_set()
logger.debug("TLS/SSL enabled for MQTT connection")
# Set up authentication if provided
if config.username:
self._client.username_pw_set(config.username, config.password)
@@ -344,6 +350,7 @@ def create_mqtt_client(
password: Optional[str] = None,
prefix: str = "meshcore",
client_id: Optional[str] = None,
tls: bool = False,
) -> MQTTClient:
"""Create and configure an MQTT client.
@@ -354,6 +361,7 @@ def create_mqtt_client(
password: MQTT password (optional)
prefix: Topic prefix
client_id: Client identifier (optional)
tls: Enable TLS/SSL connection (optional)
Returns:
Configured MQTTClient instance
@@ -365,5 +373,6 @@ def create_mqtt_client(
password=password,
prefix=prefix,
client_id=client_id,
tls=tls,
)
return MQTTClient(config)

View File

@@ -93,6 +93,13 @@ def interface() -> None:
envvar="MQTT_PREFIX",
help="MQTT topic prefix",
)
@click.option(
"--mqtt-tls",
is_flag=True,
default=False,
envvar="MQTT_TLS",
help="Enable TLS/SSL for MQTT connection",
)
@click.option(
"--log-level",
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
@@ -112,6 +119,7 @@ def run(
mqtt_username: str | None,
mqtt_password: str | None,
prefix: str,
mqtt_tls: bool,
log_level: str,
) -> None:
"""Run the interface component.
@@ -153,6 +161,7 @@ def run(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
mqtt_prefix=prefix,
mqtt_tls=mqtt_tls,
)
elif mode_upper == "SENDER":
from meshcore_hub.interface.sender import run_sender
@@ -168,6 +177,7 @@ def run(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
mqtt_prefix=prefix,
mqtt_tls=mqtt_tls,
)
else:
click.echo(f"Unknown mode: {mode}", err=True)
@@ -245,6 +255,13 @@ def run(
envvar="MQTT_PREFIX",
help="MQTT topic prefix",
)
@click.option(
"--mqtt-tls",
is_flag=True,
default=False,
envvar="MQTT_TLS",
help="Enable TLS/SSL for MQTT connection",
)
def receiver(
port: str,
baud: int,
@@ -256,6 +273,7 @@ def receiver(
mqtt_username: str | None,
mqtt_password: str | None,
prefix: str,
mqtt_tls: bool,
) -> None:
"""Run interface in RECEIVER mode.
@@ -280,6 +298,7 @@ def receiver(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
mqtt_prefix=prefix,
mqtt_tls=mqtt_tls,
)
@@ -354,6 +373,13 @@ def receiver(
envvar="MQTT_PREFIX",
help="MQTT topic prefix",
)
@click.option(
"--mqtt-tls",
is_flag=True,
default=False,
envvar="MQTT_TLS",
help="Enable TLS/SSL for MQTT connection",
)
def sender(
port: str,
baud: int,
@@ -365,6 +391,7 @@ def sender(
mqtt_username: str | None,
mqtt_password: str | None,
prefix: str,
mqtt_tls: bool,
) -> None:
"""Run interface in SENDER mode.
@@ -389,4 +416,5 @@ def sender(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
mqtt_prefix=prefix,
mqtt_tls=mqtt_tls,
)

View File

@@ -290,6 +290,7 @@ def create_receiver(
mqtt_username: Optional[str] = None,
mqtt_password: Optional[str] = None,
mqtt_prefix: str = "meshcore",
mqtt_tls: bool = False,
) -> Receiver:
"""Create a configured receiver instance.
@@ -304,6 +305,7 @@ def create_receiver(
mqtt_username: MQTT username
mqtt_password: MQTT password
mqtt_prefix: MQTT topic prefix
mqtt_tls: Enable TLS/SSL for MQTT connection
Returns:
Configured Receiver instance
@@ -324,6 +326,7 @@ def create_receiver(
password=mqtt_password,
prefix=mqtt_prefix,
client_id=f"meshcore-receiver-{device.public_key[:12] if device.public_key else 'unknown'}",
tls=mqtt_tls,
)
mqtt_client = MQTTClient(mqtt_config)
@@ -341,6 +344,7 @@ def run_receiver(
mqtt_username: Optional[str] = None,
mqtt_password: Optional[str] = None,
mqtt_prefix: str = "meshcore",
mqtt_tls: bool = False,
) -> None:
"""Run the receiver (blocking).
@@ -357,6 +361,7 @@ def run_receiver(
mqtt_username: MQTT username
mqtt_password: MQTT password
mqtt_prefix: MQTT topic prefix
mqtt_tls: Enable TLS/SSL for MQTT connection
"""
receiver = create_receiver(
port=port,
@@ -369,6 +374,7 @@ def run_receiver(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
mqtt_prefix=mqtt_prefix,
mqtt_tls=mqtt_tls,
)
# Set up signal handlers

View File

@@ -293,6 +293,7 @@ def create_sender(
mqtt_username: Optional[str] = None,
mqtt_password: Optional[str] = None,
mqtt_prefix: str = "meshcore",
mqtt_tls: bool = False,
) -> Sender:
"""Create a configured sender instance.
@@ -307,6 +308,7 @@ def create_sender(
mqtt_username: MQTT username
mqtt_password: MQTT password
mqtt_prefix: MQTT topic prefix
mqtt_tls: Enable TLS/SSL for MQTT connection
Returns:
Configured Sender instance
@@ -327,6 +329,7 @@ def create_sender(
password=mqtt_password,
prefix=mqtt_prefix,
client_id=f"meshcore-sender-{device.public_key[:12] if device.public_key else 'unknown'}",
tls=mqtt_tls,
)
mqtt_client = MQTTClient(mqtt_config)
@@ -344,6 +347,7 @@ def run_sender(
mqtt_username: Optional[str] = None,
mqtt_password: Optional[str] = None,
mqtt_prefix: str = "meshcore",
mqtt_tls: bool = False,
) -> None:
"""Run the sender (blocking).
@@ -360,6 +364,7 @@ def run_sender(
mqtt_username: MQTT username
mqtt_password: MQTT password
mqtt_prefix: MQTT topic prefix
mqtt_tls: Enable TLS/SSL for MQTT connection
"""
sender = create_sender(
port=port,
@@ -372,6 +377,7 @@ def run_sender(
mqtt_username=mqtt_username,
mqtt_password=mqtt_password,
mqtt_prefix=mqtt_prefix,
mqtt_tls=mqtt_tls,
)
# Set up signal handlers