Compare commits

..

5 commits

Author SHA1 Message Date
MHSanaei
8098d2b1b1
Return nil if no error in GetXrayErr
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
Added a check to return nil immediately if p.GetErr() returns nil in GetXrayErr, preventing further error handling when no error is present.
2026-01-13 17:40:52 +01:00
VolgaIgor
a691eaea8d
Fixed incorrect filtering for IDN top-level domains (#3666)
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
2026-01-12 02:53:43 +01:00
VolgaIgor
da447e5669
Added curl package to Dockerfile (#3665) 2026-01-11 20:18:54 +01:00
MHSanaei
f8c9aac97c
Add port selection and checks for ACME HTTP-01 listener
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
Introduces user prompts to select the port for ACME HTTP-01 certificate validation (default 80), checks if the chosen port is available, and provides guidance for port forwarding. Adds is_port_in_use helper to all scripts and improves messaging for certificate issuance and error handling.
2026-01-11 15:28:43 +01:00
MHSanaei
e42c17f2b2
Default listen address to 0.0.0.0 in GenXrayInboundConfig
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
When the listen address is empty, it now defaults to 0.0.0.0 to ensure proper dual-stack IPv4/IPv6 binding, improving compatibility on systems with bindv6only=0.
2026-01-09 20:22:33 +01:00
15 changed files with 280 additions and 384 deletions

View file

@ -1,11 +0,0 @@
.git
db
cert
*.log
Dockerfile
docker-compose.yml
.tmp
.idea
.vscode
LICENSE
README.*

3
.gitignore vendored
View file

@ -1,6 +1,3 @@
# shared volume
geodata/
# Ignore editor and IDE settings
.idea/
.vscode/

View file

@ -1,28 +1,7 @@
#!/bin/sh
set -eu
: "${MAX_GEODATA_DIR_WAIT:=30}"
: "${WAIT_INTERVAL:=10}"
: "${GEODATA_DIR:?GEODATA_DIR is required}"
FINISH_FILE="$GEODATA_DIR/cron-job-finished.txt"
ELAPSED=0
while [ ! -f "$FINISH_FILE" ] && [ "$ELAPSED" -lt "$MAX_GEODATA_DIR_WAIT" ]; do
echo "Waiting for geodata initialization... ($ELAPSED/$MAX_GEODATA_DIR_WAIT seconds)"
sleep $WAIT_INTERVAL
ELAPSED=$((ELAPSED + WAIT_INTERVAL))
done
if [ ! -f "$FINISH_FILE" ]; then
echo "ERROR: Geodata initialization timed out after $MAX_GEODATA_DIR_WAIT seconds"
echo "Container startup aborted."
exit 1
fi
# Start fail2ban
[ "$XUI_ENABLE_FAIL2BAN" = "true" ] && fail2ban-client -x start
[ $XUI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start
# Run x-ui
exec /app/x-ui

40
DockerInit.sh Executable file
View file

@ -0,0 +1,40 @@
#!/bin/sh
case $1 in
amd64)
ARCH="64"
FNAME="amd64"
;;
i386)
ARCH="32"
FNAME="i386"
;;
armv8 | arm64 | aarch64)
ARCH="arm64-v8a"
FNAME="arm64"
;;
armv7 | arm | arm32)
ARCH="arm32-v7a"
FNAME="arm32"
;;
armv6)
ARCH="arm32-v6"
FNAME="armv6"
;;
*)
ARCH="64"
FNAME="amd64"
;;
esac
mkdir -p build/bin
cd build/bin
curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}"
curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
cd ../../

View file

@ -2,40 +2,41 @@
# Stage: Builder
# ========================================================
FROM golang:1.25-alpine AS builder
WORKDIR /app
ARG TARGETARCH
RUN apk add --no-cache \
RUN apk --no-cache --update add \
build-base \
gcc
# docker CACHE
COPY go.mod go.sum ./
RUN go mod download
gcc \
curl \
unzip
COPY . .
ENV CGO_ENABLED=1
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
RUN go build -ldflags "-w -s" -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"
# ========================================================
# Stage: Final Image of 3x-ui
# ========================================================
FROM alpine
ENV TZ=Asia/Tehran
WORKDIR /app
RUN apk add --no-cache \
RUN apk add --no-cache --update \
ca-certificates \
tzdata \
fail2ban \
bash
bash \
curl
COPY DockerEntrypoint.sh /app/
COPY --from=builder /app/build/ /app/
COPY --from=builder /app/DockerEntrypoint.sh /app/
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
# Configure fail2ban
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
&& cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \
@ -51,5 +52,5 @@ RUN chmod +x \
ENV XUI_ENABLE_FAIL2BAN="true"
EXPOSE 2053
VOLUME [ "/etc/x-ui" ]
CMD [ "./x-ui" ]
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

View file

@ -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,

View file

@ -1,57 +1,16 @@
services:
3x-ui:
3xui:
build:
context: .
dockerfile: ./Dockerfile
container_name: 3xui_app
# hostname: yourhostname <- optional
volumes:
- $PWD/db/:/etc/x-ui/
- $PWD/cert/:/root/cert/
- $PWD/geodata/:/app/bin
environment:
TZ: "Asia/Tehran"
XRAY_VMESS_AEAD_FORCED: "false"
XUI_ENABLE_FAIL2BAN: "true"
GEODATA_DIR: "/app/bin"
tty: true
network_mode: host
restart: unless-stopped
depends_on:
- geodata-cron
docker-proxy:
image: tecnativa/docker-socket-proxy
container_name: docker_proxy
restart: unless-stopped
environment:
- CONTAINERS=1
- POST=1
- ALLOW_RESTARTS=1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- docker-internal
geodata-cron:
build:
context: docker-cron-runner
args:
XRAY_VERSION: "${XRAY_VERSION:-v25.10.15}"
XRAY_BUILD_DIR: "/app/xray"
container_name: geodata_cron
restart: unless-stopped
depends_on:
- docker-proxy
environment:
TZ: "UTC"
DOCKER_PROXY_URL: "http://docker-proxy:2375"
TARGET_CONTAINER_NAME: "3xui_app"
CRON_SCHEDULE: "0 */6 * * *"
SHARED_VOLUME_PATH: "/app/bin"
volumes:
- $PWD/geodata/:/app/bin/
networks:
- docker-internal
networks:
docker-internal:
driver: bridge

View file

@ -1,28 +0,0 @@
FROM alpine:3.20
ARG TARGETARCH
ARG XRAY_VERSION
ARG XRAY_BUILD_DIR
WORKDIR /app
RUN apk add --no-cache \
wget \
unzip \
curl \
bash \
ca-certificates \
tzdata
COPY xray-tools.sh entrypoint.sh cron-job-script.sh ./
RUN chmod +x /app/xray-tools.sh /app/entrypoint.sh /app/cron-job-script.sh \
&& mkdir -p "$XRAY_BUILD_DIR" \
&& ./xray-tools.sh install_xray_core "$TARGETARCH" "$XRAY_BUILD_DIR" "$XRAY_VERSION" \
&& ./xray-tools.sh update_geodata_in_docker "$XRAY_BUILD_DIR"
ENV XRAY_BUILD_DIR=${XRAY_BUILD_DIR}
#CMD ["/app/entrypoint.sh"] \
ENTRYPOINT ["/app/entrypoint.sh"]

View file

@ -1,23 +0,0 @@
#!/usr/bin/env sh
set -eu
echo "[$(date)] Starting geodata update..."
FINISHED_FLAG="${SHARED_VOLUME_PATH}/cron-job-finished.txt"
if [ -f "$FINISHED_FLAG" ]; then
rm -f "$FINISHED_FLAG"
fi
/app/xray-tools.sh update_geodata_in_docker "${SHARED_VOLUME_PATH}"
touch "$FINISHED_FLAG"
echo "[$(date)] Geodata update finished, restarting container..."
HTTP_CODE=$(
curl -s -X POST \
"${DOCKER_PROXY_URL}/containers/${TARGET_CONTAINER_NAME}/restart" \
-o /dev/null -w "%{http_code}"
)
echo "[$(date)] Restart request sent, HTTP status: ${HTTP_CODE}"

View file

@ -1,25 +0,0 @@
#!/usr/bin/env sh
set -eu
: "${CRON_SCHEDULE:=0 */6 * * *}"
: "${DOCKER_PROXY_URL:?DOCKER_PROXY_URL is required}"
: "${TARGET_CONTAINER_NAME:?TARGET_CONTAINER_NAME is required}" # required for cron-job-script.sh for container restart
: "${SHARED_VOLUME_PATH:?SHARED_VOLUME_PATH is required}"
CRON_ENV_FILE="/env.sh"
env | grep -v '^CRON_SCHEDULE=' | sed 's/^/export /' > "$CRON_ENV_FILE"
echo "${CRON_SCHEDULE} . ${CRON_ENV_FILE} && /app/cron-job-script.sh >> /var/log/cron.log 2>&1" > /etc/crontabs/root
echo "Starting crond with schedule: ${CRON_SCHEDULE}"
mkdir -p /var/log
touch /var/log/cron.log
mkdir -p "$SHARED_VOLUME_PATH"
cp -r "$XRAY_BUILD_DIR"/* "$SHARED_VOLUME_PATH"/
touch "$SHARED_VOLUME_PATH/cron-job-finished.txt" # cron job execution imitation
exec crond -f -l 2

View file

@ -1,173 +0,0 @@
#!/bin/sh
safe_download_and_update() {
url="$1"
dest="$2"
# Create a temporary file
tmp=$(mktemp "${dest}.XXXXXX") || return 1
# Download file into a temporary location
if wget -q -O "$tmp" "$url"; then
# Check that the downloaded file is not empty
if [ -s "$tmp" ]; then
# Atomically replace the destination file
mv "$tmp" "$dest"
echo "[OK] Downloaded: $dest"
else
echo "[ERR] Downloaded file is empty: $url"
rm -f "$tmp"
return 1
fi
else
echo "[ERR] Failed to download: $url"
rm -f "$tmp"
return 1
fi
}
update_all_geofiles() {
update_main_geofiles
update_ir_geofiles
update_ru_geofiles
}
update_main_geofiles() {
safe_download_and_update \
"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" \
"geoip.dat"
safe_download_and_update \
"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" \
"geosite.dat"
}
update_ir_geofiles() {
safe_download_and_update \
"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" \
"geoip_IR.dat"
safe_download_and_update \
"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" \
"geosite_IR.dat"
}
update_ru_geofiles() {
safe_download_and_update \
"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" \
"geoip_RU.dat"
safe_download_and_update \
"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" \
"geosite_RU.dat"
}
update_geodata_in_docker() {
XRAYDIR="$1"
OLD_DIR=$(pwd)
trap 'cd "$OLD_DIR"' EXIT
echo "[$(date)] Running update_geodata"
if [ ! -d "$XRAYDIR" ]; then
mkdir -p "$XRAYDIR"
fi
cd "$XRAYDIR"
update_all_geofiles
echo "[$(date)] All geo files have been updated successfully!"
}
install_xray_core() {
TARGETARCH="$1"
XRAYDIR="$2"
XRAY_VERSION="$3"
OLD_DIR=$(pwd)
trap 'cd "$OLD_DIR"' EXIT
echo "[$(date)] Running install_xray_core"
case $1 in
amd64)
ARCH="64"
FNAME="amd64"
;;
i386)
ARCH="32"
FNAME="i386"
;;
armv8 | arm64 | aarch64)
ARCH="arm64-v8a"
FNAME="arm64"
;;
armv7 | arm | arm32)
ARCH="arm32-v7a"
FNAME="arm32"
;;
armv6)
ARCH="arm32-v6"
FNAME="armv6"
;;
*)
ARCH="64"
FNAME="amd64"
;;
esac
if [ ! -d "$XRAYDIR" ]; then
mkdir -p "$XRAYDIR"
fi
cd "$XRAYDIR"
wget -q "https://github.com/XTLS/Xray-core/releases/download/${XRAY_VERSION}/Xray-linux-${ARCH}.zip"
# Validate the downloaded zip file
if [ ! -f "Xray-linux-${ARCH}.zip" ] || [ ! -s "Xray-linux-${ARCH}.zip" ]; then
echo "[ERR] Failed to download Xray-core zip or file is empty"
exit 1
fi
unzip -q "Xray-linux-${ARCH}.zip" -d ./xray-unzip
# Validate the extracted xray binary
if [ -f "./xray-unzip/xray" ]; then
cp ./xray-unzip/xray ./"xray-linux-${FNAME}"
rm -r xray-unzip
rm "Xray-linux-${ARCH}.zip"
else
echo "[ERR] Failed to extract xray binary"
exit 1
fi
}
if [ "${0##*/}" = "xray-tools.sh" ]; then
cmd="$1"
shift || true
case "$cmd" in
install_xray_core)
# args: TARGETARCH XRAYDIR XRAY_VERSION
install_xray_core "$@"
;;
update_geodata_in_docker)
# args: XRAYDIR
update_geodata_in_docker "$@"
;;
update_all_geofiles)
update_all_geofiles
;;
""|help|-h|--help)
echo "Usage:"
echo " $0 install_xray_core TARGETARCH XRAYDIR XRAY_VERSION"
echo " $0 update_geodata_in_docker XRAYDIR"
exit 0
;;
*)
echo "Unknown command: $cmd" >&2
echo "Try: $0 help" >&2
exit 1
;;
esac
fi

View file

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

View file

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

View file

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

132
x-ui.sh
View file

@ -6,8 +6,6 @@ blue='\033[0;34m'
yellow='\033[0;33m'
plain='\033[0m'
source docker-cron-runner/xray-tools.sh
#Add some basic function here
function LOGD() {
echo -e "${yellow}[DEG] $* ${plain}"
@ -21,6 +19,23 @@ function LOGI() {
echo -e "${green}[INF] $* ${plain}"
}
# Port helpers: detect listener and owning process (best effort)
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
}
# Simple helpers for domain/IP validation
is_ipv4() {
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
@ -32,7 +47,7 @@ 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
}
# check root
@ -63,7 +78,7 @@ iplimit_log_path="${log_folder}/3xipl.log"
iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
confirm() {
if [[ $# -gt 1 ]]; then
if [[ $# > 1 ]]; then
echo && read -rp "$1 [Default $2]: " temp
if [[ "${temp}" == "" ]]; then
temp=$2
@ -874,6 +889,26 @@ delete_ports() {
fi
}
update_all_geofiles() {
update_geofiles "main"
update_geofiles "IR"
update_geofiles "RU"
}
update_geofiles() {
case "${1}" in
"main") dat_files=(geoip geosite); dat_source="Loyalsoldier/v2ray-rules-dat";;
"IR") dat_files=(geoip_IR geosite_IR); dat_source="chocolate4u/Iran-v2ray-rules" ;;
"RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";;
esac
for dat in "${dat_files[@]}"; do
# Remove suffix for remote filename (e.g., geoip_IR -> geoip)
remote_file="${dat%%_*}"
curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \
https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat
done
}
update_geo() {
echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)"
echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)"
@ -887,17 +922,17 @@ update_geo() {
show_menu
;;
1)
update_main_geofiles
update_geofiles "main"
echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
restart
;;
2)
update_ir_geofiles
update_geofiles "IR"
echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
restart
;;
3)
update_ru_geofiles
update_geofiles "RU"
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
restart
;;
@ -1058,28 +1093,28 @@ ssl_cert_issue_main() {
ssl_cert_issue_for_ip() {
LOGI "Starting automatic SSL certificate generation for server IP..."
LOGI "Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)"
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
# Get server IP
local server_ip=$(curl -s --max-time 3 https://api.ipify.org)
if [ -z "$server_ip" ]; then
server_ip=$(curl -s --max-time 3 https://4.ident.me)
fi
if [ -z "$server_ip" ]; then
LOGE "Failed to get server IP address"
return 1
fi
LOGI "Server IP detected: ${server_ip}"
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
LOGI "acme.sh not found, installing..."
@ -1089,7 +1124,7 @@ ssl_cert_issue_for_ip() {
return 1
fi
fi
# install socat
case "${release}" in
ubuntu | debian | armbian)
@ -1118,26 +1153,57 @@ ssl_cert_issue_for_ip() {
LOGW "Unsupported OS for automatic socat installation"
;;
esac
# Create certificate directory
certPath="/root/cert/ip"
mkdir -p "$certPath"
# Build domain arguments
local domain_args="-d ${server_ip}"
if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then
domain_args="${domain_args} -d ${ipv6_addr}"
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"
# issue the certificate for IP with shortlived profile
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
~/.acme.sh/acme.sh --issue \
@ -1148,7 +1214,7 @@ ssl_cert_issue_for_ip() {
--days 6 \
--httpport ${WebPort} \
--force
if [ $? -ne 0 ]; then
LOGE "Failed to issue certificate for IP: ${server_ip}"
LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet"
@ -1160,7 +1226,7 @@ ssl_cert_issue_for_ip() {
else
LOGI "Certificate issued successfully for IP: ${server_ip}"
fi
# Install the certificate
# Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails,
# but the cert files are still installed. We check for files instead of exit code.
@ -1168,7 +1234,7 @@ ssl_cert_issue_for_ip() {
--key-file "${certPath}/privkey.pem" \
--fullchain-file "${certPath}/fullchain.pem" \
--reloadcmd "${reloadCmd}" 2>&1 || true
# Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero)
if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then
LOGE "Certificate files not found after installation"
@ -1178,18 +1244,18 @@ ssl_cert_issue_for_ip() {
rm -rf ${certPath} 2>/dev/null
return 1
fi
LOGI "Certificate files installed successfully"
# enable auto-renew
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
# Set certificate paths for the panel
local webCertFile="${certPath}/fullchain.pem"
local webKeyFile="${certPath}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
LOGI "Certificate configured for panel"
@ -1259,17 +1325,17 @@ ssl_cert_issue() {
while true; do
read -rp "Please enter your domain name: " domain
domain="${domain// /}" # Trim whitespace
if [[ -z "$domain" ]]; then
LOGE "Domain name cannot be empty. Please try again."
continue
fi
if ! is_domain "$domain"; then
LOGE "Invalid domain format: ${domain}. Please enter a valid domain name."
continue
fi
break
done
LOGD "Your domain is: ${domain}, checking it..."
@ -1818,7 +1884,7 @@ remove_iplimit() {
dnf autoremove -y
;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum remove fail2ban -y
yum autoremove -y
else
@ -2203,7 +2269,7 @@ show_menu() {
esac
}
if [[ $# -gt 0 ]]; then
if [[ $# > 0 ]]; then
case $1 in
"start")
check_install 0 && start 0