Service management
How to create, configure, and manage services and routes in Arctic
Prefer compose for managing services
Services and routes are part of the compose configuration. For anything you keep around - defining services, adjusting routes, setting bandwidth limits - declare it in your cluster.yaml and run arctic compose apply. That keeps the cluster's desired state in version control and reviewable, and is the recommended way to manage Arctic. The commands on this page are best for inspecting state or making a quick one-off change.
Changed in v1.4.2
The agent API is served over TLS as of v1.4.2, so the curl examples on this page use https with -k. The -k is needed because the agent's certificate is self-signed; identity is verified by fingerprint pinning rather than a CA chain. See TLS and trust.
Create a service
This section shows you how to create a service that routes traffic between two Arctic agents.
A service carries traffic in one direction, from its source peer to its target peer. For two-way communication between a pair of hosts, create a second service in the reverse direction; each direction is its own service with its own interface and routes.
Before you start
Ensure you have:
- At least two connected peers in your cluster
- The target peer ID (the peer that will receive traffic)
- Appropriate permissions (services.write scope)
Basic service creation
1. Find the target peer ID
List peers to find the ID of the destination peer:
arctic peers list2. Create the service
Create a basic TCP service:
arctic services create --target-peer TARGET_PEER_IDcurl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_peer_id": "TARGET_PEER_ID", "transport_type": "tcp"}'target_peer_id and transport_type are required. The source peer
defaults to the local peer when omitted.
This creates a service from the local peer to the target peer using TCP transport.
Advanced options
With MACVLAN interface
Create a service with a dedicated network interface for traffic isolation:
arctic services create \
--target-peer TARGET_PEER_ID \
--requires-interfacecurl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_peer_id": "TARGET_PEER_ID",
"transport_type": "tcp",
"requires_interface": true
}'With specific IP addresses
Request a specific IPv4 CIDR for the MACVLAN interface (omit to use DHCP):
arctic services create \
--target-peer TARGET_PEER_ID \
--requires-interface \
--desired-ipv4 192.168.100.10/24curl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_peer_id": "TARGET_PEER_ID",
"transport_type": "tcp",
"requires_interface": true,
"desired_interface_ipv4": "192.168.100.10/24"
}'With a pinned uplink or MACVLAN parent device
On a host with more than one usable interface, pin the egress device the service's tunnel traffic leaves by (--uplink-dev), or the parent device its MACVLAN is built on (--interface-parent-dev):
arctic services create \
--target-peer TARGET_PEER_ID \
--uplink-dev tailscale0curl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_peer_id": "TARGET_PEER_ID",
"transport_type": "tcp",
"uplink_dev": "tailscale0"
}'The update endpoint (PUT /v1/services/{id}) accepts the same
uplink_dev and interface_parent_dev fields; an empty string
clears a pin.
Both fields are local to the source host, and an empty value keeps auto-detection. They can be changed on a live service, where an empty value clears the pin:
arctic services update SERVICE_ID --uplink-dev ens19
arctic services update SERVICE_ID --uplink-dev ""Both fields show in the arctic services get output. See Uplinks and device selection for when to use each pin, including routing a service over tailscale, wireguard, or a second WAN NIC.
With Multipath TCP
Opt a TCP-transport service's tunnel dials into MPTCP, so one tunnel can spread across more than one path:
arctic services create \
--target-peer TARGET_PEER_ID \
--mptcp \
--mptcp-subflows 2curl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_peer_id": "TARGET_PEER_ID",
"transport_type": "tcp",
"mptcp": {"enabled": true, "subflows": 2}
}'--mptcp-subflows sets the requested path-manager subflow count: 0 means the engine default of 2, and the maximum is 8. MPTCP is TCP-only; combining --mptcp with --transport kcp is rejected at create time. The kernel on both peers must support MPTCP, and dials fall back to plain TCP silently when either end lacks it; see Multipath TCP for the kernel prerequisites.
With bandwidth limit
Apply a QoS bandwidth limit (in Mbps):
arctic services create \
--target-peer TARGET_PEER_ID \
--bandwidth-limit 1000curl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_peer_id": "TARGET_PEER_ID",
"transport_type": "tcp",
"qos": {"bandwidth_limit_mbps": 1000}
}'The bandwidth limit is in Mbps. Set to 0 for unlimited. See Set bandwidth limits below for details.
With fully transparent mode
Enable transparent mode to preserve the original source IP address at the destination:
arctic services create \
--target-peer TARGET_PEER_ID \
--fully-transparent \
--requires-interfacecurl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_peer_id": "TARGET_PEER_ID",
"transport_type": "tcp",
"fully_transparent": true,
"requires_interface": true
}'This is useful when the destination needs to see the real client IP for logging, access control, or rate limiting. See Transparent Mode for details.
Transparent mode requirements
Transparent mode requires --requires-interface and only works with
TCP traffic.
With KCP transport
Use KCP instead of TCP for the underlying transport:
arctic services create \
--target-peer TARGET_PEER_ID \
--transport kcpcurl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_peer_id": "TARGET_PEER_ID",
"transport_type": "kcp"
}'KCP recovers from packet loss without backing off the way TCP does, and it starts fast without a slow warm-up, so it suits lossy links and short or interactive flows. High latency makes the short-flow case stronger, but a clean, high-latency link carrying a bulk transfer still favours TCP. See Choosing a transport for the full reasoning.
From a specific source peer
By default, the source peer is the local agent. To create a service from a different peer:
arctic services create \
--source-peer SOURCE_PEER_ID \
--target-peer TARGET_PEER_IDcurl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"source_peer_id": "SOURCE_PEER_ID",
"target_peer_id": "TARGET_PEER_ID",
"transport_type": "tcp"
}'Complete example
Create a fully-configured service:
arctic services create \
--target-peer peer_01HXYZDEF789... \
--transport tcp \
--requires-interface \
--desired-ipv4 192.168.100.10/24 \
--bandwidth-limit 1000curl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_peer_id": "peer_01HXYZDEF789...",
"transport_type": "tcp",
"requires_interface": true,
"desired_interface_ipv4": "192.168.100.10/24",
"qos": {"bandwidth_limit_mbps": 1000}
}'After creating a service
A service alone does not route traffic. You must add routes to specify which traffic should use the service. See Configure routes below for instructions on adding routing rules.
Troubleshooting
Target peer not found
If the target peer ID is not found:
- Verify the peer exists:
arctic peers list - Ensure the peer has completed handshake (not just discovered)
Service limit exceeded
If you receive a service limit error:
- Check your license:
arctic license status - Delete unused services or upgrade your license
Interface creation failed
If the MACVLAN interface fails to create:
- Check agent logs:
journalctl -u arctic | grep netmgr - Verify the host has a suitable parent interface
- Ensure the agent has root privileges
Configure routes
This section shows you how to add, update, and manage routing rules for Arctic services. Routes determine which traffic flows through a service based on source and destination CIDR blocks.
Before you start
Ensure you have:
- An existing service (see Create a service above)
- The service ID
- Knowledge of the networks you want to route
Routes live on the source peer of a service. Route IDs are numeric.
Add a route
A route needs a --priority greater than 0 and at least one of --source-cidr or --dest-cidr.
Basic route
Add a route matching specific source and destination networks:
arctic routes add --service SERVICE_ID \
--source-cidr 10.0.0.0/8 \
--dest-cidr 192.168.100.0/24 \
--priority 100curl -k -X POST https://AGENT_IP:8080/v1/services/SERVICE_ID/routes \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"routes": [{
"source_cidr": "10.0.0.0/8",
"dest_cidr": "192.168.100.0/24",
"priority": 100
}]
}'The request body carries a routes array, so you can add several
routes in one call. The response returns the new route_ids and a
count.
Destination-only route
Route all traffic to a specific destination regardless of source:
arctic routes add --service SERVICE_ID \
--dest-cidr 192.168.100.0/24 \
--priority 100curl -k -X POST https://AGENT_IP:8080/v1/services/SERVICE_ID/routes \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"routes": [{
"dest_cidr": "192.168.100.0/24",
"priority": 100
}]
}'Source-only route
Route all traffic from a specific source regardless of destination:
arctic routes add --service SERVICE_ID \
--source-cidr 10.0.0.0/8 \
--priority 100curl -k -X POST https://AGENT_IP:8080/v1/services/SERVICE_ID/routes \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"routes": [{
"source_cidr": "10.0.0.0/8",
"priority": 100
}]
}'Understanding priority
Routes are matched by specificity first, then priority breaks ties.
Lower priority value wins
A LOWER priority value means a HIGHER-priority route. Priority 100 beats priority 200 when specificity is equal.
Specificity is evaluated in this order (most specific first):
- MACVLAN interface match
- Source CIDR + destination CIDR match
- Source-only CIDR match
- Destination-only CIDR match
When two routes have the same specificity, the one with the lower priority value is chosen.
Example ordering (most preferred first):
| Priority | Source CIDR | Dest CIDR | Reason |
|---|---|---|---|
| 100 | 10.1.0.0/16 | 192.168.100.0/24 | More specific source (src+dest match) |
| 200 | 10.0.0.0/8 | 192.168.100.0/24 | Less specific source |
| 100 | (none) | 192.168.0.0/16 | Dest-only match, least specific |
List routes
View all routes for a service:
arctic routes list --service SERVICE_IDcurl -k -X GET https://AGENT_IP:8080/v1/services/SERVICE_ID/routes \
-H "Authorization: Bearer $TOKEN"The response wraps the list under a routes array.
Update a route
Modify an existing route's priority or CIDR:
arctic routes update --service SERVICE_ID --route ROUTE_ID \
--priority 50curl -k -X PUT https://AGENT_IP:8080/v1/services/SERVICE_ID/routes/ROUTE_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"priority": 50,
"source_cidr": "10.0.0.0/8",
"dest_cidr": "192.168.100.0/24"
}'You can also update the CIDRs from the CLI:
arctic routes update --service SERVICE_ID --route ROUTE_ID \
--source-cidr 10.1.0.0/16 \
--dest-cidr 192.168.200.0/24Delete a route
Remove a route from a service:
arctic routes delete --service SERVICE_ID --route ROUTE_ID
# Use --yes to skip confirmation:
arctic routes delete --service SERVICE_ID --route ROUTE_ID --yescurl -k -X DELETE https://AGENT_IP:8080/v1/services/SERVICE_ID/routes/ROUTE_ID \
-H "Authorization: Bearer $TOKEN"A successful delete returns HTTP 204 with no body.
Common routing patterns
Route all traffic to a service
Route everything from one network to another:
arctic routes add --service SERVICE_ID \
--source-cidr 10.0.0.0/8 \
--dest-cidr 0.0.0.0/0 \
--priority 100Multiple routes for different subnets
Add routes for different destination subnets:
# Route to subnet A
arctic routes add --service SERVICE_ID \
--dest-cidr 192.168.100.0/24 \
--priority 100
# Route to subnet B
arctic routes add --service SERVICE_ID \
--dest-cidr 192.168.200.0/24 \
--priority 100Override with a higher-priority route
Add a more specific route, then give it a lower priority value so it wins the tie:
# General route (lower priority: higher value)
arctic routes add --service SERVICE_ID \
--source-cidr 10.0.0.0/8 \
--dest-cidr 192.168.0.0/16 \
--priority 200
# Specific override (higher priority: lower value)
arctic routes add --service SERVICE_ID \
--source-cidr 10.1.0.0/16 \
--dest-cidr 192.168.100.0/24 \
--priority 100The specific route is already preferred on specificity; the lower priority value reinforces the override when specificity is equal.
Troubleshooting
Routes not taking effect
If traffic is not being routed as expected:
- Trigger a config sync:
arctic cluster sync - Inspect the kernel ruleset:
nft list table inet arctic
Invalid CIDR notation
Ensure CIDRs are valid:
- Use slash notation:
10.0.0.0/8not10.0.0.0 - The network address must match the mask:
10.0.0.0/8not10.1.2.3/8
Priority must be greater than zero
arctic routes add requires a --priority value greater than 0.
Route not found
If a route ID is not found:
- List routes to verify:
arctic routes list --service SERVICE_ID - Ensure you are using the correct service ID
Set bandwidth limits
This section shows you how to configure bandwidth limits on Arctic services for Quality of Service (QoS) control.
How limits apply to TCP and KCP
On a TCP service, bandwidth_limit_mbps drives the full traffic shaper on the
in-process TProxy data plane (fair queueing and latency control). On a KCP
service it acts as a bandwidth ceiling that caps the link in both directions,
without the shaper's fair queueing. The other qos fields (RTT and memory
tuning) apply to TCP shaping only.
Before you start
Ensure you have:
- An existing service or the information to create one
- Knowledge of the desired bandwidth limit in Mbps
Set bandwidth during service creation
Specify a bandwidth limit when creating a new service:
arctic services create \
--target-peer TARGET_PEER_ID \
--bandwidth-limit 1000curl -k -X POST https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_peer_id": "TARGET_PEER_ID",
"transport_type": "tcp",
"qos": {"bandwidth_limit_mbps": 1000}
}'The value is in megabits per second (Mbps). 1000 means 1 Gbps.
Update bandwidth on an existing service
Change the bandwidth limit on an existing service:
arctic services update SERVICE_ID --bandwidth-limit 500curl -k -X PUT https://AGENT_IP:8080/v1/services/SERVICE_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"bandwidth_limit_mbps": 500}'Remove the bandwidth limit
Set the limit to 0 to remove the restriction (unlimited):
arctic services update SERVICE_ID --bandwidth-limit 0curl -k -X PUT https://AGENT_IP:8080/v1/services/SERVICE_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"bandwidth_limit_mbps": 0}'Verify current bandwidth
Check the current bandwidth setting:
arctic services get SERVICE_IDcurl -k -X GET https://AGENT_IP:8080/v1/services/SERVICE_ID \
-H "Authorization: Bearer $TOKEN"Look for the bandwidth_limit_mbps field in the output.
Common bandwidth values
| Use case | Bandwidth | Value |
|---|---|---|
| Low priority | 100 Mbps | 100 |
| Standard | 1 Gbps | 1000 |
| High throughput | 10 Gbps | 10000 |
| Unlimited | No limit | 0 |
How bandwidth limiting works
For a TCP service Arctic shapes traffic on the TProxy data plane: it holds the traffic at the configured limit, shares capacity fairly between flows, and keeps latency low when the link is busy. For a KCP service the limit is enforced as a bandwidth ceiling in both directions rather than full shaping, so it caps total throughput but does not queue flows fairly or manage latency.
Bandwidth limits apply to traffic flowing through the service. They do not affect traffic that does not match the service's routes.
Troubleshooting
Bandwidth not being enforced
If traffic exceeds the configured limit:
- Confirm QoS is enabled for the target peer:
arctic peers get PEER_ID - Trigger a config sync:
arctic cluster sync - Check agent logs:
journalctl -u arctic
Performance lower than expected
If throughput is below the configured limit:
- Check for network bottlenecks elsewhere in the path
- Verify the underlying network can support the desired bandwidth
- Consider TCP tuning for high-bandwidth scenarios
Delete a service
This section shows you how to delete an Arctic service and clean up its associated configuration.
Before you start
Understand that deleting a service:
- Removes all routes associated with the service
- Removes the MACVLAN interface (if one was created)
- Updates firewall rules to stop routing traffic
- Does not affect the source or target peers
Steps
1. Find the service ID
List services to find the ID:
arctic services listcurl -k -X GET https://AGENT_IP:8080/v1/services \
-H "Authorization: Bearer $TOKEN"The response wraps the list under a services array.
2. Delete the service
arctic services delete SERVICE_ID
# You will be prompted to confirm. Use --yes to skip confirmation:
arctic services delete SERVICE_ID --yescurl -k -X DELETE https://AGENT_IP:8080/v1/services/SERVICE_ID \
-H "Authorization: Bearer $TOKEN"A successful delete returns HTTP 204 with no body.
3. Verify deletion
Confirm the service was removed:
arctic services listThe service should no longer appear in the list.
What happens when you delete
When a service is deleted:
- Routes removed: All CIDR-based routing rules are deleted.
- Interface cleaned up: The MACVLAN interface is removed from the host.
- Firewall updated: The agent re-commits the nftables ruleset so it no longer matches traffic for this service.
- Data plane updated: The in-process TProxy and IP-tunnel engines are reconfigured.
Configuration changes propagate automatically, typically within seconds.
Recreating a deleted service
A deleted service leaves a tombstone behind, and the name stays bound to it. Creating a new service with the same name against the same source and target peers revives the old service under its original ID; as of v1.4.2 the revival preserves the create request's device pins along with the previous VLAN tag and MAC mode, rather than clearing them. Creating the same name against a different source or target peer mints a fresh service ID instead.
Bulk deletion
To delete several services, filter the list to JSON and loop over the IDs. The --json output is a top-level array of service objects, each with an id field:
# Delete all services targeting a specific peer
for id in $(arctic services list --target-peer PEER_ID --json | jq -r '.[].id'); do
arctic services delete "$id" --yes
doneTroubleshooting
Service not found
If the service ID is not found:
- Verify the service exists:
arctic services list - Check you are connected to the correct agent
- The service may have already been deleted
Interface not removed
If the MACVLAN interface persists after deletion:
- Check the network reconciler logs:
journalctl -u arctic | grep netmgr - Manually remove if needed:
ip link delete INTERFACE_NAME - Trigger a sync:
arctic cluster sync
Routes still active
If traffic is still being routed after deletion:
- Trigger a config sync:
arctic cluster sync - Inspect the kernel ruleset:
nft list table inet arctic - Check agent logs for config updates:
journalctl -u arctic