tor: streamline package to better follow the Effective Go guidelines
In this commit, we clean up the tor package to better follow the Effective Go guidelines. Most of the changes revolve around naming, where we'd have things like `torsvc.TorDial`. This was simplified to `tor.Dial` along with many others.
This commit is contained in:
parent
3bc026aece
commit
43cbd5a814
15
tor/README.md
Normal file
15
tor/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
tor
|
||||
===
|
||||
|
||||
The tor package contains utility functions that allow for interacting with the
|
||||
Tor daemon. So far, supported functions include routing all traffic over Tor's
|
||||
exposed socks5 proxy and routing DNS queries over Tor (A, AAAA, SRV). In the
|
||||
future more features will be added: automatic setup of v2 hidden service
|
||||
functionality, control port functionality, and handling manually setup v3 hidden
|
||||
services.
|
||||
|
||||
## Installation and Updating
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/lightningnetwork/lnd/tor
|
||||
```
|
104
tor/net.go
Normal file
104
tor/net.go
Normal file
@ -0,0 +1,104 @@
|
||||
package tor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
// TODO: this interface and its implementations should ideally be moved
|
||||
// elsewhere as they are not Tor-specific.
|
||||
|
||||
// Net is an interface housing a Dial function and several DNS functions that
|
||||
// allows us to abstract the implementations of these functions over different
|
||||
// networks, e.g. clearnet, Tor net, etc.
|
||||
type Net interface {
|
||||
// Dial connects to the address on the named network.
|
||||
Dial(network, address string) (net.Conn, error)
|
||||
|
||||
// LookupHost performs DNS resolution on a given host and returns its
|
||||
// addresses.
|
||||
LookupHost(host string) ([]string, error)
|
||||
|
||||
// LookupSRV tries to resolve an SRV query of the given service,
|
||||
// protocol, and domain name.
|
||||
LookupSRV(service, proto, name string) (string, []*net.SRV, error)
|
||||
|
||||
// ResolveTCPAddr resolves TCP addresses.
|
||||
ResolveTCPAddr(network, address string) (*net.TCPAddr, error)
|
||||
}
|
||||
|
||||
// ClearNet is an implementation of the Net interface that defines behaviour
|
||||
// for regular network connections.
|
||||
type ClearNet struct{}
|
||||
|
||||
// Dial on the regular network uses net.Dial
|
||||
func (r *ClearNet) Dial(network, address string) (net.Conn, error) {
|
||||
return net.Dial(network, address)
|
||||
}
|
||||
|
||||
// LookupHost for regular network uses the net.LookupHost function
|
||||
func (r *ClearNet) LookupHost(host string) ([]string, error) {
|
||||
return net.LookupHost(host)
|
||||
}
|
||||
|
||||
// LookupSRV for regular network uses net.LookupSRV function
|
||||
func (r *ClearNet) LookupSRV(service, proto, name string) (string, []*net.SRV, error) {
|
||||
return net.LookupSRV(service, proto, name)
|
||||
}
|
||||
|
||||
// ResolveTCPAddr for regular network uses net.ResolveTCPAddr function
|
||||
func (r *ClearNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) {
|
||||
return net.ResolveTCPAddr(network, address)
|
||||
}
|
||||
|
||||
// ProxyNet is an implementation of the Net interface that defines behaviour
|
||||
// for Tor network connections.
|
||||
type ProxyNet struct {
|
||||
// SOCKS is the host:port which Tor's exposed SOCKS5 proxy is listening
|
||||
// on.
|
||||
SOCKS string
|
||||
|
||||
// DNS is the host:port of the DNS server for Tor to use for SRV
|
||||
// queries.
|
||||
DNS string
|
||||
|
||||
// StreamIsolation is a bool that determines if we should force the
|
||||
// creation of a new circuit for this connection. If true, then this
|
||||
// means that our traffic may be harder to correlate as each connection
|
||||
// will now use a distinct circuit.
|
||||
StreamIsolation bool
|
||||
}
|
||||
|
||||
// Dial uses the Tor Dial function in order to establish connections through
|
||||
// Tor. Since Tor only supports TCP connections, only TCP networks are allowed.
|
||||
func (p *ProxyNet) Dial(network, address string) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
default:
|
||||
return nil, errors.New("cannot dial non-tcp network via Tor")
|
||||
}
|
||||
return Dial(address, p.SOCKS, p.StreamIsolation)
|
||||
}
|
||||
|
||||
// LookupHost uses the Tor LookupHost function in order to resolve hosts over
|
||||
// Tor.
|
||||
func (p *ProxyNet) LookupHost(host string) ([]string, error) {
|
||||
return LookupHost(host, p.SOCKS)
|
||||
}
|
||||
|
||||
// LookupSRV uses the Tor LookupSRV function in order to resolve SRV DNS queries
|
||||
// over Tor.
|
||||
func (p *ProxyNet) LookupSRV(service, proto, name string) (string, []*net.SRV, error) {
|
||||
return LookupSRV(service, proto, name, p.SOCKS, p.DNS, p.StreamIsolation)
|
||||
}
|
||||
|
||||
// ResolveTCPAddr uses the Tor ResolveTCPAddr function in order to resolve TCP
|
||||
// addresses over Tor.
|
||||
func (p *ProxyNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
default:
|
||||
return nil, errors.New("cannot dial non-tcp network via Tor")
|
||||
}
|
||||
return ResolveTCPAddr(address, p.SOCKS)
|
||||
}
|
160
tor/tor.go
Normal file
160
tor/tor.go
Normal file
@ -0,0 +1,160 @@
|
||||
package tor
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/roasbeef/btcd/connmgr"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
var (
|
||||
// dnsCodes maps the DNS response codes to a friendly description. This
|
||||
// does not include the BADVERS code because of duplicate keys and the
|
||||
// underlying DNS (miekg/dns) package not using it. For more info, see
|
||||
// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml.
|
||||
dnsCodes = map[int]string{
|
||||
0: "no error",
|
||||
1: "format error",
|
||||
2: "server failure",
|
||||
3: "non-existent domain",
|
||||
4: "not implemented",
|
||||
5: "query refused",
|
||||
6: "name exists when it should not",
|
||||
7: "RR set exists when it should not",
|
||||
8: "RR set that should exist does not",
|
||||
9: "server not authoritative for zone",
|
||||
10: "name not contained in zone",
|
||||
16: "TSIG signature failure",
|
||||
17: "key not recognized",
|
||||
18: "signature out of time window",
|
||||
19: "bad TKEY mode",
|
||||
20: "duplicate key name",
|
||||
21: "algorithm not supported",
|
||||
22: "bad truncation",
|
||||
23: "bad/missing server cookie",
|
||||
}
|
||||
)
|
||||
|
||||
// Dial establishes a connection to the address via Tor's SOCKS proxy. Only TCP
|
||||
// is supported over Tor. The final argument determines if we should force
|
||||
// stream isolation for this new connection. If we do, then this means this new
|
||||
// connection will use a fresh circuit, rather than possibly re-using an
|
||||
// existing circuit.
|
||||
func Dial(address, socksAddr string, streamIsolation bool) (net.Conn, error) {
|
||||
// If we were requested to force stream isolation for this connection,
|
||||
// we'll populate the authentication credentials with random data as
|
||||
// Tor will create a new circuit for each set of credentials.
|
||||
var auth *proxy.Auth
|
||||
if streamIsolation {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth = &proxy.Auth{
|
||||
User: hex.EncodeToString(b[:8]),
|
||||
Password: hex.EncodeToString(b[8:]),
|
||||
}
|
||||
}
|
||||
|
||||
// Establish the connection through Tor's SOCKS proxy.
|
||||
dialer, err := proxy.SOCKS5("tcp", socksAddr, auth, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dialer.Dial("tcp", address)
|
||||
}
|
||||
|
||||
// LookupHost performs DNS resolution on a given host via Tor's native resolver.
|
||||
// Only IPv4 addresses are returned.
|
||||
func LookupHost(host, socksAddr string) ([]string, error) {
|
||||
ip, err := connmgr.TorLookupIP(host, socksAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Only one IPv4 address is returned by the TorLookupIP function.
|
||||
return []string{ip[0].String()}, nil
|
||||
}
|
||||
|
||||
// LookupSRV uses Tor's SOCKS proxy to route DNS SRV queries. Tor does not
|
||||
// natively support SRV queries so we must route all SRV queries through the
|
||||
// proxy by connecting directly to a DNS server and querying it. The DNS server
|
||||
// must have TCP resolution enabled for the given port.
|
||||
func LookupSRV(service, proto, name, socksAddr, dnsServer string,
|
||||
streamIsolation bool) (string, []*net.SRV, error) {
|
||||
|
||||
// Connect to the DNS server we'll be using to query SRV records.
|
||||
conn, err := Dial(dnsServer, socksAddr, streamIsolation)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
dnsConn := &dns.Conn{Conn: conn}
|
||||
defer dnsConn.Close()
|
||||
|
||||
// Once connected, we'll construct the SRV request for the host
|
||||
// following the format _service._proto.name. as described in RFC #2782.
|
||||
host := fmt.Sprintf("_%s._%s.%s.", service, proto, name)
|
||||
msg := new(dns.Msg).SetQuestion(host, dns.TypeSRV)
|
||||
|
||||
// Send the request to the DNS server and read its response.
|
||||
if err := dnsConn.WriteMsg(msg); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
resp, err := dnsConn.ReadMsg()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// We'll fail if we were unable to query the DNS server for our record.
|
||||
if resp.Rcode != dns.RcodeSuccess {
|
||||
return "", nil, fmt.Errorf("unable to query for SRV records: "+
|
||||
"%s", dnsCodes[resp.Rcode])
|
||||
}
|
||||
|
||||
// Retrieve the RR(s) of the Answer section.
|
||||
var rrs []*net.SRV
|
||||
for _, rr := range resp.Answer {
|
||||
srv := rr.(*dns.SRV)
|
||||
rrs = append(rrs, &net.SRV{
|
||||
Target: srv.Target,
|
||||
Port: srv.Port,
|
||||
Priority: srv.Priority,
|
||||
Weight: srv.Weight,
|
||||
})
|
||||
}
|
||||
|
||||
return "", rrs, nil
|
||||
}
|
||||
|
||||
// ResolveTCPAddr uses Tor's proxy to resolve TCP addresses instead of the
|
||||
// standard system resolver provided in the `net` package.
|
||||
func ResolveTCPAddr(address, socksAddr string) (*net.TCPAddr, error) {
|
||||
// Split host:port since the lookup function does not take a port.
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ip, err := LookupHost(host, socksAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &net.TCPAddr{
|
||||
IP: net.ParseIP(ip[0]),
|
||||
Port: p,
|
||||
}, nil
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
torsvc
|
||||
==========
|
||||
|
||||
The torsvc package contains utility functions that allow for interacting
|
||||
with the Tor daemon. So far, supported functions include routing all traffic
|
||||
over Tor's exposed socks5 proxy and routing DNS queries over Tor (A, AAAA, SRV).
|
||||
In the future more features will be added: automatic setup of v2 hidden service
|
||||
functionality, control port functionality, and handling manually setup v3 hidden
|
||||
services.
|
||||
|
||||
## Installation and Updating
|
||||
|
||||
```bash
|
||||
$ go get -u github.com/lightningnetwork/lnd/torsvc
|
||||
```
|
@ -1,24 +0,0 @@
|
||||
package torsvc
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Net is an interface housing a Dial function and several DNS functions, to
|
||||
// abstract the implementation of these functions over both Regular and Tor
|
||||
type Net interface {
|
||||
// Dial accepts a network and address and returns a connection to a remote
|
||||
// peer.
|
||||
Dial(string, string) (net.Conn, error)
|
||||
|
||||
// LookupHost performs DNS resolution on a given hostname and returns
|
||||
// addresses of that hostname
|
||||
LookupHost(string) ([]string, error)
|
||||
|
||||
// LookupSRV allows a service and network to be specified and makes queries
|
||||
// to a given DNS server for SRV queries.
|
||||
LookupSRV(string, string, string) (string, []*net.SRV, error)
|
||||
|
||||
// ResolveTCPAddr is a used to resolve publicly advertised TCP addresses.
|
||||
ResolveTCPAddr(string, string) (*net.TCPAddr, error)
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package torsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// RegularNet is an implementation of the Net interface that defines behaviour
|
||||
// for Regular network connections
|
||||
type RegularNet struct{}
|
||||
|
||||
// Dial on the regular network uses net.Dial
|
||||
func (r *RegularNet) Dial(network, address string) (net.Conn, error) {
|
||||
return net.Dial(network, address)
|
||||
}
|
||||
|
||||
// LookupHost for regular network uses the net.LookupHost function
|
||||
func (r *RegularNet) LookupHost(host string) ([]string, error) {
|
||||
return net.LookupHost(host)
|
||||
}
|
||||
|
||||
// LookupSRV for regular network uses net.LookupSRV function
|
||||
func (r *RegularNet) LookupSRV(service, proto, name string) (string, []*net.SRV, error) {
|
||||
return net.LookupSRV(service, proto, name)
|
||||
}
|
||||
|
||||
// ResolveTCPAddr for regular network uses net.ResolveTCPAddr function
|
||||
func (r *RegularNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) {
|
||||
return net.ResolveTCPAddr(network, address)
|
||||
}
|
||||
|
||||
// TorProxyNet is an implementation of the Net interface that defines behaviour
|
||||
// for Tor network connections
|
||||
type TorProxyNet struct {
|
||||
// TorDNS is the IP:PORT of the DNS server for Tor to use for SRV queries
|
||||
TorDNS string
|
||||
|
||||
// TorSocks is the port which Tor's exposed SOCKS5 proxy is listening on.
|
||||
// This is used for an outbound-only mode, so the node will not listen for
|
||||
// incoming connections
|
||||
TorSocks string
|
||||
|
||||
// StreamIsolation is a bool that determines if we should force the
|
||||
// creation of a new circuit for this connection. If true, then this
|
||||
// means that our traffic may be harder to correlate as each connection
|
||||
// will now use a distinct circuit.
|
||||
StreamIsolation bool
|
||||
}
|
||||
|
||||
// Dial on the Tor network uses the torsvc TorDial() function, and requires
|
||||
// that network specified be tcp because only that is supported
|
||||
func (t *TorProxyNet) Dial(network, address string) (net.Conn, error) {
|
||||
if network != "tcp" {
|
||||
return nil, fmt.Errorf("Cannot dial non-tcp network via Tor")
|
||||
}
|
||||
return TorDial(address, t.TorSocks, t.StreamIsolation)
|
||||
}
|
||||
|
||||
// LookupHost on Tor network uses the torsvc TorLookupHost function.
|
||||
func (t *TorProxyNet) LookupHost(host string) ([]string, error) {
|
||||
return TorLookupHost(host, t.TorSocks)
|
||||
}
|
||||
|
||||
// LookupSRV on Tor network uses the torsvc TorLookupHost function.
|
||||
func (t *TorProxyNet) LookupSRV(service, proto, name string) (string, []*net.SRV, error) {
|
||||
return TorLookupSRV(service, proto, name, t.TorSocks, t.TorDNS)
|
||||
}
|
||||
|
||||
// ResolveTCPAddr on Tor network uses the towsvc TorResolveTCP function, and
|
||||
// requires network to be "tcp" because only "tcp" is supported
|
||||
func (t *TorProxyNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) {
|
||||
if network != "tcp" {
|
||||
return nil, fmt.Errorf("Cannot dial non-tcp network via Tor")
|
||||
}
|
||||
return TorResolveTCP(address, t.TorSocks)
|
||||
}
|
168
torsvc/torsvc.go
168
torsvc/torsvc.go
@ -1,168 +0,0 @@
|
||||
package torsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/btcsuite/go-socks/socks"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/roasbeef/btcd/connmgr"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
const (
|
||||
localhost = "127.0.0.1"
|
||||
)
|
||||
|
||||
var (
|
||||
// DNS Message Response Codes, see
|
||||
// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml
|
||||
dnsCodes = map[int]string{
|
||||
0: "No Error",
|
||||
1: "Format Error",
|
||||
2: "Server Failure",
|
||||
3: "Non-Existent Domain",
|
||||
4: "Not Implemented",
|
||||
5: "Query Refused",
|
||||
6: "Name Exists when it should not",
|
||||
7: "RR Set Exists when it should not",
|
||||
8: "RR Set that should exist does not",
|
||||
9: "Server Not Authoritative for zone",
|
||||
10: "Name not contained in zone",
|
||||
// Left out 16: "Bad OPT Version" because of duplicate keys and
|
||||
// because miekg/dns does not use this message response code.
|
||||
16: "TSIG Signature Failure",
|
||||
17: "Key not recognized",
|
||||
18: "Signature out of time window",
|
||||
19: "Bad TKEY Mode",
|
||||
20: "Duplicate key name",
|
||||
21: "Algorithm not supported",
|
||||
22: "Bad Truncation",
|
||||
23: "Bad/missing Server Cookie",
|
||||
}
|
||||
)
|
||||
|
||||
// TorDial returns a connection to a remote peer via Tor's socks proxy. Only
|
||||
// TCP is supported over Tor. The final argument determines if we should force
|
||||
// stream isolation for this new connection. If we do, then this means this new
|
||||
// connection will use a fresh circuit, rather than possibly re-using an
|
||||
// existing circuit.
|
||||
func TorDial(address, socksPort string, streamIsolation bool) (net.Conn, error) {
|
||||
p := &socks.Proxy{
|
||||
Addr: localhost + ":" + socksPort,
|
||||
TorIsolation: streamIsolation,
|
||||
}
|
||||
|
||||
return p.Dial("tcp", address)
|
||||
}
|
||||
|
||||
// TorLookupHost performs DNS resolution on a given hostname via Tor's
|
||||
// native resolver. Only IPv4 addresses are returned.
|
||||
func TorLookupHost(host, socksPort string) ([]string, error) {
|
||||
ip, err := connmgr.TorLookupIP(host, localhost+":"+socksPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var addrs []string
|
||||
// Only one IPv4 address is returned by the TorLookupIP function.
|
||||
addrs = append(addrs, ip[0].String())
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// TorLookupSRV uses Tor's socks proxy to route DNS SRV queries. Tor does not
|
||||
// natively support SRV queries so we must route all SRV queries THROUGH the
|
||||
// Tor proxy and connect directly to a DNS server and query it.
|
||||
// NOTE: TorLookupSRV uses golang's proxy package since go-socks will cause
|
||||
// the SRV request to hang.
|
||||
func TorLookupSRV(service, proto, name, socksPort, dnsServer string) (string,
|
||||
[]*net.SRV, error) {
|
||||
// _service._proto.name as described in RFC#2782.
|
||||
host := "_" + service + "._" + proto + "." + name + "."
|
||||
|
||||
// Set up golang's proxy dialer - Tor's socks proxy doesn't support
|
||||
// authentication.
|
||||
dialer, err := proxy.SOCKS5(
|
||||
"tcp",
|
||||
localhost+":"+socksPort,
|
||||
nil,
|
||||
proxy.Direct,
|
||||
)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Dial dnsServer via Tor. dnsServer must have TCP resolution enabled
|
||||
// for the port we are dialing.
|
||||
conn, err := dialer.Dial("tcp", dnsServer)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Construct the actual SRV request.
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(host, dns.TypeSRV)
|
||||
msg.RecursionDesired = true
|
||||
|
||||
dnsConn := &dns.Conn{Conn: conn}
|
||||
defer dnsConn.Close()
|
||||
|
||||
// Write the SRV request.
|
||||
dnsConn.WriteMsg(msg)
|
||||
|
||||
// Read the response.
|
||||
resp, err := dnsConn.ReadMsg()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// If the message response code was not the success code, fail.
|
||||
if resp.Rcode != dns.RcodeSuccess {
|
||||
return "", nil, fmt.Errorf("Unsuccessful SRV request, "+
|
||||
"received: %s", dnsCodes[resp.Rcode])
|
||||
}
|
||||
|
||||
// Retrieve the RR(s) of the Answer section.
|
||||
var rrs []*net.SRV
|
||||
for _, rr := range resp.Answer {
|
||||
srv := rr.(*dns.SRV)
|
||||
rrs = append(rrs, &net.SRV{
|
||||
Target: srv.Target,
|
||||
Port: srv.Port,
|
||||
Priority: srv.Priority,
|
||||
Weight: srv.Weight,
|
||||
})
|
||||
}
|
||||
|
||||
return "", rrs, nil
|
||||
}
|
||||
|
||||
// TorResolveTCP uses Tor's proxy to resolve TCP addresses instead of the
|
||||
// system resolver that ResolveTCPAddr and related functions use. Only TCP
|
||||
// resolution is supported.
|
||||
func TorResolveTCP(address, socksPort string) (*net.TCPAddr, error) {
|
||||
// Split host:port since the lookup function does not take a port.
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Look up the host's IP address via Tor.
|
||||
ip, err := TorLookupHost(host, socksPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert port to an int.
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return a *net.TCPAddr exactly like net.ResolveTCPAddr.
|
||||
return &net.TCPAddr{
|
||||
IP: net.ParseIP(ip[0]),
|
||||
Port: p,
|
||||
}, nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user