惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
Google DeepMind News
Google DeepMind News
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
NISL@THU
NISL@THU
T
Threat Research - Cisco Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
Lohrmann on Cybersecurity
V
Visual Studio Blog
Cyberwarzone
Cyberwarzone
D
Docker
The Hacker News
The Hacker News
C
CERT Recently Published Vulnerability Notes
Vercel News
Vercel News
Project Zero
Project Zero
S
Schneier on Security
aimingoo的专栏
aimingoo的专栏
I
Intezer
腾讯CDC
M
MIT News - Artificial intelligence
Hugging Face - Blog
Hugging Face - Blog
P
Palo Alto Networks Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
AWS News Blog
AWS News Blog
GbyAI
GbyAI
MongoDB | Blog
MongoDB | Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Vulnerabilities – Threatpost
G
Google Developers Blog
N
Netflix TechBlog - Medium
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
A
Arctic Wolf
S
Securelist
酷 壳 – CoolShell
酷 壳 – CoolShell
Cisco Talos Blog
Cisco Talos Blog
Recent Announcements
Recent Announcements
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
T
Threatpost
Latest news
Latest news
Blog — PlanetScale
Blog — PlanetScale
Security Latest
Security Latest
Engineering at Meta
Engineering at Meta
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
The GitHub Blog
The GitHub Blog
T
Tor Project blog
P
Proofpoint News Feed

Mohuishou

如何实现支持多集群的 Kubernetes Operator? 第三方应用如何调用我们 kubebuilder 生成的自定义资源? Kubernetes 简明教程 k8s job 为何迟迟不能结束? Go 工程化(十一) 如何优雅的写出 repo 层代码 Go 工程化(十) 如何在整洁架构中使用事务? 给博客添加章节目录 使用 Notion Database 管理静态博客文章 一个普通 Go 开发的三年 Go可用性(七) 总结: 一张图串联可用性知识点 Go可用性(六) 熔断 10. 总结 9. kubebuilder 进阶: 源码分析 8. kubebuilder 进阶: webhook 7. kubebuilder 进阶: 测试 6. kubebuilder 实战: status & event 5. kubebuilder 实战: CRUD 4. kustomize 简明教程 3. KubeBuilder 简明教程 2. Kind: 如何快速搭建本地 K8s 开发环境? 1. Operator概述: 如何对 Kubernetes 进行扩展 Go可用性(五) 自适应限流 Go可用性(四) 漏桶算法 Go可用性(三) 令牌桶的实现 rate/limt Go可用性(二) 令牌桶原理及使用 Go可用性(一) 隔离设计 Go并发编程(十二) Singleflight Go工程化(九) 项目重构实践 Go工程化(八) 单元测试 Go工程化(七) Go Module Go工程化(六) 配置管理 Go工程化(五) API 设计下: 基于 protobuf 自动生成 gin 代码 Go工程化(四) API 设计上: 项目结构 & 设计 Go工程化(三) 依赖注入框架 wire Go工程化(二) 项目目录结构 Go工程化(一) 架构整洁之道阅读笔记 Go并发编程(十一) 总结 Go并发编程(十) 深入理解 Channel Go并发编程(九) 深入理解 Context Go并发编程(八) 深入理解 sync.Once Go并发编程(七) 深入理解 errgroup Go并发编程(六) 深入理解 WaitGroup Go并发编程(五) 深入理解 sync/atomic Go并发编程(四) 深入理解 Mutex Go并发编程(三) data race Go并发编程(二) Go 内存模型 Go并发编程(一) goroutine Go错误处理最佳实践 微服务(二) 服务发现&多租户 微服务(一) 微服务概览 5. 栈下: 深入理解 defer 4. 栈上: 如何实现一个计算器 Go Struct 初始化风格的抉择 3. 数组下: 使用 GDB 调试 Golang 代码 2. 数组上: 深入理解 slice 1. 链表: 深入理解container/list&LRU缓存的实现 Go设计模式24-总结(更新完毕) Go设计模式23-中介模式 Go设计模式22-解释器模式 Go设计模式21-命令模式 Go设计模式20-备忘录模式 Go设计模式19-访问者模式 Go设计模式18-迭代器模式 Go设计模式17-状态模式 Go设计模式16-职责链模式(Gin的中间件实现) Go设计模式15-策略模式 Go模板模式14-模板模式 Go设计模式13-观察者模式(实现简单的EventBus) Go设计模式12-享元模式 Go设计模式11-组合模式 Go设计模式10-门面模式 Go设计模式09-适配器模式 Go设计模式08-装饰器模式 Go设计模式07-桥接模式 Go设计模式06-代理模式(generate实现类似动态代理) Go设计模式05-创建型模式总结 Go设计模式04-原型模式 Go设计模式03-建造者模式 Go设计模式02-工厂模式&DI容器 笔记-让你最快速地改善代码质量的20条编程规范 Go设计模式01-单例模式 一点拙见-如何写好一个技术预研报告? Go Web小技巧(四)在单个仓库中支持多个 go mod 模块 Go Web 小技巧(三)Gin 参数绑定 Go Web 小技巧(二)GORM 使用自定义类型 Go Web 小技巧(一)简化Gin接口代码 善用工具之postman高级用法概述 go generate and ast hexo-next-algolia-search全文搜索 docker镜像瘦身&优化 GORM避坑指南之含关联关系的更新 Github Actions介绍&自动构建Github Pages博客 在blog中内嵌在线PPT 记一次net http内存泄漏 使用TravisCI自动部署Blog 使用Goland调试Go程序 一个十分边缘的gorm的bug Httprouter介绍及源码阅读 Gin源码阅读 从0.1开始
4. localhost 就一定是 localhost 么?
Mohuishou · 2021-06-15 · via Mohuishou

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

我们在本地测试或者本地通讯的时候经常使用 localhost 域名,但是访问 localhost 的对应的一定就是我们的本机地址么?

背景

在一个风和日丽下午,突然收到了运维同学的反馈,说我们的一个服务调用突然报错了,关键是这个服务已经半年没有更新发版过了,询问后得知最近基础架构也没有什么变更,这就很迷了

我们排查日志后发现这个服务去调用了一个不知名的 ip 地址,这个地址还能 ping 通,但是我们明明是配置的 localhost,为什么会出现这个地址?localhost 不应该指向的是 127.0.0.1 么?我们使用 dignslookup 之后发现 localhost 的确是 127.0.0.1

我们修改了应用的配置,让这个调用直接调用 127.0.0.1 结果发现这个时候服务就正常了,然后我们在机器上抓包之后发现 localhost 竟然走了域名解析! 并且 localhost 这个域名在我们内网还被注册了,解析出来的地址就是最开始发现的这个不知名的地址

小结

所以我们下意识认为的域名解析流程应该是这样的,先去找 /etc/hosts 文件,localhost 找到了(默认是 127.0.0.1)就返回了

1

排查之后发现,实际上的流程是这样的,先做了 DNS 查询 DNS 没查到然后去查了 /etc/hosts 文件

2

直到有一天,我们的内网域名解析中添加了一个 localhost 的域名解析,就直接查询成功返回了

复现

我们先使用一段简单的代码复现一下,简单请求一下 localhost 就行了

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"net/http"
)

func main() {
client := &http.Client{}
_, err := client.Get("http://localhost:8080")
fmt.Println(err)
}

然后我们使用 GODEBUG="netdns=go+2" 环境变量执行程序,带上这个环境变量之后程序运行时就会输出是先执行 dns 查询还是先从 /etc/hosts 文件进行查询

1
2
3
4
GODEBUG="netdns=go+2" go run main.go 
go package net: GODEBUG setting forcing use of Go's resolver
go package net: hostLookupOrder(localhost) = files,dns
Get "http://localhost:8080": dial tcp [::1]:8080: connect: connection refused

上面显示的 files,dns 的意思就是先从 /etc/hosts 文件中查询,再去查询 dns 结果,但是我们当时服务的运行结果是 dns,files 这个问题出现在哪里呢?和 Go 的版本以及本地环境有关系

我们使用 Docker 模拟了线上环境,我们线上也是用的 Docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
FROM golang:1.15 as builder

WORKDIR /app

COPY main.go main.go
COPY run.sh run.sh

ENV CGO_ENABLED=0
ENV GOOS=linux

RUN go build main.go

FROM alpine:3

WORKDIR /app

COPY --from=builder /app /app
COPY run.sh run.sh

RUN chmod +x run.sh

ENV GODEBUG="netdns=go+2"
ENV CGO_ENABLED=0
ENV GOOS=linux

CMD /app/run.sh

使用这个容器运行的结果如下,可以看到已经变成了 dns,files 为什么会这样呢?

1
2
3
go package net: built with netgo build tag; using Go's DNS resolver
go package net: hostLookupOrder(localhost) = dns,files
Get "http://localhost:8080": dial tcp 127.0.0.1:8080: connect: connection refused

排查

src/net/dnsclient_unix.go

Go 中定义了下面几种 DNS 解析顺序,其中 files 表示查询 /etc/hosts 文件,dns 表示执行 dns 查询

1
2
3
4
5
6
7
8
9
10
11
12
13
// hostLookupOrder specifies the order of LookupHost lookup strategies.
// It is basically a simplified representation of nsswitch.conf.
// "files" means /etc/hosts.
type hostLookupOrder int

const (
// hostLookupCgo means defer to cgo.
hostLookupCgo hostLookupOrder = iota
hostLookupFilesDNS // files first
hostLookupDNSFiles // dns first
hostLookupFiles // only files
hostLookupDNS // only DNS
)

src/net/conf.go 中可以看到

Go 会先根据一些初始条件判断查询的顺序,然后就查找 /etc/nsswitch.conf 文件中的 hosts 配置项,如果不存在就会走一些回退逻辑。这次的问题出现在这个回退逻辑上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder) {
// ... 省略

nss := c.nss
srcs := nss.sources["hosts"]
// If /etc/nsswitch.conf doesn't exist or doesn't specify any
// sources for "hosts", assume Go's DNS will work fine.
if os.IsNotExist(nss.err) || (nss.err == nil && len(srcs) == 0) {
if c.goos == "solaris" {
// illumos defaults to "nis [NOTFOUND=return] files"
return fallbackOrder
}
if c.goos == "linux" {
// glibc says the default is "dns [!UNAVAIL=return] files"
// https://www.gnu.org/software/libc/manual/html_node/Notes-on-NSS-Configuration-File.html.
return hostLookupDNSFiles
}
return hostLookupFilesDNS
}
if nss.err != nil {
// We failed to parse or open nsswitch.conf, so
// conservatively assume we should use cgo if it's
// available.
return fallbackOrder
}
}

通过上面的代码我们可以发现,当前系统如果是 linux 并且不存在 /etc/nsswitch.conf 文件的时候,会直接返回 dns,files 的顺序,这个是参考了 glibc 的实现[2]

这个问题其实一般在虚拟机上没有问题,因为一般操作系统都会默认有这个配置文件,但是容器化之后我们一般喜欢使用 alpine linux 这种比较小的基础镜像,alpine 中就不存在的 /etc/nsswitch.conf 这个文件,所以就有可能会出现问题

上面这段逻辑不能再 1.16 中进行复现,是因为 1.16 已经修改了这个逻辑,主要就是把 linux 的这个判断分支删除掉了,感兴趣可以看这个修改记录[3] 和这个 issue[4]

总结

最大的感受就是经验主义害死人,很多时候由于我们知识点的原因所以可能会出现一些和我们认为的常识相违背的地方,这个时候就需要大胆假设小心求证了

针对这次这个问题的修复方案,我们是直接先删除了 localhost 的解析,复盘之后给出我不成熟的几点小建议

  • 公司内网就不要搞注册 localhost 域名这种骚操作了
  • 基础镜像的维护很重要,建议大家最好能够统一一个基础镜像这样不仅仅可以减少一些磁盘空间,同时还可以做一些统一的变更,例如这次这种就可以直接在基础镜像加上 /etc/nsswitch.conf 文件,避免其他业务也进坑里
  • 如果没有什么特别的版本依赖(绝大部分应用其实都没有)Go 版本建议升级 1.16 可以省很多事
  • dns 解析并不一定会先查询 hosts 文件,除了这种默认的情况外,还可以手动修改 /etc/nsswitch.conf 文件,调整解析的顺序,这个感兴趣的话可以试试

这篇文章还试着用 figma 做了几个小动画,感觉还是不错,后续有空写文章可以再搞搞(曹大不要再卷了,快学不动了)

参考文献

关注我获取更新

猜你喜欢