From 1c534039b03284e706b22aaaee86ddcb5fd069a7 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 11 Nov 2020 15:19:53 +0900 Subject: [PATCH] Add built-in DNS e.g. ```console $ dig -p 10053 +tcp example.com @127.0.42.100 ``` Signed-off-by: Akihiro Suda --- go.mod | 1 + go.sum | 6 + pkg/agent/agent.go | 39 ++++++ pkg/agent/dns/dns.go | 191 ++++++++++++++++++++++++++ pkg/builtinports/builtinports.go | 24 ++++ pkg/manager/cmdclient.go | 1 + pkg/manager/manager.go | 5 + pkg/manager/manifest/parsed/parsed.go | 14 ++ pkg/stream/jsonmsg/configure.go | 10 +- pkg/version/features.go | 3 +- 10 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 pkg/agent/dns/dns.go create mode 100644 pkg/builtinports/builtinports.go diff --git a/go.mod b/go.mod index a2a4790..00cb18d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c310bd2..287ec1d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 6eaefab..432d84d 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -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) diff --git a/pkg/agent/dns/dns.go b/pkg/agent/dns/dns.go new file mode 100644 index 0000000..3ae8964 --- /dev/null +++ b/pkg/agent/dns/dns.go @@ -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) + } +} diff --git a/pkg/builtinports/builtinports.go b/pkg/builtinports/builtinports.go new file mode 100644 index 0000000..3de2042 --- /dev/null +++ b/pkg/builtinports/builtinports.go @@ -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 +) diff --git a/pkg/manager/cmdclient.go b/pkg/manager/cmdclient.go index 29df38e..4bd8d78 100644 --- a/pkg/manager/cmdclient.go +++ b/pkg/manager/cmdclient.go @@ -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 diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index f3a00c6..2bdc940 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -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 } diff --git a/pkg/manager/manifest/parsed/parsed.go b/pkg/manager/manifest/parsed/parsed.go index 843c555..788b8f3 100644 --- a/pkg/manager/manifest/parsed/parsed.go +++ b/pkg/manager/manifest/parsed/parsed.go @@ -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 } diff --git a/pkg/stream/jsonmsg/configure.go b/pkg/stream/jsonmsg/configure.go index 739f525..cd88a2a 100644 --- a/pkg/stream/jsonmsg/configure.go +++ b/pkg/stream/jsonmsg/configure.go @@ -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 +} diff --git a/pkg/version/features.go b/pkg/version/features.go index 7864da6..eff04c5 100644 --- a/pkg/version/features.go +++ b/pkg/version/features.go @@ -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}