Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/peer_share.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type peerController interface {
Stop(ctx context.Context) error
IsActive() bool
CurrentStatus() peer.Status
RelaySnapshot() peer.Snapshot
}

// peerToggleTimeout caps blocking time on a slow router; UPnP M-SEARCH +
Expand Down Expand Up @@ -197,3 +198,13 @@ func (r *LocalBackend) PeerStatus() peer.Status {
}
return r.peerClient.CurrentStatus()
}

// PeerRelaySnapshot returns the bytes the current or most recent direct-sharing
// session has relayed, for the IPC /peer/snapshot endpoint. A nil peerClient
// reports an empty snapshot, as PeerStatus does.
func (r *LocalBackend) PeerRelaySnapshot() peer.Snapshot {
if r.peerClient == nil {
return peer.Snapshot{Peers: []string{}, PeerBytes: []peer.PeerBytes{}}
}
return r.peerClient.RelaySnapshot()
}
10 changes: 6 additions & 4 deletions backend/radiance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,9 @@ func (f *fakePeerController) Stop(_ context.Context) error {
return nil
}

func (f *fakePeerController) IsActive() bool { return f.active.Load() }
func (f *fakePeerController) CurrentStatus() peer.Status { return peer.Status{Active: f.active.Load()} }
func (f *fakePeerController) IsActive() bool { return f.active.Load() }
func (f *fakePeerController) CurrentStatus() peer.Status { return peer.Status{Active: f.active.Load()} }
func (f *fakePeerController) RelaySnapshot() peer.Snapshot { return peer.Snapshot{} }

// newPeerTestBackend wires a minimal LocalBackend with only the fields
// applyPeerShare touches. settings is initialized to a fresh tempdir per test
Expand Down Expand Up @@ -699,8 +700,9 @@ func (f *slowStartFake) Stop(_ context.Context) error {
f.active.Store(false)
return nil
}
func (f *slowStartFake) IsActive() bool { return f.active.Load() }
func (f *slowStartFake) CurrentStatus() peer.Status { return peer.Status{Active: f.active.Load()} }
func (f *slowStartFake) IsActive() bool { return f.active.Load() }
func (f *slowStartFake) CurrentStatus() peer.Status { return peer.Status{Active: f.active.Load()} }
func (f *slowStartFake) RelaySnapshot() peer.Snapshot { return peer.Snapshot{} }

// newCloseableTestBackend mirrors newPeerTestBackend but provides the fields
// Close needs (closeOnce, cancel) so we can exercise the shutdown path
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.26.9

// replace github.com/sagernet/sing => github.com/getlantern/sing v0.7.18-lantern

replace github.com/sagernet/sing-box => github.com/getlantern/sing-box-minimal v1.13.20-0.20261017021932-5a256d757969
replace github.com/sagernet/sing-box => github.com/getlantern/sing-box-minimal v1.13.20-0.20261019115333-624aa48e0252

// replace github.com/sagernet/wireguard-go => github.com/getlantern/wireguard-go v0.0.1-beta.7.0.20251208214020-d78e69f1eff4

Expand Down Expand Up @@ -40,7 +40,7 @@ require (
github.com/getlantern/common v1.2.1-0.20260910154003-08e030318f24
github.com/getlantern/domainfront v0.0.0-20260915172349-4984689a8667
github.com/getlantern/kindling v0.0.0-20261014210827-0f4a89dbe1ee
github.com/getlantern/lantern-box v0.0.154
github.com/getlantern/lantern-box v0.0.166
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b
github.com/getlantern/semconv v0.0.0-20261011133221-f5e57ec9c1c5
Expand Down Expand Up @@ -126,7 +126,7 @@ require (
github.com/getlantern/algeneva v0.0.0-20260731172949-952f13725b7b // indirect
github.com/getlantern/keepcurrent v0.0.0-20261009194109-22f33eaeac6e // indirect
github.com/getlantern/lantern-water v0.0.0-20260520145825-958775d51395 // indirect
github.com/getlantern/samizdat v0.0.3-0.20260819153658-ebc74116a064 // indirect
github.com/getlantern/samizdat v0.0.3-0.20261019142234-18bd41c612f1 // indirect
github.com/getlantern/twiddle v0.0.0-20260914204124-f698a0d67d2f // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@ github.com/getlantern/keepcurrent v0.0.0-20261009194109-22f33eaeac6e h1:iJfNXnhg
github.com/getlantern/keepcurrent v0.0.0-20261009194109-22f33eaeac6e/go.mod h1:ag5g9aWUw2FJcX5RVRpJ9EBQBy5yJuy2WXDouIn/m4w=
github.com/getlantern/kindling v0.0.0-20261014210827-0f4a89dbe1ee h1:FXod6NjAD/KDl6Aj5mP1MVCLf2AAJv8qU4kmUrYcMZk=
github.com/getlantern/kindling v0.0.0-20261014210827-0f4a89dbe1ee/go.mod h1:98jp1v8UyXJRGn8slpvzNz2tKnyzvSqtlVYkQ+kYapQ=
github.com/getlantern/lantern-box v0.0.154 h1:EPQjjBqNC1gbDb2SlpbWg1wo7pNM/TccZ/UV+yhVQjI=
github.com/getlantern/lantern-box v0.0.154/go.mod h1:gy3dGHMTQXaFi5DYIJJrMNT3WS2E7GOxuZl2jebcbuw=
github.com/getlantern/lantern-box v0.0.166 h1:Z2qy+5Yvd6s0pQBGlqP/bwRllkynK5Clf94HcHzy13Q=
github.com/getlantern/lantern-box v0.0.166/go.mod h1:RK7qWraZxaDGC6iJYJV6KEUyQvOy2njF5iEENZ1bl/k=
github.com/getlantern/lantern-water v0.0.0-20260520145825-958775d51395 h1:grfGavAUp2E9w9ZoJuM3FyWyQ0sCJ64V4ZMKtZKRqTc=
github.com/getlantern/lantern-water v0.0.0-20260520145825-958775d51395/go.mod h1:3JpJgwi4KEI6rS9loOAvcBp+F2jP65d0tTg2GQcTPBU=
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 h1:3BwvWj0JZzFEvNNiMhCu4bf60nqcIuQpTYb00Ezm1ag=
Expand All @@ -261,12 +261,12 @@ github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YL
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q=
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b h1:gMYJzEhLrmIqQ+JnjiYNm+UyUDalK3WUmVyecFwmV5g=
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b/go.mod h1:NpfXdK4ldEKkjQ4P1R+DBF4ua5VFOlxmgHROTnYrApg=
github.com/getlantern/samizdat v0.0.3-0.20260819153658-ebc74116a064 h1:NhCyQAWGEWjok/jg4gRD6fzCZvyOQy/4dVSFYhzU5xk=
github.com/getlantern/samizdat v0.0.3-0.20260819153658-ebc74116a064/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/samizdat v0.0.3-0.20261019142234-18bd41c612f1 h1:Q1AHHHTd3eiHXn0HR0mltq6/4cGnv9cQFKYmoRuXWQo=
github.com/getlantern/samizdat v0.0.3-0.20261019142234-18bd41c612f1/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/semconv v0.0.0-20261011133221-f5e57ec9c1c5 h1:oF5kQTI7RwMvjMnr4WhmQ1beAjSMl1zoQ4I7Cx5nm6I=
github.com/getlantern/semconv v0.0.0-20261011133221-f5e57ec9c1c5/go.mod h1:GkPT5P9JoOTIRXRmFWxYgu1hhXgTFFTNc2hoG7WQc3g=
github.com/getlantern/sing-box-minimal v1.13.20-0.20261017021932-5a256d757969 h1:xDlY1aEy5FILPxlncY5ciHQQY5FdyiNNZ2RC86ACfsw=
github.com/getlantern/sing-box-minimal v1.13.20-0.20261017021932-5a256d757969/go.mod h1:4E8WvF7Vi60N1NtPw1CeG994J/yiJZZkty5dKKwG+V0=
github.com/getlantern/sing-box-minimal v1.13.20-0.20261019115333-624aa48e0252 h1:p6tbxiX1SHqo1FwO9l1uYerSSaMLt/R78QtLIScpP54=
github.com/getlantern/sing-box-minimal v1.13.20-0.20261019115333-624aa48e0252/go.mod h1:4E8WvF7Vi60N1NtPw1CeG994J/yiJZZkty5dKKwG+V0=
github.com/getlantern/soar v0.2.3 h1:BAahWy3b4J6wtrAJD7WgoO2O+x2Q3T6KN2r0E3ogNgs=
github.com/getlantern/soar v0.2.3/go.mod h1:6T2+Kbx0GVqYIHqfEaUqlYI5ZRRVZYYhuoMWcxHC+xA=
github.com/getlantern/twiddle v0.0.0-20260914204124-f698a0d67d2f h1:8WL/o8hKl27jpHunB4nO5dJZKSTfyMwvS7J72ix6vqQ=
Expand Down
8 changes: 8 additions & 0 deletions ipc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ func (c *Client) PeerStatus(ctx context.Context) (peer.Status, error) {
return status, err
}

// PeerRelaySnapshot reads the bytes the current or most recent direct-sharing
// session has relayed, in the shape of UnboundedSnapshot.
func (c *Client) PeerRelaySnapshot(ctx context.Context) (peer.Snapshot, error) {
var s peer.Snapshot
err := c.doJSON(ctx, http.MethodGet, peerSnapshotEndpoint, nil, &s)
return s, err
}

/////////////
// VPN //
/////////////
Expand Down
4 changes: 4 additions & 0 deletions ipc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const (
peerStatusEndpoint = "/peer/status"
peerStatusEventsEndpoint = "/peer/status/events"
peerConnectionEventsEndpoint = "/peer/connection/events"
peerSnapshotEndpoint = "/peer/snapshot"

// Unbounded ("Basic mode") endpoints. Connection events have the same
// JSON shape as /peer/connection/events but are emitted from a
Expand Down Expand Up @@ -276,6 +277,9 @@ func newLocalAPI(b *backend.LocalBackend, withAuth bool) *localapi {
// SSE skips the tracer middleware since it buffers the entire response body.
mux.HandleFunc("GET "+peerStatusEventsEndpoint, s.peerStatusEventsHandler)
mux.HandleFunc("GET "+peerConnectionEventsEndpoint, s.peerConnectionEventsHandler)
mux.HandleFunc("GET "+peerSnapshotEndpoint, traced(func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.backend(r.Context()).PeerRelaySnapshot())
}))
mux.HandleFunc("GET "+unboundedConnectionEventsEndpoint, s.unboundedConnectionEventsHandler)

mux.HandleFunc("GET "+unboundedSnapshotEndpoint, traced(func(w http.ResponseWriter, r *http.Request) {
Expand Down
40 changes: 34 additions & 6 deletions peer/peer.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ type Client struct {
// streams over one TCP conn, so several can share a source. Guarded by
// connsMu.
verifierConns map[string]int
// verifierCounters holds the byte counters of open verifier streams. When
// events carry a counter it identifies the stream, so a close is hidden
// exactly when its own accept was; verifierConns is the fallback for
// events without one. Guarded by connsMu.
verifierCounters map[*peerconn.Counter]struct{}

// relay totals the bytes each session relays, for RelaySnapshot.
relay relayStats

// rejections tallies destinations refused by reject rules, across
// sessions, so allowlist gaps are visible. Flushed on each heartbeat and
Expand Down Expand Up @@ -330,6 +338,7 @@ func (c *Client) Start(ctx context.Context) (retErr error) {
// silence the disconnect cascade during box.Close; if we don't reset
// here, a Stop→Start cycle would leave the wrapper permanently muted.
c.listenerDraining.Store(false)
relayGen := c.relay.begin()

var (
success bool
Expand Down Expand Up @@ -362,6 +371,7 @@ func (c *Client) Start(ctx context.Context) (retErr error) {
c.listenerDraining.Store(true)
peerconn.SetListener(nil)
c.resetConnTracking()
c.relay.end()
if box != nil {
_ = c.closeBox(box)
}
Expand Down Expand Up @@ -520,14 +530,15 @@ func (c *Client) Start(ctx context.Context) (retErr error) {
if evt.Rejected {
c.rejections.record(evt.Destination)
}
if c.isVerifierConn(state, source, evt.Destination) {
if c.isVerifierConn(state, source, evt.Destination, evt.Bytes) {
slog.Debug("peer listener: hiding verifier connection",
"state", state, "source", source)
return
}
slog.Debug("peer listener: forwarding connection event",
"state", state, "source", source)
c.trackConn(state, source)
c.relay.recordFor(relayGen, state, source, evt.Bytes)
Comment thread
myleshorton marked this conversation as resolved.
events.Emit(ConnectionEvent{State: state, Source: source, Timestamp: time.Now().UnixMilli()})
})
slog.Info("peer listener: registered with peerconn", "route_id", regResp.RouteID)
Expand Down Expand Up @@ -700,6 +711,7 @@ func (c *Client) stopHeld(ctx context.Context, target stopTarget) (bool, error)
c.listenerDraining.Store(true)
peerconn.SetListener(nil)
c.resetConnTracking()
c.relay.end()

cancel()
<-done
Expand Down Expand Up @@ -833,6 +845,7 @@ func (c *Client) resetConnTracking() {
defer c.connsMu.Unlock()
c.connsByIP = nil
c.verifierConns = nil
c.verifierCounters = nil
}

// verifierCallbackHosts are the hosts lantern-cloud's verifier fetches through
Expand All @@ -844,11 +857,12 @@ var verifierCallbackHosts = []string{"api.iantem.io", "api.staging.iantem.io"}
// isVerifierConn reports whether a connection event belongs to lantern-cloud's
// verifier, which re-verifies serving peers too, so that it is neither shown
// as a person helped nor reported to the server as a client. An accept is
// matched by destination. A close carries no destination, so each hidden
// accept hides one later close from the same source. When ordinary and
// verifier streams share a source, which close is hidden may not be the
// verifier's own, but the per-IP totals still balance.
func (c *Client) isVerifierConn(state int, source, destination string) bool {
// matched by destination. A close carries no destination, so it is matched by
// its byte counter, which identifies the stream. Without a counter, each
// hidden accept hides one later close from the same source; when ordinary and
// verifier streams share a source that may not be the verifier's own close,
// but the per-IP totals still balance.
func (c *Client) isVerifierConn(state int, source, destination string, bytes *peerconn.Counter) bool {
c.connsMu.Lock()
defer c.connsMu.Unlock()
// Same reason as trackConn: a late accept must not repopulate the set
Expand All @@ -857,6 +871,13 @@ func (c *Client) isVerifierConn(state int, source, destination string) bool {
return false
}
if state < 0 {
if bytes != nil {
if _, ok := c.verifierCounters[bytes]; !ok {
return false
}
delete(c.verifierCounters, bytes)
return true
}
n := c.verifierConns[source]
if n == 0 {
return false
Expand All @@ -877,6 +898,13 @@ func (c *Client) isVerifierConn(state int, source, destination string) bool {
}) {
return false
}
if bytes != nil {
if c.verifierCounters == nil {
c.verifierCounters = make(map[*peerconn.Counter]struct{})
}
c.verifierCounters[bytes] = struct{}{}
return true
}
if c.verifierConns == nil {
c.verifierConns = make(map[string]int)
}
Expand Down
14 changes: 7 additions & 7 deletions peer/peer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1236,17 +1236,17 @@ func TestTrackConn_IgnoresUnmatchedClose(t *testing.T) {
func TestIsVerifierConn_PairsCloseWithAccept(t *testing.T) {
c := &Client{}

assert.True(t, c.isVerifierConn(1, "35.233.177.182:33792", "api.iantem.io:443"))
assert.True(t, c.isVerifierConn(1, "35.233.177.183:1000", "API.staging.iantem.io:443"))
assert.False(t, c.isVerifierConn(1, "35.233.177.182:33793", "google.com:443"))
assert.True(t, c.isVerifierConn(1, "35.233.177.182:33792", "api.iantem.io:443", nil))
assert.True(t, c.isVerifierConn(1, "35.233.177.183:1000", "API.staging.iantem.io:443", nil))
assert.False(t, c.isVerifierConn(1, "35.233.177.182:33793", "google.com:443", nil))

assert.True(t, c.isVerifierConn(-1, "35.233.177.182:33792", ""))
assert.False(t, c.isVerifierConn(-1, "35.233.177.182:33792", ""),
assert.True(t, c.isVerifierConn(-1, "35.233.177.182:33792", "", nil))
assert.False(t, c.isVerifierConn(-1, "35.233.177.182:33792", "", nil),
"a second close for the same source is not the verifier's")
assert.False(t, c.isVerifierConn(-1, "35.233.177.182:33793", ""))
assert.False(t, c.isVerifierConn(-1, "35.233.177.182:33793", "", nil))

c.resetConnTracking()
assert.False(t, c.isVerifierConn(-1, "35.233.177.183:1000", ""),
assert.False(t, c.isVerifierConn(-1, "35.233.177.183:1000", "", nil),
"a reset forgets verifier connections from the old session")
}

Expand Down
Loading
Loading