tinygo-org / tinygo

Go compiler for small places. Microcontrollers, WebAssembly (WASM/WASI), and command-line tools. Based on LLVM.
https://tinygo.org
Other
14.72k stars 858 forks source link

Full Go "net" package port, WIP #4273

Open scottfeldman opened 1 month ago

scottfeldman commented 1 month ago

[This is a resurrection of #4187, which I accidentally closed by deleting the fork it was based on.]

This PR is WIP to port the full Go "net" package to TinyGo.

With this PR, I can compile and link a simple example:

package main

import (
        "fmt"
        "net"
)

func main() {
        conn, err := net.Dial("tcp", "localhost")
        if err != nil {
                fmt.Println(err)
        }
        conn.Close()
}

tinygo build -target nano-rp2040 -tags netgo main.go

Notes:

The idea is this: with the netdev work in the last release, we truncated the "net", "net/http", and "crypto/tls" packages and inserted our own stubs to call into the network wifi drivers. With this PR, we use the full Go packages, but this time the insertion point is at the syscall level. Syscall is now where we define the interfaces to the network stack, and the network stack calls into the network drivers. It makes sense...if we were a full OS, syscall is where we'd have our OS-specific code.

Issues:

Issue # 1

Import cycle with "sync" package. Unfortunately, syscall package imports "sync" in some places, which causes an import cycle:

package tinygo.org/x/drivers/examples/net/http-get
       imports bytes
       imports io
       imports sync
       imports internal/task
       imports runtime/interrupt
       imports device/arm
       imports syscall
       imports sync: import cycle not allowed
(See src/syscall/netlink_linux.go for an example).

I've worked around this issue by stubbing out any imports of "sync", but that's not a workable solution in the long term. We'll need "sync" in the implementation of the stubbed out syscalls for the network stack and drivers. So we'll need to revisit "sync" so as to not import "syscall". Is it possible?

leongross commented 1 month ago

@scottfeldman could you maybe elaborate on the process of how the board-specific drivers are abstracted here? As my understanding of tinygos network driver model goes, drivers are represented as the a combination of Netdever and Netlinker interfaces. But I cannot find any reference to that in the inserted syscall code, so how are the bare metal board specifics picked up here?

And to enable network support for linux platforms, couldn't the functions in syscall_tinygo be linked against the according and already existing interfaces provided bey the unix package?

scottfeldman commented 1 month ago

@scottfeldman could you maybe elaborate on the process of how the board-specific drivers are abstracted here? As my understanding of tinygos network driver model goes, drivers are represented as the a combination of Netdever and Netlinker interfaces. But I cannot find any reference to that in the inserted syscall code, so how are the bare metal board specifics picked up here?

@leongross your're right, the netdev/netlink interfaces are the current TinyGo driver model for embedded net devices. In this PR, these interfaces will be replaced with a TBD interface at the syscall level, since all the "net" package calls ultimately resolve into syscalls. The next step in this PR is to discover which syscalls are needed by "net" (and crypto/tls), and let those define the interface to the device drivers. The goal is to support both raw-MAC devices (i.e. Pico-W) as well as devices with an embedded stack (i.e. wifinina, rtl8720n). Some ASCII pics:

raw-MAC stack:

your app
"net" package
-------------           <-- syscall interface TBD
network stack
device driver
-------------           <-- hw interface
device

embedded stack:

your app
"net" package
-------------           <-- syscall interface TBD
device driver
-------------           <-- embedded fw interface
embedded network stack
-------------           <-- hw interface
device

And to enable network support for linux platforms, couldn't the functions in syscall_tinygo be linked against the according and already existing interfaces provided bey the unix package?

Enabling full OS support for "net" wasn't the goal of this PR, but it seems we could make it work by linking in the OS syscalls when compiling against a full OS, bypassing the driver interface mentioned above. I suspect the work to do this is around loader/goroot.go and some built tag magic.

ydnar commented 2 weeks ago

Import cycle with "sync" package. Unfortunately, syscall package imports "sync" in some places, which causes an import cycle:

Can you work around this with //go:linkname?

scottfeldman commented 2 weeks ago

Import cycle with "sync" package. Unfortunately, syscall package imports "sync" in some places, which causes an import cycle:

Can you work around this with //go:linkname?

Yes!

I say that with excitement as I discovered that work-around last week and it's working great. So Issue #1 is not an issue.

scottfeldman commented 2 weeks ago

Update: I'm not ready to post commits, but I do have the full "net" pkg calling into wifinina driver via a custom TinyGo syscall interface. syscalls in the interface so far:

src/syscall/system.go

//go:build tinygo

package syscall

type systemer interface {
        Socket(domain, typ, proto int) (fd int, err error)
        CloseOnExec(fd int)
        SetNonblock(fd int, nonblocking bool) (err error)
        SetsockoptInt(fd, level, opt int, value int) (err error)
        Connect(fd int, ip []byte, port uint16) (err error)
        Write(fd int, buf []byte) (n int, err error)
        Read(fd int, buf []byte) (n int, err error)
}

Wifinina implements this interface. So far I have net.Dial("tcp", "foobar.com") attempting to connect. Since I'm compiling with -tags netgo, the Go DNS client will attempt to resolve "foobar.com" by opening a UDP socket on 127.0.0.1:53. So the first socket to open is the UDP socket. I'm working thru intercepting the Reads and Writes to fake a DNS server response to resolve "foobar.com". I'll have more details on this DNS business when I commit, but that's where I'm at right now.

The net.Dial() test app needs greater than -stack-size=16KB and less than -stack-size=32KB. I haven't figured out the minimum, but 32KB is good so far. The test image is ~350K flash, 8k ram.

scottfeldman commented 2 weeks ago

Update: I am making some commits to capture where I'm at so far. Not done, but I now have the first test (examples/net/tcpclient) working with wifinina using the full "net" pkg.

$ tinygo flash -monitor -tags netgo -target nano-rp2040 -size short -stack-size 32KB -ldflags="-X 'main.ssid=test' -X 'main.pass=testtest'" ./examples/net/tcpclient/

   code    data     bss |   flash     ram
 297888    5280    4232 |  303168    9512
Connected to /dev/ttyACM0. Press Ctrl-C to exit.

Tinygo ESP32 Wifi network device driver (WiFiNINA)

Driver version           : 0.27.0
ESP32 firmware version   : 1.4.8
MAC address              : 34:94:54:26:a7:cc

Connecting to Wifi SSID 'test'...CONNECTED

DHCP-assigned IP         : 10.0.0.113
DHCP-assigned subnet     : 255.255.255.0
DHCP-assigned gateway    : 10.0.0.1

---------------
Dialing TCP connection 
Sending data 

Wrote 133780 bytes in 3548 ms

Disconnecting TCP...
---------------
Dialing TCP connection 
Sending data 
deadprogram commented 2 weeks ago

This is extremely exciting @scottfeldman please let us know how we can help out!

scottfeldman commented 2 weeks ago

This is extremely exciting @scottfeldman please let us know how we can help out!

Thank you. I'm not sure how to break this up and share, but here's my short list of what still needs to be done:

  1. need to create another PR in drivers to capture the wifinina changes so far...
  2. get tcp/udp examples/net tests working with wifinina (nano-rp2040)
  3. get "crypto/tls" examples working with wifinina
  4. get all examples/net tests working with rtl8720n (wioterminal)
  5. don't forget about espat
  6. replace netdev/netlink interfaces with something cleaner, that also works with...
  7. get seqs/cyw43 working under Systemer and pass all examples/net tests

I would really like help with 5 and 6. Work on those should probably wait until we get thru 1 and 2, just to prove the new full "net" pkg solution is going to work, especially the "crypto/tls" part.

scottfeldman commented 2 weeks ago

One more comment: I noticed the "net" pkg trying to open system files like /etc/resolve.conf and /etc/hosts for DNS resolution. Opening those files fail, of course, but I do see those calls working their way down to the syscall level so I had a thought: could we put a tinyfs behind these file i/o syscalls?

scottfeldman commented 1 week ago

@deadprogram I need some help with getting "crypto/tls" working...who's my contact for "crypto/tls" for big Go? I'm trying to get examples/net/tlsclient working, and it's doing the full TLS handshake with the server and then failing trying to verify the server certificate:

Connection failed: tls: failed to verify certificate: x509: certificate signed by unknown authority

What is working since last update is DNS resolution and UDP connections. For the TLS connection, I had to first use UDP to get NTP time and then call runtime.AdjustTimeOffset() to set system time, otherwise the server certificate fails due to being out-of-date wrt system time.

Oh, also, since I'm using nano-rp2040, I had to hack "crypto/rand" with this code to provide a custom rand.Reader:

func init() {
        rand.Reader = &reader{}
}

type reader struct{}

func (r *reader) Read(b []byte) (n int, err error) {
        if len(b) == 0 {
                return
        }
        var randomByte uint32
        for i := range b {
                if i%4 == 0 {
                        randomByte, err = machine.GetRNG()
                        if err != nil {
                                return n, err
                        }
                } else {
                        randomByte >>= 8
                }
                b[i] = byte(randomByte)
        }
        return len(b), nil
}

I think I got that code snippet from @deadprogram a while back, and have just been carrying it around with my projects. But perhaps this should get moved into TinyGo somehow? Anyway, without overriding rand.Reader, the program gets a nil-pointer dereference panic. "crypto/tls" uses rand to generate the TLS Client msg for the handshake.

Ok, that's it for now. If someone can help me get passed the server certificate verification, I think full "crypto/tls" is just going to work. From wifinina's perspective, it's just a simple TCP connection, so a lot of the code we had in there to program mbedTLS is no longer needed. Yay, bunch of code deletions!

scottfeldman commented 1 week ago

Ok, I'm passed the server cert validation issue I was having. I created a root CA cert to pass into tls.Dial() by go:embed'ing a PEM file containing the CA certs. Now the validator is happy.

Next problem is OOM:

panic: runtime error at 0x100407ef: out of memory
[tinygo: panic at /usr/local/go/src/crypto/internal/bigmod/nat.go:71:15]

The -print-allocs=. output is overwhelming. Not sure where to start there.

I sprinkled some runtime.GC() calls in the Connect() and Read() driver paths, but still hitting OOM:

panic: runtime error at 0x1003281b: out of memory
[tinygo: panic at /usr/local/go/src/crypto/internal/nistec/p256.go:235:11]

Maybe it's not possible for "crypto/tls" to fit? That would be a bummer.

I don't suppose there is a way to dump annotated memory to see what's in the heap at OOM?

leongross commented 4 days ago

I did some work on enabling the unix.Syscall wrapper for tinygo 1. When the syscal interface for the network stack is done I think we have a good chance to make networking finally work on linux systems.