Skip to content

Commit c11f16f

Browse files
authored
Merge pull request #54 from orlandos-nl/jo/direct-tcpip-server
Add DirectTCPIP support to SSH servers
2 parents f265b4e + 38c5d47 commit c11f16f

6 files changed

Lines changed: 94 additions & 6 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
/*.xcodeproj
66
xcuserdata/
77
Package.resolved
8+
.vscode/launch.json

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/Citadel/ClientSession.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import NIO
22
import NIOSSH
3+
import Logging
34

45
final class ClientHandshakeHandler: ChannelInboundHandler {
56
typealias InboundIn = Any
67

78
private let promise: EventLoopPromise<Void>
9+
let logger = Logger(label: "nl.orlandos.citadel.handshake")
810

911
/// A future that will be fulfilled when the handshake is complete.
1012
public var authenticated: EventLoopFuture<Void> {
1113
promise.futureResult
1214
}
1315

14-
init(eventLoop: EventLoop) {
16+
init(eventLoop: EventLoop, loginTimeout: TimeAmount) {
1517
let promise = eventLoop.makePromise(of: Void.self)
1618
self.promise = promise
1719
}
@@ -54,7 +56,10 @@ final class SSHClientSession {
5456
algorithms: SSHAlgorithms = SSHAlgorithms(),
5557
protocolOptions: Set<SSHProtocolOption> = []
5658
) async throws -> SSHClientSession {
57-
let handshakeHandler = ClientHandshakeHandler(eventLoop: channel.eventLoop)
59+
let handshakeHandler = ClientHandshakeHandler(
60+
eventLoop: channel.eventLoop,
61+
loginTimeout: .seconds(10)
62+
)
5863
var clientConfiguration = SSHClientConfiguration(
5964
userAuthDelegate: authenticationMethod(),
6065
serverAuthDelegate: hostKeyValidator
@@ -101,7 +106,10 @@ final class SSHClientSession {
101106
group: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1),
102107
connectTimeout: TimeAmount = .seconds(30)
103108
) async throws -> SSHClientSession {
104-
let handshakeHandler = ClientHandshakeHandler(eventLoop: group.next())
109+
let handshakeHandler = ClientHandshakeHandler(
110+
eventLoop: group.next(),
111+
loginTimeout: .seconds(10)
112+
)
105113
var clientConfiguration = SSHClientConfiguration(
106114
userAuthDelegate: authenticationMethod(),
107115
serverAuthDelegate: hostKeyValidator

Sources/Citadel/DirectTCPIP.swift renamed to Sources/Citadel/DirectTCPIP/Client/DirectTCPIP+Client.swift

File renamed without changes.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import NIO
2+
import NIOSSH
3+
4+
fileprivate final class ProxyChannelHandler: ChannelOutboundHandler {
5+
typealias OutboundIn = ByteBuffer
6+
7+
private let write: (ByteBuffer, EventLoopPromise<Void>?) -> Void
8+
9+
init(write: @escaping (ByteBuffer, EventLoopPromise<Void>?) -> Void) {
10+
self.write = write
11+
}
12+
13+
func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
14+
let data = self.unwrapOutboundIn(data)
15+
write(data, promise)
16+
}
17+
}
18+
19+
public protocol DirectTCPIPDelegate {
20+
func initializeDirectTCPIPChannel(_ channel: Channel, request: SSHChannelType.DirectTCPIP, context: SSHContext) -> EventLoopFuture<Void>
21+
}
22+
23+
public struct DirectTCPIPForwardingDelegate: DirectTCPIPDelegate {
24+
internal enum Error: Swift.Error {
25+
case forbidden
26+
}
27+
28+
public var whitelistedHosts: [String]?
29+
public var whitelistedPorts: [Int]?
30+
31+
public init() {}
32+
33+
public func initializeDirectTCPIPChannel(_ channel: Channel, request: SSHChannelType.DirectTCPIP, context: SSHContext) -> EventLoopFuture<Void> {
34+
if let whitelistedHosts, !whitelistedHosts.contains(request.targetHost) {
35+
return channel.eventLoop.makeFailedFuture(Error.forbidden)
36+
}
37+
38+
if let whitelistedPorts, !whitelistedPorts.contains(request.targetPort) {
39+
return channel.eventLoop.makeFailedFuture(Error.forbidden)
40+
}
41+
42+
return ClientBootstrap(group: channel.eventLoop)
43+
.connect(host: request.targetHost, port: request.targetPort)
44+
.flatMap { remote in
45+
channel.pipeline.addHandlers([
46+
DataToBufferCodec()
47+
]).flatMap {
48+
channel.pipeline.addHandler(ProxyChannelHandler { data, promise in
49+
remote.writeAndFlush(data, promise: promise)
50+
})
51+
}.flatMap {
52+
remote.pipeline.addHandler(ProxyChannelHandler { [weak channel] data, promise in
53+
guard let channel else {
54+
promise?.fail(ChannelError.ioOnClosedChannel)
55+
return
56+
}
57+
channel.writeAndFlush(data, promise: promise)
58+
})
59+
}
60+
}
61+
}
62+
}

Sources/Citadel/Server.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ final class SubsystemHandler: ChannelDuplexHandler {
9393
final class CitadelServerDelegate {
9494
var sftp: SFTPDelegate?
9595
var exec: ExecDelegate?
96+
var directTCPIP: DirectTCPIPDelegate?
9697

9798
fileprivate init() {}
9899

@@ -109,7 +110,19 @@ final class CitadelServerDelegate {
109110
handlers.append(ExecHandler(delegate: exec, username: username))
110111

111112
return channel.pipeline.addHandlers(handlers)
112-
case .directTCPIP, .forwardedTCPIP:
113+
case .directTCPIP(let request):
114+
guard let delegate = directTCPIP else {
115+
return channel.eventLoop.makeFailedFuture(CitadelError.unsupported)
116+
}
117+
118+
return channel.pipeline.addHandler(DataToBufferCodec()).flatMap {
119+
return delegate.initializeDirectTCPIPChannel(
120+
channel,
121+
request: request,
122+
context: SSHContext(username: username)
123+
)
124+
}
125+
case .forwardedTCPIP:
113126
return channel.eventLoop.makeFailedFuture(CitadelError.unsupported)
114127
}
115128
}
@@ -148,6 +161,10 @@ public final class SSHServer {
148161
public func enableExec(withDelegate delegate: ExecDelegate) {
149162
self.delegate.exec = delegate
150163
}
164+
165+
public func enableDirectTCPIP(withDelegate delegate: DirectTCPIPDelegate) {
166+
self.delegate.directTCPIP = delegate
167+
}
151168

152169
/// Closes the SSH Server, stopping new connections from coming in.
153170
public func close() async throws {

0 commit comments

Comments
 (0)