Initial commit

Based on previous work of @neilalexander:
https://github.com/yggdrasil-network/yggdrasil-go@netstack

Signed-off-by: Vasyl Gello <vasek.gello@gmail.com>
This commit is contained in:
Vasyl Gello 2023-11-27 10:56:54 +00:00
commit 6e427fefec
9 changed files with 944 additions and 0 deletions

342
cmd/yggstack/main.go Normal file
View file

@ -0,0 +1,342 @@
package main
import (
"context"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"net"
"os"
"os/signal"
"regexp"
"strings"
"syscall"
"github.com/gologme/log"
"github.com/things-go/go-socks5"
gsyslog "github.com/hashicorp/go-syslog"
"github.com/hjson/hjson-go/v4"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
"github.com/yggdrasil-network/yggstack/contrib/netstack"
"github.com/yggdrasil-network/yggstack/src/types"
)
type node struct {
core *core.Core
multicast *multicast.Multicast
admin *admin.AdminSocket
}
// The main function is responsible for configuring and starting Yggdrasil.
func main() {
var expose types.TCPMappings
genconf := flag.Bool("genconf", false, "print a new config to stdout")
useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path")
normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised")
exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format")
confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON")
autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)")
ver := flag.Bool("version", false, "prints the version of this build")
logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"")
getaddr := flag.Bool("address", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 address")
getsnet := flag.Bool("subnet", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 subnet")
getpkey := flag.Bool("publickey", false, "use in combination with either -useconf or -useconffile, outputs your public key")
loglevel := flag.String("loglevel", "info", "loglevel to enable")
socks := flag.String("socks", "", "address to listen on for SOCKS, i.e. :1080")
nameserver := flag.String("nameserver", "", "the Yggdrasil IPv6 address to use as a DNS server for SOCKS")
flag.Var(&expose, "exposetcp", "TCP ports to expose to the network, e.g. 22, 2022:22, 22:192.168.1.1:2022")
flag.Parse()
// Catch interrupts from the operating system to exit gracefully.
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
// Create a new logger that logs output to stdout.
var logger *log.Logger
switch *logto {
case "stdout":
logger = log.New(os.Stdout, "", log.Flags())
case "syslog":
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
logger = log.New(syslogger, "", log.Flags()&^(log.Ldate|log.Ltime))
}
default:
if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
logger = log.New(logfd, "", log.Flags())
}
}
if logger == nil {
logger = log.New(os.Stdout, "", log.Flags())
logger.Warnln("Logging defaulting to stdout")
}
if *normaliseconf {
setLogLevel("error", logger)
} else {
setLogLevel(*loglevel, logger)
}
cfg := config.GenerateConfig()
var err error
switch {
case *ver:
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
return
case *autoconf:
// Use an autoconf-generated config, this will give us random keys and
// port numbers, and will use an automatically selected TUN interface.
case *useconf:
if _, err := cfg.ReadFrom(os.Stdin); err != nil {
panic(err)
}
case *useconffile != "":
f, err := os.Open(*useconffile)
if err != nil {
panic(err)
}
if _, err := cfg.ReadFrom(f); err != nil {
panic(err)
}
_ = f.Close()
case *genconf:
cfg.AdminListen = ""
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
default:
fmt.Println("Usage:")
flag.PrintDefaults()
if *getaddr || *getsnet {
fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.")
}
return
}
privateKey := ed25519.PrivateKey(cfg.PrivateKey)
publicKey := privateKey.Public().(ed25519.PublicKey)
switch {
case *getaddr:
addr := address.AddrForKey(publicKey)
ip := net.IP(addr[:])
fmt.Println(ip.String())
return
case *getsnet:
snet := address.SubnetForKey(publicKey)
ipnet := net.IPNet{
IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0),
Mask: net.CIDRMask(len(snet)*8, 128),
}
fmt.Println(ipnet.String())
return
case *getpkey:
fmt.Println(hex.EncodeToString(publicKey))
return
case *normaliseconf:
cfg.AdminListen = ""
if cfg.PrivateKeyPath != "" {
cfg.PrivateKey = nil
}
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
case *exportkey:
pem, err := cfg.MarshalPEMPrivateKey()
if err != nil {
panic(err)
}
fmt.Println(string(pem))
return
}
n := &node{}
// Setup the Yggdrasil node itself.
{
options := []core.SetupOption{
core.NodeInfo(cfg.NodeInfo),
core.NodeInfoPrivacy(cfg.NodeInfoPrivacy),
}
for _, addr := range cfg.Listen {
options = append(options, core.ListenAddress(addr))
}
for _, peer := range cfg.Peers {
options = append(options, core.Peer{URI: peer})
}
for intf, peers := range cfg.InterfacePeers {
for _, peer := range peers {
options = append(options, core.Peer{URI: peer, SourceInterface: intf})
}
}
for _, allowed := range cfg.AllowedPublicKeys {
k, err := hex.DecodeString(allowed)
if err != nil {
panic(err)
}
options = append(options, core.AllowedPublicKey(k[:]))
}
if n.core, err = core.New(cfg.Certificate, logger, options...); err != nil {
panic(err)
}
address, subnet := n.core.Address(), n.core.Subnet()
logger.Printf("Your public key is %s", hex.EncodeToString(n.core.PublicKey()))
logger.Printf("Your IPv6 address is %s", address.String())
logger.Printf("Your IPv6 subnet is %s", subnet.String())
}
// Setup the admin socket.
{
options := []admin.SetupOption{
admin.ListenAddress(cfg.AdminListen),
}
if cfg.LogLookups {
options = append(options, admin.LogLookups{})
}
if n.admin, err = admin.New(n.core, logger, options...); err != nil {
panic(err)
}
if n.admin != nil {
n.admin.SetupAdminHandlers()
}
}
// Setup the multicast module.
{
options := []multicast.SetupOption{}
for _, intf := range cfg.MulticastInterfaces {
options = append(options, multicast.MulticastInterface{
Regex: regexp.MustCompile(intf.Regex),
Beacon: intf.Beacon,
Listen: intf.Listen,
Port: intf.Port,
Priority: uint8(intf.Priority),
Password: intf.Password,
})
}
if n.multicast, err = multicast.New(n.core, logger, options...); err != nil {
panic(err)
}
if n.admin != nil && n.multicast != nil {
n.multicast.SetupAdminHandlers(n.admin)
}
}
// Setup Yggdrasil netstack
s, err := netstack.CreateYggdrasilNetstack(n.core)
if err != nil {
panic(err)
}
// Create SOCKS server
{
if socks != nil && nameserver != nil && *socks != "" {
resolver := types.NewNameResolver(s, *nameserver)
socksOptions := []socks5.Option{
socks5.WithDial(s.DialContext),
socks5.WithResolver(resolver),
}
if logger.GetLevel("debug") {
socksOptions = append(socksOptions, socks5.WithLogger(logger))
}
server := socks5.NewServer(socksOptions...)
go server.ListenAndServe("tcp", *socks) // nolint:errcheck
}
}
// Create TCP mappings
{
for _, mapping := range expose {
go func(mapping types.TCPMapping) {
listener, err := s.ListenTCP(mapping.Listen)
if err != nil {
panic(err)
}
logger.Infof("Mapping Yggdrasil port %d to %s", mapping.Listen.Port, mapping.Mapped)
for {
c, err := listener.Accept()
if err != nil {
panic(err)
}
r, err := net.DialTCP("tcp", nil, mapping.Mapped)
if err != nil {
logger.Errorf("Failed to connect to %s: %s", mapping.Mapped, err)
_ = c.Close()
continue
}
types.ProxyTCP(n.core.MTU(), c, r)
}
}(mapping)
}
}
// Block until we are told to shut down.
<-ctx.Done()
// Shut down the node.
_ = n.admin.Stop()
_ = n.multicast.Stop()
n.core.Stop()
}
func setLogLevel(loglevel string, logger *log.Logger) {
levels := [...]string{"error", "warn", "info", "debug", "trace"}
loglevel = strings.ToLower(loglevel)
contains := func() bool {
for _, l := range levels {
if l == loglevel {
return true
}
}
return false
}
if !contains() { // set default log level
logger.Infoln("Loglevel parse failed. Set default level(info)")
loglevel = "info"
}
for _, l := range levels {
logger.EnableLevel(l)
if l == loglevel {
break
}
}
}

View file

@ -0,0 +1,107 @@
package netstack
import (
"context"
"fmt"
"net"
"strconv"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)
type YggdrasilNetstack struct {
stack *stack.Stack
}
func CreateYggdrasilNetstack(ygg *core.Core) (*YggdrasilNetstack, error) {
s := &YggdrasilNetstack{
stack: stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6},
HandleLocal: true,
}),
}
if s.stack.HandleLocal() {
s.stack.AllowICMPMessage()
} else if err := s.stack.SetForwardingDefaultAndAllNICs(ipv6.ProtocolNumber, true); err != nil {
panic(err)
}
if err := s.NewYggdrasilNIC(ygg); err != nil {
return nil, fmt.Errorf("s.NewYggdrasilNIC: %s", err.String())
}
return s, nil
}
func convertToFullAddr(ip net.IP, port int) (tcpip.FullAddress, tcpip.NetworkProtocolNumber, error) {
addr := tcpip.Address{}
ip16 := ip.To16()
if ip16 != nil {
addr = tcpip.AddrFromSlice(ip16)
}
return tcpip.FullAddress{
NIC: 1,
Addr: addr,
Port: uint16(port),
}, ipv6.ProtocolNumber, nil
}
func convertToFullAddrFromString(endpoint string) (tcpip.FullAddress, tcpip.NetworkProtocolNumber, error) {
host, port, err := net.SplitHostPort(endpoint)
if err != nil {
return tcpip.FullAddress{}, 0, fmt.Errorf("net.SplitHostPort: %w", err)
}
pn := 80
if port != "" {
if pn, err = strconv.Atoi(port); err != nil {
return tcpip.FullAddress{}, 0, fmt.Errorf("strconv.Atoi: %w", err)
}
}
return convertToFullAddr(net.ParseIP(host), pn)
}
func (s *YggdrasilNetstack) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
fa, pn, err := convertToFullAddrFromString(address)
if err != nil {
return nil, fmt.Errorf("convertToFullAddrFromString: %w", err)
}
switch network {
case "tcp", "tcp6":
return gonet.DialContextTCP(ctx, s.stack, fa, pn)
case "udp", "udp6":
conn, err := gonet.DialUDP(s.stack, nil, &fa, pn)
if err != nil {
return nil, fmt.Errorf("gonet.DialUDP: %w", err)
}
return conn, nil
default:
return nil, fmt.Errorf("not supported")
}
}
func (s *YggdrasilNetstack) DialTCP(addr *net.TCPAddr) (net.Conn, error) {
fa, pn, _ := convertToFullAddr(addr.IP, addr.Port)
return gonet.DialTCP(s.stack, fa, pn)
}
func (s *YggdrasilNetstack) DialUDP(addr *net.UDPAddr) (net.PacketConn, error) {
fa, pn, _ := convertToFullAddr(addr.IP, addr.Port)
return gonet.DialUDP(s.stack, nil, &fa, pn)
}
func (s *YggdrasilNetstack) ListenTCP(addr *net.TCPAddr) (net.Listener, error) {
fa, pn, _ := convertToFullAddr(addr.IP, addr.Port)
return gonet.ListenTCP(s.stack, fa, pn)
}
func (s *YggdrasilNetstack) ListenUDP(addr *net.UDPAddr) (net.PacketConn, error) {
fa, pn, _ := convertToFullAddr(addr.IP, addr.Port)
return gonet.DialUDP(s.stack, &fa, nil, pn)
}

View file

@ -0,0 +1,136 @@
package netstack
import (
"log"
"net"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
type YggdrasilNIC struct {
stack *YggdrasilNetstack
ipv6rwc *ipv6rwc.ReadWriteCloser
dispatcher stack.NetworkDispatcher
readBuf []byte
writeBuf []byte
}
func (s *YggdrasilNetstack) NewYggdrasilNIC(ygg *core.Core) tcpip.Error {
rwc := ipv6rwc.NewReadWriteCloser(ygg)
mtu := rwc.MTU()
nic := &YggdrasilNIC{
ipv6rwc: rwc,
readBuf: make([]byte, mtu),
writeBuf: make([]byte, mtu),
}
if err := s.stack.CreateNIC(1, nic); err != nil {
return err
}
go func() {
var rx int
var err error
for {
rx, err = nic.ipv6rwc.Read(nic.readBuf)
if err != nil {
log.Println(err)
break
}
pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(nic.readBuf[:rx]),
})
nic.dispatcher.DeliverNetworkPacket(ipv6.ProtocolNumber, pkb)
}
}()
_, snet, err := net.ParseCIDR("0200::/7")
if err != nil {
return &tcpip.ErrBadAddress{}
}
subnet, err := tcpip.NewSubnet(
tcpip.AddrFromSlice(snet.IP.To16()),
tcpip.MaskFrom(string(snet.Mask)),
)
if err != nil {
return &tcpip.ErrBadAddress{}
}
s.stack.AddRoute(tcpip.Route{
Destination: subnet,
NIC: 1,
})
if s.stack.HandleLocal() {
ip := ygg.Address()
if err := s.stack.AddProtocolAddress(
1,
tcpip.ProtocolAddress{
Protocol: ipv6.ProtocolNumber,
AddressWithPrefix: tcpip.AddrFromSlice(ip.To16()).WithPrefix(),
},
stack.AddressProperties{},
); err != nil {
return err
}
}
return nil
}
func (e *YggdrasilNIC) Attach(dispatcher stack.NetworkDispatcher) { e.dispatcher = dispatcher }
func (e *YggdrasilNIC) IsAttached() bool { return e.dispatcher != nil }
func (e *YggdrasilNIC) MTU() uint32 { return uint32(e.ipv6rwc.MTU()) }
func (*YggdrasilNIC) Capabilities() stack.LinkEndpointCapabilities { return stack.CapabilityNone }
func (*YggdrasilNIC) MaxHeaderLength() uint16 { return 40 }
func (*YggdrasilNIC) LinkAddress() tcpip.LinkAddress { return "" }
func (*YggdrasilNIC) Wait() {}
func (e *YggdrasilNIC) WritePackets(
list stack.PacketBufferList,
) (int, tcpip.Error) {
var i int = 0
for i, pkt := range list.AsSlice() {
vv := pkt.ToView()
n, err := vv.Read(e.writeBuf)
if err != nil {
log.Println(err)
return i-1, &tcpip.ErrAborted{}
}
_, err = e.ipv6rwc.Write(e.writeBuf[:n])
if err != nil {
log.Println(err)
return i-1, &tcpip.ErrAborted{}
}
}
return i, nil
}
func (e *YggdrasilNIC) WriteRawPacket(*stack.PacketBuffer) tcpip.Error {
panic("not implemented")
}
func (*YggdrasilNIC) ARPHardwareType() header.ARPHardwareType {
return header.ARPHardwareNone
}
func (e *YggdrasilNIC) AddHeader(*stack.PacketBuffer) {
}
func (e *YggdrasilNIC) ParseHeader(*stack.PacketBuffer) bool {
return true
}
func (e *YggdrasilNIC) Close() error {
e.stack.stack.RemoveNIC(1)
e.dispatcher = nil
return nil
}

34
go.mod Normal file
View file

@ -0,0 +1,34 @@
module github.com/yggdrasil-network/yggstack
go 1.21.4
require (
github.com/gologme/log v1.3.0
github.com/hashicorp/go-syslog v1.0.0
github.com/hjson/hjson-go/v4 v4.3.0
github.com/things-go/go-socks5 v0.0.4
github.com/yggdrasil-network/yggdrasil-go v0.5.3
gvisor.dev/gvisor v0.0.0-20231125084359-4b4191b8cad1
)
require (
github.com/Arceliar/ironwood v0.0.0-20231126105342-ad38416a77c8 // indirect
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d // indirect
github.com/bits-and-blooms/bitset v1.5.0 // indirect
github.com/bits-and-blooms/bloom/v3 v3.3.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
github.com/quic-go/quic-go v0.39.3 // indirect
go.uber.org/mock v0.3.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.14.0 // indirect
)

116
go.sum Normal file
View file

@ -0,0 +1,116 @@
github.com/Arceliar/ironwood v0.0.0-20231126105342-ad38416a77c8 h1:qyXiPZClVoe6QsbsiDP23g0Ze/MNXiHIAL/nVNfauH0=
github.com/Arceliar/ironwood v0.0.0-20231126105342-ad38416a77c8/go.mod h1:5x7fWW0mshe9WQ1lvSMmmHBYC3BeHH9gpwW5tz7cbfw=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d h1:UK9fsWbWqwIQkMCz1CP+v5pGbsGoWAw6g4AyvMpm1EM=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d/go.mod h1:BCnxhRf47C/dy/e/D2pmB8NkB3dQVIrkD98b220rx5Q=
github.com/bits-and-blooms/bitset v1.3.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8=
github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bloom/v3 v3.3.1 h1:K2+A19bXT8gJR5mU7y+1yW6hsKfNCjcP2uNfLFKncjQ=
github.com/bits-and-blooms/bloom/v3 v3.3.1/go.mod h1:bhUUknWd5khVbTe4UgMCSiOOVJzr3tMoijSK3WwvW90=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gologme/log v1.3.0 h1:l781G4dE+pbigClDSDzSaaYKtiueHCILUa/qSDsmHAo=
github.com/gologme/log v1.3.0/go.mod h1:yKT+DvIPdDdDoPtqFrFxheooyVmoqi0BAsw+erN3wA4=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hjson/hjson-go/v4 v4.3.0 h1:dyrzJdqqFGhHt+FSrs5n9s6b0fPM8oSJdWo+oS3YnJw=
github.com/hjson/hjson-go/v4 v4.3.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.39.3 h1:o3YB6t2SR+HU/pgwF29kJ6g4jJIJEwEZ8CKia1h1TKg=
github.com/quic-go/quic-go v0.39.3/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/things-go/go-socks5 v0.0.4 h1:jMQjIc+qhD4z9cITOMnBiwo9dDmpGuXmBlkRFrl/qD0=
github.com/things-go/go-socks5 v0.0.4/go.mod h1:sh4K6WHrmHZpjxLTCHyYtXYH8OUuD+yZun41NomR1IQ=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/yggdrasil-network/yggdrasil-go v0.5.3 h1:tfAajYmaiS1L9XevdEC1VUpScpgFfCW7/oEEWxdv5aM=
github.com/yggdrasil-network/yggdrasil-go v0.5.3/go.mod h1:qRzHB8bKEIpd1pQVYoZ+SedJJTBG0X9sbyctWxQDDGA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20231125084359-4b4191b8cad1 h1:J9340hJZ+P01JeVPK8GatP/6Bk7j2dyLUiKct5k7IBA=
gvisor.dev/gvisor v0.0.0-20231125084359-4b4191b8cad1/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=

68
src/types/mapping.go Normal file
View file

@ -0,0 +1,68 @@
package types
import (
"fmt"
"net"
"strconv"
"strings"
)
type TCPMapping struct {
Listen *net.TCPAddr
Mapped *net.TCPAddr
}
type TCPMappings []TCPMapping
func (m *TCPMappings) String() string {
return ""
}
func (m *TCPMappings) Set(value string) error {
tokens := strings.Split(value, ":")
if len(tokens) > 2 {
tokens = strings.SplitN(value, ":", 2)
host, port, err := net.SplitHostPort(tokens[1])
if err != nil {
return fmt.Errorf("failed to split host and port: %w", err)
}
tokens = append(tokens[:1], host, port)
}
listenport, err := strconv.Atoi(tokens[0])
if err != nil {
return fmt.Errorf("listen port is invalid: %w", err)
}
if listenport == 0 {
return fmt.Errorf("listen port must not be zero")
}
mapping := TCPMapping{
Listen: &net.TCPAddr{
Port: listenport,
},
Mapped: &net.TCPAddr{
IP: net.IPv6loopback,
Port: listenport,
},
}
tokens = tokens[1:]
if len(tokens) > 0 {
mappedaddr := net.ParseIP(tokens[0])
if mappedaddr == nil {
return fmt.Errorf("invalid mapped address %q", tokens[0])
}
mapping.Mapped.IP = mappedaddr
tokens = tokens[1:]
}
if len(tokens) > 0 {
mappedport, err := strconv.Atoi(tokens[0])
if err != nil {
return fmt.Errorf("mapped port is invalid: %w", err)
}
if mappedport == 0 {
return fmt.Errorf("mapped port must not be zero")
}
mapping.Mapped.Port = mappedport
}
*m = append(*m, mapping)
return nil
}

28
src/types/mapping_test.go Normal file
View file

@ -0,0 +1,28 @@
package types
import "testing"
func TestEndpointMappings(t *testing.T) {
var mappings TCPMappings
if err := mappings.Set("1234"); err != nil {
t.Fatal(err)
}
if err := mappings.Set("1234:192.168.1.1"); err != nil {
t.Fatal(err)
}
if err := mappings.Set("1234:192.168.1.1:4321"); err != nil {
t.Fatal(err)
}
if err := mappings.Set("1234:[2000::1]:4321"); err != nil {
t.Fatal(err)
}
if err := mappings.Set("a"); err == nil {
t.Fatal("'a' should be an invalid exposed port")
}
if err := mappings.Set("1234:localhost"); err == nil {
t.Fatal("mapped address must be an IP literal")
}
if err := mappings.Set("1234:localhost:a"); err == nil {
t.Fatal("'a' should be an invalid mapped port")
}
}

71
src/types/resolver.go Normal file
View file

@ -0,0 +1,71 @@
package types
import (
"context"
"crypto/ed25519"
"encoding/hex"
"fmt"
"net"
"strings"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggstack/contrib/netstack"
)
const NameMappingSuffix = ".pk.ygg"
type NameResolver struct {
resolver *net.Resolver
}
func NewNameResolver(stack *netstack.YggdrasilNetstack, nameserver string) *NameResolver {
res := &NameResolver{
resolver: &net.Resolver{
PreferGo: true,
},
}
if nameserver != "" {
res.resolver.Dial = func(ctx context.Context, network, address string) (net.Conn, error) { // nolint:staticcheck
if nameserver == "" {
return nil, fmt.Errorf("no nameserver configured")
}
address, port, found := strings.Cut(nameserver, ":")
if !found {
port = "53"
}
address = net.JoinHostPort(nameserver, port)
return stack.DialContext(ctx, network, address)
}
}
return res
}
func (r *NameResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
if strings.HasSuffix(name, NameMappingSuffix) {
name = strings.TrimSuffix(name, NameMappingSuffix)
// Check if remaining string contains a dot and
// assume publickey is a rightmost token
name = name[strings.LastIndex(name, ".")+1:]
var pk [ed25519.PublicKeySize]byte
if b, err := hex.DecodeString(name); err != nil {
return nil, nil, fmt.Errorf("hex.DecodeString: %w", err)
} else {
copy(pk[:], b)
return ctx, net.IP(address.AddrForKey(pk[:])[:]), nil
}
}
ip := net.ParseIP(name)
if ip == nil {
addrs, err := r.resolver.LookupIP(ctx, "ip6", name)
if err != nil {
fmt.Println("failed to lookup", name, "due to error:", err)
return nil, nil, fmt.Errorf("failed to lookup %q: %s", name, err)
}
if len(addrs) == 0 {
fmt.Println("failed to lookup", name, "due to no addresses")
return nil, nil, fmt.Errorf("no addresses for %q", name)
}
return ctx, addrs[0], nil
}
return ctx, ip, nil
}

42
src/types/tcpproxy.go Normal file
View file

@ -0,0 +1,42 @@
package types
import (
"net"
)
func tcpProxyFunc(mtu uint64, dst, src net.Conn) error {
buf := make([]byte, mtu)
for {
n, err := src.Read(buf[:])
if err != nil {
return err
}
if n > 0 {
n, err = dst.Write(buf[:n])
if err != nil {
return err
}
}
}
return nil
}
func ProxyTCP(mtu uint64, c1, c2 net.Conn) error {
// Start proxying
errCh := make(chan error, 2)
go func() { errCh <- tcpProxyFunc(mtu, c1, c2) }()
go func() { errCh <- tcpProxyFunc(mtu, c2, c1) }()
// Wait
for i := 0; i < 2; i++ {
e := <-errCh
if e != nil {
// Close connections and return
c1.Close()
c2.Close()
return e
}
}
return nil
}