Skip to content

Instantly share code, notes, and snippets.

@techraf
Created July 1, 2016 23:02
Show Gist options
  • Save techraf/a7f90c9acc1bf28ca58daa5549ec9b49 to your computer and use it in GitHub Desktop.
Save techraf/a7f90c9acc1bf28ca58daa5549ec9b49 to your computer and use it in GitHub Desktop.
capitive portal autoswitching for dnscrypt
#!/usr/bin/env ruby
# /usr/local/libexec/run-continuously
DELAY_BETWEEN_INTERNET_CHECKS = 4 # seconds
TIMES_BEFORE_GC = 100 # 100 * 5 ~ 10 minutes
if Process.uid != 0
$stderr.puts "Must be root to run #{$0}"
exit 1
end
load '/usr/local/bin/internet-access'
module NetworkSetup
extend self
LOCAL_DNS_SERVER = %w[127.0.0.1].freeze
NO_DNS_SERVERS = %w[Empty].freeze
STAR = '*'.freeze
def interfaces
`/usr/sbin/networksetup -listallnetworkservices`
.split("\n")
.reject{ |x| x.include? STAR }
end
def set_dns_servers_on_all_interfaces(dns_servers)
interfaces.map { |iface| set_dns_servers_on_interface(dns_servers, iface) }
end
def set_local_dns_servers_on_all_interfaces
interfaces.map { |iface| set_dns_servers_on_interface(LOCAL_DNS_SERVER, iface) }
end
def remove_dns_servers_from_all_interfaces
interfaces.map { |iface| remove_dns_servers_from_interface(iface) }
end
def remove_dns_servers_from_interface(interface)
set_dns_servers_on_interface(NO_DNS_SERVERS, interface)
end
def set_dns_servers_on_interface(dns_servers, interface)
`/usr/sbin/networksetup -setdnsservers '#{interface}' #{dns_servers.join(' ')}`
end
def set_local_dns_servers_on_interface(interface)
set_dns_servers_on_interface(LOCAL_DNS_SERVER, interface)
end
end
ISO8601_FORMAT = '%Y-%m-%dT%H:%M:%SZ '.freeze
DETECTED = 'Internet detected'.freeze
UNAVAILABLE = 'Internet unavailable'.freeze
internet_last = nil
since_gc = 0
GROWLNOTIFY = '/usr/local/bin/growlnotify'.freeze
NETWORKICON = '/System/Library/PreferencePanes/Network.prefPane/Contents/Resources/Network.icns'
growl = File.exist? GROWLNOTIFY
loop do
if (internet = !!InternetAccess.internet_reachable?) != internet_last
if internet
`echo internet up | #{GROWLNOTIFY} --image #{NETWORKICON}` if growl
NetworkSetup.set_local_dns_servers_on_all_interfaces
else
`echo internet down | #{GROWLNOTIFY} --image #{NETWORKICON}` if growl
NetworkSetup.remove_dns_servers_from_all_interfaces
end
$stderr.print Time.now.utc.strftime(ISO8601_FORMAT)
$stderr.puts (internet ? DETECTED : UNAVAILABLE)
internet_last = internet
end
if (since_gc += 1) == TIMES_BEFORE_GC
since_gc = 0
GC.start
end
sleep DELAY_BETWEEN_INTERNET_CHECKS
end
#!/usr/bin/env ruby
# /usr/local/bin/internet-access
#
# is actual interenet access available?
#
# set env var DEBUG to turn on $DEBUG and see debug statements
require 'etc'
require 'net/http'
require 'resolv'
require 'timeout'
require 'uri'
# touch ~/.force-no-internet-access to force false (~ is primary user of this box)
# touch ~/.force-internet-access to force true
module InternetAccess
extend self # avoid def self.fn everywhere
HOME_DIR = Etc.getpwuid(501).dir
DNS_TIMEOUT = 4 # seconds
DNS_RETRIES = 2 # times
DNS_SERVERS = (ENV['DNS_SERVERS'] || '8.8.8.8 8.8.4.4').split # or space-seperated array
REACHABLE_WEBSITE = URI(ENV['REACHABLE_WEBSITE'] || 'http://www.thinkdifferent.us')
REACHABLE_CONTENT = ENV['REACHABLE_CONTENT'] || '<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>'
REACHABLE_RETRIES = 2 # tries
REACHABLE_TIMEOUT = 1 # seconds
REACHABLE_MAXIMUM_REDIRECTS = 5
MAXIMUM_TIME = 12 # seconds
USER_AGENT = ENV['USER_AGENT'] || 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)'
QUIET = !(ARGV & %w[-q --quiet -s --silent]).empty?
def debug(*args)
$stderr.puts(*args) if $DEBUG
end
def dns_reachable?
if (f = forced) != nil
return f
end
result = nil
t = Thread.new do
Thread.current.abort_on_exception = true
result = _dns_reachable?
end
begin
Timeout.timeout(DNS_TIMEOUT) do
t.join
end
result
rescue
t.kill if t.alive?
debug 'dns_reachable: giving up'
return false
end
end
def website_reachable?(ip = nil)
unless (f = forced).nil?
return f
end
result = false
t = Thread.new do
Thread.current.abort_on_exception = true
result = _website_reachable?(ip)
end
t.kill if t.join(MAXIMUM_TIME).nil?
result
end
def force_disabled?
d = File.exist?(File.join(HOME_DIR, '.force-no-internet-access'))
debug "force disabled = #{d}"
d
end
def force_enabled?
e = File.exist?(File.join(HOME_DIR, '.force-internet-access'))
debug "force enabled = #{e}"
e
end
def forced
return true if force_enabled?
return false if force_disabled?
end
def internet_reachable?
if ip = dns_reachable?
website_reachable? ip # to test broken system resolvers
end
end
LIST_HARDWARE_REGEX = /\AHardware Port: /
def list_network_services
lines = `networksetup -listallnetworkservices 2>/dev/null`.chop.split("\n")
lines.shift
lines
end
NOT_A_VALID_SERVICE_REGEX = /is not a recognized network service\.$/
def get_dns_servers(network_service)
lines = `networksetup -getdnsservers '#{network_service}' 2>/dev/null`.chop.split("\n")
lines.reject! do |x|
x =~ /Error: The parameters were not valid.$/ ||
x =~ /any DNS Servers set on/ ||
x =~ NOT_A_VALID_SERVICE_REGEX
end
lines unless lines.empty?
end
def bits_to_subnet(bits)
return unless bits >= 0 && bits <= 32
4.times.map do
bits -= 8
(-1 >> bits) & 0xFF
end.join('.')
end
SUBNET_TO_BITS = Hash[ (0..32).map { |bits| [bits_to_subnet(bits), bits] } ]
def subnet_to_bits(subnet)
SUBNET_TO_BITS[subnet]
end
MAC_ADDR_REGEX = /[0-9a-f:]{17,}/
IP4ROUTER_REGEX = /^Router: /
IP4SUBNET_REGEX = /^Subnet mask: /
IP_REGEX = /^IP address: /
def get_info_addr(network_service)
out = `networksetup -getinfo '#{network_service}' 2>/dev/null`.chop
return if out =~ NOT_A_VALID_SERVICE_REGEX
lines = out.split("\n")
ip4addr = lines.grep(IP_REGEX) { |m| m.sub(IP_REGEX, '') }.first
ip4subnet = lines.grep(IP4SUBNET_REGEX) { |m| m.sub(IP4SUBNET_REGEX, '') }.first
ip4router = lines.grep(IP4ROUTER_REGEX) { |m| m.sub(IP4ROUTER_REGEX, '') }.first
mac = lines.grep(MAC_ADDR_REGEX).first
mac = mac.match(MAC_ADDR_REGEX).to_s if mac
r = {}
r[:ip4addr] = ip4addr if ip4addr
r[:ip4router] = ip4router if ip4router
r[:ip4subnet] = ip4subnet if ip4subnet
r[:mac] = mac if mac
r
end
# def get_mac_addr(network_service)
# mac = `networksetup -getmacaddress '#{network_service}' 2>/dev/null`.match(MAC_ADDR_REGEX).to_s
# mac if !mac.nil? && !mac.empty?
# end
HARDWARE_PORT_REGEX = /^Hardware Port: /
DEVICE_REGEX = /^Device: /
def _get_hw_ports
ports = {}
hw_port = nil
device = nil
vlans = false
`networksetup -listallhardwareports 2>/dev/null`.chop.split("\n").reject do |x|
vlans ||= (x =~ /^VLAN Configurations/)
end.map do |y|
case y
when HARDWARE_PORT_REGEX
hw_port = y.sub(HARDWARE_PORT_REGEX, '')
when DEVICE_REGEX
device = y.sub(DEVICE_REGEX, '')
when ''
ports[hw_port] = device
hw_port = device = nil
end
end
ports
end
def get_hw_ports
@@hw_ports ||= _get_hw_ports
end
def get_hw_port(network_service)
get_hw_ports[network_service]
end
def list_dns_servers
list_network_services.map do |port|
print "#{port}:"
if hw = get_hw_port(port)
print " #{hw}"
end
if info = get_info_addr(port)
if info[:ip4addr]
print " #{info[:ip4addr]}"
if info[:ip4subnet]
subnet = subnet_to_bits(info[:ip4subnet]) || info[:ip4subnet]
print "/#{subnet}"
end
print " router #{info[:ip4router]}" if info[:ip4router]
end
print " macaddr #{info[:mac]}" if info[:mac]
end
# if mac = get_mac_addr(port)
# printf " mac #{mac}"
# end
if svrs = get_dns_servers(port)
printf " dns #{svrs.join(', ')}"
end
puts
end
end
def main
r = internet_reachable?
list_dns_servers if r && !QUIET
r
end
private
def _dns_reachable?
debug 'dns_reachable'
opts = { nameserver: DNS_SERVERS }
debug "Resolv::DNS.new #{opts}"
dns_resolvers = Resolv::DNS.new(opts)
# dns_resolvers.timeouts = DNS_TIMEOUT
retries = DNS_RETRIES
begin
host = REACHABLE_WEBSITE.host
debug "getaddress #{host}"
result = dns_resolvers.getaddress(host).to_s
debug "getaddress = #{result}"
raise 'failed' unless result =~ /./
debug 'dns_reachable: success'
result
rescue Resolv::ResolvError => e
debug e
if (retries -= 1) > 0
debug 'retry'
retry
end
debug 'giving up'
raise
end
end
def _website_reachable?(ip = nil)
uri = REACHABLE_WEBSITE
retries = REACHABLE_RETRIES
redirects = REACHABLE_MAXIMUM_REDIRECTS
begin
host, port = (ip || uri.host), uri.port
debug "internet_reachable new http #{uri} (host=#{host}, port=#{port})"
http = Net::HTTP.new(host, port)
http.set_debug_output($stderr) if $DEBUG
http.use_ssl = uri.scheme == 'https'
http.continue_timeout = REACHABLE_TIMEOUT
http.open_timeout = REACHABLE_TIMEOUT
http.read_timeout = REACHABLE_TIMEOUT
http.ssl_timeout = REACHABLE_TIMEOUT
http.close_on_empty_response = true
debug "new request GET #{uri}"
request = Net::HTTP::Get.new(uri, 'User-Agent' => USER_AGENT)
begin
debug "trying to GET #{uri}"
resp = nil
http.request(request) do |response|
debug 'saving response'
resp = response
debug 'getting body'
index = (response.body || '').index(REACHABLE_CONTENT)
if index
debug 'website_reachable: success'
else
debug 'website_reachable: failure'
return false
end
index
end
end
rescue Net::HTTPMovedTemporarily, Net::HTTPMovedPermanently
debug 'redirect'
uri = URI(resp['location'])
retry if (redirects -= 1) > 0
return false
rescue => e
debug "rescue => #{e}"
retry if (retries -= 1) > 0
debug "giving up"
return false
end
end
end
if $0 == __FILE__
$DEBUG ||= !!ENV['DEBUG']
begin
raise unless InternetAccess.main
rescue Exception
exit 1
end
end
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- /Library/LaunchDaemons/org.github.steakknife.dnscrypt-captive-portal-monitor-daemon.plist -->
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>bmf.RunContinuously</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/libexec/run-continuously</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>UserName</key>
<string>root</string>
<key>StandardOutPath</key>
<string>/Library/logs/org.github.steakknife.dnscrypt-captive-portal-monitor-daemon</string>
<key>StandardErrorPath</key>
<string>/Library/logs/org.github.steakknife.dnscrypt-captive-portal-monitor-daemon</string>
</dict>
</plist>
#!/bin/sh
rm -f ~/.force-no-internet-access ~/.force-internet-access
if [ -e ~/.force-no-internet-access ]; then
echo "internet access: automatic" >&2
elif [ -e ~/.force-internet-access ]; then
touch ~/.force-no-internet-access
echo "internet access: forced off" >&2
else
touch ~/.force-internet-access
echo "internet access: forced on" >&2
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment