Browse Source

Improve SSL certificate reload: rollback safety, defaults, and test encapsulation

pull/15301/head
Andrii Landiak 6 months ago
parent
commit
22d9506206
  1. 2
      application/src/main/resources/thingsboard.yml
  2. 10
      common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java
  3. 120
      common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java
  4. 17
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java
  5. 38
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java
  6. 21
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java
  7. 3
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java
  8. 4
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java
  9. 2
      transport/coap/src/main/resources/tb-coap-transport.yml
  10. 2
      transport/http/src/main/resources/tb-http-transport.yml
  11. 2
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  12. 2
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml

2
application/src/main/resources/thingsboard.yml

@ -1400,7 +1400,7 @@ transport:
# X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.)
reload: reload:
# Enable/disable automatic SSL certificates reload # Enable/disable automatic SSL certificates reload
enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}"
# Check interval in seconds for certificates reload # Check interval in seconds for certificates reload
check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}"

10
common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java

@ -154,14 +154,14 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial
return networkConfig; return networkConfig;
} }
DtlsConnectorConfig buildDtlsConnectorConfig(Configuration networkConfig) throws UnknownHostException { private DtlsConnectorConfig buildDtlsConnectorConfig(Configuration networkConfig) throws UnknownHostException {
TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings(); TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings();
DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(networkConfig); DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig(networkConfig);
networkConfig.set(CoapConfig.COAP_SECURE_PORT, dtlsConnectorConfig.getAddress().getPort()); networkConfig.set(CoapConfig.COAP_SECURE_PORT, dtlsConnectorConfig.getAddress().getPort());
return dtlsConnectorConfig; return dtlsConnectorConfig;
} }
CoapEndpoint buildDtlsEndpoint(Configuration networkConfig, DTLSConnector connector) { private CoapEndpoint buildDtlsEndpoint(Configuration networkConfig, DTLSConnector connector) {
CoapEndpoint.Builder dtlsCoapEndpointBuilder = new CoapEndpoint.Builder(); CoapEndpoint.Builder dtlsCoapEndpointBuilder = new CoapEndpoint.Builder();
dtlsCoapEndpointBuilder.setConfiguration(networkConfig); dtlsCoapEndpointBuilder.setConfiguration(networkConfig);
dtlsCoapEndpointBuilder.setConnector(connector); dtlsCoapEndpointBuilder.setConnector(connector);
@ -179,7 +179,7 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial
tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier();
} }
DTLSConnector createDtlsConnector(DtlsConnectorConfig config) { private DTLSConnector createDtlsConnector(DtlsConnectorConfig config) {
return new DTLSConnector(config); return new DTLSConnector(config);
} }
@ -195,7 +195,7 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial
DTLSConnector newConnector = createDtlsConnector(dtlsConnectorConfig); DTLSConnector newConnector = createDtlsConnector(dtlsConnectorConfig);
CoapEndpoint newEndpoint = buildDtlsEndpoint(networkConfig, newConnector); CoapEndpoint newEndpoint = buildDtlsEndpoint(networkConfig, newConnector);
// Stop old endpoint first to release the port before starting the new one // Stop the old endpoint first to release the port before starting the new one
if (oldDtlsEndpoint != null) { if (oldDtlsEndpoint != null) {
log.info("Stopping old DTLS endpoint to release the port..."); log.info("Stopping old DTLS endpoint to release the port...");
server.getEndpoints().remove(oldDtlsEndpoint); server.getEndpoints().remove(oldDtlsEndpoint);
@ -229,7 +229,7 @@ public class DefaultCoapServerService implements CoapServerService, SmartInitial
dtlsCoapEndpoint = newEndpoint; dtlsCoapEndpoint = newEndpoint;
tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier(); tbDtlsCertificateVerifier = (TbCoapDtlsCertificateVerifier) dtlsConnectorConfig.getAdvancedCertificateVerifier();
// Destroy old resources after successful swap // Destroy old resources after a successful swap
if (oldDtlsEndpoint != null) { if (oldDtlsEndpoint != null) {
if (oldDtlsConnector != null) { if (oldDtlsConnector != null) {
oldDtlsConnector.destroy(); oldDtlsConnector.destroy();

120
common/coap-server/src/test/java/org/thingsboard/server/coapserver/CoapDtlsCertificateReloadTest.java

@ -18,7 +18,6 @@ package org.thingsboard.server.coapserver;
import org.eclipse.californium.core.CoapServer; import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.network.CoapEndpoint; import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.Endpoint; import org.eclipse.californium.core.network.Endpoint;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.scandium.DTLSConnector; import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -26,22 +25,23 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.MockedConstruction;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -155,37 +155,42 @@ public class CoapDtlsCertificateReloadTest {
// GIVEN // GIVEN
when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings);
CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class);
DTLSConnector mockNewConnector = mock(DTLSConnector.class);
DtlsConnectorConfig mockDtlsConfig = mock(DtlsConnectorConfig.class); DtlsConnectorConfig mockDtlsConfig = mock(DtlsConnectorConfig.class);
TbCoapDtlsCertificateVerifier mockNewVerifier = mock(TbCoapDtlsCertificateVerifier.class); TbCoapDtlsCertificateVerifier mockNewVerifier = mock(TbCoapDtlsCertificateVerifier.class);
when(mockDtlsConfig.getAdvancedCertificateVerifier()).thenReturn(mockNewVerifier); when(mockDtlsConfig.getAdvancedCertificateVerifier()).thenReturn(mockNewVerifier);
when(mockDtlsConfig.getAddress()).thenReturn(new InetSocketAddress("localhost", 5684));
when(mockDtlsSettings.dtlsConnectorConfig(any())).thenReturn(mockDtlsConfig);
DefaultCoapServerService spyService = Mockito.spy(coapServerService); ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer);
ReflectionTestUtils.setField(spyService, "coapServerContext", mockCoapServerContext); ReflectionTestUtils.setField(coapServerService, "dtlsCoapEndpoint", mockDtlsEndpoint);
ReflectionTestUtils.setField(spyService, "server", mockCoapServer); ReflectionTestUtils.setField(coapServerService, "dtlsConnector", mockDtlsConnector);
ReflectionTestUtils.setField(spyService, "dtlsCoapEndpoint", mockDtlsEndpoint);
ReflectionTestUtils.setField(spyService, "dtlsConnector", mockDtlsConnector);
doReturn(mockDtlsConfig).when(spyService).buildDtlsConnectorConfig(any(Configuration.class));
doReturn(mockNewConnector).when(spyService).createDtlsConnector(any(DtlsConnectorConfig.class));
doReturn(mockNewEndpoint).when(spyService).buildDtlsEndpoint(any(Configuration.class), any(DTLSConnector.class));
List<Endpoint> endpointsList = new CopyOnWriteArrayList<>(); List<Endpoint> endpointsList = new CopyOnWriteArrayList<>();
endpointsList.add(mockDtlsEndpoint); endpointsList.add(mockDtlsEndpoint);
when(mockCoapServer.getEndpoints()).thenReturn(endpointsList); when(mockCoapServer.getEndpoints()).thenReturn(endpointsList);
// WHEN CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class);
ReflectionTestUtils.invokeMethod(spyService, "recreateDtlsEndpoint");
try (MockedConstruction<DTLSConnector> dtlsMock = mockConstruction(DTLSConnector.class);
// THEN MockedConstruction<CoapEndpoint.Builder> builderMock = mockConstruction(CoapEndpoint.Builder.class,
assertThat(endpointsList).doesNotContain(mockDtlsEndpoint); (builder, context) -> {
verify(mockDtlsEndpoint).stop(); when(builder.build()).thenReturn(mockNewEndpoint);
verify(mockDtlsEndpoint).destroy(); when(builder.setConfiguration(any())).thenReturn(builder);
verify(mockDtlsConnector).destroy(); when(builder.setConnector(any(DTLSConnector.class))).thenReturn(builder);
verify(mockCoapServer).addEndpoint(mockNewEndpoint); })) {
verify(mockNewEndpoint).start();
assertThat(ReflectionTestUtils.getField(spyService, "dtlsCoapEndpoint")).isSameAs(mockNewEndpoint); // WHEN
ReflectionTestUtils.invokeMethod(coapServerService, "recreateDtlsEndpoint");
// THEN
assertThat(endpointsList).doesNotContain(mockDtlsEndpoint);
verify(mockDtlsEndpoint).stop();
verify(mockDtlsEndpoint).destroy();
verify(mockDtlsConnector).destroy();
verify(mockCoapServer).addEndpoint(mockNewEndpoint);
verify(mockNewEndpoint).start();
assertThat(ReflectionTestUtils.getField(coapServerService, "dtlsCoapEndpoint")).isSameAs(mockNewEndpoint);
}
} }
@Test @Test
@ -193,44 +198,49 @@ public class CoapDtlsCertificateReloadTest {
// GIVEN // GIVEN
when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings); when(mockCoapServerContext.getDtlsSettings()).thenReturn(mockDtlsSettings);
CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class);
DTLSConnector mockNewConnector = mock(DTLSConnector.class);
DtlsConnectorConfig mockDtlsConfig = mock(DtlsConnectorConfig.class); DtlsConnectorConfig mockDtlsConfig = mock(DtlsConnectorConfig.class);
when(mockDtlsConfig.getAddress()).thenReturn(new InetSocketAddress("localhost", 5684));
when(mockDtlsSettings.dtlsConnectorConfig(any())).thenReturn(mockDtlsConfig);
doThrow(new IOException("start failed")).when(mockNewEndpoint).start(); ReflectionTestUtils.setField(coapServerService, "server", mockCoapServer);
ReflectionTestUtils.setField(coapServerService, "dtlsCoapEndpoint", mockDtlsEndpoint);
DefaultCoapServerService spyService = Mockito.spy(coapServerService); ReflectionTestUtils.setField(coapServerService, "dtlsConnector", mockDtlsConnector);
ReflectionTestUtils.setField(spyService, "coapServerContext", mockCoapServerContext);
ReflectionTestUtils.setField(spyService, "server", mockCoapServer);
ReflectionTestUtils.setField(spyService, "dtlsCoapEndpoint", mockDtlsEndpoint);
ReflectionTestUtils.setField(spyService, "dtlsConnector", mockDtlsConnector);
doReturn(mockDtlsConfig).when(spyService).buildDtlsConnectorConfig(any(Configuration.class));
doReturn(mockNewConnector).when(spyService).createDtlsConnector(any(DtlsConnectorConfig.class));
doReturn(mockNewEndpoint).when(spyService).buildDtlsEndpoint(any(Configuration.class), any(DTLSConnector.class));
List<Endpoint> endpointsList = new CopyOnWriteArrayList<>(); List<Endpoint> endpointsList = new CopyOnWriteArrayList<>();
endpointsList.add(mockDtlsEndpoint); endpointsList.add(mockDtlsEndpoint);
when(mockCoapServer.getEndpoints()).thenReturn(endpointsList); when(mockCoapServer.getEndpoints()).thenReturn(endpointsList);
// WHEN - the callback catches the IOException internally CoapEndpoint mockNewEndpoint = mock(CoapEndpoint.class);
spyService.afterSingletonsInstantiated(); doThrow(new IOException("start failed")).when(mockNewEndpoint).start();
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture());
Runnable reloadCallback = callbackCaptor.getValue();
reloadCallback.run();
// THEN - new resources cleaned up try (MockedConstruction<DTLSConnector> dtlsMock = mockConstruction(DTLSConnector.class);
verify(mockNewEndpoint).destroy(); MockedConstruction<CoapEndpoint.Builder> builderMock = mockConstruction(CoapEndpoint.Builder.class,
verify(mockNewConnector).destroy(); (builder, context) -> {
assertThat(endpointsList).doesNotContain(mockNewEndpoint); when(builder.build()).thenReturn(mockNewEndpoint);
// Old endpoint was stopped to release port, then restored after new one failed when(builder.setConfiguration(any())).thenReturn(builder);
verify(mockDtlsEndpoint).stop(); when(builder.setConnector(any(DTLSConnector.class))).thenReturn(builder);
verify(mockDtlsEndpoint).start(); })) {
// Old fields preserved
assertThat(ReflectionTestUtils.getField(spyService, "dtlsCoapEndpoint")).isSameAs(mockDtlsEndpoint); // WHEN
assertThat(ReflectionTestUtils.getField(spyService, "dtlsConnector")).isSameAs(mockDtlsConnector); coapServerService.afterSingletonsInstantiated();
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mockDtlsSettings).registerReloadCallback(callbackCaptor.capture());
Runnable reloadCallback = callbackCaptor.getValue();
reloadCallback.run();
// THEN - new resources cleaned up
DTLSConnector constructedConnector = dtlsMock.constructed().get(0);
verify(mockNewEndpoint).destroy();
verify(constructedConnector).destroy();
assertThat(endpointsList).doesNotContain(mockNewEndpoint);
// Old endpoint was stopped to release port, then restored after new one failed
verify(mockDtlsEndpoint).stop();
verify(mockDtlsEndpoint).start();
// Old fields preserved
assertThat(ReflectionTestUtils.getField(coapServerService, "dtlsCoapEndpoint")).isSameAs(mockDtlsEndpoint);
assertThat(ReflectionTestUtils.getField(coapServerService, "dtlsConnector")).isSameAs(mockDtlsConnector);
}
} }
} }

17
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java

@ -185,10 +185,10 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto
log.info("Creating new LwM2M Bootstrap server with updated certificates..."); log.info("Creating new LwM2M Bootstrap server with updated certificates...");
LeshanBootstrapServer newServer = getLhBootstrapServer(); LeshanBootstrapServer newServer = getLhBootstrapServer();
// Stop the old server first to release the ports before starting the new one // Stop (not destroy) the old server to release ports but keep it restartable for rollback
if (oldServer != null) { if (oldServer != null) {
log.info("Stopping old LwM2M Bootstrap server to release ports..."); log.info("Stopping old LwM2M Bootstrap server to release ports...");
oldServer.destroy(); oldServer.stop();
} }
try { try {
@ -196,13 +196,11 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to start new LwM2M Bootstrap server", e); log.error("Failed to start new LwM2M Bootstrap server", e);
newServer.destroy(); newServer.destroy();
// Attempt to restore the old server // Attempt to restart the old server (only stopped, not destroyed)
if (oldServer != null) { if (oldServer != null) {
try { try {
LeshanBootstrapServer restoredServer = getLhBootstrapServer(); oldServer.start();
restoredServer.start(); log.info("Restored old LwM2M Bootstrap server successfully.");
this.server = restoredServer;
log.info("Restored LwM2M Bootstrap server with previous configuration.");
} catch (Exception restoreEx) { } catch (Exception restoreEx) {
log.error("Failed to restore old LwM2M Bootstrap server", restoreEx); log.error("Failed to restore old LwM2M Bootstrap server", restoreEx);
} }
@ -211,6 +209,11 @@ public class LwM2MTransportBootstrapService implements SmartInitializingSingleto
} }
this.server = newServer; this.server = newServer;
log.info("New LwM2M Bootstrap server started successfully."); log.info("New LwM2M Bootstrap server started successfully.");
// Destroy the old server only after a successful swap
if (oldServer != null) {
oldServer.destroy();
}
} }
} }

38
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java

@ -236,7 +236,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar
log.info("Creating new LwM2M server with updated certificates..."); log.info("Creating new LwM2M server with updated certificates...");
LeshanServer newServer = getLhServer(); LeshanServer newServer = getLhServer();
// Stop old server first to release the ports before starting the new one // Stop (not destroy) old server to release ports but keep it restartable for rollback
if (oldServer != null) { if (oldServer != null) {
log.info("Stopping old LwM2M server to release ports..."); log.info("Stopping old LwM2M server to release ports...");
if (oldListener != null) { if (oldListener != null) {
@ -245,7 +245,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar
oldServer.getObservationService().removeListener(oldListener.observationListener); oldServer.getObservationService().removeListener(oldListener.observationListener);
oldServer.getSendService().removeListener(oldListener.sendListener); oldServer.getSendService().removeListener(oldListener.sendListener);
} }
oldServer.destroy(); oldServer.stop();
} }
try { try {
@ -253,21 +253,20 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to start new LwM2M server", e); log.error("Failed to start new LwM2M server", e);
newServer.destroy(); newServer.destroy();
// Attempt to restore the old server // Attempt to restart the old server (only stopped, not destroyed)
try { if (oldServer != null) {
LeshanServer restoredServer = getLhServer(); try {
restoredServer.start(); oldServer.start();
LwM2mServerListener restoredListener = new LwM2mServerListener(handler); if (oldListener != null) {
restoredServer.getRegistrationService().addListener(restoredListener.registrationListener); oldServer.getRegistrationService().addListener(oldListener.registrationListener);
restoredServer.getPresenceService().addListener(restoredListener.presenceListener); oldServer.getPresenceService().addListener(oldListener.presenceListener);
restoredServer.getObservationService().addListener(restoredListener.observationListener); oldServer.getObservationService().addListener(oldListener.observationListener);
restoredServer.getSendService().addListener(restoredListener.sendListener); oldServer.getSendService().addListener(oldListener.sendListener);
this.server = restoredServer; }
this.context.setServer(restoredServer); log.info("Restored old LwM2M server successfully.");
this.serverListener = restoredListener; } catch (Exception restoreEx) {
log.info("Restored LwM2M server with previous configuration."); log.error("Failed to restore old LwM2M server", restoreEx);
} catch (Exception restoreEx) { }
log.error("Failed to restore old LwM2M server", restoreEx);
} }
throw e; throw e;
} }
@ -282,6 +281,11 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService, Smar
this.context.setServer(newServer); this.context.setServer(newServer);
this.serverListener = newListener; this.serverListener = newListener;
log.info("New LwM2M server started successfully."); log.info("New LwM2M server started successfully.");
// Destroy old server only after successful swap
if (oldServer != null) {
oldServer.destroy();
}
} }
@Override @Override

21
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2mBootstrapCertificateReloadTest.java

@ -122,6 +122,7 @@ public class LwM2mBootstrapCertificateReloadTest {
// The old server should NOT be destroyed since the new server was never created. // The old server should NOT be destroyed since the new server was never created.
reloadCallback.run(); reloadCallback.run();
verify(mockBootstrapServer, never()).stop();
verify(mockBootstrapServer, never()).destroy(); verify(mockBootstrapServer, never()).destroy();
assertThat(ReflectionTestUtils.getField(bootstrapService, "server")).isSameAs(mockBootstrapServer); assertThat(ReflectionTestUtils.getField(bootstrapService, "server")).isSameAs(mockBootstrapServer);
} }
@ -164,18 +165,15 @@ public class LwM2mBootstrapCertificateReloadTest {
} }
@Test @Test
public void givenReloadCallback_whenNewServerStartFails_thenNewServerDestroyedAndRestorationAttempted() { public void givenReloadCallback_whenNewServerStartFails_thenOldServerRestarted() {
// GIVEN // GIVEN
ReflectionTestUtils.setField(bootstrapService, "server", mockBootstrapServer); ReflectionTestUtils.setField(bootstrapService, "server", mockBootstrapServer);
LeshanBootstrapServer mockNewServer = mock(LeshanBootstrapServer.class); LeshanBootstrapServer mockNewServer = mock(LeshanBootstrapServer.class);
doThrow(new RuntimeException("start failed")).when(mockNewServer).start(); doThrow(new RuntimeException("start failed")).when(mockNewServer).start();
LeshanBootstrapServer mockRestoredServer = mock(LeshanBootstrapServer.class);
LwM2MTransportBootstrapService spyService = Mockito.spy(bootstrapService); LwM2MTransportBootstrapService spyService = Mockito.spy(bootstrapService);
// First call returns the failing server, second call returns the restoration server doReturn(mockNewServer).when(spyService).getLhBootstrapServer();
doReturn(mockNewServer).doReturn(mockRestoredServer).when(spyService).getLhBootstrapServer();
ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class); ArgumentCaptor<Runnable> callbackCaptor = ArgumentCaptor.forClass(Runnable.class);
spyService.afterSingletonsInstantiated(); spyService.afterSingletonsInstantiated();
@ -187,13 +185,14 @@ public class LwM2mBootstrapCertificateReloadTest {
reloadCallback.run(); reloadCallback.run();
// THEN // THEN
// Old server is destroyed to release ports // Old server is stopped (not destroyed) to release ports
verify(mockBootstrapServer).destroy(); verify(mockBootstrapServer).stop();
// New server fails to start and is destroyed verify(mockBootstrapServer, never()).destroy();
// The new server fails to start and is destroyed
verify(mockNewServer).destroy(); verify(mockNewServer).destroy();
// Restoration server is started and becomes the active server // Old server is restarted (not rebuilt from potentially stale credentials)
verify(mockRestoredServer).start(); verify(mockBootstrapServer).start();
assertThat(ReflectionTestUtils.getField(spyService, "server")).isSameAs(mockRestoredServer); assertThat(ReflectionTestUtils.getField(spyService, "server")).isSameAs(mockBootstrapServer);
} }
} }

3
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerCertificateReloadTest.java

@ -139,9 +139,10 @@ public class LwM2mServerCertificateReloadTest {
// Force getLhServer() to fail by returning null host (causes InetSocketAddress to throw) // Force getLhServer() to fail by returning null host (causes InetSocketAddress to throw)
when(mockConfig.getHost()).thenReturn(null); when(mockConfig.getHost()).thenReturn(null);
// With create-then-swap, the old server should NOT be destroyed if the new one fails. // With create-then-swap, the old server should NOT be stopped/destroyed if the new one fails to build.
reloadCallback.run(); reloadCallback.run();
verify(mockLeshanServer, never()).stop();
verify(mockLeshanServer, never()).destroy(); verify(mockLeshanServer, never()).destroy();
// Old server should still be the active one // Old server should still be the active one
assertThat(ReflectionTestUtils.getField(lwm2mTransportService, "server")).isSameAs(mockLeshanServer); assertThat(ReflectionTestUtils.getField(lwm2mTransportService, "server")).isSameAs(mockLeshanServer);

4
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java

@ -48,7 +48,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis
private static final int MAX_CONSECUTIVE_FAILURES = 10; private static final int MAX_CONSECUTIVE_FAILURES = 10;
@Value("${transport.ssl.certificate.reload.enabled:true}") @Value("${transport.ssl.certificate.reload.enabled:false}")
private boolean reloadEnabled; private boolean reloadEnabled;
@Value("${transport.ssl.certificate.reload.check_interval_seconds:60}") @Value("${transport.ssl.certificate.reload.check_interval_seconds:60}")
@ -103,7 +103,7 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis
List<Path> filePaths = credentials.getCertificateFilePaths(); List<Path> filePaths = credentials.getCertificateFilePaths();
if (filePaths == null || filePaths.isEmpty()) { if (filePaths == null || filePaths.isEmpty()) {
log.debug("No certificate files to watch for: {} ({})", config.getName(), beanName); log.debug("No file-system certificate paths to watch for: {} ({}) — certificates may be classpath-based", config.getName(), beanName);
continue; continue;
} }

2
transport/coap/src/main/resources/tb-coap-transport.yml

@ -176,7 +176,7 @@ transport:
# X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.)
reload: reload:
# Enable/disable automatic SSL certificates reload # Enable/disable automatic SSL certificates reload
enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}"
# Check interval in seconds for certificates reload # Check interval in seconds for certificates reload
check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}"

2
transport/http/src/main/resources/tb-http-transport.yml

@ -207,7 +207,7 @@ transport:
# X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.)
reload: reload:
# Enable/disable automatic SSL certificates reload # Enable/disable automatic SSL certificates reload
enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}"
# Check interval in seconds for certificates reload # Check interval in seconds for certificates reload
check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}"

2
transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml

@ -307,7 +307,7 @@ transport:
# X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.)
reload: reload:
# Enable/disable automatic SSL certificates reload # Enable/disable automatic SSL certificates reload
enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}"
# Check interval in seconds for certificates reload # Check interval in seconds for certificates reload
check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}"

2
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -240,7 +240,7 @@ transport:
# X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.) # X.509 certificate configuration to auto-detect and reload certificate used by transport protocols in real-time (MQTT, CoAP, LwM2M, etc.)
reload: reload:
# Enable/disable automatic SSL certificates reload # Enable/disable automatic SSL certificates reload
enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:true}" enabled: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_ENABLED:false}"
# Check interval in seconds for certificates reload # Check interval in seconds for certificates reload
check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}" check_interval_seconds: "${TB_TRANSPORT_SSL_CERTIFICATE_RELOAD_CHECK_INTERVAL_SECONDS:60}"

Loading…
Cancel
Save