Port Forwarding
MsgTier supports static peer-to-peer port forwarding for bastion-style access. It is designed for cases like:
- exposing SSH from a remote node
- forwarding a database port through another node
- exposing a private UDP service such as DNS
This is not a dynamic SOCKS proxy. Both sides must declare the same rule key in config.
How It Works
There are two roles:
forwards: the local listening sideexposes: the remote target side
MsgTier matches a tunnel by:
- peer id
- rule key
If node A wants to reach node B's SSH service:
- node A adds a
forwardsentry - node B adds
exposes.ssh
Once both nodes are online and connected, node A gets a local TCP port. Connecting to that local port forwards traffic to node B's configured target.
Config Format
forwards uses this format:
{
"forwards": {
"node-b:ssh": "tcp://127.0.0.1:10022"
}
}Meaning:
- key format:
peer_id:rule_id - key peer id:
node-b - key rule id:
ssh - value protocol:
tcp - value local listen address:
127.0.0.1:10022
exposes uses this format:
{
"exposes": {
"ssh": "tcp://127.0.0.1:22"
}
}Meaning:
- rule key:
ssh - protocol:
tcp - remote target address:
127.0.0.1:22
SSH Example
This is the most common setup.
Assume:
node-ais your laptopnode-bis a remote machine running SSH on port22- you want to SSH to
node-bby connecting to127.0.0.1:10022onnode-a
Node A
node-a.jsonc:
{
"id": "node-a",
"secret": "shared-secret",
"listeners": [
"tcp://0.0.0.0:16101"
],
"peers": [
"tcp://node-b.example.com:16102"
],
"web_api": "127.0.0.1:19100",
"forwards": {
"node-b:ssh": "tcp://127.0.0.1:10022"
}
}Node B
node-b.jsonc:
{
"id": "node-b",
"secret": "shared-secret",
"listeners": [
"tcp://0.0.0.0:16102"
],
"peers": [],
"web_api": "127.0.0.1:19101",
"exposes": {
"ssh": "tcp://127.0.0.1:22"
}
}Use It
Start both nodes, wait until they are connected, then on node-a run:
ssh -p 10022 user@127.0.0.1That connection path is:
ssh client on node-a
-> 127.0.0.1:10022
-> MsgTier forward rule ssh
-> node-b
-> 127.0.0.1:22 on node-bJump To Another Host Behind The Remote Node
The expose side can also point at another address reachable from that node.
For example, if node-b can reach 10.0.0.15:22 on its private LAN:
{
"exposes": {
"ssh": "tcp://10.0.0.15:22"
}
}Then ssh -p 10022 user@127.0.0.1 on node-a will reach that private host through node-b.
This is the intended bastion use case.
UDP Example
UDP uses the same rule structure:
{
"forwards": {
"node-b:dns": "udp://127.0.0.1:1053"
},
"exposes": {
"dns": "udp://127.0.0.1:53"
}
}Then on the forward side you can query:
dig @127.0.0.1 -p 1053 example.comWeb UI
Open the network page in the Web UI. The port forwarding panel shows:
- rule key
- direction:
forwardorexpose - protocol
- peer id for forward rules
- local listen address for forward rules
- current state
Common states:
listening: local forward port is activeconfigured: rule exists but is not yet listening or matchedpeer_unavailable: the target peer is not currently reachabledisabled: the rule is not active
Troubleshooting
If the tunnel does not work, check these points first:
- The two nodes must use the same rule key.
- The
forwardsrule must point to the correct peer id. secretmust match on both nodes.- The exposed target must actually be reachable from the expose node.
- The local listen port must not already be occupied.
Useful checks:
curl http://127.0.0.1:19100/api/status
curl http://127.0.0.1:19101/api/statusLook for:
- the peer appearing in
peers - direct connections being present
metadata.port_forwards
For the SSH example, also verify the target directly on the expose node:
ssh 127.0.0.1or just check that port 22 is listening there.
Notes
- The forward side is always a local listener.
- The expose side chooses the final target address.
- MsgTier does not let the client choose arbitrary target addresses at runtime.
- This keeps the feature predictable and suitable for controlled bastion access.