Skip to content

Commit

Permalink
Support v2 Caddyfile (#79)
Browse files Browse the repository at this point in the history
* Fix probe_resistance config parsing

* Support Caddyfile

* Support HTTP/3

* Revert "Support HTTP/3"

This reverts commit f01c163.

* Fix review comments

* Update README.md to new directive names

* Use Caddy 2 logger
  • Loading branch information
klzgrad authored Oct 2, 2020
1 parent 03a7df4 commit 8d6f47b
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 23 deletions.
39 changes: 20 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,48 +14,53 @@ 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:[email protected]
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
}
```

(The square brackets `[ ]` indicate values you should replace; do not actually include the brackets.)

##### 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.
Expand Down Expand Up @@ -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:
Expand All @@ -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:_
Expand All @@ -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._
Expand Down
190 changes: 186 additions & 4 deletions forwardproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,36 @@ 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 {
Domain string `json:"domain,omitempty"`
}

type Handler struct {
logger *zap.Logger

PACPath string `json:"pac_path,omitempty"`

HideIP bool `json:"hide_ip,omitempty"`
Expand All @@ -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

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
)

0 comments on commit 8d6f47b

Please sign in to comment.