feat: support export config outline and config marshal (#27)

This commit is contained in:
mzz 2023-02-25 22:53:18 +08:00 committed by GitHub
parent 90dac980b9
commit 5cf6dca509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 689 additions and 153 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
bpf_bpfeb*.go bpf_bpfeb*.go
bpf_bpfel*.go bpf_bpfel*.go
dae dae
outline.json

View File

@ -25,4 +25,5 @@ func Execute() error {
func init() { func init() {
rootCmd.AddCommand(runCmd) rootCmd.AddCommand(runCmd)
rootCmd.AddCommand(validateCmd) rootCmd.AddCommand(validateCmd)
rootCmd.AddCommand(exportCmd)
} }

31
cmd/export.go Normal file
View File

@ -0,0 +1,31 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, v2rayA Organization <team@v2raya.org>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/v2rayA/dae/config"
)
var (
exportCmd = &cobra.Command{
Use: "export",
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Help()
},
}
exportOutlineCmd = &cobra.Command{
Use: "outline",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(config.ExportOutlineJson(Version))
},
}
)
func init() {
exportCmd.AddCommand(exportOutlineCmd)
}

View File

@ -32,16 +32,16 @@ var (
internal.AutoSu() internal.AutoSu()
// Read config from --config cfgFile. // Read config from --config cfgFile.
param, includes, err := readConfig(cfgFile) conf, includes, err := readConfig(cfgFile)
if err != nil { if err != nil {
logrus.Fatalln("readConfig:", err) logrus.Fatalln("readConfig:", err)
} }
log := logger.NewLogger(param.Global.LogLevel, disableTimestamp) log := logger.NewLogger(conf.Global.LogLevel, disableTimestamp)
logrus.SetLevel(log.Level) logrus.SetLevel(log.Level)
log.Infof("Include config files: [%v]", strings.Join(includes, ", ")) log.Infof("Include config files: [%v]", strings.Join(includes, ", "))
if err := Run(log, param); err != nil { if err := Run(log, conf); err != nil {
logrus.Fatalln(err) logrus.Fatalln(err)
} }
}, },
@ -53,16 +53,18 @@ func init() {
runCmd.PersistentFlags().BoolVarP(&disableTimestamp, "disable-timestamp", "", false, "disable timestamp") runCmd.PersistentFlags().BoolVarP(&disableTimestamp, "disable-timestamp", "", false, "disable timestamp")
} }
func Run(log *logrus.Logger, param *config.Params) (err error) { func Run(log *logrus.Logger, conf *config.Config) (err error) {
/// Get tag -> nodeList mapping. /// Get tag -> nodeList mapping.
tagToNodeList := map[string][]string{} tagToNodeList := map[string][]string{}
if len(param.Node) > 0 { if len(conf.Node) > 0 {
tagToNodeList[""] = append(tagToNodeList[""], param.Node...) for _, node := range conf.Node {
tagToNodeList[""] = append(tagToNodeList[""], string(node))
}
} }
// Resolve subscriptions to nodes. // Resolve subscriptions to nodes.
for _, sub := range param.Subscription { for _, sub := range conf.Subscription {
tag, nodes, err := internal.ResolveSubscription(log, filepath.Dir(cfgFile), sub) tag, nodes, err := internal.ResolveSubscription(log, filepath.Dir(cfgFile), string(sub))
if err != nil { if err != nil {
log.Warnf(`failed to resolve subscription "%v": %v`, sub, err) log.Warnf(`failed to resolve subscription "%v": %v`, sub, err)
} }
@ -74,7 +76,7 @@ func Run(log *logrus.Logger, param *config.Params) (err error) {
return fmt.Errorf("no node found, which could because all subscription resolving failed") return fmt.Errorf("no node found, which could because all subscription resolving failed")
} }
if len(param.Global.LanInterface) == 0 && len(param.Global.WanInterface) == 0 { if len(conf.Global.LanInterface) == 0 && len(conf.Global.WanInterface) == 0 {
return fmt.Errorf("LanInterface and WanInterface cannot both be empty") return fmt.Errorf("LanInterface and WanInterface cannot both be empty")
} }
@ -82,10 +84,10 @@ func Run(log *logrus.Logger, param *config.Params) (err error) {
t, err := control.NewControlPlane( t, err := control.NewControlPlane(
log, log,
tagToNodeList, tagToNodeList,
param.Group, conf.Group,
&param.Routing, &conf.Routing,
&param.Global, &conf.Global,
&param.Dns, &conf.Dns,
) )
if err != nil { if err != nil {
return err return err
@ -98,7 +100,7 @@ func Run(log *logrus.Logger, param *config.Params) (err error) {
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGILL) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGILL)
go func() { go func() {
if err := t.ListenAndServe(param.Global.TproxyPort); err != nil { if err := t.ListenAndServe(conf.Global.TproxyPort); err != nil {
log.Errorln("ListenAndServe:", err) log.Errorln("ListenAndServe:", err)
sigs <- nil sigs <- nil
} }
@ -110,14 +112,14 @@ func Run(log *logrus.Logger, param *config.Params) (err error) {
return nil return nil
} }
func readConfig(cfgFile string) (params *config.Params, includes []string, err error) { func readConfig(cfgFile string) (conf *config.Config, includes []string, err error) {
merger := config.NewMerger(cfgFile) merger := config.NewMerger(cfgFile)
sections, includes, err := merger.Merge() sections, includes, err := merger.Merge()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if params, err = config.New(sections); err != nil { if conf, err = config.New(sections); err != nil {
return nil, nil, err return nil, nil, err
} }
return params, includes, nil return conf, includes, nil
} }

View File

@ -46,7 +46,7 @@ func New(log *logrus.Logger, dns *config.Dns, opt *NewOption) (s *Dns, err error
return nil, fmt.Errorf("too many upstreams") return nil, fmt.Errorf("too many upstreams")
} }
tag, link := common.GetTagFromLinkLikePlaintext(upstreamRaw) tag, link := common.GetTagFromLinkLikePlaintext(string(upstreamRaw))
if tag == "" { if tag == "" {
return nil, fmt.Errorf("%w: '%v' has no tag", BadUpstreamFormatError, upstreamRaw) return nil, fmt.Errorf("%w: '%v' has no tag", BadUpstreamFormatError, upstreamRaw)
} }

View File

@ -7,10 +7,8 @@ package outbound
import ( import (
"fmt" "fmt"
"github.com/sirupsen/logrus"
"github.com/v2rayA/dae/common/consts" "github.com/v2rayA/dae/common/consts"
"github.com/v2rayA/dae/config" "github.com/v2rayA/dae/config"
"github.com/v2rayA/dae/pkg/config_parser"
"strconv" "strconv"
) )
@ -20,50 +18,38 @@ type DialerSelectionPolicy struct {
} }
func NewDialerSelectionPolicyFromGroupParam(param *config.Group) (policy *DialerSelectionPolicy, err error) { func NewDialerSelectionPolicyFromGroupParam(param *config.Group) (policy *DialerSelectionPolicy, err error) {
switch val := param.Policy.(type) { fs := config.FunctionListOrStringToFunction(param.Policy)
case string: if len(fs) > 1 || len(fs) == 0 {
switch consts.DialerSelectionPolicy(val) { return nil, fmt.Errorf("policy should be exact 1 function: got %v", len(fs))
case consts.DialerSelectionPolicy_Random, }
consts.DialerSelectionPolicy_MinAverage10Latencies, f := fs[0]
consts.DialerSelectionPolicy_MinLastLatency, switch fName := consts.DialerSelectionPolicy(f.Name); fName {
consts.DialerSelectionPolicy_MinMovingAverageLatencies: case consts.DialerSelectionPolicy_Random,
return &DialerSelectionPolicy{ consts.DialerSelectionPolicy_MinAverage10Latencies,
Policy: consts.DialerSelectionPolicy(val), consts.DialerSelectionPolicy_MinLastLatency,
}, nil consts.DialerSelectionPolicy_MinMovingAverageLatencies:
case consts.DialerSelectionPolicy_Fixed: return &DialerSelectionPolicy{
return nil, fmt.Errorf("%v need to specify node index", val) Policy: fName,
default: }, nil
return nil, fmt.Errorf("unexpected policy: %v", val) case consts.DialerSelectionPolicy_Fixed:
if f.Not {
return nil, fmt.Errorf("policy param does not support not operator: !%v()", f.Name)
} }
case []*config_parser.Function: if len(f.Params) > 1 || f.Params[0].Key != "" {
if len(val) > 1 || len(val) == 0 { return nil, fmt.Errorf(`invalid "%v" param format`, f.Name)
logrus.Debugf("%@", val)
return nil, fmt.Errorf("policy should be exact 1 function: got %v", len(val))
} }
f := val[0] strIndex := f.Params[0].Val
switch consts.DialerSelectionPolicy(f.Name) { index, err := strconv.Atoi(strIndex)
case consts.DialerSelectionPolicy_Fixed: if len(f.Params) > 1 || f.Params[0].Key != "" {
// Should be like: return nil, fmt.Errorf(`invalid "%v" param format: %w`, f.Name, err)
// policy: fixed(0)
if f.Not {
return nil, fmt.Errorf("policy param does not support not operator: !%v()", f.Name)
}
if len(f.Params) > 1 || f.Params[0].Key != "" {
return nil, fmt.Errorf(`invalid "%v" param format`, f.Name)
}
strIndex := f.Params[0].Val
index, err := strconv.Atoi(strIndex)
if len(f.Params) > 1 || f.Params[0].Key != "" {
return nil, fmt.Errorf(`invalid "%v" param format: %w`, f.Name, err)
}
return &DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy(f.Name),
FixedIndex: index,
}, nil
default:
return nil, fmt.Errorf("unexpected policy func: %v", f.Name)
} }
return &DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy(f.Name),
FixedIndex: index,
}, nil
default: default:
return nil, fmt.Errorf("unexpected param.Policy.(type): %T", val) return nil, fmt.Errorf("unexpected policy: %v", f.Name)
} }
} }

View File

@ -42,7 +42,7 @@ func (b *RulesBuilder) RegisterFunctionParser(funcName string, parser FunctionPa
func (b *RulesBuilder) Apply(rules []*config_parser.RoutingRule) (err error) { func (b *RulesBuilder) Apply(rules []*config_parser.RoutingRule) (err error) {
for _, rule := range rules { for _, rule := range rules {
b.log.Debugln("[rule]", rule.String(true)) b.log.Debugln("[rule]", rule.String(true, false, false))
outbound, err := ParseOutbound(&rule.Outbound) outbound, err := ParseOutbound(&rule.Outbound)
if err != nil { if err != nil {
return err return err
@ -80,7 +80,7 @@ func (b *RulesBuilder) Apply(rules []*config_parser.RoutingRule) (err error) {
} }
if err = functionParser(b.log, f, key, paramValueGroup, overrideOutbound); err != nil { if err = functionParser(b.log, f, key, paramValueGroup, overrideOutbound); err != nil {
return fmt.Errorf("failed to parse '%v': %w", f.String(false), err) return fmt.Errorf("failed to parse '%v': %w", f.String(false, false, false), err)
} }
} }
} }

View File

@ -87,7 +87,7 @@ func (o *MergeAndSortRulesOptimizer) Optimize(rules []*config_parser.RoutingRule
if len(mergingRule.AndFunctions) == 1 && if len(mergingRule.AndFunctions) == 1 &&
len(rules[i].AndFunctions) == 1 && len(rules[i].AndFunctions) == 1 &&
mergingRule.AndFunctions[0].Name == rules[i].AndFunctions[0].Name && mergingRule.AndFunctions[0].Name == rules[i].AndFunctions[0].Name &&
rules[i].Outbound.String(true) == mergingRule.Outbound.String(true) { rules[i].Outbound.String(true, false, true) == mergingRule.Outbound.String(true, false, true) {
mergingRule.AndFunctions[0].Params = append(mergingRule.AndFunctions[0].Params, rules[i].AndFunctions[0].Params...) mergingRule.AndFunctions[0].Params = append(mergingRule.AndFunctions[0].Params, rules[i].AndFunctions[0].Params...)
} else { } else {
newRules = append(newRules, mergingRule) newRules = append(newRules, mergingRule)
@ -134,10 +134,10 @@ func deduplicateParams(list []*config_parser.Param) []*config_parser.Param {
res := make([]*config_parser.Param, 0, len(list)) res := make([]*config_parser.Param, 0, len(list))
m := make(map[string]struct{}) m := make(map[string]struct{})
for _, v := range list { for _, v := range list {
if _, ok := m[v.String(true)]; ok { if _, ok := m[v.String(true, false)]; ok {
continue continue
} }
m[v.String(true)] = struct{}{} m[v.String(true, false)] = struct{}{}
res = append(res, v) res = append(res, v)
} }
return res return res
@ -254,6 +254,8 @@ func (o *DatReaderOptimizer) Optimize(rules []*config_parser.RoutingRule) ([]*co
params, err = o.loadGeoSite(fields[0], fields[1]) params, err = o.loadGeoSite(fields[0], fields[1])
case consts.Function_Ip: case consts.Function_Ip:
params, err = o.loadGeoIp(fields[0], fields[1]) params, err = o.loadGeoIp(fields[0], fields[1])
default:
return nil, fmt.Errorf("unsupported extension file extraction in function %v", f.Name)
} }
default: default:
// Keep this param. // Keep this param.

View File

@ -42,11 +42,24 @@ func FunctionOrStringToFunction(fs FunctionOrString) (f *config_parser.Function)
} }
} }
type FunctionListOrString interface{}
func FunctionListOrStringToFunction(fs FunctionListOrString) (f []*config_parser.Function) {
switch fs := fs.(type) {
case string:
return []*config_parser.Function{{Name: fs}}
case []*config_parser.Function:
return fs
default:
panic(fmt.Sprintf("unknown type of 'fallback' in section routing: %T", fs))
}
}
type Group struct { type Group struct {
Name string `mapstructure:"_"` Name string `mapstructure:"_"`
Filter []*config_parser.Function `mapstructure:"filter"` Filter []*config_parser.Function `mapstructure:"filter"`
Policy interface{} `mapstructure:"policy" required:""` Policy FunctionListOrString `mapstructure:"policy" required:""`
} }
type DnsRequestRouting struct { type DnsRequestRouting struct {
@ -57,31 +70,32 @@ type DnsResponseRouting struct {
Rules []*config_parser.RoutingRule `mapstructure:"_"` Rules []*config_parser.RoutingRule `mapstructure:"_"`
Fallback FunctionOrString `mapstructure:"fallback" required:""` Fallback FunctionOrString `mapstructure:"fallback" required:""`
} }
type DnsRouting struct {
Request DnsRequestRouting `mapstructure:"request"`
Response DnsResponseRouting `mapstructure:"response"`
}
type KeyableString string
type Dns struct { type Dns struct {
Upstream []string `mapstructure:"upstream"` Upstream []KeyableString `mapstructure:"upstream"`
Routing struct { Routing DnsRouting `mapstructure:"routing"`
Request DnsRequestRouting `mapstructure:"request"`
Response DnsResponseRouting `mapstructure:"response"`
} `mapstructure:"routing"`
} }
type Routing struct { type Routing struct {
Rules []*config_parser.RoutingRule `mapstructure:"_"` Rules []*config_parser.RoutingRule `mapstructure:"_"`
Fallback FunctionOrString `mapstructure:"fallback"` Fallback FunctionOrString `mapstructure:"fallback"`
Final FunctionOrString `mapstructure:"final"`
} }
type Params struct { type Config struct {
Global Global `mapstructure:"global" required:""` Global Global `mapstructure:"global" required:"" desc:"GlobalDesc"`
Subscription []string `mapstructure:"subscription"` Subscription []KeyableString `mapstructure:"subscription"`
Node []string `mapstructure:"node"` Node []KeyableString `mapstructure:"node"`
Group []Group `mapstructure:"group" required:""` Group []Group `mapstructure:"group" required:"" desc:"GroupDesc"`
Routing Routing `mapstructure:"routing" required:""` Routing Routing `mapstructure:"routing" required:""`
Dns Dns `mapstructure:"dns"` Dns Dns `mapstructure:"dns" desc:"DnsDesc"`
} }
// New params from sections. This func assumes merging (section "include") and deduplication for section names has been executed. // New params from sections. This func assumes merging (section "include") and deduplication for section names has been executed.
func New(sections []*config_parser.Section) (params *Params, err error) { func New(sections []*config_parser.Section) (conf *Config, err error) {
// Set up name to section for further use. // Set up name to section for further use.
type Section struct { type Section struct {
Val *config_parser.Section Val *config_parser.Section
@ -92,9 +106,9 @@ func New(sections []*config_parser.Section) (params *Params, err error) {
nameToSection[section.Name] = &Section{Val: section} nameToSection[section.Name] = &Section{Val: section}
} }
params = &Params{} conf = &Config{}
// Use specified parser to parse corresponding section. // Use specified parser to parse corresponding section.
_val := reflect.ValueOf(params) _val := reflect.ValueOf(conf)
val := _val.Elem() val := _val.Elem()
typ := val.Type() typ := val.Type()
for i := 0; i < val.NumField(); i++ { for i := 0; i < val.NumField(); i++ {
@ -134,9 +148,9 @@ func New(sections []*config_parser.Section) (params *Params, err error) {
// Apply config patches. // Apply config patches.
for _, patch := range patches { for _, patch := range patches {
if err = patch(params); err != nil { if err = patch(conf); err != nil {
return nil, err return nil, err
} }
} }
return params, nil return conf, nil
} }

View File

@ -132,10 +132,10 @@ func (m *Merger) dfsMerge(entry string, fatherEntry string) (err error) {
for _, include := range includes { for _, include := range includes {
switch v := include.Value.(type) { switch v := include.Value.(type) {
case *config_parser.Param: case *config_parser.Param:
nextEntry := v.String(true) nextEntry := v.String(true, false)
patterEntries = append(patterEntries, filepath.Join(m.entryDir, nextEntry)) patterEntries = append(patterEntries, filepath.Join(m.entryDir, nextEntry))
default: default:
return fmt.Errorf("unsupported include grammar in %v: %v", entry, include.String()) return fmt.Errorf("unsupported include grammar in %v: %v", entry, include.String(false, false))
} }
} }
// DFS and merge children recursively. // DFS and merge children recursively.

77
config/desc.go Normal file
View File

@ -0,0 +1,77 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
type Desc map[string]string
var SectionSummaryDesc = Desc{
"subscription": "Subscriptions defined here will be resolved as nodes and merged as a part of the global node pool.\nSupport to give the subscription a tag, and filter nodes from a given subscription in the group section.",
"node": "Nodes defined here will be merged as a part of the global node pool.",
"dns": "See more at https://github.com/v2rayA/dae/blob/main/docs/dns.md.",
"group": "Node group. Groups defined here can be used as outbounds in section \"routing\".",
"routing": `Traffic follows this routing. See https://github.com/v2rayA/dae/blob/main/docs/routing.md for full examples.
Notice: domain traffic split will fail if DNS traffic is not taken over by dae.
Built-in outbound: direct, must_direct, block.
Available functions: domain, sip, dip, sport, dport, ipversion, l4proto, pname, mac.
Available keys in domain function: suffix, keyword, regex, full. No key indicates suffix.
domain: Match domain.
sip: Match source IP. CIDR format is also supported.
dip: Match dest IP. CIDR format is also supported.
sport: Match source port. Range like 8000-9000 is also supported.
dport: Match dest port. Range like 8000-9000 is also supported.
ipversion: Match IP version. Available values: 4, 6.
l4proto: Match level 4 protocol. Available values: tcp, udp.
pname: Match process name. It only works on WAN mode and for localhost programs.
mac: Match source MAC address. It works on LAN mode.`,
}
var SectionDescription = map[string]Desc{
"GlobalDesc": GlobalDesc,
"DnsDesc": DnsDesc,
"GroupDesc": GroupDesc,
}
var GlobalDesc = Desc{
"tproxy_port": "tproxy port to listen at. It is NOT a HTTP/SOCKS port, and is just used by eBPF program.\nIn normal case, you do not need to use it.",
"log_level": "Log level: error, warn, info, debug, trace.",
"tcp_check_url": "Node connectivity check.\nHost of URL should have both IPv4 and IPv6 if you have double stack in local.\nConsidering traffic consumption, it is recommended to choose a site with anycast IP and less response.",
"udp_check_dns": "This DNS will be used to check UDP connectivity of nodes. And if dns_upstream below contains tcp, it also be used to check TCP DNS connectivity of nodes.\nThis DNS should have both IPv4 and IPv6 if you have double stack in local.",
"check_interval": "Interval of connectivity check for TCP and UDP",
"check_tolerance": "Group will switch node only when new_latency <= old_latency - tolerance.",
"lan_interface": "The LAN interface to bind. Use it if you only want to proxy LAN instead of localhost.",
"lan_nat_direct": "SNAT for incoming connection to avoid MAC learning.\nAlways set it true if you are NOT using dae as a transparent bridge, but will reduce forwarding performance for direct traffic in LAN mode.\nThis option does not affect direct traffic performance of WAN.",
"wan_interface": "The WAN interface to bind. Use it if you want to proxy localhost.",
"allow_insecure": "Allow insecure TLS certificates. It is not recommended to turn it on unless you have to.",
"dial_mode": `Optional values of dial_mode are:
1. "ip". Dial proxy using the IP from DNS directly. This allows your ipv4, ipv6 to choose the optimal path respectively, and makes the IP version requested by the application meet expectations. For example, if you use curl -4 ip.sb, you will request IPv4 via proxy and get a IPv4 echo. And curl -6 ip.sb will request IPv6. This may solve some wierd full-cone problem if your are be your node support that.
2. "domain". Dial proxy using the domain from sniffing. This will relieve DNS pollution problem to a great extent if have impure DNS environment. Generally, this mode brings faster proxy response time because proxy will re-resolve the domain in remote, thus get better IP result to connect. This policy does not impact routing. That is to say, domain rewrite will be after traffic split of routing and dae will not re-route it.
3. "domain+". Based on domain mode but do not check the authenticity of sniffing result. It is useful for users whose DNS requests do not go through dae but want faster proxy response time. Notice that, if DNS requests do not go through dae, dae cannot split traffic by domain.`,
}
var DnsDesc = Desc{
"upstream": "Value can be scheme://host:port, where the scheme can be tcp/udp/tcp+udp.\nIf host is a domain and has both IPv4 and IPv6 record, dae will automatically choose IPv4 or IPv6 to use according to group policy (such as min latency policy).\nPlease make sure DNS traffic will go through and be forwarded by dae, which is REQUIRED for domain routing.\nIf dial_mode is \"ip\", the upstream DNS answer SHOULD NOT be polluted, so domestic public DNS is not recommended.",
"request": `DNS requests will follow this routing.
Built-in outbound: asis.
Available functions: qname, qtype`,
"response": `DNS responses will follow this routing.
Built-in outbound: accept, reject.
Available functions: qname, qtype, ip, upstream`,
}
var GroupDesc = Desc{
"filter": `Filter nodes from the global node pool defined by the "subscription" and "node" sections.
Available functions: name, subtag. Not operator is supported.
Available keys in name function: keyword, regex. No key indicates full match.
Available keys in subtag function: regex. No key indicates full match.`,
"policy": `Dialer selection policy. For each new connection, select a node as dialer from group by this policy.
Available values: random, fixed, min, min_avg10, min_moving_avg.
random: Select randomly.
fixed: Select the fixed node. Connectivity check will be disabled.
min: Select node by the latency of last check.
min_avg10: Select node by the average of latencies of last 10 checks.
min_moving_avg: Select node by the moving average of latencies of checks, which means more recent latencies have higher weight.
`,
}

236
config/marshal.go Normal file
View File

@ -0,0 +1,236 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
import (
"bytes"
"fmt"
"github.com/v2rayA/dae/common"
"github.com/v2rayA/dae/pkg/config_parser"
"reflect"
"strconv"
"strings"
)
// Marshal assume all tokens should be legal, and does not prevent injection attacks.
func (c *Config) Marshal(indentSpace int) (b []byte, err error) {
m := marshaller{
indentSpace: indentSpace,
buf: new(bytes.Buffer),
}
// Root.
v := reflect.ValueOf(*c)
t := v.Type()
for i := 0; i < v.NumField(); i++ {
k, ok := t.Field(i).Tag.Lookup("mapstructure")
if !ok {
return nil, fmt.Errorf("section %v misses tag mapstructure", t.Field(i).Name)
}
if err = m.marshalSection(k, v.Field(i), 0); err != nil {
return nil, err
}
}
return m.buf.Bytes(), nil
}
type marshaller struct {
indentSpace int
buf *bytes.Buffer
}
func (m *marshaller) writeLine(depth int, line string) {
m.buf.Write(bytes.Repeat([]byte{' '}, depth*m.indentSpace))
m.buf.WriteString(line)
m.buf.WriteString("\n")
}
func (m *marshaller) marshalStringList(from reflect.Value, depth int, keyable bool) (err error) {
for i := 0; i < from.Len(); i++ {
str := from.Index(i)
if keyable {
tag, afterTag := common.GetTagFromLinkLikePlaintext(str.String())
if len(tag) > 0 {
m.writeLine(depth, tag+":"+strconv.Quote(afterTag))
continue
}
}
m.writeLine(depth, strconv.Quote(str.String()))
}
return nil
}
func (m *marshaller) marshalSection(name string, from reflect.Value, depth int) (err error) {
m.writeLine(depth, name+" {")
defer m.writeLine(depth, "}")
switch from.Kind() {
case reflect.Slice:
elemType := from.Type().Elem()
switch elemType.Kind() {
case reflect.String:
keyable := false
switch elemType {
case reflect.TypeOf(KeyableString("")):
keyable = true
default:
}
if err = m.marshalStringList(from, depth+1, keyable); err != nil {
return err
}
return nil
case reflect.Struct:
// "from" is a section list (sections in section).
/**
from {
field1 {
...
}
field2 {
...
}
}
should be parsed from:
from []struct {
Name string `mapstructure: "_"`
...
}
*/
// The struct should contain Name.
nameStructField, ok := elemType.FieldByName("Name")
if !ok || nameStructField.Type.Kind() != reflect.String || nameStructField.Tag.Get("mapstructure") != "_" {
return fmt.Errorf("a string field \"Name\" with mapstructure:\"_\" is required in struct %v from parse section", from.Type().Elem().String())
}
// Scan sections.
for i := 0; i < from.Len(); i++ {
item := from.Index(i)
nameField := item.FieldByName("Name")
if nameField.Kind() != reflect.String {
return fmt.Errorf("name field of section should be string type")
}
if err = m.marshalSection(nameField.String(), item, depth+1); err != nil {
return err
}
}
return nil
default:
goto unsupported
}
case reflect.Struct:
// Section.
return m.marshalParam(from, depth+1)
default:
goto unsupported
}
panic("code should not reach here")
unsupported:
return fmt.Errorf("unsupported section type %v", from.Type())
}
func (m *marshaller) marshalLeaf(key string, from reflect.Value, depth int) (err error) {
if from.IsZero() {
// Do not marshal zero value.
return nil
}
switch from.Kind() {
case reflect.Slice:
if from.Len() == 0 {
return nil
}
switch from.Index(0).Interface().(type) {
case fmt.Stringer, string,
uint8, uint16, uint32, uint64,
int8, int16, int32, int64,
float32, float64,
bool:
var vals []string
for i := 0; i < from.Len(); i++ {
vals = append(vals, fmt.Sprintf("%v", from.Index(i).Interface()))
}
m.writeLine(depth, key+":"+strconv.Quote(strings.Join(vals, ",")))
case *config_parser.Function:
var vals []string
for i := 0; i < from.Len(); i++ {
v := from.Index(i).Interface().(*config_parser.Function)
vals = append(vals, v.String(true, true, false))
}
m.writeLine(depth, key+":"+strings.Join(vals, "&&"))
case KeyableString:
m.writeLine(depth, key+" {")
if err = m.marshalStringList(from, depth+1, true); err != nil {
return err
}
m.writeLine(depth, "}")
default:
return fmt.Errorf("unknown leaf array type: %v", from.Type())
}
default:
switch val := from.Interface().(type) {
case fmt.Stringer, string,
uint8, uint16, uint32, uint64,
int8, int16, int32, int64,
float32, float64,
bool:
m.writeLine(depth, key+":"+strconv.Quote(fmt.Sprintf("%v", val)))
case *config_parser.Function:
m.writeLine(depth, key+":"+val.String(true, true, false))
default:
return fmt.Errorf("unknown leaf type: %T", val)
}
}
return nil
}
func (m *marshaller) marshalParam(from reflect.Value, depth int) (err error) {
if from.Kind() != reflect.Struct {
return fmt.Errorf("marshalParam can only marshal struct")
}
// Marshal section.
typ := from.Type()
for i := 0; i < from.NumField(); i++ {
field := from.Field(i)
structField := typ.Field(i)
key, ok := structField.Tag.Lookup("mapstructure")
if !ok {
return fmt.Errorf("tag mapstructure is required")
}
// Reserved field.
if key == "_" {
switch structField.Name {
case "Name":
case "Rules":
// Expand.
rules, ok := field.Interface().([]*config_parser.RoutingRule)
if !ok {
return fmt.Errorf("unexpected Rules type: %v", field.Type())
}
for _, r := range rules {
m.writeLine(depth, r.String(false, true, true))
}
default:
return fmt.Errorf("unknown reserved field: %v", structField.Name)
}
continue
}
// Section(s) field.
if field.Kind() == reflect.Struct || (field.Kind() == reflect.Slice &&
field.Type().Elem().Kind() == reflect.Struct) {
if err = m.marshalSection(key, field, depth); err != nil {
return err
}
continue
}
// Normal field.
if err = m.marshalLeaf(key, field, depth); err != nil {
return err
}
}
return nil
}

50
config/marshal_test.go Normal file
View File

@ -0,0 +1,50 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func TestMarshal(t *testing.T) {
abs, err := filepath.Abs("../example.dae")
if err != nil {
t.Fatal(err)
}
merger := NewMerger(abs)
sections, _, err := merger.Merge()
if err != nil {
t.Fatal(err)
}
conf1, err := New(sections)
if err != nil {
t.Fatal(err)
}
b, err := conf1.Marshal(2)
if err != nil {
t.Fatal(err)
}
t.Log(string(b))
// Read it again.
if err = os.WriteFile("/tmp/test.dae", b, 0640); err != nil {
t.Fatal(err)
}
sections, _, err = NewMerger("/tmp/test.dae").Merge()
if err != nil {
t.Fatal(err)
}
conf2, err := New(sections)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(conf1, conf2) {
t.Fatal("not equal")
}
}

114
config/outline.go Normal file
View File

@ -0,0 +1,114 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
import (
jsoniter "github.com/json-iterator/go"
"reflect"
"sort"
)
type Outline struct {
Version string `json:"version"`
Leaves []string `json:"leaves"`
Structure []*OutlineElem `json:"structure"`
}
type OutlineElem struct {
Name string `json:"name,omitempty"`
Mapping string `json:"mapping,omitempty"`
IsArray bool `json:"isArray,omitempty"`
Type string `json:"type,omitempty"`
ElemType string `json:"elemType,omitempty"`
Desc string `json:"desc,omitempty"`
Structure []*OutlineElem `json:"structure,omitempty"`
}
func ExportOutline(version string) *Outline {
// Get structure.
t := reflect.TypeOf(Config{})
exporter := outlineExporter{
leaves: make(map[string]reflect.Type),
pkgPathScope: t.PkgPath(),
}
structure := exporter.exportStruct(t, SectionSummaryDesc, false)
// Get string type leaves.
var leaves []string
for k := range exporter.leaves {
leaves = append(leaves, k)
}
sort.Strings(leaves)
return &Outline{
Version: version,
Leaves: leaves,
Structure: structure,
}
}
func ExportOutlineJson(version string) string {
b, err := jsoniter.MarshalIndent(ExportOutline(version), "", " ")
if err != nil {
panic(err)
}
return string(b)
}
type outlineExporter struct {
leaves map[string]reflect.Type
pkgPathScope string
}
func (e *outlineExporter) exportStruct(t reflect.Type, descSource Desc, inheritSource bool) (outlines []*OutlineElem) {
for i := 0; i < t.NumField(); i++ {
section := t.Field(i)
// Parse desc.
var desc string
if descSource != nil {
desc = descSource[section.Tag.Get("mapstructure")]
}
// Parse elem type.
var isArray bool
var typ reflect.Type
switch section.Type.Kind() {
case reflect.Slice:
typ = section.Type.Elem()
isArray = true
default:
typ = section.Type
}
if typ.Kind() == reflect.Pointer {
typ = typ.Elem()
}
// Parse children.
var children []*OutlineElem
switch typ.Kind() {
case reflect.Struct:
var nextDescSource Desc
if inheritSource {
nextDescSource = descSource
} else {
nextDescSource = SectionDescription[section.Tag.Get("desc")]
}
if typ.PkgPath() == "" || typ.PkgPath() == e.pkgPathScope {
children = e.exportStruct(typ, nextDescSource, true)
}
}
if len(children) == 0 {
// Record leaves.
e.leaves[typ.String()] = typ
}
outlines = append(outlines, &OutlineElem{
Name: section.Name,
Mapping: section.Tag.Get("mapstructure"),
IsArray: isArray,
Type: typ.String(),
Desc: desc,
Structure: children,
})
}
return outlines
}

14
config/outline_test.go Normal file
View File

@ -0,0 +1,14 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, v2rayA Organization <team@v2raya.org>
*/
package config
import (
"testing"
)
func TestExportOutline(t *testing.T) {
t.Log(ExportOutlineJson("test"))
}

View File

@ -18,19 +18,18 @@ func StringListParser(to reflect.Value, section *config_parser.Section) error {
return fmt.Errorf("StringListParser can only unmarshal section to *[]string") return fmt.Errorf("StringListParser can only unmarshal section to *[]string")
} }
to = to.Elem() to = to.Elem()
if to.Type() != reflect.TypeOf([]string{}) { if to.Type() != reflect.TypeOf([]string{}) &&
!(to.Kind() == reflect.Slice && to.Type().Elem().Kind() == reflect.String) {
return fmt.Errorf("StringListParser can only unmarshal section to *[]string") return fmt.Errorf("StringListParser can only unmarshal section to *[]string")
} }
var list []string
for _, item := range section.Items { for _, item := range section.Items {
switch itemVal := item.Value.(type) { switch itemVal := item.Value.(type) {
case *config_parser.Param: case *config_parser.Param:
list = append(list, itemVal.String(true)) to.Set(reflect.Append(to, reflect.ValueOf(itemVal.String(true, false)).Convert(to.Type().Elem())))
default: default:
return fmt.Errorf("section %v does not support type %v: %v", section.Name, item.Type.String(), item.String()) return fmt.Errorf("section %v does not support type %v: %v", section.Name, item.Type.String(), item.String(false, false))
} }
} }
to.Set(reflect.ValueOf(list))
return nil return nil
} }
@ -85,7 +84,7 @@ func ParamParser(to reflect.Value, section *config_parser.Section, ignoreType []
switch itemVal := item.Value.(type) { switch itemVal := item.Value.(type) {
case *config_parser.Param: case *config_parser.Param:
if itemVal.Key == "" { if itemVal.Key == "" {
return fmt.Errorf("unsupported text without a key: %v", itemVal.String(true)) return fmt.Errorf("unsupported text without a key: %v", itemVal.String(true, false))
} }
field, ok := keyToField[itemVal.Key] field, ok := keyToField[itemVal.Key]
if !ok { if !ok {
@ -138,7 +137,7 @@ func ParamParser(to reflect.Value, section *config_parser.Section, ignoreType []
// Assign. "to" should have field "Rules". // Assign. "to" should have field "Rules".
structField, ok := to.Type().FieldByName("Rules") structField, ok := to.Type().FieldByName("Rules")
if !ok || structField.Type != reflect.TypeOf([]*config_parser.RoutingRule{}) { if !ok || structField.Type != reflect.TypeOf([]*config_parser.RoutingRule{}) {
return fmt.Errorf("unexpected type: \"routing rule\": %v", itemVal.String(true)) return fmt.Errorf("unexpected type: \"routing rule\": %v", itemVal.String(true, false, false))
} }
if structField.Tag.Get("mapstructure") != "_" { if structField.Tag.Get("mapstructure") != "_" {
return fmt.Errorf("a []*RoutingRule field \"Rules\" with mapstructure:\"_\" is required in struct %v to parse section", to.Type().String()) return fmt.Errorf("a []*RoutingRule field \"Rules\" with mapstructure:\"_\" is required in struct %v to parse section", to.Type().String())
@ -147,7 +146,7 @@ func ParamParser(to reflect.Value, section *config_parser.Section, ignoreType []
field.Set(reflect.Append(field, reflect.ValueOf(itemVal))) field.Set(reflect.Append(field, reflect.ValueOf(itemVal)))
default: default:
if _, ignore := ignoreTypeSet[reflect.TypeOf(itemVal)]; !ignore { if _, ignore := ignoreTypeSet[reflect.TypeOf(itemVal)]; !ignore {
return fmt.Errorf("unexpected type %v: %v", item.Type.String(), item.String()) return fmt.Errorf("unexpected type %v: %v", item.Type.String(), item.String(false, false))
} }
} }
} }

View File

@ -7,32 +7,17 @@ package config
import ( import (
"fmt" "fmt"
"github.com/sirupsen/logrus"
"github.com/v2rayA/dae/common/consts" "github.com/v2rayA/dae/common/consts"
) )
type patch func(params *Params) error type patch func(params *Config) error
var patches = []patch{ var patches = []patch{
patchRoutingFallback,
patchEmptyDns, patchEmptyDns,
patchDeprecatedGlobalDnsUpstream, patchDeprecatedGlobalDnsUpstream,
} }
func patchRoutingFallback(params *Params) error { func patchEmptyDns(params *Config) error {
// We renamed final as fallback. So we apply this patch for compatibility with older users.
if params.Routing.Fallback == nil && params.Routing.Final != nil {
params.Routing.Fallback = params.Routing.Final
logrus.Warnln("Name 'final' in section routing was deprecated and will be removed in the future; please rename it as 'fallback'")
}
// Fallback is required.
if params.Routing.Fallback == nil {
return fmt.Errorf("fallback is required in routing")
}
return nil
}
func patchEmptyDns(params *Params) error {
if params.Dns.Routing.Request.Fallback == nil { if params.Dns.Routing.Request.Fallback == nil {
params.Dns.Routing.Request.Fallback = consts.DnsRequestOutboundIndex_AsIs.String() params.Dns.Routing.Request.Fallback = consts.DnsRequestOutboundIndex_AsIs.String()
} }
@ -42,9 +27,10 @@ func patchEmptyDns(params *Params) error {
return nil return nil
} }
func patchDeprecatedGlobalDnsUpstream(params *Params) error { func patchDeprecatedGlobalDnsUpstream(params *Config) error {
if params.Global.DnsUpstream != "<empty>" { if params.Global.DnsUpstream != "<empty>" {
return fmt.Errorf("'global.dns_upstream' was deprecated, please refer to the latest examples and docs for help") return fmt.Errorf("'global.dns_upstream' was deprecated, please refer to the latest examples and docs for help")
} }
params.Global.DnsUpstream = ""
return nil return nil
} }

View File

@ -283,7 +283,7 @@ func NewControlPlane(
if log.IsLevelEnabled(logrus.DebugLevel) { if log.IsLevelEnabled(logrus.DebugLevel) {
var debugBuilder strings.Builder var debugBuilder strings.Builder
for _, rule := range rules { for _, rule := range rules {
debugBuilder.WriteString(rule.String(true) + "\n") debugBuilder.WriteString(rule.String(true, false, false) + "\n")
} }
log.Debugf("RoutingA:\n%vfallback: %v\n", debugBuilder.String(), routingA.Fallback) log.Debugf("RoutingA:\n%vfallback: %v\n", debugBuilder.String(), routingA.Fallback)
} }

View File

@ -26,8 +26,8 @@ global {
#lan_interface: docker0 #lan_interface: docker0
# SNAT for incoming connection to avoid MAC learning. # SNAT for incoming connection to avoid MAC learning.
# Set it true if you are NOT using dae as a transparent bridge, but will reduce forwarding # Always set it true if you are NOT using dae as a transparent bridge, but will reduce forwarding
# performance for direct traffic. # performance for direct traffic in LAN mode.
# This option does not affect direct traffic performance of WAN. # This option does not affect direct traffic performance of WAN.
lan_nat_direct: true lan_nat_direct: true
@ -82,13 +82,15 @@ dns {
alidns: 'udp://dns.alidns.com:53' alidns: 'udp://dns.alidns.com:53'
googledns: 'tcp+udp://dns.google:53' googledns: 'tcp+udp://dns.google:53'
} }
request { routing {
fallback: asis request {
} fallback: asis
response { }
upstream(googledns) -> accept response {
!qname(geosite:cn) && ip(geoip:private) -> googledns upstream(googledns) -> accept
fallback: accept !qname(geosite:cn) && ip(geoip:private) -> googledns
fallback: accept
}
} }
} }

View File

@ -58,17 +58,17 @@ type Item struct {
Value interface{} Value interface{}
} }
func (i *Item) String() string { func (i *Item) String(compact bool, quoteVal bool) string {
var builder strings.Builder var builder strings.Builder
builder.WriteString("type: " + i.Type.String() + "\n") builder.WriteString("type: " + i.Type.String() + "\n")
var content string var content string
switch val := i.Value.(type) { switch val := i.Value.(type) {
case *RoutingRule: case *RoutingRule:
content = val.String(false) content = val.String(false, compact, quoteVal)
case *Param: case *Param:
content = val.String(false) content = val.String(false, quoteVal)
case *Section: case *Section:
content = val.String() content = val.String(compact, quoteVal)
default: default:
return "<Unknown>\n" return "<Unknown>\n"
} }
@ -85,12 +85,12 @@ type Section struct {
Items []*Item Items []*Item
} }
func (s *Section) String() string { func (s *Section) String(compact bool, quoteVal bool) string {
var builder strings.Builder var builder strings.Builder
builder.WriteString("section: " + s.Name + "\n") builder.WriteString("section: " + s.Name + "\n")
var strItemList []string var strItemList []string
for _, item := range s.Items { for _, item := range s.Items {
lines := strings.Split(item.String(), "\n") lines := strings.Split(item.String(compact, quoteVal), "\n")
for i := range lines { for i := range lines {
lines[i] = "\t" + lines[i] lines[i] = "\t" + lines[i]
} }
@ -109,21 +109,27 @@ type Param struct {
AndFunctions []*Function AndFunctions []*Function
} }
func (p *Param) String(compact bool) string { func (p *Param) String(compact bool, quoteVal bool) string {
var quote func(string) string
if quoteVal {
quote = strconv.Quote
} else {
quote = func(s string) string { return s }
}
if p.Key == "" { if p.Key == "" {
return p.Val return quote(p.Val)
} }
if p.AndFunctions != nil { if p.AndFunctions != nil {
a := paramAndFunctions{ a := paramAndFunctions{
Key: p.Key, Key: p.Key,
AndFunctions: p.AndFunctions, AndFunctions: p.AndFunctions,
} }
return a.String(compact) return a.String(compact, quoteVal)
} }
if compact { if compact {
return p.Key + ":" + p.Val return p.Key + ":" + quote(p.Val)
} else { } else {
return p.Key + ": " + p.Val return p.Key + ": " + quote(p.Val)
} }
} }
@ -133,22 +139,25 @@ type Function struct {
Params []*Param Params []*Param
} }
func (f *Function) String(compact bool) string { func (f *Function) String(compact bool, quoteVal bool, omitEmpty bool) string {
var builder strings.Builder var builder strings.Builder
if f.Not { if f.Not {
builder.WriteString("!") builder.WriteString("!")
} }
builder.WriteString(f.Name + "(") builder.WriteString(f.Name)
var strParamList []string if !(omitEmpty && len(f.Params) == 0) {
for _, p := range f.Params { builder.WriteString("(")
strParamList = append(strParamList, p.String(compact)) var strParamList []string
for _, p := range f.Params {
strParamList = append(strParamList, p.String(compact, quoteVal))
}
if compact {
builder.WriteString(strings.Join(strParamList, ","))
} else {
builder.WriteString(strings.Join(strParamList, ", "))
}
builder.WriteString(")")
} }
if compact {
builder.WriteString(strings.Join(strParamList, ","))
} else {
builder.WriteString(strings.Join(strParamList, ", "))
}
builder.WriteString(")")
return builder.String() return builder.String()
} }
@ -157,7 +166,7 @@ type paramAndFunctions struct {
AndFunctions []*Function AndFunctions []*Function
} }
func (p *paramAndFunctions) String(compact bool) string { func (p *paramAndFunctions) String(compact bool, quoteVal bool) string {
var builder strings.Builder var builder strings.Builder
if compact { if compact {
builder.WriteString(p.Key + ":") builder.WriteString(p.Key + ":")
@ -166,7 +175,7 @@ func (p *paramAndFunctions) String(compact bool) string {
} }
var strFunctionList []string var strFunctionList []string
for _, f := range p.AndFunctions { for _, f := range p.AndFunctions {
strFunctionList = append(strFunctionList, f.String(compact)) strFunctionList = append(strFunctionList, f.String(compact, quoteVal, false))
} }
if compact { if compact {
builder.WriteString(strings.Join(strFunctionList, "&&")) builder.WriteString(strings.Join(strFunctionList, "&&"))
@ -181,12 +190,16 @@ type RoutingRule struct {
Outbound Function Outbound Function
} }
func (r *RoutingRule) String(replaceParamWithN bool) string { func (r *RoutingRule) String(replaceParamWithN bool, compact bool, quoteVal bool) string {
var builder strings.Builder var builder strings.Builder
var n int var n int
for i, f := range r.AndFunctions { for i, f := range r.AndFunctions {
if i != 0 { if i != 0 {
builder.WriteString(" && ") if compact {
builder.WriteString("&&")
} else {
builder.WriteString(" && ")
}
} }
var paramBuilder strings.Builder var paramBuilder strings.Builder
n += len(f.Params) n += len(f.Params)
@ -195,9 +208,13 @@ func (r *RoutingRule) String(replaceParamWithN bool) string {
} else { } else {
for j, param := range f.Params { for j, param := range f.Params {
if j != 0 { if j != 0 {
paramBuilder.WriteString(", ") if compact {
paramBuilder.WriteString(",")
} else {
paramBuilder.WriteString(", ")
}
} }
paramBuilder.WriteString(param.String(false)) paramBuilder.WriteString(param.String(compact, quoteVal))
} }
} }
symNot := "" symNot := ""
@ -206,6 +223,10 @@ func (r *RoutingRule) String(replaceParamWithN bool) string {
} }
builder.WriteString(fmt.Sprintf("%v%v(%v)", symNot, f.Name, paramBuilder.String())) builder.WriteString(fmt.Sprintf("%v%v(%v)", symNot, f.Name, paramBuilder.String()))
} }
builder.WriteString(" -> " + r.Outbound.String(true)) if compact {
builder.WriteString("->" + r.Outbound.String(compact, quoteVal, true))
} else {
builder.WriteString(" -> " + r.Outbound.String(compact, quoteVal, true))
}
return builder.String() return builder.String()
} }

View File

@ -62,8 +62,8 @@ func (p *paramParser) parseParam(ctx *dae_config.ParameterContext) *Param {
func (p *paramParser) parseNonEmptyParamList(ctx *dae_config.NonEmptyParameterListContext) { func (p *paramParser) parseNonEmptyParamList(ctx *dae_config.NonEmptyParameterListContext) {
children := ctx.GetChildren() children := ctx.GetChildren()
if len(children) == 3 { if len(children) == 3 {
p.list = append(p.list, p.parseParam(children[2].(*dae_config.ParameterContext)))
p.parseNonEmptyParamList(children[0].(*dae_config.NonEmptyParameterListContext)) p.parseNonEmptyParamList(children[0].(*dae_config.NonEmptyParameterListContext))
p.list = append(p.list, p.parseParam(children[2].(*dae_config.ParameterContext)))
} else if len(children) == 1 { } else if len(children) == 1 {
p.list = append(p.list, p.parseParam(children[0].(*dae_config.ParameterContext))) p.list = append(p.list, p.parseParam(children[0].(*dae_config.ParameterContext)))
} }