feat(routing): BuildClientConfig 翻译用户规则(层级/IP直连/域名直连/三模式)
ClientConfigOpts 加 Profile *routing.Profile(nil = 逐字节回退旧行为)。 translateUserRules 把 profile.Rules 翻译成 route.rules,插在系统层3 (私有域名)之后、国内分流之前:action→outbound、type→字段映射;IP 直连 并入 TUN 入站的 route_exclude_address(真正生效的字段,而非顶层 route 对象);域名直连开 dns.reverse_mapping;三模式(rule/global/direct)决定 route.final 与是否跳过用户规则层/国内分流层。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/wangjia/pangolin/server/internal/dpcred"
|
||||
"github.com/wangjia/pangolin/server/internal/nodes"
|
||||
"github.com/wangjia/pangolin/server/internal/routing"
|
||||
)
|
||||
|
||||
// ClientConfigOpts carries per-request rendering options for BuildClientConfig.
|
||||
@@ -22,6 +23,81 @@ type ClientConfigOpts struct {
|
||||
// 必须排在国内分流(geoip-cn)之前:锚点(frps@ali)是国内 IP,否则 smartRoute
|
||||
// 会把它分流成直连,被 frps 侧安全组限源(仅节点出口)拦截。
|
||||
PrivateSplitDomains []string
|
||||
// Profile 是当前用户的路由档案(可配置分流,Task 1-3)。nil = 完全回退旧行为
|
||||
// (仅由 SplitCN/PrivateSplitDomains 决定),用于灰度/未迁移用户与旧调用点。
|
||||
// 非 nil 时:
|
||||
// - Mode=="rule":按 Profile.Rules(Enabled)插入用户规则层(系统层3之后、
|
||||
// 国内分流之前);国内分流条件由 opts.SplitCN 改为 Profile.Builtin.ChinaDirect;
|
||||
// route.final 由 Profile.Final 决定(direct→"direct"/否则"auto")。
|
||||
// - Mode=="global"/"direct":跳过用户规则层与国内分流层,route.final 分别为
|
||||
// "auto"/"direct";系统层(hijack-dns/LAN/私有域名)不受影响,恒生效。
|
||||
Profile *routing.Profile
|
||||
}
|
||||
|
||||
// translateUserRules 把 profile 的用户规则翻译成 route.rules 条目。仅
|
||||
// Mode=="rule" 时产出内容;其余模式(global/direct)按约定跳过用户规则层,
|
||||
// 返回全零值。nil profile 由调用方在外层门控,不会传进来。
|
||||
//
|
||||
// 返回值:
|
||||
// - rules: 按 Profile.Rules 顺序生成的 route.rules 条目(仅 Enabled==true)。
|
||||
// - extraExclude: type==ip_cidr && action==direct 的 value,供调用方并入
|
||||
// TUN 入站的 route_exclude_address(auto_route 层直连才真正生效,
|
||||
// 见 tunIn 构造处注释)。
|
||||
// - hasDomainDirect: 是否存在 action==direct 的域名类规则(domain/
|
||||
// domain_suffix/domain_keyword),供调用方决定是否开 dns.reverse_mapping。
|
||||
// - geoSets: 规则引用到的 geoip-<v>/geosite-<v> rule_set tag(去重),供
|
||||
// 调用方在国内分流(splitActive)之外也补上 rule_set 定义。
|
||||
func translateUserRules(p *routing.Profile) (rules []any, extraExclude []string, hasDomainDirect bool, geoSets []string) {
|
||||
if p == nil || p.Mode != "rule" {
|
||||
return nil, nil, false, nil
|
||||
}
|
||||
seenGeo := map[string]bool{}
|
||||
for _, r := range p.Rules {
|
||||
if !r.Enabled {
|
||||
continue
|
||||
}
|
||||
var outbound string
|
||||
switch r.Action {
|
||||
case "direct":
|
||||
outbound = "direct"
|
||||
case "proxy":
|
||||
outbound = "auto"
|
||||
case "reject":
|
||||
outbound = "block"
|
||||
default:
|
||||
// 未知 action 防御性跳过(Validate 已在写入路径拦截,这里双保险)。
|
||||
continue
|
||||
}
|
||||
switch r.Type {
|
||||
case "domain", "domain_suffix", "domain_keyword":
|
||||
rules = append(rules, map[string]any{r.Type: []string{r.Value}, "outbound": outbound})
|
||||
if outbound == "direct" {
|
||||
hasDomainDirect = true
|
||||
}
|
||||
case "ip_cidr":
|
||||
rules = append(rules, map[string]any{"ip_cidr": []string{r.Value}, "outbound": outbound})
|
||||
if outbound == "direct" {
|
||||
extraExclude = append(extraExclude, r.Value)
|
||||
}
|
||||
case "geoip", "geosite":
|
||||
tag := r.Type + "-" + strings.ToLower(r.Value)
|
||||
rules = append(rules, map[string]any{"rule_set": []string{tag}, "outbound": outbound})
|
||||
if !seenGeo[tag] {
|
||||
seenGeo[tag] = true
|
||||
geoSets = append(geoSets, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return rules, extraExclude, hasDomainDirect, geoSets
|
||||
}
|
||||
|
||||
// ruleSetDef 渲染一个自托管 remote rule_set 定义(与既有 geoip-cn/geosite-cn
|
||||
// 定义块同构:base + "/v1/rules/<tag>.srs",direct 直连下载)。
|
||||
func ruleSetDef(tag, base string) map[string]any {
|
||||
return map[string]any{
|
||||
"tag": tag, "type": "remote", "format": "binary",
|
||||
"url": base + "/v1/rules/" + tag + ".srs", "download_detour": "direct",
|
||||
}
|
||||
}
|
||||
|
||||
// BuildClientConfig renders a complete sing-box CLIENT configuration JSON that
|
||||
@@ -161,8 +237,69 @@ func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts Clien
|
||||
"outbound": "auto",
|
||||
})
|
||||
}
|
||||
|
||||
// 用户路由规则(可配置分流,Task 4):插在系统层3(私有域名)之后、
|
||||
// 国内分流(splitActive,下方)之前。opts.Profile==nil 时 translateUserRules
|
||||
// 直接返回全零值,以下每一步都随之短路,保证 nil-profile 渲染逐字节不变。
|
||||
var userRules []any
|
||||
var extraExclude []string
|
||||
var hasDomainDirect bool
|
||||
var geoSets []string
|
||||
if opts.Profile != nil {
|
||||
userRules, extraExclude, hasDomainDirect, geoSets = translateUserRules(opts.Profile)
|
||||
if opts.RulesBaseURL == "" && len(geoSets) > 0 {
|
||||
// 没有 base 就没法渲染 remote rule_set 的下载 URL,引用它的用户规则
|
||||
// 会指向未定义的 tag(sing-box FATAL)。与 splitActive 缺 base 时静默
|
||||
// 跳过国内分流同一语义:丢弃这些规则,不让配置渲染出无效引用。
|
||||
kept := userRules[:0]
|
||||
for _, ur := range userRules {
|
||||
if m, ok := ur.(map[string]any); ok {
|
||||
if _, hasRS := m["rule_set"]; hasRS {
|
||||
continue
|
||||
}
|
||||
}
|
||||
kept = append(kept, ur)
|
||||
}
|
||||
userRules = kept
|
||||
geoSets = nil
|
||||
}
|
||||
}
|
||||
if len(userRules) > 0 {
|
||||
routeRules = append(routeRules, userRules...)
|
||||
}
|
||||
if len(extraExclude) > 0 {
|
||||
// IP 直连真生效:并入 TUN 入站的 route_exclude_address(auto_route 层排除,
|
||||
// 见 tunIn 构造处注释——单靠 route.rules 的 ip_cidr→direct 在 macOS
|
||||
// strict_route 下不生效)。与既有 LAN 网段去重合并。
|
||||
exclude := []string{"192.168.0.0/16", "10.0.0.0/8"}
|
||||
seen := map[string]bool{"192.168.0.0/16": true, "10.0.0.0/8": true}
|
||||
for _, e := range extraExclude {
|
||||
if !seen[e] {
|
||||
seen[e] = true
|
||||
exclude = append(exclude, e)
|
||||
}
|
||||
}
|
||||
tunIn["route_exclude_address"] = exclude
|
||||
}
|
||||
|
||||
// route.final:三模式语义。nil profile 保持旧的硬编码 "auto"。
|
||||
final := "auto"
|
||||
if opts.Profile != nil {
|
||||
switch opts.Profile.Mode {
|
||||
case "direct":
|
||||
final = "direct"
|
||||
case "global":
|
||||
final = "auto"
|
||||
case "rule":
|
||||
if opts.Profile.Final == "direct" {
|
||||
final = "direct"
|
||||
} else {
|
||||
final = "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
route := map[string]any{
|
||||
"final": "auto",
|
||||
"final": final,
|
||||
"auto_detect_interface": true,
|
||||
// sing-box 1.12+ 要求显式声明出站域名用哪个 DNS 解析,缺失即 FATAL。
|
||||
"default_domain_resolver": map[string]any{"server": "local"},
|
||||
@@ -170,20 +307,44 @@ func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts Clien
|
||||
// 国内分流(#5):命中 geoip-cn / geosite-cn → 直连(不走隧道),省流量 + 国内快。
|
||||
// rule_set 走控制面自托管(国内可达,客户端反正连控制面);download_detour:direct
|
||||
// 让 sing-box 直连下载 .srs(不经隧道,启动期隧道还没起)。
|
||||
splitActive := opts.SplitCN && opts.RulesBaseURL != ""
|
||||
// 有 Profile 时,是否分流改由 Profile 决定(mode==rule 且 Builtin.ChinaDirect);
|
||||
// nil profile 保持旧的 opts.SplitCN 兜底,与原逻辑逐字节一致。
|
||||
chinaDirectWanted := opts.SplitCN
|
||||
if opts.Profile != nil {
|
||||
chinaDirectWanted = opts.Profile.Mode == "rule" && opts.Profile.Builtin.ChinaDirect
|
||||
}
|
||||
splitActive := chinaDirectWanted && opts.RulesBaseURL != ""
|
||||
base := strings.TrimRight(opts.RulesBaseURL, "/")
|
||||
if splitActive {
|
||||
base := strings.TrimRight(opts.RulesBaseURL, "/")
|
||||
routeRules = append(routeRules, map[string]any{
|
||||
"rule_set": []string{"geoip-cn", "geosite-cn"},
|
||||
"outbound": "direct",
|
||||
})
|
||||
route["rule_set"] = []any{
|
||||
map[string]any{"tag": "geoip-cn", "type": "remote", "format": "binary",
|
||||
"url": base + "/v1/rules/geoip-cn.srs", "download_detour": "direct"},
|
||||
map[string]any{"tag": "geosite-cn", "type": "remote", "format": "binary",
|
||||
"url": base + "/v1/rules/geosite-cn.srs", "download_detour": "direct"},
|
||||
ruleSetDef("geoip-cn", base),
|
||||
ruleSetDef("geosite-cn", base),
|
||||
}
|
||||
}
|
||||
if len(geoSets) > 0 {
|
||||
// 用户规则用到的 geo rule_set,凡未被上面的国内分流块定义过,在此补上
|
||||
// (去重)——即便 SplitCN/Builtin.ChinaDirect 关闭,用户显式规则仍要生效。
|
||||
defs, _ := route["rule_set"].([]any)
|
||||
defined := map[string]bool{}
|
||||
for _, d := range defs {
|
||||
if dm, ok := d.(map[string]any); ok {
|
||||
if tag, ok := dm["tag"].(string); ok {
|
||||
defined[tag] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, tag := range geoSets {
|
||||
if !defined[tag] {
|
||||
defs = append(defs, ruleSetDef(tag, base))
|
||||
defined[tag] = true
|
||||
}
|
||||
}
|
||||
route["rule_set"] = defs
|
||||
}
|
||||
route["rules"] = routeRules
|
||||
|
||||
// DNS: remote over tunnel, local for domestic.
|
||||
@@ -212,8 +373,11 @@ func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts Clien
|
||||
dnsRules = append(dnsRules, map[string]any{
|
||||
"domain": opts.PrivateSplitDomains, "server": "dns-system",
|
||||
})
|
||||
}
|
||||
if privateSplit || hasDomainDirect {
|
||||
// 回映射:记住"哪个 IP 是哪个域名解析出来的",给后续按 IP 发起的连接补回
|
||||
// 域名元数据——路由层的 domain 规则(私有域名→隧道)靠它才会命中。
|
||||
// 域名元数据——路由层的 domain 规则(私有域名→隧道 / 用户域名直连)靠它
|
||||
// 才会命中。hasDomainDirect:用户规则含 action==direct 的域名类规则时同样需要。
|
||||
dns["reverse_mapping"] = true
|
||||
}
|
||||
// 国内分流的 DNS 面(补 #5 数据面之外的 DNS 面):开分流时,命中 geosite-cn 的
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/wangjia/pangolin/server/internal/nodes"
|
||||
"github.com/wangjia/pangolin/server/internal/routing"
|
||||
)
|
||||
|
||||
func testNode() *nodes.NodeRow {
|
||||
@@ -235,3 +237,237 @@ func TestBuildClientConfigLANExclude(t *testing.T) {
|
||||
t.Error("route_exclude_address must NOT contain 172.16.0.0/12 (tunnel DNS 172.19.x lives there)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Task 4 test helpers ---
|
||||
//
|
||||
// NOTE on route_exclude_address: sing-box only honors this key on the TUN
|
||||
// inbound (see tunIn construction + comment at clientconfig.go:~116-121;
|
||||
// TestBuildClientConfigLANExclude above already asserts it there) — there is
|
||||
// no such key under the top-level "route" object. The task brief's sample
|
||||
// snippet checked cfg["route"]["route_exclude_address"], which does not
|
||||
// exist in main's rendering and would make "IP 直连真生效" a no-op. Fixed
|
||||
// here to read it from the tun inbound, consistent with the existing
|
||||
// mechanism this task explicitly says to reuse ("main clientconfig.go:121").
|
||||
|
||||
func ruleIndexByDomain(rules []any, domain string) int {
|
||||
for i, r := range rules {
|
||||
rm := r.(map[string]any)
|
||||
for _, key := range []string{"domain", "domain_suffix", "domain_keyword"} {
|
||||
if v, ok := rm[key]; ok {
|
||||
for _, s := range toStrings(v) {
|
||||
if s == domain {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func ruleIndexByRuleSet(rules []any, tag string) int {
|
||||
for i, r := range rules {
|
||||
rm := r.(map[string]any)
|
||||
if v, ok := rm["rule_set"]; ok {
|
||||
for _, s := range toStrings(v) {
|
||||
if s == tag {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func hasHijackDNS(rules []any) bool {
|
||||
for _, r := range rules {
|
||||
rm := r.(map[string]any)
|
||||
if rm["action"] == "hijack-dns" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func toStrings(v any) []string {
|
||||
arr, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(arr))
|
||||
for _, x := range arr {
|
||||
if s, ok := x.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func contains(ss []string, s string) bool {
|
||||
for _, x := range ss {
|
||||
if x == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// tunRouteExcludeAddress reads route_exclude_address off the TUN inbound
|
||||
// (the field that actually has effect — see NOTE above).
|
||||
func tunRouteExcludeAddress(t *testing.T, cfg map[string]any) []string {
|
||||
t.Helper()
|
||||
for _, in := range cfg["inbounds"].([]any) {
|
||||
im := in.(map[string]any)
|
||||
if im["type"] == "tun" {
|
||||
return toStrings(im["route_exclude_address"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBuildConfigUserRules(t *testing.T) {
|
||||
node := testNode()
|
||||
p := routing.Default()
|
||||
p.Rules = []routing.Rule{
|
||||
{Type: "domain_suffix", Value: "github.com", Action: "proxy", Enabled: true},
|
||||
{Type: "ip_cidr", Value: "35.190.0.0/16", Action: "direct", Enabled: true},
|
||||
{Type: "domain_suffix", Value: "git.51yanmei.com", Action: "direct", Enabled: true},
|
||||
}
|
||||
raw, err := BuildClientConfig(node, "dp", "k", ClientConfigOpts{Profile: p, SplitCN: true, RulesBaseURL: "http://x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rules := cfg["route"].(map[string]any)["rules"].([]any)
|
||||
// 用户 github→auto 规则应在 geoip-cn 规则之前
|
||||
iUser, iCN := ruleIndexByDomain(rules, "github.com"), ruleIndexByRuleSet(rules, "geoip-cn")
|
||||
if iUser < 0 || iCN < 0 || iUser > iCN {
|
||||
t.Fatalf("user rule must precede geoip-cn: %d vs %d", iUser, iCN)
|
||||
}
|
||||
// IP 直连并入 route_exclude_address(TUN 入站,见上方 NOTE)
|
||||
excl := tunRouteExcludeAddress(t, cfg)
|
||||
if !contains(excl, "35.190.0.0/16") {
|
||||
t.Fatalf("ip direct not in route_exclude_address: %v", excl)
|
||||
}
|
||||
// 有域名直连 → reverse_mapping 开
|
||||
if cfg["dns"].(map[string]any)["reverse_mapping"] != true {
|
||||
t.Fatal("reverse_mapping must be on")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigGlobalMode(t *testing.T) {
|
||||
p := routing.Default()
|
||||
p.Mode = "global"
|
||||
p.Rules = []routing.Rule{{Type: "domain_suffix", Value: "github.com", Action: "direct", Enabled: true}}
|
||||
raw, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: p, SplitCN: true, RulesBaseURL: "http://x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// global:忽略用户规则 + 无 geoip-cn 直连,final=auto,但系统层(hijack-dns/LAN)仍在
|
||||
rules := cfg["route"].(map[string]any)["rules"].([]any)
|
||||
if ruleIndexByDomain(rules, "github.com") >= 0 {
|
||||
t.Fatal("global must ignore user rules")
|
||||
}
|
||||
if cfg["route"].(map[string]any)["final"] != "auto" {
|
||||
t.Fatal("global final=auto")
|
||||
}
|
||||
if !hasHijackDNS(rules) {
|
||||
t.Fatal("system layer must survive in global")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigDirectMode(t *testing.T) {
|
||||
p := routing.Default()
|
||||
p.Mode = "direct"
|
||||
p.Rules = []routing.Rule{{Type: "domain_suffix", Value: "github.com", Action: "proxy", Enabled: true}}
|
||||
raw, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: p, SplitCN: true, RulesBaseURL: "http://x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rules := cfg["route"].(map[string]any)["rules"].([]any)
|
||||
if ruleIndexByDomain(rules, "github.com") >= 0 {
|
||||
t.Fatal("direct mode must ignore user rules")
|
||||
}
|
||||
if cfg["route"].(map[string]any)["final"] != "direct" {
|
||||
t.Fatal("direct mode final=direct")
|
||||
}
|
||||
if !hasHijackDNS(rules) {
|
||||
t.Fatal("system layer must survive in direct mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigRuleModeFinalDirect(t *testing.T) {
|
||||
p := routing.Default()
|
||||
p.Final = "direct"
|
||||
raw, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: p, RulesBaseURL: "http://x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg["route"].(map[string]any)["final"] != "direct" {
|
||||
t.Fatal("rule mode with Final=direct should render final=direct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigUserGeoRuleWithoutSplitCN(t *testing.T) {
|
||||
// SplitCN 关闭(Builtin.ChinaDirect=false)但用户手动加了 geoip-cn 规则 →
|
||||
// 仍需补 rule_set 定义,规则本身要生效。
|
||||
p := routing.Default()
|
||||
p.Builtin.ChinaDirect = false
|
||||
p.Rules = []routing.Rule{{Type: "geoip", Value: "cn", Action: "direct", Enabled: true}}
|
||||
raw, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: p, RulesBaseURL: "http://x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
route := cfg["route"].(map[string]any)
|
||||
rules := route["rules"].([]any)
|
||||
if ruleIndexByRuleSet(rules, "geoip-cn") < 0 {
|
||||
t.Fatal("user geoip-cn rule must be present in route.rules")
|
||||
}
|
||||
rs, ok := route["rule_set"].([]any)
|
||||
if !ok || len(rs) == 0 {
|
||||
t.Fatal("route.rule_set must define geoip-cn even though SplitCN(Builtin.ChinaDirect) is off")
|
||||
}
|
||||
found := false
|
||||
for _, d := range rs {
|
||||
if dm, ok := d.(map[string]any); ok && dm["tag"] == "geoip-cn" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("geoip-cn definition missing from route.rule_set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigNilProfileUnchanged(t *testing.T) {
|
||||
// Profile==nil → 与现有行为逐字节一致(回退默认)
|
||||
a, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{SplitCN: true, RulesBaseURL: "http://x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := BuildClientConfig(testNode(), "dp", "k", ClientConfigOpts{Profile: nil, SplitCN: true, RulesBaseURL: "http://x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(a, b) {
|
||||
t.Fatal("nil profile must equal no-profile")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user