From 3993c3aabe122ccfb9922fa033d74ed1e7169d8d Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:10:51 +0800 Subject: [PATCH] fix(apps): split OCI Mirror gateway and egress roles --- apps/oci-mirror/0.1.2/.env.sample | 10 +- apps/oci-mirror/0.1.2/data.yml | 138 ++++++++++----- apps/oci-mirror/0.1.2/docker-compose.yml | 54 +----- apps/oci-mirror/0.1.2/scripts/init.sh | 204 +++++++++++++++-------- apps/oci-mirror/README.md | 88 ++++++---- 5 files changed, 302 insertions(+), 192 deletions(-) mode change 100755 => 100644 apps/oci-mirror/0.1.2/scripts/init.sh diff --git a/apps/oci-mirror/0.1.2/.env.sample b/apps/oci-mirror/0.1.2/.env.sample index 6a885fcc1..336dbb9d0 100644 --- a/apps/oci-mirror/0.1.2/.env.sample +++ b/apps/oci-mirror/0.1.2/.env.sample @@ -1,9 +1,9 @@ CONTAINER_NAME=oci-mirror +OCI_MIRROR_ROLE=gateway PANEL_APP_PORT_HTTP=8443 +OCI_MIRROR_GATEWAY_MANAGEMENT_IP=127.0.0.1 PANEL_APP_PORT_MANAGEMENT=19090 -PANEL_APP_PORT_EGRESS=19443 OCI_MIRROR_PUBLIC_HOST=mirror.example.com -OCI_MIRROR_SOURCE_IPV4= OCI_MIRROR_IP_ALLOWLIST= OCI_MIRROR_CHINA_CIDR_FILE= OCI_MIRROR_CHINA_CIDR_SOURCE=APNIC delegated statistics @@ -12,5 +12,11 @@ OCI_MIRROR_TLS_ENABLED=false OCI_MIRROR_TLS_DIR= APP_DATA_DIR=./data OCI_MIRROR_MANAGEMENT_TOKEN= +OCI_MIRROR_EGRESS_NODE_ID=egress-a +OCI_MIRROR_EGRESS_PROXY_URL=http://10.66.0.2:8443 +OCI_MIRROR_SOURCE_IPV4= +OCI_MIRROR_EGRESS_LISTEN_IP=10.66.0.2 +OCI_MIRROR_GATEWAY_MANAGEMENT_URL=http://10.66.0.1:19090 +OCI_MIRROR_GATEWAY_ALLOWED_PEER=10.66.0.1/32 OCI_MIRROR_EGRESS_TOKEN= TIME_ZONE=Asia/Shanghai diff --git a/apps/oci-mirror/0.1.2/data.yml b/apps/oci-mirror/0.1.2/data.yml index 0ed82f60d..e789104be 100644 --- a/apps/oci-mirror/0.1.2/data.yml +++ b/apps/oci-mirror/0.1.2/data.yml @@ -1,30 +1,42 @@ additionalProperties: formFields: + - default: gateway + edit: true + envKey: OCI_MIRROR_ROLE + labelEn: Node Role + labelZh: 节点角色 + label: {en: Node Role, zh: 节点角色, zh-Hant: 節點角色, ja: ノードロール, ko: 노드 역할, ru: Роль узла, ms: Peranan Nod, pt-br: Função do Nó} + required: true + type: select + values: + - label: Gateway + value: gateway + - label: Egress + value: egress - default: 8443 edit: true envKey: PANEL_APP_PORT_HTTP - labelEn: Public Gateway Port - labelZh: 公网 Gateway 端口 - label: {en: Public Gateway Port, zh: 公网 Gateway 端口, zh-Hant: 公網 Gateway 連接埠, ja: 公開 Gateway ポート, ko: 공용 Gateway 포트, ru: Публичный порт Gateway, ms: Port Gateway Awam, pt-br: Porta Pública do Gateway} + labelEn: Node Service Port + labelZh: 节点服务端口 + label: {en: Node Service Port, zh: 节点服务端口, zh-Hant: 節點服務連接埠, ja: ノードサービスポート, ko: 노드 서비스 포트, ru: Сервисный порт узла, ms: Port Perkhidmatan Nod, pt-br: Porta de Serviço do Nó} required: true rule: paramPort type: number + - default: 127.0.0.1 + edit: true + envKey: OCI_MIRROR_GATEWAY_MANAGEMENT_IP + labelEn: Gateway Management Listen IP + labelZh: Gateway 管理监听 IP + label: {en: Gateway Management Listen IP, zh: Gateway 管理监听 IP, zh-Hant: Gateway 管理監聽 IP, ja: Gateway 管理リッスン IP, ko: Gateway 관리 수신 IP, ru: IP управления Gateway, ms: IP Dengar Pengurusan Gateway, pt-br: IP de Escuta de Gerenciamento do Gateway} + required: false + type: text - default: 19090 edit: true envKey: PANEL_APP_PORT_MANAGEMENT - labelEn: Local Management Port - labelZh: 本地管理端口 - label: {en: Local Management Port, zh: 本地管理端口, zh-Hant: 本機管理連接埠, ja: ローカル管理ポート, ko: 로컬 관리 포트, ru: Локальный порт управления, ms: Port Pengurusan Setempat, pt-br: Porta de Gerenciamento Local} - required: true - rule: paramPort - type: number - - default: 19443 - edit: true - envKey: PANEL_APP_PORT_EGRESS - labelEn: Local Egress Port - labelZh: 本地 Egress 端口 - label: {en: Local Egress Port, zh: 本地 Egress 端口, zh-Hant: 本機 Egress 連接埠, ja: ローカル Egress ポート, ko: 로컬 Egress 포트, ru: Локальный порт Egress, ms: Port Egress Setempat, pt-br: Porta Egress Local} - required: true + labelEn: Gateway Management Port + labelZh: Gateway 管理端口 + label: {en: Gateway Management Port, zh: Gateway 管理端口, zh-Hant: Gateway 管理連接埠, ja: Gateway 管理ポート, ko: Gateway 관리 포트, ru: Порт управления Gateway, ms: Port Pengurusan Gateway, pt-br: Porta de Gerenciamento do Gateway} + required: false rule: paramPort type: number - default: mirror.example.com @@ -33,15 +45,7 @@ additionalProperties: labelEn: Registry Host labelZh: Registry 主机名 label: {en: Registry Host, zh: Registry 主机名, zh-Hant: Registry 主機名稱, ja: Registry ホスト, ko: Registry 호스트, ru: Хост Registry, ms: Hos Registry, pt-br: Host do Registry} - required: true - type: text - - default: '' - edit: true - envKey: OCI_MIRROR_SOURCE_IPV4 - labelEn: Egress Public IPv4 - labelZh: 出口公网 IPv4 - label: {en: Egress Public IPv4, zh: 出口公网 IPv4, zh-Hant: 出口公網 IPv4, ja: 送信元グローバル IPv4, ko: 송신 공인 IPv4, ru: Публичный исходящий IPv4, ms: IPv4 Awam Keluar, pt-br: IPv4 Público de Saída} - required: true + required: false type: text - default: '' edit: true @@ -57,7 +61,7 @@ additionalProperties: labelEn: Mainland China CIDR File labelZh: 中国大陆 CIDR 文件 label: {en: Mainland China CIDR File, zh: 中国大陆 CIDR 文件, zh-Hant: 中國大陸 CIDR 檔案, ja: 中国本土 CIDR ファイル, ko: 중국 본토 CIDR 파일, ru: Файл CIDR материкового Китая, ms: Fail CIDR Tanah Besar China, pt-br: Arquivo CIDR da China Continental} - required: true + required: false type: text - default: APNIC delegated statistics edit: true @@ -65,7 +69,7 @@ additionalProperties: labelEn: CIDR Dataset Source labelZh: CIDR 数据集来源 label: {en: CIDR Dataset Source, zh: CIDR 数据集来源, zh-Hant: CIDR 資料集來源, ja: CIDR データセット出典, ko: CIDR 데이터셋 출처, ru: Источник набора CIDR, ms: Sumber Set Data CIDR, pt-br: Fonte do Conjunto CIDR} - required: true + required: false type: text - default: '' edit: true @@ -73,7 +77,7 @@ additionalProperties: labelEn: CIDR Dataset Date labelZh: CIDR 数据集日期 label: {en: CIDR Dataset Date, zh: CIDR 数据集日期, zh-Hant: CIDR 資料集日期, ja: CIDR データセット日付, ko: CIDR 데이터셋 날짜, ru: Дата набора CIDR, ms: Tarikh Set Data CIDR, pt-br: Data do Conjunto CIDR} - required: true + required: false type: text - default: 'false' edit: true @@ -81,7 +85,7 @@ additionalProperties: labelEn: Enable Gateway TLS labelZh: 启用 Gateway TLS label: {en: Enable Gateway TLS, zh: 启用 Gateway TLS, zh-Hant: 啟用 Gateway TLS, ja: Gateway TLS を有効化, ko: Gateway TLS 활성화, ru: Включить TLS Gateway, ms: Dayakan TLS Gateway, pt-br: Ativar TLS do Gateway} - required: true + required: false type: select values: - label: 'true' @@ -91,35 +95,81 @@ additionalProperties: - default: '' edit: true envKey: OCI_MIRROR_TLS_DIR - labelEn: TLS Source Directory - labelZh: TLS 源目录 - label: {en: TLS Source Directory, zh: TLS 源目录, zh-Hant: TLS 來源目錄, ja: TLS ソースディレクトリ, ko: TLS 원본 디렉터리, ru: Каталог исходных TLS-файлов, ms: Direktori Sumber TLS, pt-br: Diretório de Origem TLS} + labelEn: Gateway TLS Source Directory + labelZh: Gateway TLS 源目录 + label: {en: Gateway TLS Source Directory, zh: Gateway TLS 源目录, zh-Hant: Gateway TLS 來源目錄, ja: Gateway TLS ソースディレクトリ, ko: Gateway TLS 원본 디렉터리, ru: Каталог исходных TLS-файлов Gateway, ms: Direktori Sumber TLS Gateway, pt-br: Diretório de Origem TLS do Gateway} required: false type: text - default: ./data edit: true envKey: APP_DATA_DIR - labelEn: Cache and State Directory - labelZh: 缓存与状态目录 - label: {en: Cache and State Directory, zh: 缓存与状态目录, zh-Hant: 快取與狀態目錄, ja: キャッシュと状態ディレクトリ, ko: 캐시 및 상태 디렉터리, ru: Каталог кеша и состояния, ms: Direktori Cache dan Keadaan, pt-br: Diretório de Cache e Estado} + labelEn: Gateway Cache and State Directory + labelZh: Gateway 缓存与状态目录 + label: {en: Gateway Cache and State Directory, zh: Gateway 缓存与状态目录, zh-Hant: Gateway 快取與狀態目錄, ja: Gateway キャッシュと状態ディレクトリ, ko: Gateway 캐시 및 상태 디렉터리, ru: Каталог кеша и состояния Gateway, ms: Direktori Cache dan Keadaan Gateway, pt-br: Diretório de Cache e Estado do Gateway} required: true type: text - default: '' edit: true envKey: OCI_MIRROR_MANAGEMENT_TOKEN - labelEn: Management Token - labelZh: 管理 Token - label: {en: Management Token, zh: 管理 Token, zh-Hant: 管理 Token, ja: 管理 Token, ko: 관리 Token, ru: Токен управления, ms: Token Pengurusan, pt-br: Token de Gerenciamento} - description: {en: Use a unique 32-4096 byte token containing letters, digits, . _ ~ + / - and optional trailing =., zh: 使用独立的 32-4096 字节 Token,仅可含字母、数字、. _ ~ + / -,末尾可有 =。, zh-Hant: 使用獨立的 32-4096 位元組 Token,僅可含字母、數字、. _ ~ + / -,結尾可有 =。, ja: 32-4096 バイトの一意な Token を使用し、英数字と . _ ~ + / -、末尾の = のみを含めます。, ko: 고유한 32-4096바이트 Token을 사용하며 영문자, 숫자, . _ ~ + / - 및 끝의 =만 허용됩니다., ru: Уникальный токен длиной 32-4096 байт; допустимы буквы, цифры, . _ ~ + / - и = только в конце., ms: Gunakan Token unik 32-4096 bait dengan huruf, digit, . _ ~ + / - dan = pilihan di hujung., pt-br: Use um Token exclusivo de 32-4096 bytes com letras, dígitos, . _ ~ + / - e = opcional no final.} - required: true + labelEn: Gateway Management Token + labelZh: Gateway 管理 Token + label: {en: Gateway Management Token, zh: Gateway 管理 Token, zh-Hant: Gateway 管理 Token, ja: Gateway 管理 Token, ko: Gateway 관리 Token, ru: Токен управления Gateway, ms: Token Pengurusan Gateway, pt-br: Token de Gerenciamento do Gateway} + required: false type: password + - default: egress-a + edit: true + envKey: OCI_MIRROR_EGRESS_NODE_ID + labelEn: Egress Node ID + labelZh: Egress 节点 ID + label: {en: Egress Node ID, zh: Egress 节点 ID, zh-Hant: Egress 節點 ID, ja: Egress ノード ID, ko: Egress 노드 ID, ru: ID узла Egress, ms: ID Nod Egress, pt-br: ID do Nó Egress} + required: true + type: text + - default: http://10.66.0.2:8443 + edit: true + envKey: OCI_MIRROR_EGRESS_PROXY_URL + labelEn: Egress Private Proxy URL + labelZh: Egress 私网代理 URL + label: {en: Egress Private Proxy URL, zh: Egress 私网代理 URL, zh-Hant: Egress 私網代理 URL, ja: Egress プライベートプロキシ URL, ko: Egress 사설 프록시 URL, ru: Приватный URL прокси Egress, ms: URL Proksi Peribadi Egress, pt-br: URL Privada do Proxy Egress} + required: false + type: text + - default: '' + edit: true + envKey: OCI_MIRROR_SOURCE_IPV4 + labelEn: Egress Public Source IPv4 + labelZh: Egress 公网源 IPv4 + label: {en: Egress Public Source IPv4, zh: Egress 公网源 IPv4, zh-Hant: Egress 公網來源 IPv4, ja: Egress グローバル送信元 IPv4, ko: Egress 공인 원본 IPv4, ru: Публичный исходящий IPv4 Egress, ms: IPv4 Sumber Awam Egress, pt-br: IPv4 Público de Origem do Egress} + required: false + type: text + - default: 10.66.0.2 + edit: true + envKey: OCI_MIRROR_EGRESS_LISTEN_IP + labelEn: Egress Private Listen IP + labelZh: Egress 私网监听 IP + label: {en: Egress Private Listen IP, zh: Egress 私网监听 IP, zh-Hant: Egress 私網監聽 IP, ja: Egress プライベートリッスン IP, ko: Egress 사설 수신 IP, ru: Приватный IP прослушивания Egress, ms: IP Dengar Peribadi Egress, pt-br: IP Privado de Escuta do Egress} + required: false + type: text + - default: http://10.66.0.1:19090 + edit: true + envKey: OCI_MIRROR_GATEWAY_MANAGEMENT_URL + labelEn: Gateway Private Management URL + labelZh: Gateway 私网管理 URL + label: {en: Gateway Private Management URL, zh: Gateway 私网管理 URL, zh-Hant: Gateway 私網管理 URL, ja: Gateway プライベート管理 URL, ko: Gateway 사설 관리 URL, ru: Приватный URL управления Gateway, ms: URL Pengurusan Peribadi Gateway, pt-br: URL Privada de Gerenciamento do Gateway} + required: false + type: text + - default: 10.66.0.1/32 + edit: true + envKey: OCI_MIRROR_GATEWAY_ALLOWED_PEER + labelEn: Allowed Gateway IP/CIDR + labelZh: 允许的 Gateway IP/CIDR + label: {en: Allowed Gateway IP/CIDR, zh: 允许的 Gateway IP/CIDR, zh-Hant: 允許的 Gateway IP/CIDR, ja: 許可する Gateway IP/CIDR, ko: 허용된 Gateway IP/CIDR, ru: Разрешённый IP/CIDR Gateway, ms: IP/CIDR Gateway Dibenarkan, pt-br: IP/CIDR Permitido do Gateway} + required: false + type: text - default: '' edit: true envKey: OCI_MIRROR_EGRESS_TOKEN - labelEn: Egress Token - labelZh: Egress Token - label: {en: Egress Token, zh: Egress Token, zh-Hant: Egress Token, ja: Egress Token, ko: Egress Token, ru: Токен Egress, ms: Token Egress, pt-br: Token Egress} - description: {en: Use a different 32-4096 byte token containing letters, digits, . _ ~ + / - and optional trailing =., zh: 使用另一个独立的 32-4096 字节 Token,仅可含字母、数字、. _ ~ + / -,末尾可有 =。, zh-Hant: 使用另一個獨立的 32-4096 位元組 Token,僅可含字母、數字、. _ ~ + / -,結尾可有 =。, ja: 別の 32-4096 バイト Token を使用し、英数字と . _ ~ + / -、末尾の = のみを含めます。, ko: 별도의 32-4096바이트 Token을 사용하며 영문자, 숫자, . _ ~ + / - 및 끝의 =만 허용됩니다., ru: Другой уникальный токен длиной 32-4096 байт; допустимы буквы, цифры, . _ ~ + / - и = только в конце., ms: Gunakan Token lain 32-4096 bait dengan huruf, digit, . _ ~ + / - dan = pilihan di hujung., pt-br: Use outro Token exclusivo de 32-4096 bytes com letras, dígitos, . _ ~ + / - e = opcional no final.} + labelEn: Egress Node Token + labelZh: Egress 节点 Token + label: {en: Egress Node Token, zh: Egress 节点 Token, zh-Hant: Egress 節點 Token, ja: Egress ノード Token, ko: Egress 노드 Token, ru: Токен узла Egress, ms: Token Nod Egress, pt-br: Token do Nó Egress} required: true type: password - default: Asia/Shanghai diff --git a/apps/oci-mirror/0.1.2/docker-compose.yml b/apps/oci-mirror/0.1.2/docker-compose.yml index 432274ae7..c12574926 100644 --- a/apps/oci-mirror/0.1.2/docker-compose.yml +++ b/apps/oci-mirror/0.1.2/docker-compose.yml @@ -1,7 +1,7 @@ services: - gateway: + oci-mirror: image: "moelin/oci-mirror:0.1.2" - container_name: ${CONTAINER_NAME}-gateway + container_name: ${CONTAINER_NAME} network_mode: host init: true working_dir: /tmp @@ -24,58 +24,18 @@ services: max-size: 10m max-file: '3' command: - - gateway + - ${OCI_MIRROR_ROLE} - --config - - /etc/oci-mirror/gateway.json + - /etc/oci-mirror/runtime.json environment: - TZ=${TIME_ZONE} - OCI_MIRROR_MANAGEMENT_TOKEN=${OCI_MIRROR_MANAGEMENT_TOKEN} - OCI_MIRROR_EGRESS_TOKEN=${OCI_MIRROR_EGRESS_TOKEN} volumes: - - ./config/gateway.json:/etc/oci-mirror/gateway.json:ro - - ./config/china-mainland.cidr:/etc/oci-mirror/china-mainland.cidr:ro - - ./config/tls:/etc/oci-mirror/tls:ro + - ./config:/etc/oci-mirror:ro - ${APP_DATA_DIR}:/var/lib/oci-mirror labels: createdBy: Apps - oci-mirror.public-port: ${PANEL_APP_PORT_HTTP} + oci-mirror.role: ${OCI_MIRROR_ROLE} + oci-mirror.service-port: ${PANEL_APP_PORT_HTTP} oci-mirror.management-port: ${PANEL_APP_PORT_MANAGEMENT} - - egress: - image: "moelin/oci-mirror:0.1.2" - container_name: ${CONTAINER_NAME}-egress - network_mode: host - init: true - working_dir: /tmp - read_only: true - restart: unless-stopped - stop_grace_period: 30s - cap_drop: - - ALL - security_opt: - - no-new-privileges:true - ulimits: - nofile: - soft: 65536 - hard: 65536 - tmpfs: - - /tmp:size=16m,mode=1777 - logging: - driver: json-file - options: - max-size: 10m - max-file: '3' - command: - - egress - - --config - - /etc/oci-mirror/egress.json - environment: - - TZ=${TIME_ZONE} - - OCI_MIRROR_EGRESS_TOKEN=${OCI_MIRROR_EGRESS_TOKEN} - volumes: - - ./config/egress.json:/etc/oci-mirror/egress.json:ro - depends_on: - - gateway - labels: - createdBy: Apps - oci-mirror.egress-port: ${PANEL_APP_PORT_EGRESS} diff --git a/apps/oci-mirror/0.1.2/scripts/init.sh b/apps/oci-mirror/0.1.2/scripts/init.sh old mode 100755 new mode 100644 index ac19f3c23..53d533fdb --- a/apps/oci-mirror/0.1.2/scripts/init.sh +++ b/apps/oci-mirror/0.1.2/scripts/init.sh @@ -36,6 +36,24 @@ valid_ipv4() { awk -F. 'NF != 4 { exit 1 } { for (i = 1; i <= 4; i++) if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1 }' <<<"$1" } +valid_private_or_loopback_ipv4() { + valid_ipv4 "$1" || return 1 + awk -F. '$1 == 10 || $1 == 127 || ($1 == 172 && $2 >= 16 && $2 <= 31) || ($1 == 192 && $2 == 168) { exit 0 } { exit 1 }' <<<"$1" +} + +valid_dns_name() { + [[ "$1" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ && "$1" != *..* ]] +} + +valid_ipv4_cidr() { + local address=${1%%/*} + local prefix=32 + if [[ "$1" == */* ]]; then + prefix=${1##*/} + fi + valid_ipv4 "$address" && [[ "$prefix" =~ ^[0-9]+$ ]] && ((10#$prefix >= 0 && 10#$prefix <= 32)) +} + valid_allowlist() { [[ -z "$1" || "$1" =~ ^[A-Fa-f0-9.:/]+$ ]] } @@ -46,6 +64,25 @@ valid_bearer_token() { ((length >= 32 && length <= 4096)) && [[ "$1" =~ ^[A-Za-z0-9._~+/-]+=*$ ]] } +valid_node_id() { + [[ "$1" =~ ^[a-z][a-z0-9_-]{0,31}$ ]] +} + +valid_private_http_origin() { + local value=$1 + local host + local port + [[ "$value" =~ ^http://([^/:]+):([0-9]+)$ ]] || return 1 + host=${BASH_REMATCH[1]} + port=${BASH_REMATCH[2]} + valid_port "$port" || return 1 + if valid_ipv4 "$host"; then + valid_private_or_loopback_ipv4 "$host" + else + valid_dns_name "$host" + fi +} + safe_data_dir() { case "$1" in / | /bin | /boot | /dev | /etc | /home | /lib | /lib64 | /media | /mnt | /opt | /proc | /root | /run | /sbin | /srv | /sys | /tmp | /usr | /var | /var/lib | /workspace) @@ -56,85 +93,103 @@ safe_data_dir() { CIDR_SOURCE_PATTERN='^[A-Za-z0-9._:/ -]+$' -PUBLIC_PORT="$(read_env PANEL_APP_PORT_HTTP)" -MANAGEMENT_PORT="$(read_env PANEL_APP_PORT_MANAGEMENT)" -EGRESS_PORT="$(read_env PANEL_APP_PORT_EGRESS)" -PUBLIC_HOST="$(read_env OCI_MIRROR_PUBLIC_HOST)" -SOURCE_IPV4="$(read_env OCI_MIRROR_SOURCE_IPV4)" -IP_ALLOWLIST="$(read_env OCI_MIRROR_IP_ALLOWLIST)" -CIDR_FILE="$(read_env OCI_MIRROR_CHINA_CIDR_FILE)" -CIDR_SOURCE="$(read_env OCI_MIRROR_CHINA_CIDR_SOURCE)" -CIDR_UPDATED="$(read_env OCI_MIRROR_CHINA_CIDR_UPDATED)" -TLS_ENABLED="$(read_env OCI_MIRROR_TLS_ENABLED)" -TLS_ENABLED="${TLS_ENABLED:-false}" -TLS_ENABLED="${TLS_ENABLED,,}" -TLS_DIR="$(read_env OCI_MIRROR_TLS_DIR)" -DATA_DIR="$(absolute_path "$(read_env APP_DATA_DIR)")" -MANAGEMENT_TOKEN="$(read_env OCI_MIRROR_MANAGEMENT_TOKEN)" +ROLE="$(read_env OCI_MIRROR_ROLE)" +ROLE="${ROLE,,}" +SERVICE_PORT="$(read_env PANEL_APP_PORT_HTTP)" +NODE_ID="$(read_env OCI_MIRROR_EGRESS_NODE_ID)" EGRESS_TOKEN="$(read_env OCI_MIRROR_EGRESS_TOKEN)" +DATA_DIR_VALUE="$(read_env APP_DATA_DIR)" +[[ -n "$DATA_DIR_VALUE" ]] || die 'APP_DATA_DIR must not be empty' +DATA_DIR="$(absolute_path "$DATA_DIR_VALUE")" -valid_port "$PUBLIC_PORT" || die 'PANEL_APP_PORT_HTTP must be a valid port' -valid_port "$MANAGEMENT_PORT" || die 'PANEL_APP_PORT_MANAGEMENT must be a valid port' -valid_port "$EGRESS_PORT" || die 'PANEL_APP_PORT_EGRESS must be a valid port' -[[ "$PUBLIC_PORT" != "$MANAGEMENT_PORT" && "$PUBLIC_PORT" != "$EGRESS_PORT" && "$MANAGEMENT_PORT" != "$EGRESS_PORT" ]] || die 'all three ports must be different' -if ! valid_ipv4 "$PUBLIC_HOST"; then - [[ "$PUBLIC_HOST" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ && "$PUBLIC_HOST" == *.* ]] || die 'OCI_MIRROR_PUBLIC_HOST must be a DNS hostname or IPv4 address' -fi -valid_ipv4 "$SOURCE_IPV4" || die 'OCI_MIRROR_SOURCE_IPV4 must be an IPv4 address assigned to this host' -valid_allowlist "$IP_ALLOWLIST" || die 'OCI_MIRROR_IP_ALLOWLIST must contain one IP address or CIDR' -[[ -n "$CIDR_FILE" ]] || die 'OCI_MIRROR_CHINA_CIDR_FILE is required' -CIDR_FILE="$(absolute_path "$CIDR_FILE")" -[[ -f "$CIDR_FILE" ]] || die "CIDR file does not exist: $CIDR_FILE" -[[ -n "$CIDR_SOURCE" && -n "$CIDR_UPDATED" ]] || die 'CIDR source and update date are required' -[[ "$CIDR_SOURCE" =~ $CIDR_SOURCE_PATTERN ]] || die 'OCI_MIRROR_CHINA_CIDR_SOURCE contains invalid characters' -[[ "$CIDR_UPDATED" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die 'OCI_MIRROR_CHINA_CIDR_UPDATED must use YYYY-MM-DD' -[[ "$TLS_ENABLED" == true || "$TLS_ENABLED" == false ]] || die 'OCI_MIRROR_TLS_ENABLED must be true or false' -safe_data_dir "$DATA_DIR" || die 'APP_DATA_DIR is too broad' -valid_bearer_token "$MANAGEMENT_TOKEN" || die 'OCI_MIRROR_MANAGEMENT_TOKEN must be a 32 to 4096 byte bearer token' +[[ "$ROLE" == gateway || "$ROLE" == egress ]] || die 'OCI_MIRROR_ROLE must be gateway or egress' +valid_port "$SERVICE_PORT" || die 'PANEL_APP_PORT_HTTP must be a valid port for the selected node role' +valid_node_id "$NODE_ID" || die 'OCI_MIRROR_EGRESS_NODE_ID must match ^[a-z][a-z0-9_-]{0,31}$' valid_bearer_token "$EGRESS_TOKEN" || die 'OCI_MIRROR_EGRESS_TOKEN must be a 32 to 4096 byte bearer token' -[[ "$MANAGEMENT_TOKEN" != "$EGRESS_TOKEN" ]] || die 'management and Egress tokens must be different' +safe_data_dir "$DATA_DIR" || die 'APP_DATA_DIR is too broad' +[[ "$DATA_DIR" != "$ROOT_DIR" && "$DATA_DIR" != "$ROOT_DIR/config" && "$DATA_DIR" != "$ROOT_DIR/config/"* ]] || die 'APP_DATA_DIR must not point to the application or its config directory' -ALLOWLIST_JSON='[]' -if [[ -n "$IP_ALLOWLIST" ]]; then - [[ "$IP_ALLOWLIST" != *'"'* && "$IP_ALLOWLIST" != *$'\n'* ]] || die 'OCI_MIRROR_IP_ALLOWLIST contains invalid characters' - ALLOWLIST_JSON="[\"$IP_ALLOWLIST\"]" -fi +install -d -m 0750 "$ROOT_DIR/config" "$ROOT_DIR/config/tls" "$DATA_DIR" -TLS_JSON='' -install -d -m 0750 "$ROOT_DIR/config/tls" -if [[ "$TLS_ENABLED" == true ]]; then - [[ -n "$TLS_DIR" ]] || die 'OCI_MIRROR_TLS_DIR is required when TLS is enabled' - TLS_DIR="$(absolute_path "$TLS_DIR")" - [[ -f "$TLS_DIR/server.crt" && -f "$TLS_DIR/server.key" ]] || die 'TLS directory must contain server.crt and server.key' - install -m 0644 "$TLS_DIR/server.crt" "$ROOT_DIR/config/tls/server.crt" - install -m 0600 "$TLS_DIR/server.key" "$ROOT_DIR/config/tls/server.key" - TLS_JSON=$' "tls_cert_file": "/etc/oci-mirror/tls/server.crt",\n "tls_key_file": "/etc/oci-mirror/tls/server.key",' -else - rm -f -- "$ROOT_DIR/config/tls/server.crt" "$ROOT_DIR/config/tls/server.key" -fi +if [[ "$ROLE" == gateway ]]; then + MANAGEMENT_IP="$(read_env OCI_MIRROR_GATEWAY_MANAGEMENT_IP)" + MANAGEMENT_PORT="$(read_env PANEL_APP_PORT_MANAGEMENT)" + PUBLIC_HOST="$(read_env OCI_MIRROR_PUBLIC_HOST)" + EGRESS_PROXY_URL="$(read_env OCI_MIRROR_EGRESS_PROXY_URL)" + SOURCE_IPV4="$(read_env OCI_MIRROR_SOURCE_IPV4)" + IP_ALLOWLIST="$(read_env OCI_MIRROR_IP_ALLOWLIST)" + CIDR_FILE="$(read_env OCI_MIRROR_CHINA_CIDR_FILE)" + CIDR_SOURCE="$(read_env OCI_MIRROR_CHINA_CIDR_SOURCE)" + CIDR_UPDATED="$(read_env OCI_MIRROR_CHINA_CIDR_UPDATED)" + TLS_ENABLED="$(read_env OCI_MIRROR_TLS_ENABLED)" + TLS_ENABLED="${TLS_ENABLED:-false}" + TLS_ENABLED="${TLS_ENABLED,,}" + TLS_DIR="$(read_env OCI_MIRROR_TLS_DIR)" + MANAGEMENT_TOKEN="$(read_env OCI_MIRROR_MANAGEMENT_TOKEN)" -install -d -m 0750 "$ROOT_DIR/config" "$DATA_DIR" -install -m 0644 "$CIDR_FILE" "$ROOT_DIR/config/china-mainland.cidr" -chown 65532:65532 "$DATA_DIR" -chown -R 65532:65532 "$ROOT_DIR/config" + valid_private_or_loopback_ipv4 "$MANAGEMENT_IP" || die 'OCI_MIRROR_GATEWAY_MANAGEMENT_IP must be a private or loopback IPv4 address assigned to this host' + valid_port "$MANAGEMENT_PORT" || die 'PANEL_APP_PORT_MANAGEMENT must be a valid port for the Gateway role' + [[ "$SERVICE_PORT" != "$MANAGEMENT_PORT" ]] || die 'Gateway public and management ports must be different' + if ! valid_ipv4 "$PUBLIC_HOST"; then + if ! valid_dns_name "$PUBLIC_HOST" || [[ "$PUBLIC_HOST" != *.* ]]; then + die 'OCI_MIRROR_PUBLIC_HOST must be a DNS hostname or IPv4 address' + fi + fi + valid_private_http_origin "$EGRESS_PROXY_URL" || die 'OCI_MIRROR_EGRESS_PROXY_URL must be an HTTP origin on a private IPv4 address or private DNS name with an explicit port' + valid_ipv4 "$SOURCE_IPV4" || die 'OCI_MIRROR_SOURCE_IPV4 must be an IPv4 address assigned to the Egress host' + valid_allowlist "$IP_ALLOWLIST" || die 'OCI_MIRROR_IP_ALLOWLIST must contain one IP address or CIDR' + [[ -n "$CIDR_FILE" ]] || die 'OCI_MIRROR_CHINA_CIDR_FILE is required for the Gateway role' + CIDR_FILE="$(absolute_path "$CIDR_FILE")" + [[ -f "$CIDR_FILE" ]] || die "CIDR file does not exist: $CIDR_FILE" + [[ -n "$CIDR_SOURCE" && -n "$CIDR_UPDATED" ]] || die 'CIDR source and update date are required for the Gateway role' + [[ "$CIDR_SOURCE" =~ $CIDR_SOURCE_PATTERN ]] || die 'OCI_MIRROR_CHINA_CIDR_SOURCE contains invalid characters' + [[ "$CIDR_UPDATED" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die 'OCI_MIRROR_CHINA_CIDR_UPDATED must use YYYY-MM-DD' + [[ "$TLS_ENABLED" == true || "$TLS_ENABLED" == false ]] || die 'OCI_MIRROR_TLS_ENABLED must be true or false' + valid_bearer_token "$MANAGEMENT_TOKEN" || die 'OCI_MIRROR_MANAGEMENT_TOKEN must be a 32 to 4096 byte bearer token for the Gateway role' + [[ "$MANAGEMENT_TOKEN" != "$EGRESS_TOKEN" ]] || die 'management and Egress tokens must be different' -cat > "$ROOT_DIR/config/gateway.json" < "$ROOT_DIR/config/runtime.json" < "$ROOT_DIR/config/egress.json" < "$ROOT_DIR/config/runtime.json" < "$ROOT_DIR/config/egress.json" < Gateway 节点 -> WireGuard/私网 -> Egress 节点 -> Docker Hub +``` -1. 一个解析到服务器的 Registry 域名;受控测试可以使用服务器 IPv4。 -2. 一个真实分配到服务器网卡的公网 IPv4,不能填写 NAT 出口探测值或未绑定地址。 -3. 按上游文档生成的中国大陆 IPv4/IPv6 CIDR 文件。应用不会下载或内置可能过期的数据集。 -4. 正式对外服务时,准备包含 `server.crt` 和 `server.key` 的目录,证书必须覆盖 Registry 域名。 -5. 两个不同且长度为 32-4096 字节的高强度 Token,分别用于管理接口和 Gateway/Egress 通信。 +1. 先在入口服务器安装 `Gateway` 角色。 +2. 再在出口服务器安装 `Egress` 角色。 +3. 两次安装必须使用相同的 Egress 节点 ID 和 Egress Token。 +4. Gateway 的 Egress 私网代理 URL 必须指向 Egress 的私网监听 IP 与节点服务端口。 +5. Egress 的 Gateway 私网管理 URL 必须指向 Gateway 的管理监听 IP 与端口。 -当前包只配置 Docker Hub。GHCR、Quay、GCR、固定下载源、多 Egress、IPv6 出口池、WireGuard 和 mTLS 节点需要按上游部署文档进行专门规划,不应直接修改生成配置后期待面板升级保留。 +一个 Gateway 可调度多个 Egress,但当前 1Panel 表单每个 Gateway 安装配置一个 Egress 节点。需要多出口节点时,应按上游文档扩展 Gateway 配置并规划独立升级维护流程。 ## 访问说明 -- Gateway 使用 host network,直接监听安装表单中的公网端口。 -- 管理和 Egress 端口只绑定 `127.0.0.1`,不应对公网开放。 -- 为避免依赖安装前已存在的证书文件,表单默认关闭 TLS。正式对外服务应启用 TLS 并填写证书目录;无 TLS 模式仅适用于受控测试,或由宿主机反向代理终止 TLS 且已正确配置客户端身份传递的场景。 +- Docker 客户端只访问 Gateway 的公网端口,不直接访问 Egress。 +- Gateway 管理端口和 Egress 代理端口只应通过可信私网互通。 +- Gateway 默认关闭 TLS,仅适合受控测试或由宿主机反向代理终止 TLS 的部署;正式直接对外服务应启用 TLS。 - Docker 客户端需要信任 Gateway 证书,并按上游文档配置 Registry Mirror 或显式 Registry 地址。 -## Introduction +## 安装前提 -OCI Mirror is a read-only OCI pull-through cache and fixed-origin proxy. This package deploys the upstream-supported single-host Gateway and Egress topology for Docker Hub. +- Gateway 节点需要 Registry 域名、中国大陆 CIDR 文件、持久化目录和管理 Token。 +- Egress 节点需要实际分配到该主机的公网 IPv4;该地址同时填写到 Gateway 的 `Egress 公网源 IPv4`。 +- 两个节点之间需要 WireGuard、专用网络或其他可信私网。默认 private 模式不提供控制面 TLS,不能直接暴露到公网。 +- Gateway 管理监听 IP 和 Egress 监听 IP 必须已分配到各自宿主机;默认示例地址需要按实际网络修改。 +- 正式公开 Gateway 时,应启用 TLS 并提供包含 `server.crt` 和 `server.key` 的目录。 -## Features +## 角色字段 -- Proxies Docker Hub Registry API requests through a dedicated Egress service. -- Caches manifests and blobs with persistent SQLite state and usage accounting. -- Enforces mainland China CIDR policy, an optional allowlist, quotas, and concurrency limits. -- Supports an explicit Gateway TLS certificate directory for production deployments. +### Gateway + +- `Gateway 管理监听 IP`:供 Egress 拉取节点配置快照的私网地址。`127.0.0.1` 仅适合本机测试。 +- `Egress 私网代理 URL`:例如 `http://10.66.0.2:8443`。 +- `Egress 公网源 IPv4`:实际绑定到远端 Egress 主机的公网源地址。 +- `Egress 节点 ID` 与 `Egress 节点 Token`:必须和远端 Egress 安装一致。 + +### Egress + +- `节点服务端口`:Gateway 角色下是公网 Registry 端口,Egress 角色下是私网代理端口。 +- `Egress 私网监听 IP`:例如 WireGuard 地址 `10.66.0.2`。 +- `Gateway 私网管理 URL`:例如 `http://10.66.0.1:19090`,应用会自动追加节点快照路径。 +- `允许的 Gateway IP/CIDR`:限制可连接 Egress 的 Gateway 地址,例如 `10.66.0.1/32`。 +- `Egress 节点 ID` 与 `Egress 节点 Token`:必须和 Gateway 安装一致。 ## 持久化与升级 -- SQLite、缓存对象和运行状态保存在 `APP_DATA_DIR`。 -- 备份时同时保存数据目录、外部 CIDR 文件、TLS 证书/私钥和安装表单参数。 -- `init.sh` 与 `upgrade.sh` 会根据表单重新生成 Gateway/Egress 配置;手工修改生成的 `config/*.json` 不属于稳定升级接口。 -- 镜像采用固定标签 `moelin/oci-mirror:0.1.2`。当前公开仓库没有可用的 `latest` 标签。 +- Gateway 的 SQLite、缓存对象、用量和健康状态保存在 `APP_DATA_DIR`。 +- Egress 不保存业务数据;Compose 保留相同数据挂载以维持单一服务模板,但 Egress 运行模式不会使用该目录。 +- 备份 Gateway 时同时保存数据目录、外部 CIDR 文件、TLS 证书和安装表单参数。 +- `init.sh` 与 `upgrade.sh` 会按当前角色和表单重新生成 `config/runtime.json`;手工修改生成配置不属于稳定升级接口。 +- 镜像固定为 `moelin/oci-mirror:0.1.2`。 + +## Introduction + +OCI Mirror is a read-only OCI pull-through cache and fixed-origin proxy. This package installs one role per 1Panel instance: Gateway for the public entry, policy, cache, and scheduling, or Egress for outbound Docker Hub access from a separate node. + +## Features + +- Deploys Gateway and Egress independently on different 1Panel hosts. +- Caches Docker Hub manifests and blobs with persistent Gateway state. +- Distributes node-scoped configuration snapshots from Gateway to Egress. +- Restricts the private control path by listener address, peer CIDR, node ID, and a dedicated bearer token. ## 安全与网络风险 -> **高风险网络权限:Gateway 与 Egress 使用宿主机网络。** +> **高风险网络权限:两个角色都使用宿主机网络。** -host network 是上游绑定真实出口源地址、回环控制端口及公网 Gateway 监听器的核心要求。应用保留上游的只读根文件系统、非 root UID `65532`、`cap_drop: ALL` 和 `no-new-privileges`,不挂载 Docker Socket,也不启用 privileged 模式。 +host network 是上游绑定真实出口源地址、私网控制地址及 Gateway 公网监听器的核心要求。应用保留上游的只读根文件系统、非 root UID `65532`、`cap_drop: ALL` 和 `no-new-privileges`,不挂载 Docker Socket,也不启用 privileged 模式。 -错误的域名、证书、出口地址、CIDR 或防火墙配置可能导致服务不可用、意外暴露或访问策略失效。正式使用前请完成上游的 Registry API、Docker 客户端、出口源地址和控制平面检查。 +private 模式会校验 Gateway 管理地址、Egress 代理地址和允许的对端网段,但防火墙仍必须只允许两个节点通过可信私网互访。公网跨节点部署应按上游文档改用 mTLS,当前表单不自动配置证书身份。 ## 资料