查看 DNS 查询记录

要使用外部服务, 就要连接外部 endpoint, 通常我们都是用 FQDN 去连接外部, 偶尔直接使用 IP 地址. 如果使用 FQDN, 那么就涉及 DNS 查询. 通常 DNS 记录都有 TTL (time to live)时限, 时限一过, 就要重新 DNS 查询.
使用 dig 命令可以查看一条 DNS 记录的 ttl:

$ dig +nocmd +noall +answer +ttlid a www.tianxiaohui.com
www.tianxiaohui.com.    389    IN    A    103.144.218.
$ dig +nocmd +noall +answer +ttlid a www.baidu.com
www.baidu.com.        1200    IN    CNAME    www.a.shifen.com.
www.a.shifen.com.    239    IN    CNAME    www.wshifen.com.
www.wshifen.com.    239    IN    A    45.113.192.102
www.wshifen.com.    239    IN    A    45.113.192.101

上面结果第二列就是 ttl, 单位是秒.

如何查看 Linux 上的 DNS 查询

使用 tcpdump 查看 53 端口的流量, 就能查看 DNS 查询记录 (

$ sudo tcpdump -l port 53  # -l 表示使用 buffer
06:29:08.312928 IP 10.226.197.72.40898 > sin-dns.tianxiaohui.com.domain: 42885+ A? firestore.googleapis.com. (42)
06:29:08.422806 IP sin-dns.tianxiaohui.com.domain > 10.226.197.72.40898: 42885 1/0/0 A 142.250.4.95 (58)
06:29:45.507982 IP 10.226.197.72.5013 > sin-dns.tianxiaohui.com.domain: 2509+ A? github.com. (28)
06:29:45.575657 IP sin-dns.tianxiaohui.com.domain > 10.226.197.72.5013: 2509 1/0/0 A 20.205.243.166 (44)
06:29:45.576415 IP 10.226.197.72.42090 > sin-dns.tianxiaohui.com.domain: 64400+ A? github.com. (28)
06:29:45.640234 IP sin-dns.tianxiaohui.com.domain > 10.226.197.72.42090: 64400 1/0/0 A 20.205.243.166 (44)
06:30:05.738311 IP 10.226.197.72.54902 > sin-dns.tianxiaohui.com.domain: 51140+ A? translate.google.com. (38)

如何查看一个 Java 应用程序的 DNS 查询

Java 应用程序的 DNS 最近的查询记录都记录在 InetAddress 类的某个字段中, 所以对这个字段读, 或者你有一个 heap dump, 查看这个字段, 就能查看到最近的 DNS 记录. 详细的方法, 可以查看这段代码:
https://github.com/manecocomph/myJavaAgent/blob/master/src/com/tianxiaohui/java/agent/SampleAgent.java#L191

google search Operators 谷歌搜索操作符

  1. 限定搜索结果在某个或某些站点 -> 使用 site:

    site:github.com  
    site:*.edu
  2. 完全匹配多个字符 -> 使用双引号 ""

    "Steve Jobs"
  3. 结果不包含某些字符 -> 使用减号 -

    Socket -network
  4. 模糊匹配 -> 使用星号 *

    steve * apple
  5. 或者 -> 使用 or 或 |

    steve or gates
  6. 词典 翻译 -> 使用 define:

    define:star
  7. 返回最新缓存的 -> 使用cache:

    cache:ebay.com
  8. 特定文件类型 -> 使用 filetype: 或 ext:

    Distributed Systems, 3rd Edition filetype:pdf
  9. 搜索相关站点 -> 使用 related:

    related:github.com
  10. 搜索关键字在标题, url, 文本, 超链接 -> intitle: inurl: intext: inanchor:

    intitle:star
    inurl:star
    intext:star
  11. 搜索关键字全在标题, url, 文本, 超链接 -> allintitle: allinurl: allintext: allinanchor:

    allintitle:star life water
    allinurl:star life water
    allintext:star life water
  12. 多个条件组合 -> 使用括号

    (steve or jobs) rich

Redis 学习笔记

工作中从来没用到过 Redis, 偶尔看过文档, 这次在学习数据库的理论一本书的过程中, 又萌发了要学习一边 Redis 的兴趣.
主要文档: https://redis.io/documentation
主要书籍:

  1. Mastering Redis (Packt, 2016) by Jeremy Nelson.
  2. Redis Essentials (Packt, 2015) by Maxwell Da Silva and Hugo Tavares
    实践: docker 安装 https://hub.docker.com/_/redis
$ docker network create redis-network
$ sudo docker run --network redis-network --restart always --volume /home/supra/work/data/redis/data:/data --name redis -p 6379:6379 -d redis redis-server --save 60 1 --loglevel warning
$ docker run -it --network redis-network --rm redis redis-cli -h redis

Redis Essentials 笔记:

如何搜索 grafana 的所有仪表盘(dashboard) 和面板(panel)的标题(title)

Grafana 提供了很多各种各样的图表, 被广泛用在各种需要仪表盘(dashboard)的场合. 它默认提供的搜索, 可以搜所有的文件夹名字和 dashboard 的名字. 可是有时候我们只看到别人给了一个 panel 的截图, 如何根据 panel的名字找到对应的 dashboard 呢? 当你有上千个 dashboard 的时候, 这种情况变得很困难.


最好的方式是 Grafana 提供这样一个插件, 可以搜索所有的文件夹(folder)/仪表盘(dashboard)/面板(panel)的名字, 但是我们现在没有找到.
其次, 这些文件夹(folder)/仪表盘(dashboard)/面板(panel)都定义在 grafana 背后的一个数据库中, 如果这个数据库的表提供了全文索引, 也能方便我们搜索. 但是现在也没有.

那么有没有可能我们把所有的 文件夹(folder)/仪表盘(dashboard)/面板(panel) 的标题放到一个支持全文索引的产品里面, 然后提供一个索引搜索的可能呢?

这种方式是可行的. 首先, Grafana 提供了一整套的 API, 我们可以通过 API 列出所有的文件夹, 然后列出每个文件夹下的所有 dashboard, 然后查询每个 dashboard 的每个 panel , 这样我们就能获得 文件夹(folder)/仪表盘(dashboard)/面板(panel) 的所有标题了. 接着, 我们把获得的标题放到一个支持全文索引的产品, 就能支持全面搜索了.

这里我们给出一个 JavaScript 的脚本, 当你打开你的 Grafana 之后, 然后登录, 然后打开开发者工具, 选择 Console tab, 然后运行这个脚本. 当脚本运行完之后, 在运行导出脚本, 它会打印所有要导出的 json 内容, 然后把这些 json 内容复制到文本编辑器, 然后保存到本地.

获得所有标题的内容

使用这个脚本
https://github.com/manecocomph/scripts/blob/main/JavaScript/grafana/exportAllTitles.js
当上面的脚本运行完之后, 可以看到上面的文件结尾处在注释的地方有个导出脚本, 运行导出脚本. 导出完之后, 复制保存到本地.
export.png

导入到 MongoDB, 并设置全文索引

安装 MongoDB 和 webUI. 更改下面的 volume 到你的本地文件夹

sudo docker network create mongo-network
sudo docker run --network mongo-network --restart always -p 27017:27017 --volume /home/supra/work/data/mongo/grafana:/data/db --name mongodb -d mongo
sudo docker run --network mongo-network --restart always -e ME_CONFIG_MONGODB_SERVER=mongodb -p 8081:8081 --name mongoui mongo-express

可以选择 webUI 导入, 或者使用下面的命令行在 docker 容器内导入(更改 db 名字和 collection 名字, 以及文件名)

mongoimport --db dbName --collection collectionName --file fileName.json 

设置全文索引, 如何查询

db.grafana.createIndex({title: "text", folder: "text", panels: "text", });
db.grafana.find({$text: {$search: "EricTian"}});

导入 ElasticSearch & Kibana

安装 ES & Kibana

sudo docker network create elastic-network
sudo docker run --restart always --name es01 --network elastic-network -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -d docker.elastic.co/elasticsearch/elasticsearch:7.15.2
sudo docker run --restart always --name kib01 --network elastic-network -p 5601:5601 -e "ELASTICSEARCH_HOSTS=http://es01:9200" -d docker.elastic.co/kibana/kibana:7.15.2

打开 http://host:5601/ 然后导入数据文件, 然后查询

导入 Splunk (最方便)

安装 Splunk

sudo docker run --rm -p 8000:8000 -e "SPLUNK_START_ARGS=--accept-license"  -e "SPLUNK_PASSWORD=admin" --name splunk splunk/splunk

打开 http://host:8000/ 输入用户名/密码 admin/admin 然后选择导入数据文件, 然后查询

使用 Java 访问 https 的站点 TLS 协商中的 SNI 问题

上周有同事说他们的 Java 应用连一个新的服务, 总是有连接问题, 但是切换到老的服务一起都正常. 经过查看日志, tcp 抓包, Loader Balancer 验证, 我们发现问题的根源是 TLS 协商中 client_hello 中涉及的 SNI 扩展问题.


问题再现

这个 Java 应用连接一个 Elastic Search 服务, 这个 ES (Elastic Search) 的 endpoint 只支持 https 协议, 平时一切运行正常. 最近这个 ES 服务重新在 K8S 集群上面重新做了一个, 同样只支持 https 协议. 但是在 Java 应用这边, 把 endpoint 改成新的 URL, 可是总是报错. 一开始开发人员以为是新的 ES 可能慢, 或者网络问题, 于是加大了 client 端的 connect timeout & read timeout 时间. 即便增加了很多, 问题还是没有改善.

原因分析

首先, 我们看了出错日志, 发现是连接超时, 也就是连接本身就有问题. 然后我们在客户端抓包, 发现以下情形: 首先 TCP 的三次握手是没有任何问题的. 然后开始协商 TLS 协议, 客户端发送 client hello 包, 服务端 ack client hello, 然后就直接 reset 了这个连接. 然后一切就结束了. 所以, 可以看到是 TLS 协商的问题.
image.png

从这里推断, 我们猜测是客户端发送的 client_hello 里面的一些信息, 在服务端认为不可接受, 所以直接就 reset 了. 首先我们猜测客户端发送的这些加密方法(cipher suites) 在服务端没有一个被接受的. 客户端说我使用 TLSv1.2 协议, 发送了如下的加密方法:
image (1).png
我们查看了这些加密方法, 这些方法正是: io.netty.handler.ssl.SslUtils.java 里面默认的加密方法:
defaultcipher.png
这些加密方法是默认所有加密端都必须接受的, 所以这个不是问题.

又咨询了做加密证书的同事, 他们说这些加密方法服务端肯定是接受的, 都是默认的. 他们猜测这些连接被 reset 的原因很有可能是 SNI 导致的, 对于在K8S新加的 endpoint, 这些都要求客户端在发送 client_hello 的时候, 必须发送 SNI 扩展, 否则直接 reset 连接.

于是重新查看了 TCP dump, 发现确实没有发送 SNI 扩展项. 这就是导致问题的根源.

HTTP 协议中的 host header

在讲 TLS 协议的 SNI 之前, 我们先说一下 HTTP 协议中的 host header. 在早期的 HTTP 协议中, 是没有 host header 的. 一个 HTTP 请求大概是这样的:

GET /index.html HTTP/1.0

在那个时候, 网站还没有那么多, 一般每个网站都有一个独立的 IP. 后来网站越来越多, IPv4逐渐被用光, IPv6 还没被大规模使用, 于是兴起了虚拟主机(Virtual Host), 每个 IP 后边可能有多个网站. 为了区分不同的网站, 于是 HTTP 协议就加了一个 host header 用来区分不同的网站. 于是一个 HTTP 请求大概是这样的:

GET /index.html HTTP/1.0
Host www.tianxiaohui.com

所以, 概括起来就是: Host header 解决了一个 IP 多个网站的问题.

TLS 协议 client_hello 中的 SNI 扩展

如果使用 https 协议, 那么在 TCP 3次握手之后, 就是 TLS 协商, 如果一个 IP 上面有多个网站, 都有不同的证书, 那么如何在 TLS 协商阶段就能区分不同的网站呢? 于是 TLS 协议添加了 SNI (Server Name Indication) 扩展, 在 SNI 扩展里面设置 server_name 的字段值, 通过这个字段值, 服务端在开始握手的时候, 就能知道它要访问具体那个网站.
sni.png

Java 支持 SNI

Java 从 JDK 7 开始支持 SNI. https://docs.oracle.com/javase/7/docs/technotes/guides/security/enhancements-7.html

Server Name Indication (SNI) for JSSE client: The Java SE 7 release supports the Server Name Indication (SNI) extension in the JSSE client. SNI is described in RFC 4366. This enables TLS clients to connect to virtual servers.

Java 中常见的2种连接关于 SNI 的实验

我们的应用使用的是 JDK 8, 那么为什么还缺少 SNI 呢? 这要归因于那段代码使用的是 Netty 库, Netty 对于 TLS 的处理是使用的 JDK 底层 API, 需要自己处理 SNI 这个参数.

通常在 Java 中我们会遇到如下两种 https连接方式: 1) 使用 Java 的 URL连接; 2) 使用 Netty 的连接. 于是我们实验了这2种方式, 在使用Java 的 URL 连接的时候, 它默认是带 SNI 的. 如果使用 Netty 的方式, 就需要手工设置 SslParameter 里面设置主机名. 否则 Netty 不会发送 SNI. 通过 tcp 抓包的方式, 我们可以验证这些信息.
实验代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslProvider;

public class Test {
    public static void main(String[] args) throws IOException, InterruptedException {
        //Test.testHttpsUrlSni();
        Test.testNettyHttpsSni();
    }

    public static void testHttpsUrlSni() throws IOException {
        URL u = new URL("https://www.tianxiaohui.com/");
        HttpsURLConnection http = (HttpsURLConnection) u.openConnection();
        http.setAllowUserInteraction(true);
        http.setRequestMethod("GET");
        http.connect();

        try (InputStream is = http.getInputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {

            StringBuilder stringBuilder = new StringBuilder();
            String line = null;
            while (null != (line = reader.readLine())) {
                stringBuilder.append(line + "\n");
            }
            System.out.println(stringBuilder.toString());
        }

        System.out.println("***************Https testing completed **************");
    }

    public static void testNettyHttpsSni() throws SSLException, InterruptedException {
        EventLoopGroup childGroup = new NioEventLoopGroup();

        // SSL Parameters to set SNI TLS Extension

        SSLParameters sslParameters = new SSLParameters();
        sslParameters.setServerNames(Collections.singletonList(new SNIHostName("facebook.com")));

        // Build SSLContext for Client
        SslContext sslContext = SslContextBuilder.forClient().sslProvider(SslProvider.JDK).build();

        // SSLEngine with SSL Parameters for SNI
        SSLEngine sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT);
        // sslEngine.setSSLParameters(sslParameters);//是否设置在这里

        // SSL Handler
        SslHandler sslHandler = new SslHandler(sslEngine);

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(childGroup).channel(NioSocketChannel.class).handler(new SimpleChannelInboundHandler<>() {
            @Override
            protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
                // JUST RANDOM CODE TO MAKE TLS REQUEST
                FullHttpRequest fullHttpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/",
                        Unpooled.EMPTY_BUFFER);
                ctx.writeAndFlush(fullHttpRequest);
            }
        });

        Channel ch = bootstrap.connect(new InetSocketAddress("tianxiaohui.com", 443)).sync().channel();
        ch.pipeline().addFirst(sslHandler);
    }