Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 22 additions & 0 deletions HELP.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,28 @@ The emulator prints a line for each rule that binds successfully at startup:
iris: TCP port forward 127.0.0.1:2323 → guest:23
```

#### rsh / rlogin

Forwards to guest port 514 (`shell`) or 513 (`login`) are given a source port in
the reserved 512–1023 range, because `rshd`/`rlogind` reject any client outside
it. The guest sees the connection as coming from the **gateway** (`192.168.0.1`),
not from the host's own address, so set up the trust against that:

```sh
# on IRIX — /etc/inetd.conf must have: shell stream tcp nowait root /usr/etc/rshd rshd
echo '192.168.0.1 gateway' >> /etc/hosts
echo 'gateway yourname' > /.rhosts # ~/.rhosts for non-root; hosts.equiv never applies to root
chmod 600 /.rhosts
```

```bash
rsh -p 2514 root@127.0.0.1 uname -a
```

The rsh stderr channel (the reverse connection rshd opens back to the client)
does not survive NAT — use a client that sends `0` as the stderr port, as `rcp`
and most modern implementations do.

#### Testing

```bash
Expand Down
80 changes: 80 additions & 0 deletions rules/irix/rsh-forward-needs-reserved-source-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Inbound port-forwards to rsh/rlogin need a reserved source port

## Symptom

`rsh` into the guest through a `[[port_forward]]` to guest port 514 always
fails, while telnet through the same mechanism works fine. `rshd` closes the
connection without writing a single byte, so a client that reports the server's
own framing has nothing to show:

```
rsh: server closed connection without response
```

`/var/adm/SYSLOG` on the guest has the real reason:

```
rshd[123]: Connection from 192.168.0.1 on illegal port 49152
```

## Cause

`poll_tcp_fwd_listeners` synthesizes the SYN it injects into the guest, and it
allocated the source port from `fwd_ephemeral_next`, which starts at 49152. The
guest never sees the host client's real source port — only the one we make up.

BSD r-services authenticate with `.rhosts`/`hosts.equiv` trust, which is only
meaningful if the client proved it was root by binding a reserved port. So
`rshd` rejects anything outside 512..1023 *before reading the request*:

```c
if (fromp->sin_port >= IPPORT_RESERVED || fromp->sin_port < IPPORT_RESERVED/2)
exit(1); /* "Connection from %s on illegal port" */
```

A client that binds a reserved port on the host — as a correct rsh does — makes
no difference, because the NAT discards it. This is also why running the client
under `sudo` changes nothing, which makes the failure look unrelated to ports.

## 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.

## Guest-side setup this still needs

The forward makes the connection appear to come from the gateway
(192.168.0.1), and reverse DNS for it fails (queries go to the upstream
resolver, which knows nothing about RFC1918 space). Trust must be granted to
the gateway:

- `/etc/inetd.conf`: `shell stream tcp nowait root /usr/etc/rshd rshd`
- `/etc/hosts`: `192.168.0.1 gateway`
- `~/.rhosts`, mode 600 — `/.rhosts` for root, since `hosts.equiv` never
applies to root: `gateway <local-username>`

## The stderr channel

The rsh protocol's second connection is a reverse one: the client passes a port
number and `rshd` dials *back* to it from a `rresvport()`. That direction
already lands correctly — `nfs_remap_dst` maps guest→`192.168.0.1:N` onto host
`127.0.0.1:N` — but the NAT rewrites its source port to an OS-chosen ephemeral,
and `rcmd(3)` clients check that the reverse connection's source port is
reserved too. Clients that send `0` as the stderr port (`rcp` does, as do most
modern implementations) sidestep this entirely. Supporting the stderr channel
would require the outbound NAT connect to bind a reserved port, i.e. root on
the host.

## Verified

IRIX 5.3, NAT mode, forward `2514 → 514`:

```
$ rsh -p 2514 root@127.0.0.1 uname -a
IRIX IRIS 5.3 12200159 IP22 mips
```

Use `127.0.0.1`, not `localhost`: `bind = "localhost"` binds IPv4 only, so a
client that resolves `localhost` to `::1` gets ECONNREFUSED — a failure that
looks exactly like the forward not being there.
18 changes: 15 additions & 3 deletions src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,8 @@ pub struct NatEngine {
tcp_fwd_pending: HashMap<(u32, u16, u16), TcpFwdPending>,
// Monotonically increasing counter for generating ephemeral ports for inbound forwards.
fwd_ephemeral_next: u16,
// Same, for forwards to BSD r-services, which reject any source port outside 512..1023.
fwd_reserved_next: u16,
// Number of configured (static) forwards at the front of `tcp_fwd_listeners`;
// anything past this index is a transient FTP-ALG data forward (bounded, FIFO).
fwd_static_count: usize,
Expand Down Expand Up @@ -1157,6 +1159,7 @@ impl NatEngine {
icmp_nat: HashMap::new(), icmp_unavailable: false, deferred_rx: Vec::new(),
tcp_fwd_listeners, udp_fwd_listeners, fwd_static_count,
tcp_fwd_pending: HashMap::new(), fwd_ephemeral_next: 49152,
fwd_reserved_next: 512,
guest_mac: None, ip_id: 1, nfs, frag_reasm: HashMap::new(),
xdmcp_sessions: HashMap::new() }
}
Expand Down Expand Up @@ -2394,9 +2397,18 @@ impl NatEngine {
}
}
for (stream, guest_port) in accepted {
let ephemeral = self.fwd_ephemeral_next;
self.fwd_ephemeral_next = self.fwd_ephemeral_next.wrapping_add(1);
if self.fwd_ephemeral_next < 49152 { self.fwd_ephemeral_next = 49152; }
// 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

} else {
let p = self.fwd_ephemeral_next;
self.fwd_ephemeral_next = self.fwd_ephemeral_next.wrapping_add(1);
if self.fwd_ephemeral_next < 49152 { self.fwd_ephemeral_next = 49152; }
p
};

let client_isn = 0x6000_0000u32.wrapping_add(ephemeral as u32);
// Forward to the guest's *actual* IP (learned from its traffic), not
Expand Down
Loading