require "option_parser" require "ipaddress" require "./colors" simulation = false file = nil prefered_network_configuration_program = nil prefered_wireless_configuration_program = nil prefered_dhcp_client = nil print_autodetect = false command = "list" OptionParser.parse! do |parser| parser.on "-s", "--simulation", "Export the network configuration." do simulation = true end parser.on "-a", "--print-autodetect", "Print autodetection of the installed programs." do print_autodetect = true end parser.on "-w wireless-configuration-program", "--wireless wireless-configuration-program", "iw" do |prog| prefered_wireless_configuration_program = prog end parser.on "-n network-configuration-program", "--net-conf network-configuration-program", "ifconfig | ip" do |prog| prefered_network_configuration_program = prog end parser.on "-d dhcp-client-program", "--dhcp-client dhcp-client-program", "udhcpc" do |prog| prefered_dhcp_client = prog end parser.on "-f file", "--file file", "Parse a configuration file." do |optsn| file = optsn end # 0: nothing is printed, 1: only events, 2: events and messages parser.on "-v verbosity", "--verbosity verbosity", "Verbosity (0-2). Default: 1" do |optsn| verbosity = optsn.to_i end parser.missing_option do |opt| STDERR.puts "You missed the argument for option #{opt}" # TODO: explain the different arguments exit 1 end parser.invalid_option do |flag| STDERR.puts "Error: #{flag} not a valid option" exit 1 end parser.unknown_args do |arg| command = arg.shift case command when /^(list)/ STDERR.puts "TODO: list" unless arg.empty? when /^(connect)/ STDERR.puts "TODO: connect" unless arg.empty? else STDERR.puts "Command #{command} not understood" end unless arg.empty? STDERR.puts "unknown arg: #{arg}" exit 1 end end parser.on "-h", "--help", "Show this help" do puts parser exit 0 end end class Do < Process class_property simulation = false def self.run(cmd : String, params : Array(String) = nil) if @@simulation puts "simulation, do: #{cmd} #{params.join(" ")}" Process::Status.new 0 else Process.run cmd, params end end end class NotSetup def to_s(io : IO) io << "not setup" end end class DHCP def to_s(io : IO) io << "dhcp" end end class NetworkCommands class_property cmd_network_configuration : IfconfigCommand.class | IPCommand.class = IfconfigCommand class_property cmd_wireless_configuration : IWCommand.class | NotSetup.class = NotSetup class_property cmd_dhcp_client : UDHCPCCommand.class | DHClientCommand.class | NotSetup.class = NotSetup class IWCommand def self.get_ssid(ifname : String, ssid) unless Do.run(cmd, [ name ]).success? raise "(#{cmd}) dhcp failed on #{ifname}" end end end class UDHCPCCommand def self.run(ifname : String) # TODO: verify which dhcp client is installed on the system cmd = "udhcpc" unless Do.run(cmd, [ name ]).success? raise "(#{cmd}) dhcp failed on #{ifname}" end end end class DHClientCommand def self.run(ifname : String) # TODO: verify which dhcp client is installed on the system cmd = "udhcpc" unless Do.run(cmd, [ name ]).success? raise "(#{cmd}) dhcp failed on #{ifname}" end end end class IfconfigCommand def self.interface_exists(name : String) Do.run("ifconfig", [ name ]).success? end def self.up_or_down(name : String, updown : String) unless Do.run("ifconfig", [ name, updown ]).success? raise "(ifconfig) Cannot set #{updown} link name #{name}" end end def self.up(name : String) self.up_or_down name, "up" end def self.down(name : String) self.up_or_down name, "down" end def self.set_ip(name : String, ip : IPAddress) unless Do.run("ifconfig", [ name, "add", ip.to_string ]).success? raise "(ifconfig) Cannot set ip address #{ip.to_string} for #{name}" end end # currently, aliasses with ifconfig: ifconfig add ip/mask # same command as the ip setup def self.set_alias(name : String, ip : IPAddress) unless Do.run("ifconfig", [ name, "add", ip.to_string ]).success? raise "(ifconfig) Cannot set ip address alias #{ip.to_string} for #{name}" end end def self.mtu(name : String, mtu : Int32?) unless Do.run("ifconfig", [ name, "mtu", mtu.to_s ]).success? raise "(ifconfig) Cannot set mtu #{mtu} for #{name}" end end def self.description(name : String, description : String) unless Do.run("ifconfig", [ name, "description", "\"#{description}\"" ]).success? raise "(ifconfig) Cannot set description #{description} for #{name}" end end end class IPCommand def self.interface_exists(name : String) Do.run("ip", [ "link", "show", "dev", name ]).success? end def self.up_or_down(name : String, updown : String) unless Do.run("ip", [ "link", "set", updown, "dev", name ]).success? raise "(ip) Cannot set #{updown} link name #{name}" end end def self.up(name : String) self.up_or_down name, "up" end def self.down(name : String) self.up_or_down name, "down" end def self.set_ip(name : String, ip : IPAddress) unless Do.run("ip", [ "address", "add", ip.to_string, "dev", name ]).success? raise "(ip) Cannot add ip address #{ip.to_string} to #{name}" end end def self.set_alias(name : String, ip : IPAddress) unless Do.run("ip", [ "address", "add", ip.to_string, "dev", name ]).success? raise "(ip) Cannot add ip address alias #{ip.to_string} to #{name}" end end def self.mtu(name : String, mtu : Int32?) unless Do.run("ip", [ "link", "set", "mtu", mtu.to_s, "dev", name ]).success? raise "(ip) Cannot set mtu #{mtu} to #{name}" end end def self.description(name : String, description : String) puts "TODO: (ip) setup description '#{description}' to interface #{name}" # unless Do.run("ip", [ "link", "set", "description", description, "dev", name ]).success? # raise "(ip) Cannot set description #{description} to #{name}" # end end end def self.interface_exists(name : String) @@cmd_network_configuration.interface_exists(name) end def self.up(ifname : String) @@cmd_network_configuration.up(ifname) end def self.down(ifname : String) @@cmd_network_configuration.up(ifname) end def self.set_ip(name : String, ip : IPAddress) @@cmd_network_configuration.set_ip name, ip end def self.dhcp(name : String) cmd = @@cmd_dhcp_client case cmd when NotSetup puts "no dhcp client: cannot perform dhcp on #{name}" when UDHCPCCommand cmd.run name end end def self.set_alias(name : String, ip : IPAddress) @@cmd_network_configuration.set_alias name, ip end def self.mtu(name : String, mtu : Int32?) @@cmd_network_configuration.mtu name, mtu end def self.description(name : String, description : String) @@cmd_network_configuration.description name, description end def self.wireless_list_ssid(ifname : String) cmd = @@cmd_wireless_configuration case cmd when NotSetup puts "no wireless configuration program: cannot list ssid" when IWCommand cmd.list_ssid ifname end end def self.wireless_connect_wpa_psk(ifname : String, ssid : String, passwd : String) cmd = @@cmd_wireless_configuration case cmd when NotSetup puts "no wireless configuration program: cannot connect to ssid #{ssid}" when IWCommand cmd.list_ssid ifname end end end class WirelessAPSetup property ssid : String # This is a list of parameters that should be unique to each AP property up : Bool property description : String? property mtu : Int32? property main_ip_v4 : IPAddress | DHCP | NotSetup property main_ip_v6 : IPAddress | DHCP | NotSetup property aliasses_v4 : Array(IPAddress) property aliasses_v6 : Array(IPAddress) property dns : Array(IPAddress) # we currently only support WPA2-PSK wireless security mechanism property security : WPA class WPA property key : String def initialize(@key) end end def initialize(@ssid, @security) @main_ip_v4 = NotSetup.new @main_ip_v6 = NotSetup.new @aliasses_v4 = Array(IPAddress).new @aliasses_v6 = Array(IPAddress).new @up = true @dns = Array(IPAddress).new end def to_s(io : IO) io << to_string end def to_string String.build do |str| str << "\t#{CBLUE}#{ssid}#{CRESET}\n" str << "\t\t#description #{description.not_nil!}\n" unless description.nil? str << "\t\t#{@up? "up" : "down"}\n" str << "\t\tmtu #{mtu}\n" unless mtu.nil? # ipv4 unless main_ip_v4.is_a?(NotSetup) str << "\t\tinet #{main_ip_v4}\n" end aliasses_v4.each do |a| str << "\t\talias #{a}\n" end # ipv6 unless main_ip_v6.is_a?(NotSetup) str << "\t\tinet6 #{main_ip_v6}\n" end unless @aliasses_v6.empty? @aliasses_v6.each do |a| str << "\t\talias6 #{a}\n" end end if dns.empty? str << "\t\tno dns configured\n" else dns.each do |ip| str << "\t\tdns: #{ip}\n" end end # to improve readability str << "\n" end end end # # interface configuration # class InterfaceConfiguration property name : String property up : Bool property description : String? property mtu : Int32? property wireless : Bool property main_ip_v4 : IPAddress | DHCP | NotSetup property main_ip_v6 : IPAddress | DHCP | NotSetup property aliasses_v4 : Array(IPAddress) property aliasses_v6 : Array(IPAddress) property wireless_networks : Hash(String, WirelessAPSetup) property dns : Array(IPAddress) def initialize (@name, @up, @description, @mtu, @main_ip_v4, @main_ip_v6, aliasses, @wireless, @wireless_networks, @dns = Array(IPAddress).new ) @aliasses_v4 = Array(IPAddress).new @aliasses_v6 = Array(IPAddress).new aliasses.each do |ip| if ip.ipv4? aliasses_v4 << ip else aliasses_v6 << ip end end end def to_s(io : IO) io << to_string end def to_string String.build do |str| if NetworkCommands.interface_exists(@name) str << "#{CGREEN}#{@name}#{CRESET}\n" else str << "#{CRED}#{@name}#{CRESET}\n" end str << "\tdescription: '#{description.not_nil!}'\n" unless description.nil? str << "\t#{@up? "up" : "down"}\n" unless mtu.nil? str << "\tmtu #{mtu}\n" end # ipv4 unless main_ip_v4.is_a?(NotSetup) str << "\tinet #{main_ip_v4}\n" end unless aliasses_v4.empty? aliasses_v4.each do |a| str << "\talias #{a}\n" end end # ipv6 unless main_ip_v6.is_a?(NotSetup) str << "\tinet6 #{main_ip_v6}\n" end unless aliasses_v6.empty? aliasses_v6.each do |a| str << "\talias6 #{a}\n" end end unless dns.empty? dns.each do |ip| str << "\tdns: #{ip}\n" end end unless wireless_networks.empty? # to improve readability str << "\n" wireless_networks.each do |k,v| str << v end end end end # configure the interface def execute unless NetworkCommands.interface_exists(@name) raise "The interface #{@name} doesn't exists, yet." end if up NetworkCommands.up name else puts "not marked as 'up' -- ending here" return end unless mtu.nil? NetworkCommands.mtu name, mtu end unless description.nil? NetworkCommands.description name, description.not_nil! end # ipv4 configuration main_ip_v4.tap do |ip| case ip when IPAddress NetworkCommands.set_ip name, ip when DHCP NetworkCommands.dhcp name when NotSetup puts "no ipv4" else raise "ipv4 configuration: neither static nor dynamic" end # We wont setup aliasses unless there is an actual IP address if ip != NotSetup aliasses_v4.each do |ip_alias| NetworkCommands.set_alias name, ip_alias end end end # ipv6 configuration main_ip_v6.tap do |ip| case ip when IPAddress NetworkCommands.set_ip name, ip # TODO #when Autoconfiguration # NetworkCommands.autoconfiguration name #when DHCP # NetworkCommands.dhcp6 name when NotSetup puts "no ipv6" else raise "ipv4 configuration: neither static nor dynamic" end # We wont setup aliasses unless there is an actual IP address if ip != NotSetup aliasses_v6.each do |ip_alias| NetworkCommands.set_alias name, ip_alias end end end end end class NetworkConfigurationParser def self.parse_file(file_name : String) : InterfaceConfiguration content = File.read(file_name) content = content.rchop ifname = /.([a-zA-Z0-9]+)$/.match(file_name).try &.[1] if ifname.nil? raise "The interface name is not known from the filename: '#{file_name}'" end wireless = false wireless = true unless /^wl[0-9]+$/.match(ifname) self.parse(ifname.not_nil!, content, wireless) end def self.parse (ifname : String, data : String, wireless = false) : InterfaceConfiguration up = false description = nil mtu = nil main_ip_v4 = NotSetup.new main_ip_v6 = NotSetup.new dns = Array(IPAddress).new aliasses = [] of IPAddress wireless_networks = {} of String => WirelessAPSetup data.split("\n").each do |line| case line when /^up/ up = true when /^description/ description = /^description (.+)/.match(line).try &.[1] when /^inet6? alias .*/ ipstr = /^inet6? alias ([a-f0-9:.\/]+)/.match(line).try &.[1] if ipstr.nil? puts "wrong IP address alias, line #{line}" next end aliasses.push IPAddress.parse(ipstr) when /^inet6? dhcp/ # IP address is DHCP if /^inet /.match(line) main_ip_v4 = DHCP.new else main_ip_v6 = DHCP.new end when /^inet6 autoconf/ # IP address is autoconfigured puts "TODO: IPv6 autoconfiguration" when /^inet6? .*/ ipstr = /^inet6? ([a-f0-9:.\/]+)/.match(line).try &.[1] if ipstr.nil? puts "wrong IP address, line #{line}" next end if /^inet /.match(line) main_ip_v4 = IPAddress.parse ipstr else main_ip_v6 = IPAddress.parse ipstr end when /^join [^ \t]+ wpakey .*/ # WPA2-PSK, other security mechanisms are not supported, yet ssid = /^join ([^ \t]+)/.match(line).try &.[1] wpakeystr = /^join [^ \t]+ wpakey ([^ \t]+)/.match(line).try &.[1] if ssid.nil? puts "wrong SSID in line: #{line}" next end if wpakeystr.nil? puts "wrong wpa key in line: #{line}" next end # TODO new_ap = WirelessAPSetup.new ssid, WirelessAPSetup::WPA.new(wpakeystr) wireless_networks[ssid] = new_ap when /^network [^ \t]+ inet6 autoconf/ puts "TODO: network SSID inet6 autoconf" ssid = /^network ([^ \t]+)/.match(line).try &.[1] ipstr = /^network [^ \t]+ inet6? ([^ \t]+)/.match(line).try &.[1] if ssid.nil? puts "wrong SSID in line: #{line}" next end if ipstr.nil? puts "wrong ip address in line: #{line}" next end # TODO access_point = wireless_networks[ssid].not_nil! when /^network [^ \t]+ inet6? .*/ ssid = nil ipstr = nil /^network (?[^ \t]+) inet6? (?[^ \t]+)/.match(line).try do |m| ssid = m["ssid"] ipstr = m["ip"] end if ssid.nil? puts "wrong SSID in line: #{line}" next end if ipstr.nil? puts "wrong ip address in line: #{line}" next end # TODO ipaddr = IPAddress.parse ipstr # TODO access_point = wireless_networks[ssid].not_nil! if ipaddr.ipv4? access_point.main_ip_v4 = ipaddr elsif ipaddr.ipv6? access_point.main_ip_v6 = ipaddr else puts "wrong ip address in line: #{line} (neither ipv4 or ipv6)" end when /^network [^ ]+ dhcp6?/ ssid = /^network (?[^ \t]+)/.match(line).try &.["ssid"] if ssid.nil? puts "wrong SSID in line: #{line}" next end access_point = wireless_networks[ssid].not_nil! if /dhcp6/.match(line) access_point.main_ip_v6 = DHCP.new elsif /dhcp/.match(line) access_point.main_ip_v4 = DHCP.new else puts "wrong dhcp instruction in line: #{line}" end when /^network [^ ]+ dns .*/ ssid = nil ipstr = nil /^network (?[^ \t]+) dns (?[^ \t]+)/.match(line).try do |m| ssid = m["ssid"] ipstr = m["ip"] end if ssid.nil? puts "wrong SSID in line: #{line}" next end if ipstr.nil? puts "wrong ip address in line: #{line}" next end access_point = wireless_networks[ssid].not_nil! ipaddr = IPAddress.parse ipstr access_point.dns << ipaddr when /^mtu [0-9]+/ mtu = /^mtu ([0-9]+)/.match(line).try &.[1].to_i when /^dns [^ \t]+/ ipstr = nil /^dns (?[^ \t]+)/.match(line).try do |m| ipstr = m["ip"] end if ipstr.nil? puts "wrong ip address in line: #{line}" next end ipaddr = IPAddress.parse ipstr dns << ipaddr when /^#.*$/ # simple comment when /^[ \t]*$/ # empty line else raise "Cannot parse: #{line}" end end InterfaceConfiguration.new(ifname, up, description, mtu, main_ip_v4, main_ip_v6, aliasses, wireless, wireless_networks, dns) end end class Autodetect class_property print_autodetect : Bool = false def self.which(cmd : String) if Process.run("which", [ cmd ]).success? puts "#{cmd} installed" if print_autodetect true else puts "#{cmd} not installed" if print_autodetect false end end end Do.simulation = simulation Autodetect.print_autodetect = print_autodetect # # discover available configuration commands # # ifconfig = *bsd and some linux # ip = linux possible_network_configuration_cmds = { "ifconfig" => NetworkCommands::IfconfigCommand, "ip" => NetworkCommands::IPCommand } # udhcpc = busybox dhcp client possible_dhcp_clients = { "udhcpc" => NetworkCommands::UDHCPCCommand, "dhclient" => NetworkCommands::DHClientCommand } # iw = linux possible_wireless_configuration_cmds = { "iw" => NetworkCommands::IWCommand } key = prefered_network_configuration_program key = possible_network_configuration_cmds.keys.find { |key| Autodetect.which(key) } if key.nil? # should crash if there is no network command installed NetworkCommands.cmd_network_configuration = possible_network_configuration_cmds[key.not_nil!] key = prefered_dhcp_client key = possible_dhcp_clients.keys.find { |key| Autodetect.which(key) } if key.nil? # should not crash if there is no NetworkCommands.cmd_dhcp_client = possible_dhcp_clients[key] unless key.nil? key = prefered_wireless_configuration_program key = possible_wireless_configuration_cmds.keys.find { |key| Autodetect.which(key) } if key.nil? # should crash if there is no wireless command installed NetworkCommands.cmd_wireless_configuration = possible_wireless_configuration_cmds[key.not_nil!] if file.nil? raise "Cannot search for files yet" end case command when "list" # TODO: why having to force "not_nil!" ? Seems like a compiler bug puts NetworkConfigurationParser.parse_file(file.not_nil!) when "connect" # TODO: why having to force "not_nil!" ? Seems like a compiler bug network_configuration = NetworkConfigurationParser.parse_file(file.not_nil!) network_configuration.execute end