Administrator
发布于 2025-10-04 / 28 阅读
0
0

immortalwrt安装配置caddy

immortalwrt自带的uhttpd功能过于简单,无法胜任反代、证书管理等功能,所以参考了前人经验用caddy干掉了uhttpd,运行效果令人满意。

配置防火墙

新增一条防火墙规则,对外开放80、443端口。我这里移动宽带没有限制IPv6端口,443端口用于对外提供网页服务,80端口用于Let's Encrypt的http-challenge。设置好后看起来大致如下:

firewall.@rule[14]=rule
firewall.@rule[14].src='wan'
firewall.@rule[14].name='Allow-LUCI-WAN'
firewall.@rule[14].dest_port='80 443'
firewall.@rule[14].target='ACCEPT'

禁用uhttpd

/etc/init.d/uhttpd disable
/etc/init.d/uhttpd stop

下载caddy

caddy本身只是一个二进制文件,而且第三方插件众多,所以immortalwrt软件源中没有提供,要去官网按需下载,地址为 https://caddyserver.com/download 。我选中了这些第三方模块:

  • aksdb/caddy-cgi/v2 :没它无法访问LUCI

  • mholt/caddy-webdav:提供了webdav功能

选中这些模块后点击下载,注意架构不要错,丢到 /usr/bin 中改名为caddy,权限设置为 751 。

创建Caddyfile配置文件

在/etc/目录中新建 caddy 目录,进入其中创建名为 Caddyfile 的配置文件,内容如下:

{
    # 全局设置
    # Caddy 的指令顺序非常重要。
    # 确保 cgi 和 file_server 在 respond 之前处理,这是你现有配置的逻辑。
    order cgi before respond
    order file_server last

    # 建议在这里设置全局邮箱,Caddy 会用它注册 ACME 账户
    # Caddy 默认使用 Let's Encrypt 作为 ACME CA,通常不需要显式设置 acme_ca。
    email [email protected]

    # Caddy 存储 ACME 证书和内部状态的目录。
    # 确保这个目录存在并且 Caddy 有写入权限。
    # 默认通常是 /var/lib/caddy 或 ~/.local/share/caddy
    # 如果在 OpenWrt 上运行,可能需要指定一个可持久化的路径,例如 /etc/caddy/data
    # storage /etc/caddy/data

    # 如果你的Caddy运行在反向代理(如Cloudflare、OpenWrt上的homeproxy等)之后,
    # 并且需要基于真实客户端IP进行访问控制,请在这里配置 trusted_proxies。
    # 例如,如果你的homeproxy的IPv6地址是 2408:832e:8443:e000::1,且它转发了X-Forwarded-For头:
    # trusted_proxies 2408:832e:8443:e000::/64
    # 如果是Cloudflare:
    # trusted_proxies cloudflare
}

# 定义一个名为 'luci' 的片段,用于 OpenWrt LuCI 界面
(luci) {
    root * /www # LuCI 静态文件根目录
    route /cgi-bin* {
        @exists {
            file cgi-bin/{path.1} =404 # 检查 cgi-bin 目录下的文件是否存在
        }
        handle @exists {
            uri strip_prefix {file_match.relative} # 剥离 URI 前缀
            cgi * /www/{file_match.relative} { # 执行 CGI 脚本
                script_name {file_match.relative}
            }
        }
    }
    cgi /ubus* ubus.sh { # UBUS 接口的 CGI
        script_name /ubus
    }
    file_server # 提供静态文件服务
    redir / /cgi-bin/luci # 根路径重定向到 LuCI 登录页
}

# 定义一个名为 'security_headers' 的片段,用于添加安全 HTTP 头
(security_headers) {
    header {
        X-Frame-Options DENY
        X-Content-Type-Options nosniff
        Referrer-Policy strict-origin-when-cross-origin
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
    }
}

# 定义一个名为 'internal_access_control' 的片段,用于限制内网访问
# 这样可以复用IP访问限制逻辑,避免重复代码
(internal_access_control) {
    # 定义一个匹配器,匹配所有不允许的IP地址
    # 即,不在这些允许的IPv4/IPv6范围内的所有IP
    @not_allowed_ips {
        not remote_ip 10.89.2.0/24 192.168.20.0/24 2408:832e:8443:e000::/60
    }

    # 如果IP不在允许列表中,则拒绝访问
    handle @not_allowed_ips {
        respond "Access Denied from your IP address. Your IP: {http.request.remote}" 403
    }
}


# Luci 界面 - lab.tccmu.com
lab.tccmu.com {
    import security_headers
    import internal_access_control # 导入访问控制片段

    # 如果IP在允许列表中,则继续处理LuCI
    import luci

    # 访问日志
    log {
        output file /var/log/caddy/lab.tccmu.com.access.log
        format json
    }
}

# Docker Portainer 界面 - docker.tccmu.com
docker.tccmu.com {
    import security_headers
    import internal_access_control # 导入访问控制片段

    # 编码设置
    encode gzip

    # 反向代理到 Portainer
    reverse_proxy 10.89.2.1:9000 {
        # Portainer 可能需要一些特定的代理头,这里是通用推荐
        header_up Host {host}
        header_up X-Real-IP {remote_ip}
        header_up X-Forwarded-For {remote_ip}
        header_up X-Forwarded-Proto {scheme}
    }

    log {
        output file /var/log/caddy/docker.tccmu.com.access.log
        format json
    }
}


# 增强版 WebDAV 配置
webdav.tccmu.com {
    import security_headers

    # 基本认证 (应用于整个站点)
    basic_auth {
         admin $2a$14$..................................
    }

    # WebDAV 根目录
    # 所有WebDAV操作都将在这个目录下进行
    root * /root/webdav

    # 定义一个命名匹配器,用于匹配 WebDAV 方法
    @webdav_methods {
        # *** 关键修改:添加 GET 方法 ***
        method GET PROPFIND MKCOL PUT DELETE MOVE COPY LOCK UNLOCK
    }

    # WebDAV 配置
    # 当请求匹配 @webdav_methods 时,直接启用 WebDAV
    route @webdav_methods {
        webdav
    }

    # 访问日志
    log {
        output file /var/log/caddy/webdav.tccmu.com.access.log
        format json
    }

    # 错误处理
    handle_errors {
        respond "Authentication required" 401
        respond "Access forbidden" 403
    }
}

blog.tccmu.com {
    #import security_headers # 如果需要,可以取消注释

    # 编码设置
    encode gzip

    # 反向代理配置
    reverse_proxy 10.89.2.1:8001 {
        # Halo 需要的代理头
        header_up X-Forwarded-Host {host}
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-For {remote}
        header_up X-Real-IP {remote}

        # 确保 Host 头正确传递
        header_up Host {host}

        # 健康检查
        health_uri /actuator/health/readiness
        health_interval 30s
        health_timeout 5s
    }

    log {
        output file /var/log/caddy/blog.tccmu.com.access.log
        format json
    }
}

# 默认响应 - 处理未配置的域名 (HTTPS)
# 如果有请求到达 443 端口,但没有匹配到任何域名,则返回 404。
:443 {
    respond "Domain not configured" 404
}

# HTTP 监听 - 优先处理 ACME 挑战,然后重定向到 HTTPS
# 这个块是关键,它确保了 Let's Encrypt 的 http-01 挑战能够成功。
:80 {
    # 优先处理 Let's Encrypt 的 http-01 挑战
    # Caddy 自身在进行 ACME 挑战时,会使用这个路径来放置临时文件。
    # 这样 Caddy 就能通过 http-01 挑战来获取和续订证书。
    handle /.well-known/acme-challenge/* {
        root * /var/www/caddy-acme-challenges # Caddy 默认用于 ACME 挑战的目录
        file_server
    }

    # 对于其他所有 HTTP 请求,重定向到 HTTPS
    # Caddy 会自动将 HTTP 请求重定向到对应的 HTTPS 域名。
    # 例如,http://blog.tccmu.com 会重定向到 https://1.tccmu.com
    redir https://{host}{uri} permanent
}

这个配置是用gpt生成的,功能还算齐全,证书自动管理、反代、重定向、webdav、访问IP限制等功能都有了,不过漏洞肯定不少,欢迎网友提出意见。

创建/etc/init.d/caddy

这个脚本用于管理caddy服务,设置权限751:

#!/bin/sh /etc/rc.common

USE_PROCD=1
START=99
STOP=10

SERVICE_USE_PID=1
SERVICE_WRITE_PID=1
SERVICE_DAEMONIZE=1
SERVICE_PID_FILE=/var/run/caddy.pid
SERVICE_NAME=caddy
SERVICE_BIN=/usr/bin/caddy

CONFIG_FILE=/etc/caddy/Caddyfile

start_service() {
    procd_open_instance
    procd_set_param command "$SERVICE_BIN" run --config "$CONFIG_FILE"
    procd_set_param respawn
    procd_set_param stdout 1
    procd_set_param stderr 1
    procd_set_param pidfile "$SERVICE_PID_FILE"
    procd_close_instance
}

stop_service() {
    service_stop "$SERVICE_BIN"
}

reload_service() {
    # 检查caddy是否支持reload命令
    if "$SERVICE_BIN" reload --help >/dev/null 2>&1; then
        "$SERVICE_BIN" reload --config "$CONFIG_FILE"
    else
        # 如果不支持reload,则重启服务
        echo "Caddy does not support reload, restarting instead"
        restart
    fi
}

restart() {
    stop
    start
}

service_running() {
    service_started "$SERVICE_BIN"
}

service_enabled() {
    /etc/init.d/caddy enabled
}

service_status() {
    if service_running; then
        echo "Service is running"
        return 0
    else
        echo "Service is not running"
        return 1
    fi
}

service_info() {
    echo "Service name: $SERVICE_NAME"
    echo "Binary: $SERVICE_BIN"
    echo "Config file: $CONFIG_FILE"
    echo "PID file: $SERVICE_PID_FILE"
    if service_running; then
        pid=$(cat "$SERVICE_PID_FILE" 2>/dev/null)
        echo "PID: $pid"
        echo "Process info:"
        ps -p "$pid" -o pid,ppid,user,comm,args 2>/dev/null || echo "Process not found"
    fi
}

extra_commands="reload enabled running status info trace"

创建ubus.sh文件

该脚本文件来自 https://github.com/yurt-page/cgi-ubus 。在 /www/cgi-bin/ 目录中创建 ubus.sh ,权限751,内容如下:

#!/bin/sh
[ "$REQUEST_METHOD" != "POST" ] && printf 'Status: 405\r\n\r\n' && exit

access() {
  local sid=$1
  local obj=$2
  local fun=$3

  local req=$(printf '{ "ubus_rpc_session": "%s", "scope": "ubus", "object": "%s", "function": "%s" }' "$sid" "$obj" "$fun")
  local res=$(ubus call session access "$req" | jsonfilter -e '@.access')

  [ "$res" = "true" ]
}

error() {
  local code=$1
  local mesg=$2

  printf '{ "jsonrpc": "2.0", "id": "%s", "error": { "code": %d, "message": "%s" } }'  \
    "${RPC_ID:-null}" "$code" "$mesg"

  exit 1
}

process() {
  local request=$1

  # - use `VAR=expr` notation to let it create shell compatible export statements
  # - eval result to import variables
  eval $(jsonfilter -s "$request" \
    -e '[email protected]' \
    -e '[email protected]' \
    -e '[email protected]' \
    -e '[email protected][3].ubus_rpc_session' \
    -e '[email protected][0]' \
    -e '[email protected][1]' \
    -e '[email protected][2]')

  # verify JSON-RPC framing
  if [ -z "$RPC_ID" ] || [ "$RPC_VERSION" != "2.0" ]; then
    error -32600 "Invalid request"
  fi

  # reject invalid values to prevent shell injection
  case "$RPC_ID$UBUS_SID$UBUS_SERVICE$UBUS_CMD" in
    *[^a-zA-Z0-9_.-]*) error -32600 "Invalid request" ;;
  esac

  case "$RPC_METHOD" in
    call)
      UBUS_PAYLOAD=$(jsonfilter -s "$request" -e '@.params[3]')

      # ensure that payload is a dictionary or empty
      case "$UBUS_PAYLOAD" in
        ""|{*}) : ;;
        *) error -32602 "Invalid parameters" ;;
      esac

      # merge ubus_rpc_session parameter
      if [ -z "$UBUS_PAYLOAD" ] || [ "$UBUS_PAYLOAD" = "{ }" ]; then
        UBUS_PAYLOAD=$(printf '{ "ubus_rpc_session": "%s" }' "$UBUS_SID")
      else
        UBUS_PAYLOAD=$(printf '{ "ubus_rpc_session": "%s", %s' "$UBUS_SID" "${UBUS_PAYLOAD#\{ }")
      fi

      # reject requests with embedded ubus_rpc_session
      if [ -n "$RPC_SESSION_ARG" ]; then
        error -32602 "Invalid parameters"
      fi

      # check access
      if ! access "$UBUS_SID" "$UBUS_SERVICE" "$UBUS_CMD"; then
        error -32002 "Access denied"
      fi

      ubus_reply=$(ubus call "$UBUS_SERVICE" "$UBUS_CMD" "$UBUS_PAYLOAD")
      ubus_status=$?

      printf '{ "jsonrpc": "2.0", "id": "%s", "result": [ %d, %s ] }' \
        "$RPC_ID" "$ubus_status" "${ubus_reply:-null}"
    ;;
    list)
      RPC_PARAMS=$(jsonfilter -s "$request" -e '@.params')

      # ensure that payload is an array or empty
      case "${RPC_PARAMS:-[ ]}" in
        \[*\]) : ;;
        *) error -32602 "Invalid parameters" ;;
      esac

      # empty payload should result in list of services
      if [ "${RPC_PARAMS:-[ ]}" = "[ ]" ]; then
        services=''

        for service in $(ubus list); do
          services="${services:+$services, }\"$service\""
        done

        printf '{ "jsonrpc": "2.0", "id": "%s", "result": [ %s ] }' \
          "$RPC_ID" "$services"

      # list of services should result in { service => { method => signature } } replies
      else
        signatures=''

        eval $(jsonfilter -s "$RPC_PARAMS" -e 'indexes=@')

        for i in $indexes; do
          service=$(jsonfilter -s "$RPC_PARAMS" -e "@[$i]")
          signature=''

          IFS=$'\n\t'
          for line in $(ubus -v list "$service" | tail -n +2); do
            signature="${signature:+$signature, }$line"
          done
          IFS=$' \n\t'

          signatures="${signatures:+$signatures, }\"$service\": { $signature }"
        done

        printf '{ "jsonrpc": "2.0", "id": "%s", "result": { %s } }' \
          "$RPC_ID" "$signatures"
      fi
    ;;
    *)
      error -32601 "Method not found"
    ;;
  esac
}

# - read body from stdin (either an object or array)
# - process each item if it is an array
body=$(cat)
type=$(jsonfilter -s "$body" -t '@')

printf 'Content-Type: application/json\r\n\r\n'

if [ "$type" = "array" ]; then
  first=true
  printf '['
  jsonfilter -s "$body" -e '@.*' | while read request ; do
    # join response with ',' and the first should be omitted
    if ! $first; then
      printf ','
    else
      first=false
    fi
    # process each request
    process "$request"
  done
  printf ']'
else
  process "$body"
fi

然后再进行一些额外设置:

chmod +x /www/cgi-bin/ubus.sh
uci set luci.main.ubuspath='/cgi-bin/ubus.sh'
uci commit
service caddy enable
servcie caddy start

可以用这个命令检查配置文件是否存在语法错误:

caddy validate --config /etc/caddy/Caddyfile

配置dnsmasq

目前caddy服务已经正常运行,但是还存在一个问题:用于访问服务的域名只做了AAAA记录或者指向AAAA记录的CNAME,在使用wireguard连回家的场景中,由于客户端只有IPv4地址,很显然会访问失败。此时我们希望这些域名解析到路由器的内网IPv4地址,本例中,路由器内网IPv4地址 为 10.89.2.1,通过immortalwrt自带的dnsmasq即可实现。

编辑 /etc/config/dhcp,在dsnmasq一节追加解析条目,修改后大致如下:

config dnsmasq
        option domainneeded '1'
        option localise_queries '1'
        option rebind_protection '1'
        option rebind_localhost '1'
        option local '/lan/'
        option domain 'lan'
        option expandhosts '1'
        option min_cache_ttl '3600'
        option use_stale_cache '3600'
        option nonegcache '1'
        option authoritative '1'
        option readethers '1'
        option leasefile '/tmp/dhcp.leases'
        option localservice '1'
        option ednspacket_max '1232'
        option port '53'
        option cachesize '150'
        option dns_redirect '1'
        option nonwildcard '0'
        option allservers '1'

        list address '/lab.tccmu.com/10.89.2.1'
        list address '/blog.tccmu.com/10.89.2.1'

最后两行为新增内容,作用是将这两个域名解析到路由器地址10.89.2.1。这两个域名一个对应路由器LUCI,一个对应路由器上通过docker搭建的网站,所以地址当然要解析到路由器自身。如果服务搭建在局域网中其他设备,修改为对应IP即可。

最后重启dnsmasq服务,修改就生效了。wireguard客户端配置文件中记得把DNS服务器设置为10.89.2.1。

service dnsmasq restart


评论