Feat/UI redesign production docker#12
Conversation
Android Chrome renames text/plain downloads whose extension it doesn't associate with that MIME type, so "x.conf" landed in Downloads as "x.conf.txt" - which the WireGuard app refuses to import. Switch both download surfaces (the frontend Download .conf blob and the subscription endpoint's attachment response) to application/octet-stream; the OpenAPI spec documents the change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three real-world failures from a node install session: - A pasted join token with trailing newlines silently answered the next prompts, leaving required settings (node name, WG interface address) empty in .env. Required prompts now re-ask until non-empty, strip pasted CRs, and validate formats (host:port, numeric port, IPv4/CIDR). - The control-plane address is the panel's NODE_AGENT_PORT (48443 by default), but the prompt didn't say so and accepted anything - users naturally enter the WireGuard port. The prompt now spells it out and probes the address over TCP, warning + confirming before accepting an unreachable one. - "ERROR: problem running iptables/ufw-init" (classically a kernel upgraded without a reboot) aborted the whole install at the firewall step under set -e. Both installers now warn with the exact manual rule and continue instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A real node install failed with the agent looping on "register rejected: 405 Method Not Allowed" + nginx HTML: the control-plane address was given as the panel's web port (443), where Caddy routes /agent/register to the SPA's nginx. Attack the mistake from every side: - The join-token response now carries panel_addr (panel domain + NODE_AGENT_PORT) and the join-token dialog displays it with a copy button as "enter exactly this", so admins never guess the port. - install-node.sh probes https://<addr>/agent/register after the TCP check; an HTML answer means "this is the web UI" and the installer says so and re-prompts. - The agent itself detects an HTML/405 register response and explains the NODE_AGENT_PORT mix-up instead of dumping the nginx error page. Also rewrite `wgpanel uninstall`, which only ran `docker compose down -v` on the panel project (and silently exited non-zero unless you typed "yes"): it now tears down the self-node stack too, deletes /opt/wgpanel and /opt/wgpanel-node, removes the CLI itself, and prints exactly what it is about to destroy before asking for confirmation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request improves the node onboarding process by surfacing the correct control plane address (panel_addr) in the UI and validating it during installation. It also updates subscription configuration downloads to use application/octet-stream to prevent Android Chrome from renaming .conf files to .conf.txt, and enhances the uninstallation script. The review feedback highlights a potential issue with using /dev/tcp in bash on systems where it is disabled, a directory context issue when running docker compose down in the uninstaller, and a case-sensitivity issue when checking the Content-Type header.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if ! timeout 5 bash -c ": </dev/tcp/${PANEL_ADDR%:*}/${PANEL_ADDR##*:}" 2>/dev/null; then | ||
| warn "Cannot reach ${PANEL_ADDR} over TCP. Check the address: the port must be the" | ||
| warn "panel's node-agent port (NODE_AGENT_PORT in the panel's .env, 48443 by default)," | ||
| warn "not the WireGuard port, and it must be open in the panel server's firewall." | ||
| read -rp "Use ${PANEL_ADDR} anyway? [y/N]: " CONFIRM | ||
| [[ "${CONFIRM,,}" == y* ]] && break | ||
| continue | ||
| fi | ||
| # Reachable is not enough: the panel's WEB port (443) also answers TCP. The real | ||
| # agent endpoint replies with plain text/JSON, never HTML - an HTML answer means | ||
| # this is the web UI and registration would die with an nginx "405 Not Allowed". | ||
| if curl -skm 5 "https://${PANEL_ADDR}/agent/register" 2>/dev/null | grep -qiE '<html|<!doctype'; then | ||
| warn "${PANEL_ADDR} answers like the panel's WEB UI, not the node-agent API." | ||
| warn "Enter the panel's node-agent port instead: NODE_AGENT_PORT in the panel" | ||
| warn "server's /opt/wgpanel/.env (48443 by default) - e.g. ${PANEL_ADDR%:*}:48443." | ||
| read -rp "Use ${PANEL_ADDR} anyway? [y/N]: " CONFIRM | ||
| [[ "${CONFIRM,,}" == y* ]] && break | ||
| continue | ||
| fi |
There was a problem hiding this comment.
Debian (and by extension, some Ubuntu configurations) disables the /dev/tcp built-in redirection in bash at compile-time for security reasons. When /dev/tcp is disabled, the command bash -c ": </dev/tcp/..." will always fail, causing the script to incorrectly warn that the control plane is unreachable even when it is perfectly healthy.
Since curl is already installed and used in the subsequent check, we can use curl to check both connectivity and the response content in a single request, which is more portable, robust, and efficient.
| if ! timeout 5 bash -c ": </dev/tcp/${PANEL_ADDR%:*}/${PANEL_ADDR##*:}" 2>/dev/null; then | |
| warn "Cannot reach ${PANEL_ADDR} over TCP. Check the address: the port must be the" | |
| warn "panel's node-agent port (NODE_AGENT_PORT in the panel's .env, 48443 by default)," | |
| warn "not the WireGuard port, and it must be open in the panel server's firewall." | |
| read -rp "Use ${PANEL_ADDR} anyway? [y/N]: " CONFIRM | |
| [[ "${CONFIRM,,}" == y* ]] && break | |
| continue | |
| fi | |
| # Reachable is not enough: the panel's WEB port (443) also answers TCP. The real | |
| # agent endpoint replies with plain text/JSON, never HTML - an HTML answer means | |
| # this is the web UI and registration would die with an nginx "405 Not Allowed". | |
| if curl -skm 5 "https://${PANEL_ADDR}/agent/register" 2>/dev/null | grep -qiE '<html|<!doctype'; then | |
| warn "${PANEL_ADDR} answers like the panel's WEB UI, not the node-agent API." | |
| warn "Enter the panel's node-agent port instead: NODE_AGENT_PORT in the panel" | |
| warn "server's /opt/wgpanel/.env (48443 by default) - e.g. ${PANEL_ADDR%:*}:48443." | |
| read -rp "Use ${PANEL_ADDR} anyway? [y/N]: " CONFIRM | |
| [[ "${CONFIRM,,}" == y* ]] && break | |
| continue | |
| fi | |
| local response | |
| if ! response=$(curl -skm 5 "https://${PANEL_ADDR}/agent/register" 2>/dev/null); then | |
| warn "Cannot reach ${PANEL_ADDR} over HTTPS. Check the address: the port must be the" | |
| warn "panel's node-agent port (NODE_AGENT_PORT in the panel's .env, 48443 by default)," | |
| warn "not the WireGuard port, and it must be open in the panel server's firewall." | |
| read -rp "Use ${PANEL_ADDR} anyway? [y/N]: " CONFIRM | |
| [[ "${CONFIRM,,}" == y* ]] && break | |
| continue | |
| fi | |
| # Reachable is not enough: the panel's WEB port (443) also answers TCP. The real | |
| # agent endpoint replies with plain text/JSON, never HTML - an HTML answer means | |
| # this is the web UI and registration would die with an nginx "405 Not Allowed". | |
| if echo "$response" | grep -qiE '<html|<!doctype'; then | |
| warn "${PANEL_ADDR} answers like the panel's WEB UI, not the node-agent API." | |
| warn "Enter the panel's node-agent port instead: NODE_AGENT_PORT in the panel" | |
| warn "server's /opt/wgpanel/.env (48443 by default) - e.g. ${PANEL_ADDR%:*}:48443." | |
| read -rp "Use ${PANEL_ADDR} anyway? [y/N]: " CONFIRM | |
| [[ "${CONFIRM,,}" == y* ]] && break | |
| continue | |
| fi |
| docker compose down -v --remove-orphans \ | ||
| || warn "Panel teardown failed - finish it manually: cd ${INSTALL_DIR} && docker compose down -v" |
There was a problem hiding this comment.
Since wgpanel is installed globally as /usr/local/bin/wgpanel, users can run the uninstall command from any directory. Running docker compose down without changing to the $INSTALL_DIR directory will fail because Docker Compose won't be able to find the docker-compose.yml file.
(cd "$INSTALL_DIR" && docker compose down -v --remove-orphans) \
|| warn "Panel teardown failed - finish it manually: cd ${INSTALL_DIR} && docker compose down -v"
| if resp.StatusCode == http.StatusMethodNotAllowed || | ||
| strings.Contains(resp.Header.Get("Content-Type"), "text/html") || | ||
| bytes.Contains(bytes.ToLower(body), []byte("<html")) { |
There was a problem hiding this comment.
The Content-Type header value can be mixed-case (e.g., Text/HTML or text/HTML). Performing a case-sensitive strings.Contains check on the raw header value might fail to detect HTML responses from some web servers. It is safer to convert the header value to lowercase before checking.
| if resp.StatusCode == http.StatusMethodNotAllowed || | |
| strings.Contains(resp.Header.Get("Content-Type"), "text/html") || | |
| bytes.Contains(bytes.ToLower(body), []byte("<html")) { | |
| if resp.StatusCode == http.StatusMethodNotAllowed || | |
| strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/html") || | |
| bytes.Contains(bytes.ToLower(body), []byte("<html")) { |
No description provided.