22//
33// SPDX-License-Identifier: BUSL-1.1
44
5+ use std:: collections:: { BTreeMap , BTreeSet } ;
56use std:: fmt:: Debug ;
67use std:: sync:: atomic:: Ordering ;
78
@@ -34,8 +35,23 @@ impl AppAddress {
3435 }
3536}
3637
38+ fn select_app_address ( items : & [ Box < [ u8 ] > ] , known_apps : & BTreeMap < String , BTreeSet < String > > ) -> Result < AppAddress > {
39+ let mut fallback = None ;
40+ for data in items {
41+ if let Ok ( addr) = AppAddress :: parse ( data) {
42+ if known_apps. contains_key ( & addr. app_id ) {
43+ return Ok ( addr) ;
44+ }
45+ if fallback. is_none ( ) {
46+ fallback = Some ( addr) ;
47+ }
48+ }
49+ }
50+ fallback. context ( "no app address found in txt record" )
51+ }
52+
3753/// resolve app address by sni
38- async fn resolve_app_address ( prefix : & str , sni : & str , compat : bool ) -> Result < AppAddress > {
54+ async fn resolve_app_address( prefix : & str , sni : & str , compat : bool , state : & Proxy ) -> Result < AppAddress > {
3955 let txt_domain = format ! ( "{prefix}.{sni}" ) ;
4056 let resolver = hickory_resolver:: AsyncResolver :: tokio_from_system_conf ( )
4157 . context ( "failed to create dns resolver" ) ?;
@@ -53,6 +69,7 @@ async fn resolve_app_address(prefix: &str, sni: &str, compat: bool) -> Result<Ap
5369 let Some ( txt_record) = lookup. iter ( ) . next ( ) else {
5470 continue ;
5571 } ;
72+ <<<<<<< HEAD
5673 let Some ( data ) = txt_record. txt_data ( ) . first ( ) else {
5774 continue;
5875 } ;
@@ -90,6 +107,42 @@ async fn resolve_app_address(prefix: &str, sni: &str, compat: bool) -> Result<Ap
90107 } ) ;
91108 }
92109
110+ =======
111+ let locked = state. lock( ) ;
112+ if let Ok ( addr) = select_app_address( txt_record. txt_data( ) , & locked. state. apps) {
113+ return Ok ( addr) ;
114+ }
115+ }
116+ } else if let Ok ( lookup) = resolver. txt_lookup( txt_domain) . await {
117+ if let Some ( txt_record) = lookup. iter( ) . next( ) {
118+ let locked = state. lock( ) ;
119+ if let Ok ( addr) = select_app_address( txt_record. txt_data( ) , & locked. state. apps) {
120+ return Ok ( addr) ;
121+ }
122+ }
123+ }
124+
125+ // wildcard fallback: try {prefix}-wildcard.{parent_domain}
126+ if let Some ( ( _, parent) ) = sni. split_once( '.' ) {
127+ let wildcard_domain = format ! ( "{prefix}-wildcard.{parent}" ) ;
128+ let lookup = resolver
129+ . txt_lookup( & wildcard_domain)
130+ . await
131+ . with_context( || {
132+ format ! ( "failed to lookup wildcard app address for {sni} via {wildcard_domain}" )
133+ } ) ?;
134+ let txt_record = lookup
135+ . iter( )
136+ . next( )
137+ . with_context( || format ! ( "no txt record found for {sni} via {wildcard_domain}" ) ) ?;
138+ let locked = state. lock( ) ;
139+ return select_app_address( txt_record. txt_data( ) , & locked. state. apps)
140+ . with_context( || {
141+ format ! ( "failed to parse app address for {sni} via {wildcard_domain}" )
142+ } ) ;
143+ }
144+
145+ >>>>>>> 8e3 d6c27 ( prefer locally-known app_id when TXT record contains multiple app addresses)
93146 anyhow:: bail!( "failed to resolve app address for {sni}" ) ;
94147}
95148
@@ -102,7 +155,7 @@ pub(crate) async fn proxy_with_sni(
102155 let ns_prefix = & state. config. proxy. app_address_ns_prefix;
103156 let compat = state. config. proxy. app_address_ns_compat;
104157 let dns_timeout = state. config. proxy. timeouts. dns_resolve;
105- let addr = timeout ( dns_timeout, resolve_app_address ( ns_prefix, sni, compat) )
158+ let addr = timeout( dns_timeout, resolve_app_address( ns_prefix, sni, compat, & state ) )
106159 . await
107160 . with_context( || format ! ( "DNS TXT resolve timeout for {sni}" ) ) ?
108161 . with_context( || format ! ( "failed to resolve app address for {sni}" ) ) ?;
@@ -197,17 +250,65 @@ pub(crate) async fn proxy_to_app(
197250#[ cfg( test) ]
198251mod tests {
199252 use super :: * ;
253+ use crate :: {
254+ config:: { load_config_figment, Config , MutualConfig , TlsConfig } ,
255+ main_service:: ProxyOptions ,
256+ } ;
257+ use tempfile:: TempDir ;
258+
259+ fn boxed ( s : & [ u8 ] ) -> Box < [ u8 ] > {
260+ s. to_vec ( ) . into_boxed_slice ( )
261+ }
262+
263+ async fn create_test_proxy ( ) -> ( Proxy , TempDir ) {
264+ let figment = load_config_figment ( None ) ;
265+ let mut config = figment. focus ( "core" ) . extract :: < Config > ( ) . unwrap ( ) ;
266+ let temp_dir = TempDir :: new ( ) . expect ( "failed to create temp dir" ) ;
267+ config. sync . data_dir = temp_dir. path ( ) . to_string_lossy ( ) . to_string ( ) ;
268+ let proxy = Proxy :: new ( ProxyOptions {
269+ config,
270+ my_app_id : None ,
271+ tls_config : TlsConfig {
272+ certs : "" . to_string ( ) ,
273+ key : "" . to_string ( ) ,
274+ mutual : MutualConfig { ca_certs : "" . to_string ( ) } ,
275+ } ,
276+ } )
277+ . await
278+ . expect ( "failed to create proxy" ) ;
279+ ( proxy, temp_dir)
280+ }
200281
201282 #[ tokio:: test]
202283 async fn test_resolve_app_address ( ) {
284+ let ( state, _dir) = create_test_proxy ( ) . await ;
203285 let app_addr = resolve_app_address (
204286 "_dstack-app-address" ,
205287 "3327603e03f5bd1f830812ca4a789277fc31f577.app.dstack.org" ,
206288 false ,
289+ & state,
207290 )
208291 . await
209292 . unwrap ( ) ;
210293 assert_eq ! ( app_addr. app_id, "3327603e03f5bd1f830812ca4a789277fc31f577" ) ;
211294 assert_eq ! ( app_addr. port, 8090 ) ;
212295 }
296+
297+ #[ test]
298+ fn test_select_app_address_prefers_local ( ) {
299+ let items = vec ! [ boxed( b"aaaaaa:443" ) , boxed( b"bbbbbb:8080" ) ] ;
300+ let mut apps = BTreeMap :: new ( ) ;
301+ apps. insert ( "bbbbbb" . to_string ( ) , BTreeSet :: new ( ) ) ;
302+ let addr = select_app_address ( & items, & apps) . unwrap ( ) ;
303+ assert_eq ! ( addr. app_id, "bbbbbb" ) ;
304+ assert_eq ! ( addr. port, 8080 ) ;
305+ }
306+
307+ #[ test]
308+ fn test_select_app_address_fallback_when_none_local ( ) {
309+ let items = vec ! [ boxed( b"aaaaaa:443" ) , boxed( b"bbbbbb:8080" ) ] ;
310+ let addr = select_app_address ( & items, & BTreeMap :: new ( ) ) . unwrap ( ) ;
311+ assert_eq ! ( addr. app_id, "aaaaaa" ) ;
312+ assert_eq ! ( addr. port, 443 ) ;
313+ }
213314}
0 commit comments