본문 바로가기

3.구현/C or C++

openssl에서 nonblock socket으로 ssl 연결

최근 대용량 고속 처리가 대두 되면서 비동기 소켓 처리가 많이 늘어나고 있다. 더욱이 최근 보안이 중요해지면서 ssl에 의한 암호화된 채널 지원도 필요로 하고 있다.
이전에는 단순히 동기 형태의 소켓으로 무리 없이 간단한 통신에서는 문제가 없었지만, 서버에서 대용량 처리가 필요로 하게 되면서 기존 방식으로는 무리가 생기게 된다.
여기서는 비동기로 SSL를 통현 채널을 연결 및 데이터 송수신을 처리하는 간단한 예제를 살펴보겠다.

작성자: http://ospace.tistory.com/,2013.05.07 (ospace114@empal.com)

공통 코드

이 부분은 다음에 나오는 예제에서 사용되는 공통으로 사용되는 코드를 정리해 두었다.
단순히 편하게 사용하게 위해서 급조했기 때문에 혼동의 우려가 있을 수 있겠다. 더욱이 c++로 되어 있어서 c++에 익숙하지 안으신 분은 읽기 힘들지도 모르지만, 그냥 참고하기 바란다. ㅡ.ㅡ;

#ifndef __SSH_SOCKET_H_20130507__
#define __SSH_SOCKET_H_20130507__

#ifdef WIN32
#include <Winsock2.h>
#include <MSWSock.h>
#else
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <fcntl.h>
#endif

#include <iostream>
#include <errno.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

//간단한 로그 출력
#define LOG(m) do {\
        std::cout << __FUNCTION__ << "():" << __LINE__ << ": " << m << std::endl;\
} while(0)


#define ERRNO() (strerror(errno))
#define ERRSSL() (SSHConnection::strerror())

//Windows와 호환을 위한 코드 (검증해보지는 않았다. ㅡ.ㅡ;;;)
#ifdef WIN32
#define CLOSE_SOCKET(s) closesocket(s)
typedef int socklen_t;

#define INIT_WSA() do {\
    WSADATA wsaData;\
    if (WSAStartup(MAKEWORD(2,0), &wsaData)) return 1;\
} while(0);

#define RELEASE_WSA() WSACleanup()

#else
#define CLOSE_SOCKET(s) close(s)

#define INIT_WSA() {}
#define RELEASE_WSA() {}

#endif

//SSL_CTX의 포인터 관리를 자동으로 해줌(관리가 귀찮아서 만듬 ㅡ.ㅡ;;;)
class AutoSSLCTX {
public:
    explicit AutoSSLCTX(const std::string& type) : type_(type), ctx_(create_SSL_CTX(type)) {}
    ~AutoSSLCTX() { if (ctx_) SSL_CTX_free(ctx_); }

    static SSL_CTX* create_SSL_CTX(const std::string& type) {
        if ("SSLv3_server" == type) return SSL_CTX_new(SSLv3_server_method());
        if ("SSLv3_client" == type) return SSL_CTX_new(SSLv3_client_method());
        //계속해서 추가할 수 있음. 현재는 SSLv3만 지원.
        return NULL;
    }

    operator SSL_CTX*() { return ctx_; }

private:
    std::string type_;
    SSL_CTX* ctx_;
};

//앞의 SSL_CTX와 같은 이유로 만듬.
class AutoSSL {
public:
    explicit AutoSSL(SSL_CTX* ctx) : ssl_(SSL_new(ctx)) {}
    ~AutoSSL() {
        if (NULL == ssl_) return;
        SSL_shutdown(ssl_);
        SSL_free(ssl_);
    }

    int set_fd(int fd) { return SSL_set_fd(ssl_, fd); }

    operator SSL*() { return ssl_; }

private:
    SSL *ssl_;
};

//SSH에 필요한 기본 초기화 및 기타 등등 포함(ㅡ.ㅡ;;;;)
class SSHConnection {
public:
    //싱글톤으로 한번한 SSL 초기화 하도록 하기 위함.
    static SSHConnection *instance() {
        static SSHConnection inst;
        return &inst;
    }

    //openssl의 에러 코드를 문자열 만들어줌.
    static const char* strerror() {
        return ERR_error_string(ERR_get_error(), NULL);
    }

private:
    SSHConnection() : ctx_(NULL) {
        SSL_load_error_strings();
        SSL_library_init();
    }

    SSL_CTX *ctx_;
};

//소켓을 nonblock으로 만듬.
void set_nonblock(int fd) {
    if (0>fd) return;
#ifdef WIN32
    unsigned long nonblocking = 1;
    ioctlsocket(fd, FIONBIO, (unsigned long*) &nonblocking);
#else
    if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) {
        LOG("fcntl(O_NONBLOCK) - " << ERRNO());
    }
#endif
}

//소켓의 작업 진행 중인지 판별(nonblock에 의해 리턴된 경우)
bool is_progress() {
#ifdef WIN32
    return (WSAEWOULDBLOCK == WSAGetLastError());
#else
    return (EAGAIN == errno || EINTR == errno || EINPROGRESS == errno);
#endif
}

//간단하게 소켓 read, write에 대한 대기시간 체크
//아래는 간단하게 만든 것이기에 실제 비동기 처리와는 다른 방식이다.
//비동기로 이를 사용하면, 별로 효율적이지 못하지만, 동기 형식으로 간단한 timeout 지원할 수 있음.
enum {
    SOCK_FLAG = 1,
    SOCK_READABLE = SOCK_FLAG,
    SOCK_WRITEABLE = SOCK_FLAG<<1
};

bool is_accessible(int fd, size_t msec, int flag) {
    fd_set rset, wset;
    FD_ZERO(&rset);
    FD_ZERO(&wset);

    fd_set *prset = NULL;
    fd_set *pwset = NULL;

    if (SOCK_READABLE & flag) {
       FD_SET(fd, &rset);
       prset = &rset;
    }

    if (SOCK_WRITEABLE & flag) {
        FD_SET(fd, &wset);
        pwset = &wset;
    }


    struct timeval tv;
    tv.tv_sec = msec/1000;
    tv.tv_usec = (msec%1000)*1000;

    if (0 >= select(fd+1, prset, pwset, NULL, &tv)) return false;
    return true;
}

//소켓이 읽기 가능한지 확인
bool is_readable(int fd, size_t msec) {
    return is_accessible(fd, msec, SOCK_READABLE);
}

//소켓이 쓰기 가능한지 확인
bool is_writeable(int fd, size_t msec) {
    return is_accessible(fd, msec, SOCK_WRITEABLE);
}

#endif //__SSH_SOCKET_H_20130507__

서버측 코드

앞의 공통 코드 적용을 별로 하지 않은 코드이다. 그리고 비동기로 구성하지 않았다. 그래서 코드가 직관적일 것이다.
작업은 단순한다. 수신한 데이터를 그대로 돌려보낸다. 핑퐁 같은 형태이다.

#include "ssh_socket.hpp"

int main()
{
    INIT_WSA();

    SSHConnection::instance(); //ssl 초기화

    //리슨 소켓을 생성하고 연결한다.
    int listen_fd = socket(PF_INET, SOCK_STREAM, 0);

    if (listen_fd<0) {
        LOG("socket - " << ERRNO());
        return 1;
    }

    int optval = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (char*)&optval, (int)sizeof(optval));

    struct sockaddr_in svr_addr;
    svr_addr.sin_family = AF_INET;
    //svr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    inet_pton(AF_INET, "127.0.0.1", &(svr_addr.sin_addr.s_addr)); //for ipv4
    svr_addr.sin_port = htons(9999);
    if (0>bind(listen_fd, (struct sockaddr*)&svr_addr, sizeof(svr_addr))) {
       LOG("bind - " << ERRNO());
       return 2;
    }

    //SSL_CTX를 준비한다.
    AutoSSLCTX ctx("SSLv3_server");

    if (NULL == (SSL_CTX*)ctx) {
        LOG("SSL_CTX_new - " << SSHConnection::strerror());
        return 3;
    }

    if (1 != SSL_CTX_use_certificate_file(ctx, "ca.crt", SSL_FILETYPE_PEM)) {
        LOG("SSL_CTX_new - " << SSHConnection::strerror());
        return 4;
    }

    if (1 != SSL_CTX_use_RSAPrivateKey_file(ctx, "ca.key", SSL_FILETYPE_PEM)) {
        LOG("SSL_CTX_new - " << SSHConnection::strerror());
        return 5;
    }

    char buf[1024];
    struct sockaddr_in cli_addr;

    //client으로 부터 연결을 처리
    while(true) {
        LOG("listening...");
        if (0>listen(listen_fd, 3)) {
            LOG("listen - " << ERRNO());
            return 6;
        }

        socklen_t cli_len = sizeof(cli_addr);
        int client_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
        if(0 == client_fd) {
            LOG("accept client error");
            return 7;
        }
        char ip_str[INET_ADDRSTRLEN] = {'\0',};
        inet_ntop(AF_INET, &(cli_addr.sin_addr), ip_str, sizeof(ip_str)); //for ipv4
        LOG("accepted - fd " << client_fd << " connected from " << ip_str);

        //여기서 부터 실제 SSL 연결 과정이 시작된다.

        AutoSSL ssl(ctx);
        SSL_set_fd(ssl, client_fd);

        SSL_accept(ssl);

        //데이터를 읽어 온다.
        int len = SSL_read(ssl, buf, sizeof(buf)-1);
        if (0 > len || 0 == len) {
            LOG("closed client - " << SSHConnection::strerror());
            LOG("closed client - " << ERRNO());
            CLOSE_SOCKET(client_fd);
            continue;
        }
        buf[len] = '\0';
        LOG("recv - " << buf);

        //데이터를 전송한다.
        len = SSL_write(ssl, buf, len);
        if (0 > len || 0 == len) {
            LOG("closed client - " << SSHConnection::strerror());
            LOG("closed client - " << ERRNO());
            CLOSE_SOCKET(client_fd);
            continue;
        }
        LOG("sent - " << len << " bytes");
        CLOSE_SOCKET(client_fd);
    }

    RELEASE_WSA();

    return 0;
}

클라이언트측 코드

여기에 비동기 소켓을 추가하였다. 별다른 이유가 없고 connect에 대해서 적용하고 싶었을 뿐이다. ㅡ.ㅡ;;;;;
그리고 앞의 공통 코드를 적극적으로 적용하였다. 자세한 내용은 코드에 주석을 참고하기 바란다.

#include "ssh_socket.hpp"

size_t connect_timeout = 300;
size_t transport_timeout = 300;

int main()
{
    INIT_WSA();

    SSHConnection::instance();

    int client = socket(PF_INET, SOCK_STREAM, 0);
    if (0 > client) {
        LOG("socket - " << ERRNO());
        return 1;
    }
    set_nonblock(client);
    LOG("set nonblock: fd " << client);

    struct sockaddr_in svr_addr;
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    svr_addr.sin_port = htons(9999);

    int ret = 0;

    //연결한다. 비동에서 연결 상태 체크는 아래말고도 다른 방식이 있지만, 대충 아래 처럼하면 된다.
    //안되면 다른 방식으로 적용해야할 것이다. ㅡ.ㅡ;
    while(true) {
        ret = connect(client, (struct sockaddr*)&svr_addr, sizeof(struct sockaddr));
        if (0>ret) {
            if (is_progress()) {
                if(is_writeable(client, connect_timeout)) continue;
            }
            LOG("connect - " << ERRNO());
            return 2;
        } else {
            break;
        }
    }

    //연결이 되었다면 ssl 연결에 사용할 객체를 초기화한다.
    AutoSSLCTX ctx("SSLv3_client");
    if (NULL == (SSL_CTX*)ctx) {
        LOG("SSL_CTX_new - " << SSHConnection::strerror());
        return 3;
    }

    AutoSSL ssl(ctx);
    if (NULL == (SSL*)ssl) {
        LOG("SSL_new - " << SSHConnection::strerror());
        return 4;
    }

    ssl.set_fd(client);

    //ssl handshake을 수행한다. 여러번에 걸처 데이터를 주고 받기에 한번의 처리로 완료되지 않는다.
    //그리고 openssl에서 리턴 값은 socket에서 리턴 값과 다르다. 주의가 필요하다.
    while(true) {
        ret = SSL_connect(ssl);
        if (0>ret) {
            //핵심적인 부분, 에러 인경우 에러 값을 이용해 어떤 작업이 필요한지 판단해야 함.
            switch(SSL_get_error(ssl, ret)) {
            case SSL_ERROR_WANT_READ:
                if (is_readable(client, transport_timeout)) continue;
                break;
            case SSL_ERROR_WANT_WRITE:
                if (is_writeable(client, transport_timeout)) continue;
                break;
            case SSL_ERROR_SYSCALL: // 일부 플랫폼에서는 별도 처리가 필요.
                if (is_progress()) {
                    if (SSL_want_write(ssl)) {
                        if (is_writeable(client, transport_timeout)) continue;
                    } else if (SSL_want_read(ssl)) {
                        if (is_readable(client, transport_timeout)) continue;
                    }
                    //error
                }
                break;
            default;
                //error
                break;
            }
            LOG("SSL_connect - " << SSHConnection::strerror() << "(" << ERR_get_error() << ")");
            return 5;
        } else if (0 == ret) {
            LOG("SSL_connect - " << SSHConnection::strerror() << "(" << ERR_get_error() << ")");
            return 5;
        } else {
            break;
        }
    }

    // 전송 부분에는 비동기 작업을 적용하지 않았다. 작은 데이터이기에 충분히 걍 버퍼에 쑤셔 넣을 수 있다. ㅡ.ㅡ;;
    char msg[] = "hello world";
    int len = SSL_write(ssl, msg, sizeof(msg));

    if (0>= len) {
        LOG("SSL_write - " << SSHConnection::strerror());
        CLOSE_SOCKET(client);
        return 6;
    }

    LOG("sent - " << msg << "(" << len << " bytes)");

    // 수신하는 부분은 비동기 처리가 필요하다. 앞의 연결과 비슷하다.
    char buf[1024];
    while(true) {
        ret = SSL_read(ssl, buf, sizeof(buf));
        if (0>ret) {
            switch(SSL_get_error(ssl, ret)) {
            case SSL_ERROR_WANT_READ:
                if (is_readable(client, transport_timeout)) continue;
                break;
            case SSL_ERROR_WANT_WRITE:
                if (is_writeable(client, transport_timeout)) continue;
                break;
            }
            LOG("SSL_read - " << SSHConnection::strerror());
            CLOSE_SOCKET(client);
            return 7;
        } else if (0==ret) {
            LOG("SSL_read - " << SSHConnection::strerror());
            CLOSE_SOCKET(client);
            return 8;
        } else {
            break;
        }
    }

    buf[len] = '\0';
    LOG("return - " << buf);

    CLOSE_SOCKET(client);

    RELEASE_WSA();

    return 0;
}

결론

지금 까지 openssl에서 비동기 ssl 연결을 살펴보았다. 코드가 조금 짬봉되어서 복잡해 보일 수도 있다. 그리고 코드의 완성도를 고려하지 않았으니 추가적인 보완을 하여 사용하시기 바란다.
클라이언트 측은 공통 코드를 적극적으로 적용하여 SSL 객체에 대한 메모리 릭을 최소화 하도록 구성하였다. 중간에 에러 리턴으로 해당 함수가 리턴이되어도 할당된 SSL 객체들은 자동 해지된다. 물론 좀더 정교하게 처리가 필요하겠지만...
클라이언트 측 코드가 비동기가 추가되었지만, 중간에 SSL 객체를 관리하는 코드가 없어지므로 해서 좀더 간결하게 구성될 수 있었다.
비동기 작업은 고속의 대량의 연결을 처리하고자 하면 피할 수 없는 운명이다. 사실 이전에 SSL 연결이 비동기로 처리할 때 제대로 연결과 송수신이 안되는 문제가 많았다. 위의 코드로 어느 정도 문제가 해결되리라 본다.
모두 즐프하기 바란다.ospace.

참조

[1] ospace114@empal.com, openssl에서 소켓 ssl 연결, 2012.08, http://ospace.tistory.com/288

반응형

'3.구현 > C or C++' 카테고리의 다른 글

protothreads 정리  (0) 2023.10.25
[c++] 함수자 구현 고찰  (0) 2023.10.18
데이터 값을 비트 문자열로 변환  (0) 2012.08.14
고급 매크로 기법 Variadic macro  (0) 2012.08.14
[C++0x] 람다식  (0) 2012.07.31