kubebuilder之一:kubernetes operator工作原理
https://gist.github.com/BruceChen7/778b7c683f27da8990d924a0a1e182e8
当用户发起一个网络请求时,流量会通过默认的网卡接口流出与流入,但有时需要将流量通过指定的网卡进行流出流入,这时我们可能需要进行一些额外的开发工作,对其实现主要用到了 Dialer.Control
配置项。
type Dialer struct {
// If Control is not nil, it is called after creating the network
// connection but before actually dialing.
//
// Network and address parameters passed to Control method are not
// necessarily the ones passed to Dial. For example, passing "tcp" to Dial
// will cause the Control function to be called with "tcp4" or "tcp6".
Control func(network, address string, c syscall.RawConn) error
}
可以看到这是一个函数类型的参数。
当前系统一共两个网卡 ens33
和 ens160
,ip地址分别为 192.168.3.80
和 192.168.3.48
➜ ~ ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.3.80 netmask 255.255.255.0 broadcast 192.168.3.255
inet6 fe80::8091:2406:c51e:ecb9 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:4f:05:90 txqueuelen 1000 (Ethernet)
RX packets 4805008 bytes 826619853 (826.6 MB)
RX errors 0 dropped 104152 overruns 0 frame 0
TX packets 732513 bytes 284605386 (284.6 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.3.48 netmask 255.255.255.0 broadcast 192.168.3.255
inet6 fe80::259a:d8d4:80a9:7fa4 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:4f:05:9a txqueuelen 1000 (Ethernet)
RX packets 4158530 bytes 746167179 (746.1 MB)
RX errors 1 dropped 106875 overruns 0 frame 0
TX packets 351616 bytes 149235606 (149.2 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 426742 bytes 80978543 (80.9 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 426742 bytes 80978543 (80.9 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
路由表记录
➜ ~ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.3.1 0.0.0.0 UG 100 0 0 ens33 0.0.0.0 192.168.3.1 0.0.0.0 UG 101 0 0 ens160 169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 ens33 192.168.3.0 0.0.0.0 255.255.255.0 U 100 0 0 ens33 192.168.3.0 0.0.0.0 255.255.255.0 U 101 0 0 ens160
从最后两条路由记录可以看到对于 192.168.3.0/24
这个段的流量会匹配的两个物理网卡,但由于配置的 Metric
的优先级比较的高,因此最终流量只会走网卡 ens33
。
我们可以在另一台机器 192.168.3.58
用 python3
搭建一个HTTPServer
,用命令 python3 -m http.server 8080
即可快速启用一个webserver,这时在这台机器用 curl
发送一个请求。
curl -v http://web.test.com:8080/test/index.html
然后看下python3 的日志
sxf@sxf-virtual-machine:/data/8080$ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
192.168.3.80 - - [16/Jan/2023 15:08:18] "GET /test/index.html HTTP/1.1" 200 -
可以看到客户端的IP正是 ens33
网卡,即当前流量是经过默认网卡流出。
下面我们再看一下如何用 Golang 来指定网卡,为了方便这里直接贴出来完整的代码
package main
import (
"bufio"
"context"
"fmt"
"io"
"net/http"
"net/url"
"syscall"
"golang.org/x/sys/unix"
"net"
)
var interfaceName string = "ens160" // ens33、ens160
const (
network string = "tcp"
address string = "192.168.3.58:8080"
)
func main() {
// 重要核心代码
d := &net.Dialer{
Control: func(network, address string, c syscall.RawConn) error {
return setSocketOptions(network, address, c, interfaceName)
},
}
ctx := context.Background()
// 拨号获取一个连接
conn, err := d.DialContext(ctx, network, address)
if err != nil {
panic(err)
}
// 1. create new request
reqURL, _ := url.Parse("http://web.test.com/test/index.html")
hdr := http.Header{}
req := &http.Request{
Method: "GET",
URL: reqURL,
Header: hdr,
}
err = req.Write(conn)
if err != nil {
panic(err)
}
// 2. Get a response
r := bufio.NewReader(conn)
resp, err := http.ReadResponse(r, req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
status := resp.StatusCode
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(conn.LocalAddr())
for name, vv := range resp.Header {
fmt.Print(name, ":")
for _, v := range vv {
fmt.Print(v)
}
fmt.Println()
}
fmt.Println()
fmt.Println(status)
fmt.Println(string(body))
}
func isTCPSocket(network string) bool {
switch network {
case "tcp", "tcp4", "tcp6":
return true
default:
return false
}
}
func isUDPSocket(network string) bool {
switch network {
case "udp", "udp4", "udp6":
return true
default:
return false
}
}
func setSocketOptions(network, address string, c syscall.RawConn, interfaceName string) (err error) {
if interfaceName == "" || !isTCPSocket(network) && !isUDPSocket(network) {
return
}
var innerErr error
err = c.Control(func(fd uintptr) {
host, _, _ := net.SplitHostPort(address)
if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() {
return
}
if interfaceName != "" {
if innerErr = unix.BindToDevice(int(fd), interfaceName); innerErr != nil {
return
}
}
})
if innerErr != nil {
err = innerErr
}
return
}
我们首先自定义了 Dialer.Control
的实现
d := &net.Dialer{
Control: func(network, address string, c syscall.RawConn) error {
return setSocketOptions(network, address, c, interfaceName)
},
}
这里指定了自定义实现函数setSocketOptions
, 下面是实现
func setSocketOptions(network, address string, c syscall.RawConn, interfaceName string) (err error) {
if interfaceName == "" || !isTCPSocket(network) && !isUDPSocket(network) {
return
}
var innerErr error
err = c.Control(func(fd uintptr) {
host, _, _ := net.SplitHostPort(address)
if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() {
return
}
// 核心代理
if interfaceName != "" {
if innerErr = unix.BindToDevice(int(fd), interfaceName); innerErr != nil {
return
}
}
})
if innerErr != nil {
err = innerErr
}
return
}
这里重点是使用了 unix.BindToDevice()
函数来指定了要使用的物理网卡,其参数类型是 int
整型,其实现为官方库 https://pkg.go.dev/golang.org/x/sys/unix#BindToDevice
// BindToDevice binds the socket associated with fd to device.
func BindToDevice(fd int, device string) (err error) {
return SetsockoptString(fd, SOL_SOCKET, SO_BINDTODEVICE, device)
}
func SetsockoptString(fd, level, opt int, s string) (err error) {
var p unsafe.Pointer
if len(s) > 0 {
p = unsafe.Pointer(&[]byte(s)[0])
}
return setsockopt(fd, level, opt, p, uintptr(len(s)))
}
func setsockopt(s int, level int, name int, val unsafe.Pointer, vallen uintptr) (err error) {
_, _, e1 := Syscall6(SYS_SETSOCKOPT, uintptr(s), uintptr(level), uintptr(name), uintptr(val), uintptr(vallen), 0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
通过函数一级级的调用,最终调用了 Syscall6
函数,到这里基本可以不用再往下跟踪了,对于我们来讲,个人觉得只要跟踪到 setsocketopt
函数就可以了,对于写 c 的同学来说,对于这个函数还是非常熟悉的。
此时我们自己的环境修改一些配置并运行上面的程序,在 webserver 的日志里可以看到客户端的ip就是上面程序指定网卡的ip地址。
在一些网络底层基本都会用到 Dialer.Control
这个字段配置,在C中这么多通过 setsocketopt
实现的方法都是在这里实现的,这里推荐大家参考一下耗子叔以前写的一篇博客 从一次经历谈 TIME_WAIT 的那些事 ,采用的也是类似的方法。
上一节我们大概介绍了一下Envoy中有关速率限制(限流)的一些内容,这一节我们看一下对于外部的 gRPC限流服务它又是如何工作和配置的。
在 Envoy 中对服务限流的配置除了可以在 Envoy 本身中实现外,还可以在通过外部服务实现,此时 Envoy 将通过 gRPC 协议调用外部限流服务,官方对此实现有一套现成的解决方案,主要是redis数据库+令牌桶算法实现,可参考官方 https://github.com/envoyproxy/ratelimit
本文中的限制器或限流器均是同一个意思。
此实现是基于令牌桶算法实现,本身比较的简单,比较适合一般的使用场景。
这里是官方提供的一个配置示例
13 http_filters:
14 - name: envoy.filters.http.local_ratelimit
15 typed_config:
16 "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
17 stat_prefix: http_local_rate_limiter
18 token_bucket:
19 max_tokens: 10000
20 tokens_per_fill: 1000
21 fill_interval: 1s
22 filter_enabled:
23 runtime_key: local_rate_limit_enabled
24 default_value:
25 numerator: 100
26 denominator: HUNDRED
27 filter_enforced:
28 runtime_key: local_rate_limit_enforced
29 default_value:
30 numerator: 100
31 denominator: HUNDRED
32 response_headers_to_add:
33 - append_action: OVERWRITE_IF_EXISTS_OR_ADD
34 header:
35 key: x-local-rate-limit
36 value: 'true'
37 local_rate_limit_per_downstream_connection: false
重点关注配置项 token_bucket
,这里的配置表示当前最多有 10000 个令牌可以被使用,其中令牌在使用的过程中,只要桶中不足10000 个令牌时,则会以每秒再产生 1000 个令牌的速度产生新的令牌并放入令牌桶中,这样就可以实现后期每秒 1000个请求的需求。
这种配置方法比较简单,也不需要依赖第三方组件,大部分场景下已经足够我们使用了。
对于这种专业的限流服务,需要依赖于一些第三方组件,官方的方案主要是基于Redis数据库来实现的,当然也可以换成其它的数据库。
对于Envoy是如何与限流服务交互的其实也很好理解
429
码,表示请求过多可以看到交互还是很简单的,其实我们最主要关注是 Envoy 与 gRPC 之间是如何协同工作的。
应用程序请求是基于域(domain)和一组描述符(descriptors)的速率限制决定的,因此在 Envoy
和 限流服务
的配置都是根据这两个概念来实现的。
Domain
:域是一组速率限制的容器。 Ratelimit 服务已知的所有域必须是全局唯一的。它们作为不同团队/项目具有不冲突的速率限制配置的一种方式。
Descriptor
:描述符是域拥有的键/值对列表,Ratelimit 服务使用它来选择在限制时使用的正确速率限制。描述符区分大小写。
每个配置都包含一个顶级描述符列表和其下可能的多个嵌套列表。格式为:
domain: <unique domain ID>
descriptors:
- key: <rule key: required>
value: <rule value: optional>
rate_limit: (optional block)
name: (optional)
replaces: (optional)
- name: (optional)
unit: <see below: required>
requests_per_unit: <see below: required>
shadow_mode: (optional)
descriptors: (optional block)
- ... (nested repetition of above)
描述符列表中的每个描述符都必须有一个key。它还可以选择具有一个值以启用更具体的匹配。 “rate_limit”块是可选的,如果存在则设置实际的速率限制规则。请参阅下文了解规则的定义方式。如果不存在速率限制并且没有嵌套描述符,则描述符实际上被列入白名单。否则,嵌套描述符允许更复杂的匹配和速率限制场景。
rate_limit:
unit: <second, minute, hour, day>
requests_per_unit: <uint>
速率限流块指定匹配时将使用的实际速率限流。目前该服务支持每秒、分钟、小时和天的限制。未来可能会根据用户需求增加更多类型的限制。对于其它字段的定义请参考 https://github.com/envoyproxy/ratelimit
上面我们介绍了一些与限流服务相关的概念,我们再看一下如何配置限流服务。要启用gRPC限流服务需要在Envoy端和gRPC服务端两个地方进行一些相关配置,且它们之间的配置要合理才可以,先看一下Envoy端配置
Envoy 配置在envoy.yaml
static_resources:
clusters:
- name: ratelimit
type: STRICT_DNS
connect_timeout: 1s
lb_policy: ROUND_ROBIN
protocol_selection: USE_CONFIGURED_PROTOCOL
http2_protocol_options: {}
load_assignment:
cluster_name: ratelimit
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8081
- name: webserver
connect_timeout: 1s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: webserver
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 192.168.3.206
port_value: 80
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 8888
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: AUTO
stat_prefix: ingress
http_filters:
- name: envoy.filters.http.ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
domain: mydomain
request_type: external
stage: 0
rate_limited_as_resource_exhausted: true
failure_mode_deny: false
enable_x_ratelimit_headers: DRAFT_VERSION_03
rate_limit_service:
grpc_service:
envoy_grpc:
cluster_name: ratelimit
transport_api_version: V3
这里我们首先通过static_resources.cluster
声明了一个grpc限流服务集群 ratelimit
, 监听地址为 127.0.0.1:8080
。
接着就是对限流的配置,这里使用了全局限流器,并使用了 HTTP
的 HTTP Filter
过滤器扩展envoy.extensions.filters.http.ratelimit.v3.RateLimit
,并指定了 domain:mydomain
域,因此在限流服务端这个域必须得存在;request_type: external
表示启用外部限流服务;rate_limit_service
指定了限流服务集群为 ratelimit
。到此为止我们也只是声明了一些限流服务相关的信息,那到底具体怎么使用呢?
接着我们通过为每个 Route
指定将在该标头中设置的任何值以及请求的路径传递给速率限制器服务,这里指定了根路由 /
,也就是说整个域名都是有效的。
route_config:
name: route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: webserver
rate_limits:
- stage: 0
actions:
- {request_headers: {header_name: "x-ext-auth-ratelimit", descriptor_key: "ratelimitkey"}}
- {request_headers: {header_name: ":path", descriptor_key: "path"}}
这里使用了多个 request_headers
项,此时将表达 joint key
限流服务,除了request_headers
外还有其它几个字段,它必须是下面的其中一项:
有一点需要特别注意,当为多个路由指定了不同的限流配置时,其先后顺序是有一定的影响的,对于Envoy来讲,是从上到下进行服务请求,因此都是将根路由/
放在配置的最下方,如
route_config:
name: route
virtual_hosts:
- name: backend
domains: ["*"]
rate_limits:
actions:
- generic_key:
descriptor_value: "bar"
descriptor_key: "bar"
routes:
- match:
prefix: /header/
route:
cluster: webserver
rate_limits:
- actions: # 支持多项配置
- generic_key:
descriptor_value: "foo"
descriptor_key: "foo"
# 请求头
- match:
prefix: /post
route:
cluster: httpbin
rate_limits:
stage: 0
actions:
- header_value_match:
descriptor_key: "request"
descriptor_value: "post_method"
headers:
name: ":method"
string_match:
exact: "GET"
- match:
prefix: /anything/
route:
cluster: httpbin
rate_limits:
actions:
- request_headers:
descriptor_key: "ratelimitkey"
header_name: "x-ext-ratelimit"
- request_headers:
descriptor_key: "ratelimitkey-2"
header_name: "x-ext-value"
# 域名全局限制
- match:
prefix: /
route:
cluster: webserver
上面是Envoy端的配置,下面我们再看看gRPC限制服务端的配置
domain: mydomain
descriptors:
- key: ratelimitkey
descriptors:
- key: path
rate_limit:
requests_per_unit: 2
unit: second
- key: database
value: default
rate_limit:
unit: second
requests_per_unit: 500
指定域为 mydomain
与Envoy端的一致,而 descriptiors
则表示描述符,并且描述符是支持嵌套的。
此配置表示采用 ratelimitkey
和 path
附带的值,并将它们构建为用于速率限制的联合密钥。
我们这里只指定了两个配置,但本文章中我们只用到了第一个配置项,看到配置还是挺简单的。
然后我们参考官方的方案,先设置一些环境变量,再启用服务
git clone https://github.com/envoyproxy/ratelimit.git
cd ratelimit
make compile
export USE_STATSD=false LOG_LEVEL=debug REDIS_SOCKET_TYPE=tcp REDIS_URL=192.168.3.58:6379 RUNTIME_ROOT=/home/sxf/workspace/ratelimit RUNTIME_SUBDIRECTORY=ratelimit
环境变量 RUNTIME_ROOT
表示 RUNTIME 根目录,而 RUNTIME_SUBDIRCTORY
表示配置文件所在的子目录,服务启用从 RUNTIME_ROOT/RUNTIME_SUBDIRECTORY/config/
目录里查找所有 *.conf 配置文件,参考 https://github.com/envoyproxy/ratelimit#loading-configuration
这里同时指定了Redis 一些配置相关信息,并启用了Debug模式,禁用了统计功能。
# 将上面的配置内容写入 /home/sxf/workspace/ratelimit/ratelimit/config/config.yaml,然后启用服务
bin/ratelimit
如果一切正常的话,服务将输出
WARN[0000] statsd is not in use
INFO[0000] Tracing disabled
WARN[0000] connecting to redis on 192.168.3.58:6379 with pool size 10
DEBU[0000] Implicit pipelining enabled: false
DEBU[0000] loading domain: mydomain
DEBU[0000] Creating stats for key: 'mydomain.foo_foo'
DEBU[0000] loading descriptor: key=mydomain.foo_foo ratelimit={requests_per_unit=2, unit=MINUTE, unlimited=false, shadow_mode=false}
DEBU[0000] Creating stats for key: 'mydomain.bar_bar'
DEBU[0000] loading descriptor: key=mydomain.bar_bar ratelimit={requests_per_unit=1, unit=MINUTE, unlimited=false, shadow_mode=false}
DEBU[0000] Creating stats for key: 'mydomain.request_post_method'
DEBU[0000] loading descriptor: key=mydomain.request_post_method ratelimit={requests_per_unit=3, unit=MINUTE, unlimited=false, shadow_mode=false}
DEBU[0000] loading descriptor: key=mydomain.ratelimitkey_foo
DEBU[0000] Creating stats for key: 'mydomain.ratelimitkey_foo.ratelimitkey-2'
DEBU[0000] loading descriptor: key=mydomain.ratelimitkey_foo.ratelimitkey-2 ratelimit={requests_per_unit=3, unit=MINUTE, unlimited=false, shadow_mode=false}
DEBU[0000] waiting for runtime update
WARN[0000] Listening for gRPC on '0.0.0.0:8081'
WARN[0000] Listening for debug on '0.0.0.0:6070'
WARN[0000] Listening for HTTP on '0.0.0.0:8080'
最终启用了三个端口
:8081
gRPC服务端口,与Envoy通讯使用
:6070
golang中 pprof 性能分析,https://github.com/envoyproxy/ratelimit#debug-port
:8080
查看交互端点和服务健康检查,https://github.com/envoyproxy/ratelimit#http-port
这里只是简单介绍了其用法,更多配置信息可查看官方网站
到此,两边的配置都基本完成了,我们可以将Envoy服务启用,并用压力测试工具访问url,会发现限流服务正在发挥作用。
在 Envoy 架构中 Rate limit service 共支持 global rate limiting 和 local rate limit filter 两种速率限制。推荐使用 https://github.com/envoyproxy/ratelimit 库。
Envoy 提供了两种全局限速实现
Envoy 直接与全局 gRPC rate limiting service 集成,配置参考 https://www.envoyproxy.io/docs/envoy/latest/configuration/other_features/rate_limit#config-rate-limit-service。
速率服务可以使用任何 RPC/IDL
协议实现,但Envoy 提供了一个用 Go 编写的参考实现,它使用 Redis 作为后端。速率集成有以下两种特征:
Per connection
,Envoy 将为安装过滤器的侦听器上的每个 新连接
调用速率限制服务。该配置指定一个特定的域名和描述符设置为速率限制。这具有限制每秒传输侦听器的连接速率的最终效果。配置参考 https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/network_filters/rate_limit_filter#config-network-filters-rate-limitPer HTTP request
,Envoy 将为安装过滤器的 Listener 以及路由表指定应调用全局速率限制服务的侦听器上的每个 新请求
调用速率限制服务。对目标上游集群的所有请求以及从原始集群到目标集群的所有请求都可以进行速率限制。配置参考 https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/rate_limit_filter#config-http-filters-rate-limitEnvoy 还支持本地速率限制 local rate limiting
,本地速率限制可以与全局速率限制结合使用,以减少全局速率限制服务的负载。例如,本地令牌桶速率限制可以吸收非常大的负载突发,否则可能会压倒全局速率限制服务。因此,速率限制分两个阶段应用,在细粒度全局限制完成作业之前,由令牌桶限制执行初始粗粒度限制。
速率限制服务的开源参考实现目前不可用。费率限制配额扩展目前可以与Google Cloud费率限制服务一起使用。
基于配额的全局速率限制只能应用于 HTTP 请求。 Envoy 将使用 HTTP 过滤器配置对请求进行分桶,并从速率限制配额服务请求配额分配。配置参考 https://www.envoyproxy.io/docs/envoy/latest/configuration/other_features/rate_limit#config-rate-limit-quota-service
Envoy 除了支持通过 local rate limit filter 过滤器对 L4 连接进行本地(非分布式)速率限制。同时还支持通过 HTTP local rate limit filter 对 HTTP 请求进行本地速率限制。这可以在 侦听器
级别或更具体的级别(例如: virtual host
或 route level
)全局激活。
也就是说对于 local rate limit
可以在两个地方对其进行配置,一个是 Listener 中的 Network filter ,另一个是 HTTP 中的 HTTP Filters,注意配置在这两点的不同之处。
一般本地速率限制与全局速率限制结合使用,以减少全局速率限制服务的负载。
当请求的路由或虚拟主机具有每个过滤器本地速率限制配置时,HTTP 本地速率限制过滤器应用令牌桶速率限制。
如果检查了本地速率限制令牌桶,并且没有可用令牌,则返回 429
响应(响应是可配置的)。本地速率限制过滤器然后设置 x-envoy-ratelimited 响应标头。可以配置要返回的其他响应标头。
根据配置项 local_rate_limit_per_downstream_connection 的值,令牌桶在所有 workers 之间共享或在 a per connection 的基础上共享。这导致每个 Envoy 进程或每个下游连接应用本地速率限制。默认情况下,速率限制适用于每个 Envoy 进程。
以下配置来自官方提供的示例文件 https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/local_rate_limit_filter#example-configuration
13 http_filters:
14 - name: envoy.filters.http.local_ratelimit
15 typed_config:
16 "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
17 stat_prefix: http_local_rate_limiter
18 token_bucket:
19 max_tokens: 10000
20 tokens_per_fill: 1000
21 fill_interval: 1s
22 filter_enabled:
23 runtime_key: local_rate_limit_enabled
24 default_value:
25 numerator: 100
26 denominator: HUNDRED
27 filter_enforced:
28 runtime_key: local_rate_limit_enforced
29 default_value:
30 numerator: 100
31 denominator: HUNDRED
32 response_headers_to_add:
33 - append_action: OVERWRITE_IF_EXISTS_OR_ADD
34 header:
35 key: x-local-rate-limit
36 value: 'true'
37 local_rate_limit_per_downstream_connection: false
这里 rate limit
工作在HTTP 中的 HTTP Filters
过滤器,使用扩展 extensions.filters.http.local_ratelimit.v3.LocalRateLimit 。
token_bucket
: 是令牌桶的配置(参考,这里的意思是说令牌桶共 10000 个,每 token_bucket.fill_interval
个周期定时向桶中填充 token_bucket.tokens_per_fill
个令牌。
filter_enable
启用状态。
13 http_filters:
14 - name: envoy.filters.http.local_ratelimit
15 typed_config:
16 "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
17 stat_prefix: http_local_rate_limiter
21 route_config:
22 name: local_route
23 virtual_hosts:
24 - name: local_service
25 domains: ["*"]
26 routes:
27 - match: {prefix: "/path/with/rate/limit"}
28 route: {cluster: service_protected_by_rate_limit}
29 typed_per_filter_config:
30 envoy.filters.http.local_ratelimit:
31 "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
32 stat_prefix: http_local_rate_limiter
33 token_bucket:
34 max_tokens: 10000
35 tokens_per_fill: 1000
36 fill_interval: 1s
37 filter_enabled:
38 runtime_key: local_rate_limit_enabled
39 default_value:
40 numerator: 100
41 denominator: HUNDRED
42 filter_enforced:
43 runtime_key: local_rate_limit_enforced
44 default_value:
45 numerator: 100
46 denominator: HUNDRED
47 response_headers_to_add:
48 - append_action: OVERWRITE_IF_EXISTS_OR_ADD
49 header:
50 key: x-local-rate-limit
51 value: 'true'
52 - match: {prefix: "/"}
53 route: {cluster: default_service}
这里一共配置了两个路由,对其中的一个路由 “/path/with/rate/limit” 进行了限制,第二个路由不做任何限制。
请注意,如果此过滤器配置为全局禁用并且没有虚拟主机或路由级别令牌桶,则不会应用任何速率限制。
global rate limit
和 local rate limit
两者即可以工作在 Listeners 中的 Network filters,也可以工作在 HTTP 中的 HTTP filters 中。local rate limit
和 global rate limit
两者配合使用,优先使用 local rate limit
服务,以减少对 global rate limit
服务的负载。local rate limit
主要是基于令牌桶限制算法,见 https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto#envoy-v3-api-field-extensions-filters-http-local-ratelimit-v3-localratelimit-local-rate-limit-per-downstream-connection前段时间使用 iptables
的 DNAT
实现一个业务需求的时候,遇到了一些问题这里将其整个过程记录下来。
这里假设开发机地址为 192.168.3.80
,要实现的需求是当用户在开发机访问一个IP地址 192.168.3.196
时,将请求转发到另一台机器 192.168.3.58
,很明显直接使用 DNAT
来实现即可。
iptables 命令如下
sudo iptables -t nat -F
sudo iptables -t nat -A PREROUTING -d 192.168.3.196 -p tcp --dport 8080 -j DNAT --to-destination 192.168.3.58:8080
sudo iptables -t nat -A POSTROUTING -d 192.168.3.58 -p tcp --dport 8080 -j SNAT --to-source 192.168.3.196:8080
这时在开发机器访问
curl http://192.168.3.196:8080
发现提示错误
curl: (7) Failed to connect to 192.168.3.196 port 8080: Connection refused
奇怪了,竟然不能访问,确认路由规则是写入成功的。网上查找了一些资料好像全是这种写法,只不过用法有怕差异,这时直觉告诉我应该对 DNAT 理解不到位,遗漏了一些重要的知识点。
上面这种写法一般都是将开发机当作一个中转服务器
或跳板
来使用,多种情况下都有一个公网ip,与我们的真正需求有一些不一样。
现在我们再以将其视为中转服务器的角色测试一次,当然这个规则不能直接使用上面的这个,需要把访问的目标ip更换成开发机器的IP地址。
sudo iptables -t nat -F
sudo iptables -t nat -A PREROUTING -d 192.168.3.80 -p tcp --dport 8080 -j DNAT --to-destination 192.168.3.58:8080
sudo iptables -t nat -A POSTROUTING -d 192.168.3.58 -p tcp --dport 8080 -j SNAT --to-source 192.168.3.80:8080
第一条是数据包出去规则,需要做DNAT,第二条是数据包回来规则,必须做一次 SNAT,否则数据包将去无回,无法响应。
这时再找一台机器访问 curl 192.168.3.80:8080
,可以看到响应结果符合预期。
现在我们基本确认了是我们的用法不对,到底是哪里出错了呢?这里我们一起看一下这张 iptables
数据流向图。
从图中可以看到,对于数据流入一共有两类,一类是外部数据包流入 ,即左侧的 Incoming Packet
;另一类是本机生成的数据包流入,即右侧的 Locally generated Packet
,对于对数据包的流出只有一处,即下方的 Outgoing Packet
。
对于数据包首个经过的表是不一样的,对于外部流入的数据包首个经过的是PREROUTING
表,而对于本地生成的数据包而言经过的是 OUTPUT
这个表,最后统一从同一个地方流出。
也就是说针对不的类型的包,经过的表是不同的,这个正是我们最上面失败的原因。
我们要实现的场景其实是 Locally generated Packet
这类,所以使用的表应该是 OUTPUT
才是正确的,现在我们清除原来的规则,重新写入新规则测试一下
sudo iptables -t nat -F
sudo iptables -t nat -A OUTPUT -d 192.168.3.196 -p tcp --dport 8080 -j DNAT --to-destination 192.168.3.58:8080
注意对于 SNAT
而言,只对 INPUT/POSTROUTING
有效。
再次测试 curl 192.168.3.196:8080
响应正常。
针对 iptables 的 DNAT
的实现,需要根据数据包的来源不同而采用不同的处理方法,一共分外部数据包和本地数据包两类。其中对于外部数据包除了做 DNAT外,还要再做一个 SNAT 规则,否则数据包将有去无回;而对于本地数据包而言,只需要在 OUTPUT 表中做一个 DNAT 即可,并不需要SNAT,同时也不支持 SNAT。对于SNAT 只对 INPUT/POSTROUTEING
才有效。
环境:
客户端(windows) 192.168.6.21
服务器(Ubuntu): 192.168.6.23
$ modprobe nf_log_ipv4
$ sysctl net.netfilter.nf_log.2
net.netfilter.nf_log.2 = nf_log_ipv4
$ iptables -t raw -A PREROUTING -p icmp -j TRACE
$ iptables -t raw -A OUTPUT -p icmp -j TRACE
客户端执行 ping 命令,
$ ping 192.168.6.23 -n 1
这里使用 -n 参数指定发送的包数量为1,方便我们分析日志
此时在服务器上执行查看日志命令, 日志文件为:/var/log/syslog
或者 /var/log/kern.log
或者 /var/log/messages
$ tail -f /var/log/syslog Jul 20 11:28:40 ubuntu kernel: [ 7606.531051] TRACE: raw:PREROUTING:policy:2 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33555 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=608 Jul 20 11:28:40 ubuntu kernel: [ 7606.531146] TRACE: nat:PREROUTING:rule:1 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33555 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=608 Jul 20 11:28:40 ubuntu kernel: [ 7606.531192] TRACE: nat:DOCKER:return:3 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33555 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=608 Jul 20 11:28:40 ubuntu kernel: [ 7606.531259] TRACE: nat:PREROUTING:policy:2 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33555 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=608 Jul 20 11:28:40 ubuntu kernel: [ 7606.531316] TRACE: filter:INPUT:policy:1 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33555 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=608 Jul 20 11:28:40 ubuntu kernel: [ 7606.531373] TRACE: nat:INPUT:policy:1 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33555 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=608 Jul 20 11:28:40 ubuntu kernel: [ 7606.531424] TRACE: raw:OUTPUT:policy:2 IN= OUT=ens37 SRC=192.168.6.23 DST=192.168.6.21 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=35888 PROTO=ICMP TYPE=0 CODE=0 ID=1 SEQ=608 Jul 20 11:28:40 ubuntu kernel: [ 7606.531488] TRACE: filter:OUTPUT:policy:1 IN= OUT=ens37 SRC=192.168.6.23 DST=192.168.6.21 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=35888 PROTO=ICMP TYPE=0 CODE=0 ID=1 SEQ=608
可以看到除了流量来源 SRC 和目的 DST, 还有一些 ICMP 协议相关的字段,如 TYPE, CODE; 对于 ICMP协议 TYPE 有多类值,其中CODE 根据 TYPE 值的不同而不同。
日志字段
Jul 20 11:28:40 ubuntu kernel: [ 7606.531051] TRACE: raw:PREROUTING:policy:2 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33555 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=608
raw:PREROUTING:policy:2
这里以”:”分隔四类字段值,分别为 raw 表:PREROUTING 键:policy或 rule : 编号
IN
流量流入网卡名称,当流量 为流出时,此字段为空
OUT
流量流出网卡名称,当流量为流入时,此字段为空
MAC
网卡MAC地址
SRC
流量来源IP
DST
流量目的IP
LEN
数据包大小
TOS
服务类型
ID
流唯一标识, 如日志中请求ID为 33555, 响应ID为 35888
PROTO
数据流协议
TYPE
协议ICMP的类型,见下表
CODE
协议ICMP类型对应的code
上面日志中第2-7条记录为ping 请求(TYPE=8 CODE=0),而最后两条记录为对ping命令的响应(TYPE=0 CODE=0),由于ping 请求经过了nat 表(PREROUTING)和 filter 两个表的不同链,所有打印多条记录。
ICMP类型
TYPE | CODE | Description | Query | Error |
---|---|---|---|---|
0 | 0 | Echo Reply——回显应答(Ping应答) | x | |
3 | 0 | Network Unreachable——网络不可达 | x | |
3 | 1 | Host Unreachable——主机不可达 | x | |
3 | 2 | Protocol Unreachable——协议不可达 | x | |
3 | 3 | Port Unreachable——端口不可达 | x | |
3 | 4 | Fragmentation needed but no frag. bit set——需要进行分片但设置不分片比特 | x | |
3 | 5 | Source routing failed——源站选路失败 | x | |
3 | 6 | Destination network unknown——目的网络未知 | x | |
3 | 7 | Destination host unknown——目的主机未知 | x | |
3 | 8 | Source host isolated (obsolete)——源主机被隔离(作废不用) | x | |
3 | 9 | Destination network administratively prohibited——目的网络被强制禁止 | x | |
3 | 10 | Destination host administratively prohibited——目的主机被强制禁止 | x | |
3 | 11 | Network unreachable for TOS——由于服务类型TOS,网络不可达 | x | |
3 | 12 | Host unreachable for TOS——由于服务类型TOS,主机不可达 | x | |
3 | 13 | Communication administratively prohibited by filtering——由于过滤,通信被强制禁止 | x | |
3 | 14 | Host precedence violation——主机越权 | x | |
3 | 15 | Precedence cutoff in effect——优先中止生效 | x | |
4 | 0 | Source quench——源端被关闭(基本流控制) | ||
5 | 0 | Redirect for network——对网络重定向 | ||
5 | 1 | Redirect for host——对主机重定向 | ||
5 | 2 | Redirect for TOS and network——对服务类型和网络重定向 | ||
5 | 3 | Redirect for TOS and host——对服务类型和主机重定向 | ||
8 | 0 | Echo request——回显请求(Ping请求) | x | |
9 | 0 | Router advertisement——路由器通告 | ||
10 | 0 | Route solicitation——路由器请求 | ||
11 | 0 | TTL equals 0 during transit——传输期间生存时间为0 | x | |
11 | 1 | TTL equals 0 during reassembly——在数据报组装期间生存时间为0 | x | |
12 | 0 | IP header bad (catchall error)——坏的IP首部(包括各种差错) | x | |
12 | 1 | Required options missing——缺少必需的选项 | x | |
13 | 0 | Timestamp request (obsolete)——时间戳请求(作废不用) | x | |
14 | Timestamp reply (obsolete)——时间戳应答(作废不用) | x | ||
15 | 0 | Information request (obsolete)——信息请求(作废不用) | x | |
16 | 0 | Information reply (obsolete)——信息应答(作废不用) | x | |
17 | 0 | Address mask request——地址掩码请求 | x | |
18 | 0 | Address mask reply——地址掩码应答 | x |
在日志里同时还有 raw表的 PREROUTING 和 OUTPUT 链的相关记录。
现在我们再添加一条禁止ICMP的规则,这里即可以在filter 表中的 INPUT 链中添加,也可以在 OUTPUT 链中添加。
$ iptables -t filter -A OUTPUT -d 192.168.6.21 -j DROP
这里我们添加在了 OUTPUT 链里,所以这里使用的 -d 参数值为 192.168.6.21
现在我们再看一下日志输出
Jul 20 11:09:58 ubuntu kernel: [ 6484.565458] TRACE: raw:PREROUTING:policy:2 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33554 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=582 Jul 20 11:09:58 ubuntu kernel: [ 6484.565548] TRACE: nat:PREROUTING:rule:1 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33554 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=582 Jul 20 11:09:58 ubuntu kernel: [ 6484.565592] TRACE: nat:DOCKER:return:3 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33554 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=582 Jul 20 11:09:58 ubuntu kernel: [ 6484.565631] TRACE: nat:PREROUTING:policy:2 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33554 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=582 Jul 20 11:09:58 ubuntu kernel: [ 6484.565673] TRACE: filter:INPUT:policy:1 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33554 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=582 Jul 20 11:09:58 ubuntu kernel: [ 6484.565713] TRACE: nat:INPUT:policy:1 IN=ens37 OUT= MAC=00:0c:29:30:06:44:00:68:eb:c6:60:f2:08:00 SRC=192.168.6.21 DST=192.168.6.23 LEN=60 TOS=0x00 PREC=0x00 TTL=128 ID=33554 PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=582 Jul 20 11:09:58 ubuntu kernel: [ 6484.565763] TRACE: raw:OUTPUT:policy:2 IN= OUT=ens37 SRC=192.168.6.23 DST=192.168.6.21 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=14584 PROTO=ICMP TYPE=0 CODE=0 ID=1 SEQ=582 Jul 20 11:09:58 ubuntu kernel: [ 6484.565804] TRACE: filter:OUTPUT:rule:1 IN= OUT=ens37 SRC=192.168.6.23 DST=192.168.6.21 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=14584 PROTO=ICMP TYPE=0 CODE=0 ID=1 SEQ=582
请求ID为 33554,TYPE=8, 而响应ID为 14584,TYPE=0
我们看下最后一条日志,其中filter:OUTPUT:rule:1
表示路径为 filter
表的 OUTPUT
链中的编号为1
的规则,这条应该是我们上面添加的规则 ,我们现在确认一下
iptables -t filter -L OUTPUT -nv --line-number
Chain OUTPUT (policy ACCEPT 65 packets, 7466 bytes)
num pkts bytes target prot opt in out source destination
1 3 180 DROP all -- * * 0.0.0.0/0 192.168.6.21
这里是添加在了OUTPUT链中,所以 dst 就是客户端的ip地址,src 就是服务器的地址,这个与我们在 INPUT 链中的正好相反。
$ modprobe -r nf_log_ipv4 modprobe: FATAL: Module nf_log_syslog is in use.
HTTPS,也称作HTTP over TLS。TLS的前身是SSL,TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3。下图描述了在TCP/IP协议栈中TLS(各子协议)和HTTP的关系。
1、HTTPS协议需要到证书颁发机构(Certificate Authority,简称CA)申请证书,一般免费证书很少,需要交费。
2、HTTP是超文本传输协议,信息是明文传输,HTTPS则是具有安全性的SSL加密传输协议。
3、HTTP和HTTPS使用的是完全不同的连接方式,使用的端口也不一样,前者是80,后者是443。
4、HTTP的连接很简单,是无状态的。
5、HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比HTTP协议安全。
从上面可看出,HTTPS和HTTP协议相比提供了
· 数据完整性:内容传输经过完整性校验
· 数据隐私性:内容经过对称加密,每个连接生成一个唯一的加密密钥
· 身份认证:第三方无法伪造服务端(客户端)身份
其中,数据完整性和隐私性由TLS Record Protocol保证,身份认证由TLS Handshaking Protocols实现。
1、什么是证书呢?
2、证书中包含什么信息
证书信息:过期时间和序列号
所有者信息:姓名等
所有者公钥
3、为什么服务端要发送证书给客户端
互联网有太多的服务需要使用证书来验证身份,以至于客户端(操作系统或浏览器等)无法内置所有证书,需要通过服务端将证书发送给客户端。
4、客户端为什么要验证接收到的证书
中间人攻击
5、客户端如何验证接收到的证书
为了回答这个问题,需要引入数字签名(Digital Signature)。
将一段文本通过哈希(hash)和私钥加密处理后生成数字签名。
假设消息传递在Bob,Susan和Pat三人之间发生。Susan将消息连同数字签名一起发送给Bob,Bob接收到消息后,可以这样验证接收到的消息就是Susan发送的
当然,这个前提是Bob知道Susan的公钥。更重要的是,和消息本身一样,公钥不能在不安全的网络中直接发送给Bob。
此时就引入了证书颁发机构(Certificate Authority,简称CA),CA数量并不多,Bob客户端内置了所有受信任CA的证书。CA对Susan的公钥(和其他信息)数字签名后生成证书。
Susan将证书发送给Bob后,Bob通过CA证书的公钥验证证书签名。
Bob信任CA,CA信任Susan, 使得 Bob信任Susan,信任链(Chain Of Trust)就是这样形成的。
事实上,Bob客户端内置的是CA的根证书(Root Certificate),HTTPS协议中服务器会发送证书链(Certificate Chain)给客户端。
正式开始HTTPS的内容:
从上面可知,HTTPS能够加密信息,以免敏感信息被第三方获取。所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。
HTTPS其实是有两部分组成:HTTP +SSL/ TLS,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。具体是如何进行加密,解密,验证的,且看下图。
这个没什么好说的,就是用户在浏览器里输入一个HTTPS网址,然后连接到server的443端口。
采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面(startssl就是个不错的选择,有1年的免费服务)。这套证书其实就是一对公钥和私钥。如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。
这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。
这部分工作是由客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随机值。然后用证书对该随机值进行加密。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。
这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥(随机值)通过某种算法混合在一起,这样除非知道私钥(随机值),不然无法获取内容,而正好客户端和服务端都知道这个私钥(随机值),所以只要加密算法够彪悍,私钥(随机值)够复杂,数据就够安全。
这部分信息是服务端用私钥(随机值)加密后的信息,可以在客户端被还原
客户端用之前生成的私钥(随机值)解密服务端传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。
1、HTTPS对应的通信时序图:
2、HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL协议不仅仅是一套加密传输的协议,更是一件经过艺术家精心设计的艺术品,TLS/SSL中使用了非对称加密,对称加密以及HASH算法。握手过程的具体描述如下:
1. 浏览器将自己支持的一套加密规则发送给网站。
2.网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
3.浏览器获得网站证书之后浏览器要做以下工作:
a) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。
b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。
c) 使用约定好的HASH算法计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。
4.网站接收浏览器发来的数据之后要做以下的操作:
a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。
b) 使用密码加密一段握手消息,发送给浏览器。
5.浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。
在一些开源其中,有些文档使用git动画来介绍的话效果会好很多,所以这里把在Linux终端下如何生成git动画效果整理出来,供大家参考。
Mac
brew install asciinema
Ubuntu
sudo apt-add-repository ppa:zanchey/asciinema
Debian
sudo apt-get install asciinema
Pip安装
sudo pip3 install asciinema
目前此软件不支持 Windows。更多安装教程参考:https://zhuanlan.zhihu.com/p/28423868
❯ asciinema -h
usage: asciinema [-h] [--version] {rec,play,cat,upload,auth} ...
Record and share your terminal sessions, the right way.
positional arguments:
{rec,play,cat,upload,auth}
rec Record terminal session
play Replay terminal session
cat Print full output of terminal session
upload Upload locally saved terminal session to asciinema.org
auth Manage recordings on asciinema.org account
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
example usage:
Record terminal and upload it to asciinema.org:
asciinema rec
Record terminal to local file:
asciinema rec demo.cast
Record terminal and upload it to asciinema.org, specifying title:
asciinema rec -t "My git tutorial"
Record terminal to local file, limiting idle time to max 2.5 sec:
asciinema rec -i 2.5 demo.cast
Replay terminal recording from local file:
asciinema play demo.cast
Replay terminal recording hosted on asciinema.org:
asciinema play https://asciinema.org/a/difqlgx86ym6emrmd8u62yqu8
Print full output of recorded session:
asciinema cat demo.cast
For help on a specific command run:
asciinema <command> -h
录屏命令
asciinema rec
此时生成的文件将保存到临时目录里,一般为 /tmp/
目录
也可以指定文件名
asciinema rec demo.cast
当看到以下信息表示录屏工作开始,以后的操作将会被记录下来
asciinema: recording asciicast to /tmp/tmpg4auzrud-ascii.cast
asciinema: press <ctrl-d> or type "exit" when you're done
此时你可以进行正常的操作。
当操作完成后,按 ctrol-d
或 exit
退出录屏,看到提示信息
asciinema: recording finished
asciinema: press <enter> to upload to asciinema.org, <ctrl-c> to save locally
asciinema: asciicast saved to /tmp/tmp1tj9jqnx-ascii.cast
如果按enter
键会将结束自动上传到 asciinema.org
网站,按下Ctrl+C
表示进行本地存储, 这里存储位置为 /tmp/tmp1tj9jqnx-ascii.cast
。
这种方法会在用户本地生成一个 .cast
的文件,后面我们对其进行回放。
我们先预览下上面生成的动画效果
asciinema play demo.cast
有时候我们需要查看用户的所有终端历史会话内容,此时可执行命令
asciinema cat demo.cast
如果在屏幕过程中存在一些特殊命令,如 vi
,则会话内容将显示成为乱码,还有可能提示错误。
上传
我们也可以将本地生成的文件上传到公网
asciinema upload demo.cast
提示
asciinema upload demo.cast
View the recording at:
https://asciinema.org/a/KG2utenPw4pXk12TcEprPDaRh
This installation of asciinema recorder hasn't been linked to any asciinema.org
account. All unclaimed recordings (from unknown installations like this one)
are automatically archived 7 days after upload.
If you want to preserve all recordings made on this machine, connect this
installation with asciinema.org account by opening the following link:
https://asciinema.org/connect/4fc6bdf3-ecc4-445a-a045-540aa101dee1
我们可以直接在浏览器里访问上面的URL来访问生成的效果。
有时候我们需要将上面的录屏内容转成gif格式在网络上传播,这时我们还需要利用一些工具将其转为gif动画才可以。这里我们使用一个docker镜像 asciinema/asciicast2gif 来操作
下载 Docker 镜像到本地
docker pull asciinema/asciicast2gif
docker run --rm -v $PWD:/data asciinema/asciicast2gif -s 2 -t solarized-dark demo.cast demo.gif
为了方便,我们用命令别名操作,将以下代码保存到 .bashrc
文件中,最后再执行 source ~/.bashrc
应用配置(如果用的zsh的话,则需要保存到 .zshrc
文件)
alias asciicast2gif='docker run --rm -v $PWD:/data asciinema/asciicast2gif'
asciicast2gif demo.cast demo.gif
这里将录屏生成的json文件demo.json
转成 demo.gif
文件,这时我们可以看下gif的生成效果。
另外在生成动画的时候,也可以指定一些参数,如倍速、缩放比例、高度和宽度,如
asciicast2gif -t solarized-dark -s 2 -S 1 -w 400 -h 500 demo.cast demo.gif
参数
-t
表示颜色方案,必须为 asciinema, tango, solarized-dark, solarized-light, monokai (default: asciinema) 其中的一个,默认方案是 asciinema
-s
表示动画速度,默认为1
-S
图像比例/像素密度(默认值:2)
-w
将端子剪裁到指定的列数(宽度)
-h
将终端剪裁到指定的行数(高度)
有时候在转gif的出现失败的情况,如果指定了一些参数的话,可以试着将参数移除试看看。我在用的时候经常出现在指定宽度和高度参数的时候会转换失败,将这两个参数省略则没有问题,怀疑是需要宽高不合理造成的。
在计算机中TUN与TAP是操作系统内核中的虚拟网络设备。不同于硬件设备这些虚拟的网络设备全部用软件实现,但提供了与硬件设备完全相同的功能。
我们先了解一下物理设备的工作原理
所有主机物理网卡收到的数据包时,会先将其交给内核的 Network Stack 处理,然后通过 Socket API 通知给用户态的用户程序。
Linux 中 Tun/Tap 驱动程序为应用程序提供了两种交互方式:
我们再看下 tun 设备的工作原理
用户态应用往字符设备 /dev/tunX
写数据时,写入的数据都会出现在TUN虚拟设备上,当内核发送一个包给 TUN 虚拟设备时,通过读这个字符设备 /dev/tunX
同样可以拿到包的内容。
用户态应用程序写数据到 tun/tap 设备后进入内核态,内核态通过TCP协议复制到用户态,最后数据再次复制到内核态并通过物理网卡转发出去,期间共经历了三次用户态与内核态的复制操作,相比传统的一次复制操作来说,开销还是比较大的,因此性能会有一定的下降,这正是它的缺点。
TAP 设备与 TUN 设备工作方式完全相同,区别在于:
tun/tap 的最主要应用场景就是vpn。
基实现原理就是用到隧道技术,将无法直接发送的包通先封成允许通过的合法数据包,然后经过隧道的方式传递给对方,对方收到数据包再解包成原始数据包,再继续传递下去,直到接收端收到,然后响应并原路返回。
以下图为例
总结
我们先介绍一下在 Linux 中是如何对 TUN/TAP 虚拟设备进行操作创建管理和删除的,但对于如何充分利用这些设备必须通过编写程序代码来实现,在后面会给出网友整理出来的演示代码。
命令行输入 ip help 查看 ip 命令是否支持 tun/tap 工具,支持的话就会显示 tuntap 选项:
# ip help Usage: ip [ OPTIONS ] OBJECT { COMMAND | help } ip [ -force ] -batch filename where OBJECT := { link | addr | addrlabel | route | rule | neigh | ntable | tunnel | tuntap | maddr | mroute | mrule | monitor | xfrm | netns | l2tp | tcp_metrics | token }
不支持就请升级或下载最新的 iproute2 工具包,也可以使用类似的 tunctl 工具。
先查看一下 ip tuntap 的基本用法
# ip tuntap help Usage: ip tuntap { add | del | show | list | lst | help } [ dev PHYS_DEV ] [ mode { tun | tap } ] [ user USER ] [ group GROUP ] [ one_queue ] [ pi ] [ vnet_hdr ] [ multi_queue ] [ name NAME ] Where: USER := { STRING | NUMBER } GROUP := { STRING | NUMBER }
# ip tuntap add dev tap0 mod tap # 创建 tap # ip tuntap add dev tun0 mod tun # 创建 tun # ifconfig -a
新添加的虚拟网卡默认是 DOWN 状态.
对于 tun 类型的虚拟网卡,它的MAC地址全是 0,这个是正常的
# ip link set tun0 up # ip link set tap0 up
对它的操作与普通网卡的命令是一样的
# ip addr add 10.0.0.1/24 dev tun0
此时 PING 10.0.0.1 是可以通的。
# ping 10.0.0.1 PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data. 64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.031 ms 64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.036 ms 64 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=0.031 ms 64 bytes from 10.0.0.1: icmp_seq=4 ttl=64 time=0.037 ms
# ip tuntap del dev tap0 mod tap # 删除 tap # ip tuntap del dev tun0 mod tun # 删除 tun
代码演示
上面我们手动创建了虚拟网卡,但没有办法测试网卡的使用效果,下面是一段实现虚拟网卡读取的演示代码,代码摘自:https://segmentfault.com/a/1190000009249039
#include <net/if.h> #include <sys/ioctl.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <sys/types.h> #include <linux/if_tun.h> #include<stdlib.h> #include<stdio.h> int tun_alloc(int flags) { struct ifreq ifr; int fd, err; char *clonedev = "/dev/net/tun"; if ((fd = open(clonedev, O_RDWR)) < 0) { return fd; } memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = flags; if ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) { close(fd); return err; } printf("Open tun/tap device: %s for reading...n", ifr.ifr_name); return fd; } int main() { int tun_fd, nread; char buffer[1500]; /* Flags: IFF_TUN - TUN device (no Ethernet headers) * IFF_TAP - TAP device * IFF_NO_PI - Do not provide packet information */ tun_fd = tun_alloc(IFF_TUN | IFF_NO_PI); if (tun_fd < 0) { perror("Allocating interface"); exit(1); } while (1) { nread = read(tun_fd, buffer, sizeof(buffer)); if (nread < 0) { perror("Reading from interface"); close(tun_fd); exit(1); } printf("Read %d bytes from tun/tap devicen", nread); } return 0; }
演示
#--------------------------第一个shell窗口---------------------- #将上面的程序保存成tun.c,然后编译 dev@debian:~$ gcc tun.c -o tun #启动tun程序,程序会创建一个新的tun设备, #程序会阻塞在这里,等着数据包过来 dev@debian:~$ sudo ./tun Open tun/tap device tun1 for reading... Read 84 bytes from tun/tap device Read 84 bytes from tun/tap device Read 84 bytes from tun/tap device Read 84 bytes from tun/tap device #--------------------------第二个shell窗口---------------------- #启动抓包程序,抓经过tun1的包 # tcpdump -i tun1 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on tun1, link-type RAW (Raw IP), capture size 262144 bytes 19:57:13.473101 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 1, length 64 19:57:14.480362 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 2, length 64 19:57:15.488246 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 3, length 64 19:57:16.496241 IP 192.168.3.11 > 192.168.3.12: ICMP echo request, id 24028, seq 4, length 64 #--------------------------第三个shell窗口---------------------- #./tun启动之后,通过ip link命令就会发现系统多了一个tun设备, #在我的测试环境中,多出来的设备名称叫tun1,在你的环境中可能叫tun0 #新的设备没有ip,我们先给tun1配上IP地址 dev@debian:~$ sudo ip addr add 192.168.3.11/24 dev tun1 #默认情况下,tun1没有起来,用下面的命令将tun1启动起来 dev@debian:~$ sudo ip link set tun1 up #尝试ping一下192.168.3.0/24网段的IP, #根据默认路由,该数据包会走tun1设备, #由于我们的程序中收到数据包后,啥都没干,相当于把数据包丢弃了, #所以这里的ping根本收不到返回包, #但在前两个窗口中可以看到这里发出去的四个icmp echo请求包, #说明数据包正确的发送到了应用程序里面,只是应用程序没有处理该包 dev@debian:~$ ping -c 4 192.168.3.12 PING 192.168.3.12 (192.168.3.12) 56(84) bytes of data. --- 192.168.3.12 ping statistics --- 4 packets transmitted, 0 received, 100% packet loss, time 3023ms
Open Policy Agent
简称OPA
是一个开源的通用策略引擎,可在整个堆栈中实现统一的、上下文感知的策略实施。OPA 已经成为了云原生计算基金会 (CNCF) 领域的毕业项目,已经在 Kubernetes / Istio 等多个知名项目里使用 。
OPA的核心思想就是策略即代码。
它使用Rego
语言开发,Rego 的灵感来自 Datalog,它是一种易于理解、已有数十年的历史的查询语言。Rego 扩展了 Datalog 以支持 JSON 等文档模型。对于它的详细介绍请参考官方文档 https://www.openpolicyagent.org/docs/latest/policy-language/#what-is-rego,这里不再介绍,本方主要介绍如何使用Golang 来开发一个opa策略。
OPA 将 策略决策
与 策略执行
分离,当您的软件需要做出策略决策时,它会查询 OPA 并提供结构化数据(例如 JSON)作为输入。 OPA 接受任意结构化数据作为输入。
对于它的输入一般称为 input
, 可以为任意类型,输出也一样可以为任意类型,即可以输出布尔值 true
或 false
,也可以输出一个 JSON
字符串对象。