Hi All,
I am creating a TCP Proxy using SwiftNIO for a Web Content Filter. I am having issues connecting to a remote server, and the webpage keeps loading on my browser.
If someone helps me to guide it will be helpful.
Hi All,
I am creating a TCP Proxy using SwiftNIO for a Web Content Filter. I am having issues connecting to a remote server, and the webpage keeps loading on my browser.
If someone helps me to guide it will be helpful.
There are definitely people, not really me, on here who would help you. But you’re going to have to post a description of what you’re trying to do, what your approach has been, and the code that you’ve written to achieve that goal.
@austintatious thank you for your response
import NIOCore
import NIOPosix
import Logging
import Dispatch
let logger = Logger(label: "com.apple.nio-tcp-proxy.main")
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
// GlueHandler: Handles bidirectional data transfer between two channels
final class GlueHandler {
private var partner: GlueHandler?
private var context: ChannelHandlerContext?
private var pendingRead: Bool = false
private init() { }
static func matchedPair() -> (GlueHandler, GlueHandler) {
let first = GlueHandler()
let second = GlueHandler()
first.partner = second
second.partner = first
return (first, second)
}
private func partnerWrite(_ data: NIOAny) {
self.context?.write(data, promise: nil)
}
private func partnerFlush() {
self.context?.flush()
}
private func partnerWriteEOF() {
self.context?.close(mode: .output, promise: nil)
}
private func partnerCloseFull() {
self.context?.close(promise: nil)
}
private func partnerBecameWritable() {
if self.pendingRead {
self.pendingRead = false
self.context?.read()
}
}
private var partnerWritable: Bool {
self.context?.channel.isWritable ?? false
}
}
extension GlueHandler: ChannelDuplexHandler {
typealias InboundIn = ByteBuffer
typealias OutboundIn = ByteBuffer
typealias OutboundOut = ByteBuffer
func handlerAdded(context: ChannelHandlerContext) {
self.context = context
}
func handlerRemoved(context: ChannelHandlerContext) {
self.context = nil
self.partner = nil
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
self.partner?.partnerWrite(NIOAny(self.unwrapInboundIn(data)))
}
func channelReadComplete(context: ChannelHandlerContext) {
self.partner?.partnerFlush()
}
func channelInactive(context: ChannelHandlerContext) {
self.partner?.partnerCloseFull()
}
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
if let event = event as? ChannelEvent, case .inputClosed = event {
self.partner?.partnerWriteEOF()
}
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
self.partner?.partnerCloseFull()
}
func channelWritabilityChanged(context: ChannelHandlerContext) {
if context.channel.isWritable {
self.partner?.partnerBecameWritable()
}
}
func read(context: ChannelHandlerContext) {
if let partner = self.partner, partner.partnerWritable {
context.read()
} else {
self.pendingRead = true
}
}
}
// SNI Extractor (unchanged)
struct SNIExtractor {
static func extractSNI(from buffer: inout ByteBuffer) -> String? {
guard buffer.readableBytes >= 5,
buffer.getInteger(at: buffer.readerIndex, as: UInt8.self) == 0x16,
buffer.getInteger(at: buffer.readerIndex + 1, as: UInt16.self) == 0x0301 ||
buffer.getInteger(at: buffer.readerIndex + 1, as: UInt16.self) == 0x0303 else {
return nil
}
buffer.moveReaderIndex(forwardBy: 5)
guard buffer.readableBytes >= 4,
buffer.getInteger(at: buffer.readerIndex, as: UInt8.self) == 0x01 else {
return nil
}
buffer.moveReaderIndex(forwardBy: 4)
guard buffer.readableBytes >= 34 else { return nil }
buffer.moveReaderIndex(forwardBy: 34)
guard let sessionIDLength = buffer.readInteger(as: UInt8.self),
buffer.readableBytes >= sessionIDLength else { return nil }
buffer.moveReaderIndex(forwardBy: Int(sessionIDLength))
guard let cipherSuitesLength = buffer.readInteger(as: UInt16.self),
buffer.readableBytes >= cipherSuitesLength else { return nil }
buffer.moveReaderIndex(forwardBy: Int(cipherSuitesLength))
guard let compressionMethodsLength = buffer.readInteger(as: UInt8.self),
buffer.readableBytes >= compressionMethodsLength else { return nil }
buffer.moveReaderIndex(forwardBy: Int(compressionMethodsLength))
guard let extensionsLength = buffer.readInteger(as: UInt16.self),
buffer.readableBytes >= extensionsLength else { return nil }
let extensionsEnd = buffer.readerIndex + Int(extensionsLength)
while buffer.readerIndex < extensionsEnd {
guard let extensionType = buffer.readInteger(as: UInt16.self),
let extensionLength = buffer.readInteger(as: UInt16.self),
buffer.readableBytes >= extensionLength else { return nil }
if extensionType == 0x00 {
guard buffer.readableBytes >= 5,
let _ = buffer.readInteger(as: UInt16.self),
let nameType = buffer.readInteger(as: UInt8.self),
nameType == 0x00,
let nameLength = buffer.readInteger(as: UInt16.self),
let hostname = buffer.readString(length: Int(nameLength)) else { return nil }
return hostname
} else {
buffer.moveReaderIndex(forwardBy: Int(extensionLength))
}
}
return nil
}
}
// TCPProxyHandler with SNI extraction
final class TCPProxyHandler: ChannelDuplexHandler {
typealias InboundIn = ByteBuffer
typealias OutboundIn = ByteBuffer
typealias OutboundOut = ByteBuffer
private var clientBuffer: ByteBuffer?
private var targetHost: String?
private var targetPort: Int = 443 // Default to HTTPS port
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
var buffer = self.unwrapInboundIn(data)
if self.targetHost == nil {
// Buffer initial data and extract SNI
if self.clientBuffer == nil {
self.clientBuffer = context.channel.allocator.buffer(capacity: buffer.readableBytes)
}
self.clientBuffer?.writeBuffer(&buffer)
if let initialBuffer = self.clientBuffer,
var tempBuffer = initialBuffer.getSlice(at: initialBuffer.readerIndex, length: initialBuffer.readableBytes),
let sni = SNIExtractor.extractSNI(from: &tempBuffer) {
self.targetHost = sni
logger.info("Extracted SNI: \(sni)")
self.connectToTarget(context: context)
} else {
logger.warning("No SNI found or incomplete TLS ClientHello, buffering data")
context.read() // Request more data
}
} else {
// Forward data to the glued target channel
context.fireChannelRead(NIOAny(buffer))
}
}
func channelActive(context: ChannelHandlerContext) {
logger.info("Client connected from \(String(describing: context.channel.remoteAddress))")
}
private func connectToTarget(context: ChannelHandlerContext) {
guard let targetHost = self.targetHost else {
logger.error("No target host determined")
context.close(promise: nil)
return
}
logger.info("Connecting to \(targetHost):\(self.targetPort)")
let clientBootstrap = ClientBootstrap(group: context.eventLoop)
.connect(host: targetHost, port: self.targetPort)
clientBootstrap.whenSuccess { targetChannel in
self.connectSucceeded(targetChannel: targetChannel, context: context)
}
clientBootstrap.whenFailure { error in
logger.error("Failed to connect to \(targetHost):\(self.targetPort): \(error)")
context.close(promise: nil)
}
}
private func connectSucceeded(targetChannel: Channel, context: ChannelHandlerContext) {
logger.info("Connected to target \(String(describing: targetChannel.remoteAddress))")
// Glue the client and target channels
let (clientGlue, targetGlue) = GlueHandler.matchedPair()
context.pipeline.addHandler(clientGlue).and(targetChannel.pipeline.addHandler(targetGlue)).whenComplete { result in
switch result {
case .success:
logger.debug("Successfully glued \(ObjectIdentifier(context.channel)) and \(ObjectIdentifier(targetChannel))")
// Send buffered ClientHello to target
if let buffer = self.clientBuffer {
context.fireChannelRead(NIOAny(buffer))
self.clientBuffer = nil
}
case .failure(let error):
logger.error("Failed to glue channels: \(error)")
targetChannel.close(mode: .all, promise: nil)
context.close(promise: nil)
}
}
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
logger.error("Error in client connection: \(error)")
context.close(promise: nil)
}
}
// Set up the TCP proxy server
let bootstrap = ServerBootstrap(group: group)
.serverChannelOption(ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 1)
.childChannelOption(ChannelOptions.socket(SOL_SOCKET, SO_REUSEADDR), value: 1)
.childChannelInitializer { channel in
channel.pipeline.addHandler(TCPProxyHandler())
}
bootstrap.bind(to: try! SocketAddress(ipAddress: "127.0.0.1", port: 8080)).whenComplete { result in
switch result {
case .success(let channel):
logger.info("TCP proxy listening on (String(describing: channel.localAddress))")
case .failure(let error):
logger.error("Failed to bind 127.0.0.1:8080: (error)")
}
}
bootstrap.bind(to: try! SocketAddress(ipAddress: "::1", port: 8080)).whenComplete { result in
switch result {
case .success(let channel):
logger.info("TCP proxy listening on (String(describing: channel.localAddress))")
case .failure(let error):
logger.error("Failed to bind [::1]:8080: (error)")
}
}
// Run the event loop forever
dispatchMain()
trying to use above and but not able to load website or webpage on browser. It kept in loading state.