-
Notifications
You must be signed in to change notification settings - Fork 792
GUACAMOLE-2138: Add connection-timeout parameter #1117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
* Add defaults for parameter connection-timeout in guacamole.properties to disable feature. * Implement the connection timeout using a Map based on creation time * Integrate the connection timeout check into the existing idle timeout function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds a connection timeout feature to automatically terminate connections after a specified duration, regardless of activity level. This addresses security and auditing requirements for ensuring idle users are disconnected within set time limits.
- Adds a new
connection-timeoutparameter inguacamole.properties(defaults to 0/disabled) - Implements connection timeout tracking using a Map to store tunnel creation times
- Integrates connection timeout checks into the existing session eviction mechanism
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| HashTokenSessionMap.java | Adds connection-timeout property configuration and integrates timeout checking into session eviction task |
| GuacamoleSession.java | Implements tunnel creation time tracking and expiration logic for individual connections |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| catch (GuacamoleException e) { | ||
| logger.error("Unable to read guacamole.properties: {}", e.getMessage()); | ||
| logger.debug("Error while reading connection timeout value.", e); | ||
| connectionTimeoutValue = 0; |
Copilot
AI
Sep 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra whitespace at the end of the line should be removed.
| connectionTimeoutValue = 0; | |
| connectionTimeoutValue = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though I hate CoPilot with hell fury, it's right, here, there should not be an extra space.
| UserTunnel tunnel = tunnels.get(tunnelId); | ||
| if (tunnel != null) { | ||
| try { | ||
| tunnel.close(); | ||
| logger.debug("Closed tunnel \"{}\" due to connection timeout.", tunnelId); | ||
| } | ||
| catch (GuacamoleException e) { | ||
| logger.debug("Unable to close expired tunnel \"" + tunnelId + "\".", e); | ||
| } | ||
| // Remove from both maps regardless of whether close succeeded | ||
| tunnels.remove(tunnelId); | ||
| tunnelCreationTimes.remove(tunnelId); | ||
| closedCount++; | ||
| } | ||
| } |
Copilot
AI
Sep 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tunnel removal logic is duplicated here and in the removeTunnel method. Consider extracting this into a private helper method to avoid code duplication.
| UserTunnel tunnel = tunnels.get(tunnelId); | |
| if (tunnel != null) { | |
| try { | |
| tunnel.close(); | |
| logger.debug("Closed tunnel \"{}\" due to connection timeout.", tunnelId); | |
| } | |
| catch (GuacamoleException e) { | |
| logger.debug("Unable to close expired tunnel \"" + tunnelId + "\".", e); | |
| } | |
| // Remove from both maps regardless of whether close succeeded | |
| tunnels.remove(tunnelId); | |
| tunnelCreationTimes.remove(tunnelId); | |
| closedCount++; | |
| } | |
| } | |
| if (removeTunnelById(tunnelId, "Closed tunnel \"{}\" due to connection timeout.", "Unable to close expired tunnel \"{}\".")) { | |
| closedCount++; | |
| } | |
| } | |
| /** | |
| * Removes the tunnel with the given ID from the session, closing it and removing | |
| * its creation time. Returns true if a tunnel was removed, false otherwise. | |
| * | |
| * @param tunnelId | |
| * The ID of the tunnel to remove. | |
| * @param successMsg | |
| * The message to log on successful close. | |
| * @param errorMsg | |
| * The message to log on error. | |
| * @return | |
| * true if a tunnel was removed, false otherwise. | |
| */ | |
| private boolean removeTunnelById(String tunnelId, String successMsg, String errorMsg) { | |
| UserTunnel tunnel = tunnels.get(tunnelId); | |
| if (tunnel != null) { | |
| try { | |
| tunnel.close(); | |
| logger.debug(successMsg, tunnelId); | |
| } | |
| catch (GuacamoleException e) { | |
| logger.debug(errorMsg, tunnelId, e); | |
| } | |
| tunnels.remove(tunnelId); | |
| tunnelCreationTimes.remove(tunnelId); | |
| return true; | |
| } | |
| return false; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I agree with CoPilot, here; however, is there any reason not to use the removeTunnel() method?
| /** | ||
| * Creation times for all tunnels, indexed by tunnel UUID. | ||
| */ | ||
| private final Map<String, Long> tunnelCreationTimes = new ConcurrentHashMap<>(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on whether it makes sense to do it this way, or to just add a way within the various Tunnel interfaces/classes, to track tunnel creation time? The GuacamoleTunnel interface could get a new method getCreationTime() (or whatever makes sense), and then one of the implementing classes could get either a Long or perhaps one of the Java Time/DateTime fields to track this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with changing it to use the GuacamoleTunnel as backing for the creation time. I just need a hint on how to iterate through the currently open connections/Tunnels
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, the tunnels Map, directly above this, has all currently active tunnels indexed by UUID. So, you should just be able to iterate through tunnels and, for each tunnel, grab the creation time and close it if it has expired. I think the only real substantive change over what you're already doing is that, instead of tracking tunnel creation time in a separate Map you're just using the already-implemented map and iterating through that?
- Introduce creationTime field in AbstractGuacamoleTunnel - Implement getCreationTime method in GuacamoleTunnel interface - Update DelegatingGuacamoleTunnel to return tunnel creation time - Remove tunnel creation time tracking map from GuacamoleSession - Adjust methods in GuacamoleSession to use GuacamoleTunnel.getCreationTime() and reuse the existing Map of active tunnels.
|
I think this is the full "whole-server" version I have for now, addressing the comments above. I saw your comments about adding feedback to guacd. Is that something I would need to implement in a guacd pull request for this to be approved? |
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this extra line - our current style does not separate the import statements like this, and I don't think this is the place to start that :-).
| /** | ||
| * Returns the creation time of this GuacamoleTunnel. | ||
| * | ||
| * @return The creation time of this GuacamoleTunnel. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you're just following the style used in this particular Java file, but, overall, we've moved to the following, and code additions follow this style:
/**
* Returns the creation time of this GuacamoleTunnel
*
* @return
* The creation time of this GuacamoleTunnel.
*/
| /** | ||
| * The connection timeout for individual Guacamole connections, in minutes. | ||
| * If 0, connections will not be automatically terminated based on age. | ||
| */ | ||
| private final IntegerGuacamoleProperty CONNECTION_TIMEOUT = | ||
| new IntegerGuacamoleProperty() { | ||
|
|
||
| @Override | ||
| public String getName() { return "connection-timeout"; } | ||
|
|
||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking that we may want to name this slightly differently. "timeout" carries with it the notion that the connection is waiting for something - user input, or a response from a remote system, etc. - and what you've really implemented, here, is a maximum connection duration, at which point the connection(s) will be forcibly dropped and users will have to reconnect. My thought is that maybe this code - both internally within the code and for exposed things like properties - should call this something different - maximum-session-duration or session-limit or something similar?
| catch (GuacamoleException e) { | ||
| logger.error("Unable to read guacamole.properties: {}", e.getMessage()); | ||
| logger.debug("Error while reading connection timeout value.", e); | ||
| connectionTimeoutValue = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though I hate CoPilot with hell fury, it's right, here, there should not be an extra space.
| executor.scheduleAtFixedRate(new SessionEvictionTask(sessionTimeoutValue * 60000l), 1, 1, TimeUnit.MINUTES); | ||
| if (connectionTimeoutValue > 0) { | ||
| logger.info("Connections will be terminated after {} minutes regardless of activity.", connectionTimeoutValue); | ||
| } else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't cuddle these lines...
}
else {
| * @param sessionTimeout The maximum age of any session, in | ||
| * milliseconds. | ||
| * @param connectionTimeout The maximum age of any connection, in | ||
| * milliseconds. If 0, connections will not be | ||
| * terminated based on age. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As with the note in the above file, this should follow the newer style that most additions and updates are using:
* @param sessionTimeout
* The maximum age of any session, in milliseconds
| * | ||
| * @param tunnel The tunnel to associate with this session. | ||
| */ | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should not be a space between function documentation and the start of the function itself.
| UserTunnel tunnel = tunnels.get(tunnelId); | ||
| if (tunnel != null) { | ||
| try { | ||
| tunnel.close(); | ||
| logger.debug("Closed tunnel \"{}\" due to connection timeout.", tunnelId); | ||
| } | ||
| catch (GuacamoleException e) { | ||
| logger.debug("Unable to close expired tunnel \"" + tunnelId + "\".", e); | ||
| } | ||
| // Remove from both maps regardless of whether close succeeded | ||
| tunnels.remove(tunnelId); | ||
| tunnelCreationTimes.remove(tunnelId); | ||
| closedCount++; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I agree with CoPilot, here; however, is there any reason not to use the removeTunnel() method?
Yes, any sort of feedback look for guacd would require some changes both here and in the guacd code. |
Summary
In my company's business use case for Guacamole, for security and auditing purposes, we need to be able to ensure that any idle user is disconnected and logged off within a set period of time of idleness.
In an ideal version of such a system, we would do the following.
This requires a lot of conditionals and would be more difficult to implement and maintain in an ongoing project like Guacamole.
A more practical, yet sufficient, version is:
This second option meets our business needs, and we would like to share it with others.
Features
connection-timeoutinguacamole.properties, disconnecting users afterconnection-timeoutminutes. Defaults to 0, disabling the feature.