diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -49,8 +49,7 @@
 Use `dprox --help` or [dnsmasq manpage](http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html) for further details about these options.
 But be aware that there might be minor differences on some options like `--server`.
 
-To use `dprox` with `dnsmasq-china-list`, with "8.8.4.4" as the remote DNS server
-(systemd user can also use [this unit file](https://github.com/bjin/dprox/blob/master/systemd/dprox.service)):
+To use `dprox` with `dnsmasq-china-list`, with "8.8.4.4" as the remote DNS server:
 
 ```sh
 dprox -C /etc/dnsmasq.d/accelerated-domains.china.conf -C /etc/dnsmasq.d/bogus-nxdomain.china.conf -S 8.8.4.4
@@ -60,6 +59,16 @@
 
 ```sh
 dprox -C /opt/hosts-blocklists/domains.txt -H /opt/hosts-blocklists/hostnames.txt -h
+```
+
+There is also a customized `--ipset` option (different from `dnsmasq`). If DNS response somehow matches `ipset`,
+alternative DNS server `ipset-server` will be used instead. The exact matching policy can be set by `ipset-match`.
+
+```
+--ipset <ipmask>
+--ipset-match <none|all|any|anynotmatch>
+--ipset-server <ipaddr>[#port]
+--ipset-file <file>
 ```
 
 ### Known Issue
diff --git a/dprox.cabal b/dprox.cabal
--- a/dprox.cabal
+++ b/dprox.cabal
@@ -1,21 +1,19 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.34.4.
+-- This file has been generated from package.yaml by hpack version 0.35.1.
 --
 -- see: https://github.com/sol/hpack
---
--- hash: 595c7426c43140ed95c8e5a736712c4e64ca373cca9ba084e938e04150a516cf
 
 name:           dprox
-version:        0.3.0
-synopsis:       a lightweight DNS proxy server
+version:        0.4.0
+synopsis:       a lightweight DNS proxy server, compatible with dnsmasq-china-list
 description:    Please see the README on GitHub at <https://github.com/bjin/dprox#readme>
 category:       DNS
 homepage:       https://github.com/bjin/dprox#readme
 bug-reports:    https://github.com/bjin/dprox/issues
 author:         Bin Jin
 maintainer:     bjin@ctrl-d.org
-copyright:      2019 Bin Jin
+copyright:      2023 Bin Jin
 license:        BSD3
 license-file:   LICENSE
 build-type:     Simple
@@ -38,10 +36,13 @@
       Config
       DomainRoute
       LRU
+      Log
       Paths_dprox
   hs-source-dirs:
       src
-  ghc-options: -Wall -O2 -threaded -rtsopts -with-rtsopts=-N
+  default-extensions:
+      ImportQualifiedPost
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
   build-depends:
       attoparsec >=0.13
     , base >=4.12 && <5
@@ -49,6 +50,7 @@
     , bytestring-trie >=0.2.4
     , containers >=0.6
     , dns >=3.0.4
+    , fast-logger >=3.0
     , hashable >=1.2
     , iproute >=1.7
     , network >=2.8
@@ -57,9 +59,9 @@
     , streaming-commons >=0.2
     , time >=1.8
     , unix >=2.7
+  default-language: Haskell2010
   if flag(static)
     ghc-options: -optl-static
-  default-language: Haskell2010
 
 test-suite dprox-test
   type: exitcode-stdio-1.0
@@ -69,7 +71,9 @@
   hs-source-dirs:
       src
       test
-  ghc-options: -Wall -O2 -threaded -rtsopts -with-rtsopts=-N
+  default-extensions:
+      ImportQualifiedPost
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
   build-depends:
       attoparsec >=0.13
     , base >=4.12 && <5
@@ -77,6 +81,7 @@
     , bytestring-trie >=0.2.4
     , containers >=0.6
     , dns >=3.0.4
+    , fast-logger >=3.0
     , hashable >=1.2
     , hspec
     , iproute >=1.7
@@ -86,6 +91,6 @@
     , streaming-commons >=0.2
     , time >=1.8
     , unix >=2.7
+  default-language: Haskell2010
   if flag(static)
     ghc-options: -optl-static
-  default-language: Haskell2010
diff --git a/src/Config.hs b/src/Config.hs
--- a/src/Config.hs
+++ b/src/Config.hs
@@ -3,33 +3,35 @@
 -- Copyright (C) 2019 Bin Jin. All Rights Reserved.
 {-# LANGUAGE OverloadedStrings #-}
 module Config
-( GlobalConfig(..)
-, Config(..)
-, getConfig
-, IP(..)
-, invalidIPAddress
-, PortNumber
-) where
+  ( Config (..)
+  , GlobalConfig (..)
+  , IP (..)
+  , IPMask
+  , IPSetMatch (..)
+  , PortNumber
+  , getConfig
+  , invalidIPAddress
+  ) where
 
-import           Control.Exception                (SomeException, handle)
-import           Control.Monad                    (when)
-import           Data.Attoparsec.ByteString       ((<?>))
-import qualified Data.Attoparsec.ByteString       as P
-import qualified Data.Attoparsec.ByteString.Char8 as P8
-import qualified Data.ByteString                  as BS
-import qualified Data.ByteString.Char8            as BS8
-import           Data.IP                          (IP (..))
-import           Data.Maybe                       (catMaybes, fromMaybe,
-                                                   isNothing)
-import           Data.Streaming.Network           (HostPreference)
-import           Data.String                      (fromString)
-import           Data.Version                     (showVersion)
-import qualified Network.DNS                      as DNS
-import           Network.Socket                   (PortNumber)
-import           Options.Applicative
-import           Text.Read                        (readMaybe)
+import Control.Exception                (SomeException, handle)
+import Control.Monad                    (when)
+import Data.Attoparsec.ByteString       ((<?>))
+import Data.Attoparsec.ByteString       qualified as P
+import Data.Attoparsec.ByteString.Char8 qualified as P8
+import Data.ByteString                  qualified as BS
+import Data.ByteString.Char8            qualified as BS8
+import Data.IP                          (AddrRange, IP (..), IPv4)
+import Data.Maybe                       (catMaybes, fromMaybe, isNothing)
+import Data.Streaming.Network           (HostPreference)
+import Data.String                      (fromString)
+import Data.Version                     (showVersion)
+import Network.DNS                      qualified as DNS
+import Network.Socket                   (PortNumber)
+import Options.Applicative
+import Text.Read                        (readMaybe)
 
-import           Paths_dprox                      (version)
+import Log
+import Paths_dprox
 
 data GlobalConfig = GlobalConfig
     { setUser       :: Maybe String
@@ -37,23 +39,36 @@
     , listenAddress :: Maybe HostPreference
     , cacheSize     :: Int
     , cacheTTL      :: DNS.TTL
+    , loglevel      :: LogLevel
+    , ipsetMatch    :: IPSetMatch
+    , ipsetServer   :: Maybe (IP, Maybe PortNumber)
     } deriving (Eq, Show)
 
+type IPMask = AddrRange IPv4
+
 data Config = Server (Maybe DNS.Domain) IP (Maybe PortNumber)
             | Address DNS.Domain IP
             | Hosts DNS.Domain IP
             | BogusNX IP
+            | IPSet IPMask
     deriving (Eq, Show)
 
+data IPSetMatch = NoneMatch
+                | AllMatch
+                | AnyMatch
+                | AnyNotMatch
+    deriving (Eq, Show)
+
 getConfig :: IO (GlobalConfig, [Config])
 getConfig = do
-    (globalConfigs, configFiles, hostsFiles, confs) <- execParser opts
+    (confs, globalConfigs, configFiles, hostsFiles, ipsetFiles) <- execParser opts
     confs1 <- concat <$> mapM readConfigFromFile configFiles
     confs2 <- concat <$> mapM readHostsFromFile hostsFiles
-    return (globalConfigs, confs1 ++ confs2 ++ confs)
+    confs3 <- concat <$> mapM readIPSetFromFile ipsetFiles
+    return (globalConfigs, confs1 ++ confs2 ++ confs3 ++ confs)
   where
-    opts = info ((,,,) <$> globalOption <*> configFileOption
-                       <*> hostsFilesOption <*> plainOption <**> ver <**> helper)
+    opts = info ((,,,,) <$> plainOption <*> globalOption <*> configFileOption
+                        <*> hostsFilesOption <*> ipsetFileOption <**> ver <**> helper)
       ( fullDesc <> progDesc desc )
 
     desc = "a lightweight DNS proxy server, supports a small subset of dnsmasq options"
@@ -61,6 +76,7 @@
 
     readConfigFromFile file = handle handler (parseConfigFile <$> BS.readFile file)
     readHostsFromFile file = handle handler (parseHostsFile <$> BS.readFile file)
+    readIPSetFromFile file = handle handler (parseIPSetFile <$> BS.readFile file)
 
     handler :: SomeException -> IO [Config]
     handler _ = return []
@@ -101,6 +117,18 @@
         skipLine
         return (Hosts parsedDomain parsedIP)
 
+parseIPSetFile :: BS.ByteString -> [Config]
+parseIPSetFile bs = case P.parseOnly parseFile bs of
+    Left msg -> error msg
+    Right r  -> r
+  where
+    parseFile = catMaybes <$> P.many' (parseLine parseIPSet)
+
+    parseIPSet = do
+        parsedIPSet <- ip4mask
+        skipLine
+        return (IPSet parsedIPSet)
+
 parseLine :: P.Parser a -> P.Parser (Maybe a)
 parseLine parser = do
     eof <- P.atEnd
@@ -126,18 +154,31 @@
         Nothing     -> fail ("invalid IP address: " ++ ipstr)
         Just ipaddr -> return ipaddr
 
-invalidIPAddress :: IP
-invalidIPAddress = "::"
+ip4mask :: P.Parser IPMask
+ip4mask = (<?> "IPv4 mask") $ do
+    ipstr <- BS8.unpack <$> P8.takeWhile1 (P8.inClass "0-9./")
+    case readMaybe ipstr of
+        Nothing     -> fail ("invalid IP mask: " ++ ipstr)
+        Just ipmask -> return ipmask
 
 port :: P.Parser PortNumber
 port = read . BS8.unpack <$> P.takeWhile1 P8.isDigit_w8 <?> "Port Number"
 
+ipport :: P.Parser (IP, Maybe PortNumber)
+ipport = (,) <$> ip <*> optional (P8.char '#' *> port)
+
+invalidIPAddress :: IP
+invalidIPAddress = "::"
+
 globalOption :: Parser GlobalConfig
 globalOption = GlobalConfig <$> userOption
                             <*> portOption
                             <*> listenOption
                             <*> cacheOption
                             <*> ttlOption
+                            <*> loglevelOption
+                            <*> ipsetMatchOption
+                            <*> ipsetServerOption
   where
     userOption = optional $ strOption
         ( long "user"
@@ -169,6 +210,30 @@
        <> value 233
        <> help "Cache TTL in seconds (default value: 233)")
 
+    loglevelOption = option (maybeReader logLevelReader)
+        ( long "log-level"
+       <> metavar "<trace|debug|info|warn|error|none>"
+       <> value INFO
+       <> help "specify the logging level (default: info)")
+
+    ipsetMatchOption = option (maybeReader ipsetMatchReader)
+        ( long "ipset-match"
+       <> metavar "<none|all|any|anynotmatch>"
+       <> value AnyMatch
+       <> help ("matching policy for --ipset (default value: any). Note that the matching procedure will " ++
+                "be performed only on DNS response with at least one IPv4 address."))
+
+    ipsetServerOption = optional $ option (attoparsecReader ipport)
+        ( long "ipset-server"
+       <> metavar "<ipaddr>[#port]"
+       <> help "DNS server to use if ipset matches DNS response")
+
+    ipsetMatchReader "none"        = Just NoneMatch
+    ipsetMatchReader "all"         = Just AllMatch
+    ipsetMatchReader "any"         = Just AnyMatch
+    ipsetMatchReader "anynotmatch" = Just AnyNotMatch
+    ipsetMatchReader _             = Nothing
+
 configFileOption :: Parser [FilePath]
 configFileOption = many $ strOption
     ( long "conf-file"
@@ -193,8 +258,14 @@
        <> short 'h'
        <> help "Don't read /etc/hosts")
 
+ipsetFileOption :: Parser [FilePath]
+ipsetFileOption = many $ strOption
+    ( long "ipset-file"
+   <> metavar "<file>"
+   <> help "Read ipset from files")
+
 plainOption :: Parser [Config]
-plainOption = (++) <$> many server <*> ((++) <$> many address <*> many bogusnx)
+plainOption = (++) <$> many server <*> ((++) <$> many address <*> ((++) <$> many bogusnx <*> many ipset))
   where
     server = option (attoparsecReader serverValue)
         ( long "server"
@@ -219,8 +290,17 @@
     bogusnx = option (attoparsecReader bogusNXValue)
         ( long "bogus-nxdomain"
        <> short 'B'
-       <> metavar "ip"
+       <> metavar "<ipaddr>"
        <> help "Transform replies which contain the IP address given into \"No such domain\" replies")
+
+    ipset = option (attoparsecReader (IPSet <$> ip4mask))
+        ( long "ipset"
+       <> metavar "<ipmask>"
+       <> help ipsetMsg)
+
+    ipsetMsg = "Add an IPv4 address to ipset. If DNS responses matches ipset, DNS responses from " ++
+               "\"--ipset-server\" will be returned instead. Exact matching policy can be controlled " ++
+               "by \"--ipset-match\". Note that this option is different from the option with the same name from dnsmasq."
 
 attoparsecReader :: P.Parser a -> ReadM a
 attoparsecReader p = eitherReader (P.parseOnly (p <* P.endOfInput) . BS8.pack)
diff --git a/src/DomainRoute.hs b/src/DomainRoute.hs
--- a/src/DomainRoute.hs
+++ b/src/DomainRoute.hs
@@ -3,17 +3,17 @@
 -- Copyright (C) 2019 Bin Jin. All Rights Reserved.
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 module DomainRoute
-( DomainRoute
-, newDomainRoute
-, getDomainRouteExact
-, getDomainRouteByPrefix
-) where
+  ( DomainRoute
+  , getDomainRouteByPrefix
+  , getDomainRouteExact
+  , newDomainRoute
+  ) where
 
-import qualified Data.ByteString.Char8 as BS
-import           Data.Char             (toLower)
-import qualified Data.Trie             as T
-import qualified Data.Trie.Convenience as T
-import qualified Network.DNS           as DNS
+import Data.ByteString.Char8 qualified as BS
+import Data.Char             (toLower)
+import Data.Trie             qualified as T
+import Data.Trie.Convenience qualified as T
+import Network.DNS           qualified as DNS
 
 newtype DomainRoute a = DomainRoute (T.Trie a)
     deriving (Eq, Show, Functor, Foldable)
diff --git a/src/LRU.hs b/src/LRU.hs
--- a/src/LRU.hs
+++ b/src/LRU.hs
@@ -4,20 +4,19 @@
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE TupleSections   #-}
 module LRU
-( LRUCache
-, newCache
-, lookupCache
-, updateCache
-, purgeCache
-) where
+  ( LRUCache
+  , lookupCache
+  , newCache
+  , purgeCache
+  , updateCache
+  ) where
 
-import qualified Data.HashPSQ           as PQ
-import           Data.Hashable          (Hashable)
-import           Data.IORef             (IORef, atomicModifyIORef', newIORef,
-                                         readIORef)
-import           Data.Int               (Int64)
-import           Data.Time.Clock.System (SystemTime (..), getSystemTime)
-import           Network.DNS            (TTL)
+import Data.Hashable          (Hashable)
+import Data.HashPSQ           qualified as PQ
+import Data.Int               (Int64)
+import Data.IORef             (IORef, atomicModifyIORef', newIORef, readIORef)
+import Data.Time.Clock.System (SystemTime (..), getSystemTime)
+import Network.DNS            (TTL)
 
 type Time = Int64
 
diff --git a/src/Log.hs b/src/Log.hs
new file mode 100644
--- /dev/null
+++ b/src/Log.hs
@@ -0,0 +1,54 @@
+-- SPDX-License-Identifier: BSD-3-Clause
+--
+-- Copyright (C) 2023 Bin Jin. All Rights Reserved.
+{-# LANGUAGE OverloadedStrings #-}
+
+module Log
+  ( LogLevel (..)
+  , LogStr
+  , LogType' (..)
+  , Logger
+  , ToLogStr (..)
+  , logLevelReader
+  , pureLogger
+  , withLogger
+  ) where
+
+import System.IO.Unsafe (unsafePerformIO)
+
+import System.Log.FastLogger
+
+data LogLevel = TRACE
+              | DEBUG
+              | INFO
+              | WARN
+              | ERROR
+              | NONE
+    deriving (Show, Eq, Ord)
+
+logLevelReader :: String -> Maybe LogLevel
+logLevelReader "trace" = Just TRACE
+logLevelReader "debug" = Just DEBUG
+logLevelReader "info"  = Just INFO
+logLevelReader "warn"  = Just WARN
+logLevelReader "error" = Just ERROR
+logLevelReader "none"  = Just NONE
+loglevelReader _       = Nothing
+
+logWith :: TimedFastLogger -> LogLevel -> LogStr -> IO ()
+logWith logger level logstr = logger (\time -> toLogStr time <> " [" <> toLogStr (show level) <> "] " <> logstr <> "\n")
+
+type Logger = LogLevel -> LogStr -> IO ()
+
+{-# NOINLINE pureLogger #-}
+pureLogger :: Logger -> LogLevel -> LogStr -> a -> a
+pureLogger logger level str a = unsafePerformIO $ logger level str >> return a
+
+withLogger :: LogType -> LogLevel -> ((LogLevel -> LogStr -> IO ()) -> IO ()) -> IO ()
+withLogger logType logLevel toRun = do
+    timeCache <- newTimeCache "%Y/%m/%d %T %Z"
+    withTimedFastLogger timeCache logType $ \timedLogger ->
+        let logger level str
+                | level < logLevel = return ()
+                | otherwise        = logWith timedLogger level str
+        in toRun logger
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -6,24 +6,29 @@
 {-# LANGUAGE TupleSections     #-}
 module Main where
 
-import           Control.Concurrent        (forkIO, threadDelay)
-import           Control.Exception         (SomeException, handle)
-import           Control.Monad             (forM, forever, join)
-import           Data.ByteString           (ByteString)
-import qualified Data.Foldable             as F
-import           Data.Hashable             (Hashable (..))
-import qualified Data.Map                  as M
-import           Data.Maybe                (fromMaybe)
-import qualified Data.Set                  as S
-import           Data.Streaming.Network    (bindPortUDP)
-import qualified Network.DNS               as DNS
-import           Network.Socket.ByteString (recvFrom, sendTo)
-import           System.Posix.User         (UserEntry (..), getUserEntryForName,
-                                            setUserID)
+import Control.Concurrent        (forkIO, threadDelay)
+import Control.Exception         (SomeException, handle)
+import Control.Monad             (forM, forever, join, void)
+import Data.ByteString           (ByteString)
+import Data.Foldable             qualified as F
+import Data.Hashable             (Hashable (..))
+import Data.IP                   qualified as IP
+import Data.IP.RouteTable        qualified as IP
+import Data.Map                  qualified as M
+import Data.Maybe                (fromMaybe, isJust, maybeToList)
+import Data.Set                  qualified as S
+import Data.Streaming.Network    (bindPortUDP)
+import Data.Version              (showVersion)
+import Network.DNS               qualified as DNS
+import Network.Socket.ByteString (recvFrom, sendTo)
+import System.Posix.User
+    (UserEntry (..), getUserEntryForName, setUserID)
 
-import           Config
-import           DomainRoute
-import           LRU
+import Config
+import DomainRoute
+import Log
+import LRU
+import Paths_dprox
 
 instance Hashable DNS.TYPE where
     hashWithSalt s = hashWithSalt s . DNS.fromTYPE
@@ -56,23 +61,24 @@
     getType DNS.RD_AAAA{}  = DNS.AAAA
     getType _              = qt
 
-processDNS :: CachedResolver -> ByteString -> IO (Either DNS.DNSError ByteString)
-processDNS resolver bs
-    | Left e <- parse = return (Left e)
-    | Right q <- parse = do
-        rrs <- mapM (processQuery resolver) (DNS.question q)
-        let hd = DNS.header DNS.defaultResponse
-            resp = DNS.defaultResponse {
-                DNS.header = hd { DNS.identifier = DNS.identifier (DNS.header q) },
-                DNS.question = DNS.question q,
-                DNS.answer = concat rrs
-            }
-        return (Right (DNS.encode resp))
-  where
-    parse = do
-        q <- DNS.decode bs
-        if DNS.qOrR (DNS.flags (DNS.header q)) == DNS.QR_Query then return q else Left DNS.FormatError
+parseDNS :: ByteString -> Either DNS.DNSError DNS.DNSMessage
+parseDNS bs = do
+    q <- DNS.decode bs
+    if DNS.qOrR (DNS.flags (DNS.header q)) == DNS.QR_Query
+        then return q
+        else Left DNS.FormatError
 
+processDNS :: CachedResolver -> DNS.DNSMessage -> IO (Either DNS.DNSError ByteString)
+processDNS resolver DNS.DNSMessage{ DNS.header = hd, DNS.question = q } = do
+    rrs <- mapM (processQuery resolver) q
+    let hd0 = DNS.header DNS.defaultResponse
+        resp = DNS.defaultResponse {
+            DNS.header = hd0 { DNS.identifier = DNS.identifier hd },
+            DNS.question = q,
+            DNS.answer = concat rrs
+        }
+    return (Right (DNS.encode resp))
+
 handleServer :: DomainRoute (Maybe Resolver) -> Resolver
 handleServer route qd qt = case resolver of
     Nothing        -> return (Left DNS.NameError)
@@ -104,6 +110,36 @@
     isBlacklisted (DNS.RD_AAAA ipv6) = IPv6 ipv6 `S.member` blacklist
     isBlacklisted _                  = False
 
+handleIPSet :: [IPMask] -> IPSetMatch -> Maybe Resolver -> LRUCache DNS.Domain () -> CachedResolver -> CachedResolver
+handleIPSet [] _ _ _ = id
+handleIPSet _ _ Nothing _ = id
+handleIPSet ipset match (Just ipsetResolver) cache = handleWithResolver
+  where
+    iproute = IP.fromList $ zip ipset $ repeat ()
+    inIPSet ip = isJust $ IP.lookup (IP.makeAddrRange ip 32) iproute
+
+    check NoneMatch   = not . any inIPSet
+    check AllMatch    = all inIPSet
+    check AnyMatch    = any inIPSet
+    check AnyNotMatch = any (not.inIPSet)
+
+    handleWithResolver resolver qd qt@DNS.A = do
+        cachedInIPSet <- isJust <$> lookupCache qd cache
+        if cachedInIPSet
+          then fmap (1,) <$> ipsetResolver qd qt
+          else do
+            res <- resolver qd qt
+            case res of
+                Left _        -> return res
+                Right (_, rs) -> do
+                    let ipv4s = [ipv4 | DNS.RD_A ipv4 <- rs]
+                    if not (null ipv4s) && check match ipv4s
+                      then do
+                        updateCache qd () cache
+                        fmap (1,) <$> ipsetResolver qd qt
+                      else return res
+    handleWithResolver resolver qd qt = resolver qd qt
+
 makeResolverCache :: Int -> DNS.TTL -> IO (Resolver -> CachedResolver)
 makeResolverCache sz ttl | sz <= 0 = return $ \r qd qt -> fmap (ttl,) <$> r qd qt
 makeResolverCache sz ttl = do
@@ -129,8 +165,9 @@
 setuid user = getUserEntryForName user >>= setUserID . userID
 
 main :: IO ()
-main = do
-    (GlobalConfig{..}, conf) <- getConfig
+main = getConfig >>= \(GlobalConfig{..}, conf) -> withLogger (LogStdout 4096) loglevel $ \logger -> do
+    logger INFO $ "dprox " <> toLogStr (showVersion version) <> " started"
+
     let defaultPort = 53
         fallbackServer = Server Nothing "8.8.8.8" Nothing
 
@@ -140,6 +177,7 @@
         address = [(domain, [ip]) | Address domain ip <- conf]
         hosts = [(domain, [ip]) | Hosts domain ip <- conf]
         bogusnx = [ip | BogusNX ip <- conf]
+        ipset = [ipmask | IPSet ipmask <- conf]
 
         serverRoute = newDomainRoute (const id) server
         serverAddressSet = S.fromList $ F.toList serverRoute
@@ -149,8 +187,10 @@
 
         bogusnxSet = S.fromList bogusnx
 
+        ipsetServerPort = fmap (fromMaybe defaultPort) <$> ipsetServer
+
         resolvConfs = [ (addr, rc)
-                      | addr@(host, port) <- S.toList serverAddressSet
+                      | addr@(host, port) <- S.toList serverAddressSet ++ maybeToList ipsetServerPort
                       , host /= invalidIPAddress
                       , let rsinfo = if port == defaultPort
                                 then DNS.RCHostName (show host)
@@ -161,27 +201,46 @@
                         }
                       ]
 
-    sock <- bindPortUDP (fromIntegral $ fromMaybe defaultPort localPort) (fromMaybe "*6" listenAddress)
+    logger INFO $ "read " <> toLogStr (length server) <> " server configs"
+    logger INFO $ "read " <> toLogStr (length hosts) <> " hosts configs"
+    logger INFO $ "read " <> toLogStr (length bogusnx) <> " bogus-nxdomain configs"
+    logger INFO $ "read " <> toLogStr (length ipset) <> " ipset configs"
+
+    let (bport, bhost) = (fromIntegral $ fromMaybe defaultPort localPort, fromMaybe "*6" listenAddress)
+    sock <- bindPortUDP bport bhost
+    logger INFO $ "bind to " <> toLogStr (show bhost) <> ":" <> toLogStr bport
+
     resolvSeeds <- forM resolvConfs $ \(k, v) -> do
+        logger INFO $ "creating resolver: " <> toLogStr (show $ fst k) <> ":" <> toLogStr (show $ snd k)
         rs <- DNS.makeResolvSeed v
         return (k, rs)
 
     F.mapM_ setuid setUser
 
     resolverCache <- makeResolverCache cacheSize cacheTTL
+    ipsetCache <- newCache 4096 maxBound
 
     let processWithResolver resolver = forever $ do
             (bs, addr) <- recvFrom sock (fromIntegral DNS.maxUdpSize)
             forkIO $ do
-                resp <- processDNS resolver bs
-                F.forM_ resp $ \resp' -> sendTo sock resp' addr
+                case parseDNS bs of
+                    Left err -> logger DEBUG $ "dns parse error: " <> toLogStr (show err) <> " from: " <> toLogStr (show addr)
+                    Right q  -> do
+                        logger TRACE $ "dns request from " <> toLogStr (show addr) <> ": " <> toLogStr (show $ DNS.question q)
+                        resp <- processDNS resolver q
+                        case resp of
+                            Right resp' -> void $ sendTo sock resp' addr
+                            Left err    -> do
+                                logger DEBUG $ "dns process error: " <> toLogStr (show err) <> " from: " <> toLogStr (show addr)
 
         createResolvers ((k,v):xs) m = DNS.withResolver v $ \rs ->
             createResolvers xs (M.insert k (DNS.lookup rs) m)
         createResolvers [] m = let serverRoute' = fmap (`M.lookup`m) serverRoute
-                                   resolver = resolverCache $
-                                              handleBogusNX bogusnxSet $
+                                   ipsetResolver = join $ fmap (`M.lookup`m) ipsetServerPort
+                                   resolver = handleIPSet ipset ipsetMatch ipsetResolver ipsetCache $
+                                              resolverCache $
                                               handleAddressAndHosts addressRoute hostsRoute $
+                                              handleBogusNX bogusnxSet $
                                               handleServer serverRoute'
                                in processWithResolver resolver
     createResolvers resolvSeeds M.empty
diff --git a/systemd/dprox.service b/systemd/dprox.service
--- a/systemd/dprox.service
+++ b/systemd/dprox.service
@@ -1,12 +1,19 @@
 [Unit]
-Description=a simple DNS proxy server
+Description=a lightweight DNS proxy server, compatible with dnsmasq-china-list
 After=network.target
 
 [Service]
 Type=simple
-CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID
-ExecStart=/usr/bin/dprox -C /etc/dnsmasq.d/accelerated-domains.china.conf -C /etc/dnsmasq.d/bogus-nxdomain.china.conf -S 8.8.4.4 -u nobody
+DynamicUser=true
+ProtectSystem=true
+ProtectHome=true
+PrivateTmp=true
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+AmbientCapabilities=CAP_NET_BIND_SERVICE
+ExecStart=/usr/bin/dprox -C /etc/dnsmasq.d/accelerated-domains.china.conf -C /etc/dnsmasq.d/bogus-nxdomain.china.conf -S 8.8.4.4
 Restart=on-failure
+RestartSec=30
+StartLimitBurst=10
 
 [Install]
 WantedBy=multi-user.target
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -4,12 +4,12 @@
 {-# LANGUAGE OverloadedStrings #-}
 module Main where
 
-import           Control.Monad         (forM_)
-import qualified Data.ByteString.Char8 as BS8
-import           Data.Char             (toUpper)
-import           Test.Hspec
+import Control.Monad         (forM_)
+import Data.ByteString.Char8 qualified as BS8
+import Data.Char             (toUpper)
+import Test.Hspec
 
-import           DomainRoute
+import DomainRoute
 
 main :: IO ()
 main = hspec $ do
