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