Skip to content

Commit e77b4d9

Browse files
Adam Fiskclaude
authored andcommitted
portforward: ManualForwarder.ExternalIP returns "" again
The original manualPortForwarder (introduced in b81b6b0) returned "", nil from ExternalIP, leaning on lantern-cloud peer_handler's "external_ip empty → use observed" fall-through to fill the IP from the /v1/peer/register call's RemoteAddr. The refactor into portforward.ManualForwarder regressed this path by actively probing publicip.Detect, which fails on the only deployment where it matters: machines with Lantern's own VPN tunnel up. When the tunnel is up, outbound traffic routes through it. The publicip discovery endpoints (api.iantem.io etc) either time out or return the tunnel exit's IP rather than the user's WAN IP — neither is what we want. Worse, the failure surfaces as a hard error so peer.Client.Start aborts before /v1/peer/register is even called, breaking peer-share for every user with a manual port + an active tunnel. Empty is also more correct: peer_handler uses the IP that will actually receive inbound traffic on the manually-forwarded port (the register-call's RemoteAddr), which is by definition the right answer. Add a regression test pinning the empty-string contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b36b61a commit e77b4d9

2 files changed

Lines changed: 27 additions & 22 deletions

File tree

portforward/manual.go

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ import (
44
"context"
55
"fmt"
66
"strconv"
7-
"time"
8-
9-
"github.com/getlantern/publicip"
107
)
118

129
// ManualForwarder is a no-op port forwarder for users who can't (or won't)
@@ -78,25 +75,16 @@ func (f *ManualForwarder) UnmapPort(_ context.Context) error {
7875
// the user removes them, with no UPnP lease to renew.
7976
func (f *ManualForwarder) StartRenewal(_ context.Context) {}
8077

81-
// ExternalIP probes a public-IP discovery service (the radiance default
82-
// methods, hitting api.iantem.io etc.) since there's no UPnP gateway to
83-
// ask. Cached for the duration of the forwarder's life — the user's
84-
// public IP shouldn't change mid-session, and a stale cache yields a
85-
// clean re-register on a heartbeat 404 if it does.
86-
func (f *ManualForwarder) ExternalIP(ctx context.Context) (string, error) {
87-
probeCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
88-
defer cancel()
89-
result, err := publicip.Detect(probeCtx, &publicip.Config{
90-
Timeout: 5 * time.Second,
91-
MinConsensus: 1,
92-
})
93-
if err != nil {
94-
return "", fmt.Errorf("detect public ip: %w", err)
95-
}
96-
if result.IP == nil {
97-
return "", fmt.Errorf("public-ip detection returned no result")
98-
}
99-
return result.IP.String(), nil
78+
// ExternalIP returns "" so the server fills in the observed IP from the
79+
// register call's RemoteAddr (peer_handler's "external_ip empty → use
80+
// observed" path). Probing publicip.Detect from here regresses on
81+
// machines where Lantern's own tunnel is up — outbound goes through the
82+
// tunnel and the discovery endpoints either time out or report the
83+
// tunnel exit's IP rather than the user's WAN IP. Letting the server
84+
// use the observed RemoteAddr is also more correct: it's the IP that
85+
// will actually receive inbound traffic on the manually-forwarded port.
86+
func (f *ManualForwarder) ExternalIP(_ context.Context) (string, error) {
87+
return "", nil
10088
}
10189

10290
// ParseManualPort is a small helper for callers that want to read a

portforward/manual_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,23 @@ func TestManualForwarder_StartRenewal_NoOp(t *testing.T) {
7272
f.StartRenewal(context.Background()) // must not panic
7373
}
7474

75+
// TestManualForwarder_ExternalIP_ReturnsEmpty pins the contract that
76+
// ExternalIP returns "" with no error so the lantern-cloud peer_handler
77+
// fills the IP from the register call's RemoteAddr. A previous revision
78+
// regressed this to call publicip.Detect, which fails on machines where
79+
// Lantern's own VPN tunnel is up — outbound traffic gets routed through
80+
// the tunnel and the discovery endpoints return the tunnel exit's IP
81+
// (or time out entirely), breaking peer registration silently.
82+
func TestManualForwarder_ExternalIP_ReturnsEmpty(t *testing.T) {
83+
t.Parallel()
84+
f, err := NewManualForwarder(5698)
85+
require.NoError(t, err)
86+
ip, err := f.ExternalIP(context.Background())
87+
require.NoError(t, err)
88+
assert.Empty(t, ip,
89+
"ManualForwarder.ExternalIP must return \"\" so server uses observed RemoteAddr")
90+
}
91+
7592
func TestParseManualPort(t *testing.T) {
7693
t.Parallel()
7794

0 commit comments

Comments
 (0)