This commit is contained in:
retoor 2024-12-31 02:57:48 +01:00
commit d5a2dbfc49
7 changed files with 508 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.history
.vscode
.backup*
a.out
__pycache__

15
Makefile Normal file
View File

@ -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)

4
README.md Normal file
View File

@ -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.

39
build.py Normal file
View File

@ -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")

273
pgs.c Normal file
View File

@ -0,0 +1,273 @@
#include "py.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#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;
}

112
pgscript.py Normal file
View File

@ -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

60
py.h Normal file
View File

@ -0,0 +1,60 @@
#include <Python.h>
#include <stdbool.h>
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;
}