aboutsummaryrefslogtreecommitdiffstats
path: root/hosts/darkstar
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--hosts/darkstar/default.nix49
-rw-r--r--hosts/darkstar/mitmproxy-c64u.py303
-rw-r--r--hosts/darkstar/services.nix23
3 files changed, 361 insertions, 14 deletions
diff --git a/hosts/darkstar/default.nix b/hosts/darkstar/default.nix
index b53a7d7..e413a97 100644
--- a/hosts/darkstar/default.nix
+++ b/hosts/darkstar/default.nix
@@ -2,9 +2,10 @@
boot = {
initrd.kernelModules = [ "zfs" ];
kernel.sysctl = {
+ "kernel.hostname" = "darkstar.bitgnome.net";
"net.ipv4.ip_forward" = true;
};
- kernelPackages = pkgs.linuxPackages_6_12;
+ kernelPackages = pkgs.linuxPackages_6_18;
loader = {
efi = {
canTouchEfiVariables = true;
@@ -15,38 +16,45 @@
extraInstallCommands = ''
${pkgs.rsync}/bin/rsync -av --delete /efiboot/efi1/ /efiboot/efi2
'';
+ memtest86.enable = true;
};
timeout = 3;
};
supportedFilesystems = [ "zfs" ];
- #zfs.package = pkgs.master.zfs;
+ zfs.package = pkgs.zfs_2_4;
};
- #environment.systemPackages = with pkgs; [
- # wpa_supplicant
- # somethingelse
- #];
+ environment = {
+ etc."mitmproxy-c64u.py".source = ./mitmproxy-c64u.py;
+ systemPackages = [
+ pkgs.mitmproxy
+ pkgs.speedtest-go
+ ];
+ };
imports = [
./disks.nix
./hardware-configuration.nix
./services.nix
../common/core
- ../common/optional/services/asterisk.nix
+ #../common/optional/services/asterisk.nix
../common/optional/services/chrony.nix
../common/optional/services/dhcp.nix
../common/optional/services/nsd.nix
../common/optional/services/openssh.nix
+ ../common/optional/wdt.nix
../common/optional/zfs.nix
../common/users/nipsy
../common/users/root
];
networking = {
+ #defaultGateway = "192.168.1.1";
hostId = "f9ca5efe";
hostName = "darkstar";
- #defaultGateway = "192.168.1.1";
- domain = "bitgnome.net";
+ #hosts = {
+ # "185.187.254.229" = [ "hackerswithstyle.se" ];
+ #};
interfaces = {
enp116s0 = {
ipv4.addresses = [
@@ -65,6 +73,9 @@
internalInterfaces = [ "enp116s0" ];
};
nftables.enable = true;
+ search = [
+ "bitgnome.net"
+ ];
useDHCP = false;
vlans = {
vlan201 = { id=201; interface="enp117s0"; };
@@ -101,6 +112,7 @@
"nftables/forward" = {};
"nftables/ssh" = {};
"nix-access-token-github" = {};
+ "ssh_config".path = "/root/.ssh/config";
};
};
@@ -114,7 +126,23 @@
system.stateVersion = "23.11";
- systemd.services."nftables-extra" = let rules_script = ''
+ systemd.services = {
+ "mitmproxy" = let rules_script = ''
+ ${pkgs.mitmproxy}/bin/mitmdump -p 80 -s /etc/mitmproxy-c64u.py --mode reverse:http://185.187.254.229:80 --set block_global=false
+ ''; in {
+ description = "proxy for C64 site hackerswithstyle.se";
+ script = rules_script;
+ serviceConfig = {
+ Restart = "on-failure";
+ RestartSec = 5;
+ StandardError = "append:/var/log/mitmproxy.log";
+ StandardOutput = "append:/var/log/mitmproxy.log";
+ Type = "simple";
+ };
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+ };
+ "nftables-extra" = let rules_script = ''
${pkgs.nftables}/bin/nft -a list chain inet nixos-fw input | ${pkgs.gnugrep}/bin/grep @anveo | ${pkgs.gnugrep}/bin/grep -Eo 'handle [[:digit:]]+$' | ${pkgs.gnused}/bin/sed -e 's/^handle //' | while read handle; do ${pkgs.nftables}/bin/nft delete rule inet nixos-fw input handle ''${handle}; done
if ${pkgs.nftables}/bin/nft list set inet nixos-fw anveo 2>/dev/null; then ${pkgs.nftables}/bin/nft delete set inet nixos-fw anveo; fi
if ${pkgs.nftables}/bin/nft list ct helpers table inet nixos-fw | ${pkgs.gnugrep}/bin/grep -qE '^[[:space:]]*ct helper sip-5060 {$'; then ${pkgs.nftables}/bin/nft delete ct helper inet nixos-fw sip-5060; fi
@@ -142,6 +170,7 @@
wantedBy = [ "multi-user.target" ];
after = [ "nftables.service" ];
partOf = [ "nftables.service" ];
+ };
};
systemd.paths."nftables-extra" = {
diff --git a/hosts/darkstar/mitmproxy-c64u.py b/hosts/darkstar/mitmproxy-c64u.py
new file mode 100644
index 0000000..5fc5aa6
--- /dev/null
+++ b/hosts/darkstar/mitmproxy-c64u.py
@@ -0,0 +1,303 @@
+
+import json
+import os
+import re
+import sys
+from mitmproxy import http
+from datetime import datetime
+
+STATE_DIR = "/var/lib"
+STATE_FILE = f"{STATE_DIR}/mitmproxy-clients.json"
+
+def log(msg):
+ """Print with flush for immediate output"""
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ print(f"[{timestamp}] {msg}", flush=True)
+
+def load_state():
+ if os.path.exists(STATE_FILE):
+ with open(STATE_FILE, "r") as f:
+ return json.load(f)
+ return {}
+
+def save_state(state):
+ os.makedirs(STATE_DIR, exist_ok=True)
+ with open(STATE_FILE, "w") as f:
+ json.dump(state, f, indent=4)
+
+def request(flow: http.HTTPFlow) -> None:
+ client_ip = flow.client_conn.peername[0]
+
+ # Get Client-Id header to detect device type
+ client_id_header = flow.request.headers.get("Client-Id")
+
+ # Debug: log all incoming requests
+ log(f"REQUEST: {flow.request.method} {flow.request.path}")
+ log(f" Client-IP: {client_ip}")
+ log(f" Client-Id: {client_id_header}")
+ log(f" Query params: {dict(flow.request.query)}")
+ log(f" Headers: {dict(flow.request.headers)}")
+
+ # Load saved server choice for this client (assembly64 or commoserve)
+ state = load_state()
+ server_choice = state.get(client_ip)
+
+ # Default: give them what they can't normally access
+ # C64U (Commodore) -> default to assembly64
+ # Ultimate64 (Ultimate) -> default to commoserve
+ if server_choice is None:
+ if client_id_header == "Ultimate":
+ server_choice = "commoserve"
+ else:
+ server_choice = "assembly64"
+ log(f" Default server for {client_id_header}: {server_choice}")
+
+ # Check for server parameter in query (from menu selection)
+ server_param = flow.request.query.get("server", "").lower()
+ if server_param in ["assembly64", "commoserve"]:
+ state[client_ip] = server_param
+ save_state(state)
+ server_choice = server_param
+ log(f"SWITCH (menu): {client_ip} -> {server_param}")
+ # Remove the server param so it doesn't confuse the real server
+ del flow.request.query["server"]
+
+ # Check for server selection in query (from injected menu)
+ # Query comes as: (server:assembly64) or (server:commoserve)
+ # May also include other search params like (name:"bubble")
+ query_string = flow.request.query.get("query", "")
+ query_lower = query_string.lower()
+ name_string = flow.request.query.get("name", "").lower()
+ search_text = query_lower + " " + name_string
+
+ # Handle server menu selection - strip server: from query and continue
+ if "server:assembly64" in query_lower or "server:commoserve" in query_lower:
+ if "server:assembly64" in query_lower:
+ new_server = "assembly64"
+ else:
+ new_server = "commoserve"
+
+ state[client_ip] = new_server
+ save_state(state)
+ server_choice = new_server
+ log(f"SWITCH (menu): {client_ip} -> {new_server}")
+
+ # Remove server:xxx from query string (case insensitive)
+ # Also handle the & operator that joins query parts
+ cleaned_query = re.sub(r'\s*&\s*\(server:(assembly64|commoserve)\)', '', query_string, flags=re.IGNORECASE)
+ cleaned_query = re.sub(r'\(server:(assembly64|commoserve)\)\s*&\s*', '', cleaned_query, flags=re.IGNORECASE)
+ cleaned_query = re.sub(r'\(server:(assembly64|commoserve)\)', '', cleaned_query, flags=re.IGNORECASE)
+ # Clean up any leftover empty parens or double spaces
+ cleaned_query = cleaned_query.strip()
+
+ # If query is now empty or just whitespace, return confirmation
+ if not cleaned_query or cleaned_query == "()":
+ flow.response = http.Response.make(
+ 200,
+ json.dumps([{"name": f"Switched to {new_server.upper()}", "id": "0", "category": 0}]),
+ {"Content-Type": "application/json"}
+ )
+ log(f" No other search params, returning confirmation")
+ return
+ else:
+ # Update query with server part removed and continue with search
+ flow.request.query["query"] = cleaned_query
+ log(f" Cleaned query: {cleaned_query}")
+
+ # Also check for keywords in search query (legacy method)
+ if "assembly64" in search_text:
+ state[client_ip] = "assembly64"
+ save_state(state)
+ server_choice = "assembly64"
+ log(f"SWITCH (keyword): {client_ip} -> Assembly64")
+
+ elif "commoserve" in search_text:
+ state[client_ip] = "commoserve"
+ save_state(state)
+ server_choice = "commoserve"
+ log(f"SWITCH (keyword): {client_ip} -> Commoserve")
+
+ # Help feature
+ elif "help" in search_text:
+ device = "Ultimate64" if client_id_header == "Ultimate" else "C64U"
+ help_text = (
+ "PROXY HELP:\n"
+ f"Device: {device}\n"
+ f"Current: {server_choice}\n"
+ "Use Server menu or search 'commoserve'/'assembly64' to switch."
+ )
+ flow.response = http.Response.make(
+ 200,
+ json.dumps({"results": [{"name": help_text}]}),
+ {"Content-Type": "application/json"}
+ )
+ log(f"HELP: Sent help response to {client_ip}")
+ return
+
+ # Block requests for our fake info items (id "0" or "info")
+ # These are the "Switched to..." or "Currently browsing..." messages
+ if re.match(r'/leet/search/entries/(0|info)/', flow.request.path):
+ log(f"BLOCKED: Request for info item, returning empty")
+ flow.response = http.Response.make(
+ 200,
+ json.dumps([]),
+ {"Content-Type": "application/json"}
+ )
+ return
+
+ # Bot protection
+ if not client_id_header and "/leet/search/" not in flow.request.path:
+ log(f"BLOCKED: No Client-Id header from {client_ip}")
+ flow.kill()
+ return
+
+ # Apply header patch
+ # C64U (Commodore) accessing assembly64 -> patch to Ultimate
+ # Ultimate64 (Ultimate) accessing commoserve -> patch to Commodore
+ original_client_id = client_id_header
+
+ # Always fetch presets with Ultimate header to get full Assembly64 menu
+ if flow.request.path == "/leet/search/aql/presets":
+ if client_id_header != "Ultimate":
+ flow.request.headers["Client-Id"] = "Ultimate"
+ log(f"PATCHED: {client_ip} Client-Id: {client_id_header} -> Ultimate (fetching full menu)")
+ else:
+ log(f"FORWARDED: {client_ip} presets request (already Ultimate)")
+ elif client_id_header == "Commodore" and server_choice == "assembly64":
+ flow.request.headers["Client-Id"] = "Ultimate"
+ log(f"PATCHED: {client_ip} Client-Id: Commodore -> Ultimate (accessing Assembly64)")
+ elif client_id_header == "Ultimate" and server_choice == "commoserve":
+ flow.request.headers["Client-Id"] = "Commodore"
+ log(f"PATCHED: {client_ip} Client-Id: Ultimate -> Commodore (accessing Commoserve)")
+ else:
+ device = "Ultimate64" if client_id_header == "Ultimate" else "C64U"
+ log(f"FORWARDED: {client_ip} ({device}) -> {server_choice} (no patch needed)")
+
+ # Forwarding
+ flow.request.host = "185.187.254.229"
+ flow.request.port = 80
+ flow.request.headers["Host"] = "hackerswithstyle.se"
+ log(f" Forwarding to: {flow.request.host}:{flow.request.port}")
+
+def response(flow: http.HTTPFlow) -> None:
+ """Intercept responses and inject Server menu option into presets"""
+ client_ip = flow.client_conn.peername[0]
+
+ # Debug: log all responses
+ log(f"RESPONSE: {flow.request.path}")
+ log(f" Status: {flow.response.status_code}")
+ log(f" Content-Type: {flow.response.headers.get('Content-Type', 'unknown')}")
+ log(f" Content length: {len(flow.response.content)} bytes")
+
+ # Show first 500 chars of response for debugging
+ try:
+ content_preview = flow.response.content.decode('utf-8')[:500]
+ log(f" Content preview: {content_preview}")
+ except:
+ log(f" Content preview: (binary data)")
+
+ # Inject "Currently browsing..." info item into search results
+ # Match /leet/search/aql but not /leet/search/aql/presets
+ if flow.request.path.startswith("/leet/search/aql") and not flow.request.path.startswith("/leet/search/aql/"):
+ log(f"SEARCH RESULTS: Intercepted search response for path: {flow.request.path}")
+ try:
+ data = json.loads(flow.response.content)
+ log(f" Response type: {type(data).__name__}")
+
+ # Get current server choice for this client
+ state = load_state()
+ client_id_header = flow.request.headers.get("Client-Id")
+ server_choice = state.get(client_ip)
+
+ # Default based on device type
+ if server_choice is None:
+ if client_id_header == "Ultimate":
+ server_choice = "commoserve"
+ else:
+ server_choice = "assembly64"
+
+ # Create info item showing current server
+ server_display = "Assembly64" if server_choice == "assembly64" else "Commoserve"
+ info_item = {
+ "name": f"Browsing: {server_display}",
+ "id": "info",
+ "category": 0
+ }
+
+ # Handle both array and dict responses
+ if isinstance(data, list):
+ log(f" Original results count: {len(data)}")
+ data.insert(0, info_item)
+ log(f" After injection: {len(data)} items")
+ elif isinstance(data, dict) and "results" in data:
+ log(f" Original results count: {len(data['results'])}")
+ data["results"].insert(0, info_item)
+ log(f" After injection: {len(data['results'])} items")
+ else:
+ log(f" Unknown response format, skipping injection")
+ return
+
+ # Update response
+ new_content = json.dumps(data)
+ flow.response.content = new_content.encode()
+ log(f"INJECTED: Info item into search results")
+ except json.JSONDecodeError as e:
+ log(f"ERROR: JSON decode failed for search results: {e}")
+ except Exception as e:
+ log(f"ERROR: Search results injection failed: {type(e).__name__}: {e}")
+
+ if flow.request.path == "/leet/search/aql/presets":
+ log(f"PRESETS: Intercepted presets response")
+ try:
+ # Parse the original response
+ data = json.loads(flow.response.content)
+ log(f" Original presets count: {len(data)}")
+
+ # Get current server choice for this client
+ state = load_state()
+ client_id_header = flow.request.headers.get("Client-Id")
+ server_choice = state.get(client_ip)
+
+ # Default based on device type
+ if server_choice is None:
+ if client_id_header == "Ultimate":
+ server_choice = "commoserve"
+ else:
+ server_choice = "assembly64"
+
+ # Build server menu with current selection marked with asterisk
+ if server_choice == "assembly64":
+ server_menu = {
+ "type": "server",
+ "description": "Server",
+ "values": [
+ {"aqlKey": "assembly64", "name": "* Assembly64"},
+ {"aqlKey": "commoserve", "name": "Commoserve"}
+ ]
+ }
+ else:
+ server_menu = {
+ "type": "server",
+ "description": "Server",
+ "values": [
+ {"aqlKey": "commoserve", "name": "* Commoserve"},
+ {"aqlKey": "assembly64", "name": "Assembly64"}
+ ]
+ }
+
+ # Inject Server menu at the beginning
+ data.insert(0, server_menu)
+ log(f" After injection: {len(data)} items (current: {server_choice})")
+
+ # Update response
+ new_content = json.dumps(data)
+ flow.response.content = new_content.encode()
+ log(f"INJECTED: Server menu into presets response")
+ log(f" New content length: {len(new_content)}")
+ except json.JSONDecodeError as e:
+ log(f"ERROR: JSON decode failed: {e}")
+ log(f" Raw content: {flow.response.content[:200]}")
+ except AttributeError as e:
+ log(f"ERROR: Attribute error: {e}")
+ except Exception as e:
+ log(f"ERROR: Unexpected error: {type(e).__name__}: {e}")
diff --git a/hosts/darkstar/services.nix b/hosts/darkstar/services.nix
index 7304b48..101d435 100644
--- a/hosts/darkstar/services.nix
+++ b/hosts/darkstar/services.nix
@@ -7,10 +7,15 @@
allowedUDPPorts = [
53 # domain
];
- interfaces.enp116s0.allowedUDPPorts = [
- 69 # xinetd/tftpd
- 123 # ntp
- ];
+ interfaces.enp116s0 = {
+ allowedTCPPorts = [
+ 80 # http
+ ];
+ allowedUDPPorts = [
+ 69 # xinetd/tftpd
+ 123 # ntp
+ ];
+ };
};
};
@@ -41,24 +46,34 @@
];
local-data = [
"\"darkstar.bitgnome.net. IN A 192.168.1.1\""
+ "\"hackerswithstyle.se. IN A 192.168.1.1\""
"\"arrakis.bitgnome.net. IN A 192.168.1.2\""
+ "\"caladan.bitgnome.net. IN A 192.168.1.4\""
+ "\"mister.bitgnome.net. IN A 192.168.1.10\""
"\"jupiter.bitgnome.net. IN A 192.168.1.11\""
"\"saturn.bitgnome.net. IN A 192.168.1.12\""
"\"uranus.bitgnome.net. IN A 192.168.1.13\""
"\"neptune.bitgnome.net. IN A 192.168.1.14\""
+ "\"deck.bitgnome.net. IN A 192.168.1.16\""
"\"ginaz.bitgnome.net. IN A 192.168.1.17\""
+ "\"loadstar.bitgnome.net. IN A 192.168.1.18\""
];
local-data-ptr = [
"\"192.168.1.1 darkstar.bitgnome.net\""
"\"192.168.1.2 arrakis.bitgnome.net\""
+ "\"192.168.1.4 caladan.bitgnome.net\""
+ "\"192.168.1.10 mister.bitgnome.net\""
"\"192.168.1.11 jupiter.bitgnome.net\""
"\"192.168.1.12 saturn.bitgnome.net\""
"\"192.168.1.13 uranus.bitgnome.net\""
"\"192.168.1.14 neptune.bitgnome.net\""
+ "\"192.168.1.16 deck.bitgnome.net\""
"\"192.168.1.17 ginaz.bitgnome.net\""
+ "\"192.168.1.18 loadstar.bitgnome.net\""
];
local-zone = [
"\"bitgnome.net.\" transparent"
+ "\"hackerswithstyle.se.\" transparent"
"\"1.168.192.in-addr.arpa.\" static"
];
verbosity = 2;