2024-08-05 19:06:48 +00:00
|
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package vnet
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"cmp"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/netip"
|
2024-08-09 03:11:50 +00:00
|
|
|
|
"os"
|
2024-08-05 19:06:48 +00:00
|
|
|
|
"slices"
|
|
|
|
|
|
2024-08-09 03:11:50 +00:00
|
|
|
|
"github.com/google/gopacket/layers"
|
|
|
|
|
"github.com/google/gopacket/pcapgo"
|
2024-08-08 17:36:07 +00:00
|
|
|
|
"tailscale.com/types/logger"
|
2024-08-09 04:29:09 +00:00
|
|
|
|
"tailscale.com/util/must"
|
2024-08-05 19:06:48 +00:00
|
|
|
|
"tailscale.com/util/set"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Note: the exported Node and Network are the configuration types;
|
|
|
|
|
// the unexported node and network are the runtime types that are actually
|
|
|
|
|
// used once the server is created.
|
|
|
|
|
|
|
|
|
|
// Config is the requested state of the natlab virtual network.
|
|
|
|
|
//
|
|
|
|
|
// The zero value is a valid empty configuration. Call AddNode
|
|
|
|
|
// and AddNetwork to methods on the returned Node and Network
|
|
|
|
|
// values to modify the config before calling NewServer.
|
|
|
|
|
// Once the NewServer is called, Config is no longer used.
|
|
|
|
|
type Config struct {
|
2024-08-23 18:37:19 +00:00
|
|
|
|
nodes []*Node
|
|
|
|
|
networks []*Network
|
|
|
|
|
pcapFile string
|
|
|
|
|
blendReality bool
|
2024-08-09 03:11:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 18:37:19 +00:00
|
|
|
|
// SetPCAPFile sets the filename to write a pcap file to,
|
|
|
|
|
// or empty to disable pcap file writing.
|
2024-08-09 03:11:50 +00:00
|
|
|
|
func (c *Config) SetPCAPFile(file string) {
|
|
|
|
|
c.pcapFile = file
|
2024-08-05 19:06:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 18:37:19 +00:00
|
|
|
|
// NumNodes returns the number of nodes in the configuration.
|
2024-08-07 00:33:45 +00:00
|
|
|
|
func (c *Config) NumNodes() int {
|
|
|
|
|
return len(c.nodes)
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-23 18:37:19 +00:00
|
|
|
|
// SetBlendReality sets whether to blend the real controlplane.tailscale.com and
|
|
|
|
|
// DERP servers into the virtual network. This is mostly useful for interactive
|
|
|
|
|
// testing when working on natlab.
|
|
|
|
|
func (c *Config) SetBlendReality(v bool) {
|
|
|
|
|
c.blendReality = v
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FirstNetwork returns the first network in the config, or nil if none.
|
2024-08-08 21:11:07 +00:00
|
|
|
|
func (c *Config) FirstNetwork() *Network {
|
|
|
|
|
if len(c.networks) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return c.networks[0]
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 05:21:14 +00:00
|
|
|
|
func nodeMac(n int) MAC {
|
|
|
|
|
// 52=TS then 0xcc for cccclient
|
|
|
|
|
return MAC{0x52, 0xcc, 0xcc, 0xcc, 0xcc, byte(n)}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-05 19:06:48 +00:00
|
|
|
|
// AddNode creates a new node in the world.
|
|
|
|
|
//
|
|
|
|
|
// The opts may be of the following types:
|
|
|
|
|
// - *Network: zero, one, or more networks to add this node to
|
|
|
|
|
// - TODO: more
|
|
|
|
|
//
|
|
|
|
|
// On an error or unknown opt type, AddNode returns a
|
|
|
|
|
// node with a carried error that gets returned later.
|
|
|
|
|
func (c *Config) AddNode(opts ...any) *Node {
|
2024-08-27 05:21:14 +00:00
|
|
|
|
num := len(c.nodes) + 1
|
2024-08-05 19:06:48 +00:00
|
|
|
|
n := &Node{
|
2024-08-27 05:21:14 +00:00
|
|
|
|
num: num,
|
|
|
|
|
mac: nodeMac(num),
|
2024-08-05 19:06:48 +00:00
|
|
|
|
}
|
|
|
|
|
c.nodes = append(c.nodes, n)
|
|
|
|
|
for _, o := range opts {
|
|
|
|
|
switch o := o.(type) {
|
|
|
|
|
case *Network:
|
|
|
|
|
if !slices.Contains(o.nodes, n) {
|
|
|
|
|
o.nodes = append(o.nodes, n)
|
|
|
|
|
}
|
|
|
|
|
n.nets = append(n.nets, o)
|
2024-08-10 20:46:47 +00:00
|
|
|
|
case TailscaledEnv:
|
|
|
|
|
n.env = append(n.env, o)
|
|
|
|
|
case NodeOption:
|
2024-08-13 04:39:28 +00:00
|
|
|
|
switch o {
|
|
|
|
|
case HostFirewall:
|
2024-08-10 20:46:47 +00:00
|
|
|
|
n.hostFW = true
|
2024-08-13 04:39:28 +00:00
|
|
|
|
case VerboseSyslog:
|
|
|
|
|
n.verboseSyslog = true
|
|
|
|
|
default:
|
2024-08-10 20:46:47 +00:00
|
|
|
|
if n.err == nil {
|
|
|
|
|
n.err = fmt.Errorf("unknown NodeOption %q", o)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-05 19:06:48 +00:00
|
|
|
|
default:
|
|
|
|
|
if n.err == nil {
|
|
|
|
|
n.err = fmt.Errorf("unknown AddNode option type %T", o)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-10 20:46:47 +00:00
|
|
|
|
// NodeOption is an option that can be passed to Config.AddNode.
|
|
|
|
|
type NodeOption string
|
|
|
|
|
|
|
|
|
|
const (
|
2024-08-13 04:39:28 +00:00
|
|
|
|
HostFirewall NodeOption = "HostFirewall"
|
|
|
|
|
VerboseSyslog NodeOption = "VerboseSyslog"
|
2024-08-10 20:46:47 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TailscaledEnv is а option that can be passed to Config.AddNode
|
|
|
|
|
// to set an environment variable for tailscaled.
|
|
|
|
|
type TailscaledEnv struct {
|
|
|
|
|
Key, Value string
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-05 19:06:48 +00:00
|
|
|
|
// AddNetwork add a new network.
|
|
|
|
|
//
|
|
|
|
|
// The opts may be of the following types:
|
|
|
|
|
// - string IP address, for the network's WAN IP (if any)
|
|
|
|
|
// - string netip.Prefix, for the network's LAN IP (defaults to 192.168.0.0/24)
|
|
|
|
|
// - NAT, the type of NAT to use
|
|
|
|
|
// - NetworkService, a service to add to the network
|
|
|
|
|
//
|
|
|
|
|
// On an error or unknown opt type, AddNetwork returns a
|
|
|
|
|
// network with a carried error that gets returned later.
|
|
|
|
|
func (c *Config) AddNetwork(opts ...any) *Network {
|
2024-08-27 05:21:14 +00:00
|
|
|
|
num := len(c.networks) + 1
|
2024-08-05 19:06:48 +00:00
|
|
|
|
n := &Network{
|
2024-08-27 05:21:14 +00:00
|
|
|
|
num: num,
|
|
|
|
|
mac: MAC{0x52, 0xee, 0xee, 0xee, 0xee, byte(num)}, // 52=TS then 0xee for 'etwork
|
2024-08-05 19:06:48 +00:00
|
|
|
|
}
|
|
|
|
|
c.networks = append(c.networks, n)
|
|
|
|
|
for _, o := range opts {
|
|
|
|
|
switch o := o.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
if ip, err := netip.ParseAddr(o); err == nil {
|
2024-08-14 04:09:53 +00:00
|
|
|
|
n.wanIP4 = ip
|
2024-08-05 19:06:48 +00:00
|
|
|
|
} else if ip, err := netip.ParsePrefix(o); err == nil {
|
2024-08-14 04:09:53 +00:00
|
|
|
|
// If the prefix is IPv4, treat it as the router's internal IPv4 address + CIDR.
|
|
|
|
|
// If the prefix is IPv6, treat it as the router's WAN IPv6 + CIDR (typically a /64).
|
|
|
|
|
if ip.Addr().Is4() {
|
|
|
|
|
n.lanIP4 = ip
|
|
|
|
|
} else if ip.Addr().Is6() {
|
|
|
|
|
n.wanIP6 = ip
|
|
|
|
|
}
|
2024-08-05 19:06:48 +00:00
|
|
|
|
} else {
|
|
|
|
|
if n.err == nil {
|
|
|
|
|
n.err = fmt.Errorf("unknown string option %q", o)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case NAT:
|
|
|
|
|
n.natType = o
|
|
|
|
|
case NetworkService:
|
|
|
|
|
n.AddService(o)
|
|
|
|
|
default:
|
|
|
|
|
if n.err == nil {
|
|
|
|
|
n.err = fmt.Errorf("unknown AddNetwork option type %T", o)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node is the configuration of a node in the virtual network.
|
|
|
|
|
type Node struct {
|
|
|
|
|
err error
|
2024-08-13 15:20:43 +00:00
|
|
|
|
num int // 1-based node number
|
2024-08-05 19:06:48 +00:00
|
|
|
|
n *node // nil until NewServer called
|
|
|
|
|
|
2024-08-13 04:39:28 +00:00
|
|
|
|
env []TailscaledEnv
|
|
|
|
|
hostFW bool
|
|
|
|
|
verboseSyslog bool
|
2024-08-10 20:46:47 +00:00
|
|
|
|
|
2024-08-05 19:06:48 +00:00
|
|
|
|
// TODO(bradfitz): this is halfway converted to supporting multiple NICs
|
|
|
|
|
// but not done. We need a MAC-per-Network.
|
|
|
|
|
|
|
|
|
|
mac MAC
|
|
|
|
|
nets []*Network
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-13 15:20:43 +00:00
|
|
|
|
// Num returns the 1-based node number.
|
|
|
|
|
func (n *Node) Num() int {
|
|
|
|
|
return n.num
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// String returns the string "nodeN" where N is the 1-based node number.
|
|
|
|
|
func (n *Node) String() string {
|
|
|
|
|
return fmt.Sprintf("node%d", n.num)
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-07 00:33:45 +00:00
|
|
|
|
// MAC returns the MAC address of the node.
|
|
|
|
|
func (n *Node) MAC() MAC {
|
|
|
|
|
return n.mac
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-10 20:46:47 +00:00
|
|
|
|
func (n *Node) Env() []TailscaledEnv {
|
|
|
|
|
return n.env
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Node) HostFirewall() bool {
|
|
|
|
|
return n.hostFW
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-13 04:39:28 +00:00
|
|
|
|
func (n *Node) VerboseSyslog() bool {
|
|
|
|
|
return n.verboseSyslog
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Node) SetVerboseSyslog(v bool) {
|
|
|
|
|
n.verboseSyslog = v
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-14 04:09:53 +00:00
|
|
|
|
// IsV6Only reports whether this node is only connected to IPv6 networks.
|
|
|
|
|
func (n *Node) IsV6Only() bool {
|
|
|
|
|
for _, net := range n.nets {
|
|
|
|
|
if net.CanV4() {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, net := range n.nets {
|
|
|
|
|
if net.CanV6() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-05 19:06:48 +00:00
|
|
|
|
// Network returns the first network this node is connected to,
|
|
|
|
|
// or nil if none.
|
|
|
|
|
func (n *Node) Network() *Network {
|
|
|
|
|
if len(n.nets) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return n.nets[0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Network is the configuration of a network in the virtual network.
|
|
|
|
|
type Network struct {
|
2024-08-14 04:09:53 +00:00
|
|
|
|
num int // 1-based
|
2024-08-05 19:06:48 +00:00
|
|
|
|
mac MAC // MAC address of the router/gateway
|
|
|
|
|
natType NAT
|
|
|
|
|
|
2024-08-14 04:09:53 +00:00
|
|
|
|
wanIP6 netip.Prefix // global unicast router in host bits; CIDR is /64 delegated to LAN
|
|
|
|
|
|
|
|
|
|
wanIP4 netip.Addr // IPv4 WAN IP, if any
|
|
|
|
|
lanIP4 netip.Prefix
|
|
|
|
|
nodes []*Node
|
2024-08-05 19:06:48 +00:00
|
|
|
|
|
|
|
|
|
svcs set.Set[NetworkService]
|
|
|
|
|
|
|
|
|
|
// ...
|
|
|
|
|
err error // carried error
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-14 04:09:53 +00:00
|
|
|
|
func (n *Network) CanV4() bool {
|
|
|
|
|
return n.lanIP4.IsValid() || n.wanIP4.IsValid()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Network) CanV6() bool {
|
|
|
|
|
return n.wanIP6.IsValid()
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-08 21:11:07 +00:00
|
|
|
|
func (n *Network) CanTakeMoreNodes() bool {
|
|
|
|
|
if n.natType == One2OneNAT {
|
|
|
|
|
return len(n.nodes) == 0
|
|
|
|
|
}
|
|
|
|
|
return len(n.nodes) < 150
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-05 19:06:48 +00:00
|
|
|
|
// NetworkService is a service that can be added to a network.
|
|
|
|
|
type NetworkService string
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
NATPMP NetworkService = "NAT-PMP"
|
|
|
|
|
PCP NetworkService = "PCP"
|
|
|
|
|
UPnP NetworkService = "UPnP"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AddService adds a network service (such as port mapping protocols) to a
|
|
|
|
|
// network.
|
|
|
|
|
func (n *Network) AddService(s NetworkService) {
|
|
|
|
|
if n.svcs == nil {
|
|
|
|
|
n.svcs = set.Of(s)
|
|
|
|
|
} else {
|
|
|
|
|
n.svcs.Add(s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// initFromConfig initializes the server from the previous calls
|
|
|
|
|
// to NewNode and NewNetwork and returns an error if
|
|
|
|
|
// there were any configuration issues.
|
|
|
|
|
func (s *Server) initFromConfig(c *Config) error {
|
|
|
|
|
netOfConf := map[*Network]*network{}
|
2024-08-09 03:11:50 +00:00
|
|
|
|
if c.pcapFile != "" {
|
|
|
|
|
pcf, err := os.OpenFile(c.pcapFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
nw, err := pcapgo.NewNgWriter(pcf, layers.LinkTypeEthernet)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
pw := &pcapWriter{
|
|
|
|
|
f: pcf,
|
|
|
|
|
w: nw,
|
|
|
|
|
}
|
|
|
|
|
s.pcapWriter = pw
|
|
|
|
|
}
|
2024-08-09 04:29:09 +00:00
|
|
|
|
for i, conf := range c.networks {
|
2024-08-05 19:06:48 +00:00
|
|
|
|
if conf.err != nil {
|
|
|
|
|
return conf.err
|
|
|
|
|
}
|
2024-08-14 04:09:53 +00:00
|
|
|
|
if !conf.lanIP4.IsValid() && !conf.wanIP6.IsValid() {
|
|
|
|
|
conf.lanIP4 = netip.MustParsePrefix("192.168.0.0/24")
|
2024-08-05 19:06:48 +00:00
|
|
|
|
}
|
|
|
|
|
n := &network{
|
2024-08-14 04:09:53 +00:00
|
|
|
|
num: conf.num,
|
|
|
|
|
s: s,
|
|
|
|
|
mac: conf.mac,
|
|
|
|
|
portmap: conf.svcs.Contains(NATPMP), // TODO: expand network.portmap
|
|
|
|
|
wanIP6: conf.wanIP6,
|
|
|
|
|
v4: conf.lanIP4.IsValid(),
|
|
|
|
|
v6: conf.wanIP6.IsValid(),
|
|
|
|
|
wanIP4: conf.wanIP4,
|
|
|
|
|
lanIP4: conf.lanIP4,
|
2024-08-26 03:50:22 +00:00
|
|
|
|
nodesByIP4: map[netip.Addr]*node{},
|
2024-08-14 04:09:53 +00:00
|
|
|
|
nodesByMAC: map[MAC]*node{},
|
2024-08-27 05:21:14 +00:00
|
|
|
|
logf: logger.WithPrefix(s.logf, fmt.Sprintf("[net-%v] ", conf.mac)),
|
2024-08-05 19:06:48 +00:00
|
|
|
|
}
|
|
|
|
|
netOfConf[conf] = n
|
|
|
|
|
s.networks.Add(n)
|
2024-08-14 04:09:53 +00:00
|
|
|
|
if conf.wanIP4.IsValid() {
|
|
|
|
|
if conf.wanIP4.Is6() {
|
|
|
|
|
return fmt.Errorf("invalid IPv6 address in wanIP")
|
|
|
|
|
}
|
|
|
|
|
if _, ok := s.networkByWAN.Lookup(conf.wanIP4); ok {
|
|
|
|
|
return fmt.Errorf("two networks have the same WAN IP %v; Anycast not (yet?) supported", conf.wanIP4)
|
|
|
|
|
}
|
|
|
|
|
s.networkByWAN.Insert(netip.PrefixFrom(conf.wanIP4, 32), n)
|
|
|
|
|
}
|
|
|
|
|
if conf.wanIP6.IsValid() {
|
|
|
|
|
if conf.wanIP6.Addr().Is4() {
|
|
|
|
|
return fmt.Errorf("invalid IPv4 address in wanIP6")
|
|
|
|
|
}
|
|
|
|
|
if _, ok := s.networkByWAN.LookupPrefix(conf.wanIP6); ok {
|
|
|
|
|
return fmt.Errorf("two networks have the same WAN IPv6 %v; Anycast not (yet?) supported", conf.wanIP6)
|
|
|
|
|
}
|
|
|
|
|
s.networkByWAN.Insert(conf.wanIP6, n)
|
2024-08-05 19:06:48 +00:00
|
|
|
|
}
|
2024-08-09 04:29:09 +00:00
|
|
|
|
n.lanInterfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
|
|
|
|
|
Name: fmt.Sprintf("network%d-lan", i+1),
|
|
|
|
|
LinkType: layers.LinkTypeIPv4,
|
|
|
|
|
}))
|
|
|
|
|
n.wanInterfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
|
|
|
|
|
Name: fmt.Sprintf("network%d-wan", i+1),
|
|
|
|
|
LinkType: layers.LinkTypeIPv4,
|
|
|
|
|
}))
|
2024-08-05 19:06:48 +00:00
|
|
|
|
}
|
2024-08-13 15:20:43 +00:00
|
|
|
|
for _, conf := range c.nodes {
|
2024-08-05 19:06:48 +00:00
|
|
|
|
if conf.err != nil {
|
|
|
|
|
return conf.err
|
|
|
|
|
}
|
|
|
|
|
n := &node{
|
2024-08-13 15:20:43 +00:00
|
|
|
|
num: conf.num,
|
2024-08-13 04:39:28 +00:00
|
|
|
|
mac: conf.mac,
|
|
|
|
|
net: netOfConf[conf.Network()],
|
|
|
|
|
verboseSyslog: conf.VerboseSyslog(),
|
2024-08-05 19:06:48 +00:00
|
|
|
|
}
|
2024-08-09 04:29:09 +00:00
|
|
|
|
n.interfaceID = must.Get(s.pcapWriter.AddInterface(pcapgo.NgInterface{
|
2024-08-13 15:20:43 +00:00
|
|
|
|
Name: n.String(),
|
2024-08-09 04:29:09 +00:00
|
|
|
|
LinkType: layers.LinkTypeEthernet,
|
|
|
|
|
}))
|
2024-08-05 19:06:48 +00:00
|
|
|
|
conf.n = n
|
|
|
|
|
if _, ok := s.nodeByMAC[n.mac]; ok {
|
|
|
|
|
return fmt.Errorf("two nodes have the same MAC %v", n.mac)
|
|
|
|
|
}
|
|
|
|
|
s.nodes = append(s.nodes, n)
|
|
|
|
|
s.nodeByMAC[n.mac] = n
|
|
|
|
|
|
2024-08-14 04:09:53 +00:00
|
|
|
|
if n.net.v4 {
|
|
|
|
|
// Allocate a lanIP for the node. Use the network's CIDR and use final
|
|
|
|
|
// octet 101 (for first node), 102, etc. The node number comes from the
|
|
|
|
|
// last octent of the MAC address (0-based)
|
|
|
|
|
ip4 := n.net.lanIP4.Addr().As4()
|
|
|
|
|
ip4[3] = 100 + n.mac[5]
|
|
|
|
|
n.lanIP = netip.AddrFrom4(ip4)
|
2024-08-26 03:50:22 +00:00
|
|
|
|
n.net.nodesByIP4[n.lanIP] = n
|
2024-08-14 04:09:53 +00:00
|
|
|
|
}
|
|
|
|
|
n.net.nodesByMAC[n.mac] = n
|
2024-08-05 19:06:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now that nodes are populated, set up NAT:
|
|
|
|
|
for _, conf := range c.networks {
|
|
|
|
|
n := netOfConf[conf]
|
|
|
|
|
natType := cmp.Or(conf.natType, EasyNAT)
|
|
|
|
|
if err := n.InitNAT(natType); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|