networkctl/src/main.cr

747 lines
18 KiB
Crystal

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)
# 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
end
def to_s(io : IO)
io << to_string
end
def to_string
String.build do |str|
str << "#{CBLUE}#{ssid}: {ssid}#{CRESET}\n"
str << "\t#description #{description.not_nil!}\n" unless description.nil?
str << "\t#{@up? "up" : "down"}\n"
str << "\tmtu #{mtu}\n" unless mtu.nil?
# ipv4
str << "\tinet #{@main_ip_v4}\n"
@aliasses_v4.each do |a|
str << "\talias #{a}\n"
end
# ipv6
str << "\tinet6 #{@main_ip_v6}\n"
unless @aliasses_v6.empty?
@aliasses_v6.each do |a|
str << "\talias6 #{a}\n"
end
end
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)
def initialize (@name, @up,
@description,
@mtu,
@main_ip_v4, @main_ip_v6, aliasses,
@wireless, @wireless_networks)
@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
str << "\tinet #{@main_ip_v4}\n"
unless @aliasses_v4.empty?
@aliasses_v4.each do |a|
str << "\talias #{a}\n"
end
end
# ipv6
str << "\tinet6 #{@main_ip_v6}\n"
unless @aliasses_v6.empty?
@aliasses_v6.each do |a|
str << "\talias6 #{a}\n"
end
end
if wireless_networks.empty?
puts "no wireless connection configured" if wireless
else
wireless_networks.each do |k,v|
puts 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
mtu = @mtu
unless mtu.nil?
NetworkCommands.mtu @name, mtu
end
description = @description
unless description.nil?
NetworkCommands.description @name, description
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
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/
# IPaddress is DHCP
if /^inet /.match(line)
main_ip_v4 = DHCP.new
else
main_ip_v6 = DHCP.new
end
when /^inet6 autoconf/
# IPaddress 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 (?<ssid>[^ \t]+) inet6? (?<ip>[^ \t]+)/.match(line).try do |m|
ssid = m["ssid"]
ipstr = m["ip"]
end
# ssid = /^network ([^ \t]+)/.match(line).try &.[1]
# ipstr = /^network [^ \t]+ inet6? (?<ip>[^ \t]+)/.match(line).try &.["ip"]
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 &.[1]
if ssid.nil?
puts "wrong SSID in line: #{line}"
next
end
# TODO
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 .*/
puts "TODO: network SSID dns"
when /^mtu [0-9]+/
mtu = /^mtu ([0-9]+)/.match(line).try &.[1].to_i
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)
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