Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,28 @@ jobs:
- name: Install dependencies
run: mise dev:deps

- name: Start the test SNMP agent
run: |
sudo apt-get update -q
sudo apt-get install -y -q snmpd
SNMP_PERSISTENT_DIR="$RUNNER_TEMP/snmpd-persist" /usr/sbin/snmpd -C -c spec/support/snmpd.conf -Lo -f &
sleep 1

- name: Run tests
run: mise dev:spec

- name: Run tests (multi-threaded)
run: mise dev:spec-mt

- name: Run tests (OpenSSL legacy / DES)
run: mise dev:spec-legacy

- name: Run tests (live agent)
run: mise dev:spec-e2e
env:
TEST_SNMP_SERVER: 127.0.0.1
TEST_SNMP_PORT: "16161"

# Best-effort early warning against the Crystal nightly compiler. mise has no
# nightly build, so this job uses the nightly container directly and is allowed
# to fail. The e2e (live server) and legacy (OpenSSL legacy provider) specs are
Expand Down Expand Up @@ -125,8 +141,23 @@ jobs:
- name: Install dependencies
run: mise dev:deps

- name: Start the test SNMP agent
run: |
brew install net-snmp
SNMP_PERSISTENT_DIR="$RUNNER_TEMP/snmpd-persist" "$(brew --prefix net-snmp)/sbin/snmpd" -C -c spec/support/snmpd.conf -Lo -f &
sleep 1

- name: Run tests
run: mise dev:spec

- name: Run tests (multi-threaded)
run: mise dev:spec-mt

- name: Run tests (OpenSSL legacy / DES)
run: mise dev:spec-legacy

- name: Run tests (live agent)
run: mise dev:spec-e2e
env:
TEST_SNMP_SERVER: 127.0.0.1
TEST_SNMP_PORT: "16161"
13 changes: 12 additions & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,25 @@ description = "Run the deterministic spec suite (excludes e2e / legacy)"
run = "crystal spec --error-trace --tag '~e2e' --tag '~legacy' {{vars.SPEC_OPTS}}"

[tasks."dev:spec-e2e"]
description = "Run the live spec suite against a real SNMP server (set TEST_SNMP_SERVER)"
description = "Run the live spec suite against a real SNMP server (set TEST_SNMP_SERVER / TEST_SNMP_PORT)"
# The legacy provider is activated so the SNMPv3 DES live spec can encrypt (des-cbc).
env = { OPENSSL_CONF = "{{config_root}}/.mise/openssl-legacy.cnf" }
run = "crystal spec --error-trace --tag e2e {{vars.SPEC_OPTS}}"

[tasks."dev:spec-legacy"]
description = "Run specs needing OpenSSL legacy algorithms (DES); activates the OpenSSL legacy provider"
env = { OPENSSL_CONF = "{{config_root}}/.mise/openssl-legacy.cnf" }
run = "crystal spec --error-trace --tag legacy {{vars.SPEC_OPTS}}"

[tasks."dev:snmpd"]
description = "Run the test SNMP agent on 127.0.0.1:16161 (foreground; pair with dev:spec-e2e)"
run = """
SNMPD=$(command -v snmpd || true)
[ -x "$SNMPD" ] || SNMPD=/opt/homebrew/opt/net-snmp/sbin/snmpd
[ -x "$SNMPD" ] || SNMPD=/usr/sbin/snmpd
SNMP_PERSISTENT_DIR=$(mktemp -d) exec "$SNMPD" -C -c {{config_root}}/spec/support/snmpd.conf -Lo -f
"""

[tasks."dev:spec-mt"]
description = "Run the deterministic spec suite multi-threaded (fiber-safety gate)"
run = "crystal spec --error-trace -Dpreview_mt --tag '~e2e' --tag '~legacy' {{vars.SPEC_OPTS}}"
Expand Down
4 changes: 2 additions & 2 deletions spec/client_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ describe SNMP::Client do
end

it "should perform a walk", tags: "e2e" do
client = SNMP::Client.new(TEST_SNMP_SERVER)
client = SNMP::Client.new(TEST_SNMP_SERVER, port: TEST_SNMP_PORT)
client.should_not be_nil
messages = client.walk("1.3.6.1.2.1.1.9.1.3")
messages.should be_a(Array(SNMP::Message))
messages.empty?.should be_false
end

it "should perform a walk using a block", tags: "e2e" do
client = SNMP::Client.new(TEST_SNMP_SERVER)
client = SNMP::Client.new(TEST_SNMP_SERVER, port: TEST_SNMP_PORT)
client.should_not be_nil
messages = [] of SNMP::Message
client.walk("1.3.6.1.2.1.1.9.1.3") do |message|
Expand Down
22 changes: 22 additions & 0 deletions spec/data_types_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,26 @@ describe SNMP do
SNMP.get_unsigned32(ber).should eq(UInt32::MAX)
end
end

describe ".set_unsigned64" do
it "pads to 8 bytes by default and round-trips" do
ber = SNMP.set_unsigned64(0x0102_u64)
ber.payload.size.should eq(8)
SNMP.get_unsigned64(ber).should eq(0x0102_u64)
end

it "strips leading zero bytes with padding: false" do
ber = SNMP.set_unsigned64(0x0102_u64, padding: false)
ber.payload.should eq(Bytes[0x01, 0x02])
SNMP.get_unsigned64(ber).should eq(0x0102_u64)
end
end

describe ".set_unsigned32" do
it "strips leading zero bytes with padding: false" do
ber = SNMP.set_unsigned32(0x7F_u32, padding: false)
ber.payload.should eq(Bytes[0x7F])
SNMP.get_unsigned32(ber).should eq(0x7F_u32)
end
end
end
2 changes: 2 additions & 0 deletions spec/helper.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require "spec"

TEST_SNMP_SERVER = ENV["TEST_SNMP_SERVER"]? || "localhost"
# Port of the live test agent — the CI starts an unprivileged snmpd on a high port.
TEST_SNMP_PORT = (ENV["TEST_SNMP_PORT"]? || "161").to_i

require "../src/snmp"
40 changes: 15 additions & 25 deletions spec/snmp_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,10 @@ describe SNMP do
io.to_slice.hexstring.should eq(output)
end

it "should be able to query SNMPLabs with MD5 auth", tags: "e2e" do
pending!("not sure how to configure SNMPv3 on the simulator")

it "should be able to query the live agent with MD5 auth", tags: "e2e" do
# Connect to server
socket = UDPSocket.new
socket.connect(TEST_SNMP_SERVER, 161)
socket.connect(TEST_SNMP_SERVER, TEST_SNMP_PORT)
socket.sync = false
socket.read_timeout = 3.seconds

Expand All @@ -294,12 +292,10 @@ describe SNMP do
response.value.get_string.should eq("SNMP Laboratories, info@snmplabs.com")
end

it "should be able to query SNMPLabs with MD5 and AES auth", tags: "e2e" do
pending!("not sure how to configure SNMPv3 on the simulator")

it "should be able to query the live agent with MD5 and AES auth", tags: "e2e" do
# Connect to server
socket = UDPSocket.new
socket.connect(TEST_SNMP_SERVER, 161)
socket.connect(TEST_SNMP_SERVER, TEST_SNMP_PORT)
socket.sync = false
socket.read_timeout = 3.seconds

Expand All @@ -321,12 +317,10 @@ describe SNMP do
response.value.get_string.should eq("SNMP Laboratories, info@snmplabs.com")
end

it "should be able to query SNMPLabs with MD5 and DES auth", tags: "e2e" do
pending!("not sure how to configure SNMPv3 on the simulator")

it "should be able to query the live agent with MD5 and DES auth", tags: "e2e" do
# Connect to server
socket = UDPSocket.new
socket.connect(TEST_SNMP_SERVER, 161)
socket.connect(TEST_SNMP_SERVER, TEST_SNMP_PORT)
socket.sync = false
socket.read_timeout = 3.seconds

Expand All @@ -348,10 +342,10 @@ describe SNMP do
response.value.get_string.should eq("SNMP Laboratories, info@snmplabs.com")
end

it "should be able to query SNMPLabs with SNMPv2", tags: "e2e" do
it "should be able to query the live agent with SNMPv2", tags: "e2e" do
# Connect to server
socket = UDPSocket.new
socket.connect(TEST_SNMP_SERVER, 161)
socket.connect(TEST_SNMP_SERVER, TEST_SNMP_PORT)
socket.sync = false
socket.read_timeout = 3.seconds

Expand All @@ -365,27 +359,23 @@ describe SNMP do
response.value.get_string.should start_with("SNMP Laboratories")
end

it "should be able to set an OID on SNMPLabs with SNMPv2", tags: "e2e" do
it "should surface the agent's error on a rejected SET", tags: "e2e" do
# Connect to server
socket = UDPSocket.new
# socket.connect("localhost", 32771)
socket.connect(TEST_SNMP_SERVER, 161)
socket.connect(TEST_SNMP_SERVER, TEST_SNMP_PORT)
socket.sync = false
socket.read_timeout = 3.seconds

# Make request
# SET through a read-only community: the agent must reject it
session = SNMP::Session.new("public")
socket.write_bytes session.set("1.3.6.1.2.1.1.3.0.0.0.0", 43)
socket.flush

# Process response
# net-snmp answers noAccess(6) pointing at the first varbind — this also
# exercises non-zero error-status/index decoding against a live agent
response = session.parse(socket.read_bytes(ASN1::BER))
response.request.should eq(SNMP::Request::Response)

# Response should be: No Such Instance currently exists at this OID
response.pdu.error_status.should eq(SNMP::ErrorStatus::NoError)
response.pdu.error_index.should eq(0)
response.value.tag_class.should eq(ASN1::BER::TagClass::ContextSpecific)
response.value.tag_number.should eq(1)
response.pdu.error_status.should eq(SNMP::ErrorStatus::AccessDenied)
response.pdu.error_index.should eq(1)
end
end
19 changes: 19 additions & 0 deletions spec/support/snmpd.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Test agent for the e2e spec suite (`mise dev:spec-e2e`).
# Started unprivileged on a high port — see the CI workflow.

agentaddress udp:127.0.0.1:16161

# v1/v2c read-only community used by the specs
rocommunity public

# Values the specs assert against (kept from the original snmplabs simulator)
syscontact SNMP Laboratories, info@snmplabs.com
syslocation crystal-snmp test agent

# SNMPv3 users expected by the v3 live specs
createUser usr-md5-none MD5 authkey1
createUser usr-md5-aes MD5 authkey1 AES privkey1
createUser usr-md5-des MD5 authkey1 DES privkey1
rouser usr-md5-none auth
rouser usr-md5-aes priv
rouser usr-md5-des priv
49 changes: 49 additions & 0 deletions spec/varbind_from_value_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require "./helper"

describe SNMP::VarBind do
describe ".from_value" do
oid = "1.3.6.1.2.1.1.5.0"

it "encodes a String as an OctetString" do
vb = SNMP::VarBind.from_value(oid, "sysname")
vb.oid.should eq(oid)
vb.value.get_string.should eq("sysname")
end

it "encodes an Int as an Integer" do
vb = SNMP::VarBind.from_value(oid, 42)
vb.value.get_integer.should eq(42)
end

it "encodes a Bool as a Boolean" do
vb = SNMP::VarBind.from_value(oid, true)
vb.value.get_boolean.should be_true
end

it "encodes Nil as a Null value" do
vb = SNMP::VarBind.from_value(oid, nil)
vb.value.tag.should eq(SNMP::UniversalTags::Null)
end

it "takes a pre-built ASN1::BER verbatim" do
ber = ASN1::BER.new.set_integer(7)
vb = SNMP::VarBind.from_value(oid, ber)
vb.value.should be(ber)
end

it "re-oids a pre-built VarBind" do
other = SNMP::VarBind.new("1.3.6.1.9.9.9.0")
other.value.set_string("kept")
vb = SNMP::VarBind.from_value(oid, other)
vb.should be(other)
vb.oid.should eq(oid)
vb.value.get_string.should eq("kept")
end

it "rejects an unsupported value type" do
expect_raises(ArgumentError, /unsupported varbind value/) do
SNMP::VarBind.from_value(oid, 1.5)
end
end
end
end
Loading
Loading