Server-Config UI (aktiv/passiv) und Port-Forwarding Tipps in README

- FTP-Tab: Server Configuration mit Active/Passive Radio-Buttons
- Passive Mode: Port-Range konfigurierbar (Default 30000-31000)
- Apply-Button startet Pure-FTPd automatisch neu
- Helper-Script: serverconfig show/active/passive Kommandos
- sudoers: systemctl restart, tee und rm für Pure-FTPd Config
- README: Port-Forwarding Anleitung für Windows (netsh) und Linux (socat/iptables)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-04 19:39:24 +02:00
parent 32d95bb334
commit 2c2c5c7821
6 changed files with 383 additions and 7 deletions

View File

@ -211,6 +211,96 @@ kde-dolphin-ftp-sharing-tab/
| `ro` | Read Only — nur Download | | `ro` | Read Only — nur Download |
| `rw` | Read-Write — Upload und Download | | `rw` | Read-Write — Upload und Download |
## Tipps: Port-Forwarding für FTP
### Szenario
Ein Gerät (z.B. ein Dokumentenscanner) soll per FTP auf den Server scannen, hat aber **keinen direkten Zugang** zum Server-Netzwerk (z.B. VPN). Ein Windows- oder Linux-Rechner, der in **beiden Netzen** hängt, leitet die FTP-Ports weiter.
```
Scanner ──► Windows/Linux (Port-Forwarding) ──► VPN ──► FTP-Server
```
### Aktiver Modus (empfohlen für Port-Forwarding)
Im aktiven Modus müssen nur **2 Ports** weitergeleitet werden: **20** (Daten) und **21** (Steuerung).
### Passiver Modus
Im passiven Modus muss zusätzlich die **gesamte Port-Range** weitergeleitet werden (z.B. 30000-31000). Das ist aufwändiger, aber nötig wenn aktiver Modus nicht funktioniert.
### Windows: Port-Forwarding mit netsh
Als Administrator in der Eingabeaufforderung (cmd):
```batch
:: Aktiver Modus — Port 21 (Steuerung) und Port 20 (Daten) weiterleiten
netsh interface portproxy add v4tov4 listenport=21 listenaddress=0.0.0.0 connectport=21 connectaddress=<SERVER-VPN-IP>
netsh interface portproxy add v4tov4 listenport=20 listenaddress=0.0.0.0 connectport=20 connectaddress=<SERVER-VPN-IP>
:: Passiver Modus — zusätzlich die Port-Range weiterleiten
:: (PowerShell, da netsh keine Ranges unterstützt)
for ($p=30000; $p -le 31000; $p++) { netsh interface portproxy add v4tov4 listenport=$p listenaddress=0.0.0.0 connectport=$p connectaddress=<SERVER-VPN-IP> }
```
**Windows-Firewall öffnen:**
```batch
netsh advfirewall firewall add rule name="FTP Active" dir=in action=allow protocol=TCP localport=20,21
:: Für passiven Modus zusätzlich:
netsh advfirewall firewall add rule name="FTP Passive" dir=in action=allow protocol=TCP localport=30000-31000
```
**Port-Forwarding anzeigen / entfernen:**
```batch
:: Alle Weiterleitungen anzeigen
netsh interface portproxy show all
:: Einzelne Weiterleitung entfernen
netsh interface portproxy delete v4tov4 listenport=21 listenaddress=0.0.0.0
:: Alle Weiterleitungen entfernen
netsh interface portproxy reset
```
### Linux: Port-Forwarding mit iptables oder socat
**Variante 1: socat (einfach, gut zum Testen)**
```bash
# Aktiver Modus
sudo socat TCP-LISTEN:21,fork,reuseaddr TCP:<SERVER-VPN-IP>:21 &
sudo socat TCP-LISTEN:20,fork,reuseaddr TCP:<SERVER-VPN-IP>:20 &
# Passiver Modus — zusätzlich Port-Range
for p in $(seq 30000 31000); do
sudo socat TCP-LISTEN:$p,fork,reuseaddr TCP:<SERVER-VPN-IP>:$p &
done
```
**Variante 2: iptables (performanter, dauerhaft)**
```bash
# IP-Forwarding aktivieren
sudo sysctl -w net.ipv4.ip_forward=1
# Aktiver Modus
sudo iptables -t nat -A PREROUTING -p tcp --dport 21 -j DNAT --to-destination <SERVER-VPN-IP>:21
sudo iptables -t nat -A PREROUTING -p tcp --dport 20 -j DNAT --to-destination <SERVER-VPN-IP>:20
sudo iptables -t nat -A POSTROUTING -j MASQUERADE
# Passiver Modus — zusätzlich Port-Range
sudo iptables -t nat -A PREROUTING -p tcp --dport 30000:31000 -j DNAT --to-destination <SERVER-VPN-IP>
```
**Dauerhaft speichern:**
```bash
sudo apt install iptables-persistent
sudo netfilter-persistent save
```
## Lizenz ## Lizenz
GPLv2+ GPLv2+

View File

@ -5,6 +5,7 @@
#include <QFormLayout> #include <QFormLayout>
#include <QGroupBox> #include <QGroupBox>
#include <QHBoxLayout>
#include <QInputDialog> #include <QInputDialog>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
@ -152,6 +153,47 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword); connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword);
// Server configuration section
auto *serverGroup = new QGroupBox(i18n("Server Configuration"));
auto *serverLayout = new QVBoxLayout(serverGroup);
// Active mode
m_activeRadio = new QRadioButton(i18n("Active Mode (Port 20)"));
serverLayout->addWidget(m_activeRadio);
// Passive mode
m_passiveRadio = new QRadioButton(i18n("Passive Mode"));
serverLayout->addWidget(m_passiveRadio);
// Passive port range
m_passivePortsWidget = new QWidget();
auto *passivePortsLayout = new QHBoxLayout(m_passivePortsWidget);
passivePortsLayout->setContentsMargins(20, 0, 0, 0);
passivePortsLayout->addWidget(new QLabel(i18n("Port-Range:")));
m_passiveStartSpin = new QSpinBox();
m_passiveStartSpin->setRange(1024, 65535);
m_passiveStartSpin->setValue(30000);
passivePortsLayout->addWidget(m_passiveStartSpin);
passivePortsLayout->addWidget(new QLabel(QStringLiteral("-")));
m_passiveEndSpin = new QSpinBox();
m_passiveEndSpin->setRange(1024, 65535);
m_passiveEndSpin->setValue(31000);
passivePortsLayout->addWidget(m_passiveEndSpin);
passivePortsLayout->addStretch();
serverLayout->addWidget(m_passivePortsWidget);
// Apply button
auto *applyServerBtn = new QPushButton(i18n("Apply Server Config"));
serverLayout->addWidget(applyServerBtn);
mainLayout->addWidget(serverGroup);
connect(m_activeRadio, &QRadioButton::toggled, this, &FtpSharePlugin::onModeChanged);
connect(applyServerBtn, &QPushButton::clicked, this, &FtpSharePlugin::onApplyServerConfig);
// Load current server config
loadServerConfig();
mainLayout->addStretch(); mainLayout->addStretch();
// Connect signals // Connect signals
@ -335,6 +377,71 @@ void FtpSharePlugin::applyChanges()
} }
} }
void FtpSharePlugin::loadServerConfig()
{
QProcess proc;
proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("serverconfig"), QStringLiteral("show")});
proc.waitForFinished(5000);
const QString output = QString::fromLocal8Bit(proc.readAllStandardOutput());
bool isPassive = false;
int startPort = 30000;
int endPort = 31000;
const QStringList lines = output.split(QLatin1Char('\n'));
for (const QString &line : lines) {
if (line.startsWith(QLatin1String("mode=passive")))
isPassive = true;
else if (line.startsWith(QLatin1String("passive_start=")))
startPort = line.mid(14).toInt();
else if (line.startsWith(QLatin1String("passive_end=")))
endPort = line.mid(12).toInt();
}
if (isPassive) {
m_passiveRadio->setChecked(true);
m_passiveStartSpin->setValue(startPort);
m_passiveEndSpin->setValue(endPort);
} else {
m_activeRadio->setChecked(true);
}
onModeChanged();
}
void FtpSharePlugin::onModeChanged()
{
m_passivePortsWidget->setEnabled(m_passiveRadio->isChecked());
}
void FtpSharePlugin::onApplyServerConfig()
{
QProcess proc;
if (m_activeRadio->isChecked()) {
proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("serverconfig"), QStringLiteral("active")});
} else {
proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("serverconfig"), QStringLiteral("passive"),
QString::number(m_passiveStartSpin->value()),
QString::number(m_passiveEndSpin->value())});
}
proc.waitForFinished(10000);
if (proc.exitCode() == 0) {
QMessageBox::information(m_page,
i18n("Server Configuration"),
m_activeRadio->isChecked()
? i18n("Server set to active mode (Port 20).\nPure-FTPd restarted.")
: i18n("Server set to passive mode (Ports %1-%2).\nPure-FTPd restarted.",
m_passiveStartSpin->value(), m_passiveEndSpin->value()));
} else {
QMessageBox::warning(m_page,
i18n("Server Configuration"),
i18n("Failed to apply server configuration."));
}
}
void FtpSharePlugin::onChangePassword() void FtpSharePlugin::onChangePassword()
{ {
const QString username = m_pwUserCombo->currentText(); const QString username = m_pwUserCombo->currentText();

View File

@ -6,6 +6,8 @@
#include <QComboBox> #include <QComboBox>
#include <QLineEdit> #include <QLineEdit>
#include <QPushButton> #include <QPushButton>
#include <QRadioButton>
#include <QSpinBox>
#include <QWidget> #include <QWidget>
struct UserPermission { struct UserPermission {
@ -22,8 +24,11 @@ public:
private: private:
void loadCurrentShare(); void loadCurrentShare();
void loadServerConfig();
void onShareToggled(bool checked); void onShareToggled(bool checked);
void onChangePassword(); void onChangePassword();
void onApplyServerConfig();
void onModeChanged();
QString m_path; QString m_path;
QString m_defaultShareName; QString m_defaultShareName;
@ -34,6 +39,13 @@ private:
QWidget *m_usersWidget = nullptr; QWidget *m_usersWidget = nullptr;
QComboBox *m_pwUserCombo = nullptr; QComboBox *m_pwUserCombo = nullptr;
// Server config
QRadioButton *m_activeRadio = nullptr;
QRadioButton *m_passiveRadio = nullptr;
QSpinBox *m_passiveStartSpin = nullptr;
QSpinBox *m_passiveEndSpin = nullptr;
QWidget *m_passivePortsWidget = nullptr;
QVector<UserPermission> m_userPerms; QVector<UserPermission> m_userPerms;
bool m_isShared = false; bool m_isShared = false;

View File

@ -5,6 +5,7 @@
#include <QFormLayout> #include <QFormLayout>
#include <QGroupBox> #include <QGroupBox>
#include <QHBoxLayout>
#include <QInputDialog> #include <QInputDialog>
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
@ -152,6 +153,47 @@ FtpSharePlugin::FtpSharePlugin(QObject *parent, const QVariantList &args)
connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword); connect(pwButton, &QPushButton::clicked, this, &FtpSharePlugin::onChangePassword);
// Server configuration section
auto *serverGroup = new QGroupBox(i18n("Server Configuration"));
auto *serverLayout = new QVBoxLayout(serverGroup);
// Active mode
m_activeRadio = new QRadioButton(i18n("Active Mode (Port 20)"));
serverLayout->addWidget(m_activeRadio);
// Passive mode
m_passiveRadio = new QRadioButton(i18n("Passive Mode"));
serverLayout->addWidget(m_passiveRadio);
// Passive port range
m_passivePortsWidget = new QWidget();
auto *passivePortsLayout = new QHBoxLayout(m_passivePortsWidget);
passivePortsLayout->setContentsMargins(20, 0, 0, 0);
passivePortsLayout->addWidget(new QLabel(i18n("Port-Range:")));
m_passiveStartSpin = new QSpinBox();
m_passiveStartSpin->setRange(1024, 65535);
m_passiveStartSpin->setValue(30000);
passivePortsLayout->addWidget(m_passiveStartSpin);
passivePortsLayout->addWidget(new QLabel(QStringLiteral("-")));
m_passiveEndSpin = new QSpinBox();
m_passiveEndSpin->setRange(1024, 65535);
m_passiveEndSpin->setValue(31000);
passivePortsLayout->addWidget(m_passiveEndSpin);
passivePortsLayout->addStretch();
serverLayout->addWidget(m_passivePortsWidget);
// Apply button
auto *applyServerBtn = new QPushButton(i18n("Apply Server Config"));
serverLayout->addWidget(applyServerBtn);
mainLayout->addWidget(serverGroup);
connect(m_activeRadio, &QRadioButton::toggled, this, &FtpSharePlugin::onModeChanged);
connect(applyServerBtn, &QPushButton::clicked, this, &FtpSharePlugin::onApplyServerConfig);
// Load current server config
loadServerConfig();
mainLayout->addStretch(); mainLayout->addStretch();
// Connect signals // Connect signals
@ -335,6 +377,71 @@ void FtpSharePlugin::applyChanges()
} }
} }
void FtpSharePlugin::loadServerConfig()
{
QProcess proc;
proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("serverconfig"), QStringLiteral("show")});
proc.waitForFinished(5000);
const QString output = QString::fromLocal8Bit(proc.readAllStandardOutput());
bool isPassive = false;
int startPort = 30000;
int endPort = 31000;
const QStringList lines = output.split(QLatin1Char('\n'));
for (const QString &line : lines) {
if (line.startsWith(QLatin1String("mode=passive")))
isPassive = true;
else if (line.startsWith(QLatin1String("passive_start=")))
startPort = line.mid(14).toInt();
else if (line.startsWith(QLatin1String("passive_end=")))
endPort = line.mid(12).toInt();
}
if (isPassive) {
m_passiveRadio->setChecked(true);
m_passiveStartSpin->setValue(startPort);
m_passiveEndSpin->setValue(endPort);
} else {
m_activeRadio->setChecked(true);
}
onModeChanged();
}
void FtpSharePlugin::onModeChanged()
{
m_passivePortsWidget->setEnabled(m_passiveRadio->isChecked());
}
void FtpSharePlugin::onApplyServerConfig()
{
QProcess proc;
if (m_activeRadio->isChecked()) {
proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("serverconfig"), QStringLiteral("active")});
} else {
proc.start(QStringLiteral("dolphin-ftp-share"),
{QStringLiteral("serverconfig"), QStringLiteral("passive"),
QString::number(m_passiveStartSpin->value()),
QString::number(m_passiveEndSpin->value())});
}
proc.waitForFinished(10000);
if (proc.exitCode() == 0) {
QMessageBox::information(m_page,
i18n("Server Configuration"),
m_activeRadio->isChecked()
? i18n("Server set to active mode (Port 20).\nPure-FTPd restarted.")
: i18n("Server set to passive mode (Ports %1-%2).\nPure-FTPd restarted.",
m_passiveStartSpin->value(), m_passiveEndSpin->value()));
} else {
QMessageBox::warning(m_page,
i18n("Server Configuration"),
i18n("Failed to apply server configuration."));
}
}
void FtpSharePlugin::onChangePassword() void FtpSharePlugin::onChangePassword()
{ {
const QString username = m_pwUserCombo->currentText(); const QString username = m_pwUserCombo->currentText();

View File

@ -6,6 +6,8 @@
#include <QComboBox> #include <QComboBox>
#include <QLineEdit> #include <QLineEdit>
#include <QPushButton> #include <QPushButton>
#include <QRadioButton>
#include <QSpinBox>
#include <QWidget> #include <QWidget>
struct UserPermission { struct UserPermission {
@ -22,8 +24,11 @@ public:
private: private:
void loadCurrentShare(); void loadCurrentShare();
void loadServerConfig();
void onShareToggled(bool checked); void onShareToggled(bool checked);
void onChangePassword(); void onChangePassword();
void onApplyServerConfig();
void onModeChanged();
QString m_path; QString m_path;
QString m_defaultShareName; QString m_defaultShareName;
@ -34,6 +39,13 @@ private:
QWidget *m_usersWidget = nullptr; QWidget *m_usersWidget = nullptr;
QComboBox *m_pwUserCombo = nullptr; QComboBox *m_pwUserCombo = nullptr;
// Server config
QRadioButton *m_activeRadio = nullptr;
QRadioButton *m_passiveRadio = nullptr;
QSpinBox *m_passiveStartSpin = nullptr;
QSpinBox *m_passiveEndSpin = nullptr;
QWidget *m_passivePortsWidget = nullptr;
QVector<UserPermission> m_userPerms; QVector<UserPermission> m_userPerms;
bool m_isShared = false; bool m_isShared = false;

View File

@ -92,6 +92,10 @@ ${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/mount --bind *
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/mount -o remount?ro?bind * ${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/mount -o remount?ro?bind *
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/umount ${REAL_FTP_ROOT}/* ${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/umount ${REAL_FTP_ROOT}/*
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/pure-pw * ${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/pure-pw *
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/tee /etc/pure-ftpd/conf/*
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/rm -f /etc/pure-ftpd/conf/PassivePortRange
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/sed -i *pureftpd.passwd*
${REAL_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart pure-ftpd
EOF EOF
sudo chmod 440 /etc/sudoers.d/dolphin-ftp-share sudo chmod 440 /etc/sudoers.d/dolphin-ftp-share
@ -225,15 +229,56 @@ cmd_passwd() {
rebuild_db rebuild_db
} }
cmd_serverconfig() {
local action="$1"
case "$action" in
show)
# Output current mode and ports
if [ -f /etc/pure-ftpd/conf/PassivePortRange ]; then
local range
range=$(cat /etc/pure-ftpd/conf/PassivePortRange)
echo "mode=passive"
echo "passive_start=${range%% *}"
echo "passive_end=${range##* }"
else
echo "mode=active"
fi
;;
active)
# Remove passive port range → only active mode
sudo rm -f /etc/pure-ftpd/conf/PassivePortRange
sudo systemctl restart pure-ftpd
echo "Server set to active mode"
;;
passive)
local start_port="$2"
local end_port="$3"
if [ -z "$start_port" ] || [ -z "$end_port" ]; then
echo "Error: start and end port required" >&2
exit 1
fi
echo "${start_port} ${end_port}" | sudo tee /etc/pure-ftpd/conf/PassivePortRange >/dev/null
sudo systemctl restart pure-ftpd
echo "Server set to passive mode (ports ${start_port}-${end_port})"
;;
*)
echo "Usage: dolphin-ftp-share serverconfig {show|active|passive <start> <end>}" >&2
exit 1
;;
esac
}
case "${1:-}" in case "${1:-}" in
setup) cmd_setup ;; setup) cmd_setup ;;
info) cmd_info ;; info) cmd_info ;;
add) cmd_add "$2" "$3" "${4:-}" ;; add) cmd_add "$2" "$3" "${4:-}" ;;
delete) cmd_delete "$2" ;; delete) cmd_delete "$2" ;;
userexists) cmd_userexists "$2" ;; userexists) cmd_userexists "$2" ;;
passwd) cmd_passwd "$2" "$3" ;; passwd) cmd_passwd "$2" "$3" ;;
serverconfig) cmd_serverconfig "$2" "$3" "$4" ;;
*) *)
echo "Usage: dolphin-ftp-share {setup|info|add|delete|userexists|passwd}" >&2 echo "Usage: dolphin-ftp-share {setup|info|add|delete|userexists|passwd|serverconfig}" >&2
echo "" >&2 echo "" >&2
echo "Commands:" >&2 echo "Commands:" >&2
echo " setup Initial Pure-FTPd setup" >&2 echo " setup Initial Pure-FTPd setup" >&2
@ -242,6 +287,9 @@ case "${1:-}" in
echo " delete <name> Remove a share" >&2 echo " delete <name> Remove a share" >&2
echo " userexists <username> Check if FTP user exists" >&2 echo " userexists <username> Check if FTP user exists" >&2
echo " passwd <username> <password> Set/create FTP password" >&2 echo " passwd <username> <password> Set/create FTP password" >&2
echo " serverconfig show Show current server config" >&2
echo " serverconfig active Set active mode" >&2
echo " serverconfig passive <start> <end> Set passive mode with port range" >&2
exit 1 exit 1
;; ;;
esac esac