When using an internal IP for self hosted livekit ...
# ask-ai
d
When using an internal IP for self hosted livekit server. The agent is able to receive the request but not able to connect to the livekit server. {"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 547, "elapsed_time": 2.29, "timestamp": "2026-03-27T105210.500794+00:00"} The room connection was not established within 10 seconds after calling job_entry. This might mean that job_ctx.connect() was never invoked, or that no AgentSession with an active RoomIO has been started. The room connection was not established within 10 seconds after calling job_entry. This might mean that job_ctx.connect() was never invoked, or that no AgentSession with an active RoomIO has been started. {"message": "The room connection was not established within 10 seconds after calling job_entry. This might mean that job_ctx.connect() was never invoked, or that no AgentSession with an active RoomIO has been started.", "level": "WARNING", "name": "livekit.agents", "room": "test-room-006", "job_id": "AJ_DC7yZpHBAsaX", "worker_id": "AW_eQPYqG4rLdtY", "pid": 112, "room_id": "RM_C7sGcnGvijob", "timestamp": "2026-03-27T105218.205196+00:00"} livekit:rtc engine407livekit:rtc_engine - failed to connect: Connection("wait_pc_connection timed out"), retrying... (1/3) livekit:rtc engine407livekit:rtc_engine - failed to connect: Connection("wait_pc_connection timed out"), retrying... (1/3) {"message": "livekit:rtc engine407livekit:rtc_engine - failed to connect: Connection(\"wait_pc_connection timed out\"), retrying... (1/3)", "level": "WARNING", "name": "livekit", "room": "test-room-006", "job_id": "AJ_DC7yZpHBAsaX", "worker_id": "AW_eQPYqG4rLdtY", "pid": 112, "room_id": "RM_C7sGcnGvijob", "timestamp": "2026-03-27T105223.217134+00:00"} process exiting
✅ 1
t
Thanks for reaching out, akshat. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I can see you're hitting the repeated timeout errors when an agent tries to connect to your self‑hosted LiveKit server using an internal IP. I’ll help you narrow this down and get the room connection established. Quick summary of what the logs show • Agent receives the job but the room connection isn't established within 10s: "The room connection was not established within 10 seconds after calling job_entry". • WebRTC peer connection times out: "failed to connect: Connection("wait_pc_connection timed out")". • Process exits after retries. What this usually means • Either
ctx.connect()
(or equivalent agent connect call) was not invoked or completed, or • The agent can reach the server for signaling but the WebRTC/media path (ICE/DTLS) cannot be completed because of network/firewall/NAT issues. Steps to check now (quick checklist) 1. Confirm the agent actually calls connect() ◦ Ensure your job entrypoint executes
await ctx.connect()
(or your agent’s
job_ctx.connect()
equivalent) and that code path is reached. If you can share the relevant snippet, I can check it for you. 1. Verify the
LIVEKIT_URL
and signaling reachability ◦ Confirm
LIVEKIT_URL
is set to the internal IP and port (example:
ws://192.168.x.x:7880
). ◦ From the agent host, test connecting to that URL:
telnet <ip> <port>
or
curl
the WebSocket endpoint to confirm TCP reachability. 1. Confirm UDP and ICE traffic is allowed ◦ WebRTC requires UDP (and sometimes TCP fallback) for ICE candidates. Firewalls often allow TCP/443 but block UDP, causing peer connection timeouts. ◦ Verify firewalls (host OS, cloud security groups, network ACLs) allow the LiveKit media port range you configured and UDP traffic between agent and server. 1. Test connectivity with a connection test tool ◦ Run the LiveKit Connection Test / ConnectionChecker from the agent machine to isolate if WebSocket works but WebRTC/ICE fails. This shows whether signaling vs media path is failing. 1. Check server visibility & routing ◦ If your LiveKit server is on a different subnet or behind network segmentation, confirm routing exists both ways (agent → server and server → agent for ICE if needed). ◦ If using an internal IP, ensure your server advertises reachable ICE candidates (if you have custom IP configs). 1. Review server logs for ICE/candidate errors ◦ On the LiveKit server, look for ICE/DTLS candidate or ICE negotiation errors that match the agent connection attempts. Those logs can confirm the nature of the connectivity failure. If you want, I can help further — please provide: • The agent entrypoint snippet showing where
ctx.connect()
is called (or confirm it’s being called). • The exact
LIVEKIT_URL
value you’re using (mask any sensitive tokens). • Results of a connectivity check from the agent host (ping/telnet/curl output and Connection Test results). • Any firewall rules or cloud security group configs that might be blocking UDP. Next steps I can take once you share that: • Review your agent connect code for missing or misordered calls. • Interpret connection test logs to pinpoint signaling vs media path failure. • Suggest exact firewall port ranges to open or NAT/ICE candidate changes to make. You’re on the right track — with the above checks we can identify whether this is a code/connect call issue or a network/UDP/ICE problem and resolve it. Sources: Simple Call Answering Agent | LiveKit Documentation | MCP Agent | LiveKit Documentation | server-sdk-js | livekit/livekit_composite | DeepWiki | Diagnosing Connection Errors with Connection Test Utility | Firewall Tips for LiveKit Connectivity | LiveKit, with interactive elements
d
Livekit url is set to wss://livekit.domain.com without port. Does that need to change to ws: with port, to something like ws://livekit.internal.domain.com:7880? The agent can reach the server for signaling but the WebRTC/media path (ICE/DTLS) cannot be completed because of network/firewall/NAT issues. Probably this?
t
Thanks — that’s a helpful detail. I’ll address both parts: whether you should change the LiveKit URL, and whether the ICE/DTLS timeouts you’re seeing are likely caused by network/firewall/NAT. Short answer • You do not necessarily need to change to
ws://
with a port if
wss://livekit.domain.com
resolves and is reachable from the agent. However, if the agent must reach the server via an internal hostname/IP that differs from the public URL (for example the server is reachable only on an internal address), then you should set
LIVEKIT_URL
to that internal address including the correct scheme and port (e.g.,
ws://livekit.internal.domain.com:7880
or
wss://livekit.internal.domain.com:7880
depending on whether TLS is used). • The timeout errors and "wait_pc_connection timed out" strongly indicate the WebRTC/media path (ICE/DTLS) is failing — most commonly caused by firewall/NAT/UDP blocking or incorrect ICE candidate configuration. What to consider when choosing URL and port • Scheme (ws:// vs wss://) ◦ Use
wss://
when the agent must use TLS to connect to the signaling endpoint. Use
ws://
when TLS is not used and you have plain WebSocket connectivity inside a trusted network. • Hostname/IP ◦ If the agent is inside the same private network and the public DNS name resolves to a public IP (or through a load balancer) that the agent cannot reach, point
LIVEKIT_URL
to an internal hostname or IP reachable from the agent. • Port ◦ Include the port if your LiveKit signaling server is listening on a non-default port. Example:
ws://192.168.1.10:7880
or
wss://livekit.internal.domain.com:7880
. Ports and traffic you must allow • Signaling (WebSocket): TCP port for your websocket endpoint (defaults depend on your setup). If using wss:// on standard TLS, TCP/443. • WebRTC media / ICE: ◦ UDP for ICE candidates/media (the typical path). If using a configured UDP range on LiveKit, ensure that range is open between agent and server. ◦ TURN (if you use TURN) requires TCP/UDP ports for the TURN server and, if using fallback, TCP/443 may be used. • Common failure mode: TCP (WebSocket) works, but UDP is blocked — signaling succeeds but ICE never completes → peer connection timeout. Practical checks to run now 1. From the agent host: ◦ Confirm the websocket endpoint is reachable with your chosen URL: curl or a websocket client to
wss://livekit.domain.com
(or
ws://<internal>:<port>
). ◦ Test UDP reachability to the server media ports (you can use tools like
nc -u
or a simple STUN/TURN probe). 1. Verify DNS: ensure the agent resolves
livekit.domain.com
to the address you expect. If it resolves to a public IP but you need the internal IP, point to the internal hostname/IP instead. 2. Inspect firewall rules/security groups on both sides: allow the LiveKit media port range (UDP) and signaling port (TCP). Confirm no intermediate network device is blocking UDP. 3. If you use TLS and
wss://
internally, confirm the certificate is valid for the internal hostname or configure the agent to trust the cert (or use
ws://
on a trusted internal network). 4. Check LiveKit server logs for ICE/candidate negotiation errors at the time the agent tries to connect — they often show which candidate types are failing. If you want, tell me: • The exact
LIVEKIT_URL
currently set on the agent (mask secrets). • Whether TLS is terminated before LiveKit (LB/ingress) or LiveKit handles TLS directly. • The UDP port range configured for LiveKit media (if known) and any TURN usage. With that I can suggest the exact
LIVEKIT_URL
you should use and the firewall/port rules to open so ICE/DTLS can complete. , with interactive elements
d
nginx handles TLS UDP port range: 10k-20k Agent to server - All traffic is allowed through private IP
t
Thanks — this is very helpful. I’ll respond to the two main points you’ve given: the correct LiveKit URL when nginx handles TLS, and confirming what to check for the ICE/DTLS timeouts given your UDP range and private networking. Short answer / recommendation • Keep using
wss://
in the agent’s
LIVEKIT_URL
if the agent connects through nginx (TLS is terminated at nginx). Use the nginx hostname that the agent can resolve (for example
wss://livekit.internal.domain.com
) and include the port only if nginx is listening on a nonstandard TLS port. If nginx listens on 443 for TLS,
wss://livekit.internal.domain.com
(no port) is correct. • Your UDP media port range (10000–20000) must be reachable between agent and LiveKit server. Even though agent→server TCP (signaling) is allowed, ICE will fail if UDP is blocked. That is the most likely cause of the "wait_pc_connection timed out" errors. Why this setup works • With TLS terminated at nginx, the agent connects to nginx over
wss://
(TLS). Nginx forwards signaling to LiveKit over the internal protocol. WebRTC media/ICE is separate: browsers/agents negotiate ICE candidates and then exchange media on UDP ports (the 10k–20k range you configured). If those UDP ports, or TURN, are not reachable, ICE cannot complete. Concrete checks and actions (ordered) 1. Confirm agent uses the internal TLS hostname the agent resolves: ◦ If nginx listens on 443 for TLS: set LIVEKIT_URL to
wss://livekit.internal.domain.com
. ◦ Only add a port if nginx is on a different TLS port (e.g.,
wss://livekit.internal.domain.com:8443
). 1. Verify DNS/resolution from the agent host: ◦ On the agent host run: resolve or ping
livekit.internal.domain.com
and confirm it returns the private IP you expect. 1. Confirm nginx is proxying websocket signaling correctly: ◦ Ensure nginx is configured to proxy WebSocket upgrades to LiveKit and that headers for WebSocket are preserved. ◦ Confirm nginx proxy_pass target is the LiveKit server (internal IP:port) and that nginx logs show the agent’s connect attempt. 1. Ensure UDP range is reachable both ways: ◦ From agent host, test UDP connectivity to the LiveKit server on a sample port in 10000–20000 (tools:
nc -u
,
socat
, or a STUN probe). ◦ Confirm any host firewalls, OS-level iptables, or network ACLs allow UDP 10000–20000 between agent and server. 1. Check TURN usage (if any): ◦ If your network may still block UDP, consider running a TURN server and configure LiveKit to use it. TURN allows media relay over TCP/TLS (443), which helps when UDP is not permitted. 1. Inspect LiveKit and nginx logs during a failed attempt: ◦ LiveKit logs will show ICE/DTLS candidate/negotiation failures and which candidate types are attempted. ◦ Nginx access/error logs will confirm signaling connections and any proxied errors. What to look for in logs • On LiveKit: ICE candidate errors or messages showing only host candidates (no srflx/prflx/relay) or timeouts during ICE checks. • On the agent: repeated "wait_pc_connection timed out" and no successful RoomIO start. • On nginx: successful WebSocket upgrade entries for the agent’s connect attempts. If you want, I can help next by: • Reviewing your nginx websocket proxy snippet if you paste it here (I’ll point out any missing WebSocket headers or proxy settings). • Suggesting a quick UDP probe command you can run from the agent and what specific output to share. • Reviewing LiveKit log snippets showing ICE candidate negotiation so we can confirm whether candidates are being exchanged and which types are failing. You're very close — with TLS through nginx and the correct internal
wss://
URL plus verified UDP reachability (or a TURN fallback), ICE should complete and the agent will establish the room connection. Which of the next steps above would you like to start with? , with interactive elements
d
sip.yaml and livekit.yaml have the public IP not the internal one and use_external_ip has value True
t
Thanks — that new detail explains a likely root cause. I'll address why those settings matter and give clear next steps to fix the ICE/DTLS timeouts. Why this matters •
use_external_ip: True
and public IP entries in
sip.yaml
and
livekit.yaml
cause the LiveKit server to advertise public host candidates to peers. • If your agent is on the private network and must use the internal address to reach the server, advertising the public IP means the agent attempts ICE with candidates it cannot reach. That will cause ICE/DTLS negotiation to stall and produce the "wait_pc_connection timed out" errors you’re seeing. Recommended change 1. Update configuration to use internal addresses: ◦ Replace the public IP entries in
sip.yaml
and
livekit.yaml
with the LiveKit server’s internal/private IP or internal hostname that your agents resolve and can reach. 1. Disable external IP advertising when running inside the private network: ◦ Set
use_external_ip: False
so the server advertises host/internal candidates rather than the public IP. 1. Restart services after changes: ◦ Restart nginx (if needed) and the LiveKit service so new ICE candidates are advertised on new connections. What to verify after making the change • From the agent, start a connection and check LiveKit logs to confirm ICE candidates being advertised are the internal/private IPs (host candidates). • Confirm the agent receives and attempts those internal candidates and that ICE completes. • If ICE still fails, verify UDP 10000–20000 is reachable both ways between agent and server (host firewalls, network ACLs, etc.). Optional: Mixed environments (internal agents + external clients) • If you also have external clients that must reach the same LiveKit instance, you can: ◦ Keep
use_external_ip: True
but configure the server to advertise both internal and external candidates (if supported), or ◦ Use a TURN server for external clients and internal agents to ensure relay works when direct candidate reachability differs. • If you need help designing this mixed setup, tell me whether most clients are internal or external and whether you already have a TURN server. If you want, I can: • Review the specific lines from your
livekit.yaml
and
sip.yaml
(mask any secrets) and point out the exact fields to change. • Provide the commands you can run on the agent and server to verify the new candidates and UDP reachability. Would you like me to review your config snippets now or give the exact verification commands to run after you make the changes? , with interactive elements
d
Will that affect connections to external SIP providers or for incoming calls?
t
Good question — and a sensible concern. I’ll explain the impact and give safe options so you can fix the internal-agent ICE failures without breaking external SIP/incoming calls. Impact of setting
use_external_ip: False
and advertising internal IPs • If you switch to internal IPs and set
use_external_ip: False
, the LiveKit server will advertise private/internal host candidates. External SIP providers and remote callers will not be able to reach those internal candidates directly, so direct peer connections from the public internet to those internal addresses will fail. • That means incoming SIP calls or external clients that rely on those advertised candidates will not be able to establish media unless you provide an alternate reachable candidate (for example, a public IP/host or a TURN relay). Options to support both internal agents and external SIP providers 1) Preferred: Use both internal host candidates and a reachable external path (TURN or external IP) • Keep internal host candidates for local agents (so internal ICE completes). • Provide a TURN server (recommended): configure LiveKit to use a TURN server so external callers and NATed clients can relay media through TURN when direct connectivity fails. TURN uses TCP/TLS (443) so it works even if UDP is blocked. • If you have to advertise an external IP as well, explore configuring both internal and external candidates if your deployment supports it. Otherwise, TURN covers external connectivity without forcing server to advertise public IPs as host candidates. 2) Split signaling paths (less ideal) • Use internal
LIVEKIT_URL
for agents (internal hostname/IP) while leaving LiveKit configured to advertise external IPs for public clients. This can become complex because the server’s advertised candidates are global; mixed advertisement may still cause unreachable candidates for one side or the other. • Use separate LiveKit instances for internal agents vs public SIP traffic (operationally heavier). 3) Keep
use_external_ip: True
but add explicit handling for internal agents • If you must keep
use_external_ip: True
for SIP providers, but internal agents need internal candidates, you can: ◦ Configure an internal TURN for agents so they can relay media internally. ◦ Or update agent configuration to prefer the internal media route (if your network and LiveKit support advertising multiple candidate types such as host + srflx + relay). Practical recommendation for your scenario • Because agent→server signaling and media must work on the private network, set
use_external_ip: False
and update the YAML to internal IPs/hostnames so agents get reachable host candidates. • Simultaneously configure a TURN server (or ensure your existing TURN is reachable to external callers) and configure LiveKit to supply TURN relay candidates. External SIP providers/incoming calls will then use the TURN relay when direct host/srflx candidates aren’t usable. • This approach preserves internal connectivity and maintains external/incoming call functionality without advertising a public host candidate that breaks agent ICE. Next steps I can help with • Recommend TURN configuration settings for LiveKit and how to add them to
livekit.yaml
. • Review your
livekit.yaml
and
sip.yaml
lines to show exactly what to change (mask secrets). • Provide verification steps to confirm: ◦ Internal agents receive internal candidates and ICE completes. ◦ External SIP calls use TURN/relay and media flows correctly. Which would you like to do next — add TURN config guidance, or paste your config snippets for me to review? , with interactive elements
d
But the external SIP providers having public IP of the server shouldn't have an issue? That should get internally translated automatically?
t
Good question — short answer first: external SIP providers will only succeed if they can actually reach whatever IP address the LiveKit server advertises as a candidate. NAT or routing does not magically make an advertised private IP reachable to the public internet, and advertising the public IP from a server inside a private network does not automatically make that public IP usable by internal agents. Explanation in plain terms • ICE candidates are IP:port values that peers try to contact directly. If LiveKit advertises a public IP but your agent is on a private network and can only reach the server’s private address, the agent will try the public IP and fail — because that public IP won’t route to the agent’s private path. • Conversely, external SIP providers on the internet will try the public IP you advertise. That works only if the public IP is actually reachable from the internet and correctly NATs/forwards to your LiveKit server (for example via your router/load balancer/nginx). If that NAT/port forwarding is set up, external providers can connect to that public IP. • But the server advertising a public IP doesn’t change the network path or rewrite ICE candidates for internal agents. There is no automatic “translate advertised candidate to an internal candidate” during ICE — the server must advertise the candidates that are actually reachable by each peer. What that means for your options • If you set use_external_ip: False and advertise only internal/private candidates: ◦ Internal agents will see reachable host candidates and ICE can succeed. ◦ External SIP providers will not be able to reach those private candidates, so incoming calls from the internet will fail unless you provide a reachable alternative (TURN or public candidate). • If you keep use_external_ip: True and advertise the public IP: ◦ External SIP providers can reach the server if NAT/port forwarding is properly configured. ◦ Internal agents may fail if they cannot reach the public IP from their network (they’ll try unreachable candidates). • NAT at your edge/router does not solve the mismatch for internal agents — they still need candidates that they can route to (internal IPs or a relay they can reach). Recommended safe approach (supports both internal agents and external SIP) • Advertise internal host candidates for internal agents (use_internal/private IPs). • Provide a TURN server (configured in LiveKit) so external callers (and any clients behind restrictive NAT/firewalls) can relay media through TURN. TURN can be reachable on TCP/TLS (443), so it works for clients that can’t use UDP. • Ensure your public SIP providers can reach your public IP + forwarded ports (signaling and/or TURN) so incoming calls will use the public path or TURN as necessary. Concrete next steps I can help with • Guide you to add TURN to
livekit.yaml
and show the minimal config. • Show how to verify from an external host that the public IP and NAT are forwarding correctly. • Review your NAT/port forwarding rules or nginx setup (paste relevant snippets, mask secrets) to confirm external SIP will work while internal agents use private candidates. If you want to keep the public IP advertised for SIP providers, we can add TURN so internal agents still succeed — or we can advertise internal candidates and rely on TURN for external callers. Which approach do you prefer, or would you like help configuring TURN now? , with interactive elements
d
But incoming calls first go through the SIP server? That has a public IP?
t
You're on the right track asking how SIP signaling, public IPs, and ICE candidates interact — good question. Short answer first, then a clear explanation and recommended next steps. Short answer • Yes — incoming SIP calls are first signaled to your SIP server (which typically has a public IP). That lets the caller reach your SIP server. • However, signaling via the SIP server does not automatically make the ICE/media candidates reachable for every peer. Media still requires reachable IP:port candidates (host, srflx, or relay). If LiveKit advertises only private/internal candidates, external callers won’t be able to reach them directly; if LiveKit advertises only the public IP, internal agents might not be able to reach that candidate. The SIP server does not magically translate ICE candidates for the media path. Why (in plain terms) • SIP handles call setup (signaling). During setup, the SIP server exchanges SDP with endpoints so each side learns ICE candidates (IP:port pairs) to use for media. • ICE candidates are then tried directly between peers. There is no implicit “routing” by the SIP server for those direct peer-to-peer attempts. • If the LiveKit server advertises a public IP candidate and NAT/port forwarding is correctly configured, external SIP providers can use that candidate to reach your server. But an internal agent on the private LAN might try that public candidate and fail if that public route does not resolve/route from inside. • Conversely, if LiveKit advertises only private/internal candidates, external callers cannot reach them from the internet unless there is a relay (TURN) or the NAT makes the advertised candidate actually reachable externally. What this means for your deployment • SIP signaling to your public SIP server will still work if the SIP server is reachable and NAT/forwarding for signalling ports is correct. • Media connectivity for external callers depends on: ◦ Whether LiveKit advertises a public candidate that the internet can actually reach (NAT/port forwarding in place), or ◦ Whether a TURN server is available for relaying media when direct connectivity fails. • Media connectivity for internal agents depends on: ◦ LiveKit advertising an internal candidate that the agents can reach, or ◦ Agents being able to use a relay (TURN) that is reachable to them. Recommended approaches (pick one depending on your constraints) 1) Best balance (recommended) • Advertise internal host candidates for internal agents (use_internal/private addresses). • Configure and enable a TURN server in LiveKit for external callers and NATed clients to relay media (TURN reachable on TCP/TLS 443 is especially helpful). • Keep SIP signaling working on the SIP public IP; signaling will reach your SIP server, and SIP will negotiate SDP with LiveKit which will include TURN candidates when necessary. 2) If you prefer advertising public IPs • Keep
use_external_ip: True
and ensure NAT/port forwarding is correctly configured so the advertised public IP genuinely routes to your LiveKit server for media ports. • Understand that internal agents may still fail if their network can't reach the public IP; a TURN relay can still help internal agents connect if needed. 3) If you must use only internal candidates (not recommended alone) •
use_external_ip: False
and internal IPs only. This fixes internal agent connectivity but breaks direct external callers unless you also provide TURN or other relay reachable from the public internet. Practical next steps I recommend 1. Decide which approach you prefer (I recommend option 1). 2. If you choose option 1, add TURN config to
livekit.yaml
and ensure the TURN server is reachable from both external callers and internal agents (TCP/TLS 443 recommended). 3. Verify: ◦ External SIP caller can reach your SIP server and that NAT/forward rules forward signalling and, if applicable, TURN traffic. ◦ LiveKit advertises host + TURN candidate(s) as needed (check LiveKit logs/SDP). ◦ Internal agent receives internal candidates (host) and ICE completes. 1. If you want, paste your
livekit.yaml
and
sip.yaml
network-relevant lines (mask secrets) and I’ll point out exact fields to change and provide the minimal TURN config. If you'd like, I can now: • Show the minimal TURN configuration snippet for
livekit.yaml
. • Review your NAT/port-forwarding checklist to ensure external SIP providers can reach your server. • Review the lines in your config files and nginx proxy snippet to confirm signalling and TURN proxying are correct. Which would you like to do next? , with interactive elements
d
External candidates do not connect to livekit server, so it shouldn't matter which ip:port it advertises for SIP
t
Thanks — that clarifies things. You’re right that if external candidates can’t connect directly to the LiveKit server, those external peers will fall back to TURN (if configured). A few important points to make the behavior and choices clear so you can pick the right fix: 1) Signaling vs media • SIP signaling (call setup) goes through your SIP server/public IP, which is fine. But after signaling, media is sent using ICE candidates (IP:port pairs). Signaling working does not automatically make media reachable. 2) What it means that external candidates “do not connect” • If the server advertises public candidates that external peers try and fail to reach, the external peers will need a relay (TURN) to send/receive media. • If TURN is not available or reachable, external calls will fail for media even if signalling succeeds. 3) Why the advertised IP still matters • For internal agents: they must see candidates that are reachable from their network (internal IPs) or they must be able to use a reachable relay (TURN). • For external callers: they need either a publicly routable candidate (public IP + NAT/forwarding) or TURN. • So the advertised IP affects which path peers attempt first. If you advertise only internal IPs, external callers won’t be able to reach them. If you advertise only public IPs, internal agents might attempt unreachable public paths. 4) Recommended approach for mixed internal + external usage • Configure LiveKit to advertise internal host candidates (use_internal/private IPs) for internal agents to succeed. • Also configure a TURN server and add it to
livekit.yaml
. External callers (and any NATed clients) will then use TURN as the relay when direct candidates aren’t reachable. • Ensure TURN is reachable from the public internet (for external callers) and from your internal agents (so either side can use it). 5) Quick verification steps you can run now • From an external host: confirm whether the advertised public IP + media ports actually forward to LiveKit (try STUN/TURN probe or check LiveKit logs for incoming external ICE attempts). • From an internal agent: confirm it receives internal host candidates from LiveKit after you switch to internal IPs and that ICE completes. • Check LiveKit logs for candidate types shown (host vs srflx vs relay) and ICE negotiation failures. If you’d like, I can: • Show the minimal TURN snippet to add to
livekit.yaml
. • Review the relevant lines of your
livekit.yaml
/
sip.yaml
(mask credentials) and point out exactly what to change. • Provide commands to probe TURN and UDP media ports from both internal and external hosts. Which would be most helpful next — TURN config, a config review, or the probe commands? , with interactive elements
d
Self hosted livekit server, livekit sip and agents.
t
Thanks — great summary of your environment. I’ll give a concise, actionable checklist tailored to a self‑hosted LiveKit + SIP + agents deployment so you can restore internal agent connectivity without breaking incoming external SIP calls. Brief summary of the problem you described • Agents on the private network cannot complete ICE; logs show "wait_pc_connection timed out." • Nginx terminates TLS. UDP media port range is 10000–20000. Agents can reach server signaling but media/ICE fails. • Your configs currently advertise the public IP (use_external_ip: True), which causes the server to hand out candidates external peers may not be able to use. What to do (ordered steps) 1) Make the server advertise reachable candidates for your agents • In
livekit.yaml
and
sip.yaml
, change host entries to the server’s internal/private IP or internal hostname that agents resolve. • Set
use_external_ip: False
so LiveKit advertises internal host candidates that your agents can reach. 2) Provide a relay for external callers (TURN) • Configure a TURN server and add it to
livekit.yaml
. TURN provides relay candidates (relay) so external SIP providers or NATed clients can send/receive media even if direct public/private candidate reachability is mismatched. • Prefer TURN on TCP/TLS (443) so it works from restrictive networks. 3) Keep nginx for signaling/TLS • Continue using
wss://
against the nginx hostname that your agents resolve. If nginx listens on 443, no port is needed in
LIVEKIT_URL
. • Ensure nginx proxies WebSocket upgrades to LiveKit correctly. 4) Ensure UDP ports are open and reachable internally • Confirm host firewalls and network ACLs allow UDP 10000–20000 between agents and the LiveKit server. • Verify no intermediate device is blocking UDP even if TCP is OK. 5) Verify external SIP path for incoming calls • Your SIP server (public IP) handles signaling from external providers. That still works. • For media: ensure either ◦ NAT/port forwarding for the public IP forwards the media ports correctly to LiveKit (if you want to rely on public host candidates), OR ◦ TURN is available and reachable by external callers so media relays when direct connectivity fails. 6) Restart and test • Restart LiveKit (and nginx if changed). • From an internal agent host, confirm
LIVEKIT_URL
resolves to the internal hostname and that the agent receives internal host candidates in SDP and completes ICE. • From an external host, confirm external caller either connects via the public candidate (if NAT/forwarding in place) or via TURN (if configured). Verification commands and logs to check • DNS/resolve: ping/host/nc to confirm the internal hostname resolves to the private IP. • WebSocket signaling: curl or websocket client to
wss://livekit.internal.domain.com
to confirm signaling reachability. • UDP probe: use
nc -u
or
socat
to test UDP to a sample port in 10000–20000 from agent → server. • LiveKit logs: look for ICE candidate types (host vs srflx vs relay) and any ICE timeouts. • Nginx logs: verify WebSocket upgrade success entries for agent connections. Why this approach works • Internal agents get candidates they can actually reach (host/internal IPs) so ICE can complete. • External callers still succeed because TURN provides a reachable relay when direct host/srflx candidates aren’t usable. • This avoids trying to advertise a single candidate set that will be unreachable to one side or the other. I can help with next steps • Provide the minimal TURN config snippet to add to
livekit.yaml
. • Review the relevant lines from
livekit.yaml
,
sip.yaml
, and your nginx websocket proxy (mask secrets) and point to exact fields to change. • Give exact probe commands you can run from internal and external hosts and tell you what output to share. Which would you like next — TURN config, config review, or the probe commands? , with interactive elements
d
Who are external callers? Do they also use livekit libraries/infra?
t
Great question — thanks for clarifying. Short answer: external callers (the people dialing from the public phone network) do NOT use LiveKit client libraries; they connect via SIP trunks and are bridged into LiveKit as SIP participants. More detail so it’s clear how signaling and media flow work in your setup • External callers: Typically callers on PSTN or SIP providers. They originate the call through the provider, which forwards SIP INVITEs to your SIP server/LiveKit SIP endpoint. • They do not run LiveKit SDKs. Their audio is carried as RTP over the SIP trunk and LiveKit represents them inside the room as SIP participants (so they can publish/subscribe to audio tracks like other participants). • Signaling vs media: ◦ Signaling (call setup) goes through the SIP server/public IP — that part will work if the SIP provider can reach your SIP endpoint. ◦ Media (audio) uses ICE candidates (IP:port pairs) negotiated in SDP. Those candidates must be reachable by the remote peer for media to flow. • What that implies for your IP/candidate choices: ◦ Advertising a public IP as a host candidate only helps external callers if that public IP actually forwards RTP to your LiveKit server (NAT/port forwarding or load balancer configured). ◦ Advertising internal/private IPs helps internal agents, but external callers cannot reach those private addresses unless you provide a relay (TURN) or proper NAT forwarding. ◦ The SIP server’s role is signaling; it does not automatically translate or proxy ICE candidates for media. Media connectivity is handled by direct peer-to-peer attempts or via TURN relay. Which behavior you’ll see in practice • If external candidates cannot connect directly to LiveKit (no public route or blocked UDP), external callers will need a TURN relay to send/receive media. If TURN is configured and reachable by external callers, media will work even when direct host/srflx candidates fail. • Internal agents need candidates they can reach (internal host candidates), or a TURN server reachable from the internal network. Recommended approach for mixed environment (internal agents + external callers) • Make LiveKit advertise internal host candidates for your agents (set internal IPs and
use_external_ip: False
). • Configure a TURN server in
livekit.yaml
and ensure it’s reachable by external callers (TCP/TLS 443 recommended). External SIP providers will then use TURN if direct media fails. • Keep your SIP server/public IP for signaling — incoming calls still arrive there and get bridged into LiveKit; TURN handles media relay where needed. If you want, I can: • Show a minimal TURN config snippet to add to
livekit.yaml
. • Review your SIP and LiveKit config lines (mask credentials) and point out exactly what to change so internal agents get internal candidates while external callers use TURN. • Provide commands to test from an external host whether the public IP and TURN are reachable. Which would you like to do next? Sources: Introducing LiveKit Phone Numbers: zero to ringing in 60 seconds | LiveKit | Agents telephony integration | LiveKit Documentation | sip | livekit/livekit_composite | DeepWiki | livekit/livekit_composite | DeepWiki, with interactive elements
d
So the order of connection is first to SIP server, then to livekit server. Then if SIP is already connected, what does it matter what IP:Port of SIP the livekit server advertises?
t
Good question — it’s the right one to ask when debugging this kind of setup. Short answer • Even after SIP signaling succeeds, the IP:port that LiveKit advertises still matters because media (RTP/DTLS/SRTP) is established using ICE candidates. SIP handles signaling only; media uses the IP:port pairs exchanged in SDP. If the advertised candidate is not reachable by the caller/agent, media fails even though signaling worked. Why signaling vs media are separate • SIP signaling: the SIP server (public IP) receives INVITEs and handles call setup. That gets the call into LiveKit. • Media path: after setup, the peers exchange SDP containing ICE candidates (host, srflx, relay). Peers then probe those candidates and establish a media path (ICE → DTLS → SRTP). This is a separate network flow from signaling. Concrete examples of failure modes • LiveKit advertises a public IP, but an internal agent is on a private network. The agent tries that public candidate and cannot reach it → ICE times out (your "wait_pc_connection timed out"), even though SIP signaling already completed. • LiveKit advertises only private/internal IPs. External SIP providers on the internet cannot reach those candidates → their media will fail unless they use TURN or the public IP is NAT‑forwarded to your server. • SIP server does not automatically proxy or rewrite ICE candidates for media — it only brokers signaling. There’s no implicit “translation” of advertised candidates that makes an unreachable candidate become reachable. What actually makes media work • The advertised candidate must be reachable by the remote peer (directly or via relay). ◦ Direct reachability: proper public IP + NAT/port forwarding for external callers, or a reachable internal IP for internal agents. ◦ Relay: a TURN server that provides relay candidates (relay) reachable by both sides — commonly used to bridge internal vs external reachability gaps. Recommended approach for your mixed environment • Make LiveKit advertise internal host candidates for your agents (use internal IP/hostname; set
use_external_ip: False
) so internal agents get candidates they can actually reach. • Configure a TURN server in
livekit.yaml
and ensure it’s reachable from the public internet (use TCP/TLS 443) so external SIP providers can relay media when direct candidates aren’t reachable. • Keep the SIP server/public IP for signaling — that stays as the entry point for incoming calls, but media will flow per ICE negotiation (direct or via TURN). If you want, I can: • Show the minimal TURN snippet to add to
livekit.yaml
. • Review the SDP/LiveKit logs to confirm which candidate types are being sent and which are failing. • Provide a short checklist of commands to run from an internal agent and an external host to verify candidate reachability. Which of those would help you most next? Sources: Firewall Tips | Understanding TLS in SIP | LiveKit, with interactive elements
s
If sip server has been configured with use_external_ip false, and an external caller initiates connection with the sip server, will the connection be accepted or rejected?
t
Short answer: The SIP server will still accept the incoming SIP INVITE from an external caller. Setting
use_external_ip: false
does not cause the SIP signaling to be rejected. What it affects is which ICE candidates LiveKit advertises for the media path — and that can cause media to fail if external peers can’t reach those candidates. Details that matter • Signaling vs media are separate: ◦ SIP signaling (INVITE/OK/ACK) is handled by your SIP server and will succeed if the provider can reach that SIP endpoint. ◦ After signaling, media is established via ICE candidates exchanged in SDP.
use_external_ip: false
tells LiveKit to advertise internal/host candidates (private IPs). • Effect of
use_external_ip: false
for external callers: ◦ External callers can still register/send INVITEs to your SIP server and the call will be accepted at the signaling level. ◦ External callers will receive SDP that contains the advertised candidates. If those are internal/private IPs and the caller cannot route to them, the media path (RTP/DTLS/SRTP) will fail unless an alternate reachable candidate exists. • How media can still succeed for external callers: ◦ NAT/port-forwarding: If the public IP is forwarded correctly to your LiveKit server and you advertise a public srflx candidate (or your networking provides srflx for the same IP/ports), external media can reach the server. ◦ TURN relay: Configure a TURN server and add it to
livekit.yaml
. External callers will use TURN relay candidates when direct host/srflx candidates aren’t reachable. TURN on TCP/TLS (443) is most reliable through restrictive networks. Practical recommendation • If you set
use_external_ip: false
to ensure internal agents get reachable host candidates, also configure TURN for external callers so media will be relayed when direct connectivity fails. This keeps signaling working for external SIP providers while ensuring media can flow for both internal agents and external callers. If you want, I can: • Provide the minimal TURN configuration snippet for
livekit.yaml
. • Show commands to test from an external host whether media can reach your server (or whether TURN is being used). • Review the relevant lines from your
livekit.yaml
and
sip.yaml
(mask any credentials) and point out exactly what to change. Which would you like next? , with interactive elements