@@ -75,6 +75,9 @@ pub struct GatewayRefreshArgs {
7575 /// dstack work directory
7676 #[ arg( long) ]
7777 work_dir : PathBuf ,
78+ /// Force reconfiguration even if config unchanged
79+ #[ arg( long) ]
80+ force : bool ,
7881}
7982
8083#[ derive( Deserialize , Serialize , Clone , Default ) ]
@@ -328,37 +331,44 @@ impl HostShared {
328331 }
329332}
330333
331- const GATEWAY_CERT_CACHE_PATH : & str = "/run/dstack/gateway-client-cert-cache.json" ;
334+ const GATEWAY_CACHE_PATH : & str = "/run/dstack/gateway-cache.json" ;
335+ const WG_CONFIG_PATH : & str = "/etc/wireguard/dstack-wg0.conf" ;
332336/// Certificate validity period in seconds (10 days)
333337const CERT_VALIDITY_SECS : u64 = 10 * 24 * 3600 ;
334338
335339#[ derive( Serialize , Deserialize ) ]
336- struct CachedClientCert {
337- cert : String ,
338- key : String ,
340+ struct GatewayCache {
341+ /// Client certificate chain
342+ client_cert : String ,
343+ /// Client private key
344+ client_key : String ,
339345 /// Certificate expiry time as seconds since UNIX epoch
340- not_after : u64 ,
346+ cert_not_after : u64 ,
347+ /// WireGuard private key
348+ wg_sk : String ,
349+ /// WireGuard public key
350+ wg_pk : String ,
341351}
342352
343- impl CachedClientCert {
353+ impl GatewayCache {
344354 fn load ( ) -> Option < Self > {
345- let content = fs:: read_to_string ( GATEWAY_CERT_CACHE_PATH ) . ok ( ) ?;
355+ let content = fs:: read_to_string ( GATEWAY_CACHE_PATH ) . ok ( ) ?;
346356 serde_json:: from_str ( & content) . ok ( )
347357 }
348358
349359 fn save ( & self ) -> Result < ( ) > {
350- let content = serde_json:: to_string ( self ) . context ( "Failed to serialize cert cache" ) ?;
351- safe_write ( GATEWAY_CERT_CACHE_PATH , & content) . context ( "Failed to write cert cache" ) ?;
360+ let content = serde_json:: to_string ( self ) . context ( "Failed to serialize gateway cache" ) ?;
361+ safe_write ( GATEWAY_CACHE_PATH , & content) . context ( "Failed to write gateway cache" ) ?;
352362 Ok ( ( ) )
353363 }
354364
355- fn is_valid ( & self ) -> bool {
365+ fn is_cert_valid ( & self ) -> bool {
356366 let now = std:: time:: SystemTime :: now ( )
357367 . duration_since ( std:: time:: UNIX_EPOCH )
358368 . map ( |d| d. as_secs ( ) )
359369 . unwrap_or ( 0 ) ;
360370 // Valid if at least 10 minutes remaining
361- now + 600 < self . not_after
371+ now + 600 < self . cert_not_after
362372 }
363373}
364374
@@ -406,10 +416,25 @@ impl<'a> GatewayContext<'a> {
406416 . context ( "Failed to register CVM" )
407417 }
408418
409- async fn get_or_request_client_cert ( & self ) -> Result < ( String , String ) > {
410- if let Some ( cached) = CachedClientCert :: load ( ) . filter ( |c| c. is_valid ( ) ) {
419+ fn get_or_generate_wg_keys ( cache : Option < & GatewayCache > ) -> Result < ( String , String ) > {
420+ if let Some ( cache) = cache {
421+ info ! ( "Using cached WireGuard keys" ) ;
422+ return Ok ( ( cache. wg_sk . clone ( ) , cache. wg_pk . clone ( ) ) ) ;
423+ }
424+
425+ info ! ( "Generating new WireGuard keys" ) ;
426+ let sk = cmd ! ( wg genkey) ?;
427+ let pk = cmd ! ( echo $sk | wg pubkey) . or ( Err ( anyhow ! ( "Failed to generate public key" ) ) ) ?;
428+ Ok ( ( sk, pk) )
429+ }
430+
431+ async fn get_or_request_client_cert (
432+ & self ,
433+ cache : Option < & GatewayCache > ,
434+ ) -> Result < ( String , String ) > {
435+ if let Some ( cache) = cache. filter ( |c| c. is_cert_valid ( ) ) {
411436 info ! ( "Using cached client certificate" ) ;
412- return Ok ( ( cached . cert , cached . key ) ) ;
437+ return Ok ( ( cache . client_cert . clone ( ) , cache . client_key . clone ( ) ) ) ;
413438 }
414439
415440 info ! ( "Requesting new client certificate" ) ;
@@ -424,7 +449,8 @@ impl<'a> GatewayContext<'a> {
424449 subject_alt_names : vec ! [ ] ,
425450 usage_server_auth : false ,
426451 usage_client_auth : true ,
427- ext_quote : false ,
452+ // TODO: Disable this once pre-0.5.6 gateways are deprecated
453+ ext_quote : true ,
428454 ext_app_info : true ,
429455 not_before : None ,
430456 not_after : Some ( not_after) ,
@@ -445,20 +471,10 @@ impl<'a> GatewayContext<'a> {
445471 let cert = certs. join ( "\n " ) ;
446472 let key = key. serialize_pem ( ) ;
447473
448- // Cache the certificate for future use
449- let cached = CachedClientCert {
450- cert : cert. clone ( ) ,
451- key : key. clone ( ) ,
452- not_after,
453- } ;
454- if let Err ( e) = cached. save ( ) {
455- warn ! ( "Failed to cache client certificate: {e:?}" ) ;
456- }
457-
458474 Ok ( ( cert, key) )
459475 }
460476
461- async fn setup ( & self ) -> Result < ( ) > {
477+ async fn setup ( & self , force : bool ) -> Result < ( ) > {
462478 if !self . shared . app_compose . gateway_enabled ( ) {
463479 info ! ( "dstack-gateway is not enabled" ) ;
464480 return Ok ( ( ) ) ;
@@ -468,11 +484,28 @@ impl<'a> GatewayContext<'a> {
468484 }
469485
470486 info ! ( "Setting up dstack-gateway" ) ;
471- // Generate WireGuard keys
472- let sk = cmd ! ( wg genkey) ?;
473- let pk = cmd ! ( echo $sk | wg pubkey) . or ( Err ( anyhow ! ( "Failed to generate public key" ) ) ) ?;
474487
475- let ( client_cert, client_key) = self . get_or_request_client_cert ( ) . await ?;
488+ // Load cached data if available
489+ let cache = GatewayCache :: load ( ) ;
490+
491+ // Get or generate WireGuard keys
492+ let ( wg_sk, wg_pk) = Self :: get_or_generate_wg_keys ( cache. as_ref ( ) ) ?;
493+
494+ // Get or request client certificate
495+ let ( client_cert, client_key) = self . get_or_request_client_cert ( cache. as_ref ( ) ) . await ?;
496+
497+ // Calculate cert expiry for cache
498+ let cert_not_after = cache
499+ . as_ref ( )
500+ . filter ( |c| c. is_cert_valid ( ) )
501+ . map ( |c| c. cert_not_after )
502+ . unwrap_or_else ( || {
503+ let now = std:: time:: SystemTime :: now ( )
504+ . duration_since ( std:: time:: UNIX_EPOCH )
505+ . map ( |d| d. as_secs ( ) )
506+ . unwrap_or ( 0 ) ;
507+ now + CERT_VALIDITY_SECS
508+ } ) ;
476509
477510 if self . shared . sys_config . gateway_urls . is_empty ( ) {
478511 bail ! ( "Missing gateway urls" ) ;
@@ -482,7 +515,7 @@ impl<'a> GatewayContext<'a> {
482515 let mut error = anyhow ! ( "unknown error" ) ;
483516 for ( i, url) in self . shared . sys_config . gateway_urls . iter ( ) . enumerate ( ) {
484517 let response = self
485- . register_cvm ( url, client_key. clone ( ) , client_cert. clone ( ) , pk . clone ( ) )
518+ . register_cvm ( url, client_key. clone ( ) , client_cert. clone ( ) , wg_pk . clone ( ) )
486519 . await ;
487520 match response {
488521 Ok ( response) => {
@@ -498,21 +531,24 @@ impl<'a> GatewayContext<'a> {
498531 }
499532 return Err ( error) . context ( "Failed to register CVM, all dstack-gateway urls are down" ) ;
500533 } ;
501- let wg_info = response. wg . context ( "Missing wg info" ) ?;
534+ let mut wg_info = response. wg . context ( "Missing wg info" ) ?;
502535
503536 let client_ip = & wg_info. client_ip ;
504537
538+ // Sort peers by public key for consistent config generation
539+ wg_info. servers . sort_by ( |a, b| a. pk . cmp ( & b. pk ) ) ;
540+
505541 // Create WireGuard config
506542 let wg_listen_port = "9182" ;
507- let mut config = format ! (
543+ let mut new_config = format ! (
508544 "[Interface]\n \
509- PrivateKey = {sk }\n \
545+ PrivateKey = {wg_sk }\n \
510546 ListenPort = {wg_listen_port}\n \
511547 Address = {client_ip}/32\n \n "
512548 ) ;
513549 for WireGuardPeer { pk, ip, endpoint } in & wg_info. servers {
514550 let ip = ip. split ( '/' ) . next ( ) . unwrap_or_default ( ) ;
515- config . push_str ( & format ! (
551+ new_config . push_str ( & format ! (
516552 "[Peer]\n \
517553 PublicKey = {pk}\n \
518554 AllowedIPs = {ip}/32\n \
@@ -521,9 +557,30 @@ impl<'a> GatewayContext<'a> {
521557 ) ) ;
522558 }
523559
560+ // Save cache
561+ let new_cache = GatewayCache {
562+ client_cert,
563+ client_key,
564+ cert_not_after,
565+ wg_sk,
566+ wg_pk,
567+ } ;
568+ if let Err ( e) = new_cache. save ( ) {
569+ warn ! ( "Failed to save gateway cache: {e:?}" ) ;
570+ }
571+
572+ // Check if config has changed (skip check if force is set)
573+ if !force {
574+ let current_config = fs:: read_to_string ( WG_CONFIG_PATH ) . ok ( ) ;
575+ if current_config. as_ref ( ) == Some ( & new_config) {
576+ info ! ( "WireGuard config unchanged, skipping reconfiguration" ) ;
577+ return Ok ( ( ) ) ;
578+ }
579+ }
580+
524581 let wg_dir = Path :: new ( "/etc/wireguard" ) ;
525582 fs:: create_dir_all ( wg_dir) ?;
526- fs:: write ( wg_dir. join ( "dstack-wg0.conf" ) , config ) ?;
583+ fs:: write ( wg_dir. join ( "dstack-wg0.conf" ) , & new_config ) ?;
527584
528585 cmd ! {
529586 chmod 600 $wg_dir/dstack-wg0. conf;
@@ -612,7 +669,7 @@ pub async fn cmd_gateway_refresh(args: GatewayRefreshArgs) -> Result<()> {
612669 let keys: AppKeys = deserialize_json_file ( & keys_path)
613670 . with_context ( || format ! ( "Failed to load app keys from {}" , keys_path. display( ) ) ) ?;
614671
615- GatewayContext :: new ( & shared, & keys) . setup ( ) . await
672+ GatewayContext :: new ( & shared, & keys) . setup ( args . force ) . await
616673}
617674
618675struct AppIdValidator {
@@ -1352,7 +1409,7 @@ impl Stage1<'_> {
13521409 . notify_q ( "boot.progress" , "setting up dstack-gateway" )
13531410 . await ;
13541411 GatewayContext :: new ( & self . shared , & self . keys )
1355- . setup ( )
1412+ . setup ( true )
13561413 . await ?;
13571414 self . vmm
13581415 . notify_q ( "boot.progress" , "setting up docker" )
0 commit comments