package dnsforward import ( "crypto/tls" "errors" "fmt" "path" "strings" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/lucas-clemente/quic-go" ) // ValidateClientID returns an error if clientID is not a valid client ID. func ValidateClientID(clientID string) (err error) { err = aghnet.ValidateDomainNameLabel(clientID) if err != nil { // Replace the domain name label wrapper with our own. return fmt.Errorf("invalid client id %q: %w", clientID, errors.Unwrap(err)) } return nil } // clientIDFromClientServerName extracts and validates a client ID. hostSrvName // is the server name of the host. cliSrvName is the server name as sent by the // client. When strict is true, and client and host server name don't match, // clientIDFromClientServerName will return an error. func clientIDFromClientServerName(hostSrvName, cliSrvName string, strict bool) (clientID string, err error) { if hostSrvName == cliSrvName { return "", nil } if !strings.HasSuffix(cliSrvName, hostSrvName) { if !strict { return "", nil } return "", fmt.Errorf("client server name %q doesn't match host server name %q", cliSrvName, hostSrvName) } clientID = cliSrvName[:len(cliSrvName)-len(hostSrvName)-1] err = ValidateClientID(clientID) if err != nil { // Don't wrap the error, because it's informative enough as is. return "", err } return clientID, nil } // processClientIDHTTPS extracts the client's ID from the path of the // client's DNS-over-HTTPS request. func processClientIDHTTPS(ctx *dnsContext) (rc resultCode) { pctx := ctx.proxyCtx r := pctx.HTTPRequest if r == nil { ctx.err = fmt.Errorf("proxy ctx http request of proto %s is nil", pctx.Proto) return resultCodeError } origPath := r.URL.Path parts := strings.Split(path.Clean(origPath), "/") if parts[0] == "" { parts = parts[1:] } if len(parts) == 0 || parts[0] != "dns-query" { ctx.err = fmt.Errorf("client id check: invalid path %q", origPath) return resultCodeError } clientID := "" switch len(parts) { case 1: // Just /dns-query, no client ID. return resultCodeSuccess case 2: clientID = parts[1] default: ctx.err = fmt.Errorf("client id check: invalid path %q: extra parts", origPath) return resultCodeError } err := ValidateClientID(clientID) if err != nil { ctx.err = fmt.Errorf("client id check: %w", err) return resultCodeError } ctx.clientID = clientID return resultCodeSuccess } // tlsConn is a narrow interface for *tls.Conn to simplify testing. type tlsConn interface { ConnectionState() (cs tls.ConnectionState) } // quicSession is a narrow interface for quic.Session to simplify testing. type quicSession interface { ConnectionState() (cs quic.ConnectionState) } // processClientID extracts the client's ID from the server name of the client's // DOT or DOQ request or the path of the client's DOH. func processClientID(dctx *dnsContext) (rc resultCode) { pctx := dctx.proxyCtx proto := pctx.Proto if proto == proxy.ProtoHTTPS { return processClientIDHTTPS(dctx) } else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC { return resultCodeSuccess } srvConf := dctx.srv.conf hostSrvName := srvConf.TLSConfig.ServerName if hostSrvName == "" { return resultCodeSuccess } cliSrvName := "" if proto == proxy.ProtoTLS { conn := pctx.Conn tc, ok := conn.(tlsConn) if !ok { dctx.err = fmt.Errorf("proxy ctx conn of proto %s is %T, want *tls.Conn", proto, conn) return resultCodeError } cliSrvName = tc.ConnectionState().ServerName } else if proto == proxy.ProtoQUIC { qs, ok := pctx.QUICSession.(quicSession) if !ok { dctx.err = fmt.Errorf("proxy ctx quic session of proto %s is %T, want quic.Session", proto, pctx.QUICSession) return resultCodeError } cliSrvName = qs.ConnectionState().TLS.ServerName } clientID, err := clientIDFromClientServerName(hostSrvName, cliSrvName, srvConf.StrictSNICheck) if err != nil { dctx.err = fmt.Errorf("client id check: %w", err) return resultCodeError } dctx.clientID = clientID return resultCodeSuccess }