Skip to content

Configuration

Ying Kanyang (Harry Ying) edited this page Sep 21, 2022 · 4 revisions

Overview

The configuration of dcompass mainly consists of the upstreams definition and the script source code, and it can be written in either YAML (recommended for readability) or JSON.

Here is an example config, and you can get a taste of what it looks like:

---
verbosity: "info"
address: 0.0.0.0:2053
script: |
  pub async fn route(upstreams, inited, ctx, query) {
    // A few constants are predefined:
    // - query: the incoming query received
    // - ctx: the query context, e.g. client IP
    // - inited: the value returned by init()
    // - upstreams: the upstreams API
    if query.first_question?.qtype.to_str() == "AAAA" {
      return blackhole(query);
    }
    let resp = upstreams.send_default("domestic", query).await?;
    for ans in resp.answer? {
      match ans.rtype.to_str() {
        "A" if !inited.geoip.0.contains(ans.to_a()?.ip, "CN") => { return upstreams.send_default("secure", query).await; }
        "AAAA" if !inited.geoip.0.contains(ans.to_aaaa()?.ip, "CN") => { return upstreams.send_default("secure", query).await; }
        _ => continue,
      }
    }
    Ok(resp)
  }
  pub async fn init() {
    Ok(#{"geoip": Utils::GeoIp(GeoIp::create_default()?)})
  }
upstreams:
  114DNS:
    udp:
      addr: 114.114.114.114:53

  Ali:
    udp:
      addr: 223.6.6.6:53

  domestic:
    hybrid:
      - 114DNS
      - Ali

  cloudflare:
    https:
      uri: https://cloudflare-dns.com/dns-query
      ratelimit: 3000
      addr: 1.0.0.1

  quad9:
    https:
      uri: https://quad9.net/dns-query
      ratelimit: 3000
      addr: 9.9.9.9

  secure:
    hybrid:
      - cloudflare
      - quad9

Upstreams section

The upstreams section is composed of several upstreams. For example, in the above config, this is an upstream:

Ali: # name of the upstream
  udp: # transport type
    addr: 223.6.6.6:53 # transport details

Every upstream follows the same structure.

dcompass currently supports the following DNS transports:

  • UDP (udp)
  • DNS over TLS (tls)
  • DNS over HTTPS (https)

And DNS over TCP will be supported in the future.

Transport details

These transport types have a few common options:

  • ratelimit (OPTIONAL): limit the number of queries the upstream will process per second. If the throughput exceeds the rate limit specified, excess queries will be dropped and returned as SERVFAIL. This is a good measure to avoid unnecessary remote QoS if you know the remote server has a rate limit. (default: unlimited)
  • max_pool_size (OPTIONAL): specify the internal connection pool size. dcompass maintains a connection pool for each upstream to reuse connections and reduce delay. Appropriately increasing the pool size may improve throughput. However, this will also increase the time needed to replace stale connections if the network changes (e.g. network interfaces change from Wi-Fi to LTE.) (default: 46)
  • timeout (OPTIONAL): return SERVFAIL if the timeout is reached when waiting upstream to respond, specified in second. (default: 5)

In addition, each transport type has different options and details to be set.

UDP

  • addr (REQUIRED): the address of the remote server in IP address and port. For example, 1.1.1.1:53.

TLS

  • domain (REQUIRED): The domain of the DoH server. e.g. cloudflare-dns.com
  • addr (REQUIRED): The address of the server. e.g. 1.1.1.1:853 for Cloudflare DNS.
  • reuse_timeout (OPTIONAL): The time in millisecond to keep the underlying persistent TCP connection open for reuse (default: 60000)
  • max_reuse (OPTIONAL): The maximum number of queries to send over a single underlying TCP connection before we renew it. (default: 200)
  • SNI (OPTIONAL): whether to send Server Name Indication (SNI) when establishing TLS connection with the remote server (default: false)

HTTPS

  • uri (REQUIRED): The URL of the DoH server. e.g. https://cloudflare-dns.com/dns-query
  • addr (REQUIRED): The IP address of the server. e.g. 1.1.1.1 for Cloudflare DNS.
  • proxy (OPTIONAL): The Proxy URL used to connect the upstream server. Supports HTTP and SOCKS5 proxy formats like socks5://[user:[passwd]]@[ip:[port]]
  • SNI (OPTIONAL): whether to send Server Name Indication (SNI) when establishing TLS connection with the remote server (default: false)

In addition to these transports, there is also a special transport type hybrid. The hybrid transport concurrently query the underlying upstreams specified.

For example, the secure upstream is a hybrid upstream defined as:

  secure:
    hybrid:
      - cloudflare
      - quad9

It will concurrently query both upstreams named cloudflare and quad9. And yes! you can nest hybrid upstreams and dcompass is smart enough to figure out recursion in your definition.

Script section

The script section allows you completely customize your workflow for every incoming query with no compromise on performance in rune scripting language. The rune language is basically like Rust with erased types.

There are few important predefined types that you may work with in the script:

  • Message: a DNS message. It has a lot of fields and associated functions that you can use to modify (almost) everything on it.
  • Upstreams: an interface to interact with the upstreams that you defined. You can use it to send queries to your upstream.
  • QueryContext: the context of the incoming query. For example, it includes the IP address of the client.
  • Objects: a map with string as key and Utils as value.

And also Result and async functions are extensively used:

  • Every time a function or a field returns a Result<T>, where T is any type, you should use ? operator to "unwrap" it. You shall see it in action later.
  • If the function you call is async, you should use .await to get the result. For example, you should call async fn foo() by foo().await instead of foo() only.

dcompass calls on functions you define to route your query. Specifically, it

  1. calls pub async fn init() -> Result<Objects> to initialize your utils. If init() doesn't exist, it skips this part and use an empty Objects
  2. calls pub async fn route(upstreams: Upstreams, inited: Objects, ctx: QueryContext, query: Message) -> Result<Message> to route your message

The reason for initializing your utils is to avoid repeatedly loading databases or other resources. You should initialize all your utils in init at once.

Utils

There are mainly three util types:

  • GeoIp: A Geo IP database that helps you identify the IP's geographical origin.
  • Domain: A fast domain list that matches domain.
  • IpCidr: A fast IP CIDR list that matches IP address. In addition to that, we also have a function pub fn blackhole(query: Message) -> Result<Message> which generates a response that will inhibit the client from requesting again. It's often used to stop a QTYPE.

And each util has many functions that help you to create and utilize them.

GeoIp

  • pub fn create_default() -> Result<SealedGeoIp>: create a GeoIp matcher from the builtin database. If the build doesn't contain a builtin database, it will error.
  • pub async fn from_path(path: &str) -> Result<SealedGeoIp>: create a GeoIp matcher from the database at the path given.
  • pub fn contains(geoip: &SealedGeoIp, ip: &IpAddr, country_code: &str) -> bool: returns whether the given IP is from the country with the given country code.

To create a GeoIP matcher, typically you go with:

pub async fn init() {
  // Create other matchers...
  let geoip = GeoIp::create_default()?;
  // Or use specify a path
  let geoip = GeoIp::from_path("/path/to/database").await?;
  Ok(#{ "geoip": Utils::GeoIp(geoip) })
}

Domain

  • pub fn new() -> Domain
  • pub async fn add_file(domain: mut Domain, path: &str) -> Result<Domain>
  • pub fn add_qname(domain: mut Domain, qname: Dname) -> Result<Domain>
  • pub fn seal(domain: Domain) -> SealedDomain
  • pub fn contains(domain: &SealedDomain, addr: IpAddr) -> bool
Clone this wiki locally