背景
因为卡塔尔运营商屏蔽了 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 直连
- 可以绕过 localDNS, 防止域名劫持, 提高访问成功率
- 因为无需 DNS 解析, 可以提高网络访问速度.
- 更灵活的业务调度策略
IP 直连的问题
HTTP Host
因为请求的链接换成了 IP 地址, 请求到达服务器后,服务器无法判断是请求哪个域名.
HTTPS 握手
发送HTTPS请求首先要进行SSL/TLS握手,握手过程大致如下
- 客户端发起握手请求,携带随机数、支持算法列表等参数。
- 服务端收到请求,选择合适的算法,下发公钥证书和随机数。
- 客户端对服务端证书进行校验,并发送随机数信息,该信息使用公钥* 加密。
- 服务端通过私钥获取随机数信息。
- 双方根据以上交互的信息生成session ticket,用作该连接后续数据传输的加密密钥。
关键在于第三步骤, 验证过程有以下两个要点:
- 客户端用本地保存的根证书解开证书链,确认服务端下发的证书是由可信任的机构颁发的。
客户端需要检查证书的domain域和扩展域,看是否包含本次请求的host - 如果上述两点都校验通过,就证明当前的服务端是可信任的,否则就是不可信任,应当中断当前连接
如何 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**