feat: support SRV record (#3686)
diff --git a/apisix/core/dns/client.lua b/apisix/core/dns/client.lua
index 8ace4b6..2b28542 100644
--- a/apisix/core/dns/client.lua
+++ b/apisix/core/dns/client.lua
@@ -18,8 +18,10 @@
local log = require("apisix.core.log")
local json = require("apisix.core.json")
local table = require("apisix.core.table")
+local insert_tab = table.insert
local math_random = math.random
local package_loaded = package.loaded
+local ipairs = ipairs
local setmetatable = setmetatable
@@ -29,6 +31,67 @@
}
+local function gcd(a, b)
+ if b == 0 then
+ return a
+ end
+
+ return gcd(b, a % b)
+end
+
+
+local function resolve_srv(client, answers)
+ if #answers == 0 then
+ return nil, "empty SRV record"
+ end
+
+ local resolved_answers = {}
+ local answer_to_count = {}
+ for _, answer in ipairs(answers) do
+ if answer.type ~= client.TYPE_SRV then
+ return nil, "mess SRV with other record"
+ end
+
+ local resolved, err = client.resolve(answer.target)
+ if not resolved then
+ local msg = "failed to resolve SRV record " .. answer.target .. ": " .. err
+ return nil, msg
+ end
+
+ log.info("dns resolve SRV ", answer.target, ", result: ",
+ json.delay_encode(resolved))
+
+ local weight = answer.weight
+ if weight == 0 then
+ weight = 1
+ end
+
+ local count = #resolved
+ answer_to_count[answer] = count
+ -- one target may have multiple resolved results
+ for _, res in ipairs(resolved) do
+ local copy = table.deepcopy(res)
+ copy.weight = weight / count
+ copy.port = answer.port
+ insert_tab(resolved_answers, copy)
+ end
+ end
+
+ -- find the least common multiple of the counts
+ local lcm = answer_to_count[answers[1]]
+ for i = 2, #answers do
+ local count = answer_to_count[answers[i]]
+ lcm = count * lcm / gcd(count, lcm)
+ end
+ -- fix the weight as the weight should be integer
+ for _, res in ipairs(resolved_answers) do
+ res.weight = res.weight * lcm
+ end
+
+ return resolved_answers
+end
+
+
function _M.resolve(self, domain, selector)
local client = self.client
@@ -45,6 +108,11 @@
if selector == _M.RETURN_ALL then
log.info("dns resolve ", domain, ", result: ", json.delay_encode(answers))
+ for _, answer in ipairs(answers) do
+ if answer.type == client.TYPE_SRV then
+ return resolve_srv(client, answers)
+ end
+ end
return table.deepcopy(answers)
end
diff --git a/apisix/discovery/dns.lua b/apisix/discovery/dns.lua
index d254db2..64ffe1d 100644
--- a/apisix/discovery/dns.lua
+++ b/apisix/discovery/dns.lua
@@ -52,7 +52,7 @@
local nodes = core.table.new(#records, 0)
for i, r in ipairs(records) do
if r.address then
- nodes[i] = {host = r.address, weight = 1, port = port}
+ nodes[i] = {host = r.address, weight = r.weight or 1, port = r.port or port}
end
end
@@ -74,7 +74,7 @@
hosts = {},
resolvConf = {},
nameservers = servers,
- order = {"last", "A", "AAAA", "CNAME"}, -- avoid querying SRV (we don't support it yet)
+ order = {"last", "A", "AAAA", "SRV", "CNAME"},
}
local client, err = core.dns_client.new(opts)
diff --git a/docs/en/latest/dns.md b/docs/en/latest/dns.md
index 7d074ba..909b688 100644
--- a/docs/en/latest/dns.md
+++ b/docs/en/latest/dns.md
@@ -22,6 +22,7 @@
-->
* [service discovery via DNS](#service-discovery-via-dns)
+ * [SRV record](#src-record)
## service discovery via DNS
@@ -56,17 +57,16 @@
{
"id": 1,
"type": "roundrobin",
- "nodes": {
+ "nodes": [
{"host": "1.1.1.1", "weight": 1},
{"host": "1.1.1.2", "weight": 1}
- }
+ ]
}
```
Note that all the IPs from `test.consul.service` share the same weight.
If a service has both A and AAAA records, A record is preferred.
-Currently we support A / AAAA records, SRV has not been supported yet.
If you want to specify the port for the upstream server, you can add it to the `service_name`:
@@ -78,3 +78,60 @@
"type": "roundrobin"
}
```
+
+Another way to do it is via the SRV record, see below.
+
+### SRV record
+
+By using SRV record you can specify the port and the weight of a service.
+
+Assumed you have the SRV record like this:
+
+```
+; under the section of blah.service
+A 300 IN A 1.1.1.1
+B 300 IN A 1.1.1.2
+B 300 IN A 1.1.1.3
+srv 86400 IN SRV 10 60 1980 A
+srv 86400 IN SRV 10 20 1981 B
+```
+
+Upstream configuration like:
+
+```json
+{
+ "id": 1,
+ "discovery_type": "dns",
+ "service_name": "srv.blah.service",
+ "type": "roundrobin"
+}
+```
+
+is the same as:
+
+```json
+{
+ "id": 1,
+ "type": "roundrobin",
+ "nodes": [
+ {"host": "1.1.1.1", "port": 1980, "weight": 60},
+ {"host": "1.1.1.2", "port": 1981, "weight": 10},
+ {"host": "1.1.1.3", "port": 1981, "weight": 10}
+ ]
+}
+```
+
+Note that two records of domain B split the weight evenly.
+
+As for 0 weight SRV record, the [RFC 2782](https://www.ietf.org/rfc/rfc2782.txt) says:
+
+> Domain administrators SHOULD use Weight 0 when there isn't any server
+selection to do, to make the RR easier to read for humans (less
+noisy). In the presence of records containing weights greater
+than 0, records with weight 0 should have a very small chance of
+being selected.
+
+We treat weight 0 record has a weight of 1 so the node "have a very small chance of
+being selected", which is also the common way to treat this type of record.
+
+TODO: support priority.
diff --git a/t/coredns/db.test.local b/t/coredns/db.test.local
index fa2fb64..e4bb2fa 100644
--- a/t/coredns/db.test.local
+++ b/t/coredns/db.test.local
@@ -21,3 +21,24 @@
ttl 300 IN A 127.0.0.1
ttl.1s 1 IN A 127.0.0.1
+
+; SRV
+A IN A 127.0.0.1
+B IN A 127.0.0.2
+C IN A 127.0.0.3
+C IN A 127.0.0.4
+; RFC 2782 style
+_sip._tcp.srv 86400 IN SRV 10 60 1980 A
+_sip._tcp.srv 86400 IN SRV 10 20 1980 B
+; standard style
+srv 86400 IN SRV 10 60 1980 A
+srv 86400 IN SRV 10 20 1980 B
+
+port.srv 86400 IN SRV 10 60 1980 A
+port.srv 86400 IN SRV 10 20 1981 B
+
+zero-weight.srv 86400 IN SRV 10 60 1980 A
+zero-weight.srv 86400 IN SRV 10 0 1980 B
+
+split-weight.srv 86400 IN SRV 10 100 1980 A
+split-weight.srv 86400 IN SRV 10 0 1980 C
diff --git a/t/discovery/dns/sanity.t b/t/discovery/dns/sanity.t
index 3ab4b20..89b67a1 100644
--- a/t/discovery/dns/sanity.t
+++ b/t/discovery/dns/sanity.t
@@ -186,3 +186,83 @@
--- error_log
invalid dns discovery configuration
--- error_code: 500
+
+
+
+=== TEST 8: SRV
+--- apisix_yaml
+upstreams:
+ - service_name: "srv.test.local"
+ discovery_type: dns
+ type: roundrobin
+ id: 1
+--- grep_error_log eval
+qr/upstream nodes: \{[^}]+\}/
+--- grep_error_log_out eval
+qr/upstream nodes: \{("127.0.0.1:1980":60,"127.0.0.2:1980":20|"127.0.0.2:1980":20,"127.0.0.1:1980":60)\}/
+--- response_body
+hello world
+
+
+
+=== TEST 9: SRV (RFC 2782 style)
+--- apisix_yaml
+upstreams:
+ - service_name: "_sip._tcp.srv.test.local"
+ discovery_type: dns
+ type: roundrobin
+ id: 1
+--- grep_error_log eval
+qr/upstream nodes: \{[^}]+\}/
+--- grep_error_log_out eval
+qr/upstream nodes: \{("127.0.0.1:1980":60,"127.0.0.2:1980":20|"127.0.0.2:1980":20,"127.0.0.1:1980":60)\}/
+--- response_body
+hello world
+
+
+
+=== TEST 10: SRV (different port)
+--- apisix_yaml
+upstreams:
+ - service_name: "port.srv.test.local"
+ discovery_type: dns
+ type: roundrobin
+ id: 1
+--- grep_error_log eval
+qr/upstream nodes: \{[^}]+\}/
+--- grep_error_log_out eval
+qr/upstream nodes: \{("127.0.0.1:1980":60,"127.0.0.2:1981":20|"127.0.0.2:1981":20,"127.0.0.1:1980":60)\}/
+--- response_body
+hello world
+
+
+
+=== TEST 11: SRV (zero weight)
+--- apisix_yaml
+upstreams:
+ - service_name: "zero-weight.srv.test.local"
+ discovery_type: dns
+ type: roundrobin
+ id: 1
+--- grep_error_log eval
+qr/upstream nodes: \{[^}]+\}/
+--- grep_error_log_out eval
+qr/upstream nodes: \{("127.0.0.1:1980":60,"127.0.0.2:1980":1|"127.0.0.2:1980":1,"127.0.0.1:1980":60)\}/
+--- response_body
+hello world
+
+
+
+=== TEST 12: SRV (split weight)
+--- apisix_yaml
+upstreams:
+ - service_name: "split-weight.srv.test.local"
+ discovery_type: dns
+ type: roundrobin
+ id: 1
+--- grep_error_log eval
+qr/upstream nodes: \{[^}]+\}/
+--- grep_error_log_out eval
+qr/upstream nodes: \{(,?"127.0.0.(1:1980":200|3:1980":1|4:1980":1)){3}\}/
+--- response_body
+hello world