Support v2 Caddyfile (#79)
* Fix probe_resistance config parsing * Support Caddyfile * Support HTTP/3 * Revert "Support HTTP/3" This reverts commit f01c163ad5145cdb0a0756345a259310b2cd50aa. * Fix review comments * Update README.md to new directive names * Use Caddy 2 logger
This commit is contained in:
parent
03a7df4bf7
commit
8d6f47b4c4
2 changed files with 206 additions and 23 deletions
39
README.md
39
README.md
|
|
@ -14,31 +14,36 @@ For a complete list of features and their usage, see Caddyfile syntax:
|
|||
|
||||
## Caddyfile Syntax (Server Configuration)
|
||||
|
||||
The simplest way to enable the forward proxy without authentication just include the `forwardproxy` directive in your Caddyfile. However, this allows anyone to use your server as a proxy, which might not be desirable.
|
||||
The simplest way to enable the forward proxy without authentication just include the `forward_proxy` directive in your Caddyfile. However, this allows anyone to use your server as a proxy, which might not be desirable.
|
||||
|
||||
Open a block for more control; here's an example of all properties in use (note that the syntax is subject to change):
|
||||
The `forward_proxy` directive has no default order and must be used within a `route` directive to explicitly specify its order of evaluation. In the Caddyfile the addresses must start with `:443` for the `forward_proxy` to work for proxy requests of all origins.
|
||||
|
||||
Here's an example of all properties in use (note that the syntax is subject to change):
|
||||
|
||||
```
|
||||
forwardproxy {
|
||||
basicauth user1 0NtCL2JPJBgPPMmlPcJ
|
||||
basicauth user2 密码
|
||||
:443, example.com
|
||||
route {
|
||||
forward_proxy {
|
||||
basic_auth user1 0NtCL2JPJBgPPMmlPcJ
|
||||
basic_auth user2 密码
|
||||
ports 80 443
|
||||
hide_ip
|
||||
hide_via
|
||||
probe_resistance secret-link-kWWL9Q.com # alternatively you can use a real domain, such as caddyserver.com
|
||||
serve_pac /secret-proxy.pac
|
||||
response_timeout 30
|
||||
dial_timeout 30
|
||||
upstream https://user:password@extra-upstream-hop.com
|
||||
acl {
|
||||
allow *.caddyserver.com
|
||||
deny 192.168.1.1/32 192.168.0.0/16 *.prohibitedsite.com *.localhost
|
||||
allow ::1/128 8.8.8.8 github.com *.github.io
|
||||
allowfile /path/to/whitelist.txt
|
||||
denyfile /path/to/blacklist.txt
|
||||
allow_file /path/to/whitelist.txt
|
||||
deny_file /path/to/blacklist.txt
|
||||
allow all
|
||||
deny all # unreachable rule, remaining requests are matched by `allow all` above
|
||||
}
|
||||
}
|
||||
file_server
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -46,16 +51,16 @@ forwardproxy {
|
|||
|
||||
##### Security
|
||||
|
||||
- **basicauth [user] [password]**
|
||||
Sets basic HTTP auth credentials. This property may be repeated multiple times. Note that this is different from Caddy's built-in `basicauth` directive. BE SURE TO CHECK THE NAME OF THE SITE THAT IS REQUESTING CREDENTIALS BEFORE YOU ENTER THEM.
|
||||
- **basic_auth [user] [password]**
|
||||
Sets basic HTTP auth credentials. This property may be repeated multiple times. Note that this is different from Caddy's built-in `basic_auth` directive. BE SURE TO CHECK THE NAME OF THE SITE THAT IS REQUESTING CREDENTIALS BEFORE YOU ENTER THEM.
|
||||
_Default: no authentication required._
|
||||
|
||||
- **probe_resistance [secretlink.tld]**
|
||||
Attempts to hide the fact that the site is a forward proxy.
|
||||
Proxy will no longer respond with "407 Proxy Authentication Required" if credentials are incorrect or absent,
|
||||
and will attempt to mimic a generic Caddy web server as if the forward proxy is not enabled.
|
||||
Probing resistance works (and makes sense) only if basicauth is set up.
|
||||
To use your proxy with probe resistance, supply your basicauth credentials to your client configuration.
|
||||
Probing resistance works (and makes sense) only if `basic_auth` is set up.
|
||||
To use your proxy with probe resistance, supply your `basic_auth` credentials to your client configuration.
|
||||
If your proxy client(browser, operating system, browser extension, etc)
|
||||
allows you to preconfigure credentials, and sends credentials preemptively, you do not need secret link.
|
||||
If your proxy client does not preemptively send credentials, you will have to visit your secret link in your browser to trigger the authentication.
|
||||
|
|
@ -91,9 +96,9 @@ The hostname in each forwardproxy request will be resolved to an IP address,
|
|||
and caddy will check the IP address and hostname against the directives in order until a directive matches the request.
|
||||
acl_directive may be:
|
||||
- **allow [ip or subnet or hostname] [ip or subnet or hostname]...**
|
||||
- **allowfile /path/to/whitelist.txt**
|
||||
- **allow_file /path/to/whitelist.txt**
|
||||
- **deny [ip or subnet or hostname] [ip or subnet or hostname]...**
|
||||
- **denyfile /path/to/blacklist.txt**
|
||||
- **deny_file /path/to/blacklist.txt**
|
||||
|
||||
If you don't want unmatched requests to be subject to the default policy, you could finish
|
||||
your acl rules with one of the following to specify action on unmatched requests:
|
||||
|
|
@ -105,7 +110,7 @@ acl_directive may be:
|
|||
Note that hostname rules, matched early in the chain, will override later IP rules,
|
||||
so it is advised to put IP rules first, unless domains are highly trusted and should override the
|
||||
IP rules. Also note that domain-based blacklists are easily circumventable by directly specifying the IP.
|
||||
For `allowfile`/`denyfile` directives, syntax is the same, and each entry must be separated by newline.
|
||||
For `allow_file`/`deny_file` directives, syntax is the same, and each entry must be separated by newline.
|
||||
This policy applies to all requests except requests to the proxy's own domain and port.
|
||||
Whitelisting/blacklisting of ports on per-host/IP basis is not supported.
|
||||
_Default policy:_
|
||||
|
|
@ -117,10 +122,6 @@ _Default deny rules intend to prohibit access to localhost and local networks an
|
|||
|
||||
##### Timeouts
|
||||
|
||||
- **response_timeout [integer]**
|
||||
Sets timeout (in seconds) to get full response for HTTP requests made by proxy on behalf of users (does not affect `CONNECT`-method requests).
|
||||
_Default: no timeout._
|
||||
|
||||
- **dial_timeout [integer]**
|
||||
Sets timeout (in seconds) for establishing TCP connection to target website. Affects all requests.
|
||||
_Default: 20 seconds._
|
||||
|
|
|
|||
190
forwardproxy.go
190
forwardproxy.go
|
|
@ -11,23 +11,27 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/forwardproxy/httpclient"
|
||||
"golang.org/x/net/proxy"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(Handler{})
|
||||
httpcaddyfile.RegisterHandlerDirective("forward_proxy", parseCaddyfile)
|
||||
}
|
||||
|
||||
type ProbeResistance struct {
|
||||
|
|
@ -35,6 +39,8 @@ type ProbeResistance struct {
|
|||
}
|
||||
|
||||
type Handler struct {
|
||||
logger *zap.Logger
|
||||
|
||||
PACPath string `json:"pac_path,omitempty"`
|
||||
|
||||
HideIP bool `json:"hide_ip,omitempty"`
|
||||
|
|
@ -46,7 +52,7 @@ type Handler struct {
|
|||
// port string // port on which chain with forwardproxy is listening on
|
||||
Hosts caddyhttp.MatchHost `json:"hosts,omitempty"`
|
||||
|
||||
ProbeResistance *ProbeResistance
|
||||
ProbeResistance *ProbeResistance `json:"probe_resistance,omitempty"`
|
||||
|
||||
DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` // for initial tcp connection
|
||||
|
||||
|
|
@ -80,6 +86,8 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
|
|||
|
||||
// Provision ensures that h is set up properly before use.
|
||||
func (h *Handler) Provision(ctx caddy.Context) error {
|
||||
h.logger = ctx.Logger(h)
|
||||
|
||||
if h.DialTimeout <= 0 {
|
||||
h.DialTimeout = caddy.Duration(30 * time.Second)
|
||||
}
|
||||
|
|
@ -130,7 +138,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
|
|||
return fmt.Errorf("probe resistance requires authentication")
|
||||
}
|
||||
if len(h.ProbeResistance.Domain) > 0 {
|
||||
log.Printf("Secret domain used to connect to proxy: %s\n", h.ProbeResistance.Domain)
|
||||
h.logger.Info("Secret domain used to connect to proxy: " + h.ProbeResistance.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +175,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
|
|||
if isLocalhost(h.upstream.Hostname()) && h.upstream.Scheme == "https" {
|
||||
// disabling verification helps with testing the package and setups
|
||||
// either way, it's impossible to have a legit TLS certificate for "127.0.0.1" - TODO: not true anymore
|
||||
log.Println("Localhost upstream detected, disabling verification of TLS certificate")
|
||||
h.logger.Info("Localhost upstream detected, disabling verification of TLS certificate")
|
||||
d.DialTLS = func(network string, address string) (net.Conn, string, error) {
|
||||
conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
|
|
@ -695,8 +703,182 @@ type dialContexter interface {
|
|||
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
if !d.Next() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
args := d.RemainingArgs()
|
||||
if len(args) > 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
subdirective := d.Val()
|
||||
args := d.RemainingArgs()
|
||||
switch subdirective {
|
||||
case "basic_auth":
|
||||
if len(args) != 2 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if len(args[0]) == 0 {
|
||||
return d.Err("empty usernames are not allowed")
|
||||
}
|
||||
// TODO: Evaluate policy of allowing empty passwords.
|
||||
if strings.Contains(args[0], ":") {
|
||||
return d.Err("character ':' in usernames is not allowed")
|
||||
}
|
||||
// TODO: Support multiple basicauths.
|
||||
if h.BasicauthUser != "" || h.BasicauthPass != "" {
|
||||
return d.Err("Multi-user basicauth is not supported")
|
||||
}
|
||||
h.BasicauthUser = args[0]
|
||||
h.BasicauthPass = args[1]
|
||||
case "ports":
|
||||
if len(args) == 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if len(h.WhitelistedPorts) != 0 {
|
||||
return d.Err("ports subdirective specified twice")
|
||||
}
|
||||
h.WhitelistedPorts = make([]int, len(args))
|
||||
for i, p := range args {
|
||||
intPort, err := strconv.Atoi(p)
|
||||
if intPort <= 0 || intPort > 65535 || err != nil {
|
||||
return d.Err("ports are expected to be space-separated" +
|
||||
" and in 0-65535 range. Got: " + p)
|
||||
}
|
||||
h.WhitelistedPorts[i] = intPort
|
||||
}
|
||||
case "hide_ip":
|
||||
if len(args) != 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
h.HideIP = true
|
||||
case "hide_via":
|
||||
if len(args) != 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
h.HideVia = true
|
||||
case "probe_resistance":
|
||||
if len(args) > 1 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if len(args) == 1 {
|
||||
lowercaseArg := strings.ToLower(args[0])
|
||||
if lowercaseArg != args[0] {
|
||||
h.logger.Warn("Secret domain appears to have uppercase letters in it, which are not visitable")
|
||||
}
|
||||
h.ProbeResistance = &ProbeResistance{Domain: args[0]}
|
||||
} else {
|
||||
h.ProbeResistance = &ProbeResistance{}
|
||||
}
|
||||
case "serve_pac":
|
||||
if len(args) > 1 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if len(h.PACPath) != 0 {
|
||||
return d.Err("serve_pac subdirective specified twice")
|
||||
}
|
||||
if len(args) == 1 {
|
||||
h.PACPath = args[0]
|
||||
if !strings.HasPrefix(h.PACPath, "/") {
|
||||
h.PACPath = "/" + h.PACPath
|
||||
}
|
||||
} else {
|
||||
h.PACPath = "/proxy.pac"
|
||||
}
|
||||
h.logger.Info("Proxy Auto-Config will be served at " + h.PACPath)
|
||||
case "dial_timeout":
|
||||
if len(args) != 1 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
timeout, err := caddy.ParseDuration(args[0])
|
||||
if err != nil {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if timeout < 0 {
|
||||
return d.Err("dial_timeout cannot be negative.")
|
||||
}
|
||||
h.DialTimeout = caddy.Duration(timeout)
|
||||
case "upstream":
|
||||
if len(args) != 1 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if h.Upstream != "" {
|
||||
return d.Err("upstream directive specified more than once")
|
||||
}
|
||||
h.Upstream = args[0]
|
||||
case "acl":
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
aclDirective := d.Val()
|
||||
args := d.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
var ruleSubjects []string
|
||||
var err error
|
||||
aclAllow := false
|
||||
switch aclDirective {
|
||||
case "allow":
|
||||
ruleSubjects = args[:]
|
||||
aclAllow = true
|
||||
case "allow_file":
|
||||
if len(args) != 1 {
|
||||
return d.Err("allowfile accepts a single filename argument")
|
||||
}
|
||||
ruleSubjects, err = readLinesFromFile(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aclAllow = true
|
||||
case "deny":
|
||||
ruleSubjects = args[:]
|
||||
case "deny_file":
|
||||
if len(args) != 1 {
|
||||
return d.Err("denyfile accepts a single filename argument")
|
||||
}
|
||||
ruleSubjects, err = readLinesFromFile(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return d.Err("expected acl directive: allow/allowfile/deny/denyfile." +
|
||||
"got: " + aclDirective)
|
||||
}
|
||||
ar := ACLRule{Subjects: ruleSubjects, Allow: aclAllow}
|
||||
h.ACL = append(h.ACL, ar)
|
||||
}
|
||||
default:
|
||||
return d.ArgErr()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readLinesFromFile(filename string) ([]string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var hostnames []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
hostnames = append(hostnames, scanner.Text())
|
||||
}
|
||||
|
||||
return hostnames, scanner.Err()
|
||||
}
|
||||
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
var fp Handler
|
||||
err := fp.UnmarshalCaddyfile(h.Dispenser)
|
||||
return &fp, err
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*Handler)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
|
||||
_ caddyfile.Unmarshaler = (*Handler)(nil)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue