Start experimenting with better support for captive portals

MacOS (and probably Windows and other systems) tries to fetch a URL
before marking a network interface as available.

During this time, applications cannot use the interface at all, not
even bind their address.

When DNS queries are sent to dnscrypt-proxy, this causes the system
to wait for a response that can't come from the network, since we
hit a dead lock here.

The only option is to return hard-coded responses directly until
te interface is available.

The same captive portal configuration file can also serve a different
purpose.

Once the network is available, captive portal detection may not
work as expected if the answer is cached for too long. In fact, it
probably can't work at all since routers can't hijack DNS queries.

Once thing we can do is redirect the list of names used for captive
portal detection to the fallback resolvers. This may allow detection
to work as expected while still using a secure channel for all
other queries.
This commit is contained in:
Frank Denis 2020-08-03 18:05:42 +02:00
parent 210ba8c60f
commit 4424602e39
6 changed files with 202 additions and 6 deletions

179
dnscrypt-proxy/coldstart.go Normal file
View File

@ -0,0 +1,179 @@
package main
import (
"fmt"
"net"
"strings"
"time"
"unicode"
"github.com/jedisct1/dlog"
"github.com/miekg/dns"
)
type CaptivePortalEntryips []net.IP
type CaptivePortalEntry struct {
name string
ips CaptivePortalEntryips
}
type CaptivePortalHandler struct {
cancelChannels []chan struct{}
}
func (captivePortalHandler *CaptivePortalHandler) Stop() {
for _, cancelChannel := range captivePortalHandler.cancelChannels {
cancelChannel <- struct{}{}
_ = <-cancelChannel
}
}
func handleColdStartClient(clientPc *net.UDPConn, cancelChannel chan struct{}, ipsMap *map[string]CaptivePortalEntryips) bool {
buffer := make([]byte, MaxDNSPacketSize-1)
clientPc.SetDeadline(time.Now().Add(time.Duration(1) * time.Second))
length, clientAddr, err := clientPc.ReadFrom(buffer)
exit := false
select {
case <-cancelChannel:
exit = true
default:
}
if exit {
return true
}
if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
return false
}
if err != nil {
dlog.Warn(err)
return true
}
packet := buffer[:length]
msg := &dns.Msg{}
if err := msg.Unpack(packet); err != nil {
return false
}
if len(msg.Question) != 1 {
return false
}
question := msg.Question[0]
qType, ok := dns.TypeToString[question.Qtype]
if !ok {
qType = string(qType)
}
name, err := NormalizeQName(question.Name)
if err != nil {
return false
}
ips, ok := (*ipsMap)[name]
if !ok {
dlog.Infof("Coldstart query: [%v] (%v)", name, qType)
return false
}
dlog.Noticef("Coldstart query for captive portal detection: [%v] (%v)", name, qType)
if question.Qclass != dns.ClassINET {
return false
}
var respMsg *dns.Msg
respMsg = EmptyResponseFromMessage(msg)
ttl := uint32(1)
if question.Qtype == dns.TypeA {
for _, xip := range ips {
if ip := xip.To4(); ip != nil {
rr := new(dns.A)
rr.Hdr = dns.RR_Header{Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: ttl}
rr.A = ip
respMsg.Answer = []dns.RR{rr}
break
}
}
} else if question.Qtype == dns.TypeAAAA {
for _, xip := range ips {
if ip := xip.To16(); ip != nil {
rr := new(dns.AAAA)
rr.Hdr = dns.RR_Header{Name: question.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl}
rr.AAAA = ip
respMsg.Answer = []dns.RR{rr}
break
}
}
}
if response, err := respMsg.Pack(); err == nil {
clientPc.WriteTo(response, clientAddr)
dlog.Noticef("Coldstart query synthesized: [%v] (%v)", name, qType)
}
return false
}
func addColdStartListener(proxy *Proxy, ipsMap *map[string]CaptivePortalEntryips, listenAddrStr string, cancelChannel chan struct{}) error {
listenUDPAddr, err := net.ResolveUDPAddr("udp", listenAddrStr)
if err != nil {
return err
}
clientPc, err := net.ListenUDP("udp", listenUDPAddr)
if err != nil {
return err
}
go func() {
for !handleColdStartClient(clientPc, cancelChannel, ipsMap) {
}
clientPc.Close()
cancelChannel <- struct{}{}
}()
return nil
}
func ColdStart(proxy *Proxy) (*CaptivePortalHandler, error) {
if len(proxy.captivePortalFile) == 0 {
return nil, nil
}
bin, err := ReadTextFile(proxy.captivePortalFile)
if err != nil {
dlog.Warn(err)
return nil, err
}
ipsMap := make(map[string]CaptivePortalEntryips)
for lineNo, line := range strings.Split(string(bin), "\n") {
line = TrimAndStripInlineComments(line)
if len(line) == 0 {
continue
}
name, ipsStr, ok := StringTwoFields(line)
if !ok {
return nil, fmt.Errorf(
"Syntax error for a captive portal rule at line %d",
1+lineNo,
)
}
name, err = NormalizeQName(name)
if err != nil {
continue
}
var ips []net.IP
for _, ip := range strings.Split(ipsStr, ",") {
ipStr := strings.TrimFunc(ip, unicode.IsSpace)
if ip := net.ParseIP(ipStr); ip != nil {
ips = append(ips, ip)
} else {
return nil, fmt.Errorf(
"Syntax error for a captive portal rule at line %d",
1+lineNo,
)
}
}
ipsMap[name] = ips
}
listenAddrStrs := proxy.listenAddresses
cancelChannels := make([]chan struct{}, 0)
for _, listenAddrStr := range listenAddrStrs {
cancelChannel := make(chan struct{})
if err := addColdStartListener(proxy, &ipsMap, listenAddrStr, cancelChannel); err == nil {
cancelChannels = append(cancelChannels, cancelChannel)
}
}
captivePortalHandler := CaptivePortalHandler{
cancelChannels: cancelChannels,
}
return &captivePortalHandler, nil
}

View File

@ -68,6 +68,7 @@ type Config struct {
BlockIPLegacy BlockIPConfigLegacy `toml:"ip_blacklist"`
ForwardFile string `toml:"forwarding_rules"`
CloakFile string `toml:"cloaking_rules"`
CaptivePortalFile string `toml:"captive_portal_handler"`
StaticsConfig map[string]StaticConfig `toml:"static"`
SourcesConfig map[string]SourceConfig `toml:"sources"`
BrokenImplementations BrokenImplementationsConfig `toml:"broken_implementations"`
@ -547,6 +548,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
proxy.forwardFile = config.ForwardFile
proxy.cloakFile = config.CloakFile
proxy.captivePortalFile = config.CaptivePortalFile
allWeeklyRanges, err := ParseAllWeeklyRanges(config.AllWeeklyRanges)
if err != nil {
@ -613,7 +615,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
}
proxy.showCerts = *flags.ShowCerts || len(os.Getenv("SHOW_CERTS")) > 0
if !*flags.Check && !*flags.ShowCerts && !*flags.List && !*flags.ListAll {
if err := NetProbe(netprobeAddress, netprobeTimeout); err != nil {
if err := NetProbe(proxy, netprobeAddress, netprobeTimeout); err != nil {
return err
}
for _, listenAddrStr := range proxy.listenAddresses {

View File

@ -0,0 +1,6 @@
captive.apple.com 17.253.109.201,17.253.113.202
connectivitycheck.gstatic.com 64.233.165.94
connectivitycheck.android.com 172.217.20.110
www.msftncsi.com 2.19.98.8,2.19.98.59
dns.msftncsi.com 131.107.255.255
www.msftconnecttest.com 13.107.4.52

View File

@ -9,12 +9,15 @@ import (
"github.com/jedisct1/dlog"
)
func NetProbe(address string, timeout int) error {
cancelChannels := ColdStart([]string{"0.0.0.0:53"})
defer ColdStartStop(cancelChannels)
func NetProbe(proxy *Proxy, address string, timeout int) error {
if len(address) <= 0 || timeout == 0 {
return nil
}
if captivePortalHandler, err := ColdStart(proxy); err == nil {
defer captivePortalHandler.Stop()
} else {
dlog.Critical(err)
}
remoteUDPAddr, err := net.ResolveUDPAddr("udp", address)
if err != nil {
return err

View File

@ -7,10 +7,15 @@ import (
"github.com/jedisct1/dlog"
)
func NetProbe(address string, timeout int) error {
func NetProbe(proxy *Proxy, address string, timeout int) error {
if len(address) <= 0 || timeout == 0 {
return nil
}
if captivePortalHandler, err := ColdStart(proxy); err == nil {
defer captivePortalHandler.Stop()
} else {
dlog.Critical(err)
}
remoteUDPAddr, err := net.ResolveUDPAddr("udp", address)
if err != nil {
return err

View File

@ -67,6 +67,7 @@ type Proxy struct {
blockIPFormat string
forwardFile string
cloakFile string
captivePortalFile string
pluginsGlobals PluginsGlobals
sources []*Source
clientsCount uint32
@ -83,7 +84,7 @@ type Proxy struct {
showCerts bool
dohCreds *map[string]DOHClientCreds
skipAnonIncompatbibleResolvers bool
anonDirectCertFallback bool
anonDirectCertFallback bool
dns64Prefixes []string
dns64Resolvers []string
}