IMConnect DICOM Router – Operations Manual¶
This manual is aimed at users who configure and operate the DICOM Router –
not at developers. It explains the structure of the configuration file
dicom-router.yaml step by step, as well as day-to-day operation.
Further detail configuration of individual plugins: Plugin Reference.
1. Overview¶
The DICOM Router accepts incoming DICOM connections and forwards them to defined destinations based on filter rules. Optionally it can rewrite messages along the way, branch them off to an additional receiver, or log them.
Four core terms describe how the router works:
| Term | Description |
|---|---|
| Listener | The port and AE title under which the router itself is reachable. |
| Filter | Ordered rules that decide what happens to a connection. |
| Actions | Optional processing steps (plugins) triggered by a rule. |
| Destinations | Named destinations: either forward or reject the connection. |
Simplified data flow of a connection:
Device (modality/PACS)
│ DICOM connection
▼
┌──────────────┐
│ Listener │ accepts the connection
└──────┬───────┘
▼
┌──────────────┐
│ Filter │ first matching rule wins
└──────┬───────┘
│
┌────┴─────┐
▼ ▼
Actions Destination
(optional) ┌──────────────┬───────────────┐
▼ ▼
forward reject
(forward to target) (reject connection)
│
▼
Target system (e.g. PACS)
For each connection the router finds the first matching filter rule, executes its actions (if configured), and forwards the connection to the destination named in the rule – or cleanly rejects it.
2. Installation & first steps¶
This section is deliberately short. The complete step-by-step guide (dedicated user, directories, permissions) is in the header of the bundled systemd unit and in the deployment documentation.
The router is shipped as a .tar.gz. Rough outline of a first installation:
- Extract into the app directory (e.g.
/opt/imconnect): - Create the configuration: The
config/directory belongs to the operator and is never overwritten during updates. The templates are located undershare/with the.examplesuffix and are copied once:Then adaptmkdir -p /opt/imconnect/config cp /opt/imconnect/share/dicom-router.yaml.example /opt/imconnect/config/dicom-router.yaml cp /opt/imconnect/share/logback.xml.example /opt/imconnect/config/logback.xml cp /opt/imconnect/share/dicom-router.env.example /opt/imconnect/config/dicom-router.envdicom-router.yamlto your environment (see section 3). Database credentials live indicom-router.env(chmod 600). - Install the systemd unit (template
dicom-router.service.example) and start it: - Check that the router is running:
3. The configuration file dicom-router.yaml¶
The active configuration lives at /opt/imconnect/config/dicom-router.yaml. It is
divided into sections. For normal operation the most relevant ones are router,
destinations, filter, and actions.
3.1 router – Listener and general settings¶
router:
listener:
host: 0.0.0.0
port: 11112
aet: IMCONNECT
tcplog: false
socketTimeout: 120000
connectTimeout: 10000
max-pdu-length: 131072
storage-path: /var/tmp/dicom-cache
log-folder: /var/log/imconnect/pdu-dumps
router.listener – how the router itself is reachable:
| Field | Description |
|---|---|
host |
Address to listen on. 0.0.0.0 = all network interfaces. |
port |
Port on which DICOM connections are accepted (default 11112). |
aet |
The router's own AE title. |
tcplog |
Writes every TCP packet this listener receives and sends into the application log in full (true/false, template: false). For troubleshooting only — see the note below the table. |
socketTimeout |
Time limit for an inactive connection in milliseconds (if the field is missing, 60000 applies; the template file sets 120000). The same value also bounds how long a single write toward the client or the destination may take — a peer that stops consuming would otherwise block the router indefinitely. |
connectTimeout |
Time limit in milliseconds for opening a connection to a destination — name lookup and TCP handshake together (if the field is missing, 10000 applies). If a destination is unreachable the router gives up after this time instead of waiting for the operating system's own limit (up to two minutes on Linux) while the peer waits without an answer. |
Switch
tcplogon for troubleshooting only. The output contains the packet contents in full, and therefore patient data in the clear (name, patient ID). The application log is rotated by size and age and is not meant as a store for patient data; on top of that, one entry per TCP packet fills the log files within minutes under load. Switch it on to analyse, switch it off afterwards (neither needs a restart), and handle the log files of the time in between accordingly. For lasting traceability the database audit trail is the tool.
Additional router settings an operator may need to adjust:
| Field | Description |
|---|---|
max-pdu-length |
Maximum DICOM packet size (PDU) the router offers to and enforces on its peers. Allowed values range from 4096 to 4194304 (4 MB); the template file ships 131072 (128 KB). Larger values reduce protocol overhead for large objects; values above 1 MB are not recommended (the router buffers such packets on disk instead of in memory). The peer negotiates its own maximum independently — effectively the smaller value applies in each direction. |
storage-path |
Directory for the internal buffer for large objects. Must be writable. |
log-folder |
Directory for binary PDU captures (only with logLevel 5/6/7 enabled, see section 5). Not the regular application logs. |
The remaining router fields (memory thresholds etc.) are technical fine-tuning and
are commented in the template file – normally leave them unchanged.
3.2 destinations – Destinations¶
Destinations are named and are referenced from filter rules by their name. There are two types.
Forward (type: forward) – the normal case. type may be omitted, since
forward is the default:
| Field | Description |
|---|---|
host |
Target machine (hostname or IP). |
port |
Target port. |
aet |
AE title of the target system. |
Reject (type: reject) – cleanly refuses the connection with a DICOM rejection
(A-ASSOCIATE-RJ) instead of forwarding it:
destinations:
reject_unknown:
type: reject
result: 1 # rejected-permanent
source: 1 # service-user
reason: 7 # called-aet-not-recognized
message: "Unknown AE title – connection rejected"
The fields result, source, and reason are DICOM codes. For most cases one of
these common combinations is sufficient:
| Purpose | result |
source |
reason |
|---|---|---|---|
| Unknown target AE title (called AET) | 1 | 1 | 7 |
| Unknown sender AE title (calling AET) | 1 | 1 | 3 |
| General rejection without a specific reason | 1 | 1 | 1 |
| Temporarily no capacity (transient) | 2 | 3 | 1 |
result:1= rejected permanently,2= rejected temporarily.source:1= service user,2= ACSE,3= Presentation.reason: meaning depends onsource(see codes above).message: free text, appears only in the log.
Choose the reason to match the reason for rejection: when filtering by the
sender (unknown device, catch-all rule via callingAET), reason: 3 is the
correct code; reason: 7 fits when the requested AE title does not exist;
when in doubt, reason: 1 (no specific reason) is always acceptable.
3.3 filter – Routing rules¶
filter is an ordered list. The router evaluates the rules top to bottom and
applies the first one that matches (first match wins). After that no further
rules are considered. Therefore: specific rules at the top, a catch-all rule at the
very bottom.
The most important fields of a rule:
| Field | Description |
|---|---|
callingAET |
AE title of the sender (device). * or omitted = any. |
calledAET |
Requested target AE title. * or omitted = any. |
logLevel |
Logging level for this connection (see section 5). |
actions |
List of action names executed for this rule (optional). |
destination |
The destination of the rule: a name from destinations: (forward or reject). |
Note: The YAML field
destinationformally accepts a list, but supports exactly one destination — only the first entry is used. The list form exists so that the configuration structure stays stable in case multiple destinations become possible in the future. So: always specify exactly one entry, e.g.destination: ["pacs1"].
In addition, a rule can be restricted by network characteristics (clientLocalIp,
routerLocalPort etc.); if omitted, each defaults to "any" (IP 0.0.0.0/0, port 0).
AE title matching: Values are compared exactly by default
(case-sensitive); only the value * means "any". With the prefix
regex: the value is instead evaluated as a regular expression and must cover the
entire AE title:
An invalid expression is a configuration error and prevents startup or reload.
Example 1 – one device to a PACS, reject everything else:
filter:
- callingAET: US1
calledAET: PACS
destination:
- "pacs1"
# Catch-all rule: reject everything else
- callingAET: "*"
calledAET: "*"
destination:
- "reject_unknown"
Example 2 – several similar devices via a regular expression:
filter:
- callingAET: "regex:^US[0-9]+$" # US1, US2, ...
calledAET: PACS
destination:
- "pacs1"
- callingAET: "*"
destination:
- "reject_unknown"
Example 3 – rule with AE title rewriting before forwarding:
filter:
- callingAET: MRI1
calledAET: PACS
actions:
- "aetRewrite_mri" # defined in actions, see below
destination:
- "pacs1"
3.4 actions – Processing steps (plugins)¶
Under actions, named processing steps are defined. A name refers to a list of
plugin entries; a plugin's parameters go in its config: block. A rule activates an
action by naming it under actions (see example 3 above).
Basic structure:
actions:
aetRewrite_mri:
- plugin: <plugin identifier – see Plugin Reference>
config:
# plugin-specific parameters
enabled: true
Which plugins are available and which parameters their config: block accepts (e.g.
AE title rewriting, protocol capture, Q/R field adjustment) is described in the
Plugin Reference. Plain forwarding without rewriting requires
no actions – destination alone is sufficient.
An action only runs when a rule names it. There is no automatic loading: if no filter
rule references an action name under actions, nothing happens — defining it is not
enough. The router points this out with a warning in the log at start and on every
reload, naming the action. So always do both: define the action and enter it in the
rule it belongs to.
3.5 management and audit¶
These two sections are usually correct with their default values and rarely need to be touched.
management controls the two additional ports for operations and monitoring:
management:
enabled: true
agent-check-port: 8404 # health query by HAProxy (network-accessible)
agent-check-bind-address: "0.0.0.0"
management-port: 8405 # operator commands (local only, 127.0.0.1)
management-bind-address: "127.0.0.1"
The management port is deliberately reachable only locally (127.0.0.1). Without
additional protection it is not authenticated – i.e. any local process could stop
the router. Optionally, a password can be required via a token file; details in the
management documentation.
audit writes an audit trail to the database: one row per connection and per
DICOM operation, without data loss via a local spool
(sole exception: if the spool exceeds its spool-max-mb limit, for example during a
very long database outage, further entries are discarded and counted). A database
outage does not cause data loss and does not affect routing.
audit:
enabled: true
spool-path: /var/imconnect/audit-spool
spool-max-mb: 512
batch-size: 100
flush-interval: 1s
capture-patient-name: false # patient name (PHI!) – off by default
capture-patient-name should only be enabled for good reason, since it stores
patient data (PHI).
3.6 Which changes a reload picks up¶
Many changes take effect without a restart via the reload command (see section 4):
New connections then use the new configuration, while connections already in progress
keep their previous one. Some fundamental fields, however, are only bound at
startup and require a restart.
Applied immediately via reload |
Effective only after a restart |
|---|---|
filter (routing rules) |
router.listener.host / port / aet |
destinations (forward/reject/qr-bridge destinations) – exception: store: of a qr-bridge destination |
the entire management block |
actions (plugins and their config:) – exception: store: of CMoveTrackingStage |
the entire audit block – exception: capture-patient-name |
proxyprotocol |
store: of a CMoveTrackingStage action |
router.listener.socketTimeout / connectTimeout / tcplog |
store: of a qr-bridge destination |
audit.capture-patient-name |
|
the router memory thresholds |
For the two store: entries this covers more than the value: if such an entry
vanishes (removed or renamed) and no entry with the same store takes its place in
the same reload, that reload is restart-required as well. The store instances
are created at startup, and a renamed entry cannot be told apart from a removed
one. Pure additions stay reloadable.
If a reload attempts to change a restart-required field, it is rejected with
requires restart and the previous configuration stays active. The complete field
list is in the management documentation.
4. Operation¶
The running router is controlled via the bundled CLI tool
dicom-router-ctl (under /opt/imconnect/bin/).
| Command | Effect |
|---|---|
dicom-router-ctl status |
State, number of active connections, uptime, audit status. |
dicom-router-ctl connections |
List of active DICOM connections. |
dicom-router-ctl drain |
Stop accepting new connections; existing ones continue. |
dicom-router-ctl resume |
Accept connections again after a drain. |
dicom-router-ctl reload |
Re-read the configuration (see section 3.6). |
dicom-router-ctl shutdown |
Immediate orderly stop. |
dicom-router-ctl drain-and-shutdown |
First drain, wait for 0 connections, then stop. |
Stopping: shutdown vs. drain-and-shutdown¶
The difference matters:
shutdownstops the router immediately. Transfers still in progress are disconnected.drain-and-shutdownfirst stops accepting new connections, waits until all ongoing transfers have completed, and stops only then. This is the gentle way for a maintenance window or update.
Restarting via systemd¶
A restart is done via systemd:
This is automatically gentle: when stopping, the systemd unit internally calls
drain-and-shutdown, so it first waits for all ongoing connections before it
terminates the process. A generous time limit is set for the wait. After an
unexpected crash the service restarts automatically; a clean stop stays
stopped.
Changing the configuration (workflow)¶
- Edit
/opt/imconnect/config/dicom-router.yaml. - Optionally validate first, without touching the running router at all:
Reports
OKor lists the errors (same check as on reload). That includes every key that does not exist: a typo or the wrong spelling (tokenFileinstead oftoken-file) fails the check naming section and field, instead of silently discarding the setting. All errors of a file are reported in one pass. Important: run the command in the installation directory (or setchdirin automation such as Ansible) — plugin classes inplugins/are looked up relative to the working directory; from any other directory the check would falsely report errors when the configuration uses external plugins. - Apply:
dicom-router-ctl reload. - Check the response:
OK config reloaded– the change is active.ERROR ... requires restart– a restart-required field was changed; usesystemctl restart dicom-routerinstead.- Other
ERROR ...– the configuration contains an error; the old configuration stays active until the error is fixed and anotherreloadsucceeds.
Multiple hosts: distributing the configuration (dicom-router-sync)¶
If the router runs on multiple hosts (e.g. behind HAProxy), the configuration is
edited on one host and distributed to all others with dicom-router-sync:
- Enter the other hosts once in
/opt/imconnect/config/cluster-hosts(template:share/cluster-hosts.example; one line per host,user@hostallowed). Prerequisite is passwordless ssh access from the editing host to each target. - After editing the YAML: run
dicom-router-sync. The tool - automatically backs up the state to
config/config-history/(history for later reference and rollback — simply copy an old state back and distribute it again), - reloads the local router first as a probe: if the configuration is rejected, the tool aborts before any other host is touched — the old configuration stays active everywhere,
- then copies the file to all hosts, reloads them, and verifies via checksum
that the same state is present everywhere — where the router reports it,
additionally against the
config-hashfromstatus, i.e. the state actually loaded (result table at the end,MATCH (loaded)). - If the probe reports
requires restart, the tool distributes the file, skips the reloads, and shows the instructions for the rolling restart (systemctl restart dicom-router, host by host).
Additional modes: dicom-router-sync --check only compares the state of all hosts
(useful as a drift check, changes nothing); --no-reload only distributes.
The result table distinguishes what went wrong: UNREACHABLE means the host could not
be reached over ssh, while MISSING means the ssh connection worked but the host has
no configuration file (yet) — typical for a freshly set up node. NO-SHA256SUM reports
a host without a sha256sum command, where the state cannot be verified.
New destination with an automatic return port (return-port: 0)¶
If you use HAProxy return paths (haproxy-return with a configured
port-range), return ports no longer need to be assigned by hand:
- Create the new destination with the marker
return-port: 0: - Have the marker replaced locally:
The subcommand assigns the smallest free port of the
cd /opt/imconnect java -jar dicom-router-core-*.jar --allocate-return-ports config/dicom-router.yamlport-rangeand prints the assignment (e.g.ct_new: return-port 5014); ports already assigned to other destinations always stay unchanged. (Required before step 3 —dicom-router-haproxy-syncaborts on an unallocated marker.) - Activate the new return path on the HAProxy hosts:
dicom-router-haproxy-sync(next section). - Only now distribute the router configuration:
dicom-router-sync. In this order the router never dials a port HAProxy does not know yet. Without a cluster (single host), usedicom-router-ctl reloadinstead.
A forgotten marker can never go live unnoticed: start, reload and
--validate-config reject a configuration containing return-port: 0 with a
clear error message.
Return paths kept by hand in the HAProxy config: the generated HAProxy file
only contains destinations whose return-port lies inside the port-range. If
you keep a return path in your own HAProxy configuration (fixed port, different
backend setup, legacy entry), give that destination a return-port outside
the range. The router still dials it via vip:return-port, but the return path
no longer appears in the generated file — otherwise there would be two listen
blocks on the same bind and HAProxy would refuse to start. Skipped destinations
are listed as comments in the generated file:
# skipped (return-port outside port-range 5000-5999 — manual HAProxy entry):
# ct_b bind 10.0.0.10:4711 -> 192.168.10.9:104
This applies retroactively when updating to this version: if you have a
port-range configured and destinations with a return-port outside it,
their listen blocks disappear from the generated file on the next
dicom-router-haproxy-sync run. dicom-router-haproxy-sync --check shows this
beforehand and names the blocks explicitly as still configured ("target has
listen block(s) for destination(s) STILL in the YAML … do NOT remove them") —
move them into your own HAProxy configuration first.
Distributing HAProxy return paths (dicom-router-haproxy-sync)¶
If you use HAProxy return paths, the generated HAProxy file no longer has to be brought to the HAProxy hosts by hand:
- Enter the HAProxy hosts once in
/opt/imconnect/config/haproxy-hosts(template:share/haproxy-hosts.example; one line per host, optionally with a target path, default/etc/haproxy/conf.d/dicom-return.cfg). Prerequisites: passwordless ssh access, write access to the target path and a sudo rule forsystemctl reload haproxy(details: deployment documentation). - After every change to the return paths: run
dicom-router-haproxy-sync. The tool generates the file from the YAML, copies it to all hosts (on unchanged hosts copy and validation are skipped, the reload still runs — so a repeated run catches up on a previously failed reload), checks it there withhaproxy -cbefore it becomes active (on a failure the host keeps its previous state untouched), reloads HAProxy seamlessly (running transfers survive) and verifies via checksum that the same state is present everywhere (result table at the end). It manages only this one file — never HAProxy's main configuration.
Order rule:
- Destination added → first
dicom-router-haproxy-sync, thendicom-router-sync— a newlistenblock is harmless until someone dials it; the other way round the router would dial a port HAProxy does not know yet. - Destination removed → the other way round: first
dicom-router-sync, thendicom-router-haproxy-sync. return-portchanged to a value outside theport-range(the return path is kept by hand, see the section above) → first move thelistenblock into your own HAProxy configuration, then rundicom-router-haproxy-sync— otherwise the return path is unreachable in between.
The tool prints all three rules as a hint itself — the third one included: on the HAProxy host it looks like a removed destination, but the script recognizes it by the skip list of the generated file and then states explicitly that the block is not stale and must not be removed. The comparison works on block names: if such a destination is renamed on top of that, the script reports the old block as stale even though its return path is still in use. So check the skip list of the generated file before removing a reported block — the hint text says so as well.
Additional modes: dicom-router-haproxy-sync --check only compares the state
of all hosts (changes nothing); --no-reload distributes and checks but does
not reload.
5. Troubleshooting¶
Where are the logs?¶
There are two independent logging mechanisms – easy to confuse:
| What | Where / how | Content |
|---|---|---|
| Application log | File dicom-router.log in the log directory (default /var/log/imconnect), configured via logback.xml |
regular messages (INFO/WARN/ERROR) – the first place to look |
| Binary PDU captures | Directory from router.log-folder, enabled via logLevel 5/6/7 in a filter rule |
complete DICOM packets as binary files – for diagnosis only |
The regular application log is rotated automatically. The PDU captures are a
diagnostic tool for difficult cases: they contain complete patient data (PHI),
are not rotated or automatically deleted, and should only be enabled temporarily.
After the analysis, reset the logLevel and remove the captures.
Setting logLevel in the filter rule is sufficient — the capture function is
built in, no action is needed. logLevel is hot-reloadable: change the value,
dicom-router-ctl reload, done — and to disable it, set it back to 0 the same
way. (The plugin PduLoggingStage in the
Plugin Reference merely adds additional named loggers.)
logLevel levels in a filter rule:
| Value | Description |
|---|---|
0 |
no logging |
1 |
log inbound |
2 |
log outbound |
3 |
log inbound and outbound |
5 |
capture inbound to file |
6 |
capture outbound to file |
7 |
capture inbound and outbound to file |
Inspecting the running state¶
dicom-router-ctl status # state, connection count, uptime, audit
dicom-router-ctl connections # which connections are currently open
Typical problems¶
-
A connection is rejected. First check
dicom-router-ctl status: if the router is in thedrainstate, it is deliberately not accepting new connections (dicom-router-ctl resumefixes that). If the state isready, arejectdestination is probably matching – usually the catch-all rule at the end of the filter list (itsmessageappears in the application log). Check: is there a matching rule before the catch-all rule? DocallingAET/calledAETmatch exactly (case, no spaces)? Is the correctportbeing used? -
Connections are aborted since an action was added. If a plugin entered in a rule cannot be loaded or cannot start with its configuration, the router aborts the affected association instead of forwarding it without that processing step — a connection without its configured anonymisation or rewriting must not run unnoticed. The application log carries an
ERRORline naming plugin and action. To find this beforehand, use--validate-config(see section 4): the check loads every plugin class entered and everyconfig:block. -
Destination unreachable / forwarding fails. Check
host,port, andaetof theforwarddestination against the target system. Is the target system reachable over the network, and does it accept the AE title used by the router? -
reloadis rejected. requires restart: a restart-required field was changed (listener,management,audit, or astore:— including by removing or renaming a tracking entry or aqr-bridgedestination, see section 3.6) →systemctl restart dicom-router.- other
ERROR: the YAML contains an error (typo, invalid regular expression, unknown destination) or the file is missing (nothing is created anew in that case). The router keeps running with the old configuration until areloadcompletes without errors.