commit 2f198dcdd76acabb0da5d47dd0ed9349e021dec0
parent 0157c674bc97f02949fa8557d2d34577448c0a4e
Author: Remy Noulin <loader2x@gmail.com>
Date: Wed, 4 Jan 2023 12:05:43 +0200
add secure spartan server
README.md | 22 +-
build.sh | 6 +-
spartservPrivDrop.c | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 588 insertions(+), 4 deletions(-)
Diffstat:
3 files changed, 588 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
@@ -11,7 +11,7 @@ apt-get install gcc
./build.sh
```
-`spartserv.c` is a server handling one request at a time. The configurations for the hostname and server root are on the top of `spartserv.c`.
+`spartserv.c` is a server handling one request at a time and serves static pages. The configurations for the hostname and server root are on the top of `spartserv.c`.
By default, `spartserv` opens port 3000 and serves on hostname `localhost`.
@@ -40,3 +40,23 @@ The line type `=:` is not supported, to upload data run `sparline` with the URL
```
./sparline spartan://hostname/path --infile afile.txt
```
+
+# Running on port 300
+The default port for spartan is port 300 and in general only root processes can open a listening socket on port under 1024.
+
+`spartservPrivDrop` starts as a root and open port 300 and then chroot and drops the privileges.
+
+Only necessary syscalls are allowed.
+
+Create a user `spartserv`, the server will run as this user:
+```
+# as root
+adduser spartserv
+```
+
+In `spartservPrivDrop.c`, set the hostname and the chroot directory, this is the directory being served.
+Compile and run:
+```
+gcc -std=gnu11 -g3 spartservPrivDrop.c -o spartserv
+./spartserv
+```
diff --git a/build.sh b/build.sh
@@ -1,6 +1,6 @@
-gcc -g3 spartserv.c -o spartserv
-gcc -g3 spartclient.c -o spartclient
-gcc -g3 sparline.c -o sparline
+gcc -std=gnu11 -g3 spartserv.c -o spartserv
+gcc -std=gnu11 -g3 spartclient.c -o spartclient
+gcc -std=gnu11 -g3 sparline.c -o sparline
echo Run to start spartan server:
diff --git a/spartservPrivDrop.c b/spartservPrivDrop.c
@@ -0,0 +1,564 @@
+/*
+This server creates a socket listening to PORT and
+starts the event loop
+2 mimetypes are set depending on file extension:
+text/gemini .gmi
+text/markdown .md .markdown
+*/
+
+#define _GNU_SOURCE
+#include <string.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <stdarg.h>
+#include <limits.h>
+#include <time.h>
+
+// inet_ntoa
+//already included #include <sys/socket.h>
+//already included #include <netinet/in.h>
+#include <arpa/inet.h>
+
+// privilege drop
+// already included #include <sys/types.h>
+#include <pwd.h>
+#include <grp.h>
+
+// seccomp bpf filters
+#include <sys/syscall.h>
+#include <errno.h>
+#include <stddef.h>
+#include <linux/seccomp.h>
+#include <linux/filter.h> // struct sock_filter
+#include <linux/audit.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+
+// server configuration
+#define HOSTNAME "localhost"
+#define ROOT "/"
+#define PORT 300
+
+#define RUNAS "spartserv"
+#define CHROOT "."
+
+// ----------------- seccomp setup
+// This code is copied and modified from kore http server
+// https://kore.io
+/*
+ * Copyright (c) 2013-2022 Joris Vink <joris@coders.se>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define ARGS_LO_OFFSET 0
+#define ARGS_HI_OFFSET sizeof(u_int32_t)
+#elif __BYTE_ORDER == __BIG_ENDIAN
+#define ARGS_LO_OFFSET sizeof(u_int32_t)
+#define ARGS_HI_OFFSET 0
+#else
+#error "__BYTE_ORDER unknown"
+#endif
+
+// AUDIT_ARCH_X86_64 AUDIT_ARCH_ARM AUDIT_ARCH_AARCH64
+#define SECCOMP_AUDIT_ARCH AUDIT_ARCH_X86_64
+#define SECCOMP_KILL_POLICY SECCOMP_RET_KILL
+
+/* Load field of seccomp_data into accumulator. */
+#define KORE_BPF_LOAD(_field, _off) \
+ BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, _field) + _off)
+/* Compare the accumulator against a constant (==). */
+#define KORE_BPF_CMP(_k, _jt, _jf) \
+ BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, _k, _jt, _jf)
+
+/* Return a constant from a BPF program. */
+#define KORE_BPF_RET(_retval) \
+ BPF_STMT(BPF_RET+BPF_K, _retval)
+
+/* AND operation on the accumulator. */
+#define KORE_BPF_AND(_k) \
+ BPF_STMT(BPF_ALU+BPF_AND+BPF_K, _k)
+
+/* The length of a filter. */
+#define KORE_FILTER_LEN(x) (sizeof(x) / sizeof(x[0]))
+
+#define KORE_SYSCALL_FILTER(_name, _action) \
+ KORE_BPF_CMP(SYS_##_name, 0, 1), \
+ KORE_BPF_RET(_action)
+
+#define KORE_SYSCALL_WITH_FLAG(_name, _arg, _flag, _action) \
+ KORE_BPF_CMP(SYS_##_name, 0, 8), \
+ KORE_BPF_LOAD(args[(_arg)], ARGS_LO_OFFSET), \
+ KORE_BPF_AND(((_flag) & 0xffffffff)), \
+ KORE_BPF_CMP(((_flag) & 0xffffffff), 0, 4), \
+ KORE_BPF_LOAD(args[(_arg)], ARGS_HI_OFFSET), \
+ KORE_BPF_AND((((uint32_t)((uint64_t)(_flag) >> 32)) & 0xffffffff)), \
+ KORE_BPF_CMP((((uint32_t)((uint64_t)(_flag) >> 32)) & 0xffffffff), 0, 1), \
+ KORE_BPF_RET(_action), \
+ KORE_BPF_LOAD(nr, 0)
+
+#define KORE_SYSCALL_DENY(_name, _errno) \
+ KORE_SYSCALL_FILTER(_name, SECCOMP_RET_ERRNO|(_errno))
+
+#define KORE_SYSCALL_DENY_WITH_FLAG(_name, _arg, _flag, _errno) \
+ KORE_SYSCALL_WITH_FLAG(_name, _arg, _flag, SECCOMP_RET_ERRNO|(_errno))
+
+#define KORE_SYSCALL_ALLOW(_name) \
+ KORE_SYSCALL_FILTER(_name, SECCOMP_RET_ALLOW)
+
+
+/*
+ * The bare minimum to be able to run kore. These are added last and can
+ * be overwritten by a filter program that is added before hand.
+ */
+static struct sock_filter filter_kore[] = {
+ /* Deny these, but with EACCESS instead of dying. */
+ KORE_SYSCALL_DENY(ioctl, EACCES),
+
+ /* File related. */
+ KORE_SYSCALL_ALLOW(read),
+#if defined(SYS_stat)
+ KORE_SYSCALL_ALLOW(stat),
+#endif
+#if defined(SYS_lstat)
+ KORE_SYSCALL_ALLOW(lstat),
+#endif
+ KORE_SYSCALL_ALLOW(fstat),
+ KORE_SYSCALL_ALLOW(write),
+ KORE_SYSCALL_ALLOW(close),
+ KORE_SYSCALL_ALLOW(openat),
+#if defined(SYS_send)
+ KORE_SYSCALL_ALLOW(send),
+#endif
+ KORE_SYSCALL_ALLOW(sendto),
+ KORE_SYSCALL_ALLOW(accept),
+ KORE_SYSCALL_ALLOW(sendfile),
+#if defined(SYS_recv)
+ KORE_SYSCALL_ALLOW(recv),
+#endif
+ KORE_SYSCALL_ALLOW(recvfrom),
+ KORE_SYSCALL_ALLOW(setsockopt),
+};
+
+/* bpf program prologue. */
+static struct sock_filter filter_prologue[] = {
+ /* Load arch member into accumulator (A) (arch is __u32). */
+ KORE_BPF_LOAD(arch, 0),
+
+ /* Compare accumulator against constant, if false jump over kill. */
+ KORE_BPF_CMP(SECCOMP_AUDIT_ARCH, 1, 0),
+ KORE_BPF_RET(SECCOMP_RET_KILL),
+
+ /* Load the system call number into the accumulator. */
+ KORE_BPF_LOAD(nr, 0),
+};
+
+/* bpf program epilogue. */
+static struct sock_filter filter_epilogue[] = {
+ /* Return hit if no system calls matched our list. */
+ BPF_STMT(BPF_RET+BPF_K, SECCOMP_KILL_POLICY)
+};
+
+#define filter_prologue_len KORE_FILTER_LEN(filter_prologue)
+#define filter_epilogue_len KORE_FILTER_LEN(filter_epilogue)
+
+int kore_seccomp_tracing = 0;
+
+void kore_seccomp_enable(void) {
+ struct sock_filter *sf;
+ struct sock_fprog prog;
+ size_t prog_len, off, i;
+
+ /*
+ * If kore_seccomp_tracing is turned on, set the default policy to
+ * SECCOMP_RET_TRACE so we can log the system calls.
+ */
+ if (kore_seccomp_tracing) {
+ filter_epilogue[0].k = SECCOMP_RET_TRACE;
+ puts("seccomp tracing enabled");
+ }
+
+ /* Start with the prologue. */
+ /* Finally add the epilogue. */
+ prog_len = filter_prologue_len + KORE_FILTER_LEN(filter_kore) + filter_epilogue_len ;
+
+ /* Build the entire bpf program now. */
+ if ((sf = calloc(prog_len, sizeof(*sf))) == NULL) {
+ puts("calloc");
+ exit(1);
+ }
+
+ off = 0;
+ for (i = 0; i < filter_prologue_len; i++)
+ sf[off++] = filter_prologue[i];
+
+ for (i = 0; i < KORE_FILTER_LEN(filter_kore); i++)
+ sf[off++] = filter_kore[i];
+
+ for (i = 0; i < filter_epilogue_len; i++)
+ sf[off++] = filter_epilogue[i];
+
+ /* Lock and load it. */
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
+ printf("prctl: %s\n", strerror(errno));
+ exit(1);
+ }
+
+ prog.filter = sf;
+ prog.len = prog_len;
+
+ if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) {
+ printf("prctl: %s\n", strerror(errno));
+ exit(1);
+ }
+}
+
+// ----------------- seccomp end
+
+bool isDir(const char *path) {
+ struct stat st;
+
+ if (stat(path, &st) == -1) {
+ return(false);
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ return(false);
+ }
+ return(true);
+}
+
+int main(int ac, char **av){
+ int sock;
+ struct sockaddr_in server;
+ int mysock;
+ char buf[4096];
+ int rval;
+
+ char root[PATH_MAX] = {0};
+ if (ROOT[0] == '/') {
+ realpath(ROOT, root);
+ }
+ else {
+ // create absolute path from relative ROOT path
+ char p[PATH_MAX] = {0};
+ getcwd(p, PATH_MAX);
+ strcat(p, "/");
+ strcat(p, ROOT);
+ realpath(p, root);
+ strcat(root, "/");
+ }
+
+ size_t rootLen = strlen(root);
+ size_t slash = 0;
+
+ // count slashes at the end of root
+ // to compare paths with memcmp correctly
+ // since realpath removes the slashes at the
+ // end of the path from client.
+ while(root[rootLen-1-slash] == '/') {
+ slash++;
+ }
+
+ sock = socket(AF_INET, SOCK_STREAM, 0);
+ if (sock < 0){
+ perror("Failed to create socket");
+ }
+
+ server.sin_family = AF_INET;
+ server.sin_addr.s_addr = INADDR_ANY;
+ server.sin_port = htons(PORT);
+
+ /* setsockopt: Handy debugging trick that lets
+ * us rerun the server immediately after we kill it;
+ * otherwise we have to wait about 20 secs.
+ * Eliminates "ERROR on binding: Address already in use" error.
+ */
+ int optval = 1;
+ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int));
+
+ if (bind(sock, (struct sockaddr *) &server, sizeof(server))){
+ perror("bind failed");
+ exit(1);
+ }
+
+ listen(sock, SOMAXCONN);
+
+ struct passwd *pw = NULL;
+ pw = getpwnam(RUNAS);
+ if (!pw) {
+ perror("get user info failed");
+ exit(1);
+ }
+
+ if (chroot(CHROOT) == -1) {
+ perror("cannot chroot");
+ exit(1);
+ }
+
+ if (chdir("/") == -1) {
+ perror("cannot chdir");
+ exit(1);
+ }
+
+ if (setgroups(1, &pw->pw_gid) ||
+ setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
+ setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) {
+ printf("cannot drop privileges");
+ exit(1);
+ }
+
+ kore_seccomp_enable();
+
+ printf("Serving "HOSTNAME":%d %s\n", PORT, root);
+
+ // date for request print
+ char date[50];
+ do {
+ // struct for printing client ip in terminal with inet_ntoa
+ struct sockaddr_in addr;
+ socklen_t len = sizeof(addr);
+ mysock = accept(sock, &addr, &len);
+ if (mysock == -1)
+ perror("accept failed");
+ else {
+ // Set 10s timeouts for receive and send (SO_RCVTIMEO and SO_SNDTIMEO)
+ struct timeval timeout;
+ timeout.tv_sec = 10;
+ timeout.tv_usec = 0;
+ if (setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
+ perror("receive timeout failed");
+ close(mysock);
+ continue;
+ }
+ if (setsockopt(mysock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
+ perror("send timeout failed");
+ close(mysock);
+ continue;
+ }
+
+ // new client
+ memset(buf, 0, sizeof(buf));
+ if ((rval = recv(mysock, buf, sizeof(buf), 0)) < 0) {
+ perror("reading message");
+ close(mysock);
+ continue;
+ }
+ else if (rval == 0) {
+ puts("Ending connection");
+ close(mysock);
+ continue;
+ }
+
+ // get YMD HMS date
+ time_t clk = time(NULL);
+ struct tm *pClk = localtime(&clk);
+ strftime(date, sizeof(date), "%Y-%m-%d:%H:%M:%S", pClk);
+ printf("%s %s ",date, inet_ntoa(addr.sin_addr));
+
+ // validate request then scan hostname path and content length
+
+ // find request end
+ char *reqEnd = memmem(buf, sizeof(buf), "\r\n", 2);
+ if (!reqEnd || buf[0] == ' ') {
+ puts("4 Invalid request");
+ // add MSG_NOSIGNAL flag to ignore SIGPIPE when the client closes the socket early
+ send(mysock, "4 Invalid request\r\n", sizeof("4 Invalid request\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+
+ // check ascii
+ char *cursor = buf;
+ bool isBad = false;
+ while (cursor < reqEnd) {
+ if (*cursor < 32 || *cursor == 127) {
+ isBad = true;
+ break;
+ }
+ cursor++;
+ }
+ if (isBad) {
+ puts("4 Non ASCII");
+ send(mysock, "4 Non ASCII\r\n", sizeof("4 Non ASCII\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+
+ // print request in terminal
+ *reqEnd = 0;
+ printf("%s ", buf);
+ *reqEnd = '\r';
+
+ // parse hostname, path and content_length in request
+ char *hostname;
+ char *path;
+ char *content_length;
+
+ hostname = buf;
+
+ // hostname must match HOSTNAME
+ // comment out this test to accept nay hostname
+ int c = memcmp(hostname, HOSTNAME, strlen(HOSTNAME));
+
+ if (c != 0) {
+ puts("4 Hostname");
+ send(mysock, "4 Hostname\r\n", sizeof("4 Hostname\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+
+ // get path
+ cursor = buf;
+ while (*cursor != ' ' && cursor < reqEnd) {
+ cursor++;
+ }
+
+ cursor++;
+ if (cursor >= reqEnd || *cursor == ' ') {
+ puts("4 Path");
+ send(mysock, "4 Path\r\n", sizeof("4 Path\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+
+ path = cursor;
+
+ // get content_length
+ while (*cursor != ' ' && cursor < reqEnd) {
+ cursor++;
+ }
+
+ cursor++;
+ if (cursor >= reqEnd || *cursor == ' ') {
+ puts("4 Length");
+ send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+
+ content_length = cursor;
+ while(cursor < reqEnd) {
+ if (!isdigit(*cursor)) {
+ isBad = true;
+ break;
+ }
+ cursor++;
+ }
+
+ // the request must not have any content
+ // content_length = 0
+ if (isBad || reqEnd - content_length > 1 || *content_length != '0') {
+ puts("4 Length");
+ send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+
+ // replace SPC with 0 at the end of path
+ *(content_length-1) = 0;
+
+ // build server path
+ char localPath[PATH_MAX] = {0};
+ if (rootLen + strlen(path) >= PATH_MAX) {
+ puts("5 Path too long");
+ send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+ memcpy(localPath, root, rootLen);
+ cursor = localPath + rootLen;
+ memcpy(cursor, path, strlen(path));
+
+ // check path
+ char realPath[PATH_MAX] = {0};
+ realpath(localPath, realPath);
+ if (memcmp(realPath, root, rootLen-slash) != 0) {
+ puts("4 Not found");
+ send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+
+ size_t pathLen = strlen(realPath);
+ cursor = realPath + pathLen;
+ if (isDir(realPath)) {
+ if (pathLen + strlen("/index.gmi") >= PATH_MAX) {
+ puts("5 Path too long");
+ send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+ memcpy(cursor, "/index.gmi", strlen("/index.gmi"));
+ cursor += strlen("/index.gmi");
+ }
+
+ FILE *f = fopen(realPath, "r");
+ if (!f) {
+ puts("4 Page not found");
+ send(mysock, "4 Page not found\r\n", sizeof("4 Page not found\r\n"), MSG_NOSIGNAL);
+ close(mysock);
+ continue;
+ }
+
+ // request in buf is not needed anymore, reuse buf for response
+
+ // check gemini extension
+ char *mimetype = "application/octet-stream";
+ if (strlen(realPath) > 4 && memcmp(cursor-2, "md", 2) == 0) {
+ mimetype = "text/markdown";
+ }
+ else if (strlen(realPath) > 5 && memcmp(cursor-3, "gmi", 3) == 0) {
+ mimetype = "text/gemini";
+ }
+ else if (strlen(realPath) > 10 && memcmp(cursor-2, "markdown", 8) == 0) {
+ mimetype = "text/markdown";
+ }
+
+ int len = sprintf(buf, "2 %s\r\n", mimetype);
+ if (send(mysock, buf, len, MSG_NOSIGNAL) != -1) {
+ // print response in terminal
+ // remove \r\n
+ buf[len-2] = 0;
+ puts(buf);
+
+ // send file
+ size_t fsz;
+ while (fsz = fread(buf, 1, (size_t)sizeof(buf) , f)) {
+ ssize_t r = send(mysock, buf, fsz, MSG_NOSIGNAL);
+ if (r == -1) {
+ perror("write failed");
+ puts("closed socket");
+ break;
+ }
+ }
+ }
+ else {
+ perror("write failed");
+ }
+ fclose(f);
+ close(mysock);
+ }
+ } while(1);
+
+ puts("Server stopped.");
+}