diff --git a/README b/README deleted file mode 100644 index 819d010..0000000 --- a/README +++ /dev/null @@ -1,2 +0,0 @@ -This is basically a clone of socketstat (specifically the ss -tulnp behavior), but for Debian GNU/Hurd -It uses tcc for now, nice light compiler diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8bba10 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# hss - Hurd Socket Stat + +> A practical, heuristics-based socket table emulator for GNU/Hurd that mimics modern Linux `ss` and `netstat` behavior. + +## Architectural Motivation + +On monolithic kernels like Linux, `ss` queries a centralized kernel-space socket array via Netlink (`sock_diag`). GNU/Hurd has no monolithic kernel — the network stack and tasks are isolated user-space translators. A single global "socket-to-PID" lookup table does not exist by design. `hss` emulates this missing table from user space. + +## Features + +- **Non-blocking heuristics**: Lightning-fast connection scans using `O_NONBLOCK` and `select()` across interfaces +- **Low-level Mach/Hurd inspection**: Crawls the Mach process server using `proc_getallpids` and `proc_getprocargs` +- **Zero memory leaks**: Explicit `vm_deallocate` for all Mach page memory +- **Fast runtime**: Resolves the entire user-space distributed map in ~0.320 seconds +- **Dual compiler support**: Compiles with both `tcc` (rapid development) and `gcc` (production) + +## Sample Output + +``` +Netid State Local Address:Port Process +tcp LISTEN 192.168.86.42:22 users:(("sshd-session",pid=13635),...) +tcp LISTEN localhost:8000 users:(("python3",pid=14434)) +``` + +## Usage + +```bash +hss [target] [options] +``` + +### Options + +| Option | Description | +|--------|-------------| +| `target` | Host to scan (default: localhost) | +| `--all-processes` | Show all processes for each port | +| `--process-limit=N` | Limit processes shown per port (default: 3) | + +### Examples + +```bash +# Scan localhost with default settings +hss + +# Scan remote host +hss 192.168.1.100 + +# Show all processes +hss localhost --all-processes + +# Limit to 5 processes per port +hss localhost --process-limit=5 +``` + +## Compilation & Build + +### Dependencies + +- `libc6-dev` (standard C library) +- `hurd-dev` (Hurd system headers) +- `gcc` or `tcc` (compiler) + +### GCC (Production) +```bash +gcc -Wall -Wextra -O2 -o hss hss.c +``` + +### TCC (Development) +```bash +tcc -run hss.c +``` + +## Debian Package Target + +Planned for official inclusion with a standard `debian/` directory targeting `Architecture: hurd-any`. + +## License + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program. If not, see . + +## Author + +Mark Robillard Jr (MARKMENTAL) diff --git a/hss.c b/hss.c index 73a525f..304ec83 100755 --- a/hss.c +++ b/hss.c @@ -32,9 +32,153 @@ #define DEFAULT_PROC_LIMIT 3 #define MAX_ETH_IPS 10 #define MAX_DISPLAY_IPS 3 +#define MAX_PORTS 256 +#define CONFIG_PATH "~/.config/.hssrc" +#define MAX_SERVICE_NAME_LEN 20 +#define MAX_RANGE_SIZE 20 -static int initial_ports[] = {22, 80, 443, 21, 3306, 5432, 6379, 8080, 1337}; -#define INITIAL_PORTS_LEN (sizeof(initial_ports) / sizeof(initial_ports[0])) +static const int common_modern_ports[] = { + 8080, 8443, 3000, 5000, 8000, 8888, 9000, 9090, 1337, 27017 +}; +#define NUM_MODERN_PORTS (sizeof(common_modern_ports) / sizeof(common_modern_ports[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 (size_t i = 0; i < NUM_MODERN_PORTS; i++) { + if (common_modern_ports[i] == port) return 1; + } + return 0; +} + +static int get_common_modern_ports(struct port_entry *entries, int max_entries) { + int count = 0; + for (size_t i = 0; i < NUM_MODERN_PORTS && count < max_entries; i++) { + entries[count].port = common_modern_ports[i]; + snprintf(entries[count].service_name, sizeof(entries[count].service_name), + "%d", common_modern_ports[i]); + count++; + } + return count; +} + +static int parse_etc_services(struct port_entry *entries, int max_entries) { + FILE *f = fopen("/etc/services", "r"); + if (!f) return 0; + char line[256]; + int count = 0; + while (fgets(line, sizeof(line), f) && count < max_entries) { + if (line[0] == '#' || line[0] == '\n') continue; + char name[64]; + int port_start, port_end; + char proto[16]; + if (sscanf(line, "%63s %d-%d/%15s", name, &port_start, &port_end, proto) == 4) { + if (strstr(proto, "6")) continue; + if (port_end - port_start + 1 > MAX_RANGE_SIZE) { + port_end = port_start + MAX_RANGE_SIZE - 1; + } + for (int p = port_start; p <= port_end && count < max_entries; p++) { + if (p > 0 && p < 65536 && is_common_port(p)) { + entries[count].port = p; + if (strlen(name) > MAX_SERVICE_NAME_LEN) { + strncpy(entries[count].service_name, name, MAX_SERVICE_NAME_LEN - 3); + entries[count].service_name[MAX_SERVICE_NAME_LEN - 3] = '.'; + entries[count].service_name[MAX_SERVICE_NAME_LEN - 2] = '.'; + entries[count].service_name[MAX_SERVICE_NAME_LEN - 1] = '.'; + entries[count].service_name[MAX_SERVICE_NAME_LEN] = '\0'; + } else { + strncpy(entries[count].service_name, name, MAX_SERVICE_NAME_LEN); + entries[count].service_name[MAX_SERVICE_NAME_LEN] = '\0'; + } + count++; + } + } + } else if (sscanf(line, "%63s %d/%15s", name, &port_start, proto) == 3) { + if (strstr(proto, "6")) continue; + if (port_start > 0 && port_start < 65536 && is_common_port(port_start)) { + entries[count].port = port_start; + if (strlen(name) > MAX_SERVICE_NAME_LEN) { + strncpy(entries[count].service_name, name, MAX_SERVICE_NAME_LEN - 3); + entries[count].service_name[MAX_SERVICE_NAME_LEN - 3] = '.'; + entries[count].service_name[MAX_SERVICE_NAME_LEN - 2] = '.'; + entries[count].service_name[MAX_SERVICE_NAME_LEN - 1] = '.'; + entries[count].service_name[MAX_SERVICE_NAME_LEN] = '\0'; + } else { + strncpy(entries[count].service_name, name, MAX_SERVICE_NAME_LEN); + entries[count].service_name[MAX_SERVICE_NAME_LEN] = '\0'; + } + count++; + } + } + } + fclose(f); + return count; +} + +static int parse_config_file(const char *path, struct port_entry *entries, int max_entries) { + char expanded_path[256]; + if (path[0] == '~') { + const char *home = getenv("HOME"); + if (!home) return 0; + snprintf(expanded_path, sizeof(expanded_path), "%s%s", home, path + 1); + path = expanded_path; + } + FILE *f = fopen(path, "r"); + if (!f) return 0; + char line[256]; + int count = 0; + while (fgets(line, sizeof(line), f) && count < max_entries) { + if (line[0] == '#' || line[0] == '\n') continue; + int port = atoi(line); + if (port > 0 && port < 65536) { + entries[count].port = port; + snprintf(entries[count].service_name, sizeof(entries[count].service_name), "custom-%d", port); + count++; + } + } + fclose(f); + return count; +} + +static int merge_port_lists(struct port_entry *etc_services, int num_etc, + struct port_entry *modern, int num_modern, + struct port_entry *custom, int num_custom, + struct port_entry *merged, int max_merged) { + int count = 0; + int seen[65536] = {0}; + for (int i = 0; i < num_etc && count < max_merged; i++) { + if (!seen[etc_services[i].port]) { + merged[count++] = etc_services[i]; + seen[etc_services[i].port] = 1; + } + } + for (int i = 0; i < num_modern && count < max_merged; i++) { + if (!seen[modern[i].port]) { + merged[count++] = modern[i]; + seen[modern[i].port] = 1; + } + } + for (int i = 0; i < num_custom && count < max_merged; i++) { + if (!seen[custom[i].port]) { + merged[count++] = custom[i]; + seen[custom[i].port] = 1; + } + } + for (int i = 0; i < count - 1; i++) { + for (int j = i + 1; j < count; j++) { + if (merged[j].port < merged[i].port) { + struct port_entry tmp = merged[i]; + merged[i] = merged[j]; + merged[j] = tmp; + } + } + } + return count; +} static int get_all_ethernet_ips(char ips[][INET_ADDRSTRLEN], int max_ips) { struct ifaddrs *ifaddr, *ifa; @@ -195,9 +339,18 @@ int main(int argc, char *argv[]) { 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); + struct port_entry etc_services[MAX_PORTS]; + int num_etc = parse_etc_services(etc_services, MAX_PORTS); + struct port_entry modern[MAX_PORTS]; + int num_modern = get_common_modern_ports(modern, MAX_PORTS); + struct port_entry custom[MAX_PORTS]; + int num_custom = parse_config_file(CONFIG_PATH, custom, MAX_PORTS); + struct port_entry merged[MAX_PORTS]; + int num_merged = merge_port_lists(etc_services, num_etc, modern, num_modern, + custom, num_custom, merged, MAX_PORTS); printf("%-8s %-12s %-30s %s\n", "Netid", "State", "Local Address:Port", "Process"); - for (size_t i = 0; i < INITIAL_PORTS_LEN; i++) { - int port = initial_ports[i]; + for (int i = 0; i < num_merged; i++) { + int port = merged[i].port; if (!probe_port(target, port, local_addr, sizeof(local_addr), eth_ips, num_eth_ips)) continue; find_pids_for_port_hurd(proc_server, port, status, sizeof(status), proc_limit); printf("%-8s %-12s %-30s %s\n", "tcp", "LISTEN", local_addr, status);