# RouterOS API
[](https://github.com/jlbyh2o/routeros_api/actions/workflows/ci.yml)
[](https://hex.pm/packages/routeros_api)
[](https://hexdocs.pm/routeros_api)
Elixir client for MikroTik RouterOS binary API. Supports both plain TCP (port 8728) and TLS (port 8729) connections.
## Features
- ✅ Plain TCP connections (port 8728)
- ✅ TLS/SSL connections (port 8729) with self-signed certificate support
- ✅ RouterOS 7.x authentication (plain text)
- ✅ RouterOS 6.x authentication (MD5 challenge-response fallback)
- ✅ Response parsing to Elixir maps with type coercion
- ✅ Synchronous command execution
- ✅ **Streaming API for live monitoring** (e.g., `/interface/monitor-traffic`)
- ✅ Query filters support
- ✅ Connection pooling with NimblePool
- ✅ Telemetry integration for monitoring
- ✅ Helper functions for common operations
- ✅ Type-safe with Dialyzer
- ✅ Comprehensive test coverage
## Installation
Add `routeros_api` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:routeros_api, "~> 0.3.0"}
]
end
```
Or from GitHub:
```elixir
def deps do
[
{:routeros_api, github: "jlbyh2o/routeros_api"}
]
end
```
## Quick Start
### Basic Connection (Plain TCP)
```elixir
# Connect to router
{:ok, conn} = RouterosApi.connect(%{
host: "192.168.88.1",
port: 8728,
username: "admin",
password: "password"
})
# Execute a command
{:ok, interfaces} = RouterosApi.command(conn, ["/interface/print"])
# Result is a list of maps
[
%{
"name" => "ether1",
"type" => "ether",
"disabled" => false,
"running" => true
},
...
]
# Disconnect when done
RouterosApi.disconnect(conn)
```
### Secure Connection (TLS)
```elixir
# Auto-detect TLS from port 8729
{:ok, conn} = RouterosApi.connect(%{
host: "192.168.88.1",
port: 8729, # TLS port - auto-detected
username: "admin",
password: "password",
ssl_opts: [verify: :verify_none] # For self-signed certificates
})
# Or explicit TLS with certificate verification
{:ok, conn} = RouterosApi.connect(%{
host: "192.168.88.1",
port: 8729,
username: "admin",
password: "password",
ssl: true,
ssl_opts: [
verify: :verify_peer,
cacertfile: "/path/to/ca.pem"
]
})
```
### Self-Signed Certificates
For lab/testing environments with self-signed certificates:
```elixir
{:ok, conn} = RouterosApi.connect(%{
host: "192.168.88.1",
port: 8729,
username: "admin",
password: "password",
ssl_opts: [
verify: :verify_none # Disables certificate verification
]
})
```
**Note:** `verify: :verify_none` should only be used in lab/testing environments. For production, use proper certificates and `verify: :verify_peer`.
## Usage Examples
### List IP Addresses
```elixir
{:ok, addresses} = RouterosApi.command(conn, ["/ip/address/print"])
```
### Add IP Address
```elixir
{:ok, _} = RouterosApi.command(conn, [
"/ip/address/add",
"=address=192.168.88.2/24",
"=interface=bridge"
])
```
### Query with Filters
```elixir
{:ok, [interface]} = RouterosApi.command(conn, [
"/interface/print",
"?name=ether1"
])
```
### Error Handling
```elixir
case RouterosApi.command(conn, ["/interface/print"]) do
{:ok, data} ->
IO.inspect(data)
{:error, %RouterosApi.Error{type: :trap, message: msg}} ->
IO.puts("RouterOS error: #{msg}")
{:error, %RouterosApi.Error{type: :fatal}} ->
IO.puts("Fatal error - connection lost")
{:error, reason} ->
IO.puts("Network error: #{inspect(reason)}")
end
```
## Configuration
### Connection Options
All connection options:
```elixir
{:ok, conn} = RouterosApi.connect(%{
host: "192.168.88.1", # Required: Router hostname or IP
port: 8728, # Optional: Port (default: 8728 for TCP, 8729 for TLS)
username: "admin", # Required: RouterOS username
password: "password", # Required: RouterOS password
timeout: 5000, # Optional: Connection timeout in ms (default: 5000)
ssl: false, # Optional: Force TLS (auto-detected from port)
ssl_opts: [] # Optional: SSL options (e.g., verify: :verify_none)
})
```
### Connection Pooling
For production use with multiple concurrent requests, use connection pooling:
```elixir
# In your application.ex
def start(_type, _args) do
children = [
{RouterosApi.Pool, [
name: :main_router,
host: "192.168.88.1",
port: 8729, # Optional: Use TLS
username: "admin",
password: "password",
pool_size: 10, # Number of connections in pool
ssl_opts: [verify: :verify_none] # For self-signed certs
]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
# In your code - use pool name instead of connection PID
{:ok, interfaces} = RouterosApi.command(:main_router, ["/interface/print"])
{:ok, resource} = RouterosApi.Helpers.get_system_resource(:main_router)
```
**Benefits of pooling:**
- Handles concurrent requests efficiently
- Automatic connection health checks
- Connection recovery on failures
- Supervised connections
### Telemetry
The library emits telemetry events for monitoring:
**Connection Events:**
- `[:routeros_api, :connection, :start]` - Connection attempt started
- `[:routeros_api, :connection, :stop]` - Connection successful
- `[:routeros_api, :connection, :exception]` - Connection failed
**Command Events:**
- `[:routeros_api, :command, :start]` - Command execution started
- `[:routeros_api, :command, :stop]` - Command completed successfully
- `[:routeros_api, :command, :exception]` - Command failed
**Pool Events:**
- `[:routeros_api, :pool, :checkout]` - Connection checked out from pool
- `[:routeros_api, :pool, :checkin]` - Connection returned to pool
Example telemetry handler:
```elixir
:telemetry.attach_many(
"routeros-api-handler",
[
[:routeros_api, :command, :stop],
[:routeros_api, :command, :exception]
],
&MyApp.Telemetry.handle_event/4,
nil
)
defmodule MyApp.Telemetry do
require Logger
def handle_event([:routeros_api, :command, :stop], measurements, metadata, _config) do
if measurements.duration > 1_000_000_000 do
Logger.warning("Slow RouterOS command: #{metadata.command} (#{measurements.duration}ns)")
end
end
def handle_event([:routeros_api, :command, :exception], _measurements, metadata, _config) do
Logger.error("RouterOS command failed: #{metadata.command} - #{inspect(metadata.reason)}")
end
end
```
### Helper Functions
The library provides convenient helper functions for common operations:
```elixir
alias RouterosApi.Helpers
# List all interfaces
{:ok, interfaces} = Helpers.list_interfaces(conn)
# Get specific interface
{:ok, interface} = Helpers.get_interface(conn, "ether1")
# List IP addresses
{:ok, addresses} = Helpers.list_ip_addresses(conn)
# Add IP address
{:ok, _} = Helpers.add_ip_address(conn, "192.168.1.1/24", "ether1")
# Get system information
{:ok, resource} = Helpers.get_system_resource(conn)
# Get/set router identity
{:ok, identity} = Helpers.get_identity(conn)
{:ok, _} = Helpers.set_identity(conn, "MyRouter")
# List firewall rules
{:ok, rules} = Helpers.list_firewall_rules(conn)
# List DHCP leases
{:ok, leases} = Helpers.list_dhcp_leases(conn)
```
All helpers work with both direct connections and connection pools.
## Streaming API (Live Monitoring)
For commands that produce continuous output (like `/interface/monitor-traffic`), use the streaming API:
```elixir
# Start a dedicated streaming connection
{:ok, stream_conn} = RouterosApi.Stream.connect(%{
host: "192.168.88.1",
username: "admin",
password: "password"
})
# Create a stream for interface traffic monitoring
{:ok, stream} = RouterosApi.Stream.monitor(stream_conn, [
"/interface/monitor-traffic",
"=interface=ether1"
])
# Consume the stream - each item is a data map
stream
|> Stream.take(10) # Take 10 samples
|> Enum.each(fn data ->
rx = data["rx-bits-per-second"]
tx = data["tx-bits-per-second"]
IO.puts("RX: #{rx} bps, TX: #{tx} bps")
end)
# Disconnect when done
RouterosApi.Stream.disconnect(stream_conn)
```
### Time-Limited Monitoring
RouterOS supports duration-limited monitoring:
```elixir
{:ok, stream} = RouterosApi.Stream.monitor(stream_conn, [
"/interface/monitor-traffic",
"=interface=ether1",
"=duration=30s" # Router stops after 30 seconds
])
# Collect all samples
samples = Enum.to_list(stream)
```
### Stream Transformations
Streams work with all standard Elixir `Stream` functions:
```elixir
{:ok, stream} = RouterosApi.Stream.monitor(stream_conn, [
"/interface/monitor-traffic",
"=interface=ether1"
])
# Calculate 5-sample moving average
stream
|> Stream.chunk_every(5)
|> Stream.map(fn samples ->
avg = samples
|> Enum.map(&String.to_integer(&1["rx-bits-per-second"]))
|> Enum.sum()
|> div(5)
%{average_rx_bps: avg}
end)
|> Stream.take(10)
|> Enum.each(&IO.inspect/1)
```
### Streaming Limitations
- **Not compatible with connection pools** - Streams require dedicated connections
- **One stream per connection** - Use sequential streams or multiple connections
- **No automatic reconnection** - Connection loss terminates the stream
### Stream Telemetry Events
- `[:routeros_api, :stream, :connect, :start|:stop|:exception]` - Connection lifecycle
- `[:routeros_api, :stream, :start]` - Stream started
- `[:routeros_api, :stream, :stop]` - Stream completed/cancelled
## Authentication
The library automatically handles authentication for different RouterOS versions:
### RouterOS 7.x and 6.43+ (Plain Text)
Modern RouterOS versions use plain text authentication:
```elixir
# The library automatically detects and uses plain text auth
{:ok, conn} = RouterosApi.connect(%{
host: "192.168.88.1",
username: "admin",
password: "password"
})
```
### RouterOS pre-6.43 (MD5 Challenge-Response)
Older RouterOS versions use MD5 challenge-response authentication. The library automatically falls back to this method if plain text authentication fails:
```elixir
# Same code works - automatic fallback
{:ok, conn} = RouterosApi.connect(%{
host: "192.168.88.1",
username: "admin",
password: "password"
})
```
**Note:** The authentication method is automatically detected and handled. You don't need to specify which method to use.
## Troubleshooting
### Connection Issues
**"Connection refused"**
- Ensure the API service is enabled on the router: `/ip service print`
- Check that the correct port is being used (8728 for TCP, 8729 for TLS)
- Verify firewall rules allow connections to the API port
**"Authentication failed"**
- Verify username and password are correct
- Check that the user has API access permissions
- For RouterOS 7.x, ensure you're using the correct authentication method (automatic)
**SSL/TLS Certificate Errors**
- For self-signed certificates, use `ssl_opts: [verify: :verify_none]`
- For production, use proper certificates and `verify: :verify_peer`
- Ensure the API-SSL service is enabled: `/ip service print`
### Performance
**Slow Commands**
- Use connection pooling for concurrent requests
- Monitor with telemetry events
- Check network latency to the router
**Connection Timeouts**
- Increase timeout: `timeout: 10_000` (10 seconds)
- Check router CPU usage
- Verify network connectivity
## Documentation
Full documentation is available at [https://hexdocs.pm/routeros_api](https://hexdocs.pm/routeros_api).
## Testing
The library includes comprehensive test coverage:
```bash
# Run unit tests only
mix test
# Run all tests including integration tests
mix test --include integration --include ssl_integration
# Run with coverage
mix test --cover
# Run Dialyzer type checking
mix dialyzer
# Run code quality checks
mix credo --strict
```
**Test Coverage:**
- 109 tests (all passing)
- Unit tests for all modules
- Integration tests with real RouterOS device
- SSL/TLS integration tests
- Connection pooling tests
- Telemetry tests
- Helper function tests
## Compatibility
**Tested with:**
- Elixir 1.14 - 1.18
- OTP 25 - 28
- RouterOS 7.12.1 (stable)
- RouterOS 6.x (MD5 auth fallback)
**Supported RouterOS versions:**
- RouterOS 7.x (plain text authentication)
- RouterOS 6.43+ (plain text authentication)
- RouterOS pre-6.43 (MD5 authentication fallback)
## Development
### Quality Tools
The project uses several tools to maintain code quality:
- **Dialyzer** - Static type checking
- **Credo** - Code quality and consistency
- **ExDoc** - Documentation generation
- **GitHub Actions** - Automated CI/CD
### Running Quality Checks
```bash
# Format code
mix format
# Check formatting
mix format --check-formatted
# Run all quality checks
mix test && mix dialyzer && mix credo
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
MIT License - see [LICENSE](LICENSE) for details.
## Acknowledgments
This project was inspired by the original [erotik](https://github.com/comtihon/erotik) Erlang library.