Total Pageviews

Friday, 17 November 2017

使用Caddy快速搭建http2 proxy(属于https proxy),用以翻墙


( 括号里的这部分是关于Caddy v1的内容,已失效。
Caddy是一个使用Golang开发的http服务器,其主打的特点就是快速支持https和 HTTP2。最近在其更新的0.10.7中,支持了Forward Proxy,Chrome浏览器也内建了对HTTP2 proxy的支持。

Caddy的 Proxy支持一下功能:
  • HTTP2 Proxy
  • Probe resistance,当验证错误时,不会返回HTTP 407 错误。 当有 Probe进行探测时,其不会直接暴露自己是 一个Proxy(实验阶段,比较有意思)
  • Basic Auth
  • IP hiding
  • Servers PAC file

步骤

  • 准备
为了支持HTTTPS和HTTP2,首先需要准备一个域名以及服务器。
  • 下载Caddy
Caddy的下载页在这里,他是按照你选择的操作系统和插件类型来 下载二进制binary(有点好奇它是怎么做到的,穷举么)。在下载选择的插件时,一定要选上http.forwardproxy
运行:

curl https://getcaddy.com | bash -s personal http.forwardproxy
(使用说明,参见https://getcaddy.com/)

setcap 'cap_net_bind_service=+ep' /usr/local/bin/caddy

root@umh:~# curl https://getcaddy.com | bash -s personal http.forwardproxy
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  7380  100  7380    0     0  18665      0 --:--:-- --:--:-- --:--:-- 18683
Downloading Caddy for linux/amd64 (personal license)...
Download verification OK
Extracting...
Putting caddy in /usr/local/bin (may require password)
Caddy 0.11.0 (non-commercial use only)
Successfully installed
root@umh:~# which caddy
/usr/local/bin/caddy

root@umh:~#
  • 编写配置文件-caddyfile
Caddy的配置是靠一个caddyfile的配置文件来进行配置的,其配置语法非常简单,和Nginx的配置文件很类似。如下就是一个最简单的配置文件:
yourdomain.com:1443 {
gzip
log access.log
forwardproxy
}
可以看到直接将域名 和端口写到配置文件中, 在配置块中,我们打开了gzip选项,启用了log日志以及forwardproxy功能。 详细的forwardproxy 功能详见 forwardproxy,上面提到的功能配置项都有了。
NOTICE:由于启动caddy时,需要从Letsencrypt.org获取HTTPS证书。为了避免关闭服务器上的占用80端口的webserver程序,需加上参数:
-http-port 82
  • 运行程序:
$ caddy -http-port 82 -conf ~/caddyfile &
运行起来之后,就可以在chrome浏览器中设置 HTTP2的访问了。对于SwitchyOmega而言, 代理协议选择HTTPS,然后就能正常使用此https Proxy了。
不过命令:caddy -http-port 82 -conf ~/caddyfile &还是容易退出,我们可以利用systemd来把caddy -http-port 82 -conf ~/caddyfile运行为service:
nano /etc/systemd/system/caddy-https-proxy.service
其内容为:
[Unit]
After=network.target

[Service]
ExecStart=/usr/local/bin/caddy -http-port 82 -conf /root/caddyfile
Restart=always

[Install]
WantedBy=multi-user.target

然后运行:
systemctl start caddy-https-proxy
systemctl enable caddy-https-proxy

caddy --conf ~/caddyfile
FIN
依托letsencrypt和Caddy内建的HTTP2功能,再加上 Caddy 简单易用的插件功能,使得我们可以抛弃nghttpx和Squid这一套,直接就能够使用HTTP2 Proxy。
还记得一开始折腾 HTTP2 Proxy的痛苦,现在不用了,直接一次性搞定。让我畅游HTTP2。感谢Caddy和 Sergey Frolov。)
---------------------------------------------------------------

用Caddy2搭建HTTPS proxy

 

由于官方已经放弃Caddy v1,使用Caddy v2打入forwardproxy插件,快速搭建HTTPS代理,毕竟Caddy自动请求SSL证书,能够省下许多不必要的折腾

申请域名和填写DNS记录

首先你要注册一个免费域名,这个可以到Freenom上免费搞一个,不多讲。

域名注册好后,需要添加DNS记录,如果没有v6IP的话,就填一个v4IP的A记录,如下:

Name (prefix)TypeTTLTarget

A3600这里填写vps上的v4IP

AAAA3600vps上的v6IP,没有可不填

上面完成后,就开始服务端的配置了。

服务端安装Caddy2

v2版需要自己编译打入插件。

下面说说怎么自己编译caddy2并打入插件。

服务端编译Caddy2

配置Go

下载go并解压到/usr/local路径下:

wget https://dl.google.com/go/go1.14.linux-amd64.tar.gz -O - | tar -xz -C /usr/local/

设置go环境变量,也可以写入到profile中

vi ~/.profile 添加下面内容:

export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$HOME/.cargo/bin
export GOROOT=/usr/local/go
export GOBIN=$GOROOT/bin
export PATH=$PATH:$GOBIN

保存后,source ~/.profile

下载Xcaddy

wget https://github.com/caddyserver/xcaddy/releases/download/v0.1.5/xcaddy_0.1.5_linux_amd64.tar.gz -O - | tar -xz -C /usr/bin/

运行xcaddy并打入forwardproxy@caddy2插件:

xcaddy build master \
--with github.com/caddyserver/forwardproxy@caddy2

编译完成后caddy二进制文件会在root目录下,俺们可以移到/usr/bin/下运行:

mv caddy /usr/bin

配置caddy.json文件

上面安装好caddy2后,现在就来配置caddy.json文件,caddy v2的配置文件改成json后,有点复杂,博主也是折腾了好久。

新建一个caddy文件夹,存放caddy.json文件

mkdir -p /etc/caddy

nano /etc/caddy/caddy.json

编写caddy.json文件,添加内容如下:

{
"admin": {"disabled": true},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [":1443"],
"logs": {},
"routes": [{
"handle": [{
"handler": "forward_proxy",
"hide_ip": true, //隐藏客户端IP
"hide_via": true
}]
},
{
"match": [{"host": ["xxxx.com"]}], //域名
"handle": [{
"handler": "vars",
"root": "/var/www/html" //网站根目录
}],
"terminal": true
}],
"tls_connection_policies": [{
"match": {"sni": ["xxxx.com"]} //域名
}],
"experimental_http3": false, //未开启UDP,false改成true即可开启
"allow_h2c": false //未开启h2c,false改成true即可开启
}
}
},
"tls": {
"automation": {
"policies": [{
"subjects": ["xxxx.com"], //域名
"issuers": [{
"email": "email@email", //申请证书邮箱
"module": "acme"
}]
}]
}
}
}
}

上面需要修改的地方是自己的域名,添加自己的邮箱就可以了。

caddy.json文件建议本地编辑好后再scp到服务端,vi修改,容易换行错误,用时请删除上面的中文注释。

检查80端口有没有被其它服务占用,因为caddy需要用80/443端口申请证书,否则会启动失败。下面命令检查端口占用情况:

netstat -lntp

上面配置、检查完就可以启动caddy了,启动命令如下:

/root/caddy run --config /etc/caddy/caddy.json

如果启动后,出现申请证书不成功的情况,一般是因为域名已经申请过证书导致的,建议DNS解析页面新建一个子域名,重新配置、启动申请证书。

启动后会发现,caddy一直在前台运行,很不方便,这个时候俺们就需要用systemctl来启动caddy。

注册systemd服务

新建caddy.service文件,命令:

vi /etc/systemd/system/caddy.service

添加如下内容:

[Unit]
After=network.target

[Service]
ExecStart=/root/caddy run --config /etc/caddy/caddy.json

[Install]
WantedBy=multi-user.target

重载systemctl服务 systemctl daemon-reload

到这也就可以启动caddy了,启动命令

systemctl start caddy

systemctl enable caddy #添加开机自启动.

服务端到此也就部署完成了,下面来折腾客户端。https代理,不必多讲,安全、快捷,电脑端不用另装客户端,移动端大多APP都支持http/https代理。

(https://caddyserver.com/docs/getting-started)

客户端配置

电脑端浏览器安装SwitchyOmega插件,新建一个代理情景模式,代理协议填写HTTPS,代理服务器填写你的域名,端口填写你的服务器所使用的端口,比如1443,最后点击右边小锁,填写你的用户名和密码,就可以使用了,如果需要分流的话,可以另建立一个自动切换模式。

移动端,俺们以小火箭为例,小火箭,选择https代理,服务器填写你的域名。

Quantumult X 配置文件/编辑/server_local字头下填写:

http=你的域名:1443, username=用户名, password=密码, over-tls=true, tls-verification=true, tls-host=你的域名, fast-open=false, udp-relay=true, tls13=true, tag=caddy2

最后推荐几个移动端支持https代理的软件,iOS:Quantumult X ,小火箭, Android:Clash, Surfboard.


注:Caddy会自动创建letsencrypt提供的证书,并自动续期。(自动续期哦,太爽了. )
 疑难解答:https://caddy.community/t/what-does-unknown-field-issuer-mean/16491/
---------------------------------------------------------------------------------

使用Caddy搭建TLS1.3+HTTP2代理


目前Caddy的官方版本还不支持TLS1.3,不过我们也可以自己编译然后魔改一下尝尝鲜。据说今年2月份左右应该就会支持TLS1.3了,之前一直不支持的原因是GO标准库里不支持,而Caddy是基于这个标准库的。
虽然最后测试使用TLS1.3+HTTP2代理的效果并不理想,Caddy日志里面一堆Reset,只能说GFW是真的牛批,但是通过这篇文章可以学到如何从源头构建一个GO程序以及为Caddy添加插件的方法,还是挺不错的~
先安装一下基本组件:
yum -y install git wget nano patch
下载GO的预构建包,前几天Golang刚发布了Go1.12beta2这个版本,这个版本里面就是带有TLS1.3支持的了:
wget https://dl.google.com/go/go1.12beta2.linux-amd64.tar.gz
tar -xzvf go1.12beta2.linux-amd64.tar.gz -C /usr/bin
编辑账户配置文件:
nano ~/.bash_profile
加入GO的PATH:
export PATH=$PATH:/usr/bin/go/bin
使其生效:
source ~/.bash_profile
新建一个文件夹命名为caddy并导入GOPATH为当前这个文件夹:
mkdir caddy && export GOPATH=$PWD/caddy
拉取项目源码:
go get github.com/mholt/caddy
go get github.com/caddyserver/builds
这个是用于配置HTTP代理的插件,如果你需要就拉取,不需要可以省略:
go get github.com/caddyserver/forwardproxy
进入到caddy源码目录:
cd $GOPATH/src/github.com/mholt/caddy
下载TLS1.3补丁:
wget https://www.hnrk.io/md/caddy.patch
把补丁打上去:
patch -p1 < caddy.patch
编辑如下源码,添加HTTP代理插件:
nano $GOPATH/src/github.com/mholt/caddy/caddy/caddymain/run.go
将如下内容插入到这个文件内:
_ "github.com/caddyserver/forwardproxy"
位置如图所示:

这里不局限于这个代理插件,只要是Caddy官方支持的插件都可以通过这个方式添加。
进入到构建目录:
cd $GOPATH/src/github.com/mholt/caddy/caddy
编译:
go run build.go
完成之后在当前目录下就会有caddy的二进制文件了,我们移动到/usr/local/bin:
mv caddy /usr/local/bin
新建两个目录,一个用于存放caddy的配置文件,一个用于存放caddy自动申请的ssl证书:
mkdir -p /etc/caddy && mkdir -p /etc/ssl/caddy
新建Caddyfile配置文件:
vi /etc/caddy/Caddyfile
写入:
tls.koko.cat {
 tls 1062951199@qq.com
 log stdout
 forwardproxy {
 basicauth user password
 hide_ip
 hide_via
 }
}
注:
1.tls后面修改为你自己的邮箱,caddy自动申请ssl证书需要。
2.basicauth设置一个你的账号和密码,一定要设置,否则你搭建完的代理可能一瞬就被扫描,变成公开代理。。。
然后我们配置systemd服务:
vi /etc/systemd/system/caddy.service
写入:
[Unit]
Description=Caddy HTTP/2 web server
Documentation=https://caddyserver.com/docs
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service

[Service]
Restart=on-abnormal
User=root
Group=root
Environment=CADDYPATH=/etc/ssl/caddy
ExecStart=/usr/local/bin/caddy -log stdout -agree=true -conf=/etc/caddy/Caddyfile
ExecReload=/bin/kill -USR1 $MAINPID
KillMode=mixed
KillSignal=SIGQUIT
TimeoutStopSec=5s

[Install]
WantedBy=multi-user.target
启动caddy以及设置开机启动:
systemctl start caddy
systemctl enable caddy
如有报错或运行出错,可以执行下面的命令查看日志:
journalctl --boot -u caddy.service
接着我们打开这个网站测试一下我们配置的站点是否支持TLS1.3:
https://www.ssllabs.com
经测试正常.
TLS1.3目前属于测试/实验阶段,服务端支持了还要客户端浏览器支持才行,下面是Chrome70开启TLS1.3的方法:
chrome://flags/
搜索TLS1.3,然后这里选择Enable(Final):
代理的话,Chrome这里使用SwitchyOmega按如图设置即可:

-----------------------------------------
  • The obvious requirement here is for HTTPS at the very least, but unless you properly automate a Let’s Encrypt certificate, you have to go to do some work to maintain the HTTPS server. Pretty much all HTTP proxies these days do not support HTTP/2, and it’s pretty easy to tell (from the client-side) if a server is acting as a forward proxy. I should not even have to mention that trusting random HTTP proxies on the Internet isn’t a great idea.

This article will show you how to secure your Internet connection in a way that overcomes these weaknesses. You will learn how to set up the Caddy web server as a secure forward proxy using the http.forwardproxy plugin, which has these advantages:

  • HTTPS enabled by default. All certificate maintenance is automated. It just works!
  • Full-duplex HTTP/2. This proxy speaks HTTP/2 both ways, which gives you faster page load times.
  • End-to-end encryption. For sites that use HTTPS, your connection to the origin is E2E encrypted. Even the proxy server cannot decrypt your connection between your computer and the origin site. Once the HTTPS tunnel is established with the proxy, it simply shuttles bytes in a two-way stream. Underlying TLS connections remain intact. (Even a nefarious proxy can’t read the content of your underlying HTTPS traffic, but you should still only use proxies you trust. There’s more to traffic than its contents.)
  • Probe resistance (experimental). This feature hides the proxy behavior of the server from everyone except to those who already know how to access it and are authorized to use it. Only a secret link specified by you (the server operator) exposes the authentication prompt.
  • Innocuous traffic patterns. Since this proxy is not a VPN or SSH tunnel, it blends in better with the rest of HTTPS traffic.
  • Standard ports. Where VPN ports and SSH port 22 are blocked by firewalls, port 443 is almost always allowed because accessing the Web is so standard for most use cases. (But you can still customize the port.)
  • Hides your IP address. With just one line of config, this proxy will not add the user’s IP to the HTTP “Forwarded” header. (Note that there are ways around this out of the proxy’s control, like WebRTC in browsers.)
  • Access controls. You can specify users and passwords to restrict access to the proxy, as well as a port whitelist for the proxy.
  • Easy to set up! Best of all, this proxy is easy to get running and hard to get wrong because of sane defaults and automatic HTTPS. Caddy is designed to be easy to use to reduce the potential error surface of misconfigurations.

In particular, we’re interested in exploring how helpful this software software could be in circumventing censorship. It definitely needs more careful vetting (read on) but we hope people will try it out in low-or-no-risk scenarios.

This plugin was developed by Sergey Frolov while interning at Google and the source is available on GitHub.

Presented “As-Is”

I make no guarantees; use this tutorial and the server software at your own risk. There are a number of edge cases that ultimately depend on your client and your threat model; see especially the warning in the client configuration section below.

This technique is still fairly new. We want to make it even safer. So we do not recommend using it in high-risk situations. If you find a way to improve it, though, please contribute your feedback, issues, and pull requests!

Setting up the Server

First, you’ll need a machine that is accessible with a public IP address. Home servers can work if you forward the port(s) properly. You can also rent a cloud instance from any reputable cloud provider for a few dollars per month. Once you have such a machine, here’s what to do:

Download Caddy with the http.forwardproxy plugin included. To do that, make sure you select it in the plugins list on the download page!

Install Caddy; this is as easy as extracting the archive and putting the binary in your PATH, or using the one-line auto-installer script shown at the bottom of the download page after you select the plugins you want.

Make a file called Caddyfile that looks like this:

example.comroot /path/to/your/site
forwardproxy {
basicauth user pass
probe_resistance secret.localhost
hide_ip
}

You must replace:

  • example.com with the actual domain name pointed at your machine
  • /path/to/your/site with the actual path to the root of your site (or an empty or decoy folder if you have no site)
  • user pass with a username and password of your choice (otherwise anyone could use your server!)
  • secret.localhost with a custom, secret hostname to enable probe resistance; strongly recommended to end with “.localhost”

Then run caddy in the same folder as your Caddyfile. After a few seconds, your probe-resistant, IP-hiding, full-duplex, HTTP/2 proxy will be running with a fully-managed TLS certificate from Let’s Encrypt!

The default port is 443 (the HTTPS port) unless you specify otherwise. Read more about the Caddyfile if you want to customize further.

Setting up the Client

Now how do you use your proxy server? With a client, of course!

There are several ways to do this. For example, if you want to secure your web browsing in Chrome, you can use an extension like Proxy SwitchyOmega to configure the connection. Firefox has some network options built into its settings. You can also configure your entire OS or mobile phone to use the proxy for all applications (except those that are specifically configured to not use your OS’ proxy settings). However, we found only Chrome+SwitchyOmega to be reliable, which we explain below.

** WARNING! A weakness in any part of the proxy configuration could leak information. Even if your proxy server is secure, your client may not be. Clients which do not honor the proxy settings for all network traffic could put you at risk. This includes major browsers and operating systems. For example, browsers don’t put WebRTC requests through the proxy (by design, sigh). One way to mitigate this risk somewhat is to use a VM that tunnels all traffic across a virtual NIC. This is obviously more involved, so act according to your threat model. If you just need basic public Internet cafe privacy to check your email, maybe the VM is overkill. That’s up to you.

This was the most reliable and simple client configuration I tried.

Install SwitchyOmega. It comes with an example proxy profile (on the left) which you can modify, or you can create a new one. In the table, select “HTTPS” for the “Protocol” field and type in your domain name and the port:

Image for post

Click the lock icon by the port and enter your username and password:

Image for post

After save your credentials, click the green “Apply Changes” button to the left:

Image for post

You can turn on the proxy by clicking it from your menu:

Image for post

Congrats! Now all your Chrome connections are proxied securely through your server.

Like Chrome, Firefox also has a SwitchyOmega extension. I would suggest using extension first, since you are likely to have more success than with Firefox’s built-in network settings. The instructions are nearly the same as above.

Using Firefox’s built-in network configuration settings, I was not able to get this working in Firefox (on macOS 10.12.6) for a server with probe_resistance enabled. It appears to be a bug in Firefox. Firefox hangs when loading the page, for several minutes — and spins the CPU at 100%. Even after closing the application, my computer ran sluggish for almost a day until I found the firefox process still running in the background still using 100% of my CPU. Other than using the SwitchyOmega extension, there are two other workarounds on Mac: Use Chrome, or disable probe resistance.

To change Firefox’s built-in network configuration, go to Menu -> Preferences:

Image for post

Then Network Proxy, and click “Settings…”:

Image for post

Here you have a few options. You can “Use system proxy settings” which should use your operating system’s proxy settings. You can specify a “Manual proxy configuration” where you enter the hostname and port your proxy is listening on (EDIT: As noted in a comment, this will not utilize TLS-to-proxy. So don’t do this, use the PAC file instead — next sentence). Or, if you use the serve_pac server setting, you can specify its URL:

Image for post

Save your settings, and you should be good to go. Maybe. Good luck!

This will change the proxy settings for the entire system. Make sure this is really what you want to do instead of using the proxy only with your web browser!

Unfortunately, I was not able to successfully get macOS’ system-wide proxy configurations to work reliably, even with probe resistance / authentication turned off. I’ve also been told that some previous version(s) of macOS (or OS X) could even kernel panic when using a TLS proxy. (But I was able to get Firefox and Chrome using the proxy successfully, as described above.)

If you want to experiment with getting system-wide proxy config working, open System Preferences and go to Network. Choose your active network interface on the left and click the “Advanced” button in the lower-right. Click the “Proxies” tab. I tried both the “Automatic Proxy Configuration” (where you give the .pac file URL) and the “Secure Web Proxy (HTTPS)” options. Both had different but equally disappointing levels of support in various applications.

Image for post

Good news! Setting Linux’s system-wide proxy settings worked fairly well in my testing.

Open Network preferences and select “Network Proxy”. Choose “Automatic” for the Method, and type the configuration URL of the .pac file. (You’ll have to enable serve_pac on your server configuration inside the forwardproxy directive; choose a secret URL to serve the file on, so it doesn’t defeat your probe resistance). If you don’t want to use a .pac file, you can choose “Manual” for the Method and enter your hostname and port (443 probably) — use it for all protocols.

Since there isn’t a way to configure the system proxy to send credentials, you need to use your secret link to authenticate. Only the secret link will prompt the browser to enter credentials. You may have to do that every time you re-open your browser.

So open your web browser (maybe double-check its network configuration if you’re not sure) and go to your secret link. Enter your credentials, and you’re good to go!

This method works for WiFi networks. I haven’t looked into how to set a proxy for the cell network. It requires using a .pac file. If you haven’t already, enable the serve_pac option within the forwardproxy directive. For probe resistance, I recommend specifying a secret URL to serve it on, rather than the default /proxy.pac.

Go to your WiFi settings and long-tap on the current network. Choose “Modify Network”:

Image for post

Under Proxy, choose “Proxy Auto-Config” and type the URL to your .pac file:

Image for post

Save settings. With probe resistance enabled, you’ll have to navigate to your secret link in your browser to expose the authentication prompt. Then you should be good to go. Remember: this doesn’t apply to the cell network.

**IMPORTANT NOTE** While this can work on Windows, apparently Windows does not support TLS-to-proxy, meaning your transmissions to your proxy server will not be properly secured. If you want this fixed, you should raise a stink with Microsoft. I don’t have a Windows machine to confirm this, so in the meantime, use another solution.

These instructions are for Windows 7, but the same basic idea works in more recent versions of Windows. I’m borrowing these instructions from Sergey’s blog (with permission) because I don’t have Windows:

Control Panel → Network and Internet → Internet Options → Connections → LAN settings → Check “Use a proxy server…” and paste your “https://yourserver.com” in Address and “443” in port. Don’t lose “https://” in Address, Windows likes to remove it when you open LAN settings window again.

Image for post
Image borrowed with permission from Sergey Frolov: https://sfrolov.io/2017/08/secure-web-proxy-client-en

Conclusion

Caddy’s http.forwardproxy plugin is a promising alternative to using VPNs or SSH tunnels for certain tasks and for certain threat models. We hope this technology will help provide greater access to the Web to more people because of its unique properties and ease of use. However, client support for TLS-to-proxy must improve and become more reliable and predictable. We hope that client support for secure proxying will improve and become more robust in the future.

from https://medium.com/@mattholt/private-browsing-without-a-vpn-e91027552700

----------------------------------------------


Caddy2 简明教程 

Caddy 是一个 Go 编写的 Web 服务器,类似于 Nginx,Caddy 提供了更加强大的功能,随着 v2 版本发布 Caddy 已经可以作为中小型站点 Web 服务器的另一个选择;相较于 Nginx 来说使用 Caddy 的优势如下:

  • 自动的 HTTPS 证书申请(ACME HTTP/DNS 挑战)
  • 自动证书续期以及 OCSP stapling 等
  • 更高的安全性包括但不限于 TLS 配置以及内存安全等
  • 友好且强大的配置文件支持
  • 支持 API 动态调整配置(有木有人可以搞个 Dashboard?)
  • 支持 HTTP3(QUIC)
  • 支持动态后端,例如连接 Consul、作为 k8s ingress 等
  • 后端多种负载策略以及健康检测等
  • 本身 Go 编写,高度模块化的系统方便扩展(CoreDNS 基于 Caddy1 开发)
  • ……

就目前来说,Caddy 对于我个人印象唯一的缺点就是性能没有 Nginx 高,但是这是个仁者见仁智者见智的问题;相较于提供的这些便利性,在性能可接受的情况下完全有理由切换到 Caddy。

一、编译 Caddy2

注意: 在 Caddy1 时代,Caddy 官方发布的预编译二进制文件是不允许进行商业使用的,Caddy2 以后已经全部切换到 Apache 2.0 License,具体请参考 issue#2786

在默认情况下 Caddy2 官方提供了预编译的二进制文件,以及自定义 build 下载页面,不过对于需要集成一些第三方插件时,我们仍需采用官方提供的 xcaddy 来进行自行编译;以下为具体的编译过程:

1.1、Golang 环境安装

本部分编译环境默认为 Ubuntu 20.04 系统,同时使用 root 用户,其他环境请自行调整相关目录以及配置;编译时自行处理好科学上网相关配置,也可以直接用国外 VPS 服务器编译。

首先下载 go 语言的 SDK 压缩包,其他平台可以从 https://golang.org/dl/ 下载对应的压缩包:

wget https://golang.org/dl/go1.15.6.linux-amd64.tar.gz
下载完成后解压并配置相关变量:
# 解压
tar -zxvf go1.15.6.linux-amd64.tar.gz

# 移动到任意目录
mkdir -p /opt/devtools
mv go /opt/devtools/go

# 创建 go 相关目录
mkdir -p ${HOME}/gopath/{src,bin,pkg}

# 调整变量配置,将以下变量加入到 shell 初始化配置中
# bash 用户请编辑 ~/.bashrc
# zsh 用户请编辑 ~/.zshrc
export GOROOT='/opt/devtools/go'
export GOPATH="${HOME}/gopath"
export GOPROXY='https://goproxy.cn' # 如果已经解决了科学上网问题,GOPROXY 变量可以删除,否则可能会起反作用
export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}"

# 让配置生效
# bash 用户替换成 ~/.basrc
# 重新退出登录也可以
source ~/.zshrc
配置完成后,应该在命令行执行 go version 有成功返回.

1.2、安装 xcaddy

按照官方文档直接命令行执行 go get -u github.com/caddyserver/xcaddy/cmd/xcaddy 安装即可.
安装完成后应当在命令行可以直接执行 xcaddy 命令:
xcaddy --help

1.3、编译 Caddy2

编译之前系统需要安装 jqcurlgit 命令,没有的请使用 apt install -y curl git jq 命令安装;

自行编译的目的是增加第三方插件方便使用,其中官方列出的插件可以从 Download 页面获取到.

其他插件可以从 GitHub 上寻找或者自行编写,整理好这些插件列表以后只需要使用 xcaddy 编译即可:

# 获取最新版本号,其实直接去 GitHub realse 页复制以下就行
# 这里转化为脚本是为了方便自动化
export version=$(curl -s "https://api.github.com/repos/caddyserver/caddy/releases/latest" | jq -r .tag_name)

# 使用 xcaddy 编译
xcaddy build ${version} --output ./caddy_${version} \
--with github.com/abiosoft/caddy-exec \
--with github.com/caddy-dns/cloudflare \
--with github.com/caddy-dns/dnspod \
--with github.com/caddy-dns/duckdns \
--with github.com/caddy-dns/gandi \
--with github.com/caddy-dns/route53 \
--with github.com/greenpau/caddy-auth-jwt \
--with github.com/greenpau/caddy-auth-portal \
--with github.com/greenpau/caddy-trace \
--with github.com/hairyhenderson/caddy-teapot-module \
--with github.com/kirsch33/realip \
--with github.com/porech/caddy-maxmind-geolocation \
--with github.com/caddyserver/format-encoder \
--with github.com/mholt/caddy-webdav
稍等片刻后将会生成编译好的二进制文件.
编译成功后可以通过 list-modules 子命令查看被添加的插件是否成功编译到了 caddy 中.

二、安装 Caddy2

2.1、宿主机安装

宿主机安装 Caddy2 需要使用 systemd 进行守护,幸运的是 Caddy2 官方提供了各种平台的安装包以及 systemd 配置文件仓库;目前推荐的方式是直接采用包管理器安装标准版本的 Caddy2,然后替换自编译的可执行文件:
# 安装标准版本 Caddy2
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/gpg/gpg.155B6D79CA56EA34.key' | sudo apt-key add -
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/setup/config.deb.txt?distro=debian&version=any-version' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

# 替换二进制文件
systemctl stop caddy
rm -f /usr/bin/caddy
mv ./caddy_v2.3.0 /usr/bin/caddy

三、配置 Caddy2

Caddy2 的配置文件核心采用 json,但是 json 可读性不强,所以官方维护了一个转换器,抽象出称之为 Caddyfile 的新配置格式;关于 Caddyfile 的完整语法请查看官方文档 https://caddyserver.com/docs/caddyfile,本文仅做一些基本使用的样例.

3.1、配置片段

Caddyfile 支持类似代码中 function 一样的配置片段,这些配置片段可以在任意位置被 import,同时可以接受参数,以下为配置片断示例:
# 括号内为片段名称,可以自行定义
(TLS) {
protocols tls1.2 tls1.3
ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}

# 在任意位置可以引用此片段从而达到配置复用
import TLS

3.2、配置模块化

import 指令除了支持引用配置片段以外,还支持引用外部文件,同时支持通配符,有了这个命令以后我们就可以方便的将配置文件进行模块化处理:
# 引用外部的 /etc/caddy/*.caddy
import /etc/caddy/*.caddy

3.3、站点配置

针对于站点域名配置,Caddyfile 比较自由化,其格式如下:
地址 {
站点配置
}

关于这个 “地址” 接受多种格式,以下都为合法的地址格式:

  • localhost
  • example.com
  • :443
  • http://example.com
  • localhost:8080
  • 127.0.0.1
  • [::1]:2015
  • example.com/foo/*
  • *.example.com
  • http://

3.4、环境变量

Caddyfile 支持直接引用系统环境变量,通过此功能可以将一些敏感信息从配置文件中剔除:
# 引用环境变量 GANDI_API_TOKEN
dns gandi {$GANDI_API_TOKEN}

3.5、配置片段参数支持

针对于配置片段,Caddyfile 还支持类似于函数代码的参数支持,通过参数支持可以让外部引用时动态修改配置信息:
(LOG) {
log {
format json {
time_format "iso8601"
}
# "{args.0}" 引用传入的第一个参数,此处用于动态传入日志文件名称
output file "{args.0}" {
roll_size 100mb
roll_keep 3
roll_keep_for 7d
}
}
}

# 引用片段
import LOG "/data/logs/mritd.com.log"

3.6、自动证书申请

在启动 Caddy2 之前,如果目标域名(例如: www.example.com)已经解析到了本机,那么 Caddy2 启动后会尝试自动通过 ACME HTTP 挑战申请证书;如果期望使用 DNS 的方式申请证书则需要其他 DNS 插件支持,比如上面编译的 --with github.com/caddy-dns/gandi 为 gandi 服务商的 DNS 插件;关于使用 DNS 挑战的配置编写方式需要具体去看其插件文档,目前 gandi 的配置如下:
tls {
dns gandi {env.GANDI_API_TOKEN}
}
配置完成后 Caddy2 会通过 ACME DNS 挑战申请证书,值得注意的是即使通过 DNS 申请证书默认也不会申请泛域名证书,如果想要调整这种细节配置请使用 json 配置或管理 API。

3.7、完整模块化配置样例

了解了以上基础配置信息,我们就可以实际编写一个站点配置了;以下为本站的 Caddy 配置样例:

目录结构:

caddy
├── Caddyfile
├── yourdomain.com.caddy

3.7.1、Caddyfile

Caddyfile 主要包含一些通用的配置,并将其抽到配置片段中,类似与 nginx 的 nginx.conf 主配置;在最后部分通过 import 关键字引入其他具体站点配置,类似 nginx 的 vhost 配置。
(LOG) {
log {
# 日志格式参考 https://github.com/caddyserver/format-encoder 插件文档
format formatted "[{ts}] {request>remote_addr} {request>proto} {request>method} <- {status} -> {request>host} {request>uri} {request>headers>User-Agent>[0]}" {
time_format "iso8601"
}
output file "{args.0}" {
roll_size 100mb
roll_keep 3
roll_keep_for 7d
}
}
}

(TLS) {
# TLS 配置采用 https://mozilla.github.io/server-side-tls/ssl-config-generator/ 生成,SSL Labs 评分 A+
protocols tls1.2 tls1.3
ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}

(HSTS) {
# HSTS (63072000 seconds)
header / Strict-Transport-Security "max-age=63072000"
}

(ACME_GANDI) {
# 从环境变量获取 GANDI_API_TOKEN
dns gandi {$GANDI_API_TOKEN}
}

# 聚合上面的配置片段为新的片段
(COMMON_CONFIG) {
# 压缩支持
encode zstd gzip

# TLS 配置
tls {
import TLS
import ACME_GANDI
}

# HSTS
import HSTS
}

# 开启 HTTP3 实验性支持
{
servers :443 {
protocol {
experimental_http3
}
}
}

# 引入其他具体的站点配置
import /etc/caddy/*.caddy

3.7.2  urdomain.com.caddy


urdomain.com.caddy 为主站点配置,主站点配置内主要编写一些路由规则,TLS 等都从配置片段引入,这样可以保持统一:
www.urdomain.com {
# 重定向到 urdomain.com(默认 302)
redir https://urdomain.com{uri}

# 日志
import LOG "/data/logs/urdoamin.com.log"

# TLS、HSTS、ACME 等通用配置
import COMMON_CONFIG
}

urdomain.com {
# 路由
route /* {
reverse_proxy urdomain_com:80
}

# 日志
import LOG "/data/logs/urdomain.com.log"

# TLS、HSTS、ACME 等通用配置
import COMMON_CONFIG
}

四、启动与重载

配置文件编写完成后,通过 systemctl start caddy 可启动 caddy 服务器;每次配置修改后可以通过 systemctl reload caddy 进行配置重载,重载期间 caddy 不会重启(实际上调用 caddy reload 命令),当配置文件书写错误时,重载只会失败,不会影响正在运行的 caddy 服务器.

五、总结

本文只是列举了一些简单的 Caddy 使用样例,在强大的插件配合下,Caddy 可以实现各种 “神奇” 的功能,这些功能依赖于复杂的 Caddy 配置,Caddy 配置需要仔细阅读官方文档,关于 Caddyfile 的每个配置段在文档中都有详细的描述。

值得一提的是 Caddy 本身内置了丰富的插件,例如内置 “file_server”、内置各种负载均衡策略等,这些插件组合在一起可以实现一些复杂的功能;Caddy 是采用 go 编写的,官方也给出了详细的开发文档,相较于 Nginx 来说通过 Lua 或者 C 来开发编写插件来说,Caddy 的插件开发上手要容易得多;Caddy 本身针对数据存储、动态后端、配置文件转换等都内置了扩展接口,这为有特定需求的扩展开发打下了良好基础。

最终总结,综合来看目前 Caddy2 的性能损失可接受的情况下,相较于 Nginx 绝对是个绝佳选择,各种新功能都能够满足现代化 Web 站点的需求.






---------------

Caddy2 支持 WebP 和 AVIF

一、方案选型

目前对于 WebP 和 AVIF 格式支持大致有两种方案, 一种是动态转换, 另一种是静态转换.
  • 动态转换: 即在流量的代理层进行处理, 实现对用户的透明化, 用户无需进行任何更改, 由负载均衡器或者中间件进行动态转换处理.
  • 静态转换: 通过工具预先转换好, 然后通过请求匹配分析来选择返回的图片格式.

二、动态转换

懒得放代码了, 自己搓到一半放弃了.
对于 WebP 格式来说, 在 Caddy2 上动态转换比较简单, 基本上就是添加一个插件即可; 例如 caddy-webp 这个插件. 有关于 Caddy 的插件开发可以参考官方的 Extending Caddy 文档.
动态转换的核心思想是在接收到请求后, 判断请求中的 Accept 头, 如果包含 image/webp 则说明浏览器可以识别 WebP 格式图片(AVIF 同理); 此时可以将请求先转发给后续的 HTTP 处理逻辑, 待返回响应后将其暂时 Cache 住, 然后执行转换逻辑, 改变其格式后重新设置 Content-Type 头然后把新的格式数据返回给前端.
针对这种操作可以在负载均衡器层(例如 Caddy2 Plugin)完成, 也可以通过单独的中间件完成; 但无论哪种其涉及到一些缺陷(可优化):
  • 每次响应需要先完成 load 到内存进行转换(内存消耗大)
  • 转换过程会浪费 CPU 资源(CPU 一样消耗很大)
  • 转换过程可能导致请求失败
针对这些情况, 个人认为在生产环境资源充足的情况下完全可以解决, 核心思想还是一个: 缓存为王; 想办法在第一次转换后将其存储到缓存位置, 避免每次都转换; 如果资源足够甚至可以考虑内存级的缓存方案配合 LRU 等失效策略.

三、静态转换

经过一波搓代码从入门到放弃, 最终我选择了目前我这个小破站能承担得起的方案; 即先在本地执行转换然后直接扔到服务器上; 由于网站本身就是静态站点, 所以可以在 Caddyfile 中基于动态转换的套路处理请求头返回不同文件.
本地转换目前采用 optimizt 这个工具完成:
1
2
3
4
5
# 安装
npm i -g optimizt

# 转换
optimizt --force --webp --avif public/img
SH
这样在我的图片目录下就会生成同名的 WebP 和 AVIF 格式图片
最后只需要在 Caddyfile 中增加以下请求头匹配的配置即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# avif support
@acceptsAVIF {
header Accept *image/avif*
path_regexp avif ^(.+)\.(jpg|jpeg|png)$
}
handle @acceptsAVIF {
@hasAVIF file {re.avif.1}.avif
rewrite @hasAVIF {re.avif.1}.avif
}

# webp support
@acceptsWebp {
header Accept *image/webp*
path_regexp webp ^(.+)\.(jpg|jpeg|png)$
}
handle @acceptsWebp {
@hasWebp file {re.webp.1}.webp
rewrite @hasWebp {re.webp.1}.webp
}
SH

四、总结

动态转换比价适合商业化模式, 在资源支撑足够的情况下可以延迟随机、预热、高性能缓存等方式配合起来将图片透明转换完成, 对用户无感且友好. 小破站资源匮乏等情况比较适合静态转换完后逻辑匹配规则返回不同物理文件. 说实话我想弄成高大上的动态转换写点代码的, 但是弄到一半发现全是问题又没资源解决, 只能在打打嘴炮了.
 
from https://archive.is/djeBX#selection-323.0-743.156 
----

Caddyfile 语法浅析
 
发现大部分人在切换 Caddy 时遇到的比较大的困难就是这个 Caddyfile 不知道怎么写,一开始我也是很懵逼的状态,今天决定写写这个 Caddyfile 配置语法,顺便自己也完整的学学。

一、Caddy 配置体系

在 Caddy1 时代,Caddy 自创了一种被称之为 Caddyfile 的配置文件格式,当然可以理解为创造了一种语法,这里面深入的说就涉及到了编译原理相关知识,这里不再展开细谈(因为我也不会);Caddyfile 由内部的语法解析器进行语法、词法分析最后 “序列化” 到 Go 的配置结构体中。
随着 Caddy 壮大,到了 Caddy2 时代人们已经并不满足于单纯的 Caddyfile 配置,因为学习 Caddyfile 是有代价的,负载均衡器选型的切换本身就代价很大,还要去花心思学习 Caddyfile 语法,这无异非常痛苦。所以 Caddy2 在经过取舍过后决定使用 json 作为内部标准配置,然后其他类型的配置通过 Config Adapters 将其转换为 json 再使用,而 Caddyfile 的 Adapter 作为官方支持的内置 Adapter 存在。
最终要说明的是: Caddyfile 里支持哪些指令是由 Caddyfile 的 Adapter 决定的,内部的 json 配置对应的指令名称可能跟 Caddyfile 不同,也可能内部 json 支持一些指令,而 Caddyfile 根本不支持。

二、Caddyfile 基本结构

开局一张图,文章全靠编(下面是官方的语法结构图)

2.1、全局选项

在一个 Caddyfile 内(空白文本文件),如果仅以两个大括号括起来的配置就是全局配置项,例如下面的配置:
{
	debug
	http_port  8080
	https_port 8443
}
那么一共有哪些全局配置项呢?当然是看 官方文档:
{
	# General Options
	debug
	http_port  <port>
	https_port <port>
	order <dir1> first|last|[before|after <dir2>]
	storage <module_name> {
		<options...>
	}
	storage_clean_interval <duration>
	admin   off|<addr> {
		origins <origins...>
		enforce_origin
	}
	log [name] {
		output  <writer_module> ...
		format  <encoder_module> ...
		level   <level>
		include <namespaces...>
		exclude <namespaces...>
	}
	grace_period <duration>

	# TLS Options
	auto_https off|disable_redirects|ignore_loaded_certs
	email <yours>
	default_sni <name>
	local_certs
	skip_install_trust
	acme_ca <directory_url>
	acme_ca_root <pem_file>
	acme_eab <key_id> <mac_key>
	acme_dns <provider> ...
	on_demand_tls {
		ask      <endpoint>
		interval <duration>
		burst    <n>
	}
	key_type ed25519|p256|p384|rsa2048|rsa4096
	cert_issuer <name> ...
	ocsp_stapling off
	preferred_chains [smallest] {
		root_common_name <common_names...>
		any_common_name  <common_names...>
	}

	# Server Options
	servers [<listener_address>] {
		listener_wrappers {
			<listener_wrappers...>
		}
		timeouts {
			read_body   <duration>
			read_header <duration>
			write       <duration>
			idle        <duration>
		}
		max_header_size <size>
		protocol {
			allow_h2c
			experimental_http3
			strict_sni_host
		}
	}
}
这些全局配置具体都什么意思这里就不细说了,请自行查阅文档;当然文档也可能并不一定准确,有些兴趣的可以去查看 Caddy 源码,这些都在源码中定义了 caddyconfig/httpcaddyfile/options.go:28

2.2、代码块

叫代码块可能不太恰当,也可以叫做配置块或配置片段;这是 Caddyfile 比较棒的一个功能,配置片段可以实现类似代码这种引用使用,方便组合配置文件;配置片段的语法如下:
(配置片段名字) {
    # 这里写配置片段的内容
}
下面是一个配置片段示例(不能运行,只是举例):
# 定义一个叫 TLS_INTERMEDIATE 的配置片段
(TLS_INTERMEDIATE) {
    protocols tls1.2 tls1.3
    ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}

www.mritd.com {
    # 重定向
    redir https://mritd.com{uri}

    # 这里引用上面的 TLS_INTERMEDIATE 配置
    import TLS_INTERMEDIATE
}
这种写法与下面的配置等价,目的就是增加配置的重用和规范化:
www.mritd.com {
    # 重定向
    redir https://mritd.com{uri}

    protocols tls1.2 tls1.3
    ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}

2.3、站点配置

站点配置是 Caddyfile 的核心中的核心,从开局的图上也可以看到,能在 “Top Level” 上存在的只有三种配置,其中就包含了这个站点配置块,站点配置块格式如下:
站点域名 {
    # 其他配置
}
以下是两个合法的站点配置示例:
example1.com {
	root * /www/example.com
	file_server
}

example2.com {
	reverse_proxy localhost:9000
}

2.4、自定义匹配器

请求匹配器是 Caddy 内置的一种针对请求的过滤工具,有点类似于 nginx 配置中的 location /api {...},只不过 Caddyfile 中的匹配器更加强大;标准的请求匹配器列表如下:
自定义命名匹配器的作用是组合多个标准匹配器,然后实现复用,自定义命名匹配器语法如下:
# @ 后面跟一个自定义名称
@api {
    # 标准匹配器组合
    path /api/*
    host example.com
}
然后这个自定义的命名匹配器可以在其他位置引用:
example.com {
    @api {
        # 标准匹配器组合
        path /api/*
        host example.com
    }
    
    reverse_proxy @api 127.0.0.1:9000
}

三、Caddyfile 语法细节

3.1、Blocks

Caddyfile 中的配置块可以理解为代码中的作用域,其包含两个大括号范围内的所有配置:
... {
    ...
}
当 Caddyfile 中只有一个站点配置,且不需要其他全局配置等信息时,Blocks 可以被省略,例如:
example.com {
    reverse_proxy /api/* localhost:9001
}
这个配置可以直接简写为:
example.com

reverse_proxy /api/* localhost:9001
这么做的目的是方便单站点快速配置,但是一般不常用也不推荐使用。在同一个 Caddyfile 中可以包含多个站点配置,只要地址不同即可:
example.com {
    ...
}

abcd.com {
    ...
}

3.2、Directives

指令是指描述站点配置的一些关键字,例如下面的站点配置文件:
example.com {
    reverse_proxy /api/* localhost:9001
}
在这个配置文件中 reverse_proxy 就是一个指令,同时指令还可能包含子指令(Subdirectives),下面的配置中 lb_policy 就是 reverse_proxy 的一个子指令:
example.com {
    reverse_proxy localhost:9000 localhost:9001 {
        lb_policy first
    }
}

3.3、Tokens and quotes

在 Caddyfile 被 Caddy 读取后,Caddy 会将配置文件解析为一个个的 Token;Caddyfile 中所有 Token 都认为是空格分割,所以如果某些指令需要传递参数时我们需要通过合理的空格和引号来确保 Token 正确解析:
example.com {
    # 这里 localhost:9000 localhost:9001 空格分割就认为是两个 Token
    reverse_proxy localhost:9000 localhost:9001
}
如果某些参数需要包含空格,那么需要使用双引号包裹:
example.com {
    file_server {
        # 双引号包裹住有空格的参数
        root "/data/Application Data/html"
    }
}
如果这个参数里需要包含双引号,只需要通过反斜线转义即可,例如 "\"a b\"";如果有太多的双引号或者空格,可以使用 Go 语言中类似的反引号来定义 “绝对字符串”:
example.com {
    file_server {
        # 反引号包裹
        root `/data/Application Data/html`
    }
}

3.4、Addresses

Caddyfile 中的地址其实是一种很宽泛的格式,在上面讲站点配置时其实前面的字符串并不一定是域名,准确的说应该是地址:
地址 {
    # 站点具体配置
}
在 Caddyfile 中以下格式全部都是合法的地址:
  • localhost
  • example.com
  • :443
  • http://example.com
  • localhost:8080
  • 127.0.0.1
  • [::1]:2015
  • example.com/foo/*
  • *.example.com
  • http://
需要注意的是: 自动 HTTPS 是 Caddy 服务器的一个重要特性,但是自动 HTTPS 会隐式进行,除非在地址中明确的写明 http://example.com 这种格式时 Caddy 才会单纯监听 HTTP 协议,否则域名格式的地址 Caddy 都会进行 HTTPS 证书申请。
如果地址中指定了域名,那么只有匹配到域名的请求才会接受;例如地址为 localhost 的站点不会响应 127.0.0.1 方式的访问请求。同时地址中可以采用 * 作为通配符,通配符作用域仅在域名的英文句号 . 之内,意思就是说 *.example.com 会匹配 test.example.com 但不会匹配 abc.test.example.com
如果多个域名/地址共享一个站点配置,可以采用英文逗号分隔的方式写在一起:
example.com,www.example.com,localhost,127.0.0.1:8080 {
    file_server {
        root /data/html
    }
}

3.5、Matchers

匹配器其实在第一部分已经介绍过,这里仅做一下简单说明;匹配器一般紧跟在指令之后,其大致格式分为以下三种:
  • *: 匹配所有请求(通配符)
  • /path: 匹配特定路径
  • @name: 自定义命名匹配器
匹配器的用法样例如下:
# 自定义一个叫 websockets 的匹配器
@websockets {
    # 匹配请求头 Connection 中包含 Upgrade 的请求
    header Connection *Upgrade*
    
    # 匹配请求头 Upgrade 为 websocket 的请求
    header Upgrade    websocket
}

# 反向代理时使用 @websockets 匹配器
reverse_proxy @websockets localhost:6001
具体更细节的官方匹配器使用限于篇幅这里不再详细说明,请自行阅读 官方文档

3.6、Placeholders

占位符可以理解为 Caddyfile 内部的变量替换符号,占位符同样以大括号包裹,同时支持转义:
# 标准占位符
{system.hostname}

# 避免冲突可进行转义
\{system.hostname\}
Caddyfile 内部可用的占位符有很多,但是并非在所有情况下都可用,比如 HTTP 相关的占位符仅在处理 HTTP 请求相关配置中才可用;同时占位符也支持简写,下面是官方目前支持的占位符列表:

3.7、Snippets

片段上面也介绍过了,这里说一下片段更高级的用法: 支持参数传递;下面是定义一个通用日志格式,然后通过参数引用实现不同站点使用不同日志文件的配置:
(LOG_COMMON) {
    log {
        format formatted "[{ts}] {request>remote_addr} {request>proto} {request>method} <- {status} -> {request>host} {request>uri} {request>headers>User-Agent>[0]}"  {
            time_format "iso8601"
        }
        
        # {args.0} 声明引用传入的第一个参数
        output file "{args.0}" {
            roll_size 100mb
            roll_keep 3
            roll_keep_for 7d
        }
    }
}

example.com {
    # 此时 /data/log/example.com.log 作为 "{args.0}" 被传入
    import LOG_COMMON /data/log/example.com.log
}

3.8、Comments

注释没啥好说的,以 # 作为开头就行了。

3.9、Environment variables

环境变量和占位符类似,不同的是占位符是 Caddyfile 内置的变量,而环境变量是引用系统环境变量;环境变量的使用格式如下(推荐全大写):
# 引用一个叫 SITE_ADDRESS 的环境变量
{$SITE_ADDRESS} {
    # 站点具体配置...
}
上面的配置在 Caddy 启动时会读取 SITE_ADDRESS 作为监听地址,如果 SITE_ADDRESS 读取不到则会报错退出;如果想要为 SITE_ADDRESS 设置默认值,那么只需要使用如下格式即可:
{$SITE_ADDRESS:localhost} {
    # 站点具体配置...
}

四、其他补充

Caddyfile 并不是万能的,但是 Caddyfile 因为更易于编写和维护所以使用比较广泛;在第一部分介绍 Caddy 的配置文件体系时已经说明了,实际上 Caddy 内部是使用 json 作为配置的;这时就可能出现一些极端情况,比如说真的某个配置只能通过 json 配置,那么这时候可以考虑先通过 json 管理 API 进行动态修改,然后再去向官方发 issue,有能力也可以直接 PR;API 动态修改的流程如下:
首先假设你已经有一个能够正常启动的 Caddyfile,但是某个配置选项不支持,这时候你可以通过 API 获取内部的 json 配置:
# 结尾一定要有 /
➜ ~ curl localhost:2019/config/
{"apps":{"http":{"servers":{"srv0":{"listen":[":80"],"routes":[{"handle":[{"canonical_uris":false,"handler":"file_server","hide":["./Caddyfile"]}]}]}}}}}
得到这个配置以后,你可以通过格式化工具格式化 json,然后添加特定选项,再将其保存到一个配置文件中,然后重新 load 回去即可:
# 假设修改后的 json 文件叫 caddy.json
➜ ~ curl -XPOST http://localhost:2019/load -H "Content-Type: application/json" -d @caddy.json
 from https://archive.is/2AYwO
------
 
Caddy2 file server 的自动重定向问题_

一、事情起因

自打很多年前开始使用静态博客工具来发布博客,现在基本上博客源码编译后就是一堆 html 等静态文件;一开始使用 nginx 作为静态文件服务器,后来切换到的 Caddy2;不过最近在 Google Search Console 中发现了大量的无效链接,给出的提示是 “网页会自动重定向”。

经过测试后发现这些链接地址在访问时都会重定向一下,然后在结尾加上 /;没办法我就开始探索这个 / 是怎么来的了。

二、源码分析

没办法,也不知道那个配置影响的,只能去翻 file server 的源码,在几经查找之后找到了以下代码(而且还带着注释):

从代码逻辑上看,只要 *fsrv.CanonicalURIs 这个变量为 true,那么就会触发自动重定像,并在 “目录” 尾部补上 /;注释里也说的很清楚是为了目录规范化,如果想看详细讨论可以参考那两个 issue。

三、解决方案
3.1、Admin API

翻了这个 *fsrv.CanonicalURIs 变量以后,突然发现 Caddyfile 里其实是不支持这个配置的;所以比较 low 的办法就是利用 Admin API,先把 json 弄出来,然后加上配置再。POST 回去:

{
    "apps": {
        "http": {
            "servers": {
                "srv0": {
                    "listen": [
                        ":80"
                    ],
                    "routes": [
                        {
                            "handle": [
                                {
+                                    "canonical_uris": false,
                                    "handler": "file_server",
                                    "hide": [
                                        "./Caddyfile"
                                    ]
                                }
                            ]
                        }
                    ]
                }
            }
        }
    }
}

curl -XPOST http://localhost:2019/load -H "Content-Type: application/json" -d @caddy.json

3.2、升级版本

现在可以直接从 master 构建 Caddy,或者等待 v2.4.4 版本发布,这两种方式产生的 Caddy 二进制文件已经支持了这个配置选项,配置样例如下:

:80

file_server {
    disable_canonical_uris
}
-------
 
Caddy2 的 ListenerWrapper
 
本文所有源码分析基于 Caddy2 v2.4.2 版本进行,未来版本可能源码会有变化,阅读本文时请自行将源码切换到 v2.4.2 版本。

一、这玩意是什么?

Caddy2 对配置文件中的 listener_wrappers 配置有以下描述:
Allows configuring listener wrappers, which can modify the behaviour of the base listener. They are applied in the given order.
同时对于 tls 这个 listener_wrappers 还做了一下说明:
There is a special no-op tls listener wrapper provided as a standard module which marks where TLS should be handled in the chain of listener wrappers. It should only be used if another listener wrapper must be placed in front of the TLS handshake.
综上所述,简单的理解就是 listener_wrappers 在 Caddy2 中用于改变链接行为,这个行为可以理解为我们可以自定义接管链接,这些 “接管” 更偏向于底层,比如在 TLS 握手之前做点事情或者在 TLS 握手之后做点事情,这样我们就可以实现一些魔法操作。

二、加载与初始化

在 Caddy2 启动时首先会进行配置文件解析,例如解析 Caddyfile、json 等格式的配置文件,listener_wrappers 在配置文件中被定义为一个 ServerOption:
该配置最终会被注入到 Server 的 listenerWrappers 属性中(先解析为 ListenerWrappersRaw 然后再实例化)
最后在 App 的启动过程中遍历 listenerWrappers 并逐个应用,在应用 listenerWrappers 时有个比较重要的顺序处理:
首先 Caddy2 会尝试在 net.Listener 上应用一部分 listenerWrappers,当触及到 tls 这个 token 的 listenerWrappers 之后终止应用;终止前已被应用的这部分 listenerWrappers 被认为是 TLS 握手之前的自定义处理,然后在 TLS 握手之后再次应用剩下的 listenerWrappers,后面这部分被认为是 TLS 握手之后的自定义处理。
最终对 ListenerWrapper 加载流程分析如下:
  • 首先解析配置文件,并将配置转化为 Server 的 ListenerWrappersRaw []json.RawMessage
  • 然后通过 ctx.LoadModule(srv, "ListenerWrappersRaw") 实例化 ListenerWrapper
  • ctx.LoadModule 时,如果发现了 tls 指令则按照配置文件顺序排序 ListenerWrapper 切片,否则将 tls 这个特殊的 ListenerWrapper 放在首位;这意味着在配置中不写 tls 时,所有 ListenerWrapper 永远处于 TLS 握手之后
  • 最后在 App 启动时按照切片顺序应用 ListenerWrapper,需要注意的是 ListenerWrapper 接口针对的是 net.Listener 的处理,其底层是 net.Conn这意味着 ListenerWrapper 不会对 UDP(net.PacketConn) 做处理,代码中也可以看到 ListenerWrapper 并未对 HTTP3 处理

三、具体实际应用

说了半天,也分析了源码,那么最终回到问题原点: ListenerWrapper 能干什么?答案就是自定义协议,例如神奇的 caddy-trojan 插件。
caddy-trojan 插件实现了 ListenerWrapper,在 App 启动时通过源码可以看到,TLS 握手完成后原始的 TCP 链接将交由这个 ListenerWrapper 处理:
1
2
3
4
// finish wrapping listener where we left off before TLS
for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
ln = srv.listenerWrappers[i].WrapListener(ln)
}
GO
该插件对 WrapListener 方法的实现如下:
1
2
3
4
5
6
7
// WrapListener implements caddy.ListenWrapper
func (m *ListenerWrapper) WrapListener(l net.Listener) net.Listener {
ln := NewListener(l, m.upstream, m.logger)
// 异步后台捕获新链接
go ln.loop()
return ln
}
GO
所以这个 wrapper 核心处理在 loop() 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// loop is ...
func (l *Listener) loop() {
for {
conn, err := l.Listener.Accept()
if err != nil {
select {
case <-l.closed:
return
default:
l.logger.Error(fmt.Sprintf("accept net.Conn error: %v", err))
}
continue
}

go func(c net.Conn, lg *zap.Logger, up *Upstream) {
b := make([]byte, HeaderLen+2)
if _, err := io.ReadFull(c, b); err != nil {
if errors.Is(err, io.EOF) {
lg.Error(fmt.Sprintf("read prefix error: read tcp %v -> %v: read: %v", c.RemoteAddr(), c.LocalAddr(), err))
} else {
lg.Error(fmt.Sprintf("read prefix error: %v", err))
}
c.Close()
return
}

// check the net.Conn
if ok := up.Validate(ByteSliceToString(b[:HeaderLen])); !ok {
select {
case <-l.closed:
c.Close()
default:
l.conns <- &rawConn{Conn: c, r: bytes.NewReader(b)}
}
return
}
defer c.Close()
lg.Info(fmt.Sprintf("handle trojan net.Conn from %v", c.RemoteAddr()))

nr, nw, err := Handle(io.Reader(c), io.Writer(c))
if err != nil {
lg.Error(fmt.Sprintf("handle net.Conn error: %v", err))
}
up.Consume(ByteSliceToString(b[:HeaderLen]), nr, nw)
}(conn, l.logger, l.upstream)
}
}
GO
可以看到,当新链接进入时,首先对包头做检测 if ok := up.Validate(ByteSliceToString(b[:HeaderLen]));如果检测通过那么这个链接就完全插件自己处理后续逻辑了;如果不通过则将此链接返回给 Caddy2,让 Caddy2 继续处理。
这里面涉及到一个一开始让我不解的问题: “链接不可重复读”,后来看源码才明白作者处理方式很简单: 包装一个 rawConn,在验证部分由于已经读了一点数据,如果验证不通过就把它存起来,然后让下一个读操作先读这个 buffer,从而实现原始数据组装。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// rawConn is ...
type rawConn struct {
net.Conn
r *bytes.Reader
}

// Read is ...
func (c *rawConn) Read(b []byte) (int, error) {
if c.r == nil {
return c.Conn.Read(b)
}
n, err := c.r.Read(b)
if errors.Is(err, io.EOF) {
c.r = nil
return n, nil
}
return n, err
}
GO

四、思考和总结

ListenerWrapper 是 Caddy2 一个强大的扩展能力,在 ListenerWrapper 基础上我们可以实现对 TCP 链接自定义处理,我们因此可以创造一些奇奇怪怪的协议。同时我们通过让链接重新交由 Caddy2 处理又能做到完美的伪装: 当你去尝试访问时,如果密码学验证不通过,那么后续行为就与标准 Caddy2 表现一致,主动探测基本无效。对任何自己创造的 ListenerWrapper 来说,如果开启了类似 AEAD 这种加密,探测行为本身就会被转接到对抗密码学原理上。
 
from https://archive.is/WV6kK#selection-317.0-1609.119