From 6e427fefeceed2f3d2ba7fbb261d490be6cbb9d6 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Mon, 27 Nov 2023 10:56:54 +0000 Subject: [PATCH] Initial commit Based on previous work of @neilalexander: https://github.com/yggdrasil-network/yggdrasil-go@netstack Signed-off-by: Vasyl Gello --- cmd/yggstack/main.go | 342 ++++++++++++++++++++++++++++++++++ contrib/netstack/netstack.go | 107 +++++++++++ contrib/netstack/yggdrasil.go | 136 ++++++++++++++ go.mod | 34 ++++ go.sum | 116 ++++++++++++ src/types/mapping.go | 68 +++++++ src/types/mapping_test.go | 28 +++ src/types/resolver.go | 71 +++++++ src/types/tcpproxy.go | 42 +++++ 9 files changed, 944 insertions(+) create mode 100644 cmd/yggstack/main.go create mode 100644 contrib/netstack/netstack.go create mode 100644 contrib/netstack/yggdrasil.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 src/types/mapping.go create mode 100644 src/types/mapping_test.go create mode 100644 src/types/resolver.go create mode 100644 src/types/tcpproxy.go diff --git a/cmd/yggstack/main.go b/cmd/yggstack/main.go new file mode 100644 index 0000000..7a66776 --- /dev/null +++ b/cmd/yggstack/main.go @@ -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 + } + } +} diff --git a/contrib/netstack/netstack.go b/contrib/netstack/netstack.go new file mode 100644 index 0000000..42fb056 --- /dev/null +++ b/contrib/netstack/netstack.go @@ -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) +} diff --git a/contrib/netstack/yggdrasil.go b/contrib/netstack/yggdrasil.go new file mode 100644 index 0000000..8e84cb2 --- /dev/null +++ b/contrib/netstack/yggdrasil.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c0987c6 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..31834d5 --- /dev/null +++ b/go.sum @@ -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= diff --git a/src/types/mapping.go b/src/types/mapping.go new file mode 100644 index 0000000..0d2dd3d --- /dev/null +++ b/src/types/mapping.go @@ -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 +} diff --git a/src/types/mapping_test.go b/src/types/mapping_test.go new file mode 100644 index 0000000..b96c0e6 --- /dev/null +++ b/src/types/mapping_test.go @@ -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") + } +} diff --git a/src/types/resolver.go b/src/types/resolver.go new file mode 100644 index 0000000..246d116 --- /dev/null +++ b/src/types/resolver.go @@ -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 +} diff --git a/src/types/tcpproxy.go b/src/types/tcpproxy.go new file mode 100644 index 0000000..6696a2b --- /dev/null +++ b/src/types/tcpproxy.go @@ -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 +}