Skip to content

Commit eb55d0f

Browse files
myleshortonclaude
andcommitted
peer/portforward: address Copilot review on #503 (round 2)
portforward/manual.go: - ManualForwarder.MapPort now sets Protocol="TCP" to match the UPnP-based Forwarder, which hard-codes the same value. Samizdat-in inbound traffic is TCP-only on both code paths; widening to UDP later means widening both forwarders together. - Defensive guard: MapPort returns an error when constructed with port==0. pickManualForwarder already range-checks 1..65535 before calling NewManualForwarder, but the check belongs on the type itself — a caller that bypasses the validator (programmatic use, tests, future code paths) gets a clear error instead of silently registering port 0 with lantern-cloud. portforward/manual_test.go: - Asserts Protocol="TCP" in TestManualForwarder. - TestManualForwarder_RejectsZeroPort verifies the new guard. peer/peer.go: - slog warning in the env-var path used 'error' as the attribute key while every other log line in this file uses 'err'. Renamed for log-aggregation consistency. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7f8224d commit eb55d0f

3 files changed

Lines changed: 28 additions & 5 deletions

File tree

peer/peer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ func pickManualForwarder() portForwarder {
647647
port, err := portforward.ParseManualPort(raw)
648648
if err != nil {
649649
slog.Warn("ignoring invalid "+env.PeerExternalPort.String(),
650-
"value", raw, "error", err)
650+
"value", raw, "err", err)
651651
} else {
652652
slog.Info("peer client using manual port forward",
653653
"port", port, "source", env.PeerExternalPort.String())

portforward/manual.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,22 @@ func ParseManualPort(s string) (uint16, error) {
4545

4646
// MapPort reports the configured port as both external and internal. The
4747
// router-side rule is already in place; nothing to do at the protocol
48-
// layer.
48+
// layer. Returns an error if the forwarder was constructed with port==0
49+
// (not a valid listen/registration port) so callers can fall through to
50+
// UPnP rather than register a port no peer will listen on.
51+
//
52+
// Protocol is set to "TCP" to match the UPnP-based Forwarder, which
53+
// hard-codes the same value. Samizdat-in inbound traffic is TCP-only;
54+
// when UDP support is added the two forwarders should be widened
55+
// together.
4956
func (m *ManualForwarder) MapPort(_ context.Context, _ uint16, _ string) (*Mapping, error) {
57+
if m.port == 0 {
58+
return nil, fmt.Errorf("manual forwarder constructed with port=0")
59+
}
5060
return &Mapping{
5161
ExternalPort: m.port,
5262
InternalPort: m.port,
63+
Protocol: "TCP",
5364
Method: "manual",
5465
}, nil
5566
}

portforward/manual_test.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,18 @@ func TestParseManualPort(t *testing.T) {
4343
}
4444

4545
// ManualForwarder satisfies the portForwarder contract: MapPort returns
46-
// a Mapping with external==internal port and the "manual" method tag,
47-
// UnmapPort and StartRenewal are no-ops, ExternalIP returns "" so the
48-
// server substitutes the IP it observed on the register call.
46+
// a Mapping with external==internal port, Protocol="TCP" matching the
47+
// UPnP forwarder, and the "manual" method tag. UnmapPort and
48+
// StartRenewal are no-ops, ExternalIP returns "" so the server
49+
// substitutes the IP it observed on the register call.
4950
func TestManualForwarder(t *testing.T) {
5051
f := NewManualForwarder(5698)
5152

5253
m, err := f.MapPort(context.Background(), 30001, "ignored")
5354
require.NoError(t, err)
5455
assert.Equal(t, uint16(5698), m.ExternalPort)
5556
assert.Equal(t, uint16(5698), m.InternalPort, "external==internal — user mapped them themselves")
57+
assert.Equal(t, "TCP", m.Protocol, "Protocol matches UPnP forwarder's hard-coded value")
5658
assert.Equal(t, "manual", m.Method)
5759

5860
require.NoError(t, f.UnmapPort(context.Background()), "UnmapPort is a no-op for manual forwarders")
@@ -64,3 +66,13 @@ func TestManualForwarder(t *testing.T) {
6466
require.NoError(t, err)
6567
assert.Empty(t, ip, "empty IP signals server to use observed source address")
6668
}
69+
70+
// MapPort defensively rejects a zero-port forwarder so a caller that
71+
// somehow gets one (bypassing pickManualForwarder's range check) can
72+
// fall through to UPnP rather than register a non-listening port.
73+
func TestManualForwarder_RejectsZeroPort(t *testing.T) {
74+
f := NewManualForwarder(0)
75+
m, err := f.MapPort(context.Background(), 30001, "ignored")
76+
assert.Nil(t, m)
77+
assert.Error(t, err)
78+
}

0 commit comments

Comments
 (0)