 So, I think I've got about 30 minutes of slides. Then I'm actually, I'm going to try and do a little bit of demo. And I guess there'll be a question and answer period in another room afterwards. Like the Track 1 Q&A room, I actually don't know where that is. All right, let's go. Hello, drink what? Who's got the whiskey? Good deal. All right, my name is Ben Kurtz. I'm here to talk to you about Funk. And generally speaking, the use of functional programming for scripting network traffic. Oh, there we go. Before we get started, I'd like to point out that Funk is currently an open source project hosted on SourceForge. I'd encourage anyone here with a laptop to go to this address and get a subversion check out right now. So you can follow along with that strain in your eyes. This looks fuzzy to me, but it's a lot of fun. It's surprisingly addictive and you'll definitely get more out of looking at the full source than just listening to me talk about it. Although a little bit of both is probably the best. You may be wondering, what's this Funk? WTF? Well, Funk's an engine for the scripting of network traffic written in the functional programming paradigm using the scheme programming language. With the chicken scheme to see compiler, any script written for Funk can be compiled to straight C code or to a native binary. This makes Funk useful for deployment on an embedded system or an appliance, as well as for plinking around with your laptop. A lot of people have been asking me why I named it Funk. I like Funk. The only other suggested name was Colonel Kurtz's Chicken of Darkness. And that doesn't make an awesome acronym. So, yeah, we Funk. These are some of the overarching goals of Funk. In short, I wanted the core of the engine to be very simple but extensible to any network protocol regardless of complexity. The most important idea, the take home idea here, is that Funk creates a generic interface to every network protocol. Hey, secret sauce. This lets you keep your fuzzing logic separate from your protocol logic. I'm on a schedule here. That is, you can write completely abstract fuzzing methodologies and then apply them to any protocol. Even protocols you don't know about at the time you write the fuzzing methodology. When support for the protocol is added, all of your fuzzing methods can be applied to the new protocol immediately without making any changes. This lets you play that combinatorical marketing game. How many fuzzing variants, how many testing variants does our product do? Well, it's the number of fuzzing methodologies times the number of fields in all protocols times the number of combinations of fuzzing methodologies you can make. So the numbers go up very quickly. You see a lot of people making claims to 1 million, 2 million, 3 million different tests. And that's just multiplication. But I sat down, I tried to come up with every way to fuzz an individual field I could come up with. I came up with about 24 things. If you look at the PROS site, PROTOS, they've got a couple dozen too. But I'm not introducing a new fuzzing methodology at all. That's not the problem I'm solving. There's a half a dozen other fuzzing talks at DEF CON this year that are different fuzzing methodologies or the pros and cons of one over the other. The FUNC is a delivery system for fuzzing or any type of network scripting you want to do. It basically just removes the protocol logic from the equation. So what can I do right now? It doesn't have to be just fuzzing logic. The current implementation of FUNC can be used for flooding, spoofing, any other type of traffic generation. I have a lot of fun with it just messing around. Work is ongoing. We're heading towards scripting responses to network events using currying some lambda chaining. That's a comment just for that dude. Which will make FUNC useful for more complicated applications. Like firewalls, intrusion detection systems, or rapidly prototyping servers. It can also easily be used to fuzz file formats simply by piping it's output into a file instead of a network. As far as FUNC is concerned, the difference between a file and a packet is just where the buffer gets sent. So, yeah. Tough crowd. Oh, alright. Joke bombed a drink. That's a rule. This is going to end in tears, I know it. You know what's really ironic is that the slides is hopefully I've learned from my mistakes, which I've just disproven. But I have designed and implemented several of these protocol agnostic traffic generators. I think that actually everyone I've talked to about this has, everyone who works in this space has attempted something like this before. Sometimes they've written something very protocol specific for IP. Sometimes they've attempted a general solution. It's a little trickier than you'd think at first whack. People end up writing and rewriting code that deals with protocols. I've messed up more than my share of these already. But, well, in that space anyway, I've learned from my mistakes and now you can too. So my most successful design featured an XML-based protocol definition language. It was a domain specific programming language that I designed myself. The system was overly complicated and written in C++, which might be a coincidence and it might not. Although the system had its flaws, it was, you could say, imperfect. It did sell a few units and it did make some money. And there was a demand for it. It made some people happy anyway. But the biggest flaw was that the hand-rolled protocol definition language used a regular grammar. That is, it wasn't turning powerful, mainly because I'm lazy. This made it extraordinarily bad at handling special cases and network protocols. Things like checksums of pseudo headers or type-length value fields or any kind of dynamic protocol where there's like a type field that actually determines what the rest of the fields mean. Like OSPF, there's six packet types and you tell from the common header which type you're in. Or ICMP, DHCP, a lot of protocols are like that. But using a regular grammar made it extraordinarily... Because the checksums couldn't be dealt with in the protocol definition, special cases had to be added to the engine code itself, which made a mess. It was difficult to extend. It was difficult to maintain. And sometimes when we added a protocol, we ran into a gotcha, like something else we had to add to the engine. And it just became stringy. It did work well enough, but it kind of haunted me. Even after I was like away from it, I knew that wasn't the right way to do it. And I kept picking away at it in the back of my brain for a couple of years, really. And eventually I stumbled on something that changed the way I thought about the problem entirely. And, oops, chicken scheme. So, you know, you could write your very own Turing Powerful programming language and use it to define your protocols. Or we could use one that already exists. Since I am way too lazy to do it the hard way again, I picked chicken scheme which is uniquely suited for network traffic generation. Chicken is one of the many implementations of the scheme programming language. Scheme is a derivative of Lisp which many people believe stands for button. Yay! There are other hurdles in convincing people that scheme is awesome. I actually have to admit that I, myself, was always turned off by the parentheses. I was turned off by just its general craziness. I've always been a C++ programmer. I've always, or Java or straight C. I started in Apple Soft Basic. All imperative stuff, a lot of object oriented, you know. And these are actually the questions I would ask scheme programmers. I'd be like, why do you use scheme? I kind of had the general impression that they were sort of elitist, you know, like. But I came to scheme through this particular problem. And I spent a long time looking for an optimal solution for it. And I'm firmly convinced that this is a good way to go. And why is that? Well, scheme supports the functional programming paradigm. And funk makes heavy use of functional programming. Functional programming means functions have no side effects. Or they should have no side effects. They should avoid, depending on state and mutable data. There are many, many differences between functional programming and other paradigms and imperative object oriented and so on. But the most important thing functional programming gives us is support for higher order functions. Higher order functions are functions that can take functions as arguments and return functions as results. I'm going to say that once more. Higher order functions can take functions as arguments and return functions as results. You don't really have to remember that though. Scheme is a functional programming language that supports first class functions. First class functions means that a higher order function can go anywhere another first class value can go. A first class value is like a number. So basically you can define a function anywhere you can put a number in the scheme. Which gives you a real through the looking glass feel. It's a completely different thing. In scheme the key word to define a function is lambda. Named for lambda calculus, which I won't claim to understand fully. I've defined an anonymous function in the first bullet. Just to give you an example of the syntax. It takes no arguments. That's what the empty friends mean and it returns the string yay. So when that function is evaluated it'll say yay. Scheme makes it really easy to define new functions during execution. Like not at compile time but during execution. Bind them to variables and pass them as arguments. Proveably any program written in scheme can also be written in C++ or in Java since they're altering powerful languages. But that just says that it can be done not how efficiently or how cleanly it can be done. A really short program in scheme can't necessarily be written as a short program in C++ and vice versa in some cases. So I guess my point is different languages describe programming or different languages describe programs in different ways. That is they use different metaphors. And some of them are better suited to efficiently handling certain classes of problems than others. It's a matter of choosing the right tool for the job. It really is a very different way of thinking about it but I think it's uniquely suited for the problem of traffic generation and I'll show you some code and a couple of examples to back that up in a little bit. There are other advantages to using scheme although perhaps not as common as C++. Scheme has been around for decades and is widely used. Festival and GIMP use it to name a couple but it turns up all over. Sometimes it's a scripting language, sometimes it's what the engine is implemented in. In this case both. It's a minimalist language. The entire standard at the moment is 50 pages long which is about the length of my attention span. So, you know, I actually read it which is a little bit different than the C++ standard which I might use to keep me warm in winter. It's extremely portable. There are interpreters for every architecture and there are compilers that will generate C and will generate Java bytecode. And that's from the same code. Can be translated into C, can be translated into Java bytecode or run in a scheme interpreter. Another thing, scheme interpreters are required to optimize tail recursive calls to minimize the use of the stack. Tail recursion just means it's a recursive function where the last thing the function does is call itself. So it's like the tail of the function is what recurses. But optimized tail recursion makes it okay to use recursion to do things like iterate over lists which leads to more compact code and it's really nice once you get over the knee jerk recursion phobia that you develop from years of using C++ or scheme or pretty much anything else. Scheme has other benefits. It does do automatic memory management which prevents buffer overflows and the scheme language is closure based which means that a scheme function captures the state of its environment at the point of closure preventing further modification. That's a little bit dicey to understand. I'm not going to explain that entirely. But this helps support the no side effects goal of the functional programming paradigm and it makes callbacks a lot less scary which is the moral of the story. Scheme also has a very fast prototyping and development time according to a JPL study comparing C, C++, Java and Lisp. Why chicken specifically? There are many scheme implementations. I chose chicken because it's one of the most actively developed and one of the most highly optimized. Actually, the main developer of chicken, the guy who wrote the chicken scheme to C compiler will actually respond to emails. Even really stupid questions. He's a very nice dude. But most importantly, chicken is a scheme to C compiler which means that anything written in chicken scheme can be compiled into highly optimized straight C and you could distribute it as C files but you can also compile it into native binaries. Chicken can also be extended with libraries called eggs that can combine scheme code and straight C code and they'll compile into shared libraries that you can use from a scheme interpreter and you can also link against them when compiling and it can integrate with code written in any other language through SWIG. Additionally, it integrates with standard build environments. If anyone actually pulled down a copy of the funk source you can check out my make files for examples of how it integrates with autoconf and gmake. The more I drink, the more I stutter. Here comes the flop. When picking a core technology, I could have also gone with Python which some of the cool kids are using these days. Actually, every other general purpose fuzzer I've seen is written in Python. At least the ones that actually work. Python has some functional programming features like limited lambda support. My stress is limited. But it's incredibly slow and every benchmark I looked at usually coming in at 30 times slower than C and I am being charitable. I like Python. I use it for a lot of my general purpose scripting. But I want the funk to work efficiently on limited hardware perhaps even open WRT. So efficiency or lack thereof is a deal breaker. When you could choose chicken instead and generate optimized C, Python seems a little foul. Also, Python has stupid white... That was a bomb joke too. All right. Oh God, what is that? Yeah, I'll say. All right. Also, Python has stupid white space which I find just as irritating if not more than stupid parentheses. But if that's all that's preventing you from using a scheme there is actually a syntax extension to scheme available that will let you use white space instead of parentheses. I just don't know anyone that actually uses it. Doing it to you in your ear hole. All right. Now the fun bit. All right. During the development of funk, I've written three libraries for chicken so far. BitCat, CRC16 and RawSockets. These extensions are available in the funk source forage project and also in chicken's online egg repository at callcc.org. BitCat is a bit string concatenator which is useful if you have a lot of 13-bit fields or 1-bit fields, 32-bit fields that you need to stick together in a byte buffer. CRC16 is just the CRC16 algorithm and RawSockets is an interface to the packet socket interface on BSD or Unix platforms. Currently I've tested it on Linux and also on OSX. I did not write a packet socket driver for Windows. I don't actually have a Windows box. But hey, if someone wants to contribute to the project, I'd be absolutely thrilled. Also, it's not a wrapper for RawSockets. It's a wrapper for packet sockets. I don't know if anyone else is with me on this. I think since packet sockets are more raw than raw sockets, I think a name change is in order, but hey. All right. So just a reminder, the main goal here is to provide an abstract interface to network protocols that allow for the easy addition of any other protocol. So it's important to keep everything consistent and abstract. The crux of the biscuit, the most important thing in this whole scheme is how protocols are defined. It was an overly limited protocol definition language that crippled my last attempt after all. So what is it we want our protocol definitions to do? Well, we need an ordered list of fields and their associated bit lengths. That's a given. But we would also like fields to default reasonable values because it keeps our actual scripts short. Sometimes these values have to be calculated like the total packet length field in the IP protocol is an example, or CRCs, checksums. But we might also have to define some protocol-specific logic about how a packet is generated. This is usually something that relates the values of particular fields together, checksums, TOV fields, stuff like that. We also need a couple of other things like a way of validating field values. Since this is a tool that will be used for testing, we are most definitely going to be lying and sending out invalid values. However, I am planning on building a GUI on top of this and the ability to tell if a particular field value is valid or not will be a requirement for the GUI. And that is a decision that should be made by the protocol. When you define the protocol, the protocol knows what fields are valid and what aren't. No more whiskey. We also define a serializer for each field. The serializer just translates a field value from the string we get from the user. For example, 192.168.1.1. It just translates that to its corresponding binary. So that's all serializer does. So here's an example of a protocol definition in FUNC. If anyone downloaded this, this is an ethernet.scm. What's on the screen here deals with the entire ethernet protocol. Ethernet is a simple protocol of only three fields. For each field, we assign a name, a bit length, and procedures that will validate and serialize the field. So this is where the first class function thing comes in handy. I don't know if you can see this, but so that we have three fields, DeskMack, SourceMack, and PacketType. DeskMack and SourceMack, their bit length is 48 bits, and I'm defining the validator. I'm setting it to the MAC validator, and I'm setting the serializer to the MAC serializer, which are special functions that deal with MAC addresses, the hex, hex, colon, hex, hex, colon format. And the PacketType field is set to 16 bits, but I'm not defining a validator. I'm not defining a serializer. If you don't define a validator or a serializer, it defaults to a hex validator and a hex serializer that both... There are actually functions that generate functions that serialize and validate hex fields based on the bit length of the field. So this is one of those things that's just made a lot easier by using functional programming. Further down, we also assign default values for all three fields. My defaults are not exactly reasonable. I'm setting the destination MAC address to 1, 2, 3, 4, 5, 6, and the SourceMack to A, A, B, B, C, C. But the PacketType, I'm just setting to IP because that's what it ends up being most of the time. And the other thing to look at in this is those put-up calls at the bottom. Those are actually registering three internal protocol operations with FUNC. Generate, validate, and make layer. These three operations are... It's a requirement that every protocol provide these three operations. Generate actually produces a byte buffer that can be sent out on the wire or saved to a file. Validate reports on which fields contain correct values. It actually returns a list of booleans telling you whether or not each field is valid. And makeLayer, which creates an instance of a protocol layer and fills in any unspecified values with the appropriate defaults. But it's not really that much work. For most cases, the default generator is sufficient. The code for the default generator is in default-fuckins, fcns.scheme. If anyone has pulled this aversion. So what the default generator does, it does actually most of the work of the entire engine. And all it does is it iterates over each field. And here's where some more functional programming comes in. Fields can contain either actual values, 192.168.1.1, right? Or they can contain functions. Because you can put a function anywhere, you can put a number or a string. They can contain functions that will return a value when evaluated. If the field contains a function, first the default generator evaluates it. And then it goes forward with that value. So then the value is converted to binary using that field serializer. And that binary is appended to the outgoing buffer using the BitCat library. The BitString concatenator. Is that legible? No. So the default generator can be completely overridden or it can be extended. The current IP implementation registers its own generator with funx-operation table, which is called ip4.scm file. All the IP generator does is call the default generator with header checksum defaulting to zero. Calculates the checksum of the result and inserts the checksum in the header checksum field. This code makes the use of the CRC16 egg, but otherwise this is all the code it took to generate correct IP checksums. That's like three lines of ham. So I was pretty happy with that. As I mentioned, every field value can be replaced with a lambda, a procedure that will evaluate to a value. In the IP protocol, the default value for total length is a procedure that will calculate the correct total length for the packet. So this is a list of default values like you saw for Ethernet where I was setting the MAC address to one, two, three, four, and so on. IPs get a lot more fields, so I couldn't fit the whole thing on one slide. But this is an IP4.scm. So you can see for the total length field, I'm not giving it a value, but I'm actually just defining a function right there. Which gets passed in all this other information about the packet that's been generated so far and what the fields are and so on. And this will actually, by default, just calculate the correct total length for the packet and set that. Cool beans. So to generate an actual packet, all we have to do is stack some protocol layers and send them out. This is all it takes to generate a packet with default values. You can see in that top section, I'm setting my packet to a list of an instantiated Ethernet layer, an instantiated IP layer, and an instantiated TCP layer, and I'm not overriding any of the defaults. Then I have to find a function which is to generate my packet where you give it a length and it just adds a data section which is that byte length that's just filled in with Fs. And this is where the raw sockets egg comes in. I just open up the eth0 interface, generate a packet with 8 bytes of data then generate a packet with 16 bytes of data. This is a simple example. So where do we go from here? In the short term, we're already working on the ability to read, parse, and respond to incoming packets. We're also looking at file format and binary fuzzing sooner rather than later. And further out, I'd like to see Funk extended with a visual script designer so that packets can be generated without touching a scheme. I'd also like to use this to drive visualizations of network events. If you're interested in learning more about scheme, check out these books, the wizard book, that's the first one, probably the most definitive one. It's available in its entirety online at that link. So that's the end of the slides. I'm going to attempt a demo. But actually, if you look at the slides online, I've included 10 extra slides or so which contain instructions on how to set up a Funk development environment, which is the same thing I'm using. So if you check those out online, it's got all the info you need. If you're interested in talking to me about Funk, just come up and ask. I'll be hanging out in the prototype hacker space in 114. I'm re-boxing. All right. So how do I turn this on? Oh, good work. I'm a cheap date. I'm sorry. Does that show up all right? So for a development environment, I use Eclipse with a particular plugin for scheme that will let you connect to a redevelop print loop, which actually, if you do a subversion checkout, the make files will just build it for you. You might want to set UID. If you're using Funk because opening a packet socket requires root. So as I said before, any script you write using Funk, you can run through the interpreter and you can also compile them to binaries. So I'm just going to do a couple of quick examples. So the first thing I'm going to do, I'm going to fire up my scheme interpreter. So that shows up there. So my first example, I'm actually going to send it out over the loopback rather than Ethernet. So I also have just a BSD style loopback headers to find in a separate file. So I'm going to instantiate a loopback layer. I'm going to make an IP layer, but I'm going to override the source IP and set it to 127.002 and the desktop IP to 127.002. Then this is the same generate my packet I showed you before, and this is actually the same example. I'm going to generate a packet with 8 bytes of data and a packet of 16 bytes of data. So all you have to do to run it, bombs away, and there's my packets. I'm not doing anything with sequence numbers or acts right now, but you can see that the TCP generated and the checksum is good. That simple example. I hope that was clear. Here's a slightly better example. This one's a bit more fuzzing oriented. It's the same as the previous example, but I've defined some extra functions. I've defined a function that fills in a U8 vector, which is equivalent to an unsigned char star in C. You just give it a length and it will fill in a buffer of the given length with random data. I've defined a lambda called make random field, which has the right signature to be passed in as a field value. All that does is it reads the bit length of the field that it's put in and fills that many bits with random data. It seems, I don't know, you get used to it, but it's a very different way of thinking about it than you would be thinking about it if you're writing this in C. So this is the same. It's pointed at the loop back. If I wanted to point it at ethernet, basically all you have to do is uncomment that and then comment this. The same generate my packet, except now I've replaced the function that fills in with F, so I've replaced it with make random U8 vector. And then I've also defined another function that just generates N packets filled with random data. And it also sets the size of the data segment to a random number. So I'll run that and you can see some evidence of that running on the bottom. And that's generated a random number of randomized packets. You can see that the data segments at the bottom, this one's 22 bytes, this one's 16, this one's 48, this one's 32, and the actual data itself has been randomized. So that's what the randomized data segment, but what if you wanted to randomize a field value? Well, we can just use that function I showed you a moment ago. And instead of passing in 127.0.0.2 for the destination IP, I'm going to pass in a lambda. I'm going to pass in make random field. So I just ran that. Check that out. Completely randomized destination addresses. And that's all it takes. So some other interesting things to look at. Some of the egg code is pretty cool. So there's some scheme up top. Then you can just break into straight C. And this is the egg that opens up a packet socket, which allows you to even set the fields of Ethernet, completely avoiding the TCP IP stack. And I realize that you're probably not going to get a handle on this just from me talking right now, so I'd really strongly encourage you to go just check out the source. So it went from scheme to straight C, defining a couple functions, and then back to scheme to define interfaces to the functions, error messages, that sort of thing. So are you raising your hand? Actually, at the moment I just took it up through the TCP stack. The next thing I'm going to be looking at seriously is ASN 1, because frankly that was the thing that broke my previous one. DHCP would be easy, but it's kind of a pain in the butt because there's like 65 different variants. Yeah. Seth is flipping me off. Sorry? Absolutely. Hey, I'm absolutely looking for people to help flesh this out and implement more protocols. So if that's something you're interested in, definitely. Sorry? I think he wants me dead. Yep. Sorry? Right. You can totally screw with it. Actually, that's a very good question. I'm going to come back over here. He asked how strongly is the networking stack itself, how strongly is it hard-coded, and how easy is it to screw with that? To go back to our simple example, a little bit down further, okay. Define my packet. If I wanted to do something stupid or something crazy, I could do two IP layers in a row just like that. That's it. Sorry? That would be IP and IP, and then TCP on top of that. And the engine itself doesn't really care. You're just giving it a list of protocols. So you could do two loopback headers, followed by two IPs, followed by TCP, and things should work out fine. Well, I mean, as far as the engine's concerned, I don't think that's going to mean anything to anybody. But like I said, it's designed to be a testing tool, so we have to be allowed to break rules everywhere. One more? I really can't hear you. Yeah, that's a good question. Actually, that was something I wasn't going to get into, but hey, I ended early, so we'll go there. He asked how easy is it for, if you set a lambda to a particular field value, like how easy is it, or for the data segment, how easy is it to get access to other field values? And I think we should pull open the protocol definitions for that. This is actually some... So here's the definition for TCP. And okay, so the arguments that the generator gets are the same arguments that the lambdas you would set to a field get. And this is kind of... This is like an interface, but it's hard to enforce interfaces in scheme, which is actually its main drawback. So you kind of just have to behave correctly. But that's something you kind of have to... You just have to deal with it. But the arguments here, what they mean, packet is the packet that's been generated so far. It contains the byte buffer that's been aggregated up to this point. Fields is a list of the field definitions that come in from the protocol. So you can look up fields by name, you can get their bit length, and you can get their generator and serializer, or their serializer and validator from this. VEX is a list of vectors for all of the values of the fields that have been evaluated so far. And then the data segment is accessible from every layer at all times. And you have to do that in order to figure out things like total header length. So yeah, basically you can get to everything else from everywhere. Sorry? Okay. So in this example I actually just wrote a function that calls into the framework to send a packet out. So you could define timing in your own function. But I'm not currently providing anything that deals with timing. Actually one thing that I'm very interested in doing is instead of generating packets directly to the wire, is generating a pcap file. And also parsing pcap for later playback. So when I get into that, I'll have to respect timestamps in pcap files and have some kind of timing loop. But that would be a missing bullet point from the future work slide. It definitely doesn't do that right now. But hey, if that's something you're interested in, we should definitely talk about it. Anything else? Oh yeah, maybe I should start drinking more. Okay, well personally I run Gen2 and I run LSX. So I know that this works on Gen2, but I'm not doing anything funky. I'm using standard Linux interfaces. So it works on Gen2, it works on LSX. For future work, I definitely need to port it to Windows for the socket generator. So I think that's my time. Oh, that was five minutes. Oh, okay, cool. Three minutes. Anyone else? So I can just randomly flip through some of the protocol definitions. Ethernet, relatively simple. About a page long. IPv4. This is the checksum code. If you're not used to scheme, basically I'm just setting a buffer to the result of the default generator and setting a variable called checksum to the calculated CRC16 checksum of that buffer. And then U8 vector copy is about equivalent to memcopy. And see. This is a little bit more of a mess. Actually, as of two days ago, I kind of... There was a rule that the lambdas you passed in for field values would return strings that then had to get serialized. But I actually made... I made it possible for you to pass in a lambda that just generates a U8 vector and then it bypasses the serializer. So if anybody actually can read scheme, this definitely can be rewritten to be a line long now. But the more I work on this, the smaller the code gets. Oh, actually, hey, one other thing. The other thing that's cool about this, I completely forgot, is that you can just compile these to C files. Demo one and demo two is the non-randomized one and the randomized one. And then they just show up in the bin directory. So actually, I'll just restart this so it's cleared out. And check this out. This is the first demo. This is the one that generates one 8-byte packet and one 16-byte packet. Nope, requires some pseudo. But hey, there they are. All right? So that's no longer scheme. That's actually a compiled file. I'm pulling in a lot of libraries, regular expression stuff. So it ends up being about 137K. But that could be anything. Like the cost of additional scripting over just pulling in the libraries is minimal. For example, a slightly more complicated example is the randomized field one, which is demo two. You can see the packets show up in Wartishark. All right? And that comes in at 141K.