Created
July 1, 2016 23:02
-
-
Save techraf/a7f90c9acc1bf28ca58daa5549ec9b49 to your computer and use it in GitHub Desktop.
capitive portal autoswitching for dnscrypt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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