-
-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Caddyfile support #217
Caddyfile support #217
Conversation
Impressive amount of work!
Not really - the plan is to roll My main concern is that if we introduce it like this now, then we would need to ask users to migrate their config somehow later when we do roll it into mainline. But we can mark it as experimental in the README for now I guess, noting that it will likely change later. |
Can't wait for it to happen. Hope this PR will speed up the process.
So my understanding is all server blocks in a caddyfile are currently processed by ServerType.Setup() here. I can definitely prepare a similar ServerType for layer4 functionality. What I don't understand is how to make Caddy use different ServerTypes (http and layer4 ones) for the same adapter, given no same-named adapters are allowed to be registered here.
I don't mind if we continue working on it together. What you are talking about (layer4 global directive and readme changes) is only covered in steps 1/27 and 27/27. If you helped me solve the problem with ServerTypes described above, I would prepare a few more changes to replace global directive with scheme-specific server blocks. |
I want to throw away the concept of "server type" in the Caddyfile adapter. It was a misguided idea. It doesn't work in practice. That would be a purely internal change though, but reorganizing that would make it easier to make site blocks pluggable by changing whether we call down into Either way, I think we should wait until we're ready to move |
Sorry, I can see no point in waiting. I have seen the recent PR battle between #208 and #210, and I'm personally interested in @WeidiDeng and @ydylla having come to a mutually acceptable solution, because the problem they are trying to solve affects my use case (see caddyserver/caddy#6439). They seem to be busy with other things, just like @mholt. But all of these problems have nothing to do with caddyfile support which this PR is about.
Why don't we do it in parallel with fixing caddy-l4 stability? It will be easier/faster to move caddy-l4 into the mainline, if it has caddyfile support. Is there anything, other than waiting, I could help you with in order to make this PR mergeable? |
I'm hesitating about steps 8-11/27 that deal with
It's worth noting that One place where we can't fully merge what is done for |
Wow -- an incredible effort! Nice work, @vnxme. And this is one of the most organized, best-documented contributions I have ever seen. You get a gold star. 🌟 Just catching up on this thread... (Disclaimer: I have not reviewed the code change in any detail yet. Just a quick skim. I'm reading the discussion for starters.) First, I want to clarify that I'm inclined to accept this contribution, perhaps with some revisions/discussion first... I know Francis has some concerns about future plans related to moving this into the mainline repo, so I want to make sure we can all be satisfied with a big change like this. This module is experimental -- and documented as "in development" and to expect breaking changes. Here's how I view this repository: it's the perfect place to experiment and try different ways to get it right, or to learn valuable lessons, before we merge into mainline Caddy. So, in my opinion, it's OK if this isn't how it's done later when/if it becomes a standard part of Caddy. If the Caddyfile config looks one way while it's here, but then it changes when/if it gets merged into Caddy, that's OK because their entire build config will have to change anyway (they won't be compiling in a separate caddy-l4 plugin anymore). Was there much or any code that had to be copied out of Caddy to make this work, @vnxme ? Regarding
This is a great question... I think I like flatter (not nested) configs in general (nesting can get unwieldy IMO, but sometimes it can be helpful). That said, a couple thoughts to help guide the decision...
Indentation/nesting can be easier for some people to grok or more difficult for others, since it can be harder to scan. But it does add structure. So... up for debate.
The reason for this is because for the HTTP
This is a really cool feature IMO! Even if niche. I actually used something like this with a backup service I wrote years ago. The basic idea is that you can have one downstream client that is able to communicate with multiple identical upstreams simultaneously. One upload to the proxy, then it tees to multiple upstreams, and their outputs all get aggregated to the client. This requires special handling by the client and the servers have to be in the same state (meaning, they have to be in a state where one will error and the others don't, for example, to avoid breaking your business logic) -- but it is quite useful in some cases. I think we can even improve on how this situation is handled and make more things configurable, but I digress; that's a discussion for another day. Okay, now back to the top... Overall I think this is a solid first pass. Probably just about how I would do it.
All probably fine, especially for now. We can iterate later if needed. :)
Good point... the distinction is, of course, for So, yeah, maybe we should fix this. I'm down for
I do like this idea, and I would love to see something like this stubbed out, but it doesn't have to be right now. Hopefully without a Caddy v3 😄 (which we don't have plans for).
I'm OK with this approach for now, as it allows combining HTTP and layer4 together easier. The new ServerType would be a good way to implement an improved, experimental Caddyfile syntax to eventually upgrade what we currently have.
Yep, this is also a decent idea; though like I said I prefer flatter configs when possible :)
I agree we can and should keep it to 1 file.
Yep, that's what I'm thinking too; even if implementing a new ServerType is currently impractical (which is I think why @francislavoie says it is a "misguided idea"), we can still reuse most of the code here related to parsing tokens, UnmarshalCaddyfile, etc. We just would wire it up a little differently at the highest level. Anyway, I'd like to recommend that we carry this PR through to completion and merge it. It doesn't have to be the perfect implementation and it doesn't have to be "complete" (in the sense that there is nothing left to do -- it just has to work well). We can figure out how to work it into the Caddyfile more natively later; most of the code will remain, regardless. @francislavoie Is that agreeable? We can get this into a merge-worthy state, then figure out how to upgrade the Caddyfile as a whole. |
Yep 👍 To clarify what I meant, the "server types" idea was to have entirely separate Caddyfile parsing per type, but what we actually want is separate site block parsing per type. It's currently at the wrong layer/level of the Caddyfile IMO. Just needs to be moved down to the site address layer and fork the parsing based on the detected type from the site address. But site addresses still need to be able to send data up to the top level to be assembled after (logging, tls, etc) which live outside the app in question. |
@francislavoie Ok, that sounds good. I like that idea. Thanks! @vnxme Would you be okay helping us with massaging this into the new Caddyfile improvements when they are being worked on/released? Another question -- does this have some sort of default directive order, or do all connection handlers need to be inside a |
@mholt I don't think a default order is needed, unlike http. People will write the directives in the order they need. Although some of the handlers behave like middlewares in http, their purpose depends on the handlers besides them due to the diverse need of the users. |
@mholt, I appreciate your positive feedback, thank you! Code sharing
I don't think I had to copy much from Caddy mainline. However, I studied the approaches you had taken there to ensure as much consistency as possible. For example, ParseCaddyfileNestedMatcherSet() defined in layer4/caddyfile.go is implemented in almost the same way as ParseCaddyfileNestedMatcherSet() from modules/caddyhttp/matchers.go. Generally I tried to reuse the code existing in the mainline for layer4 purposes. Let me know if you find any inefficiencies. On the other hand, there is some code that I propose to move from this PR to the mainline. See caddyserver/caddy#6461 and caddyserver/caddy#6462. As mentioned above, we may also need to move the code related to Syntax of
|
What I'm also hesitating about is how strict caddyfile syntax checks must be. Examples:
This is just to reconfirm that I'm not too strict with caddyfile syntax checks. Because I'm not sure that I treat placeholders correctly this way. As noted above, I checked |
caddytls.MatchLocalIP is new, but is in 2.8.4: caddyserver/caddy@26748d0 So we might need to update go.mod here, since we're using new API surface.
You are very right, it is all over the place. Partly because I only went to lengths to make it strict based on bug reports or questions where people had the syntax wrong but no error. Sometimes it's not even harmful, but can be confusing. So generally I leave it up to user feedback to make it more strict (because I'm lazy with stuff like that) but it doesn't hurt to be more strict if it's not too complex. Don't stress to make it perfect everywhere though if it's tedious. |
@vnxme Would you like to join our developer Slack? Just let me know which email to invite. Also, after we merge this I can grant you collaborator privileges so you can continue to contribute in branches, instead of forks, if you want. |
I don't know why it was done that way in that spot. That's definitely weird. We tend to lean on the stricter side, rejecting anything not usable.
Yep 👍
Yeah ideally most things should support placeholders if possible, at runtime. Thankfully with Caddyfile syntax |
That's exactly what I missed, thanks. I used
Thanks for trust. I have little experience with Slack. If you think I might be of any help there, I don't mind. Do you have a public address? I could drop an e-mail there, so that not to publish it here.
I'm quite fine with working in forks. I think, it's more secure. Given I'm not a professional developer, what I would like most when I propose some changes is not to break anything because of my negligence.
Then, I believe, more tests would be needed to identify the places where runtime variables don't work. I suppose, key risk areas are IP addresses. I will try to investigate it myself. If anybody finds such a place before me, please don't hesitate to report here. By the way, I have flattened |
@vnxme Sure, no problem. Send me an email to the address near the top part of this page: https://caddyserver.com/sponsor Will take a closer look in the next few days (maybe after the weekend) :) |
Squashed a few fixes and rebased this PR onto master with #218 merged. |
This LGTM -- is there anything else we're waiting on before merging? |
I'd like to see some Caddyfile adapt tests if that's not too much to ask. It would serve as examples as well to make sure the adapter functions produce the expected JSON. See the Caddy main repo, there's tests like these https://github.com/caddyserver/caddy/tree/master/caddytest/integration/caddyfile_adapt called from https://github.com/caddyserver/caddy/blob/master/caddytest/integration/caddyfile_adapt_test.go I don't know how possible it is to write tests like this from a plugin package, but would be nice to have. |
including: - layer4.MatchRemoteIP - layer4.MatchLocalIP - layer4.MatchNot
- caddyconfig.JSON() interprets AuthMethods []uint8 as a list of bytes - changing type of AuthMethods to []uint16 resolves the problem
and inner structs: - caddytls.MatchLocalIP - caddytls.MatchRemoteIP - caddytls.MatchServerName
and inner structs: - caddytls.ConnectionPolicy - caddytls.CustomCertSelectionPolicy
Rebased this PR onto master with #219 merged. @francislavoie Thanks for a great idea. I've prepared at least 1 test per each handler and matcher. Hope it would be enough. By the way, I've caught a few copy-and-paste bugs, so it was definitely worth it. My suggestion for the next steps:
|
Excellent work. Thank you so much @vnxme. Many people are grateful to you for this! I'll attend to those other PRs next. |
Thank you, @vnxme, for your significant work on this PR, and @mholt and @francislavoie, for your thorough reviews on it, too. <3 |
Incredible! Was just looking into using this and dreading the JSON, much love from me. PS, the readme still says "Since this app does not support Caddyfile (yet?)" |
@louis-lau That's being fixed in #223 -- which I'm still reviewing 😅 |
Caddyfile support
No, you aren't sleeping. Yes, you are reading correctly. This PR introduces a fully fledged caddyfile support for mholt/caddy-l4 module. And you can try it right now. Please find details below.
Key features
Approach
General
layer4
global directive), or as a listener wrapper around the http application (vialayer4
directive insidelistener_wrappers
section ofservers
global directive):layer4
block contains a number of servers, and each server is essentially a separately configured application. Each server block is prepended by a list of network addresses. At least one network address must be provided.matching_timeout
option. The same structure applies tolayer4
listener wrapper andsubroute
handler blocks.Matcher sets
@
at the beginning and generally work the same as http named matchers. However, layer4 named matcher sets can't be mixed with http named matchers.route
directive can use named matcher sets that are defined after (below it within the same block of a caddyfile). For each composed route, named matcher sets are ordered in a way they are listed after the correspondingroute
directive.route
directive in the same block, it is completely ignored.Routes
route
. If multiple named matcher sets are provided afterroute
, they are ORed. Aroute
directive without matcher sets is valid, it will match any connection it gets.route
directives are provided in a caddyfile, i.e. no internal reordering occurs.proxy
handler which treats same-line options as upstream addresses. Individual handler syntax depends on the module used. See comments for caddyfile.Unmarshaler implementations for additional guidance.Disclaimer
caddy adapt
output.caddy adapt
command, i.e. no tests have been done to confirm that matchers and handlers do work. Given I haven't changed anything in the inner logic, I expect no matching or handling regressions could be traced to this PR.{$VAR}
syntax), they do work. No placeholders have been tested, but nothing should prevent them (at least,{env.*}
,{system.*}
and{time.*}
) from working correctly. However, some options do strict syntax checks (e.g. numbers, durations, IPs), so that they can't validate placeholders without additional changes.Known limitations
There are a few functionality limitations, mostly due to the differences between caddyfile and json syntaxes. They are listed below.
http
matcher doesn't support multiple inner matcher sets (nested OR logic). Use multiple named matcher sets instead:not
matcher doesn't support multiple inner matcher sets (nested OR logic). Use multiple named matcher sets instead:not
matcher, as well as any other layer4 matcher, is only allowed to be present once in a matcher set. While it's not a problem for other matchers, NOT AND NOT logic is only supported via subroutes:Testing
Given @mholt is most likely busy with some developments of higher importance, I would reasonably expect a delay before this PR is merged. Unless you are patient enough, you can test and use these changes right now.
Config examples (layer4 parts only)
Own working config from #6439
OpenConnect VPN (ocserv) multiplexing
Some configs from #101 and #102
SSH multiplexing
SSH over TLS
Some configs from #209
Prefetch
SOCKSv4 before SOCKSv5
Subroute
TLS in TLS
Listener wrapper matcher with no handler
Included changes
Below is a list of caddyfile directives (handlers, matchers, options), for which caddyfile.Unmarshaler interface or a similar function has been implemented in this PR.
Global Directives
layer4
by parseLayer4 in layer4/caddyfile.go and Server in layer4/server.goHandlers (layer4.handlers.*)
echo
by Handler in modules/l4echo/echo.goproxy
by Handler in modules/l4proxy/proxy.goproxy_protocol
by Handler in modules/l4proxyprotocol/handler.gosocks5
by Socks5Handler in modules/l4socks/socks5_handler.gosubroute
by Handler in modules/l4subroute/handler.gotee
by Handler in modules/l4tee/tee.gothrottle
by Handler in modules/l4throttle/throttle.gotls
by Handler in modules/l4tls/handler.goProxy load balancing selection policies (layer4.proxy.selection_policies.*):
first
by FirstSelection in modules/l4proxy/loadbalancing.goip_hash
by IPHashSelection in modules/l4proxy/loadbalancing.goleasn_conn
by LeastConnSelection in modules/l4proxy/loadbalancing.gorandom
by RandomSelection in modules/l4proxy/loadbalancing.gorandom_choose
by RandomChoiceSelection in modules/l4proxy/loadbalancing.goround_robin
by RoundRobinSelection in modules/l4proxy/loadbalancing.goProxy upstream inner blocks:
tls
by unmarshalCaddyfileTLSConfig in modules/l4proxy/upstream.go for reverseproxy.TLSConfigTLS inner blocks:
cert_selection
by unmarshalCaddyfileCertSelection in modules/l4tls/handler.go for caddytls.CustomCertSelectionPolicyconnection_policy
by unmarshalCaddyfileConnectionPolicy in modules/l4tls/handler.go for caddytls.ConnectionPolicyListener Wrappers (caddy.listeners.*)
layer4
by ListenerWrapper in layer4/listener.goMatchers (layer4.matchers.*)
ip
by MatchIP in layer4/matchers.golocal_ip
by MatchLocalIP in layer4/matchers.gonot
by MatchNot in layer4/matchers.gohttp
by MatchHTTP in modules/l4http/httpmatcher.gopostgres
by MatchPostgres in modules/l4postgres/matcher.gosocks4
by Socks4Matcher in modules/l4socks/socks4_matcher.gosocks5
by Socks5Matcher in modules/l4socks/socks5_matcher.gossh
by MatchSSH in modules/l4ssh/matcher.gotls
by MatchTLS in modules/l4tls/matcher.goxmpp
by MatchXMPP in modules/l4xmpp/matcher.goTLS handshake matchers (tls.handshake_match.*):
alpn
by MatchALPN in modules/l4tls/alpn_matcher.golocal_ip
by unmarshalCaddyfileMatchLocalIP in modules/l4tls/matcher.go for caddytls.MatchLocalIPremote_ip
by unmarshalCaddyfileMatchRemoteIP in modules/l4tls/matcher.go for caddytls.MatchRemoteIPsni
by unmarshalCaddyfileMatchServerName in modules/l4tls/matcher.go for caddytls.MatchServerNameProposed changes
caddyserver/caddy repo, non-breaking:
local_ip
,remote_ip
andsni
oftls
matcher don't implement caddyfile.Unmarshaler interface. Replacement functions and a special workaround are temporarily placed in modules/l4tls/matcher.go to support these matchers. They can be removed, when native implementations are merged (PR to follow).connection_policy
andcert_selection
oftls
handler don't implement caddyfile.Unmarshaler interface. Replacement functions are temporarily placed in modules/l4tls/handler.go to support these blocks. They can be removed, when native implementations are merged (PR to follow).tls
ofproxy
handler doesn't implement caddyfile.Unmarshaler interface. Replacement function is temporarily placed in modules/l4proxy/upstream.go to support this block. It can be removed, when native implementation is merged (PR to follow).mholt/caddy-l4 repo, breaking:
ip
, while HTTP and TLS remote IP matchers are calledremote_ip
. It seems confusing to me, and I suggest we rename it toremote_ip
for consistency, probably with some deprecation warnings. If agreed, a PR will follow.Discussions
There is a big thread on caddyfile support (#16) created over 3 years ago, and 100+ people have expressed their support so far. Below are my 2 cents regarding the ideas discussed: