c246a1d4fa
Knock previously accepted a single hostname. Now it accepts multiple comma-separated hostnames; probe resistance is bypassed if the request hostname matches any entry in the list. Matching is case-insensitive and whitespace around entries is trimmed.
177 lines
5.4 KiB
Go
177 lines
5.4 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/auth"
|
|
"github.com/go-gost/core/logger"
|
|
xnet "github.com/go-gost/x/internal/net"
|
|
)
|
|
|
|
// authenticate verifies the client's Proxy-Authorization header against the
|
|
// configured Authenticator. When authentication fails and probe resistance
|
|
// is configured, a decoy response is returned to hide the proxy.
|
|
//
|
|
// Five probe resistance strategies are supported:
|
|
//
|
|
// - "code": respond with a custom HTTP status code (e.g. "code:404").
|
|
// - "web": fetch a URL and replay its response as the decoy.
|
|
// - "host": forward the raw request to a decoy host and relay the reply.
|
|
// - "file": serve a local file with Content-Type: text/html.
|
|
// - knock: when pr.Knock is set (comma-separated hostnames), probe resistance
|
|
// only activates if the request hostname matches NONE of the knock addresses.
|
|
// Clients that know any knock hostname get the normal 407 Proxy-Auth-Required.
|
|
//
|
|
// On success it returns the authenticated client ID. On failure it writes
|
|
// the response (407 or probe-resistance decoy) to conn and returns ok=false.
|
|
func (h *httpHandler) authenticate(ctx context.Context, conn net.Conn, req *http.Request, resp *http.Response, log logger.Logger) (id string, ok bool) {
|
|
u, p, _ := basicProxyAuth(req.Header.Get("Proxy-Authorization"))
|
|
if h.options.Auther == nil {
|
|
return "", true
|
|
}
|
|
if id, ok = h.options.Auther.Authenticate(ctx, u, p, auth.WithService(h.options.Service)); ok {
|
|
return
|
|
}
|
|
|
|
pr := h.md.probeResistance
|
|
// Probe resistance activates on auth failure when:
|
|
// - pr.Knock is empty (always hide the proxy), OR
|
|
// - pr.Knock is set but the request hostname doesn't match (the client
|
|
// didn't "knock" on the right host — hide the proxy).
|
|
// When pr.Knock matches the hostname, probe resistance is bypassed and
|
|
// the normal 407 Proxy-Auth-Required is returned, revealing the proxy
|
|
// only to clients that know the knock address.
|
|
if pr != nil && (pr.Knock == "" || !knockMatch(req.URL.Hostname(), pr.Knock)) {
|
|
resp.StatusCode = http.StatusServiceUnavailable
|
|
|
|
switch pr.Type {
|
|
case "code":
|
|
if code, err := strconv.Atoi(pr.Value); err == nil {
|
|
resp.StatusCode = code
|
|
} else {
|
|
log.Warnf("invalid probe resistance code: %s", pr.Value)
|
|
}
|
|
case "web":
|
|
url := pr.Value
|
|
if !strings.HasPrefix(url, "http") {
|
|
url = "http://" + url
|
|
}
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
r, err := client.Get(url)
|
|
if err != nil {
|
|
log.Error(err)
|
|
break
|
|
}
|
|
// Replace resp content with the fetched web response.
|
|
// Body is closed after writing in the caller.
|
|
*resp = *r
|
|
case "host":
|
|
// Dial the decoy host and transparently relay the request/response.
|
|
cc, err := net.Dial("tcp", pr.Value)
|
|
if err != nil {
|
|
log.Error(err)
|
|
break
|
|
}
|
|
defer cc.Close()
|
|
|
|
req.Write(cc)
|
|
xnet.Pipe(ctx, conn, cc, xnet.WithReadTimeout(h.md.idleTimeout))
|
|
return "", false
|
|
case "file":
|
|
f, _ := os.Open(pr.Value)
|
|
if f != nil {
|
|
defer f.Close()
|
|
|
|
resp.StatusCode = http.StatusOK
|
|
if finfo, _ := f.Stat(); finfo != nil {
|
|
resp.ContentLength = finfo.Size()
|
|
}
|
|
resp.Header.Set("Content-Type", "text/html")
|
|
resp.Body = f
|
|
}
|
|
}
|
|
}
|
|
|
|
if resp.Header == nil {
|
|
resp.Header = http.Header{}
|
|
}
|
|
if resp.StatusCode == 0 {
|
|
// Normal 407 Proxy-Auth-Required response.
|
|
realm := defaultRealm
|
|
if h.md.authBasicRealm != "" {
|
|
realm = h.md.authBasicRealm
|
|
}
|
|
resp.StatusCode = http.StatusProxyAuthRequired
|
|
resp.Header.Add("Proxy-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm))
|
|
if strings.ToLower(req.Header.Get("Proxy-Connection")) == "keep-alive" {
|
|
// libcurl will keep sending auth requests on the same
|
|
// connection, which we don't support yet. Force close.
|
|
resp.Header.Set("Connection", "close")
|
|
resp.Header.Set("Proxy-Connection", "close")
|
|
}
|
|
|
|
log.Debug("proxy authentication required")
|
|
} else {
|
|
// Probe resistance sent a non-407 status. For 200 OK file/web
|
|
// responses, advertise keep-alive to appear like a normal server.
|
|
if resp.StatusCode == http.StatusOK {
|
|
resp.Header.Set("Connection", "keep-alive")
|
|
}
|
|
}
|
|
|
|
if log.IsLevelEnabled(logger.TraceLevel) {
|
|
dump, _ := httputil.DumpResponse(resp, false)
|
|
log.Trace(string(dump))
|
|
}
|
|
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
if err := resp.Write(conn); err != nil {
|
|
log.Error("write auth response: ", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// checkRateLimit verifies that the remote address has not exceeded the
|
|
// configured connection rate limit. It extracts the host portion of addr
|
|
// and looks up the per-host limiter. Returns true if the connection is
|
|
// allowed or if no rate limiter is configured.
|
|
func (h *httpHandler) checkRateLimit(addr net.Addr) bool {
|
|
if h.options.RateLimiter == nil {
|
|
return true
|
|
}
|
|
if addr == nil {
|
|
return true
|
|
}
|
|
host, _, _ := net.SplitHostPort(addr.String())
|
|
if limiter := h.options.RateLimiter.Limiter(host); limiter != nil {
|
|
return limiter.Allow(1)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// knockMatch reports whether hostname matches any entry in the
|
|
// comma-separated knock list. Matching is case-insensitive. An empty
|
|
// knock string returns false (no match).
|
|
func knockMatch(hostname, knock string) bool {
|
|
if knock == "" {
|
|
return false
|
|
}
|
|
for _, h := range strings.Split(knock, ",") {
|
|
if strings.EqualFold(hostname, strings.TrimSpace(h)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|