Add the remchild PoC: client, server, naming, and plan
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* remchild — proof-of-concept remote child client.
|
||||
*
|
||||
* Self-contained Linux client: no runtime dependencies beyond libc. It finds
|
||||
* the server, identifies itself, sends heartbeats on the documented schedule,
|
||||
* and answers ping tests. Audio, STT, and video come later; see plan.md.
|
||||
*
|
||||
* Build: cc -O2 -o remchild remchild/client/main.c
|
||||
* Run: ./remchild --server 192.168.1.10:8799
|
||||
*/
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <netpacket/packet.h>
|
||||
#endif
|
||||
|
||||
#include "words.h"
|
||||
|
||||
#define HEARTBEAT_FAST_MS 10000
|
||||
#define HEARTBEAT_MID_MS 30000
|
||||
#define HEARTBEAT_SLOW_MS 300000
|
||||
#define FAST_UNTIL_MS 60000
|
||||
#define MID_UNTIL_MS 360000
|
||||
#define LINE_MAX 4096
|
||||
|
||||
static long now_ms(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
/* FNV-1a over the machine bytes: stable across restarts. */
|
||||
static unsigned long long fnv1a(const unsigned char *data, size_t len) {
|
||||
unsigned long long hash = 1469598103934665603ULL;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
hash ^= data[i];
|
||||
hash *= 1099511628211ULL;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/* First non-loopback MAC. Returns 0 when no usable interface exists. */
|
||||
static int machine_mac(unsigned char *out6) {
|
||||
struct ifaddrs *list = NULL;
|
||||
int found = 0;
|
||||
|
||||
if (getifaddrs(&list) != 0) return 0;
|
||||
|
||||
for (struct ifaddrs *it = list; it && !found; it = it->ifa_next) {
|
||||
#if defined(__linux__)
|
||||
if (!it->ifa_addr || it->ifa_addr->sa_family != AF_PACKET) continue;
|
||||
if (it->ifa_flags & IFF_LOOPBACK) continue;
|
||||
|
||||
struct sockaddr_ll *addr = (struct sockaddr_ll *)it->ifa_addr;
|
||||
if (addr->sll_halen != 6) continue;
|
||||
|
||||
int all_zero = 1;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
out6[i] = addr->sll_addr[i];
|
||||
if (out6[i]) all_zero = 0;
|
||||
}
|
||||
if (!all_zero) found = 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
freeifaddrs(list);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Fallback identity: /etc/machine-id. */
|
||||
static int machine_id(unsigned char *out, size_t cap) {
|
||||
FILE *file = fopen("/etc/machine-id", "r");
|
||||
if (!file) return 0;
|
||||
|
||||
size_t len = fread(out, 1, cap, file);
|
||||
fclose(file);
|
||||
|
||||
while (len && (out[len - 1] == '\n' || out[len - 1] == '\r')) len--;
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
/* Two words from the two lists. Same word on both sides bumps the second. */
|
||||
static void name_for_id(const unsigned char *id, size_t len, char *out, size_t cap) {
|
||||
unsigned long long hash = fnv1a(id, len);
|
||||
int first = (int)(hash % REMCHILD_WORD_COUNT);
|
||||
int second = (int)((hash / REMCHILD_WORD_COUNT) % REMCHILD_WORD_COUNT);
|
||||
|
||||
if (second == first) second = (second + 1) % REMCHILD_WORD_COUNT;
|
||||
snprintf(out, cap, "%s %s", REMCHILD_FIRST[first], REMCHILD_SECOND[second]);
|
||||
}
|
||||
|
||||
static int connect_to(const char *host, int port) {
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons((unsigned short)port);
|
||||
|
||||
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) return -1;
|
||||
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int one = 1;
|
||||
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void send_line(int fd, const char *line) {
|
||||
char frame[1024];
|
||||
int len = snprintf(frame, sizeof(frame), "%s\n", line);
|
||||
|
||||
if (len <= 0 || send(fd, frame, (size_t)len, MSG_NOSIGNAL) != len) {
|
||||
fprintf(stderr, "[remchild] send failed: %s\n", strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char host[128] = "";
|
||||
int port = 8799;
|
||||
int once = 0;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--server") && i + 1 < argc) {
|
||||
char *value = argv[++i];
|
||||
char *colon = strrchr(value, ':');
|
||||
if (colon) {
|
||||
*colon = 0;
|
||||
port = atoi(colon + 1);
|
||||
}
|
||||
snprintf(host, sizeof(host), "%s", value);
|
||||
} else if (!strcmp(argv[i], "--once")) {
|
||||
once = 1;
|
||||
} else if (!strcmp(argv[i], "--help")) {
|
||||
printf("usage: remchild [--server host:port] [--once]\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
int interactive = isatty(STDIN_FILENO);
|
||||
|
||||
if (!host[0]) {
|
||||
printf("Server address (host:port): ");
|
||||
fflush(stdout);
|
||||
char line[128];
|
||||
if (!fgets(line, sizeof(line), stdin)) return 1;
|
||||
line[strcspn(line, "\n")] = 0;
|
||||
char *colon = strrchr(line, ':');
|
||||
if (colon) {
|
||||
*colon = 0;
|
||||
port = atoi(colon + 1);
|
||||
}
|
||||
snprintf(host, sizeof(host), "%s", line);
|
||||
}
|
||||
|
||||
unsigned char id[32];
|
||||
size_t id_len = 0;
|
||||
unsigned char mac[6];
|
||||
|
||||
if (machine_mac(mac)) {
|
||||
memcpy(id, mac, 6);
|
||||
id_len = 6;
|
||||
} else {
|
||||
int got = machine_id(id, sizeof(id));
|
||||
if (got <= 0) {
|
||||
fprintf(stderr, "[remchild] no MAC and no /etc/machine-id; cannot identify this host\n");
|
||||
return 1;
|
||||
}
|
||||
id_len = (size_t)got;
|
||||
}
|
||||
|
||||
char name[64];
|
||||
name_for_id(id, id_len, name, sizeof(name));
|
||||
|
||||
char id_hex[17];
|
||||
snprintf(id_hex, sizeof(id_hex), "%016llx", fnv1a(id, id_len));
|
||||
|
||||
printf("[remchild] identity %s (name: %s)\n", id_len == 6 ? "MAC" : "machine-id", name);
|
||||
printf("[remchild] connecting to %s:%d\n", host, port);
|
||||
|
||||
int fd = connect_to(host, port);
|
||||
if (fd < 0) {
|
||||
if (errno == ECONNREFUSED) {
|
||||
fprintf(stderr, "[remchild] could not reach %s:%d: no server is listening there.\n", host, port);
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"[remchild] could not reach %s:%d (%s).\n"
|
||||
"[remchild] A firewall may block TCP %d on the server or on this host.\n",
|
||||
host, port, strerror(errno), port);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("[remchild] connected. type p + Enter for a ping test, q + Enter to quit.\n");
|
||||
|
||||
char hello[512];
|
||||
snprintf(hello, sizeof(hello),
|
||||
"{\"type\":\"hello\",\"id\":\"%s\",\"name\":\"%s\",\"platform\":\"linux\",\"version\":\"0.1\"}",
|
||||
id_hex, name);
|
||||
send_line(fd, hello);
|
||||
|
||||
long started = now_ms();
|
||||
long last_beat = started;
|
||||
int ping_seq = 0;
|
||||
long ping_sent_at = 0;
|
||||
char buffer[LINE_MAX];
|
||||
size_t used = 0;
|
||||
int answered_ping = 0;
|
||||
|
||||
if (once) {
|
||||
ping_seq = 1;
|
||||
ping_sent_at = now_ms();
|
||||
send_line(fd, "{\"type\":\"ping\",\"seq\":1}");
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
long now = now_ms();
|
||||
long interval = now - started < FAST_UNTIL_MS ? HEARTBEAT_FAST_MS
|
||||
: now - started < MID_UNTIL_MS ? HEARTBEAT_MID_MS
|
||||
: HEARTBEAT_SLOW_MS;
|
||||
|
||||
if (now - last_beat >= interval) {
|
||||
char beat[128];
|
||||
snprintf(beat, sizeof(beat), "{\"type\":\"heartbeat\",\"ts\":%ld}", (long)time(NULL));
|
||||
send_line(fd, beat);
|
||||
last_beat = now;
|
||||
}
|
||||
|
||||
struct pollfd fds[2] = {{fd, POLLIN, 0}, {STDIN_FILENO, interactive ? POLLIN : 0, 0}};
|
||||
int ready = poll(fds, interactive ? 2 : 1, 500);
|
||||
if (ready < 0 && errno != EINTR) break;
|
||||
|
||||
if (ready > 0 && (fds[0].revents & (POLLERR | POLLHUP))) {
|
||||
printf("[remchild] server closed the connection\n");
|
||||
break;
|
||||
}
|
||||
|
||||
if (ready > 0 && (fds[0].revents & POLLIN)) {
|
||||
ssize_t got = recv(fd, buffer + used, sizeof(buffer) - used - 1, 0);
|
||||
if (got <= 0) {
|
||||
printf("[remchild] server closed the connection\n");
|
||||
break;
|
||||
}
|
||||
used += (size_t)got;
|
||||
buffer[used] = 0;
|
||||
|
||||
char *line = buffer;
|
||||
char *newline;
|
||||
while ((newline = strchr(line, '\n'))) {
|
||||
*newline = 0;
|
||||
if (strstr(line, "\"type\":\"welcome\"")) {
|
||||
printf("[remchild] welcome: %s\n", line);
|
||||
} else if (strstr(line, "\"type\":\"pong\"")) {
|
||||
if (ping_sent_at) printf("[remchild] pong seq=%d rtt=%ldms\n", ping_seq, now_ms() - ping_sent_at);
|
||||
answered_ping = 1;
|
||||
} else if (strstr(line, "\"type\":\"ping\"")) {
|
||||
int seq = 0;
|
||||
sscanf(line, "%*[^\"]\"seq\":%d", &seq);
|
||||
char pong[64];
|
||||
snprintf(pong, sizeof(pong), "{\"type\":\"pong\",\"seq\":%d}", seq);
|
||||
send_line(fd, pong);
|
||||
} else {
|
||||
printf("[remchild] server: %s\n", line);
|
||||
}
|
||||
line = newline + 1;
|
||||
}
|
||||
|
||||
used = strlen(line);
|
||||
memmove(buffer, line, used + 1);
|
||||
}
|
||||
|
||||
if (ready > 0 && (fds[1].revents & POLLIN)) {
|
||||
char input[128];
|
||||
if (!fgets(input, sizeof(input), stdin)) break;
|
||||
input[strcspn(input, "\n")] = 0;
|
||||
|
||||
if (!strcmp(input, "q")) break;
|
||||
if (!strcmp(input, "p")) {
|
||||
ping_seq++;
|
||||
ping_sent_at = now_ms();
|
||||
char ping[64];
|
||||
snprintf(ping, sizeof(ping), "{\"type\":\"ping\",\"seq\":%d}", ping_seq);
|
||||
send_line(fd, ping);
|
||||
} else if (strcmp(input, "")) {
|
||||
printf("[remchild] commands: p = ping test, q = quit\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (once && (answered_ping || now - started > 5000)) break;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/* Generated from words.json. One word per line, 128 per list. */
|
||||
#ifndef REMCHILD_WORDS_H
|
||||
#define REMCHILD_WORDS_H
|
||||
|
||||
#define REMCHILD_WORD_COUNT 128
|
||||
|
||||
static const char *const REMCHILD_FIRST[REMCHILD_WORD_COUNT] = {
|
||||
"Funky",
|
||||
"Chunky",
|
||||
"Groovy",
|
||||
"Soggy",
|
||||
"Cursed",
|
||||
"Blessed",
|
||||
"Turbo",
|
||||
"Hyper",
|
||||
"Moldy",
|
||||
"Slick",
|
||||
"Randy",
|
||||
"Beefy",
|
||||
"Sleazy",
|
||||
"Creepy",
|
||||
"Silent",
|
||||
"Loud",
|
||||
"Radio",
|
||||
"Cyber",
|
||||
"Robo",
|
||||
"Mega",
|
||||
"Ultra",
|
||||
"Micro",
|
||||
"Nano",
|
||||
"Quantum",
|
||||
"Cosmic",
|
||||
"Lunar",
|
||||
"Solar",
|
||||
"Wet",
|
||||
"Dry",
|
||||
"Deep",
|
||||
"Grimy",
|
||||
"Grumpy",
|
||||
"Sleepy",
|
||||
"Sneaky",
|
||||
"Freaky",
|
||||
"Geeky",
|
||||
"Nerdy",
|
||||
"Drunk",
|
||||
"Sober",
|
||||
"Feral",
|
||||
"Tame",
|
||||
"Wild",
|
||||
"Mild",
|
||||
"Salty",
|
||||
"Sweet",
|
||||
"Bitter",
|
||||
"Sour",
|
||||
"Spicy",
|
||||
"Bland",
|
||||
"Burnt",
|
||||
"Raw",
|
||||
"Crispy",
|
||||
"Toasted",
|
||||
"Frozen",
|
||||
"Melting",
|
||||
"Dripping",
|
||||
"Leaking",
|
||||
"Rusty",
|
||||
"Chrome",
|
||||
"Neon",
|
||||
"Pastel",
|
||||
"Goth",
|
||||
"Punk",
|
||||
"Disco",
|
||||
"Techno",
|
||||
"Opera",
|
||||
"Jazz",
|
||||
"Banjo",
|
||||
"Kazoo",
|
||||
"Bagpipe",
|
||||
"Yodel",
|
||||
"Mumble",
|
||||
"Scream",
|
||||
"Whisper",
|
||||
"Giggle",
|
||||
"Cackle",
|
||||
"Sob",
|
||||
"Sigh",
|
||||
"Snore",
|
||||
"Dozy",
|
||||
"Dizzy",
|
||||
"Wobbly",
|
||||
"Stiff",
|
||||
"Limp",
|
||||
"Floaty",
|
||||
"Heavy",
|
||||
"Tiny",
|
||||
"Huge",
|
||||
"Long",
|
||||
"Short",
|
||||
"Thin",
|
||||
"Fat",
|
||||
"Bald",
|
||||
"Hairy",
|
||||
"Beardy",
|
||||
"Toothless",
|
||||
"One-Eyed",
|
||||
"Two-Faced",
|
||||
"Three-Legged",
|
||||
"Four-Eyed",
|
||||
"Six-Fingered",
|
||||
"Seven-Toed",
|
||||
"Nine-Lived",
|
||||
"Zero-G",
|
||||
"Pixel",
|
||||
"Glitchy",
|
||||
"Laggy",
|
||||
"Buffering",
|
||||
"Offline",
|
||||
"Analog",
|
||||
"Digital",
|
||||
"Wired",
|
||||
"Wireless",
|
||||
"Nuclear",
|
||||
"Toxic",
|
||||
"Radioactive",
|
||||
"Magnetic",
|
||||
"Electric",
|
||||
"Static",
|
||||
"Vapor",
|
||||
"Misty",
|
||||
"Foggy",
|
||||
"Dusty",
|
||||
"Muddy",
|
||||
"Slimy",
|
||||
"Gooey",
|
||||
"Crusty",
|
||||
"Flaky",
|
||||
};
|
||||
|
||||
static const char *const REMCHILD_SECOND[REMCHILD_WORD_COUNT] = {
|
||||
"Monkey",
|
||||
"President",
|
||||
"Hobo",
|
||||
"Queer",
|
||||
"Groin",
|
||||
"Wizard",
|
||||
"Goblin",
|
||||
"Gnome",
|
||||
"Ogre",
|
||||
"Troll",
|
||||
"Yeti",
|
||||
"Squid",
|
||||
"Octopus",
|
||||
"Walrus",
|
||||
"Manatee",
|
||||
"Possum",
|
||||
"Raccoon",
|
||||
"Weasel",
|
||||
"Ferret",
|
||||
"Hamster",
|
||||
"Gerbil",
|
||||
"Pigeon",
|
||||
"Seagull",
|
||||
"Penguin",
|
||||
"Flamingo",
|
||||
"Ostrich",
|
||||
"Emu",
|
||||
"Turkey",
|
||||
"Chicken",
|
||||
"Nugget",
|
||||
"Waffle",
|
||||
"Pancake",
|
||||
"Bagel",
|
||||
"Donut",
|
||||
"Muffin",
|
||||
"Biscuit",
|
||||
"Gravy",
|
||||
"Cheese",
|
||||
"Pickle",
|
||||
"Onion",
|
||||
"Garlic",
|
||||
"Potato",
|
||||
"Turnip",
|
||||
"Radish",
|
||||
"Cabbage",
|
||||
"Bean",
|
||||
"Lentil",
|
||||
"Tofu",
|
||||
"Sausage",
|
||||
"Meatball",
|
||||
"Kebab",
|
||||
"Taco",
|
||||
"Burrito",
|
||||
"Nacho",
|
||||
"Pretzel",
|
||||
"Crouton",
|
||||
"Noodle",
|
||||
"Dumpling",
|
||||
"Pierogi",
|
||||
"Goulash",
|
||||
"Chili",
|
||||
"Stew",
|
||||
"Soup",
|
||||
"Broth",
|
||||
"Sludge",
|
||||
"Goo",
|
||||
"Ooze",
|
||||
"Slime",
|
||||
"Booger",
|
||||
"Toenail",
|
||||
"Earwax",
|
||||
"Bellybutton",
|
||||
"Armpit",
|
||||
"Elbow",
|
||||
"Kneecap",
|
||||
"Ankle",
|
||||
"Spleen",
|
||||
"Kidney",
|
||||
"Liver",
|
||||
"Gizzard",
|
||||
"Bladder",
|
||||
"Gullet",
|
||||
"Nostril",
|
||||
"Eyeball",
|
||||
"Eyelash",
|
||||
"Eyebrow",
|
||||
"Forehead",
|
||||
"Chin",
|
||||
"Cheek",
|
||||
"Thumb",
|
||||
"Pinky",
|
||||
"Knuckle",
|
||||
"Wrist",
|
||||
"Shin",
|
||||
"Thigh",
|
||||
"Butt",
|
||||
"Torso",
|
||||
"Spine",
|
||||
"Skull",
|
||||
"Rib",
|
||||
"Jaw",
|
||||
"Tongue",
|
||||
"Tooth",
|
||||
"Molar",
|
||||
"Denture",
|
||||
"Braces",
|
||||
"Splint",
|
||||
"Cast",
|
||||
"Crutch",
|
||||
"Wheelchair",
|
||||
"Scooter",
|
||||
"Tractor",
|
||||
"Bulldozer",
|
||||
"Forklift",
|
||||
"Zamboni",
|
||||
"Segway",
|
||||
"Unicycle",
|
||||
"Tricycle",
|
||||
"Skateboard",
|
||||
"Rollerblade",
|
||||
"Pogo",
|
||||
"Trampoline",
|
||||
"Kite",
|
||||
"Balloon",
|
||||
"Blimp",
|
||||
"Zeppelin",
|
||||
"Submarine",
|
||||
"Dinghy",
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user