523 lines
11 KiB
C
523 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Management Component Transport Protocol (MCTP) - serial transport
|
|
* binding. This driver is an implementation of the DMTF specificiation
|
|
* "DSP0253 - Management Component Transport Protocol (MCTP) Serial Transport
|
|
* Binding", available at:
|
|
*
|
|
* https://www.dmtf.org/sites/default/files/standards/documents/DSP0253_1.0.0.pdf
|
|
*
|
|
* This driver provides DSP0253-type MCTP-over-serial transport using a Linux
|
|
* tty device, by setting the N_MCTP line discipline on the tty.
|
|
*
|
|
* Copyright (c) 2021 Code Construct
|
|
*/
|
|
|
|
#include <linux/idr.h>
|
|
#include <linux/if_arp.h>
|
|
#include <linux/module.h>
|
|
#include <linux/skbuff.h>
|
|
#include <linux/tty.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/crc-ccitt.h>
|
|
|
|
#include <linux/mctp.h>
|
|
#include <net/mctp.h>
|
|
#include <net/pkt_sched.h>
|
|
|
|
#define MCTP_SERIAL_MTU 68 /* base mtu (64) + mctp header */
|
|
#define MCTP_SERIAL_FRAME_MTU (MCTP_SERIAL_MTU + 6) /* + serial framing */
|
|
|
|
#define MCTP_SERIAL_VERSION 0x1 /* DSP0253 defines a single version: 1 */
|
|
|
|
#define BUFSIZE MCTP_SERIAL_FRAME_MTU
|
|
|
|
#define BYTE_FRAME 0x7e
|
|
#define BYTE_ESC 0x7d
|
|
|
|
static DEFINE_IDA(mctp_serial_ida);
|
|
|
|
enum mctp_serial_state {
|
|
STATE_IDLE,
|
|
STATE_START,
|
|
STATE_HEADER,
|
|
STATE_DATA,
|
|
STATE_ESCAPE,
|
|
STATE_TRAILER,
|
|
STATE_DONE,
|
|
STATE_ERR,
|
|
};
|
|
|
|
struct mctp_serial {
|
|
struct net_device *netdev;
|
|
struct tty_struct *tty;
|
|
|
|
int idx;
|
|
|
|
/* protects our rx & tx state machines; held during both paths */
|
|
spinlock_t lock;
|
|
|
|
struct work_struct tx_work;
|
|
enum mctp_serial_state txstate, rxstate;
|
|
u16 txfcs, rxfcs, rxfcs_rcvd;
|
|
unsigned int txlen, rxlen;
|
|
unsigned int txpos, rxpos;
|
|
unsigned char txbuf[BUFSIZE],
|
|
rxbuf[BUFSIZE];
|
|
};
|
|
|
|
static bool needs_escape(unsigned char c)
|
|
{
|
|
return c == BYTE_ESC || c == BYTE_FRAME;
|
|
}
|
|
|
|
static int next_chunk_len(struct mctp_serial *dev)
|
|
{
|
|
int i;
|
|
|
|
/* either we have no bytes to send ... */
|
|
if (dev->txpos == dev->txlen)
|
|
return 0;
|
|
|
|
/* ... or the next byte to send is an escaped byte; requiring a
|
|
* single-byte chunk...
|
|
*/
|
|
if (needs_escape(dev->txbuf[dev->txpos]))
|
|
return 1;
|
|
|
|
/* ... or we have one or more bytes up to the next escape - this chunk
|
|
* will be those non-escaped bytes, and does not include the escaped
|
|
* byte.
|
|
*/
|
|
for (i = 1; i + dev->txpos + 1 < dev->txlen; i++) {
|
|
if (needs_escape(dev->txbuf[dev->txpos + i + 1]))
|
|
break;
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
static int write_chunk(struct mctp_serial *dev, unsigned char *buf, int len)
|
|
{
|
|
return dev->tty->ops->write(dev->tty, buf, len);
|
|
}
|
|
|
|
static void mctp_serial_tx_work(struct work_struct *work)
|
|
{
|
|
struct mctp_serial *dev = container_of(work, struct mctp_serial,
|
|
tx_work);
|
|
unsigned char c, buf[3];
|
|
unsigned long flags;
|
|
int len, txlen;
|
|
|
|
spin_lock_irqsave(&dev->lock, flags);
|
|
|
|
/* txstate represents the next thing to send */
|
|
switch (dev->txstate) {
|
|
case STATE_START:
|
|
dev->txpos = 0;
|
|
fallthrough;
|
|
case STATE_HEADER:
|
|
buf[0] = BYTE_FRAME;
|
|
buf[1] = MCTP_SERIAL_VERSION;
|
|
buf[2] = dev->txlen;
|
|
|
|
if (!dev->txpos)
|
|
dev->txfcs = crc_ccitt(0, buf + 1, 2);
|
|
|
|
txlen = write_chunk(dev, buf + dev->txpos, 3 - dev->txpos);
|
|
if (txlen <= 0) {
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
dev->txpos += txlen;
|
|
if (dev->txpos == 3) {
|
|
dev->txstate = STATE_DATA;
|
|
dev->txpos = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case STATE_ESCAPE:
|
|
buf[0] = dev->txbuf[dev->txpos] & ~0x20;
|
|
txlen = write_chunk(dev, buf, 1);
|
|
if (txlen <= 0) {
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
dev->txpos += txlen;
|
|
if (dev->txpos == dev->txlen) {
|
|
dev->txstate = STATE_TRAILER;
|
|
dev->txpos = 0;
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case STATE_DATA:
|
|
len = next_chunk_len(dev);
|
|
if (len) {
|
|
c = dev->txbuf[dev->txpos];
|
|
if (len == 1 && needs_escape(c)) {
|
|
buf[0] = BYTE_ESC;
|
|
buf[1] = c & ~0x20;
|
|
dev->txfcs = crc_ccitt_byte(dev->txfcs, c);
|
|
txlen = write_chunk(dev, buf, 2);
|
|
if (txlen == 2)
|
|
dev->txpos++;
|
|
else if (txlen == 1)
|
|
dev->txstate = STATE_ESCAPE;
|
|
else
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
txlen = write_chunk(dev,
|
|
dev->txbuf + dev->txpos,
|
|
len);
|
|
if (txlen <= 0) {
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
dev->txfcs = crc_ccitt(dev->txfcs,
|
|
dev->txbuf +
|
|
dev->txpos,
|
|
txlen);
|
|
dev->txpos += txlen;
|
|
}
|
|
}
|
|
if (dev->txstate == STATE_DATA &&
|
|
dev->txpos == dev->txlen) {
|
|
dev->txstate = STATE_TRAILER;
|
|
dev->txpos = 0;
|
|
}
|
|
break;
|
|
}
|
|
dev->txstate = STATE_TRAILER;
|
|
dev->txpos = 0;
|
|
fallthrough;
|
|
|
|
case STATE_TRAILER:
|
|
buf[0] = dev->txfcs >> 8;
|
|
buf[1] = dev->txfcs & 0xff;
|
|
buf[2] = BYTE_FRAME;
|
|
txlen = write_chunk(dev, buf + dev->txpos, 3 - dev->txpos);
|
|
if (txlen <= 0) {
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
dev->txpos += txlen;
|
|
if (dev->txpos == 3) {
|
|
dev->txstate = STATE_DONE;
|
|
dev->txpos = 0;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
netdev_err_once(dev->netdev, "invalid tx state %d\n",
|
|
dev->txstate);
|
|
}
|
|
|
|
if (dev->txstate == STATE_DONE) {
|
|
dev->netdev->stats.tx_packets++;
|
|
dev->netdev->stats.tx_bytes += dev->txlen;
|
|
dev->txlen = 0;
|
|
dev->txpos = 0;
|
|
clear_bit(TTY_DO_WRITE_WAKEUP, &dev->tty->flags);
|
|
dev->txstate = STATE_IDLE;
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
|
|
netif_wake_queue(dev->netdev);
|
|
} else {
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
}
|
|
}
|
|
|
|
static netdev_tx_t mctp_serial_tx(struct sk_buff *skb, struct net_device *ndev)
|
|
{
|
|
struct mctp_serial *dev = netdev_priv(ndev);
|
|
unsigned long flags;
|
|
|
|
WARN_ON(dev->txstate != STATE_IDLE);
|
|
|
|
if (skb->len > MCTP_SERIAL_MTU) {
|
|
dev->netdev->stats.tx_dropped++;
|
|
goto out;
|
|
}
|
|
|
|
spin_lock_irqsave(&dev->lock, flags);
|
|
netif_stop_queue(dev->netdev);
|
|
skb_copy_bits(skb, 0, dev->txbuf, skb->len);
|
|
dev->txpos = 0;
|
|
dev->txlen = skb->len;
|
|
dev->txstate = STATE_START;
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
|
|
set_bit(TTY_DO_WRITE_WAKEUP, &dev->tty->flags);
|
|
schedule_work(&dev->tx_work);
|
|
|
|
out:
|
|
kfree_skb(skb);
|
|
return NETDEV_TX_OK;
|
|
}
|
|
|
|
static void mctp_serial_tty_write_wakeup(struct tty_struct *tty)
|
|
{
|
|
struct mctp_serial *dev = tty->disc_data;
|
|
|
|
schedule_work(&dev->tx_work);
|
|
}
|
|
|
|
static void mctp_serial_rx(struct mctp_serial *dev)
|
|
{
|
|
struct mctp_skb_cb *cb;
|
|
struct sk_buff *skb;
|
|
|
|
if (dev->rxfcs != dev->rxfcs_rcvd) {
|
|
dev->netdev->stats.rx_dropped++;
|
|
dev->netdev->stats.rx_crc_errors++;
|
|
return;
|
|
}
|
|
|
|
skb = netdev_alloc_skb(dev->netdev, dev->rxlen);
|
|
if (!skb) {
|
|
dev->netdev->stats.rx_dropped++;
|
|
return;
|
|
}
|
|
|
|
skb->protocol = htons(ETH_P_MCTP);
|
|
skb_put_data(skb, dev->rxbuf, dev->rxlen);
|
|
skb_reset_network_header(skb);
|
|
|
|
cb = __mctp_cb(skb);
|
|
cb->halen = 0;
|
|
|
|
netif_rx(skb);
|
|
dev->netdev->stats.rx_packets++;
|
|
dev->netdev->stats.rx_bytes += dev->rxlen;
|
|
}
|
|
|
|
static void mctp_serial_push_header(struct mctp_serial *dev, unsigned char c)
|
|
{
|
|
switch (dev->rxpos) {
|
|
case 0:
|
|
if (c == BYTE_FRAME)
|
|
dev->rxpos++;
|
|
else
|
|
dev->rxstate = STATE_ERR;
|
|
break;
|
|
case 1:
|
|
if (c == MCTP_SERIAL_VERSION) {
|
|
dev->rxpos++;
|
|
dev->rxfcs = crc_ccitt_byte(0, c);
|
|
} else {
|
|
dev->rxstate = STATE_ERR;
|
|
}
|
|
break;
|
|
case 2:
|
|
if (c > MCTP_SERIAL_FRAME_MTU) {
|
|
dev->rxstate = STATE_ERR;
|
|
} else {
|
|
dev->rxlen = c;
|
|
dev->rxpos = 0;
|
|
dev->rxstate = STATE_DATA;
|
|
dev->rxfcs = crc_ccitt_byte(dev->rxfcs, c);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void mctp_serial_push_trailer(struct mctp_serial *dev, unsigned char c)
|
|
{
|
|
switch (dev->rxpos) {
|
|
case 0:
|
|
dev->rxfcs_rcvd = c << 8;
|
|
dev->rxpos++;
|
|
break;
|
|
case 1:
|
|
dev->rxfcs_rcvd |= c;
|
|
dev->rxpos++;
|
|
break;
|
|
case 2:
|
|
if (c != BYTE_FRAME) {
|
|
dev->rxstate = STATE_ERR;
|
|
} else {
|
|
mctp_serial_rx(dev);
|
|
dev->rxlen = 0;
|
|
dev->rxpos = 0;
|
|
dev->rxstate = STATE_IDLE;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void mctp_serial_push(struct mctp_serial *dev, unsigned char c)
|
|
{
|
|
switch (dev->rxstate) {
|
|
case STATE_IDLE:
|
|
dev->rxstate = STATE_HEADER;
|
|
fallthrough;
|
|
case STATE_HEADER:
|
|
mctp_serial_push_header(dev, c);
|
|
break;
|
|
|
|
case STATE_ESCAPE:
|
|
c |= 0x20;
|
|
fallthrough;
|
|
case STATE_DATA:
|
|
if (dev->rxstate != STATE_ESCAPE && c == BYTE_ESC) {
|
|
dev->rxstate = STATE_ESCAPE;
|
|
} else {
|
|
dev->rxfcs = crc_ccitt_byte(dev->rxfcs, c);
|
|
dev->rxbuf[dev->rxpos] = c;
|
|
dev->rxpos++;
|
|
dev->rxstate = STATE_DATA;
|
|
if (dev->rxpos == dev->rxlen) {
|
|
dev->rxpos = 0;
|
|
dev->rxstate = STATE_TRAILER;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case STATE_TRAILER:
|
|
mctp_serial_push_trailer(dev, c);
|
|
break;
|
|
|
|
case STATE_ERR:
|
|
if (c == BYTE_FRAME)
|
|
dev->rxstate = STATE_IDLE;
|
|
break;
|
|
|
|
default:
|
|
netdev_err_once(dev->netdev, "invalid rx state %d\n",
|
|
dev->rxstate);
|
|
}
|
|
}
|
|
|
|
static void mctp_serial_tty_receive_buf(struct tty_struct *tty,
|
|
const unsigned char *c,
|
|
const char *f, int len)
|
|
{
|
|
struct mctp_serial *dev = tty->disc_data;
|
|
int i;
|
|
|
|
if (!netif_running(dev->netdev))
|
|
return;
|
|
|
|
/* we don't (currently) use the flag bytes, just data. */
|
|
for (i = 0; i < len; i++)
|
|
mctp_serial_push(dev, c[i]);
|
|
}
|
|
|
|
static void mctp_serial_uninit(struct net_device *ndev)
|
|
{
|
|
struct mctp_serial *dev = netdev_priv(ndev);
|
|
|
|
cancel_work_sync(&dev->tx_work);
|
|
}
|
|
|
|
static const struct net_device_ops mctp_serial_netdev_ops = {
|
|
.ndo_start_xmit = mctp_serial_tx,
|
|
.ndo_uninit = mctp_serial_uninit,
|
|
};
|
|
|
|
static void mctp_serial_setup(struct net_device *ndev)
|
|
{
|
|
ndev->type = ARPHRD_MCTP;
|
|
|
|
/* we limit at the fixed MTU, which is also the MCTP-standard
|
|
* baseline MTU, so is also our minimum
|
|
*/
|
|
ndev->mtu = MCTP_SERIAL_MTU;
|
|
ndev->max_mtu = MCTP_SERIAL_MTU;
|
|
ndev->min_mtu = MCTP_SERIAL_MTU;
|
|
|
|
ndev->hard_header_len = 0;
|
|
ndev->addr_len = 0;
|
|
ndev->tx_queue_len = DEFAULT_TX_QUEUE_LEN;
|
|
ndev->flags = IFF_NOARP;
|
|
ndev->netdev_ops = &mctp_serial_netdev_ops;
|
|
ndev->needs_free_netdev = true;
|
|
}
|
|
|
|
static int mctp_serial_open(struct tty_struct *tty)
|
|
{
|
|
struct mctp_serial *dev;
|
|
struct net_device *ndev;
|
|
char name[32];
|
|
int idx, rc;
|
|
|
|
if (!capable(CAP_NET_ADMIN))
|
|
return -EPERM;
|
|
|
|
if (!tty->ops->write)
|
|
return -EOPNOTSUPP;
|
|
|
|
idx = ida_alloc(&mctp_serial_ida, GFP_KERNEL);
|
|
if (idx < 0)
|
|
return idx;
|
|
|
|
snprintf(name, sizeof(name), "mctpserial%d", idx);
|
|
ndev = alloc_netdev(sizeof(*dev), name, NET_NAME_ENUM,
|
|
mctp_serial_setup);
|
|
if (!ndev) {
|
|
rc = -ENOMEM;
|
|
goto free_ida;
|
|
}
|
|
|
|
dev = netdev_priv(ndev);
|
|
dev->idx = idx;
|
|
dev->tty = tty;
|
|
dev->netdev = ndev;
|
|
dev->txstate = STATE_IDLE;
|
|
dev->rxstate = STATE_IDLE;
|
|
spin_lock_init(&dev->lock);
|
|
INIT_WORK(&dev->tx_work, mctp_serial_tx_work);
|
|
|
|
rc = register_netdev(ndev);
|
|
if (rc)
|
|
goto free_netdev;
|
|
|
|
tty->receive_room = 64 * 1024;
|
|
tty->disc_data = dev;
|
|
|
|
return 0;
|
|
|
|
free_netdev:
|
|
free_netdev(ndev);
|
|
|
|
free_ida:
|
|
ida_free(&mctp_serial_ida, idx);
|
|
return rc;
|
|
}
|
|
|
|
static void mctp_serial_close(struct tty_struct *tty)
|
|
{
|
|
struct mctp_serial *dev = tty->disc_data;
|
|
int idx = dev->idx;
|
|
|
|
unregister_netdev(dev->netdev);
|
|
ida_free(&mctp_serial_ida, idx);
|
|
}
|
|
|
|
static struct tty_ldisc_ops mctp_ldisc = {
|
|
.owner = THIS_MODULE,
|
|
.num = N_MCTP,
|
|
.name = "mctp",
|
|
.open = mctp_serial_open,
|
|
.close = mctp_serial_close,
|
|
.receive_buf = mctp_serial_tty_receive_buf,
|
|
.write_wakeup = mctp_serial_tty_write_wakeup,
|
|
};
|
|
|
|
static int __init mctp_serial_init(void)
|
|
{
|
|
return tty_register_ldisc(&mctp_ldisc);
|
|
}
|
|
|
|
static void __exit mctp_serial_exit(void)
|
|
{
|
|
tty_unregister_ldisc(&mctp_ldisc);
|
|
}
|
|
|
|
module_init(mctp_serial_init);
|
|
module_exit(mctp_serial_exit);
|
|
|
|
MODULE_LICENSE("GPL v2");
|
|
MODULE_AUTHOR("Jeremy Kerr <jk@codeconstruct.com.au>");
|
|
MODULE_DESCRIPTION("MCTP Serial transport");
|