diff --git a/.ci/ci-test.sh b/.ci/ci-test.sh index 495ba74d6e..17dd61b1bd 100755 --- a/.ci/ci-test.sh +++ b/.ci/ci-test.sh @@ -64,6 +64,7 @@ t || dig -p${DNS_PORT} +dnssec PTR 168.192.in-addr.arpa @127.0.0.1 | grep -Fq 'f section t || dig -p${DNS_PORT} +dnssec darpa.mil @127.0.0.1 2>&1 | grep -Fvq 'RRSIG' || fail t || dig -p${DNS_PORT} +dnssec www.darpa.mil @127.0.0.1 2>&1 | grep -Fvq 'RRSIG' || fail +t || dig -p${DNS_PORT} A download.windowsupdate.com @127.0.0.1 | grep -Fq "NOERROR" || fail section t || dig -p${DNS_PORT} +short cloaked.com @127.0.0.1 | grep -Eq '1.1.1.1|1.0.0.1' || fail @@ -122,6 +123,7 @@ t || grep -Eq 'invalid.*SYNTH' query.log || fail t || grep -Eq '168.192.in-addr.arpa.*SYNTH' query.log || fail t || grep -Eq 'darpa.mil.*FORWARD' query.log || fail t || grep -Eq 'www.darpa.mil.*FORWARD' query.log || fail +t || grep -Eq 'download.windowsupdate.com.*FORWARD' query.log || fail t || grep -Eq 'cloaked.com.*CLOAK' query.log || fail t || grep -Eq 'www.cloaked2.com.*CLOAK' query.log || fail t || grep -Eq 'www.dnscrypt-test.*CLOAK' query.log || fail diff --git a/.ci/forwarding-rules.txt b/.ci/forwarding-rules.txt index 7a403110b6..55ad1c073b 100644 --- a/.ci/forwarding-rules.txt +++ b/.ci/forwarding-rules.txt @@ -1,2 +1,2 @@ darpa.mil 208.67.222.222 - +download.windowsupdate.com $DHCPDNS diff --git a/dnscrypt-proxy/example-forwarding-rules.txt b/dnscrypt-proxy/example-forwarding-rules.txt index 7a018242c2..c749778a25 100644 --- a/dnscrypt-proxy/example-forwarding-rules.txt +++ b/dnscrypt-proxy/example-forwarding-rules.txt @@ -4,7 +4,10 @@ ## This is used to route specific domain names to specific servers. ## The general format is: -## [:port] [, [:port]...] +## [:port] | $DHCPDNS [, [:port] | $DHCPDNS ...] +## $DHCPDNS is case sensitive, represents a DNS detected from the upstream +## router combining port 53, prefers IPv6 if `ipv6_servers`, and falls back to +## random one of `bootstrap_resolvers` if auto detection fails. ## IPv6 addresses can be specified by enclosing the address in square brackets. ## In order to enable this feature, the "forwarding_rules" property needs to diff --git a/dnscrypt-proxy/plugin_forward.go b/dnscrypt-proxy/plugin_forward.go index faec6e12f4..66d2786677 100644 --- a/dnscrypt-proxy/plugin_forward.go +++ b/dnscrypt-proxy/plugin_forward.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/jedisct1/dlog" + "github.com/lifenjoiner/dhcpdns" "github.com/miekg/dns" ) @@ -16,7 +17,9 @@ type PluginForwardEntry struct { } type PluginForward struct { - forwardMap []PluginForwardEntry + forwardMap []PluginForwardEntry + dhcpdns []*dhcpdns.Detector + dhcpdnsFallback []string } func (plugin *PluginForward) Name() string { @@ -33,6 +36,7 @@ func (plugin *PluginForward) Init(proxy *Proxy) error { if err != nil { return err } + hasVar := false for lineNo, line := range strings.Split(lines, "\n") { line = TrimAndStripInlineComments(line) if len(line) == 0 { @@ -49,7 +53,9 @@ func (plugin *PluginForward) Init(proxy *Proxy) error { var servers []string for _, server := range strings.Split(serversStr, ",") { server = strings.TrimSpace(server) - if net.ParseIP(server) != nil { + if server == "$DHCPDNS" { + hasVar = true + } else if net.ParseIP(server) != nil { server = fmt.Sprintf("%s:%d", server, 53) } servers = append(servers, server) @@ -62,6 +68,19 @@ func (plugin *PluginForward) Init(proxy *Proxy) error { servers: servers, }) } + if hasVar { + if proxy.SourceIPv6 { + d6 := &dhcpdns.Detector{RemoteIPPort: "[2001:4860:4860::8888]:80"} + go d6.Serve(9, 10) + plugin.dhcpdns = append(plugin.dhcpdns, d6) + } + if proxy.SourceIPv4 { + d4 := &dhcpdns.Detector{RemoteIPPort: "8.8.8.8:80"} + go d4.Serve(9, 10) + plugin.dhcpdns = append(plugin.dhcpdns, d4) + } + plugin.dhcpdnsFallback = proxy.xTransport.bootstrapResolvers + } return nil } @@ -92,6 +111,23 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro return nil } server := servers[rand.Intn(len(servers))] + if server == "$DHCPDNS" { + for _, dhcpdns := range plugin.dhcpdns { + n, ip, DNS, err := dhcpdns.Status() + maxFail := 9 + if err != nil && ip != "" && n > maxFail { + DNS = nil + } + if len(DNS) > 0 { + server = net.JoinHostPort(DNS[rand.Intn(len(DNS))].String(), "53") + break + } + } + if server == "$DHCPDNS" { + dlog.Noticef("$DHCPDNS han't been solved, forward to one of bootstrap_resolvers") + server = plugin.dhcpdnsFallback[rand.Intn(len(plugin.dhcpdnsFallback))] + } + } pluginsState.serverName = server client := dns.Client{Net: pluginsState.serverProto, Timeout: pluginsState.timeout} respMsg, _, err := client.Exchange(msg, server) diff --git a/go.mod b/go.mod index 187c21d0ff..c88a5ff3db 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/jedisct1/xsecretbox v0.0.0-20230811132812-b950633f9f1f github.com/k-sone/critbitgo v1.4.0 github.com/kardianos/service v1.2.2 + github.com/lifenjoiner/dhcpdns v0.0.6 github.com/miekg/dns v1.1.55 github.com/powerman/check v1.7.0 github.com/quic-go/quic-go v0.37.4 diff --git a/go.sum b/go.sum index e101c672df..6054b3300c 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/k-sone/critbitgo v1.4.0 h1:l71cTyBGeh6X5ATh6Fibgw3+rtNT80BA0uNNWgkPrb github.com/k-sone/critbitgo v1.4.0/go.mod h1:7E6pyoyADnFxlUBEKcnfS49b7SUAQGMK+OAp/UQvo0s= github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= +github.com/lifenjoiner/dhcpdns v0.0.6 h1:rn4Y5RRR5sgQ6RjWenwhA7i/uHzHW9hbZpCobA4CAJs= +github.com/lifenjoiner/dhcpdns v0.0.6/go.mod h1:BixeaGeafYzDIuDCYIUbSOdi4m+TScpzI9cZGYgzgSk= github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= diff --git a/vendor/github.com/lifenjoiner/dhcpdns/.gitignore b/vendor/github.com/lifenjoiner/dhcpdns/.gitignore new file mode 100644 index 0000000000..66fd13c903 --- /dev/null +++ b/vendor/github.com/lifenjoiner/dhcpdns/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/vendor/github.com/lifenjoiner/dhcpdns/LICENSE b/vendor/github.com/lifenjoiner/dhcpdns/LICENSE new file mode 100644 index 0000000000..8dfd662aaf --- /dev/null +++ b/vendor/github.com/lifenjoiner/dhcpdns/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 lifenjoiner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/lifenjoiner/dhcpdns/dhcpdns.go b/vendor/github.com/lifenjoiner/dhcpdns/dhcpdns.go new file mode 100644 index 0000000000..5d9a5cdddd --- /dev/null +++ b/vendor/github.com/lifenjoiner/dhcpdns/dhcpdns.go @@ -0,0 +1,503 @@ +// Copyright 2023-now by lifenjoiner. All rights reserved. +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file. + +// Package dhcpdns gets the DHCP/DHCPv6 DNS. +package dhcpdns + +import ( + "crypto/rand" + "errors" + //"log" + "net" + "runtime" + "sync" + "time" +) + +const ( + dhcpv4MessageSizeMax = 576 + dhcpv6MessageSizeComm = 1024 +) + +var ( + err4InvalidParam = errors.New("invalid DHCPv4 parameters") + err4NotReply = errors.New("not DHCPv4 reply") + err4TidNotMatch = errors.New("DHCPv4 TID not match") +) + +var ( + errInvalidReply = errors.New("invalid reply") + errNoDNSFound = errors.New("no DNS found") +) + +var ( + err6InterfaceNotRunning = errors.New("interface is not running") + err6InvalidParam = errors.New("invalid DHCPv6 parameters") + err6NoLLUAFound = errors.New("no link-local address found") + err6NotReply = errors.New("not DHCPv6 Reply") + err6TidNotMatch = errors.New("DHCPv6 TID not match") +) + +var ( + errIsLLA = errors.New("unsupported link-local address") + errIsTeredo = errors.New("unsupported Teredo Tunneling Pseudo-Interface") +) + +// Sample messages, https://wiki.wireshark.org/SampleCaptures.md + +// GetDNSFromReply4 gets DNS from a DHCP reply message. +func GetDNSFromReply4(msg []byte, tid []byte) (ip []net.IP, err error) { + n := len(msg) + + if n < 241 || len(tid) < 4 { + err = err4InvalidParam + return + } + + if msg[0] != 0x02 { + err = err4NotReply + return + } + + if msg[4] != tid[0] || msg[5] != tid[1] || msg[6] != tid[2] || msg[7] != tid[3] { + err = err4TidNotMatch + return + } + + m := 240 + for m < n { + opt := msg[m] + if opt == 255 { + // END + break + } + m++ + + if m < n { + i := m + 1 + m += 1 + int(msg[m]) + if m <= n { + if opt == 6 { + // DHCP DNS + for i+4 <= m { + ip = append(ip, msg[i:i+4]) + i += 4 + } + break + } + continue + } + } + + err = errInvalidReply + break + } + + if len(ip) == 0 { + err = errNoDNSFound + //log.Printf("%x", msg) + } + return +} + +// GetDNSByIPv4 sends DHCP message and return the DNS. +// ip is the reaching out IP. +func GetDNSByIPv4(ip string) (dns []net.IP, err error) { + ipAddr, ifi, err := getOutboundParams(ip) + if err != nil { + return nil, err + } + //log.Printf("Receiving addr Zone: %v", ipAddr.Zone) + + // Windows (WSL2) can't choose the right IP. + pc, err := reuseListenPacket("udp4", ip+":68") + if err != nil { + return nil, err + } + + // Minimal DHCP message + // We prefer to be reached by a broadcast than unicast relpy, in case of there is the OS DHCP deamon binding. + // https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol + // https://datatracker.ietf.org/doc/html/rfc2132#section-9.6 + // INIT-REBOOT: https://datatracker.ietf.org/doc/html/rfc2131#section-4.3.2 + dhcpMsg := []byte{ + 0x01, // message type + 0x01, // hardware type: Ethernet + 0x06, // hardware address length: Ethernet + 0x00, // hops + 0x48, 0x59, 0x58, 0x27, // transaction id + 0x00, 0x00, // seconds elasped + 0x80, 0x00, // flags: BROADCAST. Unicast may not be received. + 0x00, 0x00, 0x00, 0x00, // client ip: ciaddr + 0x00, 0x00, 0x00, 0x00, // your ip: yiaddr + 0x00, 0x00, 0x00, 0x00, // server ip: siaddr + 0x00, 0x00, 0x00, 0x00, // relay ip: giaddr + // client MAC: https://gitlab.com/wireshark/wireshark/-/raw/master/manuf + 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // client hardware address padding + // ServerHostName + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // BootFileName + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // magic cookie: DHCP + 0x63, 0x82, 0x53, 0x63, // 240B + // Options + 0x35, 0x01, 0x03, // DHCPREQUEST. DHCPDISCOVER may cause the server to release the OFFER. + 0x32, 0x04, 0xc0, 0xa8, 0x01, 0x04, // Requested IP address for `INIT-REBOOT` + 0x37, 0x01, 0x06, // Parameter Request List: DNS + 0x3d, 0x07, 0x01, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, // Client Identifier + 0xff, // END + // padding: min length of 300 bytes per RFC951 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + + // new transaction id + tid := dhcpMsg[4:8] + _, _ = rand.Read(tid) + + // MAC. On devices (Android) with both IPv6 and IPv6 available, MAC would be nil. + copy(dhcpMsg[28:28+16], ifi.HardwareAddr) + // Requested IP address + copy(dhcpMsg[245:245+4], ipAddr.IP.To4()) + // The DHCP server of VMware NAT mode requires Client identifier. + m := len(ifi.HardwareAddr) + //log.Printf("MAC[%v]: %v", m, ifi.HardwareAddr) + if m > 0 { + copy(dhcpMsg[255:255+m], ifi.HardwareAddr) + dhcpMsg[253] = byte(m&0xff) + 1 + dhcpMsg[255+m] = 0xff + } + + rAddr := &net.UDPAddr{IP: net.IPv4bcast, Port: 67} + _ = pc.SetDeadline(time.Now().Add(2 * time.Second)) + _, err = pc.WriteTo(dhcpMsg, rAddr) + if err != nil { + // defer doesn't work on reassignment + _ = pc.Close() + return nil, err + } + + // Prefer broadcast: + // (*nix) may have a deamon binding the local IPPort and the gateway IPPort. + // If so and the server replies with a broadcast to the local IPPort, rather than IPv4bcast, + // it may not be received on some OS. + if ipAddr.Zone != "" { + _ = pc.Close() + pc, err = reuseListenPacket("udp4", ":68") + if err != nil { + return nil, err + } + } + + //log.Printf("Receiving addr: %v", pc.LocalAddr()) + + buf := make([]byte, dhcpv4MessageSizeMax) + _ = pc.SetDeadline(time.Now().Add(2 * time.Second)) + n, _, err := pc.ReadFrom(buf[:]) + _ = pc.Close() + if err != nil { + return nil, err + } + //log.Printf("Received from: %v", rAddr2) + + dns, err = GetDNSFromReply4(buf[:n], tid) + + return +} + +// Required. +func getOutboundParams(ip string) (*net.IPAddr, *net.Interface, error) { + ipAddr, err := net.ResolveIPAddr("ip", ip) + if err != nil { + return nil, nil, err + } + + is6 := ipAddr.IP.To4() == nil + + ift, err := net.Interfaces() + if err != nil { + return nil, nil, err + } + + for _, ifi := range ift { + addrs, err := ifi.Addrs() + if err != nil { + continue + } + + var ipUnicast net.IP + var got bool + for _, addr := range addrs { + ipi := addr.(*net.IPNet).IP + if ipi.Equal(ipAddr.IP) { + got = true + } + if is6 && ipi.To4() == nil && ipi.IsLinkLocalUnicast() { + ipUnicast = ipi + } + //log.Printf("%v: %v", ifi.Name, ipi) + } + + if got { + // https://www.kernel.org/doc/html/latest/networking/operstates.html + if ifi.Flags&net.FlagRunning == net.FlagRunning { + if ipUnicast != nil { + ipAddr.IP = ipUnicast + } + // Bind fe80::/10 and ListenUDP needs Zone on *nix. + if ipAddr.Zone == "" && runtime.GOOS != "windows" { + ipAddr.Zone = ifi.Name + } + return ipAddr, &ifi, nil + } + return nil, nil, err6InterfaceNotRunning + } + } + + return nil, nil, err6NoLLUAFound +} + +func readBigEndianUint16(b []byte) uint16 { + return uint16(b[0])<<8&0xff00 | uint16(b[1]) +} + +// GetDNSFromReply6 gets DNS from a DHCPv6 REPLY message. +// https://datatracker.ietf.org/doc/html/rfc3646 +func GetDNSFromReply6(msg []byte, tid []byte) (ip []net.IP, err error) { + n := len(msg) + + if n < 7 || len(tid) < 3 { + err = err6InvalidParam + return + } + + if msg[0] != 0x07 { + err = err6NotReply + return + } + + if msg[1] != tid[0] || msg[2] != tid[1] || msg[3] != tid[2] { + err = err6TidNotMatch + return + } + + m := 4 + for m+2 <= n { + opt := readBigEndianUint16(msg[m : m+2]) + m += 2 + if m+2 < n { + i := m + 2 + m += 2 + int(readBigEndianUint16(msg[m:m+2])) + if m <= n { + if opt == 23 { + // DHCPv6 DNS + for i+16 <= m { + ip = append(ip, msg[i:i+16]) + i += 16 + } + break + } + continue + } + } + err = errInvalidReply + break + } + if len(ip) == 0 { + err = errNoDNSFound + } + return +} + +// GetDNSByIPv6 sends DHCPv6 INFORMATION-REQUEST message and return the DNS. +// ip is the reaching out IP. +func GetDNSByIPv6(ip string) (dns []net.IP, err error) { + ipAddr, _, err := getOutboundParams(ip) + if err != nil { + return nil, err + } + + pc, err := reuseListenPacket("udp6", "["+ipAddr.String()+"]:546") + if err != nil { + return nil, err + } + + //log.Printf("Receiving addr: %v", pc.LocalAddr()) + + // Minimal INFORMATION-REQUEST message + // https://en.wikipedia.org/wiki/DHCPv6 + // INFORMATION-REQUEST (11): + // https://datatracker.ietf.org/doc/html/rfc8415#section-18.2.6 + // https://datatracker.ietf.org/doc/html/rfc8415#section-8 + dhcpv6Msg := []byte{ + 0x0b, // message type + 0x48, 0x59, 0x58, // transaction id + // Options + // Elapsed Time Option: https://datatracker.ietf.org/doc/html/rfc8415#section-21.9 + 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, + // option request: INF_MAX_RT, Information Refresh Time, DNS + 0x00, 0x06, 0x00, 0x06, 0x00, 0x53, 0x00, 0x20, 0x00, 0x17, + // Client Identifier Option: https://datatracker.ietf.org/doc/html/rfc8415#section-21.2 + // anonymity profile DUID-LLT: https://datatracker.ietf.org/doc/html/rfc7844#section-4.3 + 0x00, 0x01, 0x00, 0x0e, 0x00, 0x01, 0x00, 0x01, 0x26, 0xeb, 0x58, 0x35, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, + } + + // new transaction id + tid := dhcpv6Msg[1:4] + _, _ = rand.Read(tid) + + rAddr := &net.UDPAddr{IP: net.IP{0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01, 0, 0x02}, Port: 547} + _ = pc.SetDeadline(time.Now().Add(2 * time.Second)) + _, err = pc.WriteTo(dhcpv6Msg, rAddr) + if err != nil { + return nil, err + } + + buf := make([]byte, dhcpv6MessageSizeComm) + _ = pc.SetDeadline(time.Now().Add(2 * time.Second)) + n, _, err := pc.ReadFrom(buf[:]) + _ = pc.Close() + if err != nil { + return nil, err + } + + dns, err = GetDNSFromReply6(buf[:n], tid) + + return +} + +// Detector holds the parameters and results. +// +// if err == nil { +// if lastActiveIP != "" { +// // got DNS +// } else { +// // uninitialized +// } +// } else if lastActiveIP == "" { +// // offline/invalid +// } else if constancy > x { +// // treat as (switched to a network that) can't get DNS +// } else { +// // treat as temporarily failed +// } +type Detector struct { + sync.RWMutex + got bool + // RemoteIPPort is the remote IPPort to detect within UDP. + RemoteIPPort string + lastActiveIP string + dns []net.IP + err error + constancy int +} + +func detect(d *Detector) (string, []net.IP, error) { + c, err := net.Dial("udp", d.RemoteIPPort) + if err != nil { + return "", nil, err + } + ipPort := c.LocalAddr().String() + _ = c.Close() + + ip, _, err := net.SplitHostPort(ipPort) + if err != nil { + return "", nil, err + } + + if ip[:7] == "2001:0:" { + // https://en.wikipedia.org/wiki/Teredo_tunneling#IPv6_addressing + err = errIsTeredo + } else if ip[:6] == "fe80::" || ip[:7] == "169.254" { + // Only detect valid network. https://www.wikiwand.com/en/Link-local_address + err = errIsLLA + } + + var dns []net.IP + if err == nil && (!d.got || d.lastActiveIP != ip) { + if ipPort[0] == '[' { + dns, err = GetDNSByIPv6(ip) + } else { + dns, err = GetDNSByIPv4(ip) + } + } + return ip, dns, err +} + +// Detect the DNS from the active interface which is adopted to connect to the provided IPPort address. +// The last active IP is used to reduce traffic or defense. +// If got and IP hasn't changed, skip sending DHCP messages as soft detecting. +func (d *Detector) Detect() error { + ip, dns, err := detect(d) + d.Lock() + d.lastActiveIP = ip + if err == nil { + if len(dns) > 0 { + d.dns = dns + } + d.got = true + } + if d.lastActiveIP == ip && isTheSameErr(err, d.err) { + d.constancy++ + } else { + d.constancy = 1 + } + d.err = err + d.Unlock() + return err +} + +// SetNewRound sets a new force detecting. +func (d *Detector) SetNewRound() { + d.Lock() + d.got = false + d.Unlock() +} + +// Status gets the detected results. +func (d *Detector) Status() (constancy int, ip string, dns []net.IP, err error) { + d.RLock() + constancy = d.constancy + ip = d.lastActiveIP + dns = d.dns + err = d.err + d.RUnlock() + return +} + +// Serve periodically detects the DNS as a daemon. +// cycle is the soft detecting rounds following a force detecting. sleep is in seconds. +func (d *Detector) Serve(cycle, sleep int) { + var i int + if cycle <= 0 { + cycle = 9 + } + if sleep <= 0 { + sleep = 10 + } + for { + if i%cycle == 0 { + d.SetNewRound() + } + _ = d.Detect() + i++ + time.Sleep(time.Duration(sleep) * time.Second) + } +} + +func isTheSameErr(err1, err2 error) bool { + return err1 == err2 || (err1 != nil && err2 != nil && err1.Error() == err2.Error()) +} diff --git a/vendor/github.com/lifenjoiner/dhcpdns/readme.md b/vendor/github.com/lifenjoiner/dhcpdns/readme.md new file mode 100644 index 0000000000..8ae2e684ae --- /dev/null +++ b/vendor/github.com/lifenjoiner/dhcpdns/readme.md @@ -0,0 +1,17 @@ +`dhcpdns` is a golang package to get the DHCP DNS. + +It helps programs to use the upstream DNS easily when your network changed. + +## Usage and Demo + +[cli](./cli) detects the DHCPv6/DHCPv4 DNS repeatedly. + +Enter cli dir in CLI: +``` +go build +cli +``` + +## Homepage + +https://github.com/lifenjoiner/dhcpdns diff --git a/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse0.go b/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse0.go new file mode 100644 index 0000000000..584f56d5d4 --- /dev/null +++ b/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse0.go @@ -0,0 +1,21 @@ +// Copyright 2023-now by lifenjoiner. All rights reserved. +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file. + +//go:build windows || (js && wasm) +// +build windows js,wasm + +package dhcpdns + +import ( + "net" +) + +// SO_REUSEADDR and SO_REUSEPORT: https://stackoverflow.com/questions/14388706/ + +// `SO_REUSEADDR` doesn't really work for this on Windows, if `DHCP Client` service occupies the port! +// https://learn.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse +// On Windows, the 1st bind receives the reply data. +func reuseListenPacket(network, address string) (net.PacketConn, error) { + return net.ListenPacket(network, address) +} diff --git a/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse1.go b/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse1.go new file mode 100644 index 0000000000..22d6e715f5 --- /dev/null +++ b/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse1.go @@ -0,0 +1,26 @@ +// Copyright 2023-now by lifenjoiner. All rights reserved. +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file. + +//go:build solaris +// +build solaris + +package dhcpdns + +import ( + "context" + "net" + "syscall" +) + +// No SO_REUSEPORT implemented. Doesn't work for SO_EXCLBIND on Solaris. +func reuseListenPacket(network, address string) (net.PacketConn, error) { + lc := net.ListenConfig{ + Control: func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + _ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + }) + }, + } + return lc.ListenPacket(context.Background(), network, address) +} diff --git a/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse2_1.go b/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse2_1.go new file mode 100644 index 0000000000..71bc3b9124 --- /dev/null +++ b/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse2_1.go @@ -0,0 +1,29 @@ +// Copyright 2023-now by lifenjoiner. All rights reserved. +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file. + +//go:build (linux && 386) || (linux && amd64) || (linux && arm) +// +build linux,386 linux,amd64 linux,arm + +package dhcpdns + +import ( + "context" + "net" + "syscall" +) + +const SO_REUSEPORT = 0xf + +func reuseListenPacket(network, address string) (net.PacketConn, error) { + lc := net.ListenConfig{ + Control: func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + _ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + // SO_REUSEPORT Requires same UID for security reason. + _ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, SO_REUSEPORT, 1) + }) + }, + } + return lc.ListenPacket(context.Background(), network, address) +} diff --git a/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse2_2.go b/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse2_2.go new file mode 100644 index 0000000000..e8e76cdff8 --- /dev/null +++ b/vendor/github.com/lifenjoiner/dhcpdns/sockopt_reuse2_2.go @@ -0,0 +1,26 @@ +// Copyright 2023-now by lifenjoiner. All rights reserved. +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file. + +//go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd || (linux && !386 && !amd64 && !arm) +// +build aix darwin dragonfly freebsd netbsd openbsd linux,!386,!amd64,!arm + +package dhcpdns + +import ( + "context" + "net" + "syscall" +) + +func reuseListenPacket(network, address string) (net.PacketConn, error) { + lc := net.ListenConfig{ + Control: func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + _ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + _ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1) + }) + }, + } + return lc.ListenPacket(context.Background(), network, address) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a88c3ee0b8..35cac753a0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -70,6 +70,9 @@ github.com/k-sone/critbitgo # github.com/kardianos/service v1.2.2 ## explicit; go 1.12 github.com/kardianos/service +# github.com/lifenjoiner/dhcpdns v0.0.6 +## explicit; go 1.20 +github.com/lifenjoiner/dhcpdns # github.com/miekg/dns v1.1.55 ## explicit; go 1.19 github.com/miekg/dns