/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) #include #endif #include "words.h" #define DISCOVERY_PORT 8798 #define DISCOVERY_WINDOW_MS 5000 #define DISCOVERY_MAX 8 #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 heartbeat_interval(long elapsed_ms) { if (elapsed_ms < FAST_UNTIL_MS) return HEARTBEAT_FAST_MS; if (elapsed_ms < MID_UNTIL_MS) return HEARTBEAT_MID_MS; return HEARTBEAT_SLOW_MS; } 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]); } struct Discovered { char ip[64]; char name[64]; char id[64]; int port; }; /* * Ask the LAN who is there. Probes go out as broadcast and to loopback; a * server answers with its beacon, and the address comes from the reply. * Waits up to DISCOVERY_WINDOW_MS. Returns the number of servers found. */ static int discover(struct Discovered *found, int cap) { int fd = socket(AF_INET, SOCK_DGRAM, 0); if (fd < 0) return 0; int one = 1; setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &one, sizeof(one)); struct sockaddr_in local = {0}; local.sin_family = AF_INET; local.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(fd, (struct sockaddr *)&local, sizeof(local)) != 0) { close(fd); return 0; } struct sockaddr_in broadcast = {0}; broadcast.sin_family = AF_INET; broadcast.sin_port = htons(DISCOVERY_PORT); broadcast.sin_addr.s_addr = htonl(INADDR_BROADCAST); struct sockaddr_in loopback = broadcast; loopback.sin_addr.s_addr = htonl(INADDR_LOOPBACK); const char *probe = "{\"type\":\"veskslop-remchild-probe\",\"version\":\"0.1\"}"; long deadline = now_ms() + DISCOVERY_WINDOW_MS; long next_probe = 0; int count = 0; while (now_ms() < deadline) { if (now_ms() >= next_probe) { sendto(fd, probe, strlen(probe), 0, (struct sockaddr *)&broadcast, sizeof(broadcast)); sendto(fd, probe, strlen(probe), 0, (struct sockaddr *)&loopback, sizeof(loopback)); next_probe = now_ms() + 1000; } struct pollfd pfd = {fd, POLLIN, 0}; if (poll(&pfd, 1, 200) <= 0) continue; if (!(pfd.revents & POLLIN)) continue; char buffer[1024]; struct sockaddr_in from; socklen_t from_len = sizeof(from); ssize_t got = recvfrom(fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&from, &from_len); if (got <= 0) continue; buffer[got] = 0; if (!strstr(buffer, "veskslop-remchild") || strstr(buffer, "probe")) continue; char ip[64]; if (!inet_ntop(AF_INET, &from.sin_addr, ip, sizeof(ip))) continue; int port = 0; char name[64] = "veskslop"; char id[64] = ""; char *match = strstr(buffer, "\"port\":"); if (match) port = atoi(match + 7); match = strstr(buffer, "\"name\":\""); if (match) sscanf(match + 8, "%63[^\"]", name); match = strstr(buffer, "\"id\":\""); if (match) sscanf(match + 6, "%63[^\"]", id); if (port <= 0) continue; // One server can answer from both loopback and its LAN address; the // beacon id identifies it. int duplicate = 0; for (int i = 0; i < count; i++) { if (id[0] && found[i].id[0] && !strcmp(found[i].id, id)) duplicate = 1; else if (!id[0] && !found[i].id[0] && !strcmp(found[i].name, name) && found[i].port == port) duplicate = 1; } if (duplicate || count >= cap) continue; snprintf(found[count].ip, sizeof(found[count].ip), "%s", ip); snprintf(found[count].name, sizeof(found[count].name), "%s", name); snprintf(found[count].id, sizeof(found[count].id), "%s", id); found[count].port = port; count++; } close(fd); return count; } 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]) { struct Discovered found[DISCOVERY_MAX]; printf("[remchild] looking for a server on the LAN for %d seconds...\n", DISCOVERY_WINDOW_MS / 1000); fflush(stdout); int count = discover(found, DISCOVERY_MAX); if (count == 1) { printf("[remchild] found %s at %s:%d\n", found[0].name, found[0].ip, found[0].port); snprintf(host, sizeof(host), "%s", found[0].ip); port = found[0].port; } else if (count > 1) { printf("[remchild] found %d servers:\n", count); for (int i = 0; i < count; i++) { printf(" %d) %s at %s:%d\n", i + 1, found[i].name, found[i].ip, found[i].port); } printf("pick one [1]: "); fflush(stdout); char pick_line[32]; int pick = 1; if (fgets(pick_line, sizeof(pick_line), stdin)) pick = atoi(pick_line); if (pick < 1 || pick > count) pick = 1; printf("[remchild] using %s at %s:%d\n", found[pick - 1].name, found[pick - 1].ip, found[pick - 1].port); snprintf(host, sizeof(host), "%s", found[pick - 1].ip); port = found[pick - 1].port; } else { printf("[remchild] no server answered discovery.\n"); 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); fflush(stdout); 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\",\"heartbeatMs\":%ld}", id_hex, name, heartbeat_interval(0)); 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 = heartbeat_interval(now - started); if (now - last_beat >= interval) { char beat[128]; snprintf(beat, sizeof(beat), "{\"type\":\"heartbeat\",\"ts\":%ld,\"heartbeatMs\":%ld}", (long)time(NULL), interval); 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\":\"notice\"")) { int missed = 0; char *value = strstr(line, "\"missed\":"); if (value) missed = atoi(value + 9); printf("[remchild] notice: the server missed %d heartbeat(s); the connection may be unstable.\n", missed); } else if (strstr(line, "\"type\":\"ping\"")) { int seq = 0; char *value = strstr(line, "\"seq\":"); if (value) seq = atoi(value + 6); 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; }