PHP Classes

File: docs/FTPServer.md

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   docs/FTPServer.md   Download  
File: docs/FTPServer.md
Role: Auxiliary data
Content type: text/markdown
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 9,913 bytes
 

Contents

Class file image Download

FTP Server

BinktermPHP includes a standalone FTP daemon for QWK packet exchange and file-area access.

This document covers daemon startup, .env configuration, passive FTP network requirements, and how to expose the service on standard FTP port 21 without running the daemon as root.

Features

  • Standalone daemon entrypoint: `scripts/ftp_daemon.php`
  • Disabled by default
  • Normal BBS username/password login
  • Anonymous FTP login for public file areas only
  • Virtual filesystem for: - `/qwk/download/<BBSID>.QWK` - `/qwk/upload/.REPor/qwk/upload/.ZIP` - `/incoming/<AREA>/...` - `/fileareas/...`
  • Per-user upload/download logging
  • Passive FTP (`PASV` / `EPSV`)
  • Active FTP (`PORT` / `EPRT`)
  • Single-process `stream_select()` loop, so it works on Windows too

Requirements

  • PHP 8.2+
  • BinktermPHP installed and configured
  • File Areas enabled if you want `/incoming` or `/fileareas`
  • QWK enabled if you want `/qwk/download` or `/qwk/upload`

Default Ports

| Purpose | Default | |---|---| | FTP control port | 2121 | | Passive port range | 2122-2149 |

The daemon intentionally does not bind to privileged port 21 by default. That avoids needing to run the service as root.

Enable It

The FTP daemon is disabled by default. Set this in .env:

FTPD_ENABLED=true

If FTPD_ENABLED=false, scripts/ftp_daemon.php exits immediately.

Configuration

Add or review these settings in .env:

FTPD_ENABLED=true
FTPD_BIND_HOST=0.0.0.0
FTPD_PORT=2121
FTPD_PUBLIC_HOST=bbs.example.com
FTPD_PASSIVE_PORT_START=2122
FTPD_PASSIVE_PORT_END=2149
FTPD_ALLOW_ANONYMOUS=false

Variables

| Variable | Default | Description | |---|---|---| | FTPD_ENABLED | false | Enables the standalone FTP daemon | | FTPD_BIND_HOST | 0.0.0.0 | Control-socket bind address | | FTPD_PORT | 2121 | FTP control port | | FTPD_PUBLIC_HOST | empty | Hostname or IPv4 address advertised in passive replies | | FTPD_PASSIVE_PORT_START | 2122 | First passive data port | | FTPD_PASSIVE_PORT_END | 2149 | Last passive data port | | FTPD_ALLOW_ANONYMOUS | false | Allow anonymous (anonymous/ftp) logins |

FTPD_PUBLIC_HOST

Set FTPD_PUBLIC_HOST when:

  • the daemon is behind NAT
  • the daemon binds to `0.0.0.0`
  • clients connect through a public hostname or public IP different from the machine's local address

Example:

FTPD_PUBLIC_HOST=ftp.example.com

If this is wrong, clients often log in successfully but fail on LIST, RETR, or STOR.

Active Mode Support

The daemon now supports classic active-mode data connections through both:

  • `PORT` for IPv4 clients
  • `EPRT` for IPv4 or IPv6 clients

For safety, active mode is restricted so the requested data connection target must match the same remote IP address already connected on the FTP control socket. This prevents FTP bounce behavior where a client asks the server to open data connections to unrelated third-party hosts.

If the active target does not match the control connection IP, the daemon rejects the command.

Starting the Daemon

Foreground:

php scripts/ftp_daemon.php

Background daemon mode:

php scripts/ftp_daemon.php --daemon

Defaults:

  • PID file: `data/run/ftpd.pid`
  • Log file: `data/logs/ftpd.log`

Show options:

php scripts/ftp_daemon.php --help

Command Line Options

| Option | Default | Description | |---|---|---| | --host=HOST | FTPD_BIND_HOST or 0.0.0.0 | FTP bind host | | --port=PORT | FTPD_PORT or 2121 | FTP control port | | --public-host=HOST | FTPD_PUBLIC_HOST | Host/IP advertised in passive replies | | --pasv-start=PORT | FTPD_PASSIVE_PORT_START or 2122 | First passive data port | | --pasv-end=PORT | FTPD_PASSIVE_PORT_END or 2149 | Last passive data port | | --daemon | off | Background daemon mode | | --pid-file=FILE | data/run/ftpd.pid | PID file path | | --log-file=FILE | data/logs/ftpd.log | Log file path | | --log-level=LEVEL | INFO | Log level | | --no-console | off | Disable console logging |

Running It on Windows

The FTP daemon uses a stream_select() event loop and does not require pcntl_fork for normal operation.

Run it in the foreground on Windows:

php scripts/ftp_daemon.php

--daemon is Unix-oriented and requires pcntl_fork.

Running as a Service

systemd

[Unit]
Description=BinktermPHP FTP Daemon
After=network.target

[Service]
Type=simple
User=yourusername
Group=yourusername
WorkingDirectory=/path/to/binktest
ExecStart=/usr/bin/php /path/to/binktest/scripts/ftp_daemon.php
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Then:

sudo systemctl daemon-reload
sudo systemctl enable binkterm-ftpd
sudo systemctl start binkterm-ftpd

Cron (@reboot)

@reboot /usr/bin/php /path/to/binktest/scripts/ftp_daemon.php --daemon

Virtual Filesystem

Authenticated Users

  • `/qwk/download/<BBSID>.QWK` generates the user's QWK packet on demand
  • `/qwk/upload/.REPor/qwk/upload/.ZIP` imports REP replies
  • `/incoming/<AREA>/...` uploads files to writable file areas
  • `/fileareas/...` browses and downloads approved files

Anonymous Users

Anonymous login is disabled by default. To enable it, set in .env:

FTPD_ALLOW_ANONYMOUS=true

When enabled, anonymous login is only permitted on registered systems, using:

  • username: `anonymous` or `ftp`
  • password: any string

Anonymous users are restricted to:

  • `/fileareas/...` only
  • file areas marked `is_public`

Anonymous users cannot:

  • access `/qwk/...`
  • upload to `/incoming/...`
  • upload REP packets

If the system is not registered, anonymous login is rejected regardless of the FTPD_ALLOW_ANONYMOUS setting.

Port 21 Without Running as Root

Linux only allows root to bind directly to ports below 1024, including FTP port 21.

Because of that, the recommended deployment is:

  • run `scripts/ftp_daemon.php` as a normal user on `2121`
  • redirect external port `21` to internal port `2121`
  • forward the passive range to the daemon unchanged

iptables Redirect Rules

Local Redirect on the Same Host

Use this when the machine itself receives traffic on port 21 and the FTP daemon runs locally on 2121.

sudo iptables -t nat -A PREROUTING -p tcp --dport 21 -j REDIRECT --to-ports 2121

If clients on the same host connect to the machine's own public IP, you may also need:

sudo iptables -t nat -A OUTPUT -p tcp -d YOUR.SERVER.IP --dport 21 -j REDIRECT --to-ports 2121

Replace YOUR.SERVER.IP with the machine's actual IP.

Port Forward From a Router or Firewall

If the daemon sits behind a router/NAT device, configure these forwards:

  • external TCP `21` -> internal TCP `2121`
  • external TCP `2122`-`2149` -> internal TCP `2122`-`2149`

Then set:

FTPD_PUBLIC_HOST=your.public.hostname

or:

FTPD_PUBLIC_HOST=203.0.113.10

nftables Redirect Rules

Local Redirect on the Same Host

The equivalent nftables redirect rule looks like this:

sudo nft add table ip nat
sudo nft 'add chain ip nat prerouting { type nat hook prerouting priority dstnat; }'
sudo nft add rule ip nat prerouting tcp dport 21 redirect to :2121

If clients on the same host connect to the machine's own public IP, you may also need an output-chain redirect:

sudo nft 'add chain ip nat output { type nat hook output priority -100; }'
sudo nft add rule ip nat output ip daddr YOUR.SERVER.IP tcp dport 21 redirect to :2121

Replace YOUR.SERVER.IP with the machine's actual IP.

Port Forward From a Router or Firewall

If the daemon sits behind a router/NAT device, the forwarding requirements are the same as with iptables:

  • external TCP `21` -> internal TCP `2121`
  • external TCP `2122`-`2149` -> internal TCP `2122`-`2149`

Persisting iptables

Rules added with iptables are not persistent by default.

Common persistence methods:

  • Debian/Ubuntu: `iptables-persistent`
  • RHEL/Alma/Rocky: firewalld rich/direct rules, or a saved ruleset restored at boot
  • custom boot script or systemd unit

Example on Debian/Ubuntu:

sudo apt-get install iptables-persistent
sudo netfilter-persistent save

Persisting nftables

On systems that use nftables natively, save the running ruleset and ensure it is loaded at boot.

Example:

sudo sh -c 'nft list ruleset > /etc/nftables.conf'
sudo systemctl enable nftables
sudo systemctl restart nftables

Firewall Checklist

Allow inbound TCP for:

  • `21` if you are exposing the standard FTP port externally
  • `2121` if clients connect directly to the daemon without redirect
  • `2122`-`2149` for passive transfers

If passive ports are not reachable, login may succeed but directory listings and file transfers will fail.

Logging

The daemon logs to:

data/logs/ftpd.log

Logged events include:

  • user logins
  • anonymous logins
  • uploads
  • downloads
  • transfer byte counts
  • success/failure status

Troubleshooting

Daemon exits immediately with "FTPD is disabled" - Set FTPD_ENABLED=true in .env

Login works but LIST or downloads fail - verify the passive port range is forwarded and allowed through firewalls - verify FTPD_PUBLIC_HOST matches the address clients actually use

Works on LAN but not from the Internet - check router/NAT port forwards - check external firewall rules - confirm passive ports are forwarded, not just the control port

Cannot bind to port 21 - expected when not running as root - keep the daemon on 2121 and use redirect/forwarding rules instead

Windows background mode fails - run in foreground on Windows - --daemon requires Unix pcntl_fork

See Also