IP 直连

2019-02-18  本文已影响0人  程序猿老麦

背景

因为卡塔尔运营商屏蔽了 liveme 某一个域名https 域名服务, 促使调研 GA(AWS Global Accelerator) 是否可以绕开运营商屏蔽.

GA 配置后会提供两个静态 IP加速地址到 LB 上, 如果程序通过 IP 访问 HTTPS 服务器,还是需要在 SNI 或者 header 上增加域名信息, 大概率上也不能绕过运营商屏蔽(需要针对卡塔尔进行测试后确认).

文档介绍一下 IP 直连的方案, 通过GA静态 IP 加速边缘节点到 LB 之间的链路直接访问服务器.

什么是 IP 直连

采用 IP 直接访问服务器的方式称为 IP 直连(IP Direct). 采用 IP 直接访问服务器的方式称为 IP 直连(IP Direct).

为什么 IP 直连

IP 直连的问题

HTTP Host

因为请求的链接换成了 IP 地址, 请求到达服务器后,服务器无法判断是请求哪个域名.

HTTPS 握手
https 执行过程

发送HTTPS请求首先要进行SSL/TLS握手,握手过程大致如下

关键在于第三步骤, 验证过程有以下两个要点:

如何 IP 直连

HTTP Host

只需要在 HTTP Header 手动设置 Host 属性为原域名.

HTTPS TLS 握手

非 SNI (适用于服务器只配置一个 https 域名支持)

Android (此示例针对HttpURLConnection接口)

 try {

   String url = "[https://140.205.160.59/?sprefer=sypc00](https://140.205.160.59/?sprefer=sypc00)";

   final String hostName= "m.taobao.com";

   HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();

   connection.setRequestProperty("Host", hostName);

   connection.setHostnameVerifier(new HostnameVerifier() {

/*

* 使用 IP后 URL 里设置的hostname不是远程的域名,与证书颁发的域不匹配,

* Android HttpsURLConnection提供了回调接口让用户来处理这种定制化场景

*/

     @Override

     public boolean verify(String hostname, SSLSession session) {

         return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostName,   session);

     }

   });

   connection.connect();

} catch (Exception e) {

   e.printStackTrace();

} finally {

}

iOS (此示例针对 NSURLSession)

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
    ...
    /*
     * 获取原始域名信息。(获取 host 方法需要根据项目定制)
     */
    NSString *host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
    if (!host) {
        host = self.request.URL.host;
    }
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    } else {
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }
    // 对于其他的challenges直接使用默认的验证方案
    completionHandler(disposition, credential);
}

SNI 方式 (适用于服务器配置多个 https 域名支持)

服务器名称指示(英语:Server Name Indication,简称SNI)是一个扩展的TLS计算机联网协议[1],在该协议下,在握手过程开始时客户端告诉它正在连接的服务器要连接的主机名称。

注 : 若报出SSL校验错误,比如iOS系统报错kCFStreamErrorDomainSSL, -9813; The certificate for this server is invalid,Android系统报错System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.,请检查应用场景是否为SNI(单IP多HTTPS域名)

Android (针对HttpsURLConnection接口

定制SSLSocketFactory,在createSocket时替换 IP,并进行SNI/HostNameVerify配置

class TlsSniSocketFactory extends SSLSocketFactory {
    ...
    @Override
    public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
        String peerHost = this.conn.getRequestProperty("Host");
        if (peerHost == null)
            peerHost = host;
        InetAddress address = plainSocket.getInetAddress();
        if (autoClose) {
            // we don't need the plainSocket
            plainSocket.close();
        }
        // create and connect SSL socket, but don't do hostname/certificate verification yet
        SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
        SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);
        // enable TLSv1.1/1.2 if available
        ssl.setEnabledProtocols(ssl.getSupportedProtocols());
        // set up SNI before the handshake
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            sslSocketFactory.setHostname(ssl, peerHost);
        } else {
            try {
                java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
                setHostnameMethod.invoke(ssl, peerHost);
            } catch (Exception e) {
            }
        }
        // verify hostname and certificate
        SSLSession session = ssl.getSession();
        if (!hostnameVerifier.verify(peerHost, session))
            throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);
        return ssl;
    }
}

对于需要设置SNI的站点,通常需要重定向请求,示例中也给出了重定向请求的处理方法

public void recursiveRequest(String path, String reffer) {
    URL url = null;
    try {
        url = new URL(path);
        conn = (HttpsURLConnection) url.openConnection();
        String ip = httpdns.getIpByHostAsync(url.getHost());
        if (ip != null) {
            String newUrl = path.replaceFirst(url.getHost(), ip);
            conn = (HttpsURLConnection) new URL(newUrl).openConnection();
            // 设置HTTP请求头Host域
            conn.setRequestProperty("Host", url.getHost());
        }
        conn.setConnectTimeout(30000);
        conn.setReadTimeout(30000);
        conn.setInstanceFollowRedirects(false);
        TlsSniSocketFactory sslSocketFactory = new TlsSniSocketFactory(conn);
        conn.setSSLSocketFactory(sslSocketFactory);
        conn.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                String host = conn.getRequestProperty("Host");
                if (null == host) {
                    host = conn.getURL().getHost();
                }
                return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
            }
        });
        int code = conn.getResponseCode();// Network block
        if (needRedirect(code)) {
            //临时重定向和永久重定向location的大小写有区分
            String location = conn.getHeaderField("Location");
            if (location == null) {
                location = conn.getHeaderField("location");
            }
            if (!(location.startsWith("http://") || location.startsWith("https://"))) {
                //某些时候会省略host,只返回后面的path,所以需要补全url
                URL originalUrl = new URL(path);
                location = originalUrl.getProtocol() + "://" + originalUrl.getHost() + location;
            }
            recursiveRequest(location, path);
        } else {
            // redirect finish.
            DataInputStream dis = new DataInputStream(conn.getInputStream());
            int len;
            byte[] buff = new byte[4096];
            StringBuilder response = new StringBuilder();
            while ((len = dis.read(buff)) != -1) {
                response.append(new String(buff, 0, len));
            }
        }
    } catch (MalformedURLException e) {
        Log.w(TAG, "recursiveRequest MalformedURLException");
    } catch (IOException e) {
        Log.w(TAG, "recursiveRequest IOException");
    } catch (Exception e) {
        Log.w(TAG, "unknow exception");
    } finally {
        if (conn != null) {
            conn.disconnect();
        }
    }
}
private boolean needRedirect(int code) {
    return code >= 300 && code < 400;
}

iOS (支持 Post 请求)

基于 CFNetWork ,hook 证书校验步骤, 使用 NSURLProtocol 拦截 NSURLSession 请求丢失 body

采用 Category 的方式为NSURLRequest 增加方法.

@interface NSURLRequest (NSURLProtocolExtension)
- (NSURLRequest *)httpdns_getPostRequestIncludeBody;
@end
@implementation NSURLRequest (NSURLProtocolExtension)
- (NSURLRequest *)httpdns_getPostRequestIncludeBody {
    return [[self httpdns_getMutablePostRequestIncludeBody] copy];
}
- (NSMutableURLRequest *)httpdns_getMutablePostRequestIncludeBody {
    NSMutableURLRequest * req = [self mutableCopy];
    if ([self.HTTPMethod isEqualToString:@"POST"]) {
        if (!self.HTTPBody) {
            NSInteger maxLength = 1024;
            uint8_t d[maxLength];
            NSInputStream *stream = self.HTTPBodyStream;
            NSMutableData *data = [[NSMutableData alloc] init];
            [stream open];
            BOOL endOfStreamReached = NO;
            //不能用 [stream hasBytesAvailable]) 判断,处理图片文件的时候这里的[stream hasBytesAvailable]会始终返回YES,导致在while里面死循环。
            while (!endOfStreamReached) {
                NSInteger bytesRead = [stream read:d maxLength:maxLength];
                if (bytesRead == 0) { //文件读取到最后
                    endOfStreamReached = YES;
                } else if (bytesRead == -1) { //文件读取错误
                    endOfStreamReached = YES;
                } else if (stream.streamError == nil) {
                    [data appendBytes:(void *)d length:bytesRead];
                }
            }
            req.HTTPBody = [data copy];
            [stream close];
        }
    }
    return req;
}

在用于拦截请求的 NSURLProtocol 的子类中实现方法 +canonicalRequestForRequest: 并处理 request 对象

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return [request httpdns_getPostRequestIncludeBody];
}

注意在拦截 NSURLSession 请求时,需要将用于拦截请求的 NSURLProtocol 的子类添加到 NSURLSessionConfiguration 中,用法如下:

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

NSArray *protocolArray = @[ [CUSTOMEURLProtocol class] ];
configuration.protocolClasses = protocolArray;

NSURLSession *session = [NSURLSession 
sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];

参考文献:

https://help.aliyun.com/document_detail/30143.html?spm=a2c4g.11186623.6.565.72ea7797r1oQbB

https://help.aliyun.com/knowledge_detail/60147.html

https://juejin.im/post/5a81bbd66fb9a0634c266fe1

https://www.jianshu.com/p/cd4c1bf1fd5f

https://zh.wikipedia.org/wiki/%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%90%8D%E7%A7%B0%E6%8C%87%E7%A4%BA**

上一篇 下一篇

猜你喜欢

热点阅读