Add built-in DNS

e.g.
```console
$ dig -p 10053 +tcp example.com @127.0.42.100
```

Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
This commit is contained in:
Akihiro Suda
2020-11-11 15:19:53 +09:00
parent 32e33a1e3d
commit 1c534039b0
10 changed files with 292 additions and 2 deletions

1
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/hashicorp/go-multierror v1.0.0
github.com/mattn/go-isatty v0.0.12
github.com/miekg/dns v1.1.35
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0
github.com/urfave/cli/v2 v2.3.0

6
go.sum
View File

@@ -180,6 +180,8 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs=
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -268,6 +270,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -303,6 +306,7 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
@@ -336,6 +340,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -376,6 +381,7 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201021000207-d49c4edd7d96/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -26,8 +26,10 @@ import (
"strings"
"sync"
"github.com/miekg/dns"
"github.com/norouter/norouter/pkg/agent/bicopy"
"github.com/norouter/norouter/pkg/agent/bicopy/bicopyutil"
agentdns "github.com/norouter/norouter/pkg/agent/dns"
"github.com/norouter/norouter/pkg/agent/etchosts"
agenthttp "github.com/norouter/norouter/pkg/agent/http"
"github.com/norouter/norouter/pkg/agent/loopback"
@@ -179,6 +181,10 @@ func (a *Agent) configure(args *jsonmsg.ConfigureRequestArgs) error {
}
}
if err := a.configureDNS(); err != nil {
return err
}
a.router, err = router.New(a.config.Routes)
if err != nil {
return err
@@ -214,6 +220,39 @@ func (a *Agent) configure(args *jsonmsg.ConfigureRequestArgs) error {
return nil
}
func (a *Agent) configureDNS() error {
var dnsSrv *dns.Server
for _, f := range a.config.NameServers {
if f.IP.Equal(a.config.Me) {
if f.Proto != "tcp" {
return errors.Errorf("expected \"tcp\", got %q as the built-in DNS port", f.Proto)
}
logrus.Debugf("dns virtual TCP port=%d", f.Port)
if dnsSrv != nil {
return errors.New("duplicated DNS?")
}
var err error
dnsSrv, err = agentdns.New(a.stack, a.config.Me, int(f.Port), a.config.HostnameMap)
if err != nil {
return err
}
}
if !a.config.Loopback.Disable {
if err := loopback.GoOther(a.stack, f.IPPortProto); err != nil {
return err
}
}
}
if dnsSrv != nil {
go func() {
if e := dnsSrv.ActivateAndServe(); e != nil {
panic(e)
}
}()
}
return nil
}
func (a *Agent) configureHTTP() error {
logrus.Debugf("http listen=%q", a.config.HTTP.Listen)
l, err := net.Listen("tcp", a.config.HTTP.Listen)

191
pkg/agent/dns/dns.go Normal file
View File

@@ -0,0 +1,191 @@
/*
Copyright (C) NoRouter authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dns
import (
"bufio"
"bytes"
"fmt"
"net"
"os/exec"
"runtime"
"strings"
"github.com/miekg/dns"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
func New(st *stack.Stack, vip net.IP, tcpPort int, hostnameMap map[string]net.IP) (*dns.Server, error) {
h, err := NewHandler(hostnameMap)
if err != nil {
return nil, err
}
fullAddr := tcpip.FullAddress{
Addr: tcpip.Address(vip),
Port: uint16(tcpPort),
}
l, err := gonet.ListenTCP(st, fullAddr, ipv4.ProtocolNumber)
if err != nil {
return nil, errors.Wrapf(err, "failed to listen on %q", fullAddr)
}
srv := &dns.Server{
Handler: h,
Listener: l,
}
return srv, nil
}
func NewClientConfig() (*dns.ClientConfig, error) {
if runtime.GOOS == "windows" {
return newClientConfigWindows()
}
return dns.ClientConfigFromFile("/etc/resolv.conf")
}
func newClientConfigWindows() (*dns.ClientConfig, error) {
powershell, err := exec.LookPath("powershell.exe")
if err != nil {
return nil, err
}
args := []string{"-NoProfile", "-NonInteractive", "Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object -ExpandProperty ServerAddresses"}
logrus.Debugf("executing %v", append([]string{powershell}, args...))
out, err := exec.Command(powershell, args...).Output()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(bytes.NewReader(out))
var ips []net.IP
for scanner.Scan() {
line := scanner.Text()
ip := net.ParseIP(line)
if ip == nil {
logrus.Warnf("unexpected line from Powershell output: %q", line)
continue
}
ips = append(ips, ip)
}
if err := scanner.Err(); err != nil {
return nil, errors.Wrap(err, "failed to parse Powershell output")
}
if len(ips) == 0 {
return nil, errors.New("no DNS found")
}
return NewStaticClientConfig(ips)
}
func NewStaticClientConfig(ips []net.IP) (*dns.ClientConfig, error) {
s := ``
for _, ip := range ips {
s += fmt.Sprintf("nameserver %s\n", ip.String())
}
r := strings.NewReader(s)
return dns.ClientConfigFromReader(r)
}
func NewHandler(hostnameMap map[string]net.IP) (dns.Handler, error) {
cc, err := NewClientConfig()
if err != nil {
fallbackIPs := []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("1.1.1.1")}
logrus.WithError(err).Warnf("failed to detect system DNS, falling back to %v", fallbackIPs)
cc, err = NewStaticClientConfig(fallbackIPs)
if err != nil {
return nil, err
}
}
clients := []*dns.Client{
&dns.Client{}, // UDP
&dns.Client{Net: "tcp"},
}
canonMap := make(map[string]net.IP)
for vague, ip := range hostnameMap {
canon := dns.CanonicalName(vague)
canonMap[canon] = ip
}
h := &Handler{
clientConfig: cc,
clients: clients,
canonMap: canonMap,
}
return h, nil
}
type Handler struct {
clientConfig *dns.ClientConfig
clients []*dns.Client
canonMap map[string]net.IP
}
func (h *Handler) handleQuery(w dns.ResponseWriter, req *dns.Msg) {
var (
reply dns.Msg
handled bool
)
reply.SetReply(req)
for _, q := range reply.Question {
canon := dns.CanonicalName(q.Name)
switch q.Qtype {
case dns.TypeA:
if ip, ok := h.canonMap[canon]; ok {
a := &dns.A{
Hdr: dns.RR_Header{
Name: q.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
},
A: ip,
}
reply.Answer = append(reply.Answer, a)
handled = true
}
}
}
if handled {
w.WriteMsg(&reply)
return
}
h.handleDefault(w, req)
}
func (h *Handler) handleDefault(w dns.ResponseWriter, req *dns.Msg) {
for _, client := range h.clients {
for _, srv := range h.clientConfig.Servers {
addr := fmt.Sprintf("%s:%s", srv, h.clientConfig.Port)
reply, _, err := client.Exchange(req, addr)
if err == nil {
w.WriteMsg(reply)
return
}
}
}
var reply dns.Msg
reply.SetReply(req)
w.WriteMsg(&reply)
}
func (h *Handler) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
switch req.Opcode {
case dns.OpcodeQuery:
h.handleQuery(w, req)
default:
h.handleDefault(w, req)
}
}

View File

@@ -0,0 +1,24 @@
/*
Copyright (C) NoRouter authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package builtinports
const (
// DNSTCP is the default TCP port number of the built-in DNS.
// The port number was chosen so that it can be associated with a loopback device without the root privileges.
// Note that resolv.conf does not support specifying non-53 port.
DNSTCP = 10053
)

View File

@@ -94,6 +94,7 @@ func NewCmdClient(ctx context.Context, hostname string, pm *parsed.ParsedManifes
configRequestArgs.StateDir.Disable = h.StateDir.Disable
configRequestArgs.WriteEtcHosts = h.WriteEtcHosts
configRequestArgs.Routes = pm.Routes
configRequestArgs.NameServers = pm.NameServers
configRequestArgsB, err := json.Marshal(configRequestArgs)
if err != nil {
return nil, err

View File

@@ -218,6 +218,11 @@ func (r *Manager) validateAgentFeatures(vip string, data jsonmsg.ConfigureResult
vip, version.FeatureRoutes)
}
}
if _, ok := fm[version.FeatureDNS]; !ok {
// not a critical error
logrus.Warnf("%s lacks feature %q, built-in DNS will be disabled",
vip, version.FeatureDNS)
}
return nil
}

View File

@@ -22,6 +22,7 @@ import (
"strings"
"github.com/google/shlex"
"github.com/norouter/norouter/pkg/builtinports"
"github.com/norouter/norouter/pkg/manager/manifest"
"github.com/norouter/norouter/pkg/stream/jsonmsg"
"github.com/pkg/errors"
@@ -32,6 +33,7 @@ type ParsedManifest struct {
Hosts map[string]*Host
PublicHostPorts []*jsonmsg.IPPortProto
Routes []jsonmsg.Route
NameServers []jsonmsg.NameServer
}
type Host struct {
@@ -178,6 +180,18 @@ func New(raw *manifest.Manifest) (*ParsedManifest, error) {
}
pm.Routes = append(pm.Routes, *route)
}
// TODO: support specifying custom DNS ports via YAML
for _, h := range pm.Hosts {
ns := jsonmsg.NameServer{
IPPortProto: jsonmsg.IPPortProto{
IP: h.VIP,
Port: builtinports.DNSTCP,
Proto: "tcp",
},
}
pm.NameServers = append(pm.NameServers, ns)
}
return pm, nil
}

View File

@@ -38,7 +38,9 @@ type ConfigureRequestArgs struct {
Loopback Loopback `json:"loopback,omitempty"`
StateDir StateDir `json:"stateDir,omitempty"`
WriteEtcHosts bool `json:"writeEtcHosts,omitempty"`
Routes []Route `json:"routes,omitempty"`
// Fields added in v0.5.0
Routes []Route `json:"routes,omitempty"`
NameServers []NameServer `json:"nameServers,omitempty"`
}
type ConfigureResultData struct {
@@ -46,6 +48,7 @@ type ConfigureResultData struct {
Version string `json:"version,omitempty"`
}
// Forward uses snake_case rather than camelCase by accident :(
type Forward struct {
// listenIP is "me"
ListenPort uint16 `json:"listen_port"`
@@ -84,3 +87,8 @@ type Route struct {
To []string `json:"to"`
Via net.IP `json:"via"`
}
// NameServer represents a built-in virtual DNS
type NameServer struct {
IPPortProto
}

View File

@@ -31,8 +31,9 @@ const (
FeatureEtcHosts = "etchosts" // Writing /etc/hosts when possible
// Features introduced in v0.5.0:
FeatureRoutes = "routes" // Drawing packets into a specific host. Only meaningful for HTTP and SOCKS proxy modes.
FeatureDNS = "dns" // Built-in DNS (10053/tcp)
// Features introduced in vX.Y.Z:
// ...
)
var Features = []Feature{FeatureLoopback, FeatureTCP, FeatureHTTP, FeatureLoopbackDisable, FeatureSOCKS, FeatureHostAliases, FeatureEtcHosts, FeatureRoutes}
var Features = []Feature{FeatureLoopback, FeatureTCP, FeatureHTTP, FeatureLoopbackDisable, FeatureSOCKS, FeatureHostAliases, FeatureEtcHosts, FeatureRoutes, FeatureDNS}