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 <= 0x7F1len,最低字节
0x80 <= len <= 0x3FFF2len
0x4000 <= len <= 0x1FFFFF3len
0x200000 <= len <= 0xFFFFFFF4len
len >= 0x1000000050xF0 以及作为四字节的 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 命令停止它。
  • getall

    • 在任何有 CLI print 命令的地方都可以使用 getall 命令(getall 是 print 的别名)。
    • 回复包含 =.id=<item internal number> 属性。
  • print

    • API 的 print 命令与 CLI 对应命令在以下方面不同:
      • 不支持 where 参数;可以使用查询词过滤项目。
      • .proplist 参数是要包含在返回项目中的属性名称的逗号分隔列表。
        • 返回的项目可能带有额外属性。
        • 返回属性的顺序并不重要,不应依赖。
        • 如果列表中包含重复项,重复项的处理方式未定义。
        • 如果某个属性出现在 .proplist 中但项目中没有该属性,则该项目没有该属性值(对于该项目,?name 求值为 false)。
        • 如果省略 .proplist,则会按 print 命令的要求包含所有属性,包括访问耗时较长的属性(例如文件内容和性能计数器)。因此,建议使用 .proplist。如果设置了 =detail= 参数,省略 .proplist 可能会带来较高的性能开销。

查询

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
>>>