原文地址:TLS

etcd支持用于客户端到服务器以及对等方(服务器到服务器/集群)通信的自动TLS以及通过客户端证书的身份验证.

要启动并运行,首先要获得一个成员的CA证书和签名密钥对。 建议为集群中的每个成员创建并签名一个新的密钥对。

为了方便起见,cfssl工具提供了一个简单的接口来生成证书,我们在此处提供了使用该工具的示例。 或者,尝试使用本指南生成自签名密钥对

基本设置


etcd通过命令行参数或环境变量采用了几种与证书相关的配置选项:

客户端到服务器的通信:

--cert-file=<path>:用于SSL/TLSetcd的连接的证书。设置此选项后,advertise-client-urls可以使用HTTPS模式。

--key-file=<path>:证书的密钥。 必须未加密。

--client-cert-auth:设置此选项后,etcd将检查所有传入的HTTPS请求以查找由受信任CA签名的客户端证书,不提供有效客户端证书的请求将失败。 如果启用了身份验证,则证书将为“公用名”字段指定的用户名提供凭据。

--trusted-ca-file=<path>:受信任的证书颁发机构。

--auto-tls:使用自动生成的自签名证书进行与客户端的TLS连接。

对等节点(服务器到服务器/集群)间的通信:

对等节点选项的工作方式与客户端到服务器的选项相同:

--peer-cert-file=<path>:用于SSL/TLS对等节点之间的连接的证书。这将用于监听对等方地址以及向其他对等方发送请求。

--peer-key-file=<path>:证书的密钥。 必须未加密。

--peer-client-cert-auth:设置此选项后,etcd将检查所有传入的对等节点请求以查找由受信任CA签名的客户端证书.

--peer-trusted-ca-file=<path>:受信任的证书颁发机构。

--peer-auto-tls:使用自动生成的自签名证书进行与对等节点之间的TLS连接。

如果提供了客户端到服务器或对等节点证书,则还必须设置密钥。 所有这些配置选项也可以通过环境变量ETCD_CA_FILEETCD_PEER_CA_FILE等获得。

--cipher-suites:服务器/客户端与对等方之间受支持的TLS密码套件的逗号分隔列表(空将由Go自动填充)。从v3.2.22+,v3.3.7+v3.4+起可用。

示例1:客户端通过HTTPS与服务器进行加密传输


为此,请准备好CA证书(ca.crt)和签名密钥对(server.crtserver.key)。

让我们配置etcd以逐步提供简单的HTTPS传输安全性:

$ etcd --name infra0 --data-dir infra0 \
--cert-file=/path/to/server.crt --key-file=/path/to/server.key \
--advertise-client-urls=https://127.0.0.1:2379 --listen-client-urls=https://127.0.0.1:2379

这应该可以正常启动,并且可以通过对etcd用HTTPS方式来测试配置:

$ curl --cacert /path/to/ca.crt https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v

该命令应显示握手成功。 由于我们使用具有自己的证书颁发机构的自签名证书,因此必须使用--cacert选项将CA传递给curl。 另一种可能性是将CA证书添加到系统的受信任证书目录(通常在/etc/pki/tls/certs/etc/ssl/certs中)。

OSX10.9+的用户:OSX 10.9+上的curl 7.30.0无法理解在命令行中传递的证书。可以替代的方法是将虚拟ca.crt直接导入到钥匙串中,或添加-k标志来curl以忽略错误。要在没有-k标志的情况下进行测试,请运行打开的./fixtures/ca/ca.crt并按照提示进行操作。测试后请删除此证书!如果有解决方法,请告诉我们。

示例2:使用HTTPS客户端证书的客户端到服务器身份验证


目前,我们已经为etcd客户端提供了验证服务器身份并提供传输安全性的功能。 但是,我们也可以使用客户端证书来防止对etcd的未经授权的访问。

客户端将向服务器提供其证书,服务器将检查证书是否由提供的CA签名并决定是否满足请求。

为此,需要第一个示例中提到的相同文件,以及由同一证书颁发机构签名的客户端密钥对(client.crtclient.key)。

$ etcd --name infra0 --data-dir infra0 \
--client-cert-auth --trusted-ca-file=/path/to/ca.crt --cert-file=/path/to/server.crt --key-file=/path/to/server.key \
--advertise-client-urls https://127.0.0.1:2379 --listen-client-urls https://127.0.0.1:2379

现在,对该服务器尝试与上述相同的请求:

$ curl --cacert /path/to/ca.crt https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v

该请求应该是被服务器拒绝:

...
routines:SSL3_READ_BYTES:sslv3 alert bad certificate
...

为了使其成功,我们需要将CA签名的客户端证书提供给服务器:

$ curl --cacert /path/to/ca.crt --cert /path/to/client.crt --key /path/to/client.key \
-L https://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -v

输出应包括:

...
SSLv3, TLS handshake, CERT verify (15):
...
TLS handshake, Finished (20)

以及服务器的响应:

{
"action": "set",
"node": {
"createdIndex": 12,
"key": "/foo",
"modifiedIndex": 12,
"value": "bar"
}
}

指定密码套件以阻止较弱的TLS密码套件

当使用无效密码套件请求客户端问候时,TLS握手将失败。

例如:

$ etcd \
--cert-file ./server.crt \
--key-file ./server.key \
--trusted-ca-file ./ca.crt \
--cipher-suites TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

然后,客户端请求必须指定服务器中指定的密码套件之一:

# 有效的加密套件
$ curl \
--cacert ./ca.crt \
--cert ./server.crt \
--key ./server.key \
-L [CLIENT-URL]/metrics \
--ciphers ECDHE-RSA-AES128-GCM-SHA256 # 成功请求
etcd_server_version{server_version="3.2.22"} 1
...
# 无效的加密套件
$ curl \
--cacert ./ca.crt \
--cert ./server.crt \
--key ./server.key \
-L [CLIENT-URL]/metrics \
--ciphers ECDHE-RSA-DES-CBC3-SHA # 请求失败
(35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure

示例3:集群中的传输安全性和客户端证书


etcd支持与上述对等节点通信相同的模型,这意味着集群中etcd成员之间的通信。

假设我们有这个ca.crt和两个由此CA签名的成员,它们具有自己的密钥对(member1.crtmember1.keymember2.crtmember2.key),我们按以下方式启动etcd:

DISCOVERY_URL=... # from https://discovery.etcd.io/new

# member1
$ etcd --name infra1 --data-dir infra1 \
--peer-client-cert-auth --peer-trusted-ca-file=/path/to/ca.crt --peer-cert-file=/path/to/member1.crt --peer-key-file=/path/to/member1.key \
--initial-advertise-peer-urls=https://10.0.1.10:2380 --listen-peer-urls=https://10.0.1.10:2380 \
--discovery ${DISCOVERY_URL} # member2
$ etcd --name infra2 --data-dir infra2 \
--peer-client-cert-auth --peer-trusted-ca-file=/path/to/ca.crt --peer-cert-file=/path/to/member2.crt --peer-key-file=/path/to/member2.key \
--initial-advertise-peer-urls=https://10.0.1.11:2380 --listen-peer-urls=https://10.0.1.11:2380 \
--discovery ${DISCOVERY_URL}

etcd成员将形成一个集群,并且集群中成员之间的所有通信都将使用客户端证书进行加密和身份验证。 etcd的输出将显示它连接以使用HTTPS的地址。

示例4:自动自签名传输安全性


对于需要通信加密而不是身份验证的情况,etcd支持使用自动生成的自签名证书来加密其消息。 因为不需要在etcd之外管理证书和密钥,所以这简化了部署。

配置etcd以使用带有--auto-tls--peer-auto-tls标志的自签名证书进行客户端和对等节点连接:

DISCOVERY_URL=... # from https://discovery.etcd.io/new

# member1
$ etcd --name infra1 --data-dir infra1 \
--auto-tls --peer-auto-tls \
--initial-advertise-peer-urls=https://10.0.1.10:2380 --listen-peer-urls=https://10.0.1.10:2380 \
--discovery ${DISCOVERY_URL} # member2
$ etcd --name infra2 --data-dir infra2 \
--auto-tls --peer-auto-tls \
--initial-advertise-peer-urls=https://10.0.1.11:2380 --listen-peer-urls=https://10.0.1.11:2380 \
--discovery ${DISCOVERY_URL}

自签名证书不会验证身份,因此curl将返回错误:

curl: (60) SSL certificate problem: Invalid certificate chain

要禁用证书链检查,请使用-k标志调用curl

$ curl -k https://127.0.0.1:2379/v2/keys/foo -Xput -d value=bar -v

DNS SRV的注意事项


如果连接是安全的,则etcd proxy从其客户端TLS终端,并使用--peer-key-file--peer-cert-file中指定的代理自身的密钥/证书与etcd成员进行通信。

代理通过给定成员的--advertise-client-urls--advertise-peer-urls与etcd成员进行通信。 它将客户端请求转发到etcd成员广播的客户端URL,并通过etcd成员广播的对等URL同步初始集群配置。

为etcd成员启用客户端身份验证后,管理员必须确保代理的--peer-cert-file选项中指定的对等节点证书对该身份验证有效。如果启用了对等节点身份验证,则代理的对等节点证书也必须对对等节点身份验证有效。

TLS 身份验证的注意事项


v3.2.0开始,TLS证书将在每个客户端连接上重新加载。 这在不停止etcd服务器而替换到期证书时很有用; 可以通过用新证书覆盖旧证书来完成。 刷新每个连接的证书应该没有太多的开销,但是将来可以通过缓存层进行改进。 示例测试可以在这里找到。

v3.2.0开始,服务器使用错误的IP SAN拒绝传入的对等证书。 例如,如果对等节点证书在“使用者备用名称”(SAN)字段中包含任何IP地址,则服务器仅在远程IP地址与这些IP地址之一匹配时才对对等节点身份验证。 这是为了防止未经授权的端点加入群集。 例如,对等节点B的CSR(带有cfssl)为:

{
"CN": "etcd peer",
"hosts": [
"*.example.default.svc",
"*.example.default.svc.cluster.local",
"10.138.0.27"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "US",
"L": "CA",
"ST": "San Francisco"
}
]
}

当对等节点B的实际IP地址是10.138.0.2,而不是10.138.0.27。 当对等节点B尝试加入集群时,对等节点A将拒绝B,并显示错误x509:证书对10.138.0.27有效,而不对10.138.0.2有效,因为B的远程IP地址与“使用者备用名称(SAN)”字段中的地址不匹配。

v3.2.0开始,服务器在检查SAN时解析TLS DNSNames。 例如,如果对等节点证书在“使用者备用名称”(SAN)字段中仅包含DNS名称(不包含IP地址),则仅当这些DNS名称上的正向查找(dig b.com)具有与远程IP匹配的IP时,服务器才对对等身份验证 地址。 例如,对等B的CSR(带有cfssl)为:

{
"CN": "etcd peer",
"hosts": [
"b.com"
],

当对等节点B的远程IP地址为10.138.0.2时。 当对等节点B尝试加入集群时,对等节点A查找传入的主机b.com以获取IP地址列表(例如dig b.com)。如果列表不包含IP 10.138.0.2,则出现错误tls: 10.138.0.2 does not match any of DNSNames ["b.com"].

v3.2.2开始,如果IP匹配,服务器将接受连接,而无需检查DNS条目。 例如,如果对等节点证书在“使用者备用名称(SAN)”字段中包含IP地址和DNS名称,并且远程IP地址与这些IP地址之一匹配,则服务器仅接受连接而无需进一步检查DNS名称。 例如,对等节点B的CSR(带有cfssl)为:

{
"CN": "etcd peer",
"hosts": [
"invalid.domain",
"10.138.0.2"
],

当对等节点B的远程IP地址是10.138.0.2并且invalid.domain是无效的主机时。 当对等节点B尝试加入集群时,对等节点A成功地对节点B进行了身份验证,因为“使用者备用名称(SAN)”字段具有有效的匹配IP地址。 有关更多详细信息,请参见问题#8206

v3.2.5开始,服务器支持在通配符DNS SAN上进行反向查找。 例如,如果对等节点证书在“使用者备用名称”(SAN)字段中仅包含DNS名称(不包含IP地址),则服务器首先对远程IP地址进行反向查找,以获取映射到该地址的名称列表(例如nslookup IPADDR)。如果这些名称的名称与对等节点证书的DNS名称(通过完全匹配或通配符匹配)匹配,则接受连接。 如果没有匹配项,则服务器将对等节点证书中的每个DNS条目进行正向查找(例如,如果条目为*.example.default.svc,则查找example.default.svc),并且仅在主机的解析地址具有匹配的IP时接受连接 地址和对等节点的远程IP地址。 例如,对等B的CSR(带有cfssl)为:

{
"CN": "etcd peer",
"hosts": [
"*.example.default.svc",
"*.example.default.svc.cluster.local"
],

当对等节点B的远程IP地址为10.138.0.2时。 当对等节点B尝试加入集群时,对等节点A反向查找IP 10.138.0.2以获取主机名列表。 并且,“主题备用名称”(SAN)字段中的主机名必须与对等节点B的证书DNS名称完全匹配或与通配符匹配。 如果反向/正向查找均无效,则返回错误"tls: "10.138.0.2" does not match any of DNSNames ["*.example.default.svc","*.example.default.svc.cluster.local"]。有关更多详细信息,请参见问题#8268

v3.3.0添加了etcd --peer-cert-allowed-cn参数,以支持基于CN(通用名称)的对等节点连接的身份验证。 Kubernetes TLS引导涉及为etcd成员和其他系统组件(例如API服务器,kubelet等)生成动态证书。 为每个组件维护不同的CA可提供对etcd集群的更严格的访问控制,但通常很乏味。 指定--peer-cert-allowed-cn标志时,即使具有共享的CA,节点也只能以匹配的通用名称加入。 例如,三节点群集中的每个成员都设置有CSR(使用cfssl),如下所示:

{
"CN": "etcd.local",
"hosts": [
"m1.etcd.local",
"127.0.0.1",
"localhost"
],
{
"CN": "etcd.local",
"hosts": [
"m2.etcd.local",
"127.0.0.1",
"localhost"
],
{
"CN": "etcd.local",
"hosts": [
"m3.etcd.local",
"127.0.0.1",
"localhost"
],

如果给定--peer-cert-allowed-cn etcd.local,则只有具有相同通用名称的对等方将被认证。 CSR中具有不同CN或--peer-cert-allowed-cn的节点将被拒绝:

$ etcd --peer-cert-allowed-cn m1.etcd.local

I | embed: rejected connection from "127.0.0.1:48044" (error "CommonName authentication failed", ServerName "m1.etcd.local")
I | embed: rejected connection from "127.0.0.1:55702" (error "remote error: tls: bad certificate", ServerName "m3.etcd.local")

每个进程都应以以下内容开始:

etcd --peer-cert-allowed-cn etcd.local

I | pkg/netutil: resolving m3.etcd.local:32380 to 127.0.0.1:32380
I | pkg/netutil: resolving m2.etcd.local:22380 to 127.0.0.1:22380
I | pkg/netutil: resolving m1.etcd.local:2380 to 127.0.0.1:2380
I | etcdserver: published {Name:m3 ClientURLs:[https://m3.etcd.local:32379]} to cluster 9db03f09b20de32b
I | embed: ready to serve client requests
I | etcdserver: published {Name:m1 ClientURLs:[https://m1.etcd.local:2379]} to cluster 9db03f09b20de32b
I | embed: ready to serve client requests
I | etcdserver: published {Name:m2 ClientURLs:[https://m2.etcd.local:22379]} to cluster 9db03f09b20de32b
I | embed: ready to serve client requests
I | embed: serving client requests on 127.0.0.1:32379
I | embed: serving client requests on 127.0.0.1:22379
I | embed: serving client requests on 127.0.0.1:2379

v3.2.19v3.3.4修复了当证书SAN字段仅包含IP地址但不包含域名时TLS重新加载的问题。 例如,如下设置了具有CSR(具有cfssl)的成员:

{
"CN": "etcd.local",
"hosts": [
"127.0.0.1"
],

在Go中,仅当服务器的(* tls.Config).Certificates字段不为空或(* tls.ClientHelloInfo).ServerName不为空且具有有效SNI时,服务器才会调用(* tls.Config).GetCertificate来重新加载TLS 来自客户。 以前,etcd始终填充(* tls.Config)。在初始客户端TLS握手上的证书为非空。 因此,总是希望客户端提供匹配的SNI,以便通过TLS验证并触发(* tls.Config).GetCertificate以重新加载TLS数据。

但是,其SAN字段仅包括IP地址不包含任何域名的证书将请求* tls.ClientHelloInfo带有空的ServerName字段,从而无法在初始TLS握手时触发TLS重新加载;当需要在线更换过期证书时,这将成为一个问题。

现在,(* tls.Config).Certificates在初始TLS客户端握手时创建为空,首先触发(* tls.Config).GetCertificate,然后在每个新的TLS连接上填充其余证书,即使客户端SNI为 为空(例如,证书仅包括IP)。

主机白名单的注意事项


etcd --host-whitelist参数指定HTTP客户端请求中可接受的主机名。 客户端源策略可以防止对不安全的etcd服务器的“DNS重新绑定”攻击。 也就是说,任何网站都可以简单地创建一个授权的DNS名称,并将DNS定向到“localhost”(或任何其他地址)。 然后,侦听“localhost”上的etcd服务器的所有HTTP端点都可以访问,因此容易受到DNS重新绑定攻击。 有关更多详细信息,请参见CVE-2018-5702

客户原始策略的工作方式如下:

  1. 如果客户端通过HTTPS连接是安全的,则允许使用任何主机名。
  2. 如果客户端连接不安全且“ HostWhitelist”不为空,则仅允许其Host字段列在白名单中的HTTP请求。

请注意,无论是否启用身份验证,都会实施客户端来源策略,以进行更严格的控制。

默认情况下,etcd --host-whitelistembed.Config.HostWhitelist设置为空以允许所有主机名。请注意,在指定主机名时,不会自动添加回送地址。 要允许环回接口,请手动将其添加到白名单(例如“ localhost”,“127.0.0.1”等)。

常见问题


使用TLS客户端身份验证时,我看到SSLv3警报握手失败?

golangcrypto/tls软件包在使用之前检查证书公钥的密钥用法。要使用证书公共密钥进行客户端身份验证,我们需要在创建证书公共密钥时将clientAuth添加到“Extended Key Usage”中。

这是操作方法:

将以下部分添加到openssl.cnf中:

[ ssl_client ]
...
extendedKeyUsage = clientAuth
...

创建证书时,请确保在-extensions参数中引用它:

$ openssl ca -config openssl.cnf -policy policy_anything -extensions ssl_client -out certs/machine.crt -infiles machine.csr

通过对等证书身份验证,我收到“证书对127.0.0.1有效,而不对$我的Ip有效”

确保使用主题名称(成员的公共IP地址)对证书进行签名。 例如,etcd-ca工具为其new-cert命令提供了--ip=选项。

需要在其使用者名称中为成员的FQDN签署证书,使用使用者备用名称(简称IP SAN)添加IP地址。 etcd-ca工具为其new-cert命令提供了--domain=选项,openssl也可以做到一点。

ETCD:TLS的更多相关文章

  1. ETCD TLS 配置的坑

    一.环境准备 环境总共 3 台虚拟机,系统为centos7,1个 master,2 个 etcd 节点,master 同时也作为 node 负载 pod,在分发证书等阶段将在另外一台主机上执行,该主机 ...

  2. 二进制搭建kubernetes多master集群【一、使用TLS证书搭建etcd集群】

    上一篇我们介绍了kubernetes集群架构以及系统参数配置,参考:二进制搭建kubernetes多master集群[开篇.集群环境和功能介绍] 下面本文etcd集群才用三台centos7.5搭建完成 ...

  3. etcd集群部署

    etcd是用于共享配置和服务发现的分布式KV存储系统,随着CoreOS和Kubernetes等项目在开源社区日益火热,它们都用到了etcd组件作为一个高可用.强一致性的服务发现存储仓库.操作系统版本: ...

  4. 使用Ansible部署etcd 3.2高可用集群

    之前写过一篇手动搭建etcd 3.1集群的文章<etcd 3.1 高可用集群搭建>,最近要初始化一套新的环境,考虑用ansible自动化部署整套环境, 先从部署etcd 3.2集群开始. ...

  5. 高可用Kubernetes集群-3. etcd高可用集群

    五.部署高可用etcd集群 etcd是key-value存储(同zookeeper),在整个kubernetes集群中处于中心数据库地位,以集群的方式部署,可有效避免单点故障. 这里采用静态配置的方式 ...

  6. CentOS 7 ETCD集群配置大全

    目录 前言 环境准备 安装 静态集群 配置 node01 配置文件 node02 配置文件 node03 配置文件 启动测试 查看集群状态 生成TLS证书 etcd证书创建 安装cfssl工具集 生成 ...

  7. kubeadm部署kubernetes-1.12.0 HA集群-ipvs

    一.概述 主要介绍搭建流程及使用注意事项,如果线上使用的话,请务必做好相关测试及压测. 1.基础环境准备 系统:ubuntu TLS 16.04  5台 docker-ce:17.06.2 kubea ...

  8. kubernetes(k8s)集群安装calico

    添加hosts解析 cat /etc/hosts 10.39.7.51 k8s-master-51 10.39.7.57 k8s-master-57 10.39.7.52 k8s-master-52 ...

  9. Kubeadm 1.9 HA 高可用集群本地离线镜像部署【已验证】

    k8s介绍 k8s 发展速度很快,目前很多大的公司容器集群都基于该项目,如京东,腾讯,滴滴,瓜子二手车,易宝支付,北森等等. kubernetes1.9版本发布2017年12月15日,每三个月一个迭代 ...

随机推荐

  1. 1篇文章搞清楚8种JVM内存溢出(OOM)的原因和解决方法

    前言 撸Java的同学,多多少少会碰到内存溢出(OOM)的场景,但造成OOM的原因却是多种多样. 堆溢出 这种场景最为常见,报错信息: java.lang.OutOfMemoryError: Java ...

  2. 14个Java并发容器,你用过几个?

    作者:acupt 前言 不考虑多线程并发的情况下,容器类一般使用ArrayList.HashMap等线程不安全的类,效率更高.在并发场景下,常会用到ConcurrentHashMap.ArrayBlo ...

  3. Linux 怎么清理缓存

    linux清理缓存的命令   查看缓存的命令 free -m 清理缓存的命令  echo 1 > /proc/sys/vm/drop_caches echo 2 > /proc/sys/v ...

  4. 从零开始—Socket系统调用和多态封装

    1 重新搭建实验环境 前面都是用实验楼环境做的实验,偷的懒总是要还的,这一次重装环境前后花了十几个小时,踩了无数的坑. 1.1 Ubuntu和LINUX内核的区别 Ubuntu是基于LINUX内核编写 ...

  5. Windows下mysql-5.7.28下载、安装、配置教程

    最近需要更换mysql数据库的版本,写一篇文章,记录一下 一.下载mysql数据库 mysql的下载共有两种,一种是zip压缩文件,另一种是msi安装程序 官方5.7版本zip压缩文件下载页面 官方5 ...

  6. CouchDB学习一

    端口 端口号 协议 作用 5984 tcp 标椎集群端口用于所有的HTTP API请求 5986 tcp 用于管理员对节点与分片的管理 4369 tcp Erlang端口到daemon的映射 配置介绍 ...

  7. Tomcat系列(二)- EndPoint源码解析

    在上一节中我们描述了Tomcat的整体架构, 我们知道了Tomcat分为两个大组件,一个连接器和一个容器. 而我们这次要讲的 EndPoint的组件就是属于连接器里面的. 它是一个通信的端点,就是负责 ...

  8. Java工作流引擎jflow对流程的结束大总结

    关键字: 工作流程管理系统 工作流引擎 asp.net工作流引擎 java工作流引擎. 表单引擎 工作流功能说明  工作流设计 工作流快速开发平台   业务流程管理   bpm工作流系统  java工 ...

  9. MySQL基础-存储过程

    存储过程 定义:将一批为了完成特定功能的SQL语句集,根据传入的参数(也可没有),调用,完成单个sql语句更复杂的功能 存储过程思想很简单,就是SQL语句层面上的代码封装和重用 优点:1) 可封装,并 ...

  10. 报错解决 unable to unroll loop, loop does not appear to terminate in a timely manner (994 iterations) or unrolled loop is too large, use the [unroll(n)] attribute to force an exact higher number

    在 Unity 写 Shader 的时候,在一个循环里面使用了 tex2D 函数,类似与下面这样: fixed2 center = fixed2(0.5,0.5); fixed2 uv = i.uv ...