ppp linux kernel module
| These are some brief notes on the structure and function of the PPP linux kernel
module. This is work in progress. The purposes of this web page include:
The PPP module has a user-space interface which is used by pppd. 2003-9-19: I created this web page as a sketchpad for discussing a query I had from a colleague in November 2002 about how to modify the PPP kernel software for TX packet queueing, in particular to implement PPP suspend/resume. Later I used this understanding to implement ROHC/PPP. So this web page has already fulfilled its objective. In the unlikely event that anyone else finds this web page useful, you're welcome. |
| Structure of the linux `ppp' kernel module. The place to start is the
directory /usr/src/linux/. arch CPU-dependent stuff drivers device drivers fs file systems include header files init boot-time code ipc inter-process communication (shared memory etc.) kernel kernel core software lib [could be anything] mm memory (RAM) management net network protocols scripts [don't ask me] From the point of view of ppp, only the directories "drivers", "include", and "net" are directly relevant, although there are many interactions with the kernel core of course. These files in the drivers/net directory are relevant to PPP: drivers/net/bsd_comp.c drivers/net/ppp_async.c drivers/net/ppp_deflate.c drivers/net/ppp_generic.c drivers/net/ppp_synctty.c drivers/net/pppoe.c drivers/net/pppox.c drivers/net/zlib.c These header files are directly imported by the ppp*.c files: "zlib.c" asm/atomic.h asm/uaccess.h linux/config.h linux/devfs_fs_kernel.h linux/errno.h linux/etherdevice.h linux/file.h linux/filter.h linux/if_arp.h linux/if_ether.h linux/if_ppp.h linux/if_pppox.h linux/if_pppvar.h linux/inetdevice.h linux/init.h linux/ip.h linux/kernel.h linux/kmod.h linux/list.h linux/module.h linux/net.h linux/netdevice.h linux/notifier.h linux/poll.h linux/ppp-comp.h linux/ppp_channel.h linux/ppp_defs.h linux/proc_fs.h linux/rtnetlink.h linux/sched.h linux/skbuff.h linux/slab.h linux/smp_lock.h linux/spinlock.h linux/string.h linux/tcp.h linux/tty.h linux/vmalloc.h net/slhc_vj.h net/sock.h The above listing is obtained with this command in the /usr/src/linux directory.
fgrep -h #include drivers/net/ppp*.c | sort | uniq | awk '{ print $2 }' | sed 's/[<>]//g'
Apart from the zlib.c file, all of the above headers are imported from the `include'
directory, and most are from `include/linux', which is the kernel directory which is made
available to application software as an API to the kernel. The directory net/ipv4 contains most of the Internet stack (TCP/IP and UDP/IP etc.)
|
| List of functions in drivers/net/ppp_generic.c. The following functions are found in the file `ppp_generic.c': static inline int proto_to_npindex(int proto) static inline int ethertype_to_npindex(int ethertype) static int ppp_open(struct inode *inode, struct file *file) static int ppp_release(struct inode *inode, struct file *file) static ssize_t ppp_read(struct file *file, char *buf, size_t count, loff_t *ppos) static ssize_t ppp_file_read(struct ppp_file *pf, struct file *file, char *buf, size_t count) static ssize_t ppp_write(struct file *file, const char *buf, size_t count, loff_t *ppos) static ssize_t ppp_file_write(struct ppp_file *pf, const char *buf, size_t count) static unsigned int ppp_poll(struct file *file, poll_table *wait) static int ppp_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) static int ppp_unattached_ioctl(struct ppp_file *pf, struct file *file, unsigned int cmd, unsigned long arg) int __init ppp_init(void) static int ppp_start_xmit(struct sk_buff *skb, struct net_device *dev) static struct net_device_stats *ppp_net_stats(struct net_device *dev) static int ppp_net_ioctl(struct net_device *dev, struct ifreq *ifr, int cmd) int ppp_net_init(struct net_device *dev) static void ppp_xmit_process(struct ppp *ppp) static void ppp_send_frame(struct ppp *ppp, struct sk_buff *skb) static void ppp_push(struct ppp *ppp) static int ppp_mp_explode(struct ppp *ppp, struct sk_buff *skb) static void ppp_channel_push(struct channel *pch) static inline void ppp_do_recv(struct ppp *ppp, struct sk_buff *skb, struct channel *pch) void ppp_input(struct ppp_channel *chan, struct sk_buff *skb) void ppp_input_error(struct ppp_channel *chan, int code) static void ppp_receive_frame(struct ppp *ppp, struct sk_buff *skb, struct channel *pch) static void ppp_receive_error(struct ppp *ppp) static void ppp_receive_nonmp_frame(struct ppp *ppp, struct sk_buff *skb) static struct sk_buff *ppp_decompress_frame(struct ppp *ppp, struct sk_buff *skb) static void ppp_receive_mp_frame(struct ppp *ppp, struct sk_buff *skb, struct channel *pch) static void ppp_mp_insert(struct ppp *ppp, struct sk_buff *skb) struct sk_buff *ppp_mp_reconstruct(struct ppp *ppp) int ppp_register_channel(struct ppp_channel *chan) int ppp_channel_index(struct ppp_channel *chan) int ppp_unit_number(struct ppp_channel *chan) void ppp_unregister_channel(struct ppp_channel *chan) void ppp_output_wakeup(struct ppp_channel *chan) static int ppp_set_compress(struct ppp *ppp, unsigned long arg) static void ppp_ccp_peek(struct ppp *ppp, struct sk_buff *skb, int inbound) static void ppp_ccp_closed(struct ppp *ppp) static struct compressor_entry *find_comp_entry(int proto) int ppp_register_compressor(struct compressor *cp) void ppp_unregister_compressor(struct compressor *cp) static struct compressor *find_compressor(int type) static void ppp_get_stats(struct ppp *ppp, struct ppp_stats *st) static struct ppp *ppp_create_interface(int unit, int *retp) static void init_ppp_file(struct ppp_file *pf, int kind) static void ppp_destroy_interface(struct ppp *ppp) static struct ppp *ppp_find_unit(int unit) static struct channel *ppp_find_channel(int unit) static int ppp_connect_channel(struct channel *pch, int unit) static int ppp_disconnect_channel(struct channel *pch) static void ppp_destroy_channel(struct channel *pch) static void __exit ppp_cleanup(void) module_init(ppp_init); module_exit(ppp_cleanup); EXPORT_SYMBOL(ppp_register_channel); EXPORT_SYMBOL(ppp_unregister_channel); EXPORT_SYMBOL(ppp_channel_index); EXPORT_SYMBOL(ppp_unit_number); EXPORT_SYMBOL(ppp_input); EXPORT_SYMBOL(ppp_input_error); EXPORT_SYMBOL(ppp_output_wakeup); EXPORT_SYMBOL(ppp_register_compressor); EXPORT_SYMBOL(ppp_unregister_compressor); EXPORT_SYMBOL(all_ppp_units); /* for debugging */ EXPORT_SYMBOL(all_channels); /* for debugging */ |
| Groups of functions in drivers/net/ppp_generic.c. Below is a set of
categories for the functions in ppp_generic.c.
|
| Load-time entry points in source files drivers/net/*.c. These are the module init/cleanup points for drivers/net/ppp_generic.c: module_init(ppp_init); module_exit(ppp_cleanup); The file ppp_async.c contains similar module entry points: module_init(ppp_async_init); module_exit(ppp_async_cleanup); The file ppp_deflate.c has this: module_init(deflate_init); module_exit(deflate_cleanup); The file ppp_synctty.c contains this: module_init(ppp_sync_init); module_exit(ppp_sync_cleanup); The module_init(...) lines declare entry points for boot/modload time. fgrep module_ drivers/net/ppp*.c You get these entry points: drivers/net/ppp_async.c:module_init(ppp_async_init); drivers/net/ppp_async.c:module_exit(ppp_async_cleanup); drivers/net/ppp_deflate.c:module_init(deflate_init); drivers/net/ppp_deflate.c:module_exit(deflate_cleanup); drivers/net/ppp_generic.c:module_init(ppp_init); drivers/net/ppp_generic.c:module_exit(ppp_cleanup); drivers/net/ppp_synctty.c:module_init(ppp_sync_init); drivers/net/ppp_synctty.c:module_exit(ppp_sync_cleanup); drivers/net/pppoe.c:module_init(pppoe_init); drivers/net/pppoe.c:module_exit(pppoe_exit); drivers/net/pppox.c:module_init(pppox_init); drivers/net/pppox.c:module_exit(pppox_exit); All of the module_init(...) declarations result in boot/load-time invocations. |
| Initialization of the `ppp' kernel module.
devfs_register_chrdev(PPP_MAJOR, "ppp", &ppp_device_fops);
devfs_handle = devfs_register(NULL, "ppp", DEVFS_FL_DEFAULT,
PPP_MAJOR, 0,
S_IFCHR | S_IRUSR | S_IWUSR,
&ppp_device_fops, NULL);
These functions are imported from `include/linux/devfs_fs_kernel.h'.
/**
* devfs_register_chrdev - Optionally register a conventional character driver.
* @major: The major number for the driver.
* @name: The name of the driver (as seen in /proc/devices).
* @fops: The &file_operations structure pointer.
*
* This function will register a character driver provided the "devfs=only"
* option was not provided at boot time.
* Returns 0 on success, else a negative error code on failure.
*/
int devfs_register_chrdev (unsigned int major, const char *name,
struct file_operations *fops)
{
if (boot_options & OPTION_ONLY) return 0;
return register_chrdev (major, name, fops);
} /* End Function devfs_register_chrdev */
An interesting little function!
/**
* devfs_register - Register a device entry.
* @dir: The handle to the parent devfs directory entry. If this is %NULL the
* new name is relative to the root of the devfs.
* @name: The name of the entry.
* @flags: A set of bitwise-ORed flags (DEVFS_FL_*).
* @major: The major number. Not needed for regular files.
* @minor: The minor number. Not needed for regular files.
* @mode: The default file mode.
* @ops: The &file_operations or &block_device_operations structure.
* This must not be externally deallocated.
* @info: An arbitrary pointer which will be written to the @private_data
* field of the &file structure passed to the device driver. You can set
* this to whatever you like, and change it once the file is opened (the next
* file opened will not see this change).
*
* Returns a handle which may later be used in a call to devfs_unregister().
* On failure %NULL is returned.
*/
devfs_handle_t devfs_register (devfs_handle_t dir, const char *name,
unsigned int flags,
unsigned int major, unsigned int minor,
umode_t mode, void *ops, void *info)
/* .... */
Anyway, what this registration does is to register a character device driver with major number 108, and the following `jump table' is registered:
static struct file_operations ppp_device_fops = {
owner: THIS_MODULE,
read: ppp_read,
write: ppp_write,
poll: ppp_poll,
ioctl: ppp_ioctl,
open: ppp_open,
release: ppp_release
};
This module will be called back through the `ppp_open' function, which is invoked by
the application open() function called for the path `/dev/ppp'.
int tty_register_ldisc(int disc, struct tty_ldisc *new_ldisc)
{
if (disc
The function tty_register_ldisc() registers a jump-table of type "tty_ldisc", which is defined in include/linux/tty_ldisc.c as follows:
struct tty_ldisc {
int magic;
char *name;
int num;
int flags;
/*
* The following routines are called from above.
*/
int (*open)(struct tty_struct *);
void (*close)(struct tty_struct *);
void (*flush_buffer)(struct tty_struct *tty);
ssize_t (*chars_in_buffer)(struct tty_struct *tty);
ssize_t (*read)(struct tty_struct * tty, struct file * file,
unsigned char * buf, size_t nr);
ssize_t (*write)(struct tty_struct * tty, struct file * file,
const unsigned char * buf, size_t nr);
int (*ioctl)(struct tty_struct * tty, struct file * file,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct termios * old);
unsigned int (*poll)(struct tty_struct *, struct file *,
struct poll_table_struct *);
/*
* The following routines are called from below.
*/
void (*receive_buf)(struct tty_struct *, const unsigned char *cp,
char *fp, int count);
int (*receive_room)(struct tty_struct *);
void (*write_wakeup)(struct tty_struct *);
};
This has function-pointers for various events including an "open" event,
which causes the async channel definition to be registered.
static struct tty_ldisc ppp_ldisc = {
magic: TTY_LDISC_MAGIC,
name: "ppp",
open: ppp_asynctty_open,
close: ppp_asynctty_close,
read: ppp_asynctty_read,
write: ppp_asynctty_write,
ioctl: ppp_asynctty_ioctl,
poll: ppp_asynctty_poll,
receive_room: ppp_asynctty_room,
receive_buf: ppp_asynctty_receive,
write_wakeup: ppp_asynctty_wakeup,
};
The ppp_write() function accepts raw packets from the pppd program (via /dev/ppp) for
transmission on the PPP link.
/*
* Read does nothing - no data is ever available this way.
* Pppd reads and writes packets via /dev/ppp instead.
*/
static ssize_t
ppp_asynctty_read(struct tty_struct *tty, struct file *file,
unsigned char *buf, size_t count)
{
return -EAGAIN;
}
/*
* Write on the tty does nothing, the packets all come in
* from the ppp generic stuff.
*/
static ssize_t
ppp_asynctty_write(struct tty_struct *tty, struct file *file,
const unsigned char *buf, size_t count)
{
return -EAGAIN;
}
So it seems that there are really only two ways to send data `down' through the PPP
kernel module: either as a raw packet via /etc/ppp or as standard IP traffic through the
internet connection. |
| Opening the /dev/ppp device by pppd.
if (!capable(CAP_NET_ADMIN))
return -EPERM;
return 0;
|
| The ppp_ioctl() function - the initialisation sequence. The ppp_ioctl()
function is where the ppp module is controlled by the pppd program.
static int ppp_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg)
{
struct ppp_file *pf = (struct ppp_file *) file->private_data;
/* ... */
if (pf == 0)
return ppp_unattached_ioctl(pf, file, cmd, arg);
/* ... */
As above, the first thing to happen in ppp_ioctl() is to check if it has been called
before.
switch (cmd) {
case PPPIOCNEWUNIT:
/* Create a new ppp unit */
/* ... */
break;
case PPPIOCATTACH:
/* Attach to an existing ppp unit */
/* ... */
break;
case PPPIOCATTCHAN:
/* ... */
break;
The first case is as follows:
case PPPIOCNEWUNIT:
/* Create a new ppp unit */
if (get_user(unit, (int *) arg))
break;
ppp = ppp_create_interface(unit, &err);
if (ppp == 0)
break;
file->private_data = &ppp->file;
err = -EFAULT;
if (put_user(ppp->file.index, (int *) arg))
break;
err = 0;
break;
In the above code, the integer unit number `unit' is fetched from the caller-indicated
address `arg'.
/*
* Create a new ppp interface unit. Fails if it can't allocate memory
* or if there is already a unit with the requested number.
* unit == -1 means allocate a new number.
*/
static struct ppp *
ppp_create_interface(int unit, int *retp)
{
struct ppp *ppp;
struct net_device *dev;
/* Check to see if the unit is allocated. If it is, return it. */
/* ... */
/* Create a new ppp structure and link it before `list'. */
ppp = kmalloc(sizeof(struct ppp), GFP_ATOMIC);
memset(ppp, 0, sizeof(struct ppp));
dev = kmalloc(sizeof(struct net_device), GFP_ATOMIC);
memset(dev, 0, sizeof(struct net_device));
ppp->file.index = unit;
ppp->mru = PPP_MRU;
init_ppp_file(&ppp->file, INTERFACE);
ppp->file.hdrlen = PPP_HDRLEN - 2; /* don't count proto bytes */
for (i = 0; i
The above (abbreviated) code means that two structures are allocated: a `struct ppp'
and a `struct net_device'. The ppp_net_init() function is called by the network layer to initialise the net_device structure which was registered by the ppp module.
static int
ppp_net_init(struct net_device *dev)
{
dev->hard_header_len = PPP_HDRLEN;
dev->mtu = PPP_MTU;
dev->hard_start_xmit = ppp_start_xmit;
dev->get_stats = ppp_net_stats;
dev->do_ioctl = ppp_net_ioctl;
dev->addr_len = 0;
dev->tx_queue_len = 3;
dev->type = ARPHRD_PPP;
dev->flags = IFF_POINTOPOINT | IFF_NOARP | IFF_MULTICAST;
return 0;
}
The above event-handler functions and data are used by the network layer to know what
to do with packets etc. |
| Outgoing packet handling. To understand how the TX packet
stream works, it is useful to trace all of the TX functions to their callers as follows.
static int
ppp_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct ppp *ppp = (struct ppp *) dev->priv;
int npi, proto;
unsigned char *pp;
npi = ethertype_to_npindex(ntohs(skb->protocol));
if (npi <0) goto outf; /* Drop, accept or reject the packet */ switch (ppp->npmode[npi]) {
case NPMODE_PASS:
break;
case NPMODE_QUEUE:
/* it would be nice to have a way to tell the network
system to queue this one up for later. */
goto outf;
case NPMODE_DROP:
case NPMODE_ERROR:
goto outf;
}
/* Put the 2-byte PPP protocol number on the front,
making sure there is room for the address and control fields. */
if (skb_headroom(skb)
The above code does the following things:
/*
* An instance of /dev/ppp can be associated with either a ppp
* interface unit or a ppp channel. In both cases, file->private_data
* points to one of these.
*/
struct ppp_file {
enum {
INTERFACE=1, CHANNEL
} kind;
struct sk_buff_head xq; /* pppd transmit queue */
struct sk_buff_head rq; /* receive queue for pppd */
wait_queue_head_t rwait; /* for poll on reading /dev/ppp */
atomic_t refcnt; /* # refs (incl /dev/ppp attached) */
int hdrlen; /* space to leave for headers */
struct list_head list; /* link in all_* list */
int index; /* interface unit / channel number */
};
Then the function ppp_xmit_process() is called to deal with the packet as follows:
/*
* Called to do any work queued up on the transmit side
* that can now be done.
*/
static void
ppp_xmit_process(struct ppp *ppp)
{
struct sk_buff *skb;
ppp_xmit_lock(ppp);
ppp_push(ppp);
while (ppp->xmit_pending == 0
&& (skb = skb_dequeue(&ppp->file.xq)) != 0)
ppp_send_frame(ppp, skb);
/* If there's no work left to do, tell the core net
code that we can accept some more. */
if (ppp->xmit_pending == 0 && skb_peek(&ppp->file.xq) == 0
&& ppp->dev != 0)
netif_wake_queue(ppp->dev);
ppp_xmit_unlock(ppp);
}
The above function invokes the following ppp_push() function.
|
| Locking, blocking and timing of the TX packet stream As noted above, an
outgoing (host->network) packet passes through multiple stretches of code on its way to
the network.
At each interface, as in the case of applications sending data or packets via sockets,
a blocking or non-blocking style of interaction may be used.
sent = tty->driver.write(tty, 0, ap->optr, avail);
In other words, it should not block, because that would cause a late return to the IP
layer call. The tty->driver.write() call is sent to the driver for the character device which
PPP is being run on. static struct tty_driver serial_driver, callout_driver; and this bit of code in the module load routine rs_init():
memset(&serial_driver, 0, sizeof(struct tty_driver));
serial_driver.magic = TTY_DRIVER_MAGIC;
#if (LINUX_VERSION_CODE > 0x20100)
serial_driver.driver_name = "serial";
#endif
#if (LINUX_VERSION_CODE > 0x2032D && defined(CONFIG_DEVFS_FS))
serial_driver.name = "tts/%d";
#else
serial_driver.name = "ttyS";
#endif
serial_driver.major = TTY_MAJOR;
serial_driver.minor_start = 64 + SERIAL_DEV_OFFSET;
serial_driver.num = NR_PORTS;
serial_driver.type = TTY_DRIVER_TYPE_SERIAL;
serial_driver.subtype = SERIAL_TYPE_NORMAL;
serial_driver.init_termios = tty_std_termios;
serial_driver.init_termios.c_cflag =
B9600 | CS8 | CREAD | HUPCL | CLOCAL;
serial_driver.flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_NO_DEVFS;
serial_driver.refcount = &serial_refcount;
serial_driver.table = serial_table;
serial_driver.termios = serial_termios;
serial_driver.termios_locked = serial_termios_locked;
serial_driver.open = rs_open;
serial_driver.close = rs_close;
serial_driver.write = rs_write;
serial_driver.put_char = rs_put_char;
serial_driver.flush_chars = rs_flush_chars;
serial_driver.write_room = rs_write_room;
serial_driver.chars_in_buffer = rs_chars_in_buffer;
serial_driver.flush_buffer = rs_flush_buffer;
serial_driver.ioctl = rs_ioctl;
serial_driver.throttle = rs_throttle;
serial_driver.unthrottle = rs_unthrottle;
serial_driver.set_termios = rs_set_termios;
serial_driver.stop = rs_stop;
serial_driver.start = rs_start;
serial_driver.hangup = rs_hangup;
#if (LINUX_VERSION_CODE >= 131394) /* Linux 2.1.66 */
serial_driver.break_ctl = rs_break;
#endif
#if (LINUX_VERSION_CODE >= 131343)
serial_driver.send_xchar = rs_send_xchar;
serial_driver.wait_until_sent = rs_wait_until_sent;
serial_driver.read_proc = rs_read_proc;
#endif
The only important line here is
serial_driver.write = rs_write;
Here is the rs_write() function:
static int rs_write(struct tty_struct * tty, int from_user,
const unsigned char *buf, int count)
{
int c, ret = 0;
struct async_struct *info = (struct async_struct *)tty->driver_data;
unsigned long flags;
if (serial_paranoia_check(info, tty->device, "rs_write"))
return 0;
if (!tty || !info->xmit.buf || !tmp_buf)
return 0;
save_flags(flags);
if (from_user) {
down(&tmp_buf_sem);
while (1) {
int c1;
c = CIRC_SPACE_TO_END(info->xmit.head,
info->xmit.tail,
SERIAL_XMIT_SIZE);
if (count
In include/linux/serial_reg.h are the UART declarations: #define UART_IER 1 /* Out: Interrupt Enable Register */ and /* * These are the definitions for the Interrupt Enable Register */ #define UART_IER_MSI 0x08 /* Enable Modem status interrupt */ #define UART_IER_RLSI 0x04 /* Enable receiver line status interrupt */ #define UART_IER_THRI 0x02 /* Enable Transmitter holding register int. */ #define UART_IER_RDI 0x01 /* Enable receiver data interrupt */
#define SERIAL_XMIT_SIZE PAGE_SIZE The value of PAGE_SIZE is set in include/asm/page.h: #define PAGE_SHIFT 12 #define PAGE_SIZE (1UL << PAGE_SHIFT)In other words, the serial device buffer size is fixed as 4096 bytes. This is a rather large amount of buffer if your serial line is slow!!! (Actually it's 4095 bytes because the last byte is never used.) Here is the serial_out() function in drivers/char/serial.c:
static _INLINE_ void serial_out(struct async_struct *info, int offset,
int value)
{
switch (info->io_type) {
#ifdef CONFIG_HUB6
case SERIAL_IO_HUB6:
outb(info->hub6 - 1 + offset, info->port);
outb(value, info->port+1);
break;
#endif
case SERIAL_IO_MEM:
writeb(value, (unsigned long) info->iomem_base +
(offset<
Generally the default case will be taken (I guess).
/*
* This routine is used to handle the "bottom half" processing for the
* serial driver, known also the "software interrupt" processing.
* This processing is done at the kernel interrupt level, after the
* rs_interrupt() has returned, BUT WITH INTERRUPTS TURNED ON. This
* is where time-consuming activities which can not be done in the
* interrupt driver proper are done; the interrupt driver schedules
* them using rs_sched_event(), and they get done here.
*/
static void do_serial_bh(void)
{
run_task_queue(&tq_serial);
}
static void do_softint(void *private_)
{
struct async_struct *info = (struct async_struct *) private_;
struct tty_struct *tty;
tty = info->tty;
if (!tty)
return;
if (test_and_clear_bit(RS_EVENT_WRITE_WAKEUP, &info->event)) {
if ((tty->flags & (1 << TTY_DO_WRITE_WAKEUP)) && tty->ldisc.write_wakeup)
(tty->ldisc.write_wakeup)(tty);
wake_up_interruptible(&tty->write_wait);
#ifdef SERIAL_HAVE_POLL_WAIT
wake_up_interruptible(&tty->poll_wait);
#endif
}
}
Function do_softint() just wakes up the PPP async software to send more data. |
Go to some notes on linux pppd configuration.
Go to linux kernel links.
Go to linux links.
Go to ROHC (robust header compression) links.
Go to my notes on other linux configuration topics.
Go to Alan Kennington's home page.