API

本页介绍 RouterOS 的应用程序接口(API),用于与路由器进行安全通信,并说明语法规则、句子结构、编码方式以及 login、cancel 等受支持的命令。

API

**应用程序接口(API)允许用户创建自定义软件解决方案,与 RouterOS 通信以收集信息、调整配置和管理路由器。API 紧密遵循命令行界面(CLI)**的语法。您可以用它创建经过翻译或自定义的配置工具,让 RouterOS 路由器的运行和管理更加方便。

在尝试建立连接之前,必须先启用 API 服务,使用 /ip/service set api disabled=no 启用。默认情况下,API 使用 TCP 端口 8728 和 8729(安全)。

API-SSL 服务可以在有证书或无证书两种模式下运行。如果服务未配置证书,则只提供匿名 Diffie-Hellman 加密套件,当前 TLS 客户端不接受这些套件,因此连接会因握手错误而失败。如果服务配置了证书,客户端可以建立常规 TLS 会话。服务设置请参阅 ip/service CLI 参考。

协议

与路由器通信时,您发送句子并接收一个或多个句子作为返回。句子是由零长度字终止的字序列。字是句子的一部分,以特定方式编码:编码长度后跟数据。当路由器收到完整句子(命令字、一个或多个属性字以及零长度字)后,它会求值并执行该句子,然后形成并返回回复。

API 句子

句子是与 API 通信的主要对象。

  • 空句子会被忽略。
  • 收到零长度字后才会处理句子。
  • 客户端在登录前可以发送的句子数量和大小有限制。
  • 不要依赖属性字的顺序,因为 .proplist 属性可能改变顺序和数量。

句子结构如下:

  • 第一个字必须包含命令字。
  • 句子必须包含零长度字作为终止。
  • 句子可以包含零个或多个属性字。属性字的顺序无关紧要。
  • 句子可以包含零个或多个查询字。查询字的顺序很重要。

危险

如果未提供零长度字,路由器不会开始求值已发送的字,并会把后续所有输入视为同一个句子的一部分。

API 字

  • 字按句子分组。零长度字终止句子。
  • 每个字编码为长度后跟相应字节数的内容。
  • 编码支持最大 0xFFFFFFFF 的长度;五字节形式(0xF0 + 四个字节)用于编码最大长度。
  • len 字节按最高有效位在前(网络字节序)发送。
  • 如果字的第一个字节 >= 0xF8,则它是保留控制字节。收到未知控制字节后,API 客户端无法继续,因为不知道如何解释后续字节。
  • 当前未使用控制字节。

字的长度编码如下:

长度值字节数编码
0 <= len <= 0x7F1len,最低字节
0x80 <= len <= 0x3FFF2len | 0x8000,两个低字节
0x4000 <= len <= 0x1FFFFF3len | 0xC00000,三个低字节
0x200000 <= len <= 0xFFFFFFF4len | 0xE0000000
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,然后关闭连接。

初始登录

login 命令支持两种身份验证方法。

明文密码 - 使用当前密码格式创建的帐户所采用的方法:

/login
=name=admin
=password=

如果身份验证成功,路由器回复 !done。密码以明文发送。登录成功后,客户端可以开始发出命令。

如果凭据错误或帐户无法使用 API,路由器会回复错误:

!trap
=message=invalid user name or password

!done

旧版质询-响应 - 使用旧密码格式创建的帐户所采用的方法:

当路由器对登录返回 =ret=<challenge> 而不是简单的 !done 时,登录使用旧版方法。不带密码的登录句子会得到质询,例如:

/login

!done
=ret=93b438ec9b80057c06dd9fe67d56aa9a

客户端在第二条消息中回答用户名和质询响应:两个字符 00,后跟 0x00 字节、密码和收到的质询的小写十六进制 MD5 摘要。

/login
=name=admin
=response=00e134102a9d330dd7b1849fedfea3cb57

!done

使用当前密码格式的帐户无法完成旧版交换;路由器会拒绝该响应。

关闭 TCP 连接会结束 API 会话。

标签

API 允许同时运行多个命令,而无需等待前一个命令完成。为了区分回复,请在命令句子中添加 API 属性字 .tag=<value>;这样该句子的每个回复都会带有相同的 .tag=<value> 字。

如果省略 .tag 字或将其值留空,则该命令的回复不带标签。

请注意标签与名为 tag 的参数之间的区别:标签是 API 属性字(.tag=),而 /cancel 的 =tag= 等命令参数是常规属性字(=name=value)。

命令说明

  • /cancel

    • 可选参数:=tag=<要取消的命令标签>;不包含该参数时,所有正在运行的命令都会被取消。
    • 不会取消自身。
    • 所有被取消的命令都会被中断,通常会生成 !trap 和 !done 响应。
    • 请注意,/cancel 是独立命令,可以有自己的唯一 .tag 参数,该参数与命令的 =tag 参数无关。
  • listen

    • 只要 CLI print 命令可用,listen 命令就可用,但它可能无法在所有位置正常工作。
    • 当特定项目列表发生变化时,会生成 !re 句子。
    • 当项目被删除或消失时,!re 句子包含布尔值 =.dead=true。
    • 该命令不会自行终止;请使用 /cancel 命令停止它。
  • getall

    • 只要 CLI print 命令可用,getall 命令就可用(getall 是 print 的别名)。
    • 回复包含 =.id=<项目内部编号> 属性。
  • 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 的属性通过名称以 .oid 结尾的属性暴露;将这些名称添加到 .proplist 的值中即可检索它们。示例:

/system/resource/print
=.proplist=uptime,cpu-frequency,uptime.oid,cpu-frequency.oid

路由器发送回复:

!re
=uptime=16m44s
=cpu-frequency=864
=uptime.oid=.1.3.6.1.2.1.1.3.0
=cpu-frequency.oid=.1.3.6.1.4.1.14988.1.1.3.14.0

!done

并非每个 SNMP 属性都通过 API 暴露。具有 OID 的属性就是路由器通过 print oid 命令显示的属性;没有 OID 的属性(例如系统资源中的 cpu-load)没有对应的 .oid。

!trap

当 API 句子执行失败时,路由器返回 trap 句子。trap 带有 message 属性,包含失败详情;某些 trap 还带有 category 属性,表示错误类别:

  • 0 - 缺少项目或命令
  • 1 - 参数值错误
  • 2 - 命令执行被中断
  • 3 - 脚本相关错误
  • 4 - 一般错误
  • 5 - API 相关错误
  • 6 - TTY 相关错误
  • 7 - 由 :return 命令生成的值

例如,未知命令会返回带 =category=0 的 trap:

/foobar/print

路由器回复:

!trap
=category=0
=message=no such command or directory (foobar)

!trap
=message=no such command prefix

!done

错误的参数值会返回仅带 message 的 trap:

/ip/address/add
=address=192.168.88.1
=interface=asdf

路由器回复:

!trap
=message=input does not match any value of interface

!done

修改现有项目

与 CLI 对应命令一样,API 有 set 命令,接受项目 ID 和要设置的参数。.id 属性的值可以是内部编号(例如 *1),对于有名称的项目也可以是名称(例如 ether1)。 唯一的例外是 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

set 成功时,路由器回复单个 !done(无数据);如果未找到项目,则回复 !trap。

命令示例

listen 通知

开始监听接口列表的变化:

/interface/listen

每当项目出现或发生变化时,路由器发送 !re 句子。项目被移除时,!re 句子带有布尔值 =.dead=true:

!re
=.id=*1
=name=ether1
=disabled=false
=running=true

!re
=.id=*1
=.dead=true

使用 /cancel 停止监听。

/cancel 与同时运行的命令

开始监听接口变化(tag 为 2):

/interface/listen
.tag=2

发送命令禁用接口(tag 为 3):

/interface/set
=disabled=true
=.id=ether1
.tag=3

路由器对 disable 命令(以 tag 3 执行)回复 !done;该变更还会为 listen 命令(tag 2)生成更新:

!re
=.id=*1
=disabled=true
=dynamic=false
=running=false
=name=ether1
=mtu=1500
=type=ether
.tag=2

!done
.tag=3

启用接口(tag 为 4):

/interface/set
=disabled=false
=.id=ether1
.tag=4

该变更会为 listen 命令生成另一次更新,随后是 enable 命令的 done:

!re
=.id=*1
=disabled=false
=dynamic=false
=running=true
=name=ether1
=mtu=1500
=type=ether
.tag=2

!done
.tag=4

发送获取接口列表的命令(tag 为 5):

/interface/getall
.tag=5

客户端收到 getall 命令(以 tag 5 执行)的回复:

!re
=.id=*1
=disabled=false
=dynamic=false
=running=true
=name=ether1
=mtu=1500
=type=ether
.tag=5

!re
=.id=*2
=disabled=false
=dynamic=false
=running=true
=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 客户端,支持两种登录方法,并在终端打印整个交互过程。