6666 errProtonCredsMismatch = errors .New ("proton: creds mismatch" )
6767)
6868
69- const maxProtonLogicalsRefreshThreshold = 72 * time .Hour
69+ const (
70+ maxProtonLogicalsRefreshThreshold = 72 * time .Hour
71+ maxPerRegionWgConfs = 6
72+ maxRegisterCertTries = 3
73+ )
7074
7175var protonLogicalsUpdateTime = time.Time {}
7276
@@ -310,19 +314,20 @@ type ProtonServerResponse struct {
310314// "Load": 63
311315// }
312316type ProtonLogicals struct {
313- Name string `json:"Name"`
314- EntryCountry string `json:"EntryCountry"`
315- ExitCountry string `json:"ExitCountry"`
316- Domain string `json:"Domain"`
317- Tier int `json:"Tier"`
318- Features int `json:"Features"`
319- Region string `json:"Region"`
320- City string `json:"City"`
321- Score float64
322- HostCountry string `json:"HostCountry"`
323- Organization string `json:"OrganizationID"`
324- VPNGatewayID string `json:"VPNGatewayID"`
325- ID string `json:"ID"`
317+ Name string `json:"Name"`
318+ EntryCountry string `json:"EntryCountry"`
319+ ExitCountry string `json:"ExitCountry"`
320+ Domain string `json:"Domain"`
321+ Tier int `json:"Tier"`
322+ Features int `json:"Features"`
323+ Region string `json:"Region"`
324+ City string `json:"City"`
325+ Score float64 `json:"Score"`
326+ HostCountry string `json:"HostCountry"`
327+ Organization string `json:"OrganizationID"`
328+ VPNGatewayID string `json:"VPNGatewayID"`
329+ ID string `json:"ID"`
330+ Load int `json:"Load"`
326331 Location ProtonServerLocation
327332 Status int `json:"Status"`
328333 Servers []ProtonServer
@@ -350,6 +355,8 @@ type ProtonServerLocation struct {
350355// "ServicesDownReason": null
351356// }
352357type ProtonServer struct {
358+ Name string `json:"Name"`
359+ Load int `json:"Load"`
353360 EntryIP string `json:"EntryIP"`
354361 ExitIP string `json:"ExitIP"`
355362 Domain string `json:"Domain"`
@@ -390,19 +397,22 @@ type ProtonWgConfig struct {
390397 UID string `json:"UID"`
391398 SessionAccessToken string `json:"SessionAccessToken"`
392399 SessionRefreshToken string `json:"SessionRefreshToken"`
393- UserID string `json:"UserID"`
394- CredsAccessToken string `json:"UserAccessToken"`
395- CredsRefreshToken string `json:"UserRefreshToken"`
396- CertSerialNumber string `json:"CertSerialNumber"`
397- CertExpTime int `json:"CertExpTime"`
398- CertRefreshTime int `json:"CertRefreshTime"`
399400
401+ UserID string `json:"UserID"`
402+ CredsAccessToken string `json:"UserAccessToken"`
403+ CredsRefreshToken string `json:"UserRefreshToken"`
404+
405+ CertSerialNumber string `json:"CertSerialNumber"`
406+ CertExpTime int `json:"CertExpTime"`
407+ CertRefreshTime int `json:"CertRefreshTime"`
408+
409+ CreateTimestamp int64 `json:"CreateTimestamp"`
400410 RegionalWgConfs []* RegionalWgConf `json:"RegionalWgConfs"`
401411}
402412
403413type RegionalWgConf struct {
404414 CC string `json:"CC"`
405- Load int `json:"Load "`
415+ Name string `json:"Name "`
406416
407417 ClientAddr4 string `json:"ClientAddr4"`
408418 ClientAddr6 string `json:"ClientAddr6"`
@@ -509,30 +519,12 @@ func newProtonGw(ctx context.Context, k ProtonKey, logicals []ProtonLogicals, h2
509519 publicKeyPem = publicKeyPem [6 :16 ]
510520 }
511521
512- m := make (map [string ][]ProtonServer , 0 )
513- skips := 0
514- tot := 0
515- for _ , x := range logicals {
516- // github.com/ProtonVPN/android-app/blob/b9c6e59de40/app/src/main/java/com/protonvpn/android/utils/ServerManager.kt#L251
517- // skip premium or restricted or offline servers
518- if x .securecore () || x .gateway () || x .offline () {
519- skips ++
520- continue
521- }
522- if c , ok := m [x .EntryCountry ]; ok {
523- m [x .EntryCountry ] = append (c , x .Servers ... )
524- } else {
525- m [x .EntryCountry ] = x .Servers
526- }
527- tot += len (x .Servers )
528- }
529- log .I ("proton: new gw for %s: sz: l(%d) => [cc(%d) => svcs(%d) / skip: %d]" ,
530- publicKeyPem , len (logicals ), len (m ), tot , skips )
522+ m := protonServersByCountry (logicals )
531523
532524 a := & protongw {
533525 http : h2 ,
534526 key : k ,
535- servers : m ,
527+ servers : m , // may be empty
536528 sched : core .NewScheduler (ctx ),
537529 sess : struct {
538530 uid string
@@ -547,6 +539,8 @@ func newProtonGw(ctx context.Context, k ProtonKey, logicals []ProtonLogicals, h2
547539 config : nil ,
548540 }
549541
542+ log .I ("proton: gw: new: %s / %d" , publicKeyPem , len (m ))
543+
550544 return a , nil
551545}
552546
@@ -599,25 +593,30 @@ func (a *protongw) newConf() error {
599593 rwgConfs := make ([]* RegionalWgConf , 0 , len (a .servers ))
600594 for cc , ss := range a .servers {
601595 wc := new (RegionalWgConf )
596+ wc .CC = cc
602597 wc .ClientAddr4 = protonClientAddr4
603598 wc .ClientPrivKey = clientPrivKey
604599 wc .ClientPubKey = clientPubKey
605600 wc .ClientDNS4 = protonDNSAddr4
606601
602+ n := 0
607603 for _ , s := range ss {
604+ if n > maxPerRegionWgConfs {
605+ break
606+ }
608607 // github.com/ProtonVPN/android-app/blob/b9c6e59de40/app/src/main/java/com/protonvpn/android/utils/ServerManager.kt#L251
609608 if s .online () && s .wg () {
609+ n ++
610+ wc .Name = s .Name
610611 wc .ServerPubKey = s .X25519PublicKey
611612 wc .ServerIPPort4 = fmt .Sprintf ("%s:%d" , s .EntryIP , protonPrimaryTcpPort )
612613 wc .ServerDomainPort = fmt .Sprintf ("%s:%d" , s .Domain , protonPrimaryTcpPort )
613614 wc .AllowedIPs = protonAllowedIPs
614615
615616 rwgConfs = append (rwgConfs , wc )
616617
617- log .VV ("proton: genconf: %s: %s@%s; peer: %s@%s" ,
618- cc , wc .ClientAddr4 , wc .ClientPubKey [:6 ], wc .ServerIPPort4 , wc .ServerPubKey [:6 ])
619-
620- break
618+ log .VV ("proton: genconf: %s n:%d, l:%d; x: %s@%s; p: %s@%s" ,
619+ s .Name , n , s .Load , wc .ClientAddr4 , wc .ClientPubKey [:6 ], wc .ServerIPPort4 , wc .ServerPubKey [:6 ])
621620 }
622621 }
623622 }
@@ -643,13 +642,14 @@ func (a *protongw) newConf() error {
643642 // wg info
644643 pc .RegionalWgConfs = rwgConfs
645644
645+ pc .CreateTimestamp = time .Now ().Unix ()
646+
646647 a .config = pc
647648
648649 return nil // success
649650}
650651
651652func (a * protongw ) registerCert () error {
652- const maxRegisterCertTries = 3
653653 tries := 0
654654
655655retryAfterRefresh:
@@ -1041,6 +1041,20 @@ func (a *protongw) reg() error {
10411041 return a .newConf ()
10421042}
10431043
1044+ func (a * protongw ) refreshServers () error {
1045+ const nofile = ""
1046+
1047+ oldEnough := time .Since (protonLogicalsUpdateTime ) > maxProtonLogicalsRefreshThreshold
1048+ missingConfig := a .config == nil || len (a .config .RegionalWgConfs ) <= 0
1049+ if oldEnough || missingConfig {
1050+ t := protonLogicalsUpdateTime .Format (time .RFC1123 )
1051+ log .I ("proton: refresh servers; old(%s)? %t / missing? %t" , oldEnough , t , missingConfig )
1052+ a .servers = protonServersByCountry (protonServersFrom (nofile , a .http ))
1053+ }
1054+
1055+ return nil
1056+ }
1057+
10441058func (a * protongw ) rereg () error {
10451059 if len (a .sess .uid ) <= 0 {
10461060 log .W ("proton: re-reg: no session; initiating reg" )
@@ -1102,8 +1116,7 @@ func (w *Client) MakeProtonWgFrom(ctx context.Context, fromConfigJson []byte, al
11021116 return nil , err
11031117 }
11041118
1105- svcs := protonServersFrom (allServersFilePath , & w .h2 )
1106-
1119+ svcs := protonServersPrebuilt () // refreshed if needed later
11071120 a , err := newProtonGw (ctx , k , svcs , & w .h2 )
11081121 if err != nil {
11091122 return nil , err
@@ -1119,6 +1132,11 @@ func (w *Client) MakeProtonWgFrom(ctx context.Context, fromConfigJson []byte, al
11191132 return nil , err
11201133 }
11211134
1135+ err = a .refreshServers ()
1136+ if err != nil {
1137+ return nil , err
1138+ }
1139+
11221140 return a .config , nil
11231141}
11241142
@@ -1138,16 +1156,52 @@ func (a *protongw) load(conf *ProtonWgConfig) error {
11381156 a .cert .ExpirationTime = conf .CertExpTime
11391157 a .cert .RefreshTime = conf .CertRefreshTime
11401158
1159+ protonLogicalsUpdateTime = time .Unix (conf .CreateTimestamp , 0 )
1160+
11411161 return nil
11421162}
11431163
1144- // go.dev/play/p/9kapzPiG72r
1145- func protonServersFrom (allServersFilePath string , c * http.Client ) []ProtonLogicals {
1146- var prebuilts , all ProtonServerResponse
1164+ func protonServersByCountry (logicals []ProtonLogicals ) map [string ][]ProtonServer {
1165+ m := make (map [string ][]ProtonServer , 0 )
1166+ skips := 0
1167+ tot := 0
1168+ for _ , x := range logicals {
1169+ // github.com/ProtonVPN/android-app/blob/b9c6e59de40/app/src/main/java/com/protonvpn/android/utils/ServerManager.kt#L251
1170+ // skip premium or restricted or offline servers
1171+ if x .securecore () || x .gateway () || x .offline () {
1172+ skips ++
1173+ continue
1174+ }
1175+ for _ , s := range x .Servers {
1176+ s .Load = x .Load
1177+ s .Name = x .Name
1178+ }
1179+ if c , ok := m [x .EntryCountry ]; ok {
1180+ m [x .EntryCountry ] = append (c , x .Servers ... )
1181+ } else {
1182+ m [x .EntryCountry ] = x .Servers
1183+ }
1184+ tot += len (x .Servers )
1185+ }
1186+ log .I ("proton: servers: sz: l(%d) => [cc(%d) => svcs(%d) / skip: %d]" ,
1187+ len (logicals ), len (m ), tot , skips )
1188+ return m
1189+ }
1190+
1191+ func protonServersPrebuilt () []ProtonLogicals {
1192+ var prebuilts []ProtonLogicals
11471193 err := json .Unmarshal (prebuiltProtonServersJson , & prebuilts )
11481194 if err != nil {
11491195 log .E ("proton: servers: %d unmarshal: %v" , len (prebuiltProtonServersJson ), err )
11501196 }
1197+ return prebuilts
1198+ }
1199+
1200+ // go.dev/play/p/9kapzPiG72r
1201+ func protonServersFrom (allServersFilePath string , c * http.Client ) []ProtonLogicals {
1202+ var all ProtonServerResponse
1203+
1204+ prebuilts := protonServersPrebuilt ()
11511205
11521206 if len (allServersFilePath ) > 0 {
11531207 fp := filepath .Clean (allServersFilePath )
@@ -1224,16 +1278,15 @@ func protonServersFrom(allServersFilePath string, c *http.Client) []ProtonLogica
12241278 _ , err = f .Write (b )
12251279 if err != nil {
12261280 log .E ("proton: servers: write %s, err: %v" , fp , err )
1227- } else {
1228- protonLogicalsUpdateTime = time .Now ()
1229- }
1281+ } // else: written
12301282 }
1231- }
1283+ } // else: no-store
1284+ protonLogicalsUpdateTime = time .Now ()
12321285 }
12331286 }
12341287 }
12351288 }
12361289 }
1237- }
1238- return append (all .R , prebuilts .R . .. )
1290+ } // else: contains remote servers
1291+ return append (all .R , prebuilts ... )
12391292}
0 commit comments