Skip to content

Instantly share code, notes, and snippets.

@carlzulauf
Last active April 1, 2019 16:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save carlzulauf/b345272f3c34cc087c6417b94c2c068f to your computer and use it in GitHub Desktop.
Save carlzulauf/b345272f3c34cc087c6417b94c2c068f to your computer and use it in GitHub Desktop.
Script to wrap openvpn, detect DNS settings, and tell systemd-resolved about them
#!/usr/bin/env ruby
OVPN_CONFIG_PATH = "~/path/to/your.ovpn"
require 'open3'
class VpnWrapper
attr_reader :buffer, :local_interface, :local_dns, :pid
def initialize
@buffer = String.new
@local_interface = "wlp59s0"
@local_dns = read_local_dns
end
def run
Signal.trap("INT") { down }
Open3.popen2e(cmd) do |stdin, stdout, process|
puts "Connecting..."
@pid = process.pid
read_until_dns(stdout)
puts "Connected and authenticated!"
up(dns_options)
process.join
end
end
def up(options)
puts "Configuring systemd-resolved DNS"
if (ip = options[:ip])
puts "Setting DNS for tun0 and #{local_interface} to #{ip}"
`systemd-resolve --interface=tun0 --set-dns=#{ip}`
`systemd-resolve --interface=#{local_interface} --set-dns=#{ip}`
end
if (domains = options[:domains])
puts "Setting search domains for tun0"
set_domains = domains.map{ |d| "--set-domain=#{d}" }.join(" ")
`systemd-resolve --interface=tun0 #{set_domains}`
end
end
def down
puts "Restoring #{local_interface} DNS to #{local_dns}"
`systemd-resolve --interface=#{local_interface} --set-dns=#{local_dns}`
puts "Bringing down VPN connection"
Process.kill("INT", pid)
end
private
def read_local_dns
`systemd-resolve #{local_interface} --status`.lines.grep(/DNS Servers/).first.split(":").last.strip
end
def dns_push_pattern
/PUSH: Received control message.+dhcp-option/
end
def cmd
"sudo openvpn --config #{OVPN_CONFIG_PATH}"
end
def dns_push_line
buffer.lines.grep(dns_push_pattern).first
end
def dns_option_message
dns_push_line.match(/'(.+)'/)[1]
end
def dns_options
{}.tap do |options|
dns_option_message.split(",").grep(/dhcp-option/).map do |line|
parts = line.split(" ")
case parts[1]
when "DNS" then options[:ip] = parts[2]
when "SEARCH" then options[:domains] = parts[2..-1]
end
end
end
end
def read_until_dns(io)
loop do
begin
buffer << io.read_nonblock(16_384)
return if dns_push_line
rescue EOFError
return
rescue IO::EAGAINWaitReadable
sleep 0.5
end
end
end
end
VpnWrapper.new.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment