commit d5a2dbfc49a66420a560ef4ebdcead8f73d827e9 Author: retoor Date: Tue Dec 31 02:57:48 2024 +0100 Cleanup. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66c78d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.history +.vscode +.backup* +a.out +__pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e4d8229 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +CC=gcc +CFLAGS=-Wall -g +LDFLAGS=-lm +OBJS=pgs.o +TARGET=pgs + + +build: + $(CC) pgs.c $(CFLAGS) $(LDFLAGS) -o $(TARGET) + +run: build + ./$(TARGET) + +clean: + rm -f $(OBJS) $(TARGET) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4384434 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +Entropy: + +Sequence entropy: 4.37 +This is close to the theoretical maximum entropy (log2(21) ≈ 4.39), suggesting the sequence is highly random. \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..b08bb1c --- /dev/null +++ b/build.py @@ -0,0 +1,39 @@ + +@task +def format(): + """ + Format C source files. + """ + system("clang-format -i *.c *.h") + +@task +def upstreams(): + """ + Upstreams to molodetz and random for retoor2 local server. + """ + system("ssh -f -N -L 3028:127.0.0.1:3028 molodetz.nl") + system("ssh -f -N -L 8082:127.0.0.1:8082 molodetz.nl") + +@task +def bench(): + """ + Benchmark using Apache Benchmark for retoor2 local server. + """ + system("ab -n 1000 -c 5 http://molodetz.localhost:2222/") + system("ab -n 1000 -c 5 http://random.localhost:2222/") + +@task +def build(): + """ + Build the program. Output is pgs. + """ + format() + system("gcc pgs.c -o pgs -lpython3.12 -I/usr/include/python3.14") + +@task +def run(): + """ + Build the program and run it. + """ + build() + system("./pgs") diff --git a/pgs.c b/pgs.c new file mode 100644 index 0000000..5b9a222 --- /dev/null +++ b/pgs.c @@ -0,0 +1,273 @@ +#include "py.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define LISTEN_PORT 2222 +#define UPSTREAM_HOST "127.0.0.1" +#define UPSTREAM_PORT 9999 +#define MAX_EVENTS 8096 +#define BUFFER_SIZE 1024 + +void set_nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + if (flags == -1) { + perror("fcntl get"); + exit(EXIT_FAILURE); + } + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { + perror("fcntl set"); + exit(EXIT_FAILURE); + } +} + +int prepare_upstream() { + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + + return sockfd; +} + +int connect_upstream(const char *host, int port) { + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd == -1) { + perror("socket"); + return -1; + } + + set_nonblocking(sockfd); + + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(port); + if (inet_pton(AF_INET, host, &server_addr.sin_addr) <= 0) { + perror("inet_pton"); + close(sockfd); + return -1; + } + + if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == + -1) { + if (errno != EINPROGRESS) { + perror("connect"); + close(sockfd); + return -1; + } + } + + return sockfd; +} + +int create_listening_socket(int port) { + int listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (listen_fd == -1) { + perror("socket"); + return -1; + } + + int opt = 1; + if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == + -1) { + perror("setsockopt"); + close(listen_fd); + return -1; + } + + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = INADDR_ANY; + server_addr.sin_port = htons(port); + + if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == + -1) { + perror("bind"); + close(listen_fd); + return -1; + } + + if (listen(listen_fd, SOMAXCONN) == -1) { + perror("listen"); + close(listen_fd); + return -1; + } + + set_nonblocking(listen_fd); + return listen_fd; +} + +typedef struct { + int client_fd; + int upstream_fd; +} connection_t; + +void close_connection(int epoll_fd, connection_t *conn) { + if (conn->client_fd != -1) { + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn->client_fd, NULL); + close(conn->client_fd); + } + if (conn->upstream_fd != -1) { + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn->upstream_fd, NULL); + close(conn->upstream_fd); + } +} + +int forward_data(int from_fd, int to_fd) { + static char buffer[BUFFER_SIZE]; + // Feels great to do somehow. Better safe than sorry. + memset(buffer, 0, BUFFER_SIZE); + ssize_t bytes_read = recv(from_fd, buffer, sizeof(buffer), 0); + if (bytes_read > 0) { + ssize_t bytes_written = send(to_fd, buffer, bytes_read, 0); + if (bytes_written == -1) { + perror("write"); + } + } else if (bytes_read == 0) { + printf("Connection closed by remote (fd=%d)\n", from_fd); + } else { + perror("read"); + } + return (int)bytes_read; +} + +int listen_fd = 0; +int epoll_fd = 0; +void cleanup() { + close(epoll_fd); + close(listen_fd); + py_destruct(); + printf("Graceful exit.\n"); +} +void handle_sigint(int sig) { + printf("\nCtrl+C pressed.\n"); + exit(0); +} + +int main() { + if (signal(SIGINT, handle_sigint) == SIG_ERR) { + perror("Failed to register signal handler"); + return EXIT_FAILURE; + } + + listen_fd = create_listening_socket(LISTEN_PORT); + if (listen_fd == -1) { + fprintf(stderr, "Failed to create listening socket\n"); + return EXIT_FAILURE; + } + atexit(cleanup); + epoll_fd = epoll_create1(0); + if (epoll_fd == -1) { + perror("epoll_create1"); + close(listen_fd); + return EXIT_FAILURE; + } + + struct epoll_event event; + event.events = EPOLLIN; + event.data.fd = listen_fd; + + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) == -1) { + perror("epoll_ctl"); + close(listen_fd); + close(epoll_fd); + return EXIT_FAILURE; + } + + struct epoll_event events[MAX_EVENTS]; + memset(events, 0, sizeof(events)); + + printf("Intercepting load balancer listening on port %d\n", LISTEN_PORT); + connection_t connections[MAX_EVENTS][sizeof(connection_t)] = {0}; + while (1) { + int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); + if (num_events == -1) { + perror("epoll_wait"); + break; + } + + for (int i = 0; i < num_events; i++) { + if (events[i].data.fd == listen_fd) { + struct sockaddr_in client_addr; + socklen_t client_len = sizeof(client_addr); + int client_fd = + accept(listen_fd, (struct sockaddr *)&client_addr, &client_len); + if (client_fd == -1) { + perror("accept"); + continue; + } + set_nonblocking(client_fd); + + struct epoll_event client_event; + client_event.events = EPOLLIN | EPOLLOUT | EPOLLERR | EPOLLHUP; + client_event.data.ptr = connections[client_fd]; + client_event.data.fd = client_fd; + connections[client_fd]->upstream_fd = -1; + connections[client_fd]->client_fd = client_fd; + + // connections[client_fd]->upstream_fd = upstream_fd; + // connections[conn->client_fd] = conn; + // connections[upstream_fd]->client_fd = client_fd; + // connections[upstream_fd]->upstream_fd = upstream_fd; + + epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_event); + + } else { + // Handle data forwarding for existing connections + connection_t *conn = connections[events[i].data.fd]; + if (events[i].events & (EPOLLHUP | EPOLLERR)) { + printf("Connection closed: client_fd=%d, upstream_fd=%d\n", + conn->client_fd, conn->upstream_fd); + close_connection(epoll_fd, conn); + } else if (events[i].events & EPOLLIN) { + if (conn->upstream_fd == -1) { + conn->upstream_fd = prepare_upstream(); + int upstream_fd = py_route(conn->client_fd, conn->upstream_fd); + + if (upstream_fd == -1) { + close(conn->client_fd); + continue; + } + set_nonblocking(upstream_fd); + struct epoll_event upstream_event; + upstream_event.events = EPOLLIN | EPOLLOUT | EPOLLERR | EPOLLHUP; + upstream_event.data.ptr = connections[upstream_fd]; + upstream_event.data.fd = upstream_fd; + + connections[conn->client_fd]->upstream_fd = upstream_fd; + connections[upstream_fd]->client_fd = conn->client_fd; + connections[upstream_fd]->upstream_fd = upstream_fd; + + epoll_ctl(epoll_fd, EPOLL_CTL_ADD, upstream_fd, &upstream_event); + + printf("Connected: client_fd=%d, upstream_fd=%d\n", + conn->client_fd, conn->upstream_fd); + continue; + } + + if (events[i].data.fd == conn->client_fd) { + + if (forward_data(conn->client_fd, conn->upstream_fd) < 1) { + close_connection(epoll_fd, conn); + } + } else if (events[i].data.fd == conn->upstream_fd) { + + if (forward_data(conn->upstream_fd, conn->client_fd) < 1) { + close_connection(epoll_fd, conn); + } + } + } + } + } + } + + close(listen_fd); + close(epoll_fd); + return EXIT_SUCCESS; +} diff --git a/pgscript.py b/pgscript.py new file mode 100644 index 0000000..83b69f0 --- /dev/null +++ b/pgscript.py @@ -0,0 +1,112 @@ +import socket +import os + +# Hostname, only resolved at startup. +hostname = socket.gethostname() + +# Specify environment based on hostname. +env = "dev" if hostname.startswith("retoor") else "prod" + +# This dict will contain the connections in this format: {downstream_fd:upstream_fd] +streams = {} + +# This is debug variable. It holds the number of connections total made. +counter = 0 + +def is_ssh(header_bytes): + return b'SSH' in header_bytes + +def is_http(header_bytes): + """ + Check if the header is an HTTP request. + """ + return b'HTTP/1.1' in header_bytes or b'HTTP/1.0' in header_bytes or b'HTTP/2.0' in header_bytes or b'HTTP/3.0' in header_bytes or b'Connection: ' in header_bytes + +def is_https(header_bytes): + return not any([is_ssh(header_bytes), is_http(header_bytes)]) + +def route(downstream,upstream): + """ + This is a connection router which will be called by the server every + time a client connects. This function will be used to determine + the upstream. The downstream and upstream are file descriptors. + The upstream is not connected yet, it only holds a file descriptor. + The connection will be made in this function. The connection will be + set non blocking after this function by pgs. + This way of routing is so dynamic that you can: + - Create [your-site].localhost redirects without configuring DNS. + - Run multiple services on the same port. It is possible to run ssh + on the same port as your HTTPS server. This is a good idea in sense + of security. Very unique, who does / expects that? + - Rewrite urls in general. + - Make clients always connect to the same upstream. Servers only have + to manage their own session instead of having to communicate with + redis. + - You can inject headers in the request. + - You can add HTTP Basic Authentication to protect all your services + in a very early stage. + - This server is quick, it can act as ddos protection. + - You can make your server act as a load balancer. + - You can make your server act as a reverse proxy. + - You can apply rate limits. + - You can cache responses. + - You can implement a complete custom protocol here. Complete own + design. This feature will probably moved in the future. + - You can do static file serving. + - You can protect sensitive data not leaving the network by + intercepting it. + - You can call AI to make modications. + - You can call databases. + - You can save statistics. + """ + + global streams + global counter + + counter += 1 + + print("Connection nr.", counter) + u = socket.fromfd(upstream, socket.AF_INET, socket.SOCK_STREAM) + #u = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + #print("FD:",u.fileno()) + peek = os.read(downstream, 4096) + + if peek.startswith(b"SSH"): + print("Forwarding to ssh molodetz") + u.connect(("molodetz.nl", 22)) + elif is_http(peek): + print("Forwarding to zhurnal") + if b'/random' in peek or b'random.' in peek: + peek = peek.replace(b'/random', b'/') + peek = peek.replace(b'random.', b'') + u.connect(("127.0.0.1", 3028)) + elif b'molodetz.local' in peek: + peek = peek.replace(b'molodetz.local', b'localhost') + u.connect(("127.0.0.1", 8082)) + elif is_https(peek) and env == "prod": + print("Forwarding to dev.to") + u.connect(("devrant.com", 443)) + peek = peek.replace(b'localhost', b'devrant.com') + peek = peek.replace(b'molodetz.nl', b'devrant.com') + else: + # Error. + print("Could not find upstream for header content.") + print(b"Closing connection. Your current environment: {env}") + # Don't have to close socket, pgs will do that himself. + # Socket handling is done at one place to avoid race conditions. + + if not u: + return -1 + + # Remove reference to the socket so it doesn't get garbage collected. + # This could break the connection. This way, it stays open. + u = None + + os.write(upstream,peek) + + # Keep track of connections. Not sure if this is needed. + streams[downstream] = upstream + streams[upstream] = downstream + + # Return exact same value as what is given as parameter. + return upstream diff --git a/py.h b/py.h new file mode 100644 index 0000000..4a39b9f --- /dev/null +++ b/py.h @@ -0,0 +1,60 @@ +#include +#include + +PyObject *_pModule = NULL; +bool python_initialized = false; + +PyObject *py_construct() { + if (!python_initialized) { + Py_Initialize(); + python_initialized = true; + PyObject *sysPath = PySys_GetObject("path"); + PyList_Append(sysPath, PyUnicode_FromString(".")); + // Py_DECREF(sysPath); + PyObject *pName = PyUnicode_DecodeFSDefault("pgscript"); + _pModule = PyImport_Import(pName); + Py_DECREF(pName); + + python_initialized = true; + } + return _pModule; +} + +void py_destruct() { + if (!python_initialized) + return; + Py_DECREF(_pModule); + Py_Finalize(); + python_initialized = false; +} + +int py_route(int downstream, int upstream) { + PyObject *pModule = py_construct(); + long upstream_fd = 0; + if (pModule != NULL) { + PyObject *pFunc = PyObject_GetAttrString(pModule, "route"); + if (PyCallable_Check(pFunc)) { + PyObject *pArgs = PyTuple_Pack(2, PyLong_FromLong(downstream), + PyLong_FromLong(upstream)); + PyObject *pValue = PyObject_CallObject(pFunc, pArgs); + Py_DECREF(pArgs); + if (pValue != NULL) { + upstream_fd = PyLong_AsLong(pValue); + Py_DECREF(pValue); + } else { + PyErr_Print(); + fprintf(stderr, "Call failed\n"); + } + } else { + PyErr_Print(); + fprintf(stderr, "Cannot find function 'route'\n"); + } + + Py_XDECREF(pFunc); + } else { + PyErr_Print(); + fprintf(stderr, "Failed to load 'script'\n"); + } + + return (int)upstream_fd; +}