Routing gopher requests (reverse proxy) + Tor ___ _ _ | _ \ ___ _ _ | |_ (_) _ _ __ _ | / / _ \ | || | | _| | | | ' \ / _` | |_|_\ \___/ \_,_| \__| |_| |_||_| \__, | |___/ _ __ _ ___ _ __ | |_ ___ _ _ / _` | / _ \ | '_ \ | ' \ / -_) | '_| \__, | \___/ | .__/ |_||_| \___| |_| |___/ |_| _ _ _ ___ __ _ _ _ ___ ___ | |_ ___ | '_| / -_) / _` | | || | / -_) (_-< | _| (_-< |_| \___| \__, | \_,_| \___| /__/ \__| /__/ |_| __ / / _ _ ___ __ __ ___ _ _ ___ ___ | | | '_| / -_) \ V / / -_) | '_| (_-< / -_) | | |_| \___| \_/ \___| |_| /__/ \___| \_\ __ _ __ _ _ ___ __ __ _ _ \ \ | '_ \ | '_| / _ \ \ \ / | || | | | | .__/ |_| \___/ /_\_\ \_, | | | |_| |__/ /_/ _ _| |_ |_ _| |_| _____ |_ _| ___ _ _ | | / _ \ | '_| |_| \___/ |_| ╔─*──*──*──*──*──*──*──*──*──*──*──*──*──*──*──*─╗ ║1 ........................................ 1║ ║2* ........................................ *2║ ║3 ........................................ 3║ ║1 ...........Posted: 2025-02-25........... 1║ ║2* ......Tags: sysadmin gopher linux ...... *2║ ║3 ........................................ 3║ ║1 ........................................ 1║ ╚────────────────────────────────────────────────╝ I host different Gopher Protocol services which use different ports on one box. So I kind of need a *reverse proxy* of sorts. I wanted to make sure I could access all of them using port 70. The idea: * Don't touch my existing Gopher Protocol service * Have a new Gopher service running on port 70 * On the new Gopher service, associate certain internal ports with certain selectors * for example if a selector beginning with `/phorum` is requested on port 70, I want to strip the `/phorum` bit, then request localhost 7070 and serve that * For all internal links, prepend the selector and change the port to 70 In other words, I have various Gopher Protocol services running on different ports, but with xinetd I serve them all on port 70, just under different selectors. This is basically a Gopher Protocol routing/reverse proxy! ## It's running live! I use this configuration on this server. `gopher://gopher.someodd.zip:70/1/someodd/`: basically puts my localhost 7071 gopher service to be served on port 70, under the `/someodd` selector. `gopher://gopher.someodd.zip:70/1/phorum/`: serves phorum (running on localhost 7070) on port 70, under the `/someodd` selector. This configuration also offers a service index at `gopher://gopher.someodd.zip/`. ## Configure the server Install `xnetd`: ``` sudo apt-get install -y xinetd ``` Add this file `/etc/xinetd.d/gopher`: ``` service gopher { type = UNLISTED port = 70 socket_type = stream wait = no user = root server = /usr/local/bin/gopher_router.sh log_on_success += USERID log_on_failure += USERID log_type = FILE /var/log/xinetd_gopher.log disable = no } ``` Create this script `/usr/local/bin/gopher_router.sh`: ``` #!/bin/bash # Read the selector from stdin read selector # Function to process and replace paths and ports in the response process_response() { local prefix="$1" local port="$2" local target_host="gopher.someodd.zip" local target_port="70" # Strip the prefix from the selector and ensure the leading slash is retained local stripped_selector="${selector#$prefix}" if [[ "$stripped_selector" != /* ]]; then stripped_selector="/$stripped_selector" fi # Send the selector to the appropriate service and process the response echo "$stripped_selector" | nc localhost $port | \ sed -e "/\t${target_host}\t${port}/s|\t/|\t${prefix}/|g" \ -e "s|\(${target_host}\)\t${port}|\1\t${target_port}|g" } # Handle the selector case "$selector" in "/phorum"*) process_response "/phorum" 7070 ;; *) process_response "/" 7071 ;; esac ``` Restart: ``` sudo systemctl restart xinetd ``` For testing: ``` echo "/parent2/little-notes" | nc localhost 70 ``` Also `ufw` (firewall entry): ``` sudo ufw allow 70/tcp comment 'gopher (router)' ``` ## Bonus ### Serve default menu You could even serve a default menu: ``` # Handle the selector case "$selector" in "/phorum"*) process_response "/phorum" 7070 ;; "/someodd"*) process_response "/someodd" 7071 ;; *) # Return a Gopher menu with links to /phorum and /someodd echo "1Phorum link /phorum gopher.someodd.zip 70" echo "1someodd link /someodd gopher.someodd.zip 70" echo "" ;; esac ``` ### Handle rewriting for Tor + my modern setup This is the setup I use now, actually. I have kept the older ones above because they might be useful or interesting. I should really clean this article up. in `/etc/xinetd.d/gopher`: ``` service gopher { type = UNLISTED port = 70 socket_type = stream wait = no user = root server = /bin/bash server_args = /usr/local/bin/gopher_router.sh log_on_success += USERID log_on_failure += USERID log_type = FILE /var/log/xinetd_gopher.log disable = no } ``` now i updated my routing script: ``` #!/bin/bash # xinetd wrapper for Gopher services with /phorum branch # - Preserves index-search tabs (type 7) # - Sends CRLF to backend # - Does NOT strip /phorum for the 7070 service (fixes /phorum/newthread) # - Rewrites backend host/port in responses # - Optional prefix reattach for relative selectors returned by /phorum set -Eeuo pipefail # --- Config ------------------------------------------------------------------- DEFAULT_HOST="gopher.someodd.zip" DEFAULT_PORT="70" PHORUM_BACKEND_PORT="7070" OTHER_BACKEND_PORT="7071" # If your /phorum backend returns relative selectors like "/newthread" and you # want follow-up clicks to stay under /phorum, set this to "true". PHORUM_REWRITE_PREFIX="false" # "true" or "false" # Onion to use when served via Tor hidden service ONION_HOST="xj2o2wylbqkprajldswuyxm6dffca4eepegelblgvux3uuqmtb2l56id.onion" # --- Read request line exactly (keep tabs), trim trailing CR ------------------- IFS= read -r selector || selector="" # strip a single trailing CR if present selector=${selector%$'\r'} # --- Helpers ------------------------------------------------------------------ is_tor_connection() { # xinetd exports remote endpoint in REMOTE_HOST/REMOTE_ADDR case "${REMOTE_HOST:-${REMOTE_ADDR:-}}" in 127.0.0.1|::1|::ffff:127.0.0.1) return 0 ;; *) return 1 ;; esac } current_target_host() { if is_tor_connection; then printf '%s' "$ONION_HOST" else printf '%s' "$DEFAULT_HOST" fi } rewrite_backend_links() { # $1 = backend_port, $2 = rewrite_prefix (true/false), $3 = (optional) prefix text like "/phorum" local backend_port="$1" local rewrite_prefix="$2" local prefix="${3:-/phorum}" local tgt_host; tgt_host="$(current_target_host)" # Replace host:port emitted by backend with public host:70 # Also normalize any literal "host/path" occurrences emitted by some servers # Finally, optionally reattach prefix to *returned* selectors that start with "/" if [[ "$rewrite_prefix" == "true" ]]; then sed -e "s|\t${DEFAULT_HOST}\t${backend_port}|\t${tgt_host}\t${DEFAULT_PORT}|g" \ -e "s|\t${DEFAULT_HOST}/|\t${tgt_host}/|g" \ -e "s|\t/|\t${prefix}/|g" else sed -e "s|\t${DEFAULT_HOST}\t${backend_port}|\t${tgt_host}\t${DEFAULT_PORT}|g" \ -e "s|\t${DEFAULT_HOST}/|\t${tgt_host}/|g" fi } proxy_to() { # $1 = backend_port local port="$1" # Forward the request exactly as received (including any \tquery), with CRLF # Use -w to avoid hanging if the backend closes slowly; -N if your nc supports it printf "%s\r\n" "$selector" | nc localhost "$port" | rewrite_backend_links "$port" "false" } proxy_phorum() { # For /phorum we forward *as-is* (no prefix stripping). This fixes /phorum/newthread. # Optionally reattach /phorum to returned relative selectors if desired. local port="$PHORUM_BACKEND_PORT" local tgt_host; tgt_host="$(current_target_host)" if [[ "$PHORUM_REWRITE_PREFIX" == "true" ]]; then printf "%s\r\n" "$selector" | nc localhost "$port" | rewrite_backend_links "$port" "true" "/phorum" else printf "%s\r\n" "$selector" | nc localhost "$port" | rewrite_backend_links "$port" "false" fi } # --- Routing ------------------------------------------------------------------ case "$selector" in /phorum*) proxy_phorum ;; *) proxy_to "$OTHER_BACKEND_PORT" ;; esac ```