Im new in SwiftNIO and NetworkExtension. I try to build proxy server which will be working in iOS Network extension. Thanks to some samples I had done this. But I can't understand how it is working((
Will be very thankful if you can answer some questions.
I use PacketTunnelProvider and on startTunnel
override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
// Add code here to start the process of connecting the tunnel.
Logger.tunProvider.info("GGGG ---- startTunnel method called ----")
pendingStartCompletion = completionHandler
proxyService = ProxyService(host: defaultHost, port: defaultPort)
proxyService?.start { result in
switch result {
case .success:
self.setupSession()
case .failure(let error):
Logger.tunProvider.error("***************** Proxy Service Run Failed! \(error.localizedDescription, privacy: .public)")
self.pendingStartCompletion(error)
}
}
}
I create NIO server (on localhost)
(default example from swift-nio-examples/connect-proxy at main · apple/swift-nio-examples · GitHub)
public func start(_ completion: @escaping (Result<Void, Error>) -> Void) {
Logger.proxyService.info("----- Starting Proxy Service -----")
networkMonitorService.startMonitoring()
bootstrap = ServerBootstrap(group: groupWorker)
.serverChannelOption(ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 1)
.childChannelOption(ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 1)
.childChannelInitializer { channel in
channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes))).flatMap {
channel.pipeline.addHandler(HTTPResponseEncoder()).flatMap {
channel.pipeline.addHandler(ConnectHandler(self.networkMonitorService.currentCunnection))
}
}
}
do {
bootstrap.bind(to: try SocketAddress(ipAddress: host, port: port)).whenComplete { result in
// Need to create this here for thread-safety purposes
switch result {
case .success(let channel):
Logger.proxyService.info("Listening on \(String(describing: channel.localAddress), privacy: .public)")
completion(.success(()))
case .failure(let error):
Logger.proxyService.error("Failed to bind \(self.host, privacy: .public):\(String(self.port), privacy: .public), \(error.localizedDescription, privacy: .public)")
completion(.failure(error))
}
}
} catch {
Logger.proxyService.error("Can't create address for \(self.host, privacy: .public):\(String(self.port), privacy: .public), \(error.localizedDescription, privacy: .public)")
}
}
ConnectHandler is main listener handler, it has
typealias InboundIn = HTTPServerRequestPart
typealias OutboundOut = HTTPServerResponsePart
, and on channelRead
it connect to - 2 GlueHandler
private func connectTo(host: String, port: Int, context: ChannelHandlerContext, throttle: Bool = false) {
Logger.proxyService.info("Connecting to \(String(describing: host), privacy: .public):\(String(describing: port), privacy: .public)")
let channelFuture = ClientBootstrap(group: context.eventLoop)
.connect(host: String(host), port: port)
channelFuture.whenSuccess { channel in
self.connectSucceeded(channel: channel, context: context, throttle: throttle)
}
channelFuture.whenFailure { error in
self.connectFailed(error: error, context: context)
}
}
private func connectSucceeded(channel: Channel, context: ChannelHandlerContext, throttle: Bool) {
Logger.proxyService.info("Connected to \(String(describing: channel.remoteAddress), privacy: .public)")
switch self.upgradeState {
case .beganConnecting:
// Ok, we have a channel, let's wait for end.
self.upgradeState = .awaitingEnd(connectResult: channel)
case .awaitingConnection(pendingBytes: let pendingBytes):
// Upgrade complete! Begin gluing the connection together.
self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes)
self.glue(channel, context: context, throttle: throttle)
case .awaitingEnd(let peerChannel):
// This case is a logic error, close already connected peer channel.
peerChannel.close(mode: .all, promise: nil)
context.close(promise: nil)
case .idle, .upgradeFailed, .upgradeComplete:
// These cases are logic errors, but let's be careful and just shut the connection.
context.close(promise: nil)
}
}
private func glue(_ peerChannel: Channel, context: ChannelHandlerContext, throttle: Bool = false) {
Logger.proxyService.debug("Gluing together \(String(describing: ObjectIdentifier(context.channel)), privacy: .public) and \(String(describing: ObjectIdentifier(peerChannel)), privacy: .public)") // swiftlint:disable:this line_length
// Ok, upgrade has completed! We now need to begin the upgrade process.
// First, send the 200 message.
// This content-length header is MUST NOT, but we need to workaround NIO's insistence that we set one.
let headers = HTTPHeaders([("Content-Length", "0")])
let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok, headers: headers)
context.write(self.wrapOutboundOut(.head(head)), promise: nil)
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
// Now remove the HTTP encoder.
self.removeEncoder(context: context)
// Now we need to glue our channel and the peer channel together.
let (localGlue, peerGlue) = GlueHandler.matchedPair(throttle)
context.channel.pipeline.addHandler(localGlue).and(peerChannel.pipeline.addHandler(peerGlue)).whenComplete { result in
switch result {
case .success(_):
context.pipeline.removeHandler(self, promise: nil)
case .failure(_):
// Close connected peer channel before closing our channel.
peerChannel.close(mode: .all, promise: nil)
context.close(promise: nil)
}
}
}
- I don't clearly understand how data are going from device -> to proxy handlers -> to external resources, and how it goes back. I read How to understand InboundIn InboundOut OutboundOut OutboundIn names, but still cant imaging all the flow.
- Why do we need GlueHandlers, and why do we remove Connect handler after GlueHandles has been added to the pipeline?