Skip to content
Open
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
97 changes: 97 additions & 0 deletions docs/xdmcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# XDMCP reverse proxy (remote X login into the guest)

XDMCP (X Display Manager Control Protocol, UDP/177) lets a modern **X server**
display the IRIX login screen and desktop by asking the guest's `xdm` for a
session. Behind IRIS's software NAT the guest isn't directly addressable, so a
small application-layer gateway (ALG) makes it work — analogous to the FTP PASV
gateway. Both **IRIX 5.3 and 6.5** speak XDMCP 1.0; there's no version skew.

This applies to **NAT mode** (`[network] mode = "nat"`). In **PCAP bridged
mode** the guest is a real LAN host, so XDMCP works directly with no proxy.

## How it works

```
X server (Xephyr/XQuartz) IRIS host guest (IRIX xdm)
───────────────────────── ───────── ────────────────
XDMCP Query/Request ──udp 177──▶ (redirect 177→11177)
udp forward 11177 ──▶ gateway:11177 ──▶ :177
ALG rewrites Request's
connection-address → gateway
X11 session ◀──tcp 6000+N── relay ◀── gateway:(6000+N) ◀── xdm dials gateway:(6000+N)
```

1. A UDP port-forward `host:11177 → guest:177` carries the XDMCP control channel.
2. The ALG (`src/xdmcp.rs` + `net.rs`) rewrites the `Request` packet's
*connection-addresses* to the NAT gateway and records `display → X-server
address` (the datagram's source).
3. The guest's `xdm` opens the X11 session to `gateway:(6000+display)`. The NAT
relays that to the real X server: to `127.0.0.1` for an X server on the IRIS
host (the generic gateway→loopback path), or to the LAN address recorded in
step 2 for an X server on another machine.

## Setup

### 1. IRIS: add the XDMCP forward

GUI → **Network** tab → **+ Add forward → "XDMCP (host 11177 to guest 177, UDP)"**.
It binds all interfaces so LAN X servers work. Or in `iris.toml`:

```toml
[[network.port_forward]]
proto = "udp"
host_port = 11177
guest_port = 177
bind = "any"
```

Port 11177 is the default (unprivileged so IRIS needs no elevation). Any
UDP forward to guest port 177 activates the ALG.

### 2. Guest: enable XDMCP in `xdm`

On the IRIX guest, make sure `xdm` accepts XDMCP queries: `/usr/lib/X11/xdm/Xaccess`
must allow the querying host (a bare `*` line allows direct queries), and
`xdm-config` must keep `DisplayManager.requestPort: 177` (not `0`). Restart `xdm`.

### 3. X-server host: redirect 177 → 11177

Stock X servers always send XDMCP to UDP **177** with no way to change the port,
so redirect 177→11177 on the **IRIS host** (one-time, needs admin once — keeps
IRIS itself unprivileged):

- **macOS / BSD (pf):** add to `/etc/pf.conf`, then `sudo pfctl -ef /etc/pf.conf`:
```
rdr pass on lo0 inet proto udp from any to any port 177 -> 127.0.0.1 port 11177
rdr pass on en0 inet proto udp from any to any port 177 -> 127.0.0.1 port 11177
```
- **Linux (iptables):**
```
sudo iptables -t nat -A PREROUTING -p udp --dport 177 -j REDIRECT --to-ports 11177
sudo iptables -t nat -A OUTPUT -p udp --dport 177 -j REDIRECT --to-ports 11177
```

### 4. X server: it must listen on TCP

The X11 session connects over TCP, so the X server must accept TCP (many disable
it by default with `-nolisten tcp`). The easy test path is **Xephyr**, which
listens and queries in one command:

```
Xephyr :1 -query <iris-host> -ac -screen 1280x1024
```

(On macOS, XQuartz: Preferences → Security → "Allow connections from network
clients", then `Xephyr`/`Xnest` via XQuartz, or `X :1 -query <iris-host>`.)

The IRIX `xdm` greeter should appear in the Xephyr window; log in to get the
full desktop.

## Caveats

- **Auth:** only `MIT-MAGIC-COOKIE-1` / no-auth are supported. `XDM-AUTHORIZATION-1`
cryptographically binds the addresses, so the ALG's rewrite would break it — it
is detected and left unrewritten (the session won't establish). Configure `xdm`
for magic-cookie or no auth.
- One active session per display number (the X11 port `6000+display`).
- IPv4 only.
7 changes: 7 additions & 0 deletions iris-gui/src/config_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,13 @@ fn show_network(
add = Some(PortForwardConfig { proto: ForwardProto::Tcp, host_port: 2121, guest_port: 21, bind: ForwardBind::Localhost });
ui.close_menu();
}
if ui.add_enabled(!has_port(177), egui::Button::new("XDMCP (host 11177 to guest 177, UDP)"))
.on_hover_text("Remote X login: an X server XDMCP-queries the guest's xdm. Binds all interfaces (LAN X servers OK). Stock X servers use UDP 177 — redirect 177→11177 on the X-server host, or use a chooser that accepts host:port.")
.clicked()
{
add = Some(PortForwardConfig { proto: ForwardProto::Udp, host_port: 11177, guest_port: 177, bind: ForwardBind::Any });
ui.close_menu();
}
if ui.button("Custom (empty row)").clicked() {
add = Some(PortForwardConfig { proto: ForwardProto::Tcp, host_port: 0, guest_port: 0, bind: ForwardBind::Localhost });
ui.close_menu();
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub mod locks;
pub mod pit8254;
pub mod net;
pub mod nfsudp;
pub mod xdmcp;
#[cfg(feature = "pcap")]
pub mod net_pcap;
pub mod seeq8003;
Expand Down
38 changes: 37 additions & 1 deletion src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const UDP_PORT_DNS: u16 = 53;
const UDP_PORT_PORTMAP: u16 = 111;
const UDP_PORT_TIME: u16 = 37;
const UDP_PORT_NTP: u16 = 123;
const XDMCP_GUEST_PORT: u16 = 177; // XDMCP control channel inside the guest
const X11_BASE_PORT: u16 = 6000; // X11 display N's TCP port = 6000 + N
const TCP_PORT_TIME: u16 = 37;
const BOOTP_OP_REQUEST: u8 = 1;

Expand Down Expand Up @@ -762,6 +764,11 @@ pub struct NatEngine {
// Inbound IP-fragment reassembly buffers, keyed by (src_ip, ip_id, proto).
// NFS writes arrive fragmented when wsize > MTU.
frag_reasm: HashMap<(u32, u16, u8), FragReasm>,
// XDMCP reverse-proxy ALG: active X11 session targets, keyed by the X11 TCP
// port (6000 + display). nfs_remap_dst relays guest→gateway:(that port) to the
// real X server recorded here instead of the generic loopback. Populated by
// the XDMCP UDP-forward hook in poll_udp_fwd_listeners.
xdmcp_sessions: HashMap<u16, Ipv4Addr>,
}

/// Reassembly state for one fragmented inbound IP datagram.
Expand Down Expand Up @@ -853,7 +860,8 @@ 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,
guest_mac: None, ip_id: 1, nfs, frag_reasm: HashMap::new() }
guest_mac: None, ip_id: 1, nfs, frag_reasm: HashMap::new(),
xdmcp_sessions: HashMap::new() }
}

/// Rebind the static port-forward listeners from a new rule set, live (no
Expand Down Expand Up @@ -887,6 +895,7 @@ impl NatEngine {
self.udp_nat.clear(); // drops all UdpSockets
self.icmp_nat.clear(); // drops all ICMP raw sockets
self.tcp_fwd_pending.clear();
self.xdmcp_sessions.clear();
self.tcp_fwd_listeners.truncate(self.fwd_static_count); // drop transient FTP data forwards
self.ctl.routed.store(false, Ordering::Relaxed); // re-arm plug-and-play adoption
}
Expand Down Expand Up @@ -1537,6 +1546,11 @@ impl NatEngine {
// through here — it's served in-process by the NAT before this point.)
fn nfs_remap_dst(&self, dst_ip: Ipv4Addr, dport: u16) -> (Ipv4Addr, u16) {
if dst_ip != self.config.gateway_ip { return (dst_ip, dport); }
// XDMCP X11 session: the guest's xdm dials gateway:(6000+display); relay
// it to the real X server the XDMCP ALG recorded for that display.
if let Some(&xserver) = self.xdmcp_sessions.get(&dport) {
return (xserver, dport);
}
// Generic outbound: guest→gateway becomes guest→host loopback on
// the same port. Lets the guest reach any service the host is
// running on 127.0.0.1:<dport> (pyftpdlib on 2121, python -m
Expand All @@ -1547,6 +1561,11 @@ impl NatEngine {
// Reverse: translate (127.0.0.1, host_port) back to the address the guest
// dialed, so replies look like they came from the gateway.
fn nfs_unmap_src(&self, src_ip: Ipv4Addr, sport: u16) -> (Ipv4Addr, u16) {
// XDMCP X11 session reply: traffic from the real X server is presented to
// the guest as coming from the gateway it dialed.
if let Some(&xserver) = self.xdmcp_sessions.get(&sport) {
if src_ip == xserver { return (self.config.gateway_ip, sport); }
}
if src_ip != Ipv4Addr::LOCALHOST { return (src_ip, sport); }
// Generic outbound: reply from host-side dport becomes gateway:dport
// to the guest.
Expand Down Expand Up @@ -2152,6 +2171,23 @@ impl NatEngine {
let guest_ip = self.ctl.observed_guest_ip().unwrap_or(self.config.client_ip);
let gw_ip = self.config.gateway_ip;
let gw_mac = self.config.gateway_mac;
// XDMCP ALG: on the control forward (→ guest:177), rewrite a Request's
// connection-addresses to the gateway so the guest's xdm opens the X11
// session at gateway:(6000+display) — which nfs_remap_dst then relays
// to the real X server. Record display→X-server (the datagram source).
let data = if guest_port == XDMCP_GUEST_PORT {
match (crate::xdmcp::rewrite_request_ipv4(&data, gw_ip), from.ip()) {
(Some(rw), IpAddr::V4(xserver)) => {
let x11_port = X11_BASE_PORT.wrapping_add(rw.display_number);
self.xdmcp_sessions.insert(x11_port, xserver);
dlog_dev!(LogModule::Net, "XDMCP Request: display {} → relay gateway:{} to X server {}", rw.display_number, x11_port, xserver);
rw.packet
}
_ => data,
}
} else {
data
};
// Inject as UDP: src=gateway_ip:host_port dst=guest_ip:guest_port
let udp = udp_packet(gw_ip, guest_ip, host_port, guest_port, &data);
let id = self.ip_id; self.ip_id = self.ip_id.wrapping_add(1);
Expand Down
Loading
Loading