busted.systems http://busted.systems Elegant software solutions Bypassing At&t U-verse hardware NAT table limits http://busted.systems/Bypassing-At-t-U-verse-hardware-NAT-table-limits 12 Dec 2015 At&t U-verse comes with a vendor-supplied router that is designed for home-use, even when having a "small business" contract. This shows in the router's config interface, designed for non-tech-savvy people, but also in terms of a bunch of limitations. The main problem, especially when used in a business context, is a limitation of the router's NAT table, capped at 2048 sessions (with the At&t support claiming already flaky behaviour when over 1000). Not sure why At&t, defining small business as up to 50 people, is even selling this, as a lot of them will end up calling support very soon, complaining about intermittent packet loss.

The box provides some passthrough modes for people that run their own routers, but although it sounds like this would easily avoid that limitation, it doesn't. For whatever not-so apparent reason, all of those passthrough modes actually just route, still filling up the NAT table. Some older firmwares had an exploitable vulnerability, which allowed to root some models and enable a true bridge mode, but newer versions plugged this; and ideally we would like to have a solution for a business environment, that doesn't need tampering with At&t's equipment, anyways.

Looks like many others are having the same problem (across different router models), debating workarounds, but no real hands-off solution was found that doesn't involve to either root the router or having to re-rig things occasionally.

One solution that does work and is easy to set up is to tunnel all the office's outgoing traffic, exactly generating one entry in the At&t box' NAT table. However, although a valid solution, this post will try to focus on an internal-network-only workaround. Also, such a tunnel wouldn't necessarily fix the problem for inbound connections, either.

Why can't we just use our own hardware? Well, the U-verse home-use uplinks require 802.1X authentication, with therefore a certificate that is on the router. It additionally sends every 24h a CWMP periodic inform message to At&t, which we probably should keep sending, also. I think it would be possible to open the router and dump the box' ROM contents to get a hold of the cert, then reimplement the logic on a better box. Or, if the limitation is software based, we could even attempt flashing the router with a modified version, as the firmwares used seem to be all open source and available (excluding the cert, of course). However, both approaches would be tampering with their equipment.

All of this means, that since we don't want to tamper with the router, it has to stay part of the equation.

So, let's try to have the At&t router still connected to the uplink to do the authentication and heartbeat, but not pass any real traffic through it, that doesn't need to. Basically, we want the following:


                       uplink
                         │
                         │
                 ┌──── magic ────┐
                 │               │
                 │             At&t
                 │            router
              intranet

Note that the traffic that goes from uplink to the intranet does not pass through the At&t box, at all. The latter will simply stay attached to the uplink (whether you use fiber, ONT, etc., in my case it was a fiber cable), but that's the only link connected to it. No other cable will be attached to it.

So, magic now has to split the traffic into:

  • all 802.1X/EAP and management traffic should be allowed to and from the At&t box
  • everything else should flow between uplink and the intranet

Let's hook up a box in between the At&t router and the uplink, first, so we can intercept the traffic. It doesn't really matter what type of box it is, but needs to be more programmable/flexible than your standard managed switch's or router's vendor GUI/CLI. In my case, an Ubiquiti EdgeRouterPro was used, as it allows full Linux shell access.

So, with the At&t box connected to... let's say eth6, and the uplink connected to eth7, let's just bridge those interfaces (br0), so the At&t box can talk to the uplink. Turns out that the 802.1D standard (which is about bridging), says that standards compliant bridges aren't supposed to pass MAC addresses in the range of 01:80:c2:00:00:00 to 01:80:c2:00:00:0f. 802.1X uses 01:80:c2:00:00:03, so it's effectively not going through the bridge. So much about the general assumption that a bridge is just a "virtual switch"... not.

Anyways, looks like we can enable this on our bridge br0, so if this is a >=3.2 kernel:

echo 8 > /sys/class/net/br0/bridge/group_fwd_mask

If this is a pre-3.2 kernel, some folks maintained a patch to achieve the same.

With this out of the way, the At&t router, with an uplink connection going through magic, should now sync with the At&t uplink just fine (and all sync/broadband LEDs turn steady green).

On to the next step. What's basically missing is to now direct all incoming traffic to the correct next hop, depending on whether it's for the At&t box itself (802.1X, management traffic, etc.), or else. This is luckily fairly easy to figure out, on paper. The way this is setup for a U-verse "small business" contract that comes with 5 public IPs, is the following:

  • the At&t box gets provisioned with a static IP, which the support guy called "street IP", which "usually doesn't change, except if the phyisical wiring changes or something like that"; note that that IP is different from our 5 public ones
  • this is the IP assigned to the box itself, so everything sent to the box itself is using that one
  • the rest, with one of our 5 public IPs as destination, is also sent to the At&t box, for further routing
  • in other words, everything from the outside is passed to the At&t box, as either destination or to be routed for our public IP subnet; this means in layer 2 terms, that every incoming ethernet frame has the At&t box' MAC as destination address
  • for outgoing traffic, everything coming from the intranet needs to go directly to the uplink (which sounds obvious, but given that the gateway address for our public IP block ARPs to the At&t router, we also need a rule on magic to redirect those packets, so they actually go out and don't get dropped on the WAN interface of the At&t router)

Note that the last bullet point mentioned the gateway address - as a sidenote here: we will use the same gateway address with this setup as we would by using the At&t box the default way (running all traffic through it). This will be the address used by the intranet as internet gateway. This address can be looked up in the At&t box' configuration, if you don't know it - it definitely has to match the one in the configuration, though, or the below won't work. The benefit here is that this allows for removing magic at any time from the setup, replugging everything in the old way, and it will work (except for then having NAT table limitations again).

So, what we want to do is to filter on layer 2 by matching on destination IP address, and rewrite the MACs to either go to the At&t box, or to the intranet. Well, for the latter, we need a destination now, so we need a box there, somewhere, as router, with one of our public IPs set. This would be your main firewall (in my case it runs simply on the EdgeRouterPRO, also, but virtually separated):


                       uplink
                         │
                         │
                 ┌──── magic ────┐
                 │               │
              router/fw        At&t
                 │            router
              intranet

We can use ebtables to do the traffic-splitting, as this is layer 2 logic. Unfortunately, there is another stumbling block. At&t seems to use (not sure if always) a VLAN, here, probably per customer.
The current version of ebtables can either match layer 3 addresses but not VLAN tags, or vice versa, but not both at the same time. So we need to strip the VLAN/802.1Q tag from the ethernet frames, first, to be able to make use of ebtables. To do that, we need to figure out the tag they assigned to us, first.

So, on magic's eth7, run a tcpdump with -e, and make sure some traffic goes through there (e.g. plug a machine into the LAN ports of the At&t router, and visit a website, or so):

tcpdump -ei eth7

Output will be something like this, with the VLAN tag displayed

16:36:56.631607 10:20:30:40:50:60 (oui Unknown) > 60:50:40:30:20:10 (oui Unknown), ethertype 802.1Q (0x8100), length 147: vlan 2, p 0, ethertype IPv4, 1.2.3.4.5555 > 4.3.2.1.7777: UDP, length 101

So, in our case it's VLAN 2 that At&t assigned to us. Let's create VLAN interfaces eth6.2 and eth7.2, and also add them to br0. Now we can run ebtables rules on eth7.2 with matching of IP address, as this interface receives the incoming eth7 traffic with the VLAN tag removed. Before we get to the rules, we need to additionally add to our bridge the interface that links to the intranet. Let's say this is eth5, so add eth5 to br0 for our example. Then for ebtables:

# This dnats destination MAC addresses on eth5 for our public IP subnet to the uplink MAC.
ebtables -t nat -A PREROUTING -p IPv4 -i eth5 --ip-src $OUR_PUB_IP_RANGE -j dnat --to-dst $MAC_ATT_UPLINK --dnat-target ACCEPT

# This will set the firewall box' MAC as destination for packets coming from the ISP. Do
# it on eth7.vlan, so ebtables can match on IP (wich it can only on packets with VLAN
# tag stripped).
ebtables -t nat -A PREROUTING -p IPv4 -i eth7.2 --ip-dst $OUR_PUB_IP_RANGE -j dnat --to-dst $MAC_INTERNALFW --dnat-target ACCEPT

# This snats source MACs that go to the uplink (all of them) to the at&t box' MAC,
# to spoof it coming from there. This makes the packet go out to the ISP.
ebtables -t nat -A POSTROUTING -d $MAC_ATT_UPLINK -j snat --to-src $MAC_ATT_RG_WAN --snat-target ACCEPT

As you can see, there are some blanks to fill in, namely the following variables:

OUR_PUB_IP_RANGEthe public IP block given to you from At&t, e.g. 1.2.3.4/29
MAC_INTERNALFW MAC address of interface connected to eth5, the interface of the internal firewall box
MAC_ATT_RG_WAN MAC address of the At&t box' WAN interface; usually printed on At&t box' back
MAC_ATT_UPLINK MAC address of uplink hop's equipment, see below

In order to figure out MAC_ATT_UPLINK, which is the MAC address of the device at the other end of the cable coming out of your wall, you can use the following on magic:

brctl showmacs br0

This will list all interfaces that are part, or directly attached to br0:

port no mac addr                is local?       ageing timer
  5     44:77:44:77:44:77       no                 0.00
  1     10:20:30:40:50:60       no                 0.00
  1     00:11:77:44:22:05       yes                0.00
  2     00:11:77:44:22:06       yes                0.00
  4     00:11:77:44:22:07       yes                0.00
  3     a4:7a:77:a7:7a:77       no                44.82

Filtering out the three local addresses, which are the bridge interfaces eth5, eth6 and eth7 (note that eth6.2 and eth7.2 use the same MACs as eth6 and eth7), three others are left. Two of those are addresses we know, namely the firewall's (here: 10:20:30:40:50:60) and the At&t box' MAC (let's say this is a4:7a:77:a7:7a:77). The only one left is 44:77:44:77:44:77 in our example, which is the one we are looking for. (If you have more than one remaining line, this might come from having had something else plugged in, which the bridge learned. In that case, just check the ageing timer column for an ever increasing value, and let it time out. Eventually there should be only one address left.)

Now, put together your ebtables rules and give it a test.

This is basically it. You might want to think of the following though:

  • At&t might change the uplink hardware, so MAC_ATT_UPLINK might change; so it's a good idea to have your brctl showmacs br0 in some cronjob to update the uplink MAC in case of it changing
  • theoretically, At&t might also change the VLAN id, however, I don't think this is realistic as it's already an abstraction and easier to keep for them than when replacing hardware
  • be aware that every time you destroy and recreate br0, the group_fwd_mask needs to be reset to let 802.1X traffic pass

UPDATE (2016-07-22): new methods to root the At&t box were discovered, in the meantime (see comments, below); so the statement in the introduction doesn't fully hold, anymore

UPDATE (2016-10-02): as reported by others in the comments below, there are uplink setups not using any VLAN tagging, but still using 802.1Q frames with special value of 0 for the VLAN ID, using it as a priority tag, only. See comments below for more info.

]]>
File rescue with dd and gawk http://busted.systems/File-rescue-with-dd-and-gawk 11 Sep 2015 I recently had to undelete some accidentally deleted pictures on some SD card, after the owner of it was trying out different tools, and even brought it to some computer store (which tried more tools), but was only able to recover half of them. It was clear that the files have been deleted, only, but not overwritten, as he noticed his mistake immediately and refrained from using the card afterwards. The way default deletion usually works, means that pretty much everything still had to be recoverable.

Turns out it was, and even without any rescue tool. When I started looking into it, the first tool I saw in the FreeBSD ports was magicrescue, but somehow no matter what I tried, it always exited with the same error. Looking at the man-page I noticed right at the beginning:

It looks at "magic bytes" in file contents, [...] It works on any file system,
but on very fragmented file systems it can only recover the first chunk of each
file.  These chunks are sometimes as big as 50MB, however.

So, the tool is file-system agnostic, it seems to only look for some sequence of bytes and then to recover some sequential number of bytes. This also means that very complex file-systems or features like compression and deduplication will obviously not be suited for recovery with magicrescue.

Makes sense. And that applies also to my case: the card had a FAT32 file system on it (like probably most cameras use), meaning there won't be any fancy file system features. Also, given that a camera stores one picture after the other (and if people delete some it's often always the last right after taking it), there probably also is little fragmentation.

So, basically, all I need to do is read all bytes off of the card, and split on certain patterns. split(1) unfortunately doesn't help, as although you can use a pattern for splitting, it's only matching on entire lines.
Inspecting the first few megabytes on the SD card revealed, that the images on there are stored as Exif-JPEG files (starting with magic numbers 0xff 0xd8, and then 0xff 0xe1 for this subtype, details here). This is not something general purpose, of course. And even for this one type of JPEG file not something to rely on, but I didn't want to split on 0xff 0xd8, only (to keep false positives low), and assumed that the camera wrote all images in the same format/way.

Completely ignoring the end-markers of JPEG files, accepting that the recovered images might have some garbage data appended, I started splitting the data on the SD card up on those 4 byte patterns. And that works quite nicely with dd and gawk (note, POSIX awk won't work, as the record separator can only be one byte):

dd if=$SRC of=/dev/stdout bs=1M | \
  gawk 'BEGIN { FS="fs is not important"; RS="\xff\xd8\xff\xe1" } { print RS$0 > sprintf("%04d.jpg", NR) }'

Of course, set $SRC to the device you want to recover your files from.

That's it - I was able to recover every single image off of that card, with a shell one-liner! Of course, this is a specific case that made this possible: simple file system, no fragmentation, only JPEG files to recover, and only one JPEG type to look for, etc., but it can easily extended to suit other purposes.

Here's a little bit more convenient version as a shell script, allowing to seek, set the size to recover, and an optional prefix for the recovered images (still only looking for the same 4 bytes to separate on, though):

#!/bin/sh
if [ $# -lt 3 ]; then echo Usage: $0 DEV SIZE_MB SEEK_MB OUT_PREFIX; exit; fi
dd if=$1 of=/dev/stdout bs=1M count=$2 iseek=$3 | gawk 'BEGIN { FS="fs is not important"; RS="\xff\xd8\xff\xe1" } { print RS$0 > sprintf("'$4'%04d.jpg", NR) }'
]]>
Converting my work laptop to btrfs http://busted.systems/Converting-my-work-laptop-to-btrfs 03 Sep 2015

The more I read about containers, the more clear it is that btrfs is the future filesystem for DevOps work. This is because btrfs' copy-on-write behavior means that provisioning the disk for a new container is just a 'btrfs snapshot' operation under the hood, and only the blocks that I change use additional space. I've been working more with LXD, which enables this behavior when it detects that /var/lib/lxd is on a btrfs filesystem.

I've read some articles, but it's time to take the plunge. The candidate system for this is my work laptop, since I like to run some local containers. Also, unlike my personal laptop, the disk isn't encrypted, so there's a layer I don't have to reason about.

The first time I considered this, I planned how to do it the old painful way: Install a new OS onto btrfs on a free partition, then over time re-install everything and migrate data from the old partition as needed. Then when I was satisfied that I had everything I needed from the old install, I could expand onto the old partition using btrfs' RAID 0 capability.

The thing is, btrfs provides a new way: the btrfs-convert utility can turn an ext2/3/4 filesystem into a btrfs filesystem in place. This works since btrfs doesn't put its superblocks in fixed places, so it can put the new superblocks around the old ones. The tool even has a --rollback flag if you want to go back to ext2/3/4 after the conversion. I'm committed enough to my current setup that this conversion seems like the best way forward.

Prep work is done

The target machine already has a btrfs-aware kernel, which I know from some toy filesystems created in loopback files.

GRUB 2 is installed. I expect some futzing will be required to make GRUB understand btrfs, but I've configured GRUB modules before (for LVM I think).

I have System Rescue CD on a thumb drive (at all times, on my key ring!) I also have an external USB drive with much free space.

The plan is to boot the machine with the rescue distro, then back up the whole disk to the external USB drive using dd. I'll run btrfs-convert, then adjust my fstab and bootloader as needed.

I need the machine for work tomorrow, so I have to be fully rolled-forward or fully rolled-back by morning. Let's go!

Insurance

I boot into System Rescue CD and attach the external drive. One partition has 1.5T free, more than enough for my dd operation.

mount /dev/sdc1 /mnt/gentoo
cd /mnt/gentoo
dd if=/dev/sda of=system76bak.img
In another terminal (Alt-F2) I used watch ls -hl /mnt/gentoo/system76bak.img to view the copy. It's also possible to periodically kill -USR1 2431 to cause the dd process (the last arg) to output its progress. This took two and a half hours for 250G on eight cores, but is worth every minute for the peace of mind.

Conversion

System Rescue CD has btrfs-progs version 3.18.2. This is good enough for me, since 3.14.2 is the latest in Gentoo's stable branch. I just didn't want to use a super-old btrfs-convert, and I'm satisfied I won't.

It was as simple as

btrfs-convert /dev/sda2

I'm a little concerned that the original filesystem was at 93% capacity. I can clear off a few gigs here and there if I need to, but certainly a disk-nearly-full condition is a dealbreaker for an operation like this. I'm counting on the rollback flag in that case. It will help if deduplication is part of this process.

Of course I only saw the -p (show progress) flag after hitting enter. This could take hours, yes? I'll never know now.

Full output:

root@sysresccd /root % btrfs-convert /dev/sda2
creating btrfs metadata.
creating ext2fs image file.
cleaning up system chunk.
conversion complete.
    Observations:
  • Done in 32 minutes without error!
  • The old filesystem has been made available at /ext2_saved/image (178G)
  • The new filesystem shows 94% full now (was 93% before the conversion). That's a remarkably efficient operation.

Make it bootable

First I'll fix /etc/fstab, the easy part right? blkid now gives me a UUID="..." and a SUB_UUID="..." I'm sure the subvolume ID is valid as a mount point, but let's confirm this on the web. Both Arch and Gentoo wikis state UUID, and both remind me that the last field should be 0 to disable fsck on boot.

Now let's update GRUB. We'll do it from within, so chroot in the normal way.

mount --rbind /dev /mnt/gentoo/dev
mount --rbind /proc /mnt/gentoo/proc
chroot /mnt/gentoo /bin/bash

Re-install GRUB

grub2-install --modules=btrfs /dev/sda

There were some "device node not found" messages, but at the end it claimed to encounter no errors.

Finally, let's make sure the grub.cfg has the UUID for the correct volume

grub2-mkconfig -o /boot/grub/grub.cfg

Again, more "device node not found" messages. But let's take our chances and reboot, since it might just work now.

And, voila! It booted immediately into the converted filesystem the first time. I had to check the output of "mount" to be sure that it was really working. Well done, btrfs devs, on making the conversion as intuitive and painless as possible!

Postscript

The following day at the office, the system twice hung on disk I/O to the point of requiring a hard reboot. I could, for instance, type in a terminal until I did something that required a disk read (e.g. attempt a tab completion), and then that terminal was hung, until they all were.

I had the discard mount option enabled, and have disabled it since reading some warnings about the discard action fully blocking the disk, which sounds a lot like the hang I was experiencing. I'll have to do manual TRIMs. I've also enabled the ssd mount option (which is unrelated to discard). Let's hope for no more hangs!

Somewhat unrelated, a co-worker recommended ncdu, that is "ncurses du", for the problem of quickly identifying what's using all your disk space. I later freed up over 100G - if I knew it was so easy, I'd have done it before my dd backup. ]]> Use GNU screen to hide your login http://busted.systems/Use-GNU-screen-to-hide-your-login 27 Jun 2015 A friend of mine showed me a cool trick to hide your login somewhat, using GNU screen. What I can tell from online searches, this doesn't look like to be that known... GNU screen has command line options -l and -ln (or command C-a L to toggle) to control the window's login behaviour. From the man page:

-l and -ln
    turns login mode on or off (for  /etc/utmp  updating).   This  can
    also be defined through the "deflogin" .screenrc command.
C-a L       (login)       Toggle this  windows  login  slot.  Available
                          only  if  screen  is configured to update the
                          utmp database.

The benefits we get from this is, once turned off, we are still controlling screen, so we are logged-in, but there is no record anymore in the user accounting login records. So, lets see, starting screen and running who(1):

$ who
dude            ttyv0    Jun 27 08:51
dude            pts/0    Jun 27 08:52 (:0)
dude            pts/1    Jun 27 08:52 (:0)
dude            pts/3    Jun 27 10:45 (:0)

Now after toggling login mode with C-a L:

$ who
dude            ttyv0    Jun 27 08:51
dude            pts/0    Jun 27 08:52 (:0)
dude            pts/1    Jun 27 08:52 (:0)
Also, other commands like w(1) on FreeBSD behave similar. However, it's not all hidden, of course. The screen process shows up with ps(1), along with user that runs it, and I'm sure there are more ways... e.g. on FreeBSD I can use getent(1) to get some idea of what's going on:
$ getent utmpx active
[1435387900.738886 -- Sat Jun 27 08:51:40 2015] user process: id="74a6f9bdc43b89a6" pid="1631" user="dude" line="ttyv0" host=""
[1435387921.482140 -- Sat Jun 27 08:52:01 2015] user process: id="cdab068901338b19" pid="1976" user="dude" line="pts/0" host=":0"
[1435387922.927262 -- Sat Jun 27 08:52:02 2015] user process: id="0eb0980869dbcdaf" pid="2027" user="dude" line="pts/1" host=":0"
[1435394277.047666 -- Sat Jun 27 10:45:57 2015] user process: id="2f33000000000000" pid="1518" user="dude" line="pts/3" host=":0"

And after toggling, it still shows the same number of entries, with a slight suspicious difference:

$ getent utmpx active
[1435387900.738886 -- Sat Jun 27 08:51:40 2015] user process: id="74a6f9bdc43b89a6" pid="1631" user="dude" line="ttyv0" host=""
[1435387921.482140 -- Sat Jun 27 08:52:01 2015] user process: id="cdab068901338b19" pid="1976" user="dude" line="pts/0" host=":0"
[1435387922.927262 -- Sat Jun 27 08:52:02 2015] user process: id="0eb0980869dbcdaf" pid="2027" user="dude" line="pts/1" host=":0"
[1435394277.047666 -- Sat Jun 27 10:45:57 2015] user process: id="2f33000000000000" pid="1518"

What I don't understand, though, is how this actually works. On FreeBSD I can achieve a similar effect using utxrm(8) by modifying the accounting database, directly. However, this is limited to root. At first I thought that screen has those permissions as it is by default installed with the setuid (or setgid on some distributions) bit set, which is necessary to make multiuser sharing work. However, a test shows that it still works after removing that bit...

Oh well, the answer is probably in what the manpage states, that this only works "if screen is configured to update the utmp database", which on all systems I touched seems to be the case.

Pretty neat... unfortunately, tmux doesn't have this feature. :(

]]>
A racing game in Rust http://busted.systems/A-racing-game-in-Rust 18 May 2015 In celebration of Rust's 1.0 release, here's my first rust program. I'm not up to a full rust tutorial, so instead I'll just dump this and hope it's instructive for someone.

There's really not much to say about the program. There's a Box<> wrapping type that's not necessary, except a co-worker dared me to heap-allocate some things. He wanted to make a point that dealing with a lot of dynamic, heap-allocated objects is where Rust's lifetime system becomes difficult and unwieldy. I didn't find that, but this program is admittedly quite trivial.

A curses program that uses sleep() for its game loop requires a second thread for taking input, so there's a channel setup.

I'm a little ashamed of the unwrap() calls, since they represent unhandled null checks, and negate some benefits of using a language that claims memory safety as a priority. That's on me, though, since I could check my Option<> return values instead of taking the easy way out.

src/main.rs

#![feature(collections)]

extern crate collections;
extern crate ncurses;
extern crate rand;

use std::char;
use std::thread;
use std::sync::mpsc::channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::TryRecvError;
use collections::vec_deque::VecDeque;
use ncurses::*;
use rand::Rng;

const FRAMERATE: u32 = 2;
const MAP_VISIBLE_WIDTH: usize = 10;
const ROAD_WIDTH: usize = 3;
const HILL_CHANCE_ONE_IN_X: u32 = 7;

fn get_road_char(terrain: &Terrain) -> u64 {
    match terrain {
        &Terrain::None => '_' as u64,
        &Terrain::Hill => 'A' as u64,
    }
}

fn is_crash(player: &PlayerState, map: &VecDeque<Box<MapSlice>>) -> bool {
    match map.get(player.x as usize).unwrap().cells[player.y as usize] {
        Terrain::None => false,
        _ => true
    }
}

fn draw(player: &PlayerState, map: &VecDeque<Box<MapSlice>>) {
    clear();

    for i in 0..map.0 {
        let &slice = &map.get(i).unwrap();
        for j in 0..slice.cells.0 {
            mvaddch(3+j as i32,3+i as i32,get_road_char(&slice.cells[j]));
        }
    }

    mvaddch( 3+player.y, 3+player.x, match is_crash(player,map) {
        true => '*' as u64,
        _ => '>' as u64
    });

    mvaddstr( 8, 3, "a and d to move, q to quit");
}

struct PlayerState {
    x: i32,
    y: i32,
}

enum Terrain {
    None,
    Hill
}

// a single vertical slice of the map
struct MapSlice {
    cells: [Terrain; ROAD_WIDTH]
}

fn gameloop(rx: Receiver<char>) {
    let mut player = PlayerState { x: 0, y: 1 };
    let mut map = VecDeque::<Box<MapSlice>>::new(); // Box is gratuitous heap allocation
    let mut rng = rand::thread_rng();

    // Start game loop
    loop {

        while map.0 < MAP_VISIBLE_WIDTH {

            // TODO: populate in a less repetitive way?
            let cells = [match rng.gen_weighted_bool(HILL_CHANCE_ONE_IN_X) {
                true => Terrain::Hill,
                _ => Terrain::None,
            },match rng.gen_weighted_bool(HILL_CHANCE_ONE_IN_X) {
                true => Terrain::Hill,
                _ => Terrain::None,
            },match rng.gen_weighted_bool(HILL_CHANCE_ONE_IN_X) {
                true => Terrain::Hill,
                _ => Terrain::None,
            }];
            let slice = Box::new(MapSlice { cells: cells });
            map.push_back(slice);
        }

        // Read all input characters since last '
        let mut quit = false;
        while !quit {
            let res = rx.try_recv();
            match res {
                Ok(val) => {
                    printw(format!("Got {}\n",val).as_ref());
                    match val {
                        'q' => quit = true,
                        'a' => {
                            player.y -= 1;
                            if player.y < 0 { player.y = 0 }
                        },
                        'd' => {
                            player.y += 1;
                            if player.y > 2 { player.y = 2 }
                        },
                        _ => ()
                    }
                },
                Err(TryRecvError::Empty) => break,
                Err(TryRecvError::Disconnected) => panic!("Channel disconnected")
            };
        }

        draw(&player,&map);
        refresh();

        if quit || is_crash(&player,&map) { break }

        map.pop_front();

        // Sleep until next '
        thread::sleep_ms(1000/FRAMERATE);
    }

}

fn main() {

    // ncurses screen initialization
    initscr();
    clear();
    noecho();
    curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);

    // Make a channel for input thread to send characters to main thread
    let (tx,rx) = channel();

    // start input thread
    {
        let tx = tx.clone();
        thread::spawn(move || {
            loop {
                let c: i32 = getch();
                tx.send(char::from_u32(c as u32).unwrap()).unwrap();
            }
        });
    }

    refresh();

    gameloop(rx);

    thread::sleep_ms(2000);

    // Clean up
    endwin();
}

Cargo.toml

[package]
name = "rustcurse"
version = "0.1.0"
authors = ["Erik Mackdanz "]

[dependencies]
ncurses="5.73.0"
rand="0.3.8"
]]>