diff --git a/.github/workflows/candidate.yml b/.github/workflows/candidate.yml index 7489716..8191319 100644 --- a/.github/workflows/candidate.yml +++ b/.github/workflows/candidate.yml @@ -300,10 +300,7 @@ jobs: echo "value=$actual_client_sha" >> "$GITHUB_OUTPUT" - name: Resolve the private Docker host transport run: | - probe_host="$(ip -4 route show default | awk '/^default / {print $3; exit}')" - ruby -ripaddr -e \ - 'address = IPAddr.new(ARGV.fetch(0)); exit(address.ipv4? && address.private? ? 0 : 1)' \ - "$probe_host" + probe_host="$(ruby scripts/private_default_gateway.rb)" echo "OPENCODE_PROBE_HOST=$probe_host" >> "$GITHUB_ENV" - name: Exercise the complete manifest matrix without artifact claims env: diff --git a/scripts/private_default_gateway.rb b/scripts/private_default_gateway.rb new file mode 100755 index 0000000..085b52f --- /dev/null +++ b/scripts/private_default_gateway.rb @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "ipaddr" + +route_path = ARGV.fetch(0, "/proc/net/route") +routes = File.foreach(route_path).drop(1).filter_map do |line| + fields = line.split + next unless fields.length >= 8 + next unless fields.fetch(1) == "00000000" + next unless (fields.fetch(3).to_i(16) & 0x2) == 0x2 + + fields +end + +routes.sort_by { |fields| fields.fetch(6).to_i }.each do |fields| + gateway_hex = fields.fetch(2) + next unless gateway_hex.match?(/\A[0-9A-Fa-f]{8}\z/) + + gateway = gateway_hex.scan(/../).reverse.map { |octet| octet.to_i(16) }.join(".") + address = IPAddr.new(gateway) + next unless address.ipv4? && address.private? + + puts address + exit 0 +end + +abort "no private IPv4 default-route gateway found in #{route_path}" diff --git a/test/image_contract_evidence_test.rb b/test/image_contract_evidence_test.rb index 9f1ca78..d3b80bf 100644 --- a/test/image_contract_evidence_test.rb +++ b/test/image_contract_evidence_test.rb @@ -4,11 +4,13 @@ require "fileutils" require "json" require "minitest/autorun" require "open3" +require "rbconfig" require "tmpdir" class ImageContractEvidenceTest < Minitest::Test ROOT = File.expand_path("..", __dir__) RUNNER = File.join(ROOT, "scripts/run_image_contract.sh") + GATEWAY_RESOLVER = File.join(ROOT, "scripts/private_default_gateway.rb") VALID_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'a' * 64}" def setup @@ -125,6 +127,32 @@ class ImageContractEvidenceTest < Minitest::Test end end + def test_resolves_a_private_default_gateway_from_linux_route_hex + route_path = File.join(@tmp, "route") + File.write(route_path, <<~ROUTES) + Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT + eth0 00000000 010011AC 0003 0 0 10 00000000 0 0 0 + ROUTES + + output, error, status = Open3.capture3(RbConfig.ruby, GATEWAY_RESOLVER, route_path) + + assert status.success?, error + assert_equal "172.17.0.1\n", output + end + + def test_gateway_resolver_rejects_a_public_default_gateway + route_path = File.join(@tmp, "public-route") + File.write(route_path, <<~ROUTES) + Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT + eth0 00000000 08080808 0003 0 0 10 00000000 0 0 0 + ROUTES + + _output, error, status = Open3.capture3(RbConfig.ruby, GATEWAY_RESOLVER, route_path) + + refute status.success? + assert_match(/no private IPv4 default-route gateway found/, error) + end + private def base_environment(checkout, commit, evidence_path) diff --git a/test/repository_test.rb b/test/repository_test.rb index 007dbf2..7064d37 100644 --- a/test/repository_test.rb +++ b/test/repository_test.rb @@ -287,8 +287,7 @@ class RepositoryTest < Minitest::Test assert_includes workflow, "exact-image-contract-gitea:" assert_includes workflow, "if: github.server_url != 'https://github.com'" assert_includes workflow, "run: scripts/run_image_matrix_contract.sh" - assert_includes workflow, "ip -4 route show default" - assert_includes workflow, "address.ipv4? && address.private?" + assert_includes workflow, 'probe_host="$(ruby scripts/private_default_gateway.rb)"' assert_includes workflow, 'echo "OPENCODE_PROBE_HOST=$probe_host" >> "$GITHUB_ENV"' assert_operator workflow.scan('BUNDLE_GEMFILE: ${{ github.workspace }}/ruby-client/Gemfile').length, :>=, 2 assert_operator workflow.scan("generated JSON is transient and is not review evidence").length, :>=, 3