NAT: use a reserved source port for forwards to rsh/rlogin#56
Conversation
Inbound port-forwards synthesize the SYN injected into the guest and took the
source port from fwd_ephemeral_next (49152+). BSD r-services reject any client
whose source port falls outside 512..1023, and they do it before reading the
request, so rshd hung up on every forwarded connection while telnet through the
same mechanism worked fine.
The host client's own source port never reaches the guest, so a client binding a
reserved port (or being run as root) made no difference -- which made the
failure look unrelated to ports.
Allocate from a separate 512..1023 counter when the forward targets 513/514.
512 ports is ample; the NAT key (guest_ip, guest_port, sport) stays unique.
Verified on IRIX 5.3 with a 2514 -> 514 forward:
$ rsh -p 2514 root@127.0.0.1 uname -a
IRIX IRIS 5.3 12200159 IP22 mips
There was a problem hiding this comment.
Pull request overview
Adjusts the NAT inbound TCP port-forward path so that forwards targeting BSD r-services (rsh/rlogin) use a reserved source port, matching the guest-side security expectations of rshd/rlogind.
Changes:
- Add a dedicated reserved-port allocator (
512..=1023) for inbound forwards targeting guest ports 513/514. - Use the reserved allocator when synthesizing the forwarded SYN in
poll_tcp_fwd_listeners. - Document the behavior and guest-side setup in
HELP.mdand add a dedicated rules note.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/net.rs |
Adds fwd_reserved_next and uses reserved source ports for forwards to guest 513/514 when synthesizing inbound SYNs. |
rules/irix/rsh-forward-needs-reserved-source-port.md |
New troubleshooting/design note documenting why reserved source ports are required for rsh/rlogin forwards. |
HELP.md |
Adds user-facing documentation for rsh/rlogin forwarding requirements and limitations (stderr channel). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // rshd/rlogind reject a client whose source port isn't reserved, and the | ||
| // guest only ever sees the port synthesized here, not the host client's. | ||
| let ephemeral = if matches!(guest_port, 513 | 514) { | ||
| let p = self.fwd_reserved_next; | ||
| self.fwd_reserved_next = if p >= 1023 { 512 } else { p + 1 }; | ||
| p |
There was a problem hiding this comment.
Good catch — fixed in b3b9c2d.
The port is now allocated by alloc_fwd_sport(), which probes the range and returns the first candidate with no live entry in tcp_fwd_pending, tcp_nat, or tcp_tw for that guest port, or None if the whole range is occupied (the caller then drops the accept). This covers the ephemeral range too, since it had the same latent bug with a larger range. I also reordered so an accept dropped for an unknown guest MAC no longer burns a port.
Verified on IRIX 5.3: 8 concurrent rsh sessions each get a distinct reserved source port with no collision or overwrite.
The reserved (512..=1023) and ephemeral (49152..) source-port counters for inbound forwards advanced blindly. Since the chosen port is part of the tcp_fwd_pending / tcp_nat / tcp_tw keys, a wrapped counter could reuse a port still live for the same guest port and silently overwrite that entry via HashMap::insert, breaking the active connection. Only 512 reserved ports exist, so rsh/rlogin is where this bites first. Add alloc_fwd_sport(), which probes the range and returns the first port with no live entry in any of the three maps, or None if the range is exhausted (the caller then drops the accept). Also reorder so an accept dropped for an unknown guest MAC no longer consumes a port. Verified on IRIX 5.3: 8 concurrent rsh sessions each get a distinct reserved source port with no collision.
Problem
rshinto the guest through a[[port_forward]]to guest port 514 fails everytime, while telnet through the same mechanism works.
rshdcloses theconnection without writing a byte, and
/var/adm/SYSLOGon the guest shows why:poll_tcp_fwd_listenerssynthesizes the SYN it injects into the guest and tookthe source port from
fwd_ephemeral_next, which starts at 49152. BSDr-services authenticate via
.rhosts/hosts.equivtrust, which only meansanything if the client proved it was root by binding a reserved port, so
rshdrejects anything outside 512..1023 before reading the request:
The host client's own source port never reaches the guest, so a client that
correctly binds a reserved port — or is run under
sudo— behaves identically.That's what makes this one hard to spot from the client side.
Fix
Allocate the injected source port from a separate 512..1023 counter when the
forward targets 513/514. 512 ports is ample; the NAT key
(guest_ip, guest_port, sport)stays unique.Verified
IRIX 5.3, NAT mode, forward
2514 → 514:Notes
Guest-side trust must be granted to the gateway, since that's who the
connection appears to come from (documented in HELP.md and the new rules note):
The rsh stderr channel still won't work, and this PR doesn't try to fix it.
The reverse connection already lands in the right place —
nfs_remap_dstmapsguest→
192.168.0.1:Nonto host127.0.0.1:N— but the NAT rewrites its sourceport to an OS-chosen ephemeral, and
rcmd(3)clients check that port isreserved too. Supporting it would mean binding a reserved port on the outbound
NAT connect, i.e. root on the host. Clients that send
0as the stderr port(
rcp, and most modern implementations) are unaffected.Also worth knowing: use
127.0.0.1, notlocalhost—bind = "localhost"binds IPv4 only, so a client resolving
localhostto::1gets ECONNREFUSED,which looks exactly like the forward not existing.