diff --git a/Dockerfile b/Dockerfile index e7dcc5b6..145c81fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,7 +54,8 @@ RUN apk add --no-cache \ ca-certificates \ tzdata \ fail2ban \ - bash + bash \ + curl WORKDIR /app diff --git a/database/model/model.go b/database/model/model.go index 4ca39d87..6225df52 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -80,9 +80,12 @@ type HistoryOfSeeders struct { // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model. func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { listen := i.Listen - if listen != "" { - listen = fmt.Sprintf("\"%v\"", listen) + // Default to 0.0.0.0 (all interfaces) when listen is empty + // This ensures proper dual-stack IPv4/IPv6 binding in systems where bindv6only=0 + if listen == "" { + listen = "0.0.0.0" } + listen = fmt.Sprintf("\"%v\"", listen) return &xray.InboundConfig{ Listen: json_util.RawMessage(listen), Port: i.Port, diff --git a/install.sh b/install.sh index 93d77b23..00111845 100644 --- a/install.sh +++ b/install.sh @@ -53,7 +53,24 @@ is_ip() { is_ipv4 "$1" || is_ipv6 "$1" } is_domain() { - [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 + [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 +} + +# Port helpers +is_port_in_use() { + local port="$1" + if command -v ss >/dev/null 2>&1; then + ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' + return + fi + if command -v netstat >/dev/null 2>&1; then + netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' + return + fi + if command -v lsof >/dev/null 2>&1; then + lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 + fi + return 1 } install_base() { @@ -180,7 +197,7 @@ setup_ip_certificate() { echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}" echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}" - echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}" + echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}" # Check for acme.sh if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then @@ -216,6 +233,43 @@ setup_ip_certificate() { # Set reload command for auto-renewal (add || true so it doesn't fail during first install) local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true" + # Choose port for HTTP-01 listener (default 80, prompt override) + local WebPort="" + read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort + WebPort="${WebPort:-80}" + if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then + echo -e "${red}Invalid port provided. Falling back to 80.${plain}" + WebPort=80 + fi + echo -e "${green}Using port ${WebPort} for standalone validation.${plain}" + if [[ "${WebPort}" -ne 80 ]]; then + echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}" + fi + + # Ensure chosen port is available + while true; do + if is_port_in_use "${WebPort}"; then + echo -e "${yellow}Port ${WebPort} is in use.${plain}" + + local alt_port="" + read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port + alt_port="${alt_port// /}" + if [[ -z "${alt_port}" ]]; then + echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}" + return 1 + fi + if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then + echo -e "${red}Invalid port provided.${plain}" + return 1 + fi + WebPort="${alt_port}" + continue + else + echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}" + break + fi + done + # Issue certificate with shortlived profile echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1 @@ -226,12 +280,12 @@ setup_ip_certificate() { --server letsencrypt \ --certificate-profile shortlived \ --days 6 \ - --httpport 80 \ + --httpport ${WebPort} \ --force if [ $? -ne 0 ]; then echo -e "${red}Failed to issue IP certificate${plain}" - echo -e "${yellow}Please ensure port 80 is open and accessible from the internet${plain}" + echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${ipv4} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null diff --git a/lib/common.sh b/lib/common.sh index ca1cc80d..3f442c79 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -39,7 +39,7 @@ is_ip() { } is_domain() { - [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 + [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 } # Generate random string diff --git a/lib/ssl.sh b/lib/ssl.sh index 5ba2bb19..6fefdfac 100644 --- a/lib/ssl.sh +++ b/lib/ssl.sh @@ -30,6 +30,23 @@ install_acme() { return 0 } +is_port_in_use() { + local port="$1" + if command -v ss >/dev/null 2>&1; then + ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' + return + fi + if command -v netstat >/dev/null 2>&1; then + netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' + return + fi + if command -v lsof >/dev/null 2>&1; then + lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 + fi + return 1 +} + + ssl_cert_issue_main() { echo -e "${green}\t1.${plain} Get SSL (Domain)" echo -e "${green}\t2.${plain} Revoke" @@ -224,10 +241,41 @@ ssl_cert_issue_for_ip() { LOGI "Including IPv6 address: ${ipv6_addr}" fi - # Use port 80 for certificate issuance - local WebPort=80 + # Choose port for HTTP-01 listener (default 80, allow override) + local WebPort="" + read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort + WebPort="${WebPort:-80}" + if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then + LOGE "Invalid port provided. Falling back to 80." + WebPort=80 + fi LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}" - LOGI "Make sure port ${WebPort} is open and not in use..." + if [[ "${WebPort}" -ne 80 ]]; then + LOGI "Reminder: Let's Encrypt still reaches port 80; forward external port 80 to ${WebPort} for validation." + fi + + while true; do + if is_port_in_use "${WebPort}"; then + LOGI "Port ${WebPort} is currently in use." + + local alt_port="" + read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port + alt_port="${alt_port// /}" + if [[ -z "${alt_port}" ]]; then + LOGE "Port ${WebPort} is busy; cannot proceed with issuance." + return 1 + fi + if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then + LOGE "Invalid port provided." + return 1 + fi + WebPort="${alt_port}" + continue + else + LOGI "Port ${WebPort} is free and ready for standalone validation." + break + fi + done # Reload command - restarts panel after renewal local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null" diff --git a/update.sh b/update.sh index 9f69f053..91c37c37 100755 --- a/update.sh +++ b/update.sh @@ -78,7 +78,24 @@ is_ip() { is_ipv4 "$1" || is_ipv6 "$1" } is_domain() { - [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 + [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 +} + +# Port helpers +is_port_in_use() { + local port="$1" + if command -v ss >/dev/null 2>&1; then + ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' + return + fi + if command -v netstat >/dev/null 2>&1; then + netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' + return + fi + if command -v lsof >/dev/null 2>&1; then + lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 + fi + return 1 } gen_random_string() { @@ -205,7 +222,7 @@ setup_ip_certificate() { echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}" echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}" - echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}" + echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}" # Check for acme.sh if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then @@ -241,6 +258,43 @@ setup_ip_certificate() { # Set reload command for auto-renewal (add || true so it doesn't fail if service stopped) local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true" + # Choose port for HTTP-01 listener (default 80, prompt override) + local WebPort="" + read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort + WebPort="${WebPort:-80}" + if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then + echo -e "${red}Invalid port provided. Falling back to 80.${plain}" + WebPort=80 + fi + echo -e "${green}Using port ${WebPort} for standalone validation.${plain}" + if [[ "${WebPort}" -ne 80 ]]; then + echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}" + fi + + # Ensure chosen port is available + while true; do + if is_port_in_use "${WebPort}"; then + echo -e "${yellow}Port ${WebPort} is currently in use.${plain}" + + local alt_port="" + read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port + alt_port="${alt_port// /}" + if [[ -z "${alt_port}" ]]; then + echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}" + return 1 + fi + if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then + echo -e "${red}Invalid port provided.${plain}" + return 1 + fi + WebPort="${alt_port}" + continue + else + echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}" + break + fi + done + # Issue certificate with shortlived profile echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1 @@ -251,12 +305,12 @@ setup_ip_certificate() { --server letsencrypt \ --certificate-profile shortlived \ --days 6 \ - --httpport 80 \ + --httpport ${WebPort} \ --force if [ $? -ne 0 ]; then echo -e "${red}Failed to issue IP certificate${plain}" - echo -e "${yellow}Please ensure port 80 is open and accessible from the internet${plain}" + echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${ipv4} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null diff --git a/web/service/xray.go b/web/service/xray.go index 43178d2f..511ffdda 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -40,6 +40,9 @@ func (s *XrayService) GetXrayErr() error { } err := p.GetErr() + if err == nil { + return nil + } if runtime.GOOS == "windows" && err.Error() == "exit status 1" { // exit status 1 on Windows means that Xray process was killed