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.
1. 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.

2. 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(); } }
2. 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; }
3. 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:
- ServerDriver luôn dùng thông tin mới nhất về FullChain và PrivateKey được lưu trong InstanceManager:
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ừ InstanceManagerprivateKey: InstanceManager.Instance.PrivateKey // Lấy FullChain từ InstanceManager);DefaultDriverBuilder.RegisterServerWebSocketDriver(world, ref driverStore, netDebug, settings);}endif
endif
}}}