Article originally posted on InfoQ. Visit InfoQ
Transcript
Bechberger: I’m an OpenJDK developer. I do Java stuff.
To start with, consider we have a simple web application, you probably all have these. Here in my example, it’s a small documentation website that I have for all the JFR events, for all the profiling events, and it’s pretty cool. Consider now I have a DDoS attack. Someone malicious tries to get the server down, and consider it’s not just a simple application, but consider it’s the application of your company. What do you do? What’s the naive way to do? That’s why I got these flowers here, so the naive way is to just cut the thing. Then things happen. That’s the naive way, just cutting it, as you saw here. Maybe terrible things happen. Maybe just attackers cut off, or you cut off, and they aren’t receiving any packages. There must be a better way than just carrying your pliers in the data center and cutting cables off, because that’s not done in software. We’re here at a software conference.
The simple thing you could do is, you could have a set of blocked IP addresses, and every time a request comes in, for example, through your Spring Boot application, you just check, is this IP address in the list of blocked ones? If it is, then block it. If not, then pass it through. That’s quite simple, and that’s quite nice. Any of you see any problems with this approach? It’s terribly slow. It’s even slower than going to the data center and cutting things. The main issue is, consider now here, we have our Linux network stack. I’m looking here onto Linux, because all of our web applications are running mostly on Linux stacks. You have the package that comes through the network interface, then goes through the whole Linux network stack, and then it arrives at your application. That’s pretty high above. What you could instead do, you could use a firewall. For example, here, to block an IP address, you could call iptables and execute this command and block it.
That’s pretty decent. That works. That’s programmatic, but couldn’t we improve it? If you see it here, it looks quite performant. That’s like a benchmark done by the Cloudflare people. They drop packages. What you see here on the y-axis, the amount of packages that are dropped, and with iptables, you can drop on their machine like 2 million packages per second, which is not that bad, but could get better. Of course, because we want to become a 10x firewall. It’s literally 10x. You see here, we can do things that are almost 10 times faster, or in this case 8 times faster, approaching the 12 million packages per second, which essentially saturates your Ethernet cable. It’s close to literally cutting the Ethernet cable when you drop all packages suite-wise. That’s pretty cool. The idea is that we take advantage of the eXpress Data Plane, that’s the XDP.
This lies essentially between your network interface and your Linux network stack. The first time the package enters your system from the Linux point of view, that’s the place where we’re checking these packages. That’s pretty nice, because consider when allocations occur, you have one allocation that happens when your package arrives from your network, directly from your cable into your network interface. Then you have another one when it enters your proper Linux network stack and you attach things like IP addresses and other information that you attach as metadata. You want to have as little allocations as possible, because allocations just cost cycles.
If you use it there above at the application layer, you have a few allocations, and especially here it’s even more expensive. When we cut it early, we essentially can block the packets at the speed of your network connection. You can do it even faster with offload. That’s pretty cool. There are currently approaches of implementing XDP, which is a way to programmatically modify your network interfaces directly in the network interface, directly in silicon, which is then even faster, because the packet then never reaches your CPU if it’s malicious. That’s cool.
There were traditionally two ways. There was the way of like, change the kernel. That’s a cool option, because you can just modify the network driver itself. You would have to get a change past Linus Torvalds, and we all know that it’s totally easy and it only takes a couple of centuries for even single changes. I’ve seen it with other things that I’ll tell you later, where people try to get things in, and it’s cool. Linus Torvalds really cares about community, so he will probably not insert your change that you made in an afternoon into a mainline Linux kernel. You have the option of having a kernel module, which essentially allows you to hook into the kernel and modify kernel functionality, but that’s also not great, because it has problems with stability. To quote Greg Kroah-Hartman, a kernel maintainer, “You think you want a stable kernel interface, but you really do not, and you don’t even know it”, or why you want.
He quoted things about, for example, the USB interface, where the USB interface with every USB iteration it changed, so it wasn’t stable. What the kernel really cares about is performance. You want to have the most performant, the most stable interface, the most stable operating system. You don’t really care about stability that much in this regard, regarding APIs. That’s a problem, so essentially there are two ways if you aren’t a company like Meta or Google. Even they find it quite difficult to do this, because it costs a lot of time, and it can fundamentally crash your kernel, and that’s not a great thing in production.
eBPF
Can we do better? Yes. There’s where the bee comes in. It’s eBPF. It’s really cool. To quote a guy who is most famous on the internet for shouting at hard drives, Brendan Gregg, “eBPF is a crazy technology, it’s like putting JavaScript into a Linux kernel”. He normally looks like this if he isn’t shouting. He essentially wanted to show his monitoring tools for disks. The idea is that eBPF is making the kernel programmable at native execution speed. What this means is you can hook into a kernel at certain places, and hook it in. Some of you might know Java, so when you have Java UI frameworks, and every time you press a button, you can register a handler, and then this handler can modify some behavior. That’s the same thing here, but only way cooler.
The idea is we work with eBPF. We have our eBPF program. We typically write it in C or Rust. This is then compiled down to a bytecode. It’s not too different from basic JVM bytecode or such. It’s compiled down. This is all happening in user land. What you have in the Linux world, you have a user land and a kernel land. User land are the applications that you normally write unprivileged things, and in kernel land, that’s running directly in the kernel. We can use a system call to communicate between both. It’s essentially an API of your kernel.
You tell the kernel, please load me this program in, and then you have a verifier, which is pretty cool, because when your eBPF program has, for example, misaligned memory access, or it doesn’t check whether a pointer is null and accesses it, then you can potentially crash your kernel. We don’t want to do this, because we can’t really recover on this. We have a verifier that checks for out-of-bounds things, for misaccesses of pointers and other things, and it can do it because it limits the amount of steps that you can execute in eBPF. That means essentially that it’s a restricted execution, but it’s good enough. We can write simple loops. That’s good enough for most applications. Just keep this in mind. Making the verifier happy is the same like with Rust. Making the borrow checker happy can take some time.
Then, when you have a program that’s approved, that’s cool. Then it’s usually JIT compiled. There are JIT compilers for this bytecode on all the common platforms like x86 and s390. You can even run it on your mainframes at home. Then you attach it. We saw before that you can attach it directly in the network interface via XDP, but you can also attach it at various other places. That’s pretty cool. Because then you can also communicate via system calls with your application. What it offers us, it offers us safety and security, because we have a verifier, so we check it before.
It offers us also continuous delivery, because we don’t have to restart the kernel, rebuild it, and everything, when we modify, for example, our network stack. It gives you efficiency, because it’s almost close to native execution speed, because of the just-in-time compiler and how this was developed. It essentially came out of the movement of computation as software, which we have with Docker. Then people thought, can we have network-defined software? That’s what it essentially is. It allows us to modify things in software that were previously not possible. The cool thing is, it’s a standard. Yes, it changes, but it’s standardized enough that you can run applications on multiple kernels when you compile them for one. That’s pretty decent.
How Data is Shared (eBPF Maps)
Of course, you might wonder, how can we then share data? You saw, we can attach it in the kernel. I’d like to show you examples of how to properly do it in demos. How can we share data? An idea would be, in normal applications, to use sockets or to use shared memory. The only problem is, we’re communicating between kernel space and user space, so we want to have more checks on it. What we can do, we can use eBPF maps. eBPF maps don’t have to be maps. It’s more like the PHP terminology of a map, where everything is essentially a map. Here, essentially, map describes a commonly used data structure that’s shared, and you have loads of different map size, but the idea is you have your eBPF program that communicates with these maps, sets values and such.
Then your user land program can come in and also access them, which makes it pretty nice, because you can, for example, lock values out. That’s how it works. It’s really the cornerstone here. There are lots of types, because the kernel developer thought, yes, we probably usually need more than just a HashMap, so there are HashMaps that remove the least recently used element, if they get full, which is pretty useful for caching. I wish that Java would have these by default. We have arrays, we have ring buffers, and all this makes it fairly easy to develop this. It’s far easier to develop than the normal C programs, because in C, when I start out, I have to get a library for getting simple HashMaps I implemented myself, so it’s pretty nice.
eBPF Hooks
As I said, you can hook everything. You can attach small programs to modify behavior. Almost every place in the kernel, which is pretty decent, they even approach us to do this with user land programs, but it’s still an early field. You can attach it at so many places that I only myself explore a percentage of those. I show you later at the end that we can do even more crazier things. Where it’s commonly used, besides doing firewalling, like Cloudflare uses it for their firewalls, Meta uses it also for their load balancing, but you can also use it for observability and monitoring. Many people in the OpenTelemetry space use it, because you have application-wide knowledge.
The idea is that you’re on the kernel level, so you see every application, and you can do profiling across multiple languages, which is pretty cool. You have access to things like network information that you would never have access to, because you’re really logging the packages at the source when they’re coming first in. It’s also cool for security control. If you ever heard of AppArmor, that can also be used with eBPF. What’s also pretty cool, this is CrowdStrike, it was really nice when everything ground to a halt, just because you had these in every out-of-bounds reads. That could never happen with the verifier, which is cool, because the verifier checks, and is like, no, I can’t verify that you can access every value. That’s cool. That prevents a lot of things here. That’s probably also one of the reasons why Microsoft pushes eBPF into their own kernel. They’ve even now released eBPF for Windows. Of course, eBPF has bugs too.
All my applications have bugs all the time, yours probably too, so why should eBPF differ? Most of the bugs are in the verifier. Please, don’t run eBPF programs from untrusted users. Even if you run them from trusted users, know why you’re executing them, not blindly execute them. Because they have kernel-level access, they can access all the memories, they can access all the passwords, everything stored in. Please don’t install malware on your systems. It would be nice, would make life a little bit easier. The interesting thing is that with eBPF, because you can modify kernel-level applications, you can also pretty much hide your eBPF attacking applications from the view of normal systems that aren’t running in kernel, and that makes it pretty hard to detect them.
eBPF Ecosystem
The eBPF ecosystem is quite large. It grew. When eBPF started in 2016, it was just a couple of folks between a couple of companies, Netflix, Google, and Meta working on it, but nowadays it grows, and adds even more applications. As you see, the ecosystem consists of the Linux kernel and the Windows kernel. Then you have on user space, some SDKs that work with that, and then on top you have eBPF projects that facilitate the use cases. There’s even a children’s book, if you’d like to read something with your child. Anything missing here in the user space? There’s no Java. That’s not good, because I like Java. I’m an OpenJDK developer, why can’t I use Java here? That’s essentially what I thought, when last year I thought, what could I contribute to the eBPF ecosystem? I’m like, maybe add a little duke there.
I was in Vienna at the Linux Plumbers Conference, and talked about the very same topic, be like, have more Java in the kernel. To quote Brendan Gregg again, “eBPF is a crazy technology, it’s like putting Java into the Linux kernel”. Once I was introduced at a conference being someone who worked on JavaScript, and this is my redemption. For those of you who know the JavaZone conference, “I want to use a programming language which doesn’t only run in user land”, so here you go. That’s a project called Hello eBPF. It’s humbly known as Hello eBPF, hello Java. Of course, it’s a work in progress. It’s a side project for me. I’m happy that I can work on it, but still, it’s a prototype. I’ll show you in the following how you can use eBPF, at least in demo applications.
Demo
Now we’re going to do live coding. I’ll call it, having fun with eBPF. First, a short demo, so I can show you what you can actually do with eBPF. This is the title-giving demo. I finished this demo three months after submitting this talk here, so let’s see whether it works. Essentially, this is an application written in JavaScript on the frontend, but Spring Boot on the backend. The cool thing is, when you develop all this in Java, as I’ll show you, you can easily connect it to Spring Boot without that much fanfare, without calling other applications via shell commands or anything, or even writing Python or Go. What we can do, we can just send some JSON in there.
The idea here is that we say, please block every IP address from source port 443. That’s essentially the HTTPS port. Let’s see. Essentially, we can do this. We added a new firewall rule. Now we can trigger a request to google.com. I always use google.com because I assume they are online all the time, so we can request it. We see here, it’s blocked. That’s running in the kernel, so you have code running in the kernel that blocks all these packages. We can reset the rules, and now it stops.
We’re doing some live coding. Of course, we do. First, so that you trust me that I can write Spring code, don’t do it, I use ChatGPT for it. It’s the best way and the only way to write Java applications. No. Essentially, the idea here is that when we add our firewall rule, it’s serialized by the browser into a JSON. You saw a JSON here. Then it’s deserialized automatically by Spring Boot. What we can do here, we add the rule directly in the kernel. That’s essentially all the code to put firewall rules in the kernel. I’m going to show you now how we can develop our own applications. What we have to do, because it’s a more complicated thing that I’m doing here, so we have to first give it a license. Because it’s important to know that in the kernel when you’re doing eBPF, there are methods that you can use with an MIT license, but many of them are not with a license exception.
Many of the methods you use, for example, when you do some more sophisticated things, you have to declare that your program is GPL. That’s what we’re doing here. Then what we want to do is we extend our BPF program. That essentially means that we tell my code later that it’s an eBPF program. Here now we want to do some system call hooks because it’s easy. We want to hook, for example, the Openat system call. Openat, let’s look for it. That’s just an interface that we implemented. Every time we enter Openat, we call this here. We can call trace_printk. Every time we access a file with a system call, we call hello world. To make it easier to see that it’s really changing, we add a file name here. Now we have to tell our system how to load it and how to compile and everything.
In common Java fashion, we have a main method. What we essentially then do, we load our program first. We use BPFProgram.load. I’m doing magic behind the scenes. I’ll show you how it works. Essentially, when you compile a Java application, this part of the code here is compiled to C code, and then compiled to eBPF bytecode. Then when you call load, you load this in the kernel. This code here runs in the kernel. This code here doesn’t run in the kernel. I have here a limited way of Java that I can express. Here I can do every Java. I can, for example, write a Spring Boot application if I want. Then I attach it, and that should work. I hope it works. The only problem is the Java ecosystem is not the fastest ecosystem to compile. Essentially, when you’re adding some magic, it’s not getting better.
The cool thing with this is you can write your application’s eBPF code in the same class that you also write your application code. You don’t have that many problems with code duplication. You can even have code, because the amount of Java that you can write is limited, but you can even write code that can be run in both. Let’s see, run demo.Sample. You see here, that’s like every file that’s currently accessed in your system. That’s just a couple of lines of Java code and implementing the interface system call hooks. We can do many things with it. A more interesting example, when we’re relating to firewalls and to other things, is that we can simply write a program that drops every third incoming package. This could be useful, for example, for monkey testing, for chaos testing.
Essentially, monkey, that every third package is like, no. You can even do pseudo random number generators or something, so you can do interesting stuff. What we first do, we define a global variable called count, as you see here. Then we have a method. We annotate it as BPF function. The function here, the XDP handle packet method is implicitly annotated with this, so it should drop. What does it do? It just checks, should get, is it modal 3, is it 1? If so, return true. What we have with handle packet, that’s from the interface XDP hook. The idea is, essentially, that it gets a pointer to an XDP struct. You have to see that, essentially what we’re doing in Java, we’re modeling C, but it’s still type check, so you’re writing a mixture between Java and C, and that’s part of the magic. We first increment the count.
Then, if we should drop, we return the enum value, XDPDrop. If we shouldn’t, we pass it. Now we just attach it here. The cool thing is, we can access the count variable the same way as we access it here in the eBPF program. Now, we can probably try it. It compiled the last time, so we don’t have to recompile it this time. XDPDropEveryThirdPacket. What you might have noticed is that this is a Mac that doesn’t run Linux, just because having Linux systems on stage in presentations is slightly riskier. This runs a VM, so, essentially, when I’m pressing enter, I’m creating new messages that are passed into the VM. You see here, it locks, and it’s pretty decent. You can do it for simple monitoring tasks, and you can even build your own Wireshark clone with it, which I find pretty nice.
How it Works, Under the Hood
How does it work under the hood? I told you before that, when a network packet comes from the internet, your network driver essentially asks your XDP hook attached program, what should I do with this packet? It can then decide whether it should pass or not. Then it gets passed further up in the Linux network stack, and then to your application. The cool thing is, your application will never know that there’s an XDP hook running in kernel doing this. It only sees that every third package is just dropping, but that might be due to a flaky connection. Then your eBPF program, as we see, can communicate statistics up to the eBPF application. Of course, we can also use maps, as I told you before, to pass, for example, blocked IP addresses down.
The interesting part here is that we’re getting, at XDP level, the package, like the proper packets, just a few bytes, and we have to pass it ourselves. I was at university too many years ago, and we learned this, and I had to re-look at my old material, how networking works, because usually when you’re developing applications, you never consider, where’s this byte? An interesting part is also, the network byte order and your host system byte order is different, and that leads to quite interesting bugs when you don’t consider it, because one is big endian and one is lower endian. What you saw here is that I’m essentially trying to run Java in the Linux kernel. To quote Clarke’s second law, “The only way of discovering the limits of the possible is to venture a little past them into the impossible”.
This all was made possible by Project Panama that came in in JDK 22, so quite recently, which allows us to invoke a C method quite easily. Because what I’m essentially doing, I’m building a sophisticated wrapper around libbpf, which offers me basic functionality like load something into the kernel. How this works, when we consider this application here, our method should drop. We take this, it looks like Java, it works like Java, but you also see that we need to support unsigned integers, so there’s an unsigned annotation there. Then we take it to an annotation process that essentially takes all the data structures, and you can define structs and units and such, and converts them to C code.
Then we have a Java compiler plugin that takes the abstract syntax tree, analyze it, and then emits eBPF bytecode. What it essentially does, it converts the Java code that you see here to something that looks similar to the C code. That’s pretty cool. It allows you to write kernel code inside your application. For all the compiler nerds out there, essentially what it then does, it uses Clang to compile it down to eBPF bytecode, which is pretty simple.
Demo
What you might have thought about is whether we could use our sample for something nefarious. Any ideas what we could do when we get your pointer to the file name in? Anything? Yes, we can, and that’s pretty simple. What we can do if you’re nefarious, we can forbid the user to access a file. This here is a simple function that’s also compiled to C code, as before, that just checks, is this file that’s coming in, the /tmp /ForbiddenFile, and what we in enterOpenat then just do is we have to copy the file name in. Because we have to keep in mind that file names come from the user because it’s a system call, it comes from user land, so we have to first read it because we can’t otherwise access it. bpf_trace_printk is interesting, it knows how to do this. That’s also magic.
The idea here is, when this file is forbidden, then we write back to the user, for example, the empty file, and write back to the file name the empty file, and then because it’s the enterOpenat system call, what this means is that the system call that’s like for the process, but Linux kernel is like, I don’t know this empty file. What do you want, user? Then just returns an error. That’s pretty cool because we can essentially forbid the user to access this file using this file name. Let’s try it out. I’m starting another shell. The important thing here is that it of course has problems, for example, when a user uses a symbolic link, it doesn’t work anymore because then the file name is different. We can now do it.
For example, we can touch it, touch tmp/forbidden. It touches like, what do you want with the empty file? It’s also locked here. Access to file is forbidden, as we saw here. That’s pretty cool because we can write simple applications to test and also do some logging. Of course, there are other ways to do bad BPF. Someone even gave a talk about it. You can see that it’s a pretty cool tool. What’s also nice is you can do more stuff. For example, with Hello eBPF, you can define structs in Java code that you can both use in Java code and in C code.
For example, you can define them when we lock all the Openats, which file was accessed by which process. We can define an entry. That’s a class with a String comm, and an int count. Then count per process how many files were read. Then we have here maps that we can easily define and access both in Java land and in user land. For example, you see here, we access the map. What do we do here? We read from this map and then increment. Also, in Java land, what we do, we can just use forEach over it and access it directly, and do many more crazy things and also implement firewalls, which other people have done. I tried my very share on it. Of course, if you want to know more, I write a blog post every other week since January, so there are now 15 in, and you can look forward to having some demos also coming on the blog.
A Glimpse into the Future
What my aim is with this project is to make eBPF accessible for more than just your standard C developer, but for people like you that might wonder how this works and might want to dabble a little bit in it, because many people know Java, and so it’s really accessible. I hope that Java will just also be a part of the ecosystem because it’s, after all, one of the languages that I use for [inaudible 00:35:59] development. Of course, if you go back from the Java side and go more into the broader eBPF world, it gets us a little bit closer to microkernel. We’re getting more functionality out of kernel modules, and out of the kernel into applications that are essentially written by a user and then inserted at runtime. That makes things so much easier. Of course, it makes also debugging harder.
Recently, it’s also quite interesting when you’re, for example, on a distribution vendor like Fedora or such, and now you have code that’s running in the kernel at your customer that you can’t control, so it will probably also make quite a mess. I don’t want to be this poor person that needs to help their clients, be like, my application doesn’t work, my kernel is broken. Yes, you installed this random eBPF code from somewhere on the internet. Let’s see how that works? It worked really well in the JavaScript world anyway. What you can also do, we can reimagine kernel fixes because we can modify the behavior at kernel level.
For example, when we know, ok, this access to a system call, this can break potentially our system, we found an issue. Then we can write a small eBPF program, hook it in, and check for these arguments and just disable them. That’s far easier because we can distribute it. With eBPF, you can do things that previously were like, I have to recompile the Linux kernel and then ship it to our 10 servers to test it out. It’s like, flick of a finger. You saw how fast it is. Even when you bring Java in the game, it’s just a matter of seconds.
A thing that I pretty much like and I’m now part of just as a hobby because I like the people there, is sched_ext. The idea is that you can write your own Linux scheduler. You might wonder, can we write our own Linux scheduler in Java? Am I the only one who wonders this? I’m the only one. I’m not wondering anymore. I did this. I wrote this, and I gave a talk on it at the eBPF Summit online.
Essentially, the idea is that with a prototype of my Hello eBPF library, you can just implement the interface scheduler and implement a few methods, essentially 4, then 25 lines of Java code later, and I mean the whole Java file is just 25 lines of code, you have a scheduler that works, that runs. When I gave the presentation, and I had a Linux machine, this even ran on the machine that I was doing the presentation with. That’s pretty cool because you can use this, for example, to reimagine testing, testing for concurrency stuff. That’s the thing that I’ll be working on, so you can follow up. To do proper concurrency testing where we can control the scheduling order. That’s the cool thing with eBPF, we as the user, we can control things that previously were only controllable by people that worked for years in C code mines of the Linux underbelly. It’s actually what’s coming.
For example, with Linux scheduler, to work on the Linux scheduler before sched_ext, you had to have so many years of experience to even start, to even be able to get something into the kernel. Now you just implement the scheduler interface in Java and be done. That’s pretty cool. My final thoughts is that it’s a cool environment to work on because the ecosystem is so young. Even as a humble JDK developer, you can still make a splash in the ecosystem because it’s not that large, so you can just join and have fun. Yes, come to conferences and also have fun.
Resources
If you want to know more, I collected all my eBPF resources at this link here, https://mostlynerdless.de/. There you can also find links to my blog. I work at SapMachine. We’re like the third biggest contributor to the OpenJDK, and one of the open-source projects at SAP.
Why Write a Firewall in Java?
Losio: I’m not asking you why you write a firewall in Java.
Bechberger: It was the first thing that people asked me at Linux Plumbers, and I’m like, because I can. Why not?
Questions and Answers
Participant 1: If you can, have you thought about implementing SELinux in eBPF?
Bechberger: That’s what people essentially did. AppArmor is, I think, part of SELinux. Yes, people are working on it, not me, and they don’t want to have these things implemented in Java, I think. Of course, there are people working on SELinux-related stuff. It’s another project of Linux Security Modules, LSM. You can find quite a lot of information, and that’s where most of the effort is going into in the security space.
Participant 1: Using eBPF?
Bechberger: Yes, they’re using eBPF. I couldn’t get it running on my machine, but let’s see. You have LSM hooks, and you can essentially have methods like restrictFileOpen. If you want to have a peek under the hood, you have the method, restrictFileOpen, and that’s what it looks like in C, behind the scenes. This is, for example, used to restrict a file access properly, not like we did it here in this example where you can just set a symbolic link. People are working on that, people far smarter than me.
Participant 2: Do you see this as an interesting technology or interface for regular application developers, like working on plain old business backend project, or more something for people that happen to be working for Grafana?
Bechberger: No. Especially with the testing angle and it’s an angle that I want to continue working on on the side in the next couple of months, is that when you consider, for example, you control the scheduler. You can quite easily test for execution order. What you have in your applications, especially when you have multi-threading, you assume this thread runs and this thread runs, but when you have a unit test, you never can test this thread runs on a different CPU than this thread, and they run concurrently, and now I’m testing this very scenario.
That’s especially important when you want to test how your system behaves on the simulated load, because you can, for example, push these two threads onto the same CPU, see what happens. See what happens if half of your threads are randomly stopped for 30 seconds. Does anything crash? Especially when what I want to do is add an interface layer on top where you just have a Java API and you use this, I think as a normal application developer, you probably will be in the future using tools based on this. You can try it out. It’s not that hard. It only costs you a couple of months.
See more presentations with transcripts