Tickling a Sun Ray 2 - DHCP Discovery
This Christmas I just got from my partner a brand new, still sealed, Sun Ray 2. Basically the best gift I ever got. Period.
The Ray is a thin client that connects to a remote Sun server. I already ordered a Sun Fire, but until it arrives, I decided to poke the little thing and see what makes it tick!
After powering on, and having a network cable connected, the Ray shows a little screen with its MAC address, the network cable speed, and if it’s reached a local server. Since the Sun Fire is not here, the last part just does not happen, so let’s begin by observing how it tries to reach the server.
DHCPDISCOVER
The Ray starts with a DHCPDISCOVER to try to reach its server, which is no big
surprise:
0000 ff ff ff ff ff ff 00 14 4f 85 f2 0b 08 00 45 00 ........O.....E.
0010 01 48 00 28 40 00 ff 11 7a 7d 00 00 00 00 ff ff .H.(@...z}......
0020 ff ff 00 44 00 43 01 34 00 00 01 01 06 00 ff 4e ...D.C.4.......N
0030 c3 a7 04 7e 00 00 00 00 00 00 00 00 00 00 00 00 ...~............
0040 00 00 00 00 00 00 00 14 4f 85 f2 0b 00 00 00 00 ........O.......
0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0110 00 00 00 00 00 00 63 82 53 63 35 01 01 3c 0e 53 ......c.Sc5..<.S
0120 55 4e 57 2e 4e 65 77 54 2e 53 55 4e 57 3d 07 01 UNW.NewT.SUNW=..
0130 00 14 4f 85 f2 0b ff 00 00 00 00 00 00 00 00 00 ..O.............
0140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0150 00 00 00 00 00 00 ......
DIX / Ethernet II
The first portion is a classic DIX Ethernet frame (or Ethernet II, if you’re
still litigating DIX credit). From 0x00 to 0x05 we have the broadcast
address ff:ff:ff:ff:ff:ff, the second one is the Ray’s address,
00:14:4f:85:f2:0b, 0x0C to 0x0D indicates an IPv4 packet (0x0800).
IPv4
The second portion, as expected, is the IPv4 header. Nothing new there as well,
the only relevant information there is that the destination address is
255.255.255.255, and it’s a UDP packet; this basically covers bytes 0x0E to
0x21.
UDP
From 0x22 to 0x29 we can observe the UDP header. Interesting points are
source port 68, and destination port 67.
BOOTP/DHCP fixed header + DHCP options
From 0x2A onwards we have the DHCPDISCOVER payload. Finally something
interesting:
- Message Type is
0x01, meaning it’s a Boot Request, - Hardware Type is
0x01, or “Ethernet” - Hardware Address Length is
0x06 - Hops is set as
0x00 - There’s a Transaction ID (
0xff4ec3a7) - A “Seconds Elapsed” field, from the BOOTP era (which the Ray is actively using!).
This is an interesting find: nowadays many implementations don’t do much with
this field, and plenty of clients just leave it at zero, but this was important
back then for a few different reasons! This field aided:
- Relay agents decide whether to forward a request
- Servers decide whether to prioritize a client that has been waiting longer
- Debugging slow or failing DHCP negotiations
- BOOTP flags all zeroed; basically indicating the client is capable of receiving unicast replies
- Zeroed IP addresses
- The Ray’s MAC address, again
- A lot of padding for the Ray’s hardware address
- An empty server hostname
- An empty boot file name
- The Magic Cookie
- And FINALLY what I’m looking for:
- Option 53, indicating the discovery packet
- Option 60, containing the vendor class identifier
- The value is
SUNW.NewT.SUNW
- The value is
- Option 61 identifies the client using a hardware-type-prefixed identifier (Ethernet + MAC address), effectively restating the Ray’s MAC in a DHCP-standardized form. Again.
- Option 255, indicating no more options follow
- And a lot of padding.
So what’s interesting so far?
The big oddity is what’s missing: Option 55 (Parameter Request List). Most DHCP clients use it to ask for things like DNS servers, routes, NTP, etc. The Sun Ray doesn’t ask, it just identifies itself (Vendor Class + Client ID) and waits to see what comes back. That suggests this client either runs with strong defaults, or expects a Sun-aware environment to “just know” what to provide.
Also: SUNW.NewT.SUNW looks like a Sun vendor-class marker; possibly a
New Terminal generation marker rather than a generic Sun Ray family
identifier.
This is the first part of the whole negotiation, and lots can already be observed and speculated!
Now, let’s make something that replies the Ray as it expects. I could just leverage dnsmasq for that, but let’s go the fun way(tm) and write a bit of C:
ray_dhcp.c
ray_dhcp.c//
// Created by vitosartori on 12/13/25.
//
// Minimal DHCP responder (DISCOVER->OFFER, REQUEST->ACK) for RE labs.
// Build: gcc -O2 -Wall -Wextra -o ray_dhcp ray_dhcp.c
// Run: sudo ./ray_dhcp enp2s0 192.168.100.1 192.168.100.50 255.255.255.0 192.168.100.1
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
enum {
BOOTREQUEST = 1,
BOOTREPLY = 2,
HTYPE_ETHERNET = 1,
HLEN_ETHERNET = 6,
DHCP_COOKIE = 0x63825363,
// DHCP Options
OPT_PAD = 0,
OPT_SUBNETMASK = 1,
OPT_ROUTER = 3,
OPT_REQ_IP = 50,
OPT_LEASETIME = 51,
OPT_MSGTYPE = 53,
OPT_SERVERID = 54,
OPT_VENDORCLASS= 60,
OPT_CLIENTID = 61,
OPT_END = 255,
// DHCP Message Types
DHCPDISCOVER = 1,
DHCPOFFER = 2,
DHCPREQUEST = 3,
DHCPACK = 5
};
#pragma pack(push, 1)
typedef struct {
uint8_t op;
uint8_t htype;
uint8_t hlen;
uint8_t hops;
uint32_t xid;
uint16_t secs;
uint16_t flags;
uint32_t ciaddr;
uint32_t yiaddr;
uint32_t siaddr;
uint32_t giaddr;
uint8_t chaddr[16];
uint8_t sname[64];
uint8_t file[128];
// Following is cookie + TLVs
} bootp_t;
#pragma pack(pop)
typedef struct {
uint8_t msgtype; // Option 53
uint8_t has_msgtype;
uint8_t vendor[256]; // Option 60
uint8_t vendor_len;
uint8_t has_vendor;
uint32_t req_ip; // Option 50 (optional)
uint8_t has_req_ip;
} dhcp_opts_t;
static void die(const char *msg) {
perror(msg);
exit(1);
}
static int parse_ipv4(const char *s, uint32_t *out_nbo) {
struct in_addr a;
if (inet_pton(AF_INET, s, &a) != 1) return 0;
*out_nbo = a.s_addr;
return 1;
}
static void print_mac(const uint8_t mac[6], char *buf, const size_t buf_len) {
snprintf(buf, buf_len, "%02x:%02x:%02x:%02x:%02x:%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
static int parse_dhcp_options(const uint8_t *p, const size_t n, dhcp_opts_t *o) {
memset(o, 0, sizeof(*o));
// First the cookie
if (n < 4) return 0;
uint32_t cookie;
memcpy(&cookie, p, 4);
if (ntohl(cookie) != DHCP_COOKIE) {
fprintf(stderr, "drop: cookie mismatch (got %08x)\n", ntohl(cookie));
return 0;
}
size_t i = 4;
while (i < n) {
const uint8_t code = p[i++];
if (code == OPT_PAD) continue;
if (code == OPT_END) break;
if (i >= n) break;
const uint8_t len = p[i++];
if (i + len > n) break;
const uint8_t *val = p + i;
if (code == OPT_MSGTYPE && len == 1) {
o->msgtype = val[0];
o->has_msgtype = 1;
} else if (code == OPT_VENDORCLASS && len > 0 && len <= 255) {
memcpy(o->vendor, val, len);
o->vendor[len] = 0;
o->vendor_len = len;
o->has_vendor = 1;
} else if (code == OPT_REQ_IP && len == 4) {
memcpy(&o->req_ip, val, 4);
o->has_req_ip = 1;
}
i += len;
}
return 1;
}
static uint8_t *opt_put(uint8_t *w, const uint8_t code, const void *val, const uint8_t len) {
*w++ = code;
*w++ = len;
memcpy(w, val, len);
return w + len;
}
static size_t build_reply(uint8_t *out,
const size_t outcap,
const bootp_t *req,
const dhcp_opts_t *reqo,
const uint8_t dhcp_type,
const uint32_t server_ip,
const uint32_t yiaddr,
const uint32_t subnet_mask,
const uint32_t router_ip,
const uint32_t lease_seconds) {
if (outcap < sizeof(bootp_t) + 4 + 64) return 0;
bootp_t rep = {0};
rep.op = BOOTREPLY;
rep.htype = req->htype;
rep.hlen = req->hlen;
rep.hops = 0;
rep.xid = req->xid;
rep.secs = 0;
rep.flags = req->flags; // Echo client flags
rep.ciaddr = 0;
rep.yiaddr = yiaddr; // Offered/acked IP
rep.siaddr = server_ip; // the mothership hint
rep.giaddr = 0;
memcpy(rep.chaddr, req->chaddr, 16);
memcpy(out, &rep, sizeof(rep));
uint8_t *w = out + sizeof(rep);
const uint32_t cookie = htonl(DHCP_COOKIE);
memcpy(w, &cookie, 4);
w += 4;
// Options
w = opt_put(w, OPT_MSGTYPE, &dhcp_type, 1);
w = opt_put(w, OPT_SERVERID, &server_ip, 4);
const uint32_t lt = htonl(lease_seconds);
w = opt_put(w, OPT_LEASETIME, <, 4);
w = opt_put(w, OPT_SUBNETMASK, &subnet_mask, 4);
w = opt_put(w, OPT_ROUTER, &router_ip, 4);
// Optional: echo vendor class back, may help with “vendor-aware feel”
if (reqo->has_vendor && reqo->vendor_len > 0) {
w = opt_put(w, OPT_VENDORCLASS, reqo->vendor, reqo->vendor_len);
}
*w++ = OPT_END;
return (size_t) (w - out);
}
int main(const int argc, char **argv) {
if (argc != 6) {
fprintf(stderr,
"Usage: %s <ifname> <server_ip> <offer_ip> <subnet_mask> <router_ip>\n"
"Example: %s enp2s0 192.168.100.1 192.168.100.50 255.255.255.0 192.168.100.1\n",
argv[0], argv[0]);
return 2;
}
const char *ifname = argv[1];
uint32_t server_ip, offer_ip, mask_ip, router_ip;
if (!parse_ipv4(argv[2], &server_ip) ||
!parse_ipv4(argv[3], &offer_ip) ||
!parse_ipv4(argv[4], &mask_ip) ||
!parse_ipv4(argv[5], &router_ip)) {
fprintf(stderr, "Invalid IPv4 argument\n");
return 2;
}
const int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0) die("socket");
const int yes = 1;
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) die("setsockopt REUSEADDR");
if (setsockopt(s, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(yes)) < 0) die("setsockopt BROADCAST");
// Bind to port 67 on all addresses
struct sockaddr_in bindaddr = {0};
bindaddr.sin_family = AF_INET;
bindaddr.sin_port = htons(67);
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(s, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) < 0) {
fprintf(stderr, "bind(:67) failed: %s\n", strerror(errno));
fprintf(stderr, "Tip: run as root or grant cap_net_bind_service.\n");
return 1;
}
// Pin to interface so we don't respond on the wrong NIC.
if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname) + 1) < 0) {
fprintf(stderr, "SO_BINDTODEVICE(%s) failed: %s\n", ifname, strerror(errno));
return 1;
}
fprintf(stderr, "Listening on %s UDP/67. server=%s offer=%s\n", ifname, argv[2], argv[3]);
uint8_t inbuf[2048];
uint8_t outbuf[2048];
for (;;) {
struct sockaddr_in src;
socklen_t srclen = sizeof(src);
const ssize_t n = recvfrom(s, inbuf, sizeof(inbuf), 0, (struct sockaddr*)&src, &srclen);
if (n < 0) {
if (errno == EINTR) continue;
die("recvfrom");
}
fprintf(stderr, "got %zd bytes from %s:%d\n",
n, inet_ntoa(src.sin_addr), ntohs(src.sin_port));
fflush(stderr);
enum { BOOTP_FIXED_LEN = 236 };
if ((size_t)n < BOOTP_FIXED_LEN + 4) continue;
const size_t opt_len = (size_t)n - BOOTP_FIXED_LEN;
const bootp_t *req = (const bootp_t *)inbuf; // prolly OK for chaddr/xid/etc.
if (req->op != BOOTREQUEST) { fprintf(stderr,"drop: not BOOTREQUEST\n"); continue; }
if (req->htype != HTYPE_ETHERNET) { fprintf(stderr,"drop: htype=%u\n", req->htype); continue; }
if (req->hlen != HLEN_ETHERNET) { fprintf(stderr,"drop: hlen=%u\n", req->hlen); continue; }
// Options start at offset 236
dhcp_opts_t o = {0};
if (!parse_dhcp_options(inbuf + BOOTP_FIXED_LEN, opt_len, &o)) continue;
if (!o.has_msgtype) {
fprintf(stderr,"drop: no option 53\n");
continue;
}
char macs[32];
print_mac(req->chaddr, macs, sizeof(macs));
fprintf(stderr, "opts[0..15]: ");
for (int i = 0; i < 16 && (size_t)i < opt_len; i++) fprintf(stderr, "%02x ", inbuf[BOOTP_FIXED_LEN + i]);
fprintf(stderr, "\n");
uint8_t reply_type = 0;
if (o.msgtype == DHCPDISCOVER) reply_type = DHCPOFFER;
else if (o.msgtype == DHCPREQUEST) reply_type = DHCPACK;
else {
fprintf(stderr, "drop: Unknown msgtype 0x%02x\n", o.msgtype);
continue;
}
// Honor Option 50 Requested IP if present.
uint32_t yiaddr = offer_ip;
if (o.has_req_ip) yiaddr = o.req_ip;
size_t outlen = build_reply(outbuf, sizeof(outbuf), req, &o, reply_type,
server_ip, yiaddr, mask_ip, router_ip, 3600);
if (!outlen) continue;
// Send to broadcast:68, as we noticed the client doesn't have an IP
struct sockaddr_in dst = {0};
dst.sin_family = AF_INET;
dst.sin_port = htons(68);
dst.sin_addr.s_addr = htonl(INADDR_BROADCAST);
if (sendto(s, outbuf, outlen, 0, (struct sockaddr*)&dst, sizeof(dst)) < 0) {
fprintf(stderr, "sendto failed: %s\n", strerror(errno));
continue;
}
fprintf(stderr, "Sent %lu bytes\n", outlen);
if (o.has_vendor) {
fprintf(stderr, "%s xid=%08x -> %s (vendor=%s)\n",
macs, ntohl(req->xid),
(reply_type == DHCPOFFER) ? "OFFER" : "ACK",
(const char *)o.vendor);
} else {
fprintf(stderr, "%s xid=%08x -> %s\n",
macs, ntohl(req->xid),
(reply_type == DHCPOFFER) ? "OFFER" : "ACK");
}
}
}
This replies with what the Ray expects, offering it an IP leased from the Linux box. I only forgot to mention how things are wired here at my lab:
Sun Ray ↔ USB Ethernet ↔ Linux box (DHCP responder) ↔ rest of LAN
So the magic is happening on interface enx00e04c3f6690, I’ll give myself an IP
192.168.100.1, and give 192.168.100.50 to the Ray. First, let’s get an IP on
that interface:
sudo ip addr add 192.168.100.1/24 dev enx00e04c3f6690
sudo ip link set enx00e04c3f6690 up
And finally compile and run (as sudo). The result is positive! The Ray updated
the UI indicating it has communicated with the server! Then, a wild UDP packet
appears:
0000 ff ff ff ff ff ff 00 14 4f 85 f2 0b 08 00 45 00 ........O.....E.
0010 00 27 00 06 40 00 40 11 15 e6 c0 a8 64 32 ff ff .'..@[email protected]..
0020 ff ff 1b 61 1b 61 00 13 00 00 73 65 72 76 65 72 ...a.a....server
0030 51 3d 31 0a 00 00 00 00 00 00 00 00 Q=1.........
Ignoring the framing information and other headers we get 11 payload bytes plus some padding. The payload is:
0000 73 65 72 76 65 72 51 3d 31 0a 00 serverQ=1..
That’s… Literally serverQ=1\n\0. But there’s more! Here’s what the packet
gives:
- Both source and destination ports are
7009 - The source IP is the IP we gave to the Ray! (I omitted a few
DHCPACKandDHCPINFORMpackets) - The destination IP is STILL the broadcast address
This might be Sun Ray’s appliance protocol territory (UDP/7009 shows up in Sun Ray land), but I’m treating it as an unknown for now. The protocol is proprietary and not documented, but let’s wrap up for now and just be happy for the little, still important, progress.
Next: what happens if we answer serverQ=1?