Cách EzyTank tự động reload Socket SSL
Back to ezytankLetsencrypt certificate do certbot sinh ra có thời hạn 90 ngày và được tự động tạo mới sau 60 ngày. Bài viết này thảo luận cách EzyTank tự động cập nhật socket SSL khi Letsencrypt certificate được tạo mới.
Lưu đường dẫn đến Full Chain file và Private Key file của Letsencrypt certificate
Trong "Settings" menu của game-master-server plugin ở admin, ta có thể thấy đường dẫn mặc định đến Full Chain file và Private Key file. Admin có thể thay đổi đường dẫn này nếu cần thiết.

Appender ở plugin game-master-server theo dõi Letsencrypt certificate
AdminSocketSslKeystoreReloadAppender theo dõi full chain file, nếu có thay đổi nó sẽ cập thật last modifed date time của file này vào settings:
package org.youngmonkeys.masterserver.admin.appender; import com.fasterxml.jackson.databind.ObjectMapper; import com.tvd12.ezyfox.bean.annotation.EzySingleton; import org.youngmonkeys.ezyplatform.admin.appender.AdminDataAppender; import org.youngmonkeys.ezyplatform.admin.service.AdminSettingService; import org.youngmonkeys.masterserver.admin.service.GameMasterServerSettingService; import java.io.File; import java.util.List; @EzySingleton public class AdminSocketSslKeystoreReloadAppender extends AdminDataAppender<Void, Void, Void> { private final GameMasterServerSettingService masterServerSettingService; public AdminSocketSslKeystoreReloadAppender( ObjectMapper mapper, AdminSettingService settingService, GameMasterServerSettingService masterServerSettingService ) { super(mapper, settingService); this.masterServerSettingService = masterServerSettingService; } @Override protected boolean defaultStarted() { return true; } @Override protected void doAppend() { File cert = new File(masterServerSettingService.getFullChainPath()); if (!cert.exists()) { return; } long currentLastModified = cert.lastModified(); long previousLastModified = masterServerSettingService.getFullChainLastModified(); if (currentLastModified > previousLastModified) { logger.info("Cert file has been updated"); masterServerSettingService.saveFullChainLastModified(currentLastModified); } } @Override protected String getAppenderNamePrefix() { return "socket_ssl_keystore_reload_appender"; } @Override protected Void defaultPageToken() { throw new UnsupportedOperationException(); } @Override protected Class<Void> pageTokenType() { throw new UnsupportedOperationException(); } @Override protected Void extractNewLastPageToken(List<Void> v, Void p) { throw new UnsupportedOperationException(); } @Override protected List<Void> getValueList(Void p) { throw new UnsupportedOperationException(); } }
API /api/v1/instances/{port} trả về thông tin certificate khi có thay đổi
API endpoint này nhận vào
activeFullChainLastModified parameter từ client gửi lên, so sánh với giá trị tương ứng đã lưu trong settings để xác định có cần trả về thông tin certificate cho người dùng hay không:@DoGet("/{port}") public AdminInstanceResponse portGet( @PathVariable int port, @RequestParam(value = "apiKey") String apiKey, @RequestParam( value = "activeFullChainLastModified", defaultValue = "0" ) long activeFullChainLastModified ) { apiKeyValidator.validate(apiKey); return instanceControllerService.getInstance( port, activeFullChainLastModified ); }
package org.youngmonkeys.masterserver.admin.response; import lombok.Getter; import lombok.experimental.SuperBuilder; import org.youngmonkeys.masterserver.response.InstanceResponse; @Getter @SuperBuilder public class AdminInstanceResponse extends InstanceResponse { private int playerCount; private long createdAt; private long updatedAt; private long sslFullChainLastModified; private String sslFullChain; // null nếu không có thay đổi gì private String sslPrivateKey; }
Unity client tự động restart khi nhận về thông tin certificate mới
- Trong "ServerEntryScene",
ServerNetworkStreamDriverEntryPresenterchỉ chuyển qua "ServerLoungeScene" khi có thông tin về SSL certificate
using System; using Unity.Entities; using Unity.NetCode; using Unity.Networking.Transport; using UnityEngine; using UnityEngine.SceneManagement; namespace Youngmonkeys.EzyTank { public class ServerNetworkStreamDriverEntryPresenter : AbstractNetworkStreamDriverEntry<ServerNetworkStreamDriverEntryPresenter> { public void Start() { if (InstanceManager.Instance.FullChain.Equals("")) { return; // Không tiếp tục nếu chưa có thông tin FullChain } if (ClientServerBootstrap.RequestedPlayType == ClientServerBootstrap.PlayType.Server) { var oldConstructor = NetworkStreamReceiveSystem.DriverConstructor; try { NetworkStreamReceiveSystem.DriverConstructor = new IcpUdpWsNetworkDriverConstructor(); var server = ClientServerBootstrap.CreateServerWorld("ServerWorld"); string commandPort = EzyTankCommandLineUtils.GetPort(); Debug.Log($"CommandPort = {commandPort}"); NetworkEndpoint ep = NetworkEndpoint.AnyIpv4.WithPort(Convert.ToUInt16(commandPort)); { using EntityQuery drvQuery = server.EntityManager.CreateEntityQuery(ComponentType.ReadWrite<NetworkStreamDriver>()); drvQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.RequireConnectionApproval = true; drvQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.Listen(ep); } DestroyLocalSimulationWorld(); World.DefaultGameObjectInjectionWorld ??= server; SceneManager.LoadSceneAsync("ServerLoungeScene", LoadSceneMode.Single); } finally { NetworkStreamReceiveSystem.DriverConstructor = oldConstructor; } } } } }
- InstanceManager liên tục gửi requests đến
/api/v1/instances/{port}sau 1 khoảng thời gian, nếu thấy thay đổi về SSL certificate, nó sẽ gọi đếnServerNetworkRestartManager.Instance.RequestServerNetworkRestart()
public class InstanceManager : SingletonMonoBehaviour<InstanceManager> { ... private void Update() { if (ClientServerBootstrap.RequestedPlayType == ClientServerBootstrap.PlayType.Client) { return; } mTimerInSeconds -= Time.deltaTime; if (mTimerInSeconds > 0) { return; } mTimerInSeconds = timerMaxInSeconds; string url = HttpUtils.ToGameMasterServerApiUrl( $"/instances/{mCommandPort}?apiKey={mCommandApiKey}&activeFullChainLastModified={mActiveFullChainLastModified}", true ); HttpGetRequest getRequest = new HttpGetRequest(url); HttpClient.Instance.SendHttpRequest<HttpGetRequest, InstanceModel>( getRequest, HandleGetInstanceResponse, HandleGetInstanceError ); } ... private void HandleGetInstanceResponse(InstanceModel instance) { if (instance.Status == "SHUTTING_DOWN") { string url = HttpUtils.ToGameMasterServerApiUrl($"/instances/{mCommandPort}?apiKey={mCommandApiKey}", true); HttpDeleteRequest deleteRequest = new HttpDeleteRequest(url); HttpClient.Instance.SendHttpRequest<HttpDeleteRequest, NoContentResponse>( deleteRequest, QuitApplication, HandleDeleteInstanceError ); } if (instance.SslFullChain != null) // Có thông tin mới về certificate { mActiveFullChainLastModified = instance.SslFullChainLastModified; mFullChain = instance.SslFullChain; mPrivateKey = instance.SslPrivateKey; ServerNetworkRestartManager.Instance.RequestServerNetworkRestart(); } } ... }
- Game server chỉ restart khi không còn connection nào:
/// <summary> /// MonoBehaviour-based manager that handles server network restarts. /// Lives outside the ECS world to survive world disposal. /// </summary> public class ServerNetworkRestartManager : SingletonMonoBehaviour<ServerNetworkRestartManager> { private static ServerNetworkRestartManager INSTANCE; private bool mRestartPending; private string commandPort; private void Start() { mRestartPending = false; commandPort = EzyTankCommandLineUtils.GetPort(); } private void Update() { // Only run on server if (ClientServerBootstrap.RequestedPlayType != ClientServerBootstrap.PlayType.Server) { return; } if (Input.GetKeyDown(KeyCode.R)) { RequestServerNetworkRestart(); } // Only restart when no connections are left if (mRestartPending) { int connectionCount = GetActiveConnectionCount(); if (connectionCount > 0) { // Still have active connections, keep waiting return; } Debug.Log($"[ServerNetworkRestartManager] No active connections. Restarting server network driver now..."); StartCoroutine(RestartServerNetworkCoroutine()); // Reset state mRestartPending = false; } } public void RequestServerNetworkRestart() { mRestartPending = true; } private int GetActiveConnectionCount() { var serverWorld = GetServerWorld(); if (serverWorld == null || !serverWorld.IsCreated) { return 0; } var entityManager = serverWorld.EntityManager; var connectionQuery = entityManager.CreateEntityQuery( ComponentType.ReadOnly<NetworkId>(), ComponentType.ReadOnly<NetworkStreamConnection>() ); int count = connectionQuery.CalculateEntityCount(); connectionQuery.Dispose(); return count; } private World GetServerWorld() { foreach (var world in World.All) { if (world.IsServer()) { return world; } } return null; } private IEnumerator RestartServerNetworkCoroutine() { Debug.Log($"[ServerNetworkRestartManager] Preparing to restart on port {commandPort}"); // Get reference to the old world var worldToDispose = GetServerWorld(); // Clean up scenes associated with the old world CleanupOldScenes(); yield return null; // IMPORTANT: Dispose the old world to release the port Debug.Log("[ServerNetworkRestartManager] Disposing old server world to release port..."); if (worldToDispose != null) { worldToDispose.Dispose(); } // Wait a frame to ensure cleanup is complete yield return new WaitForSeconds(2); SceneManager.LoadSceneAsync("ServerEntryScene", LoadSceneMode.Single); } private void CleanupOldScenes() { // Unload ServerLoungeScene if it's loaded for (int i = 0; i < SceneManager.sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); if (scene.name == "ServerLoungeScene" && scene.isLoaded) { Debug.Log($"[ServerNetworkRestartManager] Unloading scene: {scene.name}"); SceneManager.UnloadSceneAsync(scene); } } } }
- ServerDriver luôn dùng thông tin mới nhất về FullChain và PrivateKey được lưu trong InstanceManager:
using Unity.Entities; using Unity.NetCode; using Unity.Networking.Transport; using Unity.Networking.Transport.TLS; using UnityEngine; namespace Youngmonkeys.EzyTank { public struct IcpUdpWsNetworkDriverConstructor : INetworkStreamDriverConstructor { ... public void CreateServerDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug) { #if UNITY_EDITOR || !UNITY_WEBGL var settings = DefaultDriverBuilder.GetNetworkServerSettings(); DefaultDriverBuilder.RegisterServerIpcDriver(world, ref driverStore, netDebug, settings); DefaultDriverBuilder.RegisterServerUdpDriver(world, ref driverStore, netDebug, settings); #if !UNITY_WEBGL if (ClientServerBootstrap.RequestedPlayType != ClientServerBootstrap.PlayType.ClientAndServer) { settings.WithSecureServerParameters( certificate: InstanceManager.Instance.FullChain, // Lấy FullChain từ InstanceManager privateKey: InstanceManager.Instance.PrivateKey // Lấy FullChain từ InstanceManager ); DefaultDriverBuilder.RegisterServerWebSocketDriver(world, ref driverStore, netDebug, settings); } #endif #endif } } }