Code cleanup, split much of the implementation code into header files

This commit is contained in:
mrkmntal 2026-09-20 20:05:37 -04:00
commit 0d55018460
5 changed files with 293 additions and 196 deletions

75
hss-ports.h Normal file
View file

@ -0,0 +1,75 @@
#ifndef HSS_PORTS_H
#define HSS_PORTS_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_PORTS 256
#define CONFIG_PATH "~/.config/.hssrc"
static const int hss_modern_ports[] = {
8080, 8443, 3000, 5000, 8000, 8888, 9000, 9090, 1337, 27017, 0
};
struct hss_port_entry {
int port;
char service_name[64];
};
static int hss_is_common_port(int port) {
if (port >= 1 && port <= 1023) return 1;
for (int i = 0; hss_modern_ports[i]; i++)
if (hss_modern_ports[i] == port) return 1;
return 0;
}
/* Append port to list in sorted order, skipping duplicates; name NULL -> use port number */
static void hss_add_entry(struct hss_port_entry *e, int *n, int port, const char *name) {
for (int i = 0; i < *n; i++)
if (e[i].port == port) return;
if (*n >= MAX_PORTS) return;
int i = (*n)++;
while (i > 0 && e[i - 1].port > port) { e[i] = e[i - 1]; i--; }
e[i].port = port;
if (name) snprintf(e[i].service_name, sizeof(e[i].service_name), "%.20s", name);
else snprintf(e[i].service_name, sizeof(e[i].service_name), "%d", port);
}
static void hss_parse_services(struct hss_port_entry *e, int *n) {
FILE *f = fopen("/etc/services", "r");
if (!f) return;
char line[256], name[64], proto[16];
int start, end;
while (fgets(line, sizeof(line), f)) {
if (line[0] == '#' || line[0] == '\n') continue;
end = -1;
if (sscanf(line, "%63s %d-%d/%15s", name, &start, &end, proto) < 4 &&
sscanf(line, "%63s %d/%15s", name, &start, proto) != 3) continue;
if (end < 0) end = start;
if (strstr(proto, "6")) continue;
if (end - start + 1 > 20) end = start + 19;
for (int p = start; p <= end; p++)
if (p > 0 && p < 65536 && hss_is_common_port(p)) hss_add_entry(e, n, p, name);
}
fclose(f);
}
static void hss_parse_config(const char *path, struct hss_port_entry *e, int *n) {
char expanded[256], line[256];
if (path[0] == '~') {
if (!getenv("HOME")) return;
snprintf(expanded, sizeof(expanded), "%s%s", getenv("HOME"), path + 1);
path = expanded;
}
FILE *f = fopen(path, "r");
if (!f) return;
while (fgets(line, sizeof(line), f)) {
if (line[0] == '#' || line[0] == '\n') continue;
int port = atoi(line);
if (port > 0 && port < 65536) hss_add_entry(e, n, port, NULL);
}
fclose(f);
}
#endif /* HSS_PORTS_H */

75
hss-probe.h Normal file
View file

@ -0,0 +1,75 @@
#ifndef HSS_PROBE_H
#define HSS_PROBE_H
#include <netdb.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <net/if.h>
#define TIMEOUT_SEC 2
static int hss_try_connect(const char *host, int port) {
struct addrinfo hints = {.ai_family = AF_INET, .ai_socktype = SOCK_STREAM}, *res;
char port_str[16];
int status = 0;
snprintf(port_str, sizeof(port_str), "%d", port);
if (getaddrinfo(host, port_str, &hints, &res) != 0) return 0;
int sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock >= 0) {
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK);
if (connect(sock, res->ai_addr, res->ai_addrlen) == 0) status = 1;
else if (errno == EINPROGRESS) {
fd_set fdset; FD_ZERO(&fdset); FD_SET(sock, &fdset);
struct timeval tv = {TIMEOUT_SEC, 0};
if (select(sock + 1, NULL, &fdset, NULL, &tv) > 0) {
int err = 0; socklen_t l = sizeof(err);
getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &l);
status = !err;
}
}
close(sock);
}
freeaddrinfo(res);
return status;
}
static int hss_probe_port(const char *host, int port, char *out, size_t len,
char (*ips)[INET_ADDRSTRLEN], int nips) {
if (!hss_try_connect(host, port)) return 0;
int local = !strcmp(host, "localhost") || !strcmp(host, "127.0.0.1");
if (local && nips > 0) {
size_t off = 0;
int shown = 0;
for (int i = 0; i < nips && shown < 3; i++)
if (hss_try_connect(ips[i], port))
off += snprintf(out + off, len - off, "%s%s:%d", shown++ ? ", " : "", ips[i], port);
if (shown) {
if (nips > 3) snprintf(out + off, len - off, ", +more");
return 1;
}
}
snprintf(out, len, "%s:%d", host, port);
return 1;
}
static int hss_get_eth_ips(char ips[][INET_ADDRSTRLEN], int max) {
struct ifaddrs *ifaddr, *ifa;
int count = 0;
if (getifaddrs(&ifaddr) == -1) return 0;
for (ifa = ifaddr; ifa && count < max; ifa = ifa->ifa_next) {
if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET ||
!(ifa->ifa_flags & IFF_UP) || (ifa->ifa_flags & IFF_LOOPBACK)) continue;
inet_ntop(AF_INET, &((struct sockaddr_in *)ifa->ifa_addr)->sin_addr, ips[count++], INET_ADDRSTRLEN);
}
freeifaddrs(ifaddr);
return count;
}
#endif /* HSS_PROBE_H */

60
hss-proc.h Normal file
View file

@ -0,0 +1,60 @@
#ifndef HSS_PROC_H
#define HSS_PROC_H
#include <hurd.h>
#include <hurd/process.h>
#include <mach/mach.h>
#include <stdio.h>
#include <string.h>
#define DEFAULT_PROC_LIMIT 3
static int hss_match_port_token(const char *args, const char *needle) {
size_t n = strlen(needle);
for (const char *p = args; (p = strstr(p, needle)); p++)
if ((p == args || p[-1] < '0' || p[-1] > '9') && (p[n] < '0' || p[n] > '9')) return 1;
return 0;
}
static int hss_match_process(int port, const char *args, const char *needle) {
if (port == 22 && strstr(args, "sshd")) return 1;
if ((port == 80 || port == 443) && (strstr(args, "httpd") || strstr(args, "apache") || strstr(args, "nginx") || strstr(args, "lighttpd"))) return 1;
if ((port == 21 && strstr(args, "ftp")) || (port == 5432 && strstr(args, "postgres")) || (port == 6379 && strstr(args, "redis"))) return 1;
if (port == 3306 && (strstr(args, "mysql") || strstr(args, "mariadb"))) return 1;
return (strstr(args, "python") || strstr(args, "node") || strstr(args, "java") || strstr(args, "ruby")) && hss_match_port_token(args, needle);
}
static void hss_find_pids(process_t proc, int port, char *out, size_t len, int max_procs) {
pid_t *pids = NULL;
mach_msg_type_number_t num_pids = 0;
char buf[256] = "", needle[16];
size_t off = 0;
int matches = 0;
snprintf(needle, sizeof(needle), "%d", port);
if (proc_getallpids(proc, &pids, &num_pids) != 0) { snprintf(out, len, "users:((\"unknown\",pid=0))"); return; }
for (size_t i = 0; i < num_pids; i++) {
char *args = NULL;
mach_msg_type_number_t args_len = 0;
if (proc_getprocargs(proc, pids[i], &args, &args_len) != 0 || !args || !args_len) continue;
for (size_t j = 0; j < args_len; j++) if (!args[j]) args[j] = ' ';
if (hss_match_process(port, args, needle)) {
if (max_procs > 0 && matches >= max_procs) {
snprintf(buf + off, sizeof(buf) - off, ",+more");
vm_deallocate(mach_task_self(), (vm_address_t)args, args_len);
break;
}
char *base = strrchr(args, '/'), *space;
base = base ? base + 1 : args;
if ((space = strchr(base, ' '))) *space = '\0';
off += snprintf(buf + off, sizeof(buf) - off, "%s(\"%.40s\",pid=%d)", matches ? "," : "", base, pids[i]);
if (off > sizeof(buf) - 1) off = sizeof(buf) - 1;
matches++;
}
vm_deallocate(mach_task_self(), (vm_address_t)args, args_len);
}
if (pids && num_pids) vm_deallocate(mach_task_self(), (vm_address_t)pids, num_pids * sizeof(pid_t));
if (matches) snprintf(out, len, "users:(%s)", buf);
else snprintf(out, len, "users:((\"unknown\",pid=0))");
}
#endif /* HSS_PROC_H */

282
hss.c
View file

@ -1,217 +1,93 @@
#!/usr/bin/tcc -run
/* hss: Hurd Socket Stat, GPLv3 licensed, originally written by MARKMENTAL
* Made possible thanks to Debian, TCC and GNU. Condensed with the assistance of Kimi */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <hurd.h>
#include <hurd/paths.h>
#include <hurd/lookup.h>
#include <hurd/process.h>
#include <mach/mach.h>
/* hss: Hurd Socket Stat, GPLv3 licensed, originally written by MARKMENTAL */
#define TIMEOUT_SEC 2
#define DEFAULT_PROC_LIMIT 3
#define MAX_PORTS 256
#define CONFIG_PATH "~/.config/.hssrc"
#include "hss.h"
static const int modern[] = {8080, 8443, 3000, 5000, 8000, 8888, 9000, 9090, 1337, 27017, 0};
struct port_entry { int port; char service_name[64]; };
static int is_common_port(int port) {
if (port >= 1 && port <= 1023) return 1;
for (int i = 0; modern[i]; i++)
if (modern[i] == port) return 1;
return 0;
}
static void print_help(void) {
/* Append port to list in sorted order, skipping duplicates; name NULL -> use port number */
static void add_entry(struct port_entry *e, int *n, int port, const char *name) {
for (int i = 0; i < *n; i++)
if (e[i].port == port) return;
if (*n >= MAX_PORTS) return;
int i = (*n)++;
while (i > 0 && e[i - 1].port > port) { e[i] = e[i - 1]; i--; }
e[i].port = port;
if (name) snprintf(e[i].service_name, sizeof(e[i].service_name), "%.20s", name);
else snprintf(e[i].service_name, sizeof(e[i].service_name), "%d", port);
}
static void parse_etc_services(struct port_entry *e, int *n) {
FILE *f = fopen("/etc/services", "r");
if (!f) return;
char line[256], name[64], proto[16];
int start, end;
while (fgets(line, sizeof(line), f)) {
if (line[0] == '#' || line[0] == '\n') continue;
end = -1;
if (sscanf(line, "%63s %d-%d/%15s", name, &start, &end, proto) < 4 &&
sscanf(line, "%63s %d/%15s", name, &start, proto) != 3) continue;
if (end < 0) end = start;
if (strstr(proto, "6")) continue;
if (end - start + 1 > 20) end = start + 19;
for (int p = start; p <= end; p++)
if (p > 0 && p < 65536 && is_common_port(p)) add_entry(e, n, p, name);
}
fclose(f);
}
static void parse_config_file(const char *path, struct port_entry *e, int *n) {
char expanded[256], line[256];
if (path[0] == '~') {
if (!getenv("HOME")) return;
snprintf(expanded, sizeof(expanded), "%s%s", getenv("HOME"), path + 1);
path = expanded;
}
FILE *f = fopen(path, "r");
if (!f) return;
while (fgets(line, sizeof(line), f)) {
if (line[0] == '#' || line[0] == '\n') continue;
int port = atoi(line);
if (port > 0 && port < 65536) add_entry(e, n, port, NULL);
}
fclose(f);
}
static int get_all_ethernet_ips(char ips[][INET_ADDRSTRLEN], int max) {
struct ifaddrs *ifaddr, *ifa;
int count = 0;
if (getifaddrs(&ifaddr) == -1) return 0;
for (ifa = ifaddr; ifa && count < max; ifa = ifa->ifa_next) {
if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET ||
!(ifa->ifa_flags & IFF_UP) || (ifa->ifa_flags & IFF_LOOPBACK)) continue;
inet_ntop(AF_INET, &((struct sockaddr_in *)ifa->ifa_addr)->sin_addr, ips[count++], INET_ADDRSTRLEN);
}
freeifaddrs(ifaddr);
return count;
}
static int try_connect(const char *host, int port) {
struct addrinfo hints = {.ai_family = AF_INET, .ai_socktype = SOCK_STREAM}, *res;
char port_str[16];
int status = 0;
snprintf(port_str, sizeof(port_str), "%d", port);
if (getaddrinfo(host, port_str, &hints, &res) != 0) return 0;
int sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock >= 0) {
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK);
if (connect(sock, res->ai_addr, res->ai_addrlen) == 0) status = 1;
else if (errno == EINPROGRESS) {
fd_set fdset; FD_ZERO(&fdset); FD_SET(sock, &fdset);
struct timeval tv = {TIMEOUT_SEC, 0};
if (select(sock + 1, NULL, &fdset, NULL, &tv) > 0) {
int err = 0; socklen_t l = sizeof(err);
getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &l);
status = !err;
}
}
close(sock);
}
freeaddrinfo(res);
return status;
}
static int probe_port(const char *host, int port, char *out, size_t len, char (*ips)[INET_ADDRSTRLEN], int nips) {
if (!try_connect(host, port)) return 0;
int local = !strcmp(host, "localhost") || !strcmp(host, "127.0.0.1");
if (local && nips > 0) {
size_t off = 0;
int shown = 0;
for (int i = 0; i < nips && shown < 3; i++)
if (try_connect(ips[i], port))
off += snprintf(out + off, len - off, "%s%s:%d", shown++ ? ", " : "", ips[i], port);
if (shown) {
if (nips > 3) snprintf(out + off, len - off, ", +more");
return 1;
}
}
snprintf(out, len, "%s:%d", host, port);
return 1;
}
static int has_port_token(const char *args, const char *needle) {
size_t n = strlen(needle);
for (const char *p = args; (p = strstr(p, needle)); p++)
if ((p == args || p[-1] < '0' || p[-1] > '9') && (p[n] < '0' || p[n] > '9')) return 1;
return 0;
}
static int is_process_match(int port, const char *args, const char *needle) {
if (port == 22 && strstr(args, "sshd")) return 1;
if ((port == 80 || port == 443) && (strstr(args, "httpd") || strstr(args, "apache") || strstr(args, "nginx") || strstr(args, "lighttpd"))) return 1;
if ((port == 21 && strstr(args, "ftp")) || (port == 5432 && strstr(args, "postgres")) || (port == 6379 && strstr(args, "redis"))) return 1;
if (port == 3306 && (strstr(args, "mysql") || strstr(args, "mariadb"))) return 1;
return (strstr(args, "python") || strstr(args, "node") || strstr(args, "java") || strstr(args, "ruby")) && has_port_token(args, needle);
}
static void find_pids_for_port_hurd(process_t proc, int port, char *out, size_t len, int max_procs) {
pid_t *pids = NULL;
mach_msg_type_number_t num_pids = 0;
char buf[256] = "", needle[16];
size_t off = 0;
int matches = 0;
snprintf(needle, sizeof(needle), "%d", port);
if (proc_getallpids(proc, &pids, &num_pids) != 0) { snprintf(out, len, "users:((\"unknown\",pid=0))"); return; }
for (size_t i = 0; i < num_pids; i++) {
char *args = NULL;
mach_msg_type_number_t args_len = 0;
if (proc_getprocargs(proc, pids[i], &args, &args_len) != 0 || !args || !args_len) continue;
for (size_t j = 0; j < args_len; j++) if (!args[j]) args[j] = ' ';
if (is_process_match(port, args, needle)) {
if (max_procs > 0 && matches >= max_procs) {
snprintf(buf + off, sizeof(buf) - off, ",+more");
vm_deallocate(mach_task_self(), (vm_address_t)args, args_len);
break;
}
char *base = strrchr(args, '/'), *space;
base = base ? base + 1 : args;
if ((space = strchr(base, ' '))) *space = '\0';
off += snprintf(buf + off, sizeof(buf) - off, "%s(\"%.40s\",pid=%d)", matches ? "," : "", base, pids[i]);
if (off > sizeof(buf) - 1) off = sizeof(buf) - 1;
matches++;
}
vm_deallocate(mach_task_self(), (vm_address_t)args, args_len);
}
if (pids && num_pids) vm_deallocate(mach_task_self(), (vm_address_t)pids, num_pids * sizeof(pid_t));
if (matches) snprintf(out, len, "users:(%s)", buf);
else snprintf(out, len, "users:((\"unknown\",pid=0))");
printf("Usage: %s [target] [options]\n"
"\n"
"Heuristic socket-table emulator for GNU/Hurd, mimicking ss/netstat.\n"
"\n"
" target host to scan (default: localhost)\n"
" --all-processes show all processes for each listening port\n"
" --process-limit=N limit processes shown per port (default: 3)\n"
" --version print version and exit\n"
" --help print this help and exit\n"
"\n"
"Scans curated common ports (from /etc/services and a built-in list),\n"
"probes each with a non-blocking TCP connect, and identifies owning\n"
"processes by inspecting the Mach proc server.\n", APP_NAME);
}
int main(int argc, char *argv[]) {
const char *target = (argc > 1) ? argv[1] : "localhost";
int proc_limit = DEFAULT_PROC_LIMIT;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--all-processes")) proc_limit = 0;
else if (!strncmp(argv[i], "--process-limit=", 16)) proc_limit = atoi(argv[i] + 16);
if (!strcmp(argv[i], "--version")) {
printf("%s %s\n", APP_NAME, APP_VERSION);
return 0;
}
else if (!strcmp(argv[i], "--help")) {
print_help();
return 0;
}
else if (!strcmp(argv[i], "--all-processes")) {
proc_limit = 0;
}
else if (!strncmp(argv[i], "--process-limit=", 16)) {
proc_limit = atoi(argv[i] + 16);
}
}
static struct port_entry ports[MAX_PORTS];
/* Unlike Linux `ss` (which reads a kernel socket table via netlink), Hurd
* has no centralized socket table, so we build a candidate port list
* heuristically and probe each with a TCP connect. */
static struct hss_port_entry ports[MAX_PORTS];
int num_ports = 0;
parse_etc_services(ports, &num_ports);
for (int i = 0; modern[i]; i++) add_entry(ports, &num_ports, modern[i], NULL);
parse_config_file(CONFIG_PATH, ports, &num_ports);
char eth_ips[10][INET_ADDRSTRLEN];
int num_eth_ips = get_all_ethernet_ips(eth_ips, 10);
process_t proc_server = getproc();
if (proc_server == MACH_PORT_NULL) { fprintf(stderr, "hss: Error fetching Hurd proc server port via getproc()\n"); return 1; }
file_t sock_port = file_name_lookup(_SERVERS_SOCKET "/2", 0, 0);
if (sock_port == MACH_PORT_NULL) fprintf(stderr, "hss: Warning - IPv4 socket translator not responding\n");
else mach_port_deallocate(mach_task_self(), sock_port);
printf("%-8s %-12s %-30s %s\n", "Netid", "State", "Local Address:Port", "Process");
for (int i = 0; i < num_ports; i++) {
char local_addr[256], status[256];
if (!probe_port(target, ports[i].port, local_addr, sizeof(local_addr), eth_ips, num_eth_ips)) continue;
find_pids_for_port_hurd(proc_server, ports[i].port, status, sizeof(status), proc_limit);
printf("%-8s %-12s %-30s %s\n", "tcp", "LISTEN", local_addr, status);
hss_parse_services(ports, &num_ports);
for (int i = 0; hss_modern_ports[i]; i++) {
hss_add_entry(ports, &num_ports, hss_modern_ports[i], NULL);
}
mach_port_deallocate(mach_task_self(), proc_server);
hss_parse_config(CONFIG_PATH, ports, &num_ports);
/* On Linux, `ss` reads bound interface addresses from the kernel. We
* enumerate the machine's ethernet IPs to replicate that per-interface
* "Local Address:Port" column ourselves. */
char eth_ips[10][INET_ADDRSTRLEN];
int num_eth_ips = hss_get_eth_ips(eth_ips, 10);
/* getproc() returns a Mach port handle to the Hurd process server — a
* userspace translator, not a kernel syscall. Used below to enumerate
* every running process. */
process_t proc_server = getproc();
if (proc_server == MACH_PORT_NULL) {
fprintf(stderr, "%s: Error fetching Hurd proc server port via getproc()\n", APP_NAME);
return 1;
}
printf("%-8s %-12s %-30s %s\n", "Netid", "State", "Local Address:Port", "Process");
/* For each candidate: attempt a non-blocking TCP connect. If it succeeds,
* something is listening. Then crawl the proc server to match owning PIDs
* by inspecting each process's command line against known service names
* (sshd, nginx, ...) or the port number itself. */
for (int i = 0; i < num_ports; i++) {
char local_addr[256], proc_status[256];
if (!hss_probe_port(target, ports[i].port, local_addr, sizeof(local_addr),
eth_ips, num_eth_ips)) {
continue;
}
hss_find_pids(proc_server, ports[i].port, proc_status, sizeof(proc_status), proc_limit);
printf("%-8s %-12s %-30s %s\n", "tcp", "LISTEN", local_addr, proc_status);
}
return 0;
}

11
hss.h Normal file
View file

@ -0,0 +1,11 @@
#ifndef HSS_H
#define HSS_H
#define APP_NAME "hss"
#define APP_VERSION "0.1"
#include "hss-ports.h"
#include "hss-probe.h"
#include "hss-proc.h"
#endif /* HSS_H */