TCP Proxy for macOS System Extension

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.