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:
parent
210ba8c60f
commit
4424602e39
|
@ -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
|
||||||
|
}
|
|
@ -68,6 +68,7 @@ type Config struct {
|
||||||
BlockIPLegacy BlockIPConfigLegacy `toml:"ip_blacklist"`
|
BlockIPLegacy BlockIPConfigLegacy `toml:"ip_blacklist"`
|
||||||
ForwardFile string `toml:"forwarding_rules"`
|
ForwardFile string `toml:"forwarding_rules"`
|
||||||
CloakFile string `toml:"cloaking_rules"`
|
CloakFile string `toml:"cloaking_rules"`
|
||||||
|
CaptivePortalFile string `toml:"captive_portal_handler"`
|
||||||
StaticsConfig map[string]StaticConfig `toml:"static"`
|
StaticsConfig map[string]StaticConfig `toml:"static"`
|
||||||
SourcesConfig map[string]SourceConfig `toml:"sources"`
|
SourcesConfig map[string]SourceConfig `toml:"sources"`
|
||||||
BrokenImplementations BrokenImplementationsConfig `toml:"broken_implementations"`
|
BrokenImplementations BrokenImplementationsConfig `toml:"broken_implementations"`
|
||||||
|
@ -547,6 +548,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
|
|
||||||
proxy.forwardFile = config.ForwardFile
|
proxy.forwardFile = config.ForwardFile
|
||||||
proxy.cloakFile = config.CloakFile
|
proxy.cloakFile = config.CloakFile
|
||||||
|
proxy.captivePortalFile = config.CaptivePortalFile
|
||||||
|
|
||||||
allWeeklyRanges, err := ParseAllWeeklyRanges(config.AllWeeklyRanges)
|
allWeeklyRanges, err := ParseAllWeeklyRanges(config.AllWeeklyRanges)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -613,7 +615,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
}
|
}
|
||||||
proxy.showCerts = *flags.ShowCerts || len(os.Getenv("SHOW_CERTS")) > 0
|
proxy.showCerts = *flags.ShowCerts || len(os.Getenv("SHOW_CERTS")) > 0
|
||||||
if !*flags.Check && !*flags.ShowCerts && !*flags.List && !*flags.ListAll {
|
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
|
return err
|
||||||
}
|
}
|
||||||
for _, listenAddrStr := range proxy.listenAddresses {
|
for _, listenAddrStr := range proxy.listenAddresses {
|
||||||
|
|
|
@ -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
|
|
@ -9,12 +9,15 @@ import (
|
||||||
"github.com/jedisct1/dlog"
|
"github.com/jedisct1/dlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NetProbe(address string, timeout int) error {
|
func NetProbe(proxy *Proxy, address string, timeout int) error {
|
||||||
cancelChannels := ColdStart([]string{"0.0.0.0:53"})
|
|
||||||
defer ColdStartStop(cancelChannels)
|
|
||||||
if len(address) <= 0 || timeout == 0 {
|
if len(address) <= 0 || timeout == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if captivePortalHandler, err := ColdStart(proxy); err == nil {
|
||||||
|
defer captivePortalHandler.Stop()
|
||||||
|
} else {
|
||||||
|
dlog.Critical(err)
|
||||||
|
}
|
||||||
remoteUDPAddr, err := net.ResolveUDPAddr("udp", address)
|
remoteUDPAddr, err := net.ResolveUDPAddr("udp", address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -7,10 +7,15 @@ import (
|
||||||
"github.com/jedisct1/dlog"
|
"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 {
|
if len(address) <= 0 || timeout == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if captivePortalHandler, err := ColdStart(proxy); err == nil {
|
||||||
|
defer captivePortalHandler.Stop()
|
||||||
|
} else {
|
||||||
|
dlog.Critical(err)
|
||||||
|
}
|
||||||
remoteUDPAddr, err := net.ResolveUDPAddr("udp", address)
|
remoteUDPAddr, err := net.ResolveUDPAddr("udp", address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -67,6 +67,7 @@ type Proxy struct {
|
||||||
blockIPFormat string
|
blockIPFormat string
|
||||||
forwardFile string
|
forwardFile string
|
||||||
cloakFile string
|
cloakFile string
|
||||||
|
captivePortalFile string
|
||||||
pluginsGlobals PluginsGlobals
|
pluginsGlobals PluginsGlobals
|
||||||
sources []*Source
|
sources []*Source
|
||||||
clientsCount uint32
|
clientsCount uint32
|
||||||
|
|
Loading…
Reference in New Issue