summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdam Leff <adam@leff.co>2016-02-19 09:25:17 -0500
committerAdam Leff <adam@leff.co>2016-02-19 09:25:17 -0500
commita15a010c351c12271d090a35c855c5fe8135297c (patch)
treea1f5b76b9b180f2bae89d2c862c103a1fc4730bc
parent8710640580950d52791678a4862e54dbaf225193 (diff)
parentd11b1b9a15d6d0df675dea59950cb8d3669aa788 (diff)
downloadohai-a15a010c351c12271d090a35c855c5fe8135297c.tar.gz
Merge pull request #682 from glennmatthews/linux_network_ipaddress_cornercase
ipaddress on Linux - default route pointing to unaddressed interface, with route src
-rw-r--r--lib/ohai/plugins/linux/network.rb92
-rw-r--r--spec/unit/plugins/linux/network_spec.rb222
2 files changed, 283 insertions, 31 deletions
diff --git a/lib/ohai/plugins/linux/network.rb b/lib/ohai/plugins/linux/network.rb
index 32b77650..b46448ef 100644
--- a/lib/ohai/plugins/linux/network.rb
+++ b/lib/ohai/plugins/linux/network.rb
@@ -113,7 +113,15 @@ Ohai.plugin(:Network) do
# a sanity check, especially for Linux-VServer, OpenVZ and LXC:
# don't report the route entry if the src address isn't set on the node
- next if route_entry[:src] and not iface[route_int][:addresses].has_key? route_entry[:src]
+ # unless the interface has no addresses of this type at all
+ if route_entry[:src]
+ addr = iface[route_int][:addresses]
+ unless addr.nil? || addr.has_key?(route_entry[:src]) ||
+ addr.values.all? { |a| a["family"] != family[:name] }
+ Ohai::Log.debug("Skipping route entry whose src does not match the interface IP")
+ next
+ end
+ end
iface[route_int][:routes] = Array.new unless iface[route_int][:routes]
iface[route_int][:routes] << route_entry
@@ -126,6 +134,7 @@ Ohai.plugin(:Network) do
# for information, default routes can be of this form :
# - default via 10.0.2.4 dev br0
# - default dev br0 scope link
+ # - default dev eth0 scope link src 1.1.1.1
# - default via 10.0.3.1 dev eth1 src 10.0.3.2 metric 10
# - default via 10.0.4.1 dev eth2 src 10.0.4.2 metric 20
@@ -310,46 +319,71 @@ Ohai.plugin(:Network) do
# returns the macaddress for interface from a hash of interfaces (iface elsewhere in this file)
def get_mac_for_interface(interfaces, interface)
- interfaces[interface][:addresses].select { |k, v| v["family"] == "lladdr" }.first.first unless interfaces[interface][:flags].include? "NOARP"
+ interfaces[interface][:addresses].select { |k, v| v["family"] == "lladdr" }.first.first unless interfaces[interface][:addresses].nil? || interfaces[interface][:flags].include?("NOARP")
end
# returns the default route with the lowest metric (unspecified metric is 0)
def choose_default_route(routes)
- default_route = routes.select do |r|
+ routes.select do |r|
r[:destination] == "default"
end.sort do |x, y|
(x[:metric].nil? ? 0 : x[:metric].to_i) <=> (y[:metric].nil? ? 0 : y[:metric].to_i)
end.first
end
+ def interface_has_no_addresses_in_family?(iface, family)
+ return true if iface[:addresses].nil?
+ iface[:addresses].values.all? { |addr| addr["family"] != family }
+ end
+
+ def interface_have_address?(iface, address)
+ return false if iface[:addresses].nil?
+ iface[:addresses].key?(address)
+ end
+
+ def interface_address_not_link_level?(iface, address)
+ iface[:addresses][address][:scope].downcase != "link"
+ end
+
+ def interface_valid_for_route?(iface, address, family)
+ return true if interface_has_no_addresses_in_family?(iface, family)
+
+ interface_have_address?(iface, address) && interface_address_not_link_level?(iface, address)
+ end
+
+ def route_is_valid_default_route?(route, default_route)
+ # if the route destination is a default route, it's good
+ return true if route[:destination] == "default"
+
+ # the default route has a gateway and the route matches the gateway
+ !default_route[:via].nil? && IPAddress(route[:destination]).include?(IPAddress(default_route[:via]))
+ end
+
# ipv4/ipv6 routes are different enough that having a single algorithm to select the favored route for both creates unnecessary complexity
# this method attempts to deduce the route that is most important to the user, which is later used to deduce the favored values for {ip,mac,ip6}address
# we only consider routes that are default routes, or those routes that get us to the gateway for a default route
def favored_default_route(routes, iface, default_route, family)
routes.select do |r|
if family[:name] == "inet"
- # selecting routes for ipv4
- # using the source field when it's specified :
- # 1) in the default route
- # 2) in the route entry used to reach the default gateway
- r[:src] and # it has a src field
- iface[r[:dev]] and # the iface exists
- iface[r[:dev]][:addresses].has_key? r[:src] and # the src ip is set on the node
- iface[r[:dev]][:addresses][r[:src]][:scope].downcase != "link" and # this isn't a link level addresse
- ( r[:destination] == "default" or
- ( default_route[:via] and # the default route has a gateway
- IPAddress(r[:destination]).include? IPAddress(default_route[:via]) # the route matches the gateway
- )
- )
+ # the route must have a source address
+ next if r[:src].nil? || r[:src].empty?
+
+ # the interface specified in the route must exist
+ route_interface = iface[r[:dev]]
+ next if route_interface.nil? # the interface specified in the route must exist
+
+ # the interface must have no addresses, or if it has the source address, the address must not
+ # be a link-level address
+ next unless interface_valid_for_route?(route_interface, r[:src], "inet")
+
+ # the route must either be a default route, or it must have a gateway which is accessible via the route
+ next unless route_is_valid_default_route?(r, default_route)
+
+ true
elsif family[:name] == "inet6"
- # selecting routes for ipv6
- iface[r[:dev]] and # the iface exists
- iface[r[:dev]][:state] == "up" and # the iface is up
- ( r[:destination] == "default" or
- ( default_route[:via] and # the default route has a gateway
- IPAddress(r[:destination]).include? IPAddress(default_route[:via]) # the route matches the gateway
- )
- )
+ iface[r[:dev]] &&
+ iface[r[:dev]][:state] == "up" &&
+ route_is_valid_default_route?(r, default_route)
end
end.sort_by do |r|
# sorting the selected routes:
@@ -430,11 +464,11 @@ Ohai.plugin(:Network) do
default_route = choose_default_route(routes)
if default_route.nil? or default_route.empty?
- attribute_name == if family[:name] == "inet"
- "default_interface"
- else
- "default_#{family[:name]}_interface"
- end
+ attribute_name = if family[:name] == "inet"
+ "default_interface"
+ else
+ "default_#{family[:name]}_interface"
+ end
Ohai::Log.debug("Unable to determine '#{attribute_name}' as no default routes were found for that interface family")
else
network["#{default_prefix}_interface"] = default_route[:dev]
diff --git a/spec/unit/plugins/linux/network_spec.rb b/spec/unit/plugins/linux/network_spec.rb
index 213d0e2d..40bbdc17 100644
--- a/spec/unit/plugins/linux/network_spec.rb
+++ b/spec/unit/plugins/linux/network_spec.rb
@@ -138,6 +138,14 @@ xapi1 Link encap:Ethernet HWaddr E8:39:35:C5:C8:50
TX packets:6 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:21515031 (20.5 MiB) TX bytes:2052 (2.0 KiB)
+
+fwdintf Link encap:Ethernet HWaddr 00:00:00:00:00:0a
+ inet6 addr: fe80::200:ff:fe00:a/64 Scope:Link
+ UP RUNNING NOARP MULTICAST MTU:1496 Metric:1
+ RX packets:0 errors:0 dropped:0 overruns:0 frame:0
+ TX packets:2 errors:0 dropped:1 overruns:0 carrier:0
+ collisions:0 txqueuelen:1000
+ RX bytes:0 (0.0 B) TX bytes:140 (140.0 B)
EOM
# Note that ifconfig shows foo:veth0@eth0 but fails to show any address information.
# This was not a mistake collecting the output and Apparently ifconfig is broken in this regard.
@@ -225,6 +233,8 @@ EOM
link/ether e8:39:35:c5:c8:50 brd ff:ff:ff:ff:ff:ff
inet 192.168.13.34/24 brd 192.168.13.255 scope global xapi1
valid_lft forever preferred_lft forever
+13: fwdintf: <MULTICAST,NOARP,UP,LOWER_UP> mtu 1496 qdisc pfifo_fast state UNKNOWN group default qlen 1000
+ link/ether 00:00:00:00:00:0a brd ff:ff:ff:ff:ff:ff
EOM
}
@@ -278,6 +288,12 @@ EOM
21468183 159866 0 0 0 0
TX: bytes packets errors dropped carrier collsns
2052 6 0 0 0 0
+13: fwdintf: <MULTICAST,NOARP,UP,LOWER_UP> mtu 1496 qdisc pfifo_fast state UNKNOWN mode DEFAULT group default qlen 1000
+ link/ether 00:00:00:00:00:0a brd ff:ff:ff:ff:ff:ff promiscuity 0
+ RX: bytes packets errors dropped overrun mcast
+ 0 0 0 0 0 0
+ TX: bytes packets errors dropped carrier collsns
+ 140 2 0 1 0 0
EOM
}
@@ -359,8 +375,162 @@ EOM
end
end
- %w{ifconfig iproute2}.each do |network_method|
+ describe '#interface_has_no_addresses_in_family?' do
+ context "when interface has no addresses" do
+ let(:iface) { {} }
+
+ it "returns true" do
+ expect(plugin.interface_has_no_addresses_in_family?(iface, "inet")).to eq(true)
+ end
+ end
+
+ context "when an interface has no addresses in family" do
+ let(:iface) { { addresses: { "1.2.3.4" => { "family" => "inet6" } } } }
+
+ it "returns true" do
+ expect(plugin.interface_has_no_addresses_in_family?(iface, "inet")).to eq(true)
+ end
+ end
+
+ context "when an interface has addresses in family" do
+ let(:iface) { { addresses: { "1.2.3.4" => { "family" => "inet" } } } }
+
+ it "returns false" do
+ expect(plugin.interface_has_no_addresses_in_family?(iface, "inet")).to eq(false)
+ end
+ end
+ end
+
+ describe '#interface_have_address?' do
+ context "when interface has no addresses" do
+ let(:iface) { {} }
+
+ it "returns false" do
+ expect(plugin.interface_have_address?(iface, "1.2.3.4")).to eq(false)
+ end
+ end
+
+ context "when interface has a matching address" do
+ let(:iface) { { addresses: { "1.2.3.4" => {} } } }
+
+ it "returns true" do
+ expect(plugin.interface_have_address?(iface, "1.2.3.4")).to eq(true)
+ end
+ end
+
+ context "when interface does not have a matching address" do
+ let(:iface) { { addresses: { "4.3.2.1" => {} } } }
+
+ it "returns false" do
+ expect(plugin.interface_have_address?(iface, "1.2.3.4")).to eq(false)
+ end
+ end
+ end
+
+ describe '#interface_address_not_link_level?' do
+ context "when the address scope is link" do
+ let(:iface) { { addresses: { "1.2.3.4" => { scope: "Link" } } } }
+
+ it "returns false" do
+ expect(plugin.interface_address_not_link_level?(iface, "1.2.3.4")).to eq(false)
+ end
+ end
+
+ context "when the address scope is global" do
+ let(:iface) { { addresses: { "1.2.3.4" => { scope: "Global" } } } }
+
+ it "returns true" do
+ expect(plugin.interface_address_not_link_level?(iface, "1.2.3.4")).to eq(true)
+ end
+ end
+ end
+
+ describe '#interface_valid_for_route?' do
+ let(:iface) { double("iface") }
+ let(:address) { "1.2.3.4" }
+ let(:family) { "inet" }
+
+ context "when interface has no addresses" do
+ it "returns true" do
+ expect(plugin).to receive(:interface_has_no_addresses_in_family?).with(iface, family).and_return(true)
+ expect(plugin.interface_valid_for_route?(iface, address, family)).to eq(true)
+ end
+ end
+
+ context "when interface has addresses" do
+ before do
+ expect(plugin).to receive(:interface_has_no_addresses_in_family?).with(iface, family).and_return(false)
+ end
+
+ context "when interface contains the address" do
+ before do
+ expect(plugin).to receive(:interface_have_address?).with(iface, address).and_return(true)
+ end
+
+ context "when interface address is not a link-level address" do
+ it "returns true" do
+ expect(plugin).to receive(:interface_address_not_link_level?).with(iface, address).and_return(true)
+ expect(plugin.interface_valid_for_route?(iface, address, family)).to eq(true)
+ end
+ end
+
+ context "when the interface address is a link-level address" do
+ it "returns false" do
+ expect(plugin).to receive(:interface_address_not_link_level?).with(iface, address).and_return(false)
+ expect(plugin.interface_valid_for_route?(iface, address, family)).to eq(false)
+ end
+ end
+ end
+
+ context "when interface does not contain the address" do
+ it "returns false" do
+ expect(plugin).to receive(:interface_have_address?).with(iface, address).and_return(false)
+ expect(plugin.interface_valid_for_route?(iface, address, family)).to eq(false)
+ end
+ end
+ end
+ end
+
+ describe '#route_is_valid_default_route?' do
+ context "when the route destination is default" do
+ let(:route) { { destination: "default" } }
+ let(:default_route) { double("default_route") }
+
+ it "returns true" do
+ expect(plugin.route_is_valid_default_route?(route, default_route)).to eq(true)
+ end
+ end
+
+ context "when the route destination is not default" do
+ let(:route) { { destination: "10.0.0.0/24" } }
+
+ context "when the default route does not have a gateway" do
+ let(:default_route) { {} }
+
+ it "returns false" do
+ expect(plugin.route_is_valid_default_route?(route, default_route)).to eq(false)
+ end
+ end
+
+ context "when the gateway is within the destination" do
+ let(:default_route) { { via: "10.0.0.1" } }
+
+ it "returns true" do
+ expect(plugin.route_is_valid_default_route?(route, default_route)).to eq(true)
+ end
+ end
+
+ context "when the gateway is not within the destination" do
+ let(:default_route) { { via: "20.0.0.1" } }
+ it "returns false" do
+ expect(plugin.route_is_valid_default_route?(route, default_route)).to eq(false)
+ end
+ end
+ end
+ end
+
+ %w{ifconfig iproute2}.each do |network_method|
describe "gathering IP layer address info via #{network_method}" do
before(:each) do
allow(plugin).to receive(:iproute2_binary_available?).and_return( network_method == "iproute2" )
@@ -374,7 +544,7 @@ EOM
end
it "detects the interfaces" do
- expect(plugin["network"]["interfaces"].keys.sort).to eq(["eth0", "eth0.11", "eth0.151", "eth0.152", "eth0.153", "eth0:5", "eth3", "foo:veth0@eth0", "lo", "ovs-system", "tun0", "venet0", "venet0:0", "xapi1"])
+ expect(plugin["network"]["interfaces"].keys.sort).to eq(["eth0", "eth0.11", "eth0.151", "eth0.152", "eth0.153", "eth0:5", "eth3", "foo:veth0@eth0", "fwdintf", "lo", "ovs-system", "tun0", "venet0", "venet0:0", "xapi1"])
end
it "detects the layer one details of an ethernet interface" do
@@ -896,6 +1066,54 @@ EOM
end
end
+ describe "with a link level default route to an unaddressed int" do
+ let(:linux_ip_route) { <<-EOM
+default dev eth3 scope link
+EOM
+ }
+
+ before(:each) do
+ plugin.run
+ end
+
+ it "completes the run" do
+ expect(Ohai::Log).not_to receive(:debug).with(/Plugin linux::network threw exception/)
+ expect(plugin["network"]).not_to be_nil
+ end
+
+ it "sets default_interface" do
+ expect(plugin["network"]["default_interface"]).to eq("eth3")
+ end
+
+ it "doesn't set ipaddress" do
+ expect(plugin["ipaddress"]).to be_nil
+ end
+ end
+
+ describe "with a link level default route with a source" do
+ let(:linux_ip_route) { <<-EOM
+default dev fwdintf scope link src 2.2.2.2
+EOM
+ }
+
+ before(:each) do
+ plugin.run
+ end
+
+ it "completes the run" do
+ expect(Ohai::Log).not_to receive(:debug).with(/Plugin linux::network threw exception/)
+ expect(plugin["network"]).not_to be_nil
+ end
+
+ it "sets default_interface" do
+ expect(plugin["network"]["default_interface"]).to eq("fwdintf")
+ end
+
+ it "sets ipaddress" do
+ expect(plugin["ipaddress"]).to eq("2.2.2.2")
+ end
+ end
+
describe "when not having a global scope ipv6 address" do
let(:linux_ip_route_inet6) { <<-EOM
fe80::/64 dev eth0 proto kernel metric 256