swift-nio-ssl icon indicating copy to clipboard operation
swift-nio-ssl copied to clipboard

expose CNIOBoringSSL

Open xlc opened this issue 1 year ago • 11 comments

We would like to use some low level openssl features directly and wanting to use CNIOBoringSSL. What is the reason of not exposing CNIOBoringSSL? If it is just some compiler warnings, some compiler flags should address them. Happy to contribute if needed.

xlc avatar Oct 13 '24 21:10 xlc

Hi @xlc, thanks for asking. We discussed this on the Swift forums when we originally proposed cutting over to BoringSSL, but I'll reiterate here:

BoringSSL does not provide any API or ABI stability guarantees. They are free to, and indeed do, break APIs regularly. If BoringSSL were exposed as as Swift Package Manager package, it would bump its major version number every release. This would make it impossible for the wider community to depend on: different packages would be pinning to different major versions, and dependency resolution would immediately fail.

swift-nio-ssl will present an API that does not contain any part of BoringSSL. If users need to use libssl or libcrypto themselves, they will need to bring their own copy of libssl.

When we update our copy of BoringSSL, we fairly frequently get compile errors that we have to fix. This is despite the fact that we only use a relatively small amount of the API surface. Across the whole of the API surface we'd never be able to revise it.

What features do you want from BoringSSL? We may have good solutions for them in Swift in other packages.

Lukasa avatar Oct 14 '24 07:10 Lukasa

Thanks for the explanation. That's a bit unfortunate but make sense. For the use case here, we are implementing this protocol over QUIC. It have some requirements of the X509 certificate, more specifically, require the peers using a published Ed25519 key. So we will need to be able to generate and verify such certificates.

Moreover, I am using https://github.com/microsoft/msquic as the QUIC implementation, and it requires PKCS12 format if I want to pass the keys in-memory (i.e. not loading from a PEM file).

I discovered CNIOBoringSSL from this issue: https://github.com/apple/swift-certificates/issues/114

xlc avatar Oct 14 '24 07:10 xlc

Ok, so let's turn that into a series of feature requests on the higher-level projects. I think there are two features we need:

  1. Add support for Ed25519 keys in swift-certificates. This should be very do-able so long as we get the key formats right, and I expect you will have some sample certs laying around somewhere.

  2. Add support for serializing PKCS#12 in swift-nio-ssl. In the short term, PKCS12_create is sufficient for this.

    In the longer term I think the rightest solution is to add support for PKCS12 in Swift Crypto's _CryptoExtras. This should be done in a way that lets Swift ASN1 do the ASN.1 pieces, and just have CCryptoBoringSSL do the encryption/decryption steps. That will avoid us needing to offer API to use some of the horrible terrible crypto that PKCS12 requires, but still get support for the format in an arbitrary form.

    cc @0xTim who I know is interested in getting some p12 support in CryptoExtras, as the above would be the best way to do that.

Lukasa avatar Oct 14 '24 08:10 Lukasa

That sounds good. Happy to help testing if needed. I decided to use openssl for now and see if we can refactor the low level C API usage to Swift API when they are ready.

xlc avatar Oct 14 '24 09:10 xlc

Some updates. For Ed25519 keys, see apple/swift-certificates#205. For PKCS#12, see #486.

Would you like to investigate these two options for your use-case?

Lukasa avatar Oct 29 '24 21:10 Lukasa

Thanks! I think those will work for us. Here is our current C code BTW https://github.com/open-web3-stack/boka/blob/master/Networking/Sources/CHelpers/helpers.c

xlc avatar Oct 29 '24 21:10 xlc

Hi @xlc @Lukasa @fourplusone @Yasumoto @federicobucchi Anyone has tutorial or example to create TCP proxy using SwiftNIO. I am facing issue to create connection between remote server. Getting certificate issue.

dhanarajkawde avatar Mar 20 '25 23:03 dhanarajkawde

There are a number of examples on the Swift forums that you can follow.

Lukasa avatar Mar 21 '25 10:03 Lukasa

@Lukasa can you please help me to review below code. I am just trying to connect with remote server like plain transparent proxy.

Facing issue: Not able to load WebPage/Website on browser, kept in loading state.

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()

dhanarajkawde avatar Mar 24 '25 07:03 dhanarajkawde

There are a number of examples on the Swift forums that you can follow.

can you please share any one of example. It will really helpful.

dhanarajkawde avatar Mar 24 '25 07:03 dhanarajkawde

What logging output do you see from the above code during connection?

Lukasa avatar Apr 09 '25 10:04 Lukasa