Skip to content

Commit 62f6f52

Browse files
committed
ipn/proton: store multiple wg configs per cc
1 parent 610de80 commit 62f6f52

File tree

3 files changed

+115
-61
lines changed

3 files changed

+115
-61
lines changed

intra/backend/ipn_proxies.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ type Rpn interface {
6363
// RegisterAmnezia registers a new Amnezia installation.
6464
RegisterAmnezia(publicKeyBase64 string) (json []byte, err error)
6565
// RegisterProton registers a new Proton installation.
66-
RegisterProton(existingStateJson []byte, serversFile string) (json []byte, err error)
66+
RegisterProton(existingStateJson []byte) (json []byte, err error)
6767
// TestWarp connects to some Warp IPs and returns reachable ones.
6868
TestWarp() (ips string, errs error)
6969
// TestAmnezia connects to the Amnezia gateway and returns its IP if reachable.

intra/ipn/proxies.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -846,14 +846,15 @@ func (px *proxifier) RegisterAmnezia(pub string) ([]byte, error) {
846846
}
847847

848848
// RegisterProton implements x.Rpn.
849-
func (px *proxifier) RegisterProton(existingStateJson []byte, serversFile string) (stateJson []byte, err error) {
849+
func (px *proxifier) RegisterProton(existingStateJson []byte) (stateJson []byte, err error) {
850+
const nostore = ""
850851
var id *warp.ProtonWgConfig // may be nil
851852

852853
redo := len(existingStateJson) > 0
853854
if redo {
854-
id, err = px.extc.MakeProtonWgFrom(px.ctx, existingStateJson, serversFile)
855+
id, err = px.extc.MakeProtonWgFrom(px.ctx, existingStateJson, nostore)
855856
} else {
856-
id, err = px.extc.MakeProtonWg(px.ctx, serversFile)
857+
id, err = px.extc.MakeProtonWg(px.ctx, nostore)
857858
}
858859
px.lastProtonErr = err // may be nil
859860
if err != nil {

intra/ipn/warp/proton.go

Lines changed: 110 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ var (
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

7175
var protonLogicalsUpdateTime = time.Time{}
7276

@@ -310,19 +314,20 @@ type ProtonServerResponse struct {
310314
// "Load": 63
311315
// }
312316
type 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
// }
352357
type 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

403413
type 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

651652
func (a *protongw) registerCert() error {
652-
const maxRegisterCertTries = 3
653653
tries := 0
654654

655655
retryAfterRefresh:
@@ -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+
10441058
func (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

Comments
 (0)