diff --git a/src/autodetect_environment.cr b/src/autodetect_environment.cr new file mode 100644 index 0000000..014ca85 --- /dev/null +++ b/src/autodetect_environment.cr @@ -0,0 +1,14 @@ + +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 diff --git a/src/cli.cr b/src/cli.cr new file mode 100644 index 0000000..d4d07b5 --- /dev/null +++ b/src/cli.cr @@ -0,0 +1,170 @@ + +file_option : String? = nil + +OptionParser.parse! do |parser| + parser.on "-s", "--simulation", "Export the network configuration." do + Context.simulation = true + end + + parser.on "-a", "--print-autodetect", "Print autodetection of the installed programs." do + Context.print_autodetect = true + end + + parser.on "-w wireless-configuration-program", "--wireless wireless-configuration-program", "iw" do |prog| + Context.prefered_wireless_configuration_program = prog + end + + parser.on "-n network-configuration-program", "--net-conf network-configuration-program", "ifconfig | ip" do |prog| + Context.prefered_network_configuration_program = prog + end + + parser.on "-d dhcp-client-program", "--dhcp-client dhcp-client-program", "udhcpc | dhclient" do |prog| + Context.prefered_dhcp_client = prog + end + + parser.on "-r root", "--root root", "Root where to search for /etc/hostname.* files." do |optsn| + Context.root = optsn + end + + parser.on "-f file", "--file file", "Parse a configuration file." do |optsn| + file_option = 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| + Context.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| + Context.command = arg.shift + Context.args = arg + + case Context.command + when /^(list)/ + when /^(up)/ + when /^(down)/ + when /^(scan)/ + else + STDERR.puts "Command #{Context.command} not understood" + exit 1 + 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 + + +Do.simulation = Context.simulation +Autodetect.print_autodetect = Context.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, + "ifconfig" => NetworkCommands::IfconfigCommand +} + +key = Context.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 = Context.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 = Context.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!] + + + +files = Array(String).new +Dir.children("#{Context.root}/etc/").each do |f| + if /^hostname\./.match(f) + files << f + end +end + +interface_files = Array(String).new + +if ! file_option.nil? + # file passed via the '-f' option + # TODO: why having to force "not_nil!" ? Seems like a compiler bug + interface_files << file_option.not_nil! +elsif Context.args.empty? + # every configured interface + files.each do |f| + interface_files << "#{Context.root}/etc/#{f}" + end +else + # only interfaces in arguments + Context.args.each do |interface| + interface_files << "#{Context.root}/etc/hostname.#{interface}" + end +end + + + +begin + case Context.command + when "list" + interface_files.each do |f| + puts NetworkConfigurationParser.parse_file(f) + end + when "up" + interface_files.each do |f| + network_configuration = NetworkConfigurationParser.parse_file(f) + network_configuration.execute + end + when "down" + interface_files.each do |f| + network_configuration = NetworkConfigurationParser.parse_file(f) + network_configuration.down + end + when "scan" + interface_files.each do |f| + network_configuration = NetworkConfigurationParser.parse_file(f) + network_configuration.scan + end + end +rescue e + STDERR.puts "#{CRED}Exception: #{CRESET}#{e}" +end diff --git a/src/configuration.cr b/src/configuration.cr new file mode 100644 index 0000000..ebafe4c --- /dev/null +++ b/src/configuration.cr @@ -0,0 +1,292 @@ + +class NotSetup + def to_s(io : IO) + io << "not setup" + end +end + +class Autoconfiguration + def to_s(io : IO) + io << "autoconfiguration" + end +end + +class DHCP + def to_s(io : IO) + io << "dhcp" + 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 | Autoconfiguration | 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" + + aliasses_v4.each do |a| + str << "\t\talias #{a}\n" + end + end + + # ipv6 + unless main_ip_v6.is_a?(NotSetup) + str << "\t\tinet6 #{main_ip_v6}\n" + + @aliasses_v6.each do |a| + str << "\t\talias6 #{a}\n" + end + end + + dns.each do |ip| + str << "\t\tdns: #{ip}\n" + 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 | Autoconfiguration | 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" + + aliasses_v4.each do |a| + str << "\talias #{a}\n" + end + end + + # warning: alias but no main ip address + if main_ip_v4.is_a?(NotSetup) && ! aliasses_v4.empty? + str << "\t#{CRED}alias configured but no main ipv4 configuration.#{CRESET}\n" + str << "\t#{CRED}Should main ipv4 be obtained from DHCP? Static configuration?#{CRESET}\n" + end + + # ipv6 + unless main_ip_v6.is_a?(NotSetup) + str << "\tinet6 #{main_ip_v6}\n" + + aliasses_v6.each do |a| + str << "\talias6 #{a}\n" + end + end + + # warning: alias but no main ip address + if main_ip_v6.is_a?(NotSetup) && ! aliasses_v6.empty? + str << "\t#{CRED}alias6 configured but no main ipv6 configuration.#{CRESET}\n" + str << "\t#{CRED}Should main ipv6 be obtained from autoconfiguration? DHCP? Static configuration?#{CRESET}\n" + 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 + + # TODO: treat differently wireless and non-wireless interfaces + if @wireless + puts "interface #{name} is wireless" + puts "TODO:" + puts "1. scan for SSID" + puts "2. select configured SSID then try to connect" + puts "3. connectivity check with the gateway" + else + puts "interface #{name} is not wireless" + 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 + # do nothing + 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 + when Autoconfiguration + NetworkCommands.autoconfiguration name + when DHCP + NetworkCommands.dhcp6 name + when NotSetup + # do nothing + else + raise "ipv6 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 + + def down + unless NetworkCommands.interface_exists(@name) + raise "The interface #{@name} doesn't exists or is already down." + end + + NetworkCommands.flush name + NetworkCommands.down name + end + + def scan + unless NetworkCommands.interface_exists(@name) + raise "The interface #{@name} doesn't exists or is already down." + end + + ssid_list = NetworkCommands.scan name + pp! ssid_list + end +end diff --git a/src/context.cr b/src/context.cr new file mode 100644 index 0000000..3dfb2de --- /dev/null +++ b/src/context.cr @@ -0,0 +1,17 @@ + +class Context + class_property root : String = "/" + + class_property simulation = false + class_property verbosity = 1 + + class_property prefered_network_configuration_program : String? = nil + class_property prefered_wireless_configuration_program : String? = nil + class_property prefered_dhcp_client : String? = nil + + class_property root = "/" + class_property print_autodetect = false + + class_property command = "list" + class_property args = Array(String).new +end diff --git a/src/do.cr b/src/do.cr new file mode 100644 index 0000000..6a2ed9c --- /dev/null +++ b/src/do.cr @@ -0,0 +1,22 @@ + +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 + + def self.run(cmd : String, params : Array(String) = nil, &block : Process -> _) + if @@simulation + puts "simulation, do: #{cmd} #{params.join(" ")}" + Process::Status.new 0 + else + Process.run cmd, params, &block + end + end +end diff --git a/src/main.cr b/src/main.cr index 66741cd..5ba3392 100644 --- a/src/main.cr +++ b/src/main.cr @@ -2,997 +2,10 @@ require "option_parser" require "ipaddress" require "./colors" - -class Context - class_property root : String = "/" - - class_property simulation = false - class_property verbosity = 1 - - class_property prefered_network_configuration_program : String? = nil - class_property prefered_wireless_configuration_program : String? = nil - class_property prefered_dhcp_client : String? = nil - - class_property root = "/" - class_property print_autodetect = false - - class_property command = "list" - class_property args = Array(String).new -end - -file_option : String? = nil - -OptionParser.parse! do |parser| - parser.on "-s", "--simulation", "Export the network configuration." do - Context.simulation = true - end - - parser.on "-a", "--print-autodetect", "Print autodetection of the installed programs." do - Context.print_autodetect = true - end - - parser.on "-w wireless-configuration-program", "--wireless wireless-configuration-program", "iw" do |prog| - Context.prefered_wireless_configuration_program = prog - end - - parser.on "-n network-configuration-program", "--net-conf network-configuration-program", "ifconfig | ip" do |prog| - Context.prefered_network_configuration_program = prog - end - - parser.on "-d dhcp-client-program", "--dhcp-client dhcp-client-program", "udhcpc | dhclient" do |prog| - Context.prefered_dhcp_client = prog - end - - parser.on "-r root", "--root root", "Root where to search for /etc/hostname.* files." do |optsn| - Context.root = optsn - end - - parser.on "-f file", "--file file", "Parse a configuration file." do |optsn| - file_option = 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| - Context.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| - Context.command = arg.shift - Context.args = arg - - case Context.command - when /^(list)/ - when /^(up)/ - when /^(down)/ - when /^(scan)/ - else - STDERR.puts "Command #{Context.command} not understood" - exit 1 - 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 - - def self.run(cmd : String, params : Array(String) = nil, &block : Process -> _) - if @@simulation - puts "simulation, do: #{cmd} #{params.join(" ")}" - Process::Status.new 0 - else - Process.run cmd, params, &block - end - end -end - -class NotSetup - def to_s(io : IO) - io << "not setup" - end -end - -class Autoconfiguration - def to_s(io : IO) - io << "autoconfiguration" - 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 : IfconfigCommand.class | IWCommand.class | NotSetup.class = NotSetup - class_property cmd_dhcp_client : UDHCPCCommand.class | DHClientCommand.class | NotSetup.class = NotSetup - - class DNS - def initialize(@addresses : Array(String), @search : Array(String)) - end - - def execute - File.open("#{Context.root}/etc/resolv.conf", "w") do |file| - @addresses.each do |address| - file.puts "nameserver #{address}\n" - end - - file.puts "search #{@search.join(" ")}" - end - end - end - - class IWCommand - # get the available SSID - def self.scan(ifname : String) : Array(String) - ssids = Array(String).new - - Do.run("iw", [ ifname, "scan" ]) do |p| - p.output.each_line do |line| - ssid = /SSID: (?[a-zA-Z0-9_-]+)/.match(line).try &.["ssid"] - - unless ssid.nil? - ssids << ssid - end - end - end - - if ssids.empty? - raise "(iw) cannot get ssid list from #{ifname}" - end - - ssids - end - end - - class UDHCPCCommand - def self.run(ifname : String) - unless Do.run("udhcpc", [ ifname ]).success? - raise "(udhcpc) dhcp failed on #{ifname}" - end - end - - def self.dhcp6(ifname : String) - # we suppose udhcpc6 installed along with udhcpc - unless Do.run("udhcpc6", [ ifname ]).success? - raise "(udhcpc6) dhcp failed on #{ifname}" - end - end - end - - class DHClientCommand - def self.run(ifname : String) - unless Do.run("dhclient", [ ifname ]).success? - raise "(dhclient) dhcp failed on #{ifname}" - end - end - - def self.dhcp6(ifname : String) - unless Do.run("dhclient", [ "-6", ifname ]).success? - raise "(dhclient) dhcp6 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, 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_s ]).success? - raise "(ifconfig) Cannot set ip address alias #{ip.to_s} 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 - - def self.flush(name : String) - unless Do.run("ifconfig", [ name, "0.0.0.0" ]).success? - raise "(ifconfig) Cannot flush #{name} ip addresses" - end - end - - def self.down(name : String) - unless Do.run("ifconfig", [ name, "down" ]).success? - raise "(ifconfig) Cannot set down #{name}" - end - end - - # ifconfig also performs wireless configuration on some OSs - def self.scan(name : String) - puts "TODO: (ifconfig) ifconfig ifname scan | grep SSID" - 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.flush(name : String) - unless Do.run("ip", [ "address", "flush", "dev", name ]).success? - raise "(ip) Cannot flush #{name} ip addresses" - end - end - - def self.down(name : String) - unless Do.run("ip", [ "link", "set", "down", "dev", name ]).success? - raise "(ip) Cannot set down #{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.class - puts "no dhcp client: cannot perform dhcp on #{name}" - else - 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.flush(name : String) - @@cmd_network_configuration.flush name - end - - def self.down(name : String) - @@cmd_network_configuration.down name - end - - def self.scan(ifname : String) - cmd = @@cmd_wireless_configuration - case cmd - when NotSetup.class - puts "no wireless configuration program: cannot list ssid" - else - cmd.scan ifname - end - end - - def self.autoconfiguration(ifname : String) - puts "TODO: IPv6 autoconfiguration setup" - puts "sysctl -w net.ipv6.conf.#{ifname}.autoconf=1" - puts "sysctl -w net.ipv6.conf.#{ifname}.accept_ra=1" - end - - def self.dhcp6(ifname : String) - puts "TODO: dhcpv6" - @@cmd_dhcp_client.dhcp6 ifname - end - - def self.wireless_connect_wpa_psk(ifname : String, ssid : String, passwd : String) - cmd = @@cmd_wireless_configuration - case cmd - when NotSetup.class - puts "no wireless configuration program: cannot connect to ssid #{ssid}" - else - 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 | Autoconfiguration | 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" - - aliasses_v4.each do |a| - str << "\t\talias #{a}\n" - end - end - - # ipv6 - unless main_ip_v6.is_a?(NotSetup) - str << "\t\tinet6 #{main_ip_v6}\n" - - @aliasses_v6.each do |a| - str << "\t\talias6 #{a}\n" - end - end - - dns.each do |ip| - str << "\t\tdns: #{ip}\n" - 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 | Autoconfiguration | 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" - - aliasses_v4.each do |a| - str << "\talias #{a}\n" - end - end - - # warning: alias but no main ip address - if main_ip_v4.is_a?(NotSetup) && ! aliasses_v4.empty? - str << "\t#{CRED}alias configured but no main ipv4 configuration.#{CRESET}\n" - str << "\t#{CRED}Should main ipv4 be obtained from DHCP? Static configuration?#{CRESET}\n" - end - - # ipv6 - unless main_ip_v6.is_a?(NotSetup) - str << "\tinet6 #{main_ip_v6}\n" - - aliasses_v6.each do |a| - str << "\talias6 #{a}\n" - end - end - - # warning: alias but no main ip address - if main_ip_v6.is_a?(NotSetup) && ! aliasses_v6.empty? - str << "\t#{CRED}alias6 configured but no main ipv6 configuration.#{CRESET}\n" - str << "\t#{CRED}Should main ipv6 be obtained from autoconfiguration? DHCP? Static configuration?#{CRESET}\n" - 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 - - # TODO: treat differently wireless and non-wireless interfaces - if @wireless - puts "interface #{name} is wireless" - puts "TODO:" - puts "1. scan for SSID" - puts "2. select configured SSID then try to connect" - puts "3. connectivity check with the gateway" - else - puts "interface #{name} is not wireless" - 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 - # do nothing - 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 - when Autoconfiguration - NetworkCommands.autoconfiguration name - when DHCP - NetworkCommands.dhcp6 name - when NotSetup - # do nothing - else - raise "ipv6 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 - - def down - unless NetworkCommands.interface_exists(@name) - raise "The interface #{@name} doesn't exists or is already down." - end - - NetworkCommands.flush name - NetworkCommands.down name - end - - def scan - unless NetworkCommands.interface_exists(@name) - raise "The interface #{@name} doesn't exists or is already down." - end - - ssid_list = NetworkCommands.scan name - pp! ssid_list - 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 - main_ip_v6 = Autoconfiguration.new - - 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 - - access_point = wireless_networks[ssid].not_nil! - access_point.main_ip_v6 = Autoconfiguration.new - puts "for SSID: #{ssid} ipv6 configuration = autoconf" - - 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 - - ipaddr = IPAddress.parse ipstr - 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 = Context.simulation -Autodetect.print_autodetect = Context.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, - "ifconfig" => NetworkCommands::IfconfigCommand -} - -key = Context.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 = Context.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 = Context.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!] - - - -files = Array(String).new -Dir.children("#{Context.root}/etc/").each do |f| - if /^hostname\./.match(f) - files << f - end -end - -interface_files = Array(String).new - -if ! file_option.nil? - # file passed via the '-f' option - # TODO: why having to force "not_nil!" ? Seems like a compiler bug - interface_files << file_option.not_nil! -elsif Context.args.empty? - # every configured interface - files.each do |f| - interface_files << "#{Context.root}/etc/#{f}" - end -else - # only interfaces in arguments - Context.args.each do |interface| - interface_files << "#{Context.root}/etc/hostname.#{interface}" - end -end - - - -begin - case Context.command - when "list" - interface_files.each do |f| - puts NetworkConfigurationParser.parse_file(f) - end - when "up" - interface_files.each do |f| - network_configuration = NetworkConfigurationParser.parse_file(f) - network_configuration.execute - end - when "down" - interface_files.each do |f| - network_configuration = NetworkConfigurationParser.parse_file(f) - network_configuration.down - end - when "scan" - interface_files.each do |f| - network_configuration = NetworkConfigurationParser.parse_file(f) - network_configuration.scan - end - end -rescue e - STDERR.puts "#{CRED}Exception: #{CRESET}#{e}" -end +require "./context" +require "./network_commands" +require "./network_configuration_parser" +require "./do" +require "./configuration" +require "./autodetect_environment" +require "./cli" diff --git a/src/network_commands.cr b/src/network_commands.cr new file mode 100644 index 0000000..1365282 --- /dev/null +++ b/src/network_commands.cr @@ -0,0 +1,280 @@ + +# TODO: OpenBSD: no '-w' parameter for sysctl +# TODO: OpenBSD: ifconfig scan +# TODO: Linux: description with ip-* command family + +class NetworkCommands + class_property cmd_network_configuration : IfconfigCommand.class | IPCommand.class = IfconfigCommand + class_property cmd_wireless_configuration : IfconfigCommand.class | IWCommand.class | NotSetup.class = NotSetup + class_property cmd_dhcp_client : UDHCPCCommand.class | DHClientCommand.class | NotSetup.class = NotSetup + + class DNS + def initialize(@addresses : Array(String), @search : Array(String)) + end + + def execute + File.open("#{Context.root}/etc/resolv.conf", "w") do |file| + @addresses.each do |address| + file.puts "nameserver #{address}\n" + end + + file.puts "search #{@search.join(" ")}" + end + end + end + + class IWCommand + # get the available SSID + def self.scan(ifname : String) : Array(String) + ssids = Array(String).new + + Do.run("iw", [ ifname, "scan" ]) do |p| + p.output.each_line do |line| + ssid = /SSID: (?[a-zA-Z0-9_-]+)/.match(line).try &.["ssid"] + + unless ssid.nil? + ssids << ssid + end + end + end + + if ssids.empty? + raise "(iw) cannot get ssid list from #{ifname}" + end + + ssids + end + end + + class UDHCPCCommand + def self.run(ifname : String) + unless Do.run("udhcpc", [ ifname ]).success? + raise "(udhcpc) dhcp failed on #{ifname}" + end + end + + def self.dhcp6(ifname : String) + # we suppose udhcpc6 installed along with udhcpc + unless Do.run("udhcpc6", [ ifname ]).success? + raise "(udhcpc6) dhcp failed on #{ifname}" + end + end + end + + class DHClientCommand + def self.run(ifname : String) + unless Do.run("dhclient", [ ifname ]).success? + raise "(dhclient) dhcp failed on #{ifname}" + end + end + + def self.dhcp6(ifname : String) + unless Do.run("dhclient", [ "-6", ifname ]).success? + raise "(dhclient) dhcp6 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, 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_s ]).success? + raise "(ifconfig) Cannot set ip address alias #{ip.to_s} 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 + + def self.flush(name : String) + unless Do.run("ifconfig", [ name, "0.0.0.0" ]).success? + raise "(ifconfig) Cannot flush #{name} ip addresses" + end + end + + def self.down(name : String) + unless Do.run("ifconfig", [ name, "down" ]).success? + raise "(ifconfig) Cannot set down #{name}" + end + end + + # ifconfig also performs wireless configuration on some OSs + def self.scan(name : String) + puts "TODO: (ifconfig) ifconfig ifname scan | grep SSID" + 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.flush(name : String) + unless Do.run("ip", [ "address", "flush", "dev", name ]).success? + raise "(ip) Cannot flush #{name} ip addresses" + end + end + + def self.down(name : String) + unless Do.run("ip", [ "link", "set", "down", "dev", name ]).success? + raise "(ip) Cannot set down #{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.class + raise "no dhcp client: cannot perform dhcp on #{name}" + else + 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.flush(name : String) + @@cmd_network_configuration.flush name + end + + def self.down(name : String) + @@cmd_network_configuration.down name + end + + def self.scan(ifname : String) + cmd = @@cmd_wireless_configuration + case cmd + when NotSetup.class + raise "no wireless configuration program: cannot list ssid" + else + cmd.scan ifname + end + end + + def self.autoconfiguration(ifname : String) + # TODO: no '-w' option on openbsd + unless Do.run("sysctl", [ "-w", "net.ipv6.conf.#{ifname}.autoconf=1" ]).success? + raise "(sysctl) cannot set 'net.ipv6.conf.#{ifname}.autoconf=1'" + end + + unless Do.run("sysctl", [ "-w", "net.ipv6.conf.#{ifname}.accept_ra=1" ]).success? + raise "(sysctl) cannot set 'net.ipv6.conf.#{ifname}.accept_ra=1'" + end + end + + def self.dhcp6(ifname : String) + cmd = @@cmd_dhcp_client + case cmd + when NotSetup.class + raise "no dhcpv6 program" + else + cmd.dhcp6 ifname + end + end + + def self.wireless_connect_wpa_psk(ifname : String, ssid : String, passwd : String) + cmd = @@cmd_wireless_configuration + case cmd + when NotSetup.class + puts "no wireless configuration program: cannot connect to ssid #{ssid}" + else + cmd.list_ssid ifname + end + end +end diff --git a/src/network_configuration_parser.cr b/src/network_configuration_parser.cr new file mode 100644 index 0000000..4c3f33b --- /dev/null +++ b/src/network_configuration_parser.cr @@ -0,0 +1,210 @@ + +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 + main_ip_v6 = Autoconfiguration.new + + 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 + + access_point = wireless_networks[ssid].not_nil! + access_point.main_ip_v6 = Autoconfiguration.new + puts "for SSID: #{ssid} ipv6 configuration = autoconf" + + 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 + + ipaddr = IPAddress.parse ipstr + 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