Design¶
How the library works and why. For what it supports, see compatibility.md.
Read-only by construction¶
Every call the client is able to send is listed in
reads.py. A ReadSpec refuses any method that is not a
read (get, list, info, tablesget), and SrmClient.read() refuses any spec that is not in
that list. Through the public API, a write cannot be sent by accident.
This is a safeguard against mistakes, not a security boundary: code that calls the client's private methods can send anything, and a contributor could widen the list. A test pins the list of allowed calls, so any change to it shows up in review.
Why not use Synology's own marker? SRM's API descriptors flag methods with allowDemo, which
looks like "safe to call". On an RT2600ac with SRM 1.3.2 that flag also sits on 44 set, 11
delete and 9 create methods, so it says nothing about whether a call writes.
Sessions¶
- The client logs in on first use and keeps the session id. The login version comes from the catalog (3 or 2); only when the catalog cannot be read does it try 3, without remembering that. A router that cannot be reached fails at the catalog, before any login is sent.
- When the router reports an ended session (codes 106, 107, 119), the client logs in once more
and repeats the request. Concurrent requests share one login. Two clients with the same
session_namedo not end each other's sessions (checked on SRM 1.3.2). - A failed login is remembered: every following request raises the same error without contacting
the router, until
login()is called again. Concurrent reads therefore cost one failed attempt, not one each, and a wrong password cannot run into SRM's auto block (by default 10 attempts in 5 minutes) on its own. async with SrmClient(...)logs out at the end; otherwise calllogout().- The aiohttp session is passed in and never closed by the client.
- A two-factor code is sent with one login only, whether it was accepted or not. Codes are valid
once, and repeating a rejected one counts towards SRM's auto block. A later login without a code
raises
SrmTwoFactorRequiredError, so unattended use needs an account without two-factor authentication. Trusted-device tokens, which would avoid this on DSM, are not implemented: that SRM's login offers them is not verified. - Error 407 becomes
SrmBlockedError. The meaning ("max tries", the auto block) comes from the DSM error table that py-synologydsm-api keeps, and SRM ships a matching login message; it was not triggered on purpose. In the same table, 105 is "insufficient user privilege" (SrmPermissionError) and 117 an internal error, so 117 is a plainSrmApiError.
Error codes¶
The Synology Web API reports errors as numbers. SRM does not document them; the meanings below come from the DSM error tables that py-synologydsm-api keeps (DSM and SRM share the Web API framework), plus what SRM itself shows.
| Code | Meaning | Raised as | Checked on SRM |
|---|---|---|---|
| 102, 103, 104 | API, method or version does not exist | SrmUnsupportedError; for a version the catalog offered, the catalog is read again once first (firmware update) |
103 seen |
| 105 | insufficient user privilege | SrmPermissionError |
no |
| 106, 107, 119 | session expired, ended by a duplicate login, invalid session id | logged in again once | two logins with the same session name do not cause 107 |
| 114 | missing required parameters | SrmApiError |
seen |
| 117 | unknown internal error | SrmApiError |
no |
| 400, 401, 402, 408, 409, 410 | wrong account or password, disabled account, no permission, password expired or to be changed | SrmAuthenticationError (remembered) |
no |
| 403, 404, 406 | two-factor code needed, wrong, or enforced | SrmTwoFactorRequiredError (remembered) |
no |
| 407 | "max tries": blocked by auto block | SrmBlockedError (remembered; not an authentication error) |
SRM ships a matching login message (error_max_tries); not triggered on purpose |
| 4302 | missing or wrong parameters for this API (not in the DSM tables) | SrmApiError |
seen |
Any other code is an SrmApiError with the number in code. At login, such codes are not
remembered: they can come from a router that is just restarting.
Certificates¶
ssl is handed to aiohttp unchanged. Pinning uses aiohttp.Fingerprint, the SHA-256 of the
router's own certificate, the same value get_certificate_fingerprint() returns. That function
completes a TLS handshake without verification and sends nothing else; trusting the value is up
to the caller, ideally after the user compared it with the certificate shown in SRM.
TLS failures (untrusted certificate, fingerprint mismatch) raise SrmCertificateError. It is
deliberately not a SrmConnectionError: retrying does not help, and code that retries connection
errors must not retry a changed certificate. An application asks the user to confirm the new
fingerprint instead.
Versions¶
The router's API catalog (SYNO.API.Info, readable without login) lists each API with its
minimum and maximum version. Each ReadSpec names the versions its parser understands; the
client uses the highest one the router offers. If there is none, the call is not sent and
SrmUnsupportedError is raised.
Concurrency¶
SRM routers are slow, around half a second per call on an RT2600ac. The client limits parallel
requests (max_concurrency, default 2) so it does not swamp the router.
Privacy¶
- Credentials travel in the POST body, never in a URL, so they do not end up in proxy or web server logs.
- Exception messages carry the error code and API name, never the password, the session id or the response body. Connection errors keep aiohttp's exception as their cause, which can name the host and port; that is useful for diagnosis and holds no credentials.
- With
use_https=Falsethe password crosses the network unencrypted; the client logs a warning when it is created that way. - Test fixtures are real router answers made anonymous by
scripts/anonymize.py. It works as an allowlist: every text field is either fixed vocabulary that stays, an address (MAC, IPv4, IPv6, DUID, host name) that gets a stable placeholder, or a name that is replaced as a whole. An unclassified field stops the script. Port numbers, country codes and the time zone are replaced too, because together they would describe one real network. The script refuses to write anything if an original value is left. What stays is the device type of each client and the firmware, which the tests need.
Units¶
Field names never carry a unit, because they are SRM's keys. Where a unit was verified on a router, the field's docstring names it (MHz, MB, KiB, Mbit/s, seconds); where not, it says so.
Data model¶
The models follow Home Assistant's guidance for API libraries: "Represent the data the way the API does. Keep the names and the structure", and "A library represents data, it does not interpret it". In practice:
- One getter, one SRM call, one model per answer. Nested objects and lists keep SRM's
structure (
DhcpClients.client_list.ipv4,EthPortInfo.nodes[].eth_port_info). A getter whose answer is a bare list returns a tuple (get_external_ip(),get_firewall_hit_counts()). - Field names are SRM's keys in snake_case, typos included:
transferRXRate->transfer_rx_rate,targetIP->target_ip,is_banedstays. A key that starts with a digit moves it to the end (1min_load->load_1min). Each field declares its key withsrm(), and a test checks the name for every model. - Values keep SRM's units and formats and are only converted to Python types:
"4"becomes4,0/1become booleans, but"143:35:20","0,22,37"and comma-separated address lists stay text. Nothing is filtered out: an entry with an odd MAC address is still returned. - Wrapper models carry SRM's own names, even where they are not pretty: a getter returns the
whole answer, so its list sits under SRM's key (
Ipv6Prefixes.prefix,DhcpServers.data,WifiRadioStatus.status_list). A nested object SRM did not send isNone; a missing list is(). Check forNoneat each level (clients.client_list.ipv4). - Keys the library does not map yet are in each model's read-only
raw. - Everything derived lives in
aiosynologysrm.helpers: joins (pair_radios,hits_by_rule), classifications (address_scope,is_root), parsed durations and dates, sums and percentages, andconfig_hash. Each helper documents its rule, so a caller can decide whether it fits.