Skip to content

Commit 0bf9387

Browse files
committed
feat(model): provider_egress_location storage
1 parent cc64358 commit 0bf9387

3 files changed

Lines changed: 309 additions & 0 deletions

File tree

db_migrations.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4385,4 +4385,30 @@ var migrations = []any{
43854385
CREATE INDEX IF NOT EXISTS network_client_top_level_contract_time
43864386
ON network_client (contract_time) WHERE (active = true AND source_client_id IS NULL AND contract_time IS NOT NULL)
43874387
`),
4388+
4389+
// provider egress locations: locations learned by probing a provider's own
4390+
// egress (see docs/superpowers/specs/2026-07-24-provider-egress-geolocation-design.md).
4391+
// Keyed by client_id, one row per provider, upserted by the operator's
4392+
// prober. location_id is the canonical country (or city, when the probe was
4393+
// city-confident) location row. observed_at is when the probe ran, and is
4394+
// what freshness is judged against.
4395+
newSqlMigration(`
4396+
CREATE TABLE IF NOT EXISTS provider_egress_location (
4397+
client_id uuid NOT NULL PRIMARY KEY,
4398+
location_id uuid NOT NULL,
4399+
country_code varchar(2) NOT NULL,
4400+
asn int NOT NULL DEFAULT 0,
4401+
org varchar(256) NOT NULL DEFAULT '',
4402+
hosting bool NOT NULL DEFAULT false,
4403+
proxy bool NOT NULL DEFAULT false,
4404+
mobile bool NOT NULL DEFAULT false,
4405+
city_confident bool NOT NULL DEFAULT false,
4406+
observed_at timestamp NOT NULL,
4407+
update_time timestamp NOT NULL
4408+
)
4409+
`),
4410+
newSqlMigration(`
4411+
CREATE INDEX IF NOT EXISTS provider_egress_location_observed_at
4412+
ON provider_egress_location (observed_at)
4413+
`),
43884414
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package model
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/urnetwork/server"
8+
)
9+
10+
// ProviderEgressLocationMaxAge bounds how long a probed egress location is
11+
// trusted. Past this, the location is ignored and the caller falls back to the
12+
// mmdb lookup on the observed control ip.
13+
const ProviderEgressLocationMaxAge = 7 * 24 * time.Hour
14+
15+
// ProviderEgressLocation is a provider location learned by probing the
16+
// provider's own egress, rather than by looking up its control-connection ip.
17+
type ProviderEgressLocation struct {
18+
ClientId server.Id
19+
LocationId server.Id
20+
CountryCode string
21+
ASN int
22+
Org string
23+
Hosting bool
24+
Proxy bool
25+
Mobile bool
26+
CityConfident bool
27+
ObservedAt time.Time
28+
UpdateTime time.Time
29+
}
30+
31+
// SetProviderEgressLocation upserts the probed location for a provider.
32+
func SetProviderEgressLocation(ctx context.Context, e *ProviderEgressLocation) {
33+
server.Tx(ctx, func(tx server.PgTx) {
34+
server.RaisePgResult(tx.Exec(
35+
ctx,
36+
`
37+
INSERT INTO provider_egress_location (
38+
client_id,
39+
location_id,
40+
country_code,
41+
asn,
42+
org,
43+
hosting,
44+
proxy,
45+
mobile,
46+
city_confident,
47+
observed_at,
48+
update_time
49+
)
50+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
51+
ON CONFLICT (client_id) DO UPDATE
52+
SET
53+
location_id = $2,
54+
country_code = $3,
55+
asn = $4,
56+
org = $5,
57+
hosting = $6,
58+
proxy = $7,
59+
mobile = $8,
60+
city_confident = $9,
61+
observed_at = $10,
62+
update_time = $11
63+
`,
64+
e.ClientId,
65+
e.LocationId,
66+
e.CountryCode,
67+
e.ASN,
68+
e.Org,
69+
e.Hosting,
70+
e.Proxy,
71+
e.Mobile,
72+
e.CityConfident,
73+
e.ObservedAt.UTC(),
74+
server.NowUtc(),
75+
))
76+
})
77+
}
78+
79+
// GetProviderEgressLocation returns the stored location for a provider, or nil.
80+
func GetProviderEgressLocation(ctx context.Context, clientId server.Id) *ProviderEgressLocation {
81+
var e *ProviderEgressLocation
82+
server.Db(ctx, func(conn server.PgConn) {
83+
result, err := conn.Query(
84+
ctx,
85+
`
86+
SELECT
87+
client_id,
88+
location_id,
89+
country_code,
90+
asn,
91+
org,
92+
hosting,
93+
proxy,
94+
mobile,
95+
city_confident,
96+
observed_at,
97+
update_time
98+
FROM provider_egress_location
99+
WHERE client_id = $1
100+
`,
101+
clientId,
102+
)
103+
server.WithPgResult(result, err, func() {
104+
if result.Next() {
105+
e = &ProviderEgressLocation{}
106+
server.Raise(result.Scan(
107+
&e.ClientId,
108+
&e.LocationId,
109+
&e.CountryCode,
110+
&e.ASN,
111+
&e.Org,
112+
&e.Hosting,
113+
&e.Proxy,
114+
&e.Mobile,
115+
&e.CityConfident,
116+
&e.ObservedAt,
117+
&e.UpdateTime,
118+
))
119+
}
120+
})
121+
})
122+
return e
123+
}
124+
125+
// GetFreshProviderEgressLocation is GetProviderEgressLocation, filtered to
126+
// entries probed within maxAge. The cutoff is computed in Go and bound as a
127+
// parameter: observed_at is a naive timestamp holding utc, and comparing it
128+
// against sql now() would cast through the session timezone.
129+
func GetFreshProviderEgressLocation(
130+
ctx context.Context,
131+
clientId server.Id,
132+
maxAge time.Duration,
133+
) *ProviderEgressLocation {
134+
e := GetProviderEgressLocation(ctx, clientId)
135+
if e == nil {
136+
return nil
137+
}
138+
if e.ObservedAt.Before(server.NowUtc().Add(-maxAge)) {
139+
return nil
140+
}
141+
return e
142+
}
143+
144+
// RemoveExpiredProviderEgressLocations drops entries probed before
145+
// minObservedAt.
146+
func RemoveExpiredProviderEgressLocations(ctx context.Context, minObservedAt time.Time) {
147+
server.MaintenanceTx(ctx, func(tx server.PgTx) {
148+
server.RaisePgResult(tx.Exec(
149+
ctx,
150+
`DELETE FROM provider_egress_location WHERE observed_at < $1`,
151+
minObservedAt.UTC(),
152+
))
153+
})
154+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package model
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/go-playground/assert/v2"
9+
10+
"github.com/urnetwork/server"
11+
)
12+
13+
func TestProviderEgressLocationUpsertAndGet(t *testing.T) {
14+
server.DefaultTestEnv().Run(t, func(t testing.TB) {
15+
ctx := context.Background()
16+
17+
country := &Location{
18+
LocationType: LocationTypeCountry,
19+
Country: "United States",
20+
CountryCode: "us",
21+
}
22+
CreateLocation(ctx, country)
23+
24+
clientId := server.NewId()
25+
now := server.NowUtc()
26+
SetProviderEgressLocation(ctx, &ProviderEgressLocation{
27+
ClientId: clientId,
28+
LocationId: country.LocationId,
29+
CountryCode: "us",
30+
ASN: 401486,
31+
Org: "RAVNIX LLC",
32+
Hosting: true,
33+
ObservedAt: now,
34+
})
35+
36+
got := GetProviderEgressLocation(ctx, clientId)
37+
if got == nil {
38+
t.Fatal("expected a stored egress location")
39+
}
40+
assert.Equal(t, got.LocationId, country.LocationId)
41+
assert.Equal(t, got.CountryCode, "us")
42+
assert.Equal(t, got.ASN, 401486)
43+
assert.Equal(t, got.Hosting, true)
44+
assert.Equal(t, got.Proxy, false)
45+
46+
// upsert replaces
47+
SetProviderEgressLocation(ctx, &ProviderEgressLocation{
48+
ClientId: clientId,
49+
LocationId: country.LocationId,
50+
CountryCode: "us",
51+
ASN: 999,
52+
Hosting: false,
53+
Proxy: true,
54+
ObservedAt: now,
55+
})
56+
got = GetProviderEgressLocation(ctx, clientId)
57+
assert.Equal(t, got.ASN, 999)
58+
assert.Equal(t, got.Hosting, false)
59+
assert.Equal(t, got.Proxy, true)
60+
})
61+
}
62+
63+
func TestProviderEgressLocationFreshness(t *testing.T) {
64+
server.DefaultTestEnv().Run(t, func(t testing.TB) {
65+
ctx := context.Background()
66+
67+
country := &Location{
68+
LocationType: LocationTypeCountry,
69+
Country: "United States",
70+
CountryCode: "us",
71+
}
72+
CreateLocation(ctx, country)
73+
74+
fresh := server.NewId()
75+
SetProviderEgressLocation(ctx, &ProviderEgressLocation{
76+
ClientId: fresh, LocationId: country.LocationId, CountryCode: "us",
77+
ObservedAt: server.NowUtc(),
78+
})
79+
stale := server.NewId()
80+
SetProviderEgressLocation(ctx, &ProviderEgressLocation{
81+
ClientId: stale, LocationId: country.LocationId, CountryCode: "us",
82+
ObservedAt: server.NowUtc().Add(-8 * 24 * time.Hour),
83+
})
84+
85+
if GetFreshProviderEgressLocation(ctx, fresh, ProviderEgressLocationMaxAge) == nil {
86+
t.Fatal("fresh entry must be returned")
87+
}
88+
if GetFreshProviderEgressLocation(ctx, stale, ProviderEgressLocationMaxAge) != nil {
89+
t.Fatal("stale entry must not be returned")
90+
}
91+
// absent
92+
if GetFreshProviderEgressLocation(ctx, server.NewId(), ProviderEgressLocationMaxAge) != nil {
93+
t.Fatal("absent entry must return nil")
94+
}
95+
})
96+
}
97+
98+
func TestRemoveExpiredProviderEgressLocations(t *testing.T) {
99+
server.DefaultTestEnv().Run(t, func(t testing.TB) {
100+
ctx := context.Background()
101+
102+
country := &Location{
103+
LocationType: LocationTypeCountry,
104+
Country: "United States",
105+
CountryCode: "us",
106+
}
107+
CreateLocation(ctx, country)
108+
109+
keep := server.NewId()
110+
drop := server.NewId()
111+
SetProviderEgressLocation(ctx, &ProviderEgressLocation{
112+
ClientId: keep, LocationId: country.LocationId, CountryCode: "us",
113+
ObservedAt: server.NowUtc(),
114+
})
115+
SetProviderEgressLocation(ctx, &ProviderEgressLocation{
116+
ClientId: drop, LocationId: country.LocationId, CountryCode: "us",
117+
ObservedAt: server.NowUtc().Add(-30 * 24 * time.Hour),
118+
})
119+
120+
RemoveExpiredProviderEgressLocations(ctx, server.NowUtc().Add(-14*24*time.Hour))
121+
122+
if GetProviderEgressLocation(ctx, keep) == nil {
123+
t.Fatal("recent entry must survive the sweep")
124+
}
125+
if GetProviderEgressLocation(ctx, drop) != nil {
126+
t.Fatal("old entry must be swept")
127+
}
128+
})
129+
}

0 commit comments

Comments
 (0)