diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -187,7 +187,7 @@
       same "printed page" as the copyright notice for easier
       identification within third-party archives.
 
-   Copyright 2019 Bin Jin
+   Copyright 2023 Bin Jin
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@
 * Provide PAC file for easy client side configuration (supports Chrome and Firefox).
 * Websocket redirection (compatible with [v2ray-plugin](https://github.com/shadowsocks/v2ray-plugin)).
 * Reverse proxy support (redirect requests to a fallback server).
+* DNS-over-HTTPS (DoH) support.
 * Implemented as a middleware, compatible with any Haskell Web Application built with `wai` interface.
   Defaults to fallback to a dumb application which simulate the default empty page from Apache.
 
diff --git a/hprox.cabal b/hprox.cabal
--- a/hprox.cabal
+++ b/hprox.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           hprox
-version:        0.1.2
+version:        0.2.0
 synopsis:       a lightweight HTTP proxy server, and more
 description:    Please see the README on GitHub at <https://github.com/bjin/hprox#readme>
 category:       Web
@@ -13,7 +13,7 @@
 bug-reports:    https://github.com/bjin/hprox/issues
 author:         Bin Jin
 maintainer:     bjin@ctrl-d.org
-copyright:      2019 Bin Jin
+copyright:      2023 Bin Jin
 license:        Apache-2.0
 license-file:   LICENSE
 build-type:     Simple
@@ -32,20 +32,23 @@
 executable hprox
   main-is: Main.hs
   other-modules:
+      DoH
       HProx
       Paths_hprox
+      Util
   hs-source-dirs:
       src
   ghc-options: -Wall -O2 -threaded -rtsopts -with-rtsopts=-N
   build-depends:
       async >=2.2
     , base >=4.12 && <5
-    , base64-bytestring >=1.0
+    , base64-bytestring >=1.1
     , binary >=0.8
     , bytestring >=0.10
     , case-insensitive >=1.2
     , conduit >=1.3
     , conduit-extra >=1.3
+    , dns >=4.0
     , http-client >=0.5
     , http-client-tls >=0.3.4
     , http-reverse-proxy >=0.4.0
diff --git a/src/DoH.hs b/src/DoH.hs
new file mode 100644
--- /dev/null
+++ b/src/DoH.hs
@@ -0,0 +1,66 @@
+-- SPDX-License-Identifier: Apache-2.0
+--
+-- Copyright (C) 2023 Bin Jin. All Rights Reserved.
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+module DoH (
+  createResolver,
+  dnsOverHTTPS
+) where
+
+import qualified Data.ByteString.Base64.URL as Base64
+import qualified Data.ByteString.Char8      as BS8
+import qualified Data.ByteString.Lazy       as LBS
+import           Network.DNS                (DNSHeader (..), DNSMessage (..),
+                                             Question (..), ResolvConf (..),
+                                             Resolver)
+import qualified Network.DNS                as DNS
+import qualified Network.HTTP.Types         as HT
+
+import           Network.Wai
+import           Util
+
+createResolver :: String -> (Resolver -> IO a) -> IO a
+createResolver remote handle = do
+    seed <- DNS.makeResolvSeed conf
+    DNS.withResolver seed handle
+  where
+    (h, p) = parseHostPortWithDefault 53 (BS8.pack remote)
+    info = DNS.RCHostPort (BS8.unpack h) (fromIntegral p)
+
+    conf = DNS.defaultResolvConf { resolvInfo = info }
+
+dnsOverHTTPS :: Resolver -> Middleware
+dnsOverHTTPS resolver fallback req respond
+    | pathInfo req == ["dns-query"] && isSecure req = handleDoH resolver req respond
+    | otherwise = fallback req respond
+
+handleDoH :: Resolver -> Application
+handleDoH resolver req respond
+    | requestMethod req == "GET",
+      [("dns", Just dnsStr)] <- queryString req,
+      Right dnsQuery <- Base64.decodeUnpadded dnsStr,
+      Right (DNSMessage { question = [q], header = DNSHeader {..} }) <- DNS.decode dnsQuery =
+        handleQuery identifier q
+    | requestMethod req == "POST",
+      KnownLength len <- requestBodyLength req,
+      len <= 4096 = do
+        dnsQuery <- getRequestBodyChunk req
+        case DNS.decode dnsQuery of
+            Right (DNSMessage { question = [q], header = DNSHeader {..} }) -> handleQuery identifier q
+            _ -> respond errorResp
+    | otherwise = respond errorResp
+  where
+    errorResp = responseLBS HT.status400 [("Content-Type", "text/plain")] "invalid dns-over-https request"
+
+    handleQuery ident Question{..} = do
+        resp <- DNS.lookupRaw resolver qname qtype
+        respond $ case resp of
+            Left _ -> errorResp
+            Right dnsResp@DNSMessage{header = header} ->
+                let encoded = DNS.encode (dnsResp {header = header {identifier = ident} }) in
+                    responseLBS HT.status200
+                        [("Content-Type", "application/dns-message"),
+                         ("Content-Length", BS8.pack $ show (BS8.length encoded))]
+                        (LBS.fromStrict encoded)
diff --git a/src/HProx.hs b/src/HProx.hs
--- a/src/HProx.hs
+++ b/src/HProx.hs
@@ -1,6 +1,6 @@
 -- SPDX-License-Identifier: Apache-2.0
 --
--- Copyright (C) 2019 Bin Jin. All Rights Reserved.
+-- Copyright (C) 2023 Bin Jin. All Rights Reserved.
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards   #-}
 
@@ -41,6 +41,8 @@
 import           Data.Conduit
 import           Network.Wai
 
+import           Util
+
 data ProxySettings = ProxySettings
   { proxyAuth  :: Maybe (BS.ByteString -> Bool)
   , passPrompt :: Maybe BS.ByteString
@@ -79,19 +81,6 @@
         [("Upgrade", "TLS/1.0, HTTP/1.1"), ("Connection", "Upgrade")]
         ""
 
-parseHostPort :: BS.ByteString -> Maybe (BS.ByteString, Int)
-parseHostPort hostPort = do
-    lastColon <- BS8.elemIndexEnd ':' hostPort
-    port <- BS8.readInt (BS.drop (lastColon+1) hostPort) >>= checkPort
-    return (BS.take lastColon hostPort, port)
-  where
-    checkPort (p, bs)
-        | BS.null bs && 1 <= p && p <= 65535 = Just p
-        | otherwise                          = Nothing
-
-parseHostPortWithDefault :: Int -> BS.ByteString -> (BS.ByteString, Int)
-parseHostPortWithDefault defaultPort hostPort =
-    fromMaybe (hostPort, defaultPort) $ parseHostPort hostPort
 
 isProxyHeader :: HT.HeaderName -> Bool
 isProxyHeader k = "proxy" `BS.isPrefixOf` CI.foldedCase k
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -1,6 +1,6 @@
 -- SPDX-License-Identifier: Apache-2.0
 --
--- Copyright (C) 2019 Bin Jin. All Rights Reserved.
+-- Copyright (C) 2023 Bin Jin. All Rights Reserved.
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards   #-}
 
@@ -12,7 +12,8 @@
 import           Network.TLS                 as TLS
 import           Network.Wai.Handler.Warp    (HostPreference, defaultSettings,
                                               runSettings, setBeforeMainLoop,
-                                              setHost, setNoParsePath, setPort,
+                                              setHost, setNoParsePath,
+                                              setOnException, setPort,
                                               setServerName)
 import           Network.Wai.Handler.WarpTLS (OnInsecure (..), onInsecure,
                                               runTLS, tlsServerHooks,
@@ -25,6 +26,7 @@
 import           Data.Version                (showVersion)
 import           Options.Applicative
 
+import           DoH
 import           HProx                       (ProxySettings (..), dumbApp,
                                               forceSSL, httpProxy, reverseProxy)
 import           Paths_hprox                 (version)
@@ -37,6 +39,7 @@
   , _auth :: Maybe FilePath
   , _ws   :: Maybe String
   , _rev  :: Maybe String
+  , _doh  :: Maybe String
   }
 
 data CertFile = CertFile
@@ -70,6 +73,7 @@
                 <*> auth
                 <*> ws
                 <*> rev
+                <*> doh
 
     bind = optional $ fromString <$> strOption
         ( long "bind"
@@ -111,7 +115,12 @@
        <> metavar "remote-host:port"
        <> help "remote host for reverse proxy (port 443 indicates HTTPS remote server)")
 
+    doh = optional $ strOption
+        ( long "doh"
+       <> metavar "dns-server:port"
+       <> help "enable DNS-over-HTTPS(DoH) support (53 will be used if port is not specified)")
 
+
 setuid :: String -> IO ()
 setuid user = getUserEntryForName user >>= setUserID . userID
 
@@ -126,7 +135,10 @@
         (primaryHost, primaryCert) = head certfiles
         otherCerts = tail $ zip (map fst certfiles) certs
 
-        settings = setNoParsePath True $
+        settings = setHost (fromMaybe "*6" _bind) $
+                   setPort _port $
+                   setOnException (\_ _ -> return ()) $
+                   setNoParsePath True $
                    setServerName "Apache" $
                    maybe id (setBeforeMainLoop . setuid) _user
                    defaultSettings
@@ -135,12 +147,11 @@
         hooks = (tlsServerHooks tlsset') { onServerNameIndication = onSNI }
         tlsset = tlsset' { tlsServerHooks = hooks, onInsecure = AllowInsecure }
 
-        failSNI = fail "SNI" >> return mempty
-        onSNI Nothing = failSNI
+        onSNI Nothing = fail "SNI: unspecified"
         onSNI (Just host)
           | host == primaryHost = return mempty
           | otherwise           = case lookup host otherCerts of
-              Nothing   -> failSNI
+              Nothing   -> fail ("SNI: unknown hostname (" ++ show host ++ ")")
               Just cert -> return (TLS.Credentials [cert])
 
         runner | isSSL     = runTLS tlsset
@@ -153,6 +164,7 @@
 
     let pset = ProxySettings pauth Nothing (BS8.pack <$> _ws) (BS8.pack <$> _rev)
         proxy = (if isSSL then forceSSL pset else id) $ gzip def $ httpProxy pset manager $ reverseProxy pset manager dumbApp
-        port = _port
 
-    runner (setHost (fromMaybe "*6" _bind) $ setPort port settings) proxy
+    case _doh of
+        Nothing  -> runner settings proxy
+        Just doh -> createResolver doh (\resolver -> runner settings (dnsOverHTTPS resolver proxy))
diff --git a/src/Util.hs b/src/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Util.hs
@@ -0,0 +1,25 @@
+-- SPDX-License-Identifier: Apache-2.0
+--
+-- Copyright (C) 2023 Bin Jin. All Rights Reserved.
+module Util (
+    parseHostPort,
+    parseHostPortWithDefault
+) where
+
+import qualified Data.ByteString       as BS
+import qualified Data.ByteString.Char8 as BS8
+import           Data.Maybe            (fromMaybe)
+
+parseHostPort :: BS.ByteString -> Maybe (BS.ByteString, Int)
+parseHostPort hostPort = do
+    lastColon <- BS8.elemIndexEnd ':' hostPort
+    port <- BS8.readInt (BS.drop (lastColon+1) hostPort) >>= checkPort
+    return (BS.take lastColon hostPort, port)
+  where
+    checkPort (p, bs)
+        | BS.null bs && 1 <= p && p <= 65535 = Just p
+        | otherwise                          = Nothing
+
+parseHostPortWithDefault :: Int -> BS.ByteString -> (BS.ByteString, Int)
+parseHostPortWithDefault defaultPort hostPort =
+    fromMaybe (hostPort, defaultPort) $ parseHostPort hostPort
