API
本页介绍 RouterOS 的应用程序编程接口(API),用于与路由器进行安全通信,详细说明语法规则、语句结构、编码方式,以及用于配置管理的登录(login)和取消(cancel)等受支持命令。
API
应用程序编程接口(API) 允许用户创建自定义软件解决方案,与 RouterOS 通信以收集信息、调整配置和管理路由器。API 紧密遵循 命令行界面(CLI) 的语法。你可以用它创建翻译后的或自定义的配置工具,让 RouterOS 路由器的运行和管理更轻松。
在尝试建立连接之前,必须先启用 API 服务。默认情况下,API 使用 TCP 端口 8728 和 8729(安全)。
API-SSL 服务可以以两种模式运行:带证书或不带证书。如果 /ip service 设置中未提供证书,客户端必须使用匿名 Diffie-Hellman 加密套件来建立连接。如果服务使用证书,客户端可以建立 TLS 会话。
协议
你通过发送语句并接收一条或多条语句回复来与路由器通信。语句(sentence)是由一个零长度词终止的词序列。词(word)是语句的一部分,以特定方式编码:编码后的长度后跟数据。当路由器收到完整语句(命令词、一个或多个属性词,以及一个零长度词)后,它会解析并执行该语句,然后生成并返回回复。
语句
语句是与 API 通信的主要对象。
- 空语句会被忽略。
- 语句在收到零长度词后才会被处理。
- 客户端在登录之前可以发送的语句数量和大小是有限制的。
- 不要依赖属性词的顺序,因为顺序和数量可能会被
.proplist属性改变。
语句结构如下:
- 第一个词必须包含一个 命令词。
- 语句必须包含一个 零长度词 来终止它。
- 语句可以包含零个或多个 属性词。属性词的顺序无关紧要。
- 语句可以包含零个或多个 查询词。查询词的顺序很重要。
危险
如果未提供零长度词,路由器不会开始解析已发送的词,并会将所有后续输入视为同一语句的一部分。
词
- 词被分组为语句。零长度词终止一条语句。
- 每个词编码为长度后跟该长度的内容字节。
- 该方案允许编码最长 0xFFFFFFFF 的长度;仅支持四字节长度。
- len 字节以最高有效位在前的方式发送(网络字节序)。
- 如果词的首字节 >= 0xF8,则为保留的控制字节。收到未知控制字节后,API 客户端无法继续,因为它不知道如何解释后续字节。
- 目前未使用控制字节。
词长度编码方式如下:
| 长度值 | 字节数 | 编码 |
|---|---|---|
0 <= len <= 0x7F | 1 | len,最低字节 |
0x80 <= len <= 0x3FFF | 2 | len |
0x4000 <= len <= 0x1FFFFF | 3 | len |
0x200000 <= len <= 0xFFFFFFF | 4 | len |
len >= 0x10000000 | 5 | 0xF0 以及作为四字节的 len |
一般而言,词 可以描述为 <encoded-word-length><word-content>。
词内容可以分为五部分:命令词、属性词、API 属性词、查询词 和 回复词
命令词
语句中的第一个词必须是命令,后跟属性词和零长度或终止词。命令词的名称必须以斜杠(/)开头。命令名称紧密遵循 CLI。不支持路径对象之间的空格;必须用斜杠(/)替换。
有些命令是 API 特有的:
login- 用于登录过程,提供登录凭据。cancel- 用于取消当前正在运行的命令。
命令词按严格顺序的结构:
- 编码后的长度
- 内容前缀 /
- CLI 转换后的命令
命令词内容的几个示例:
/login
/user/active/listen
/interface/vlan/remove
/system/reboot
属性词
每个 命令词 根据内容不同,都有其自己的 属性词 列表。
属性词 结构按以下顺序由五部分组成:
- 编码后的长度
- 内容以等号(
=)字符为前缀 - 属性名称
- 等号(
=)字符作为名称和值的分隔符 - 属性值(如果有)。
一些属性示例(不含编码长度前缀):
=address=10.0.0.1
=disable-running-check=yes
值可以包含 等号(=)符号:
=name=iu=c3Eeg
值可以为空:
=comment=
请记住,属性词和 API 参数顺序并不重要,不应依赖它们。
API 属性词
API 属性词结构按严格顺序为:
- 编码后的长度
- 内容以点(
.)字符为前缀 - 属性名称
- 等号(
=)字符作为名称和值的分隔符 - 属性值
目前,唯一的此类 API 属性是 tag。
查询词
语句可以带有额外的查询参数来限制其范围。详情请参阅查询部分。
- 查询词以
?开头。 - 目前只有
print命令会处理查询词。
使用查询词属性的语句示例:
/interface/print
?type=ether
?type=vlan
?#|!
信息
查询词的顺序很重要
回复词
它仅由路由器为响应从客户端收到的完整语句而发送。
- 回复的第一个词以
!开头。 - 发送的每条语句至少生成一条回复(如果连接未被终止)。
- 每条语句的最后一条回复是第一个词为
!done的回复。 - 错误和异常情况以
!trap开头。 - 数据回复以
!re开头。 - 没有任何数据要回复的命令,其回复以
!empty开头。 - 如果 API 连接必须关闭,RouterOS 会发送带有描述中原因的
!fatal,然后关闭连接。
初始登录
-
客户端在第一条消息中发送用户名和密码。在我们的示例中,我们使用
admin和空密码:/login =name=admin =password=如果认证成功,路由器回复
!done。 -
密码以明文发送。
-
如果发生错误,回复包含
=message=<error message>。 -
成功登录后,客户端可以开始发送命令。
标签
API 允许同时运行多个命令,而不必等待前一个命令完成。如果 API 客户端这样做并且需要区分命令响应,可以在命令语句中使用 tag 参数。
如果一条语句包含 tag,则该语句的每条回复都会带有相同的 tag 值。
如果省略 tag 参数或将其留空,该命令的响应将不包含 tag 参数。
命令说明
-
/cancel- 可选参数:
=tag=<tag of command to cancel>;如果不提供,所有正在运行的命令都会被取消。 - 它不会取消自身。
- 所有被取消的命令都会被中断,通常会生成
!trap和!done响应。 - 注意
/cancel是一个独立命令,可以拥有自己独有的.tag参数,这与命令的=tag参数无关。
- 可选参数:
-
listen- 在任何有 CLI
print命令的地方都可以使用listen命令,但它可能并非处处可用。 - 当特定项目列表中的内容发生变化时,会生成
!re语句。 - 当某个项目被删除或消失时,
!re语句会包含值=.dead=yes。 - 此命令不会自行终止;请使用
/cancel命令停止它。
- 在任何有 CLI
-
getall- 在任何有 CLI
print命令的地方都可以使用getall命令(getall是print的别名)。 - 回复包含
=.id=<item internal number>属性。
- 在任何有 CLI
-
print- API 的
print命令与 CLI 对应命令在以下方面不同:- 不支持
where参数;可以使用查询词过滤项目。 .proplist参数是要包含在返回项目中的属性名称的逗号分隔列表。- 返回的项目可能带有额外属性。
- 返回属性的顺序并不重要,不应依赖。
- 如果列表中包含重复项,重复项的处理方式未定义。
- 如果某个属性出现在
.proplist中但项目中没有该属性,则该项目没有该属性值(对于该项目,?name求值为 false)。 - 如果省略
.proplist,则会按print命令的要求包含所有属性,包括访问耗时较长的属性(例如文件内容和性能计数器)。因此,建议使用.proplist。如果设置了=detail=参数,省略.proplist可能会带来较高的性能开销。
- 不支持
- API 的
查询
print 和 getall 命令接受查询词,用于限制返回语句的集合。
- 查询词以
?开头。 - 查询词的顺序很重要。查询从第一个词开始求值。
- 查询针对列表中的每个项目进行求值。如果查询成功,则处理该项目;如果查询失败,则忽略该项目。
- 查询使用布尔值栈进行求值。初始时,栈中包含无限多个
true值。求值结束时,如果栈中至少包含一个false值,则查询失败。 - 查询词按以下规则工作:
| 查询 | 描述 |
|---|---|
?name | 如果项目具有属性 name 的值,则压入 true,否则压入 false。 |
?-name | 如果项目没有属性 name 的值,则压入 true,否则压入 false。 |
?name=x | 如果属性 name 的值等于 x,则压入 true,否则压入 false。 |
?<name=x | 如果属性 name 的值小于 x,则压入 true,否则压入 false。 |
?>name=x | 如果属性 name 的值大于 x,则压入 true,否则压入 false。 |
?#operations | 对栈中的值执行操作。操作字符串从左到右求值。后跟任意其他字符或词结尾的十进制数字序列会被解释为栈索引。栈顶值的索引为 0。后跟字符的索引会压入该索引处值的副本。后跟词结尾的索引会用该索引处的值替换所有值。! 字符将栈顶值替换为其相反值。<code>&</code> 弹出两个值并压入逻辑 and 运算的结果。| 弹出两个值并压入逻辑 or 运算的结果。索引后的 . 不做任何事。其他字符后的 . 会压入栈顶值的副本。 |
信息
API 不支持正则表达式,因此不要尝试发送带有
~符号的查询 示例:
- 获取所有以太网和 VLAN 接口(等价于 CLI 命令
/interface/print where type=ether || type=vlan):
/interface/print
?type=ether
?type=vlan
?#|
- 获取所有注释非空的路由(等价于 CLI 命令
/ip/route/print where comment):
/ip/route/print
?>comment=
- 获取所有 distance 不大于 1 且 gateway 等于 172.16.1.1 的路由(等价于 CLI 命令
/ip/route/print where !(distance>1 && gateway=172.16.1.1)):
/ip/route/print
?>distance=1
?gateway=172.16.1.1
?#&!
OID
print 命令可以返回 SNMP 中可用属性的 OID 值。
在 CLI 中,可以通过运行 print oid 命令查看 OID 值。在 API 中,这些属性的名称以 .oid 结尾,可以通过将其名称添加到 .proplist 的值中来获取。示例如下:
/system/resource/print
=.proplist=uptime,cpu-load,uptime.oid,cpu-load.oid
路由器发送回复:
!re
=uptime=01:22:53
=cpu-load=0
=uptime.oid=.1.3.6.1.2.1.1.3.0
=cpu-load.oid=.1.3.6.1.2.1.25.3.3.1.2.1
!done
!trap
当某条 API 语句因任何原因失败时,路由器会返回一个 trap,并附带:
- 一个
message属性,提供有关失败的更多细节。 - 一个
category属性。如果错误是一般性错误,路由器会返回错误类别。此属性的可能值有:- 0 - 缺少项目或命令
- 1 - 参数值失败
- 2 - 命令执行被中断
- 3 - 与脚本相关的失败
- 4 - 一般性失败
- 5 - 与 API 相关的失败
- 6 - 与 TTY 相关的失败
- 7 - 由 :return 命令生成的值
/ip/address/add
=address=192.168.88.1
=interface=asdf
路由器回复:
!trap
=category=1
=message=input does not match any value of interface
修改现有条目
与 CLI 对应命令一样,API 有一个 set 命令,接受项目的 ID 和要设置的参数。
唯一的例外是 API 不接受直接对 set 命令使用查询。例如,CLI 命令为所有以太网接口设置 MTU 值:
/interface set [find where type=ether] mtu=1500
要用 API 实现同样的效果,首先需要运行 print 查询以获取 ID,然后才能执行 set 命令。
/interface/print
=.proplist=.id
?type=ether
路由器回复:
!re
=.id=*1
!re
=.id=*2
现在需要遍历返回的 ID,并为每个 ID 发送 set 命令:
/interface/set
=.id=*1
=mtu=1500
/interface/set
=.id=*2
=mtu=1500
命令示例
/user/active/listen
/user/active/listen
!re
=.id=*68
=radius=no
=when=2006-10-24 08:40:42
=name=admin
=address=0.0.0.0
=via=console
!re
=.id=*68
=.dead=yes
... more !re sentences ...
/cancel,并发命令
开始监听接口变化(tag 为 2):
/interface/listen
.tag=2
发送命令以禁用接口(tag 为 3):
/interface/set
=disabled=yes
=.id=ether1
.tag=3
路由器为 disable 命令(以 tag 3 执行)回复 !done:
!done
.tag=3
启用接口(tag 为 4):
/interface/set
=disabled=no
=.id=ether1
.tag=4
客户端从路由器收到由第一个 set 命令(以 tag 3 执行)所做更改生成的 listen 命令更新:
!re
=.id=*1
=disabled=yes
=dynamic=no
=running=no
=name=ether1
=mtu=1500
=type=ether
.tag=2
随后是 enable 命令(以 tag 4 执行)的 done:
!done
.tag=4
发送命令以获取接口列表(tag 为 5):
/interface/getall
.tag=5
客户端收到由第二个 set 命令(以 tag 4 执行)所做更改生成的更新:
!re
=.id=*1
=disabled=no
=dynamic=no
=running=yes
=name=ether1
=mtu=1500
=type=ether
.tag=2
客户端收到对 getall 命令(以 tag 5 执行)的回复:
!re
=.id=*1
=disabled=no
=dynamic=no
=running=yes
=name=ether1
=mtu=1500
=type=ether
.tag=5
!re
=.id=*2
=disabled=no
=dynamic=no
=running=yes
=name=ether2
=mtu=1500
=type=ether
.tag=5
!done
.tag=5
停止监听 - 请求取消 tag 为 2 的命令,cancel 自身使用 tag 7:
/cancel
=tag=2
.tag=7
listen 命令被中断(tag 2):
!trap
=category=2
=message=interrupted
.tag=2
cancel 命令完成(tag 7):
!done
.tag=7
listen 命令完成(tag 2):
!done
.tag=2
示例客户端
一个简单的 Python3 API 客户端
使用旧登录方法的示例输出:
debian@localhost:~/api-test$ ./api.py 10.0.0.1 admin ''
<<< /login
<<<
>>> !done
>>> =ret=93b438ec9b80057c06dd9fe67d56aa9a
>>>
<<< /login
<<< =name=admin
<<< =response=00e134102a9d330dd7b1849fedfea3cb57
<<<
>>> !done
>>>
/user/getall
<<< /user/getall
<<<
>>> !re
>>> =.id=*1
>>> =disabled=no
>>> =name=admin
>>> =group=full
>>> =address=0.0.0.0/0
>>> =netmask=0.0.0.0
>>>
>>> !done
>>>