Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 62 additions & 4 deletions src/main/java/com/jcraft/jsch/Channel.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public abstract class Channel {

private static final AtomicInteger index = new AtomicInteger();

// sanity cap for setLocalPacketSize: lmpsize is both used to allocate a Buffer of that size
// (e.g. ChannelSftp.start()) and advertised to the server as the max size of a single
// CHANNEL_DATA payload it may send us. That payload is wrapped in an SSH packet together with
// ~9 bytes of channel-data framing plus padding, so it must stay comfortably under
// Session.PACKET_MAX_SIZE (RFC 4253 6.1 Maximum Packet Length) or the resulting packet gets
// discarded by the transport layer, killing the connection.
private static final int MAX_LOCAL_PACKET_SIZE = Session.PACKET_MAX_SIZE - 4096;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know what max values other implementations use?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my current understanding, neither sshj nor OpenSSH caps local max packet size beyond the RFC 4253 transport packet-length ceiling: sshj's setMaxPacketSize only rejects values <= 0 (default 32KB, no upper bound), and OpenSSH hardcodes CHAN_SES_PACKET_DEFAULT at 32KB with no user-configurable option, while its own PACKET_MAX_SIZE transport ceiling is 256KB, the same value and name jsch uses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to expose setting both the max packet size and max window size in order to obtain the performance increase you desire? Or is exposing just one or the other sufficient?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only setLocalPacketSize is strictly necessary: with the default 2MB window and 16 max pipelined requests, in-flight data stays well under the window even at 64KB packets, so the window increase shouldn't be required by that math. That said, we only benchmarked both changes together, not packet size alone, so this isn't empirically confirmed. However IMHO symmetry of setters should be present for parity's sake.


int id;
volatile int recipient = -1;
protected byte[] type = Util.str2byte("foo");
Expand Down Expand Up @@ -406,16 +414,66 @@ synchronized void checkSpace(int len) throws IOException {
}
}

void setLocalWindowSizeMax(int foo) {
this.lwsize_max = foo;
/**
* Sets the maximum local window size.
*
* <p>
* Both the maximum local window size and the initial local window size are set to the specified
* {@code size}.
* </p>
*
* @param size the maximum local window size in bytes
* @throws JSchException if the channel is already connected or if {@code size} is not positive
*/
public void setLocalWindowSizeMax(int size) throws JSchException {
if (isConnected()) {
throw new JSchException("local window size max cannot be changed after channel is connected");
}
if (size <= 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should disallow setting lwsize_max to < lwsize?

@norrisjeremy norrisjeremy Sep 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, I think we definitely should enforce that lwsize_max >= lwsize.
Otherwise, in Session.java when we send a window update, we could end up sending an absurd value to the server (buf.putInt(channel.lwsize_max - channel.lwsize);).

throw new JSchException("local window size max must be positive: " + size);
}
this.lwsize_max = size;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes me a bit nervous to allow users to set lwsize_max to an unbounded upper value.
Do you know if other SSH implementations allow an unbound upper value or do they enforce a cap?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sshj (net.schmizz.sshj) does not enforce an upper bound on window size.

  • ConnectionImpl.setWindowSize(long windowSize): plain setter, no validation at all (not even a positive-value check). Default is 2048 * 1024 (2MB).
  • ConnectionImpl.setMaxPacketSize(int maxPacketSize): only checks maxPacketSize > 0; no upper cap. Default is 32 * 1024.
  • Window / Window.Local / Window.Remote constructors: only validate maxPacketSize > 0. The window size argument itself is not validated.

Source: hierynomus/sshj, src/main/java/net/schmizz/sshj/connection/ConnectionImpl.java and src/main/java/net/schmizz/sshj/connection/channel/Window.java.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the information about sshj.
Do you know how implementations handle this, such as OpenSSH?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding checked JSchException vs IllegalArgumentException/IllegalStateException: since
Session.run() now uses updateLocalWindowSize(), switching the public setters to throws JSchException
would not touch any internal call sites. I am happy to switch them to JSchException if you prefer
that for JSch's API conventions, or leave them as unchecked argument/state exceptions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go ahead and switch them to throwing checked JSchException's so that users are aware of the potential for an exception being thrown.
Can you also add Javadocs to both these methods, being sure to highlight the reasons why a JSchException could be thrown too?

this.lwsize = size;
}

/**
* Gets the maximum local window size.
*
* @return the maximum local window size in bytes
*/
public int getLocalWindowSizeMax() {
return this.lwsize_max;
}

void setLocalWindowSize(int foo) {
this.lwsize = foo;
}

void setLocalPacketSize(int foo) {
this.lmpsize = foo;
/**
* Sets the maximum local packet size.
*
* @param size the maximum local packet size in bytes
* @throws JSchException if the channel is already connected, if {@code size} is not positive, or
* if {@code size} exceeds the maximum allowed packet size
*/
public void setLocalPacketSize(int size) throws JSchException {
if (isConnected()) {
throw new JSchException("local packet size cannot be changed after channel is connected");
}
if (size <= 0 || size > MAX_LOCAL_PACKET_SIZE) {
throw new JSchException("local packet size must be positive and not exceed "
+ MAX_LOCAL_PACKET_SIZE + ": " + size);
}
this.lmpsize = size;
}

/**
* Gets the maximum local packet size.
*
* @return the maximum local packet size in bytes
*/
public int getLocalPacketSize() {
return this.lmpsize;
}

synchronized void setRemoteWindowSize(long foo) {
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/com/jcraft/jsch/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ public class Session {
static final int SSH_MSG_CHANNEL_SUCCESS = 99;
static final int SSH_MSG_CHANNEL_FAILURE = 100;

private static final int PACKET_MAX_SIZE = 256 * 1024;
// RFC 4253 6.1. Maximum Packet Length: hard ceiling enforced on any incoming decrypted SSH
// packet (see read(Buffer) below); package-visible so Channel can bound its local packet size
// against it.
static final int PACKET_MAX_SIZE = 256 * 1024;

private byte[] V_S; // server version
private byte[] V_C = Util.str2byte("SSH-2.0-JSCH_" + JSch.VERSION); // client version
Expand Down