/* * Copyright (C) 2009 Barracuda Networks, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include #include #include #include #include #include #include #include #include #include #include #include // scope values: 0 = local, 1 = link-local, 2 = private, 3 = public static int getAddressScope(const QHostAddress &a) { if(a.protocol() == QAbstractSocket::IPv6Protocol) { if(a == QHostAddress(QHostAddress::LocalHostIPv6)) return 0; else if(XMPP::Ice176::isIPv6LinkLocalAddress(a)) return 1; } else if(a.protocol() == QAbstractSocket::IPv4Protocol) { quint32 v4 = a.toIPv4Address(); quint8 a0 = v4 >> 24; quint8 a1 = (v4 >> 16) & 0xff; if(a0 == 127) return 0; else if(a0 == 169 && a1 == 254) return 1; else if(a0 == 10) return 2; else if(a0 == 172 && a1 >= 16 && a1 <= 31) return 2; else if(a0 == 192 && a1 == 168) return 2; } return 3; } // -1 = a is higher priority, 1 = b is higher priority, 0 = equal static int comparePriority(const QHostAddress &a, const QHostAddress &b) { // prefer closer scope int a_scope = getAddressScope(a); int b_scope = getAddressScope(b); if(a_scope < b_scope) return -1; else if(a_scope > b_scope) return 1; // prefer ipv6 if(a.protocol() == QAbstractSocket::IPv6Protocol && b.protocol() != QAbstractSocket::IPv6Protocol) return -1; else if(b.protocol() == QAbstractSocket::IPv6Protocol && a.protocol() != QAbstractSocket::IPv6Protocol) return 1; return 0; } static QList sortAddrs(const QList &in) { QList out; foreach(const QHostAddress &a, in) { int at; for(at = 0; at < out.count(); ++at) { if(comparePriority(a, out[at]) < 0) break; } out.insert(at, a); } return out; } // some encoding routines taken from psimedia demo static QString urlishEncode(const QString &in) { QString out; for(int n = 0; n < in.length(); ++n) { if(in[n] == '%' || in[n] == ',' || in[n] == ';' || in[n] == ' ' || in[n] == '\n') { unsigned char c = (unsigned char)in[n].toLatin1(); out += QString().sprintf("%%%02x", c); } else out += in[n]; } return out; } static QString urlishDecode(const QString &in, bool *ok = 0) { QString out; for(int n = 0; n < in.length(); ++n) { if(in[n] == '%') { if(n + 2 >= in.length()) { if(ok) *ok = false; return QString(); } QString hex = in.mid(n + 1, 2); bool b; int x = hex.toInt(&b, 16); if(!b) { if(ok) *ok = false; return QString(); } unsigned char c = (unsigned char)x; out += c; n += 2; } else out += in[n]; } if(ok) *ok = true; return out; } static QString candidate_to_line(const XMPP::Ice176::Candidate &in) { QStringList list; list += QString::number(in.component); list += in.foundation; list += QString::number(in.generation); list += in.id; list += in.ip.toString(); list += QString::number(in.network); list += QString::number(in.port); list += QString::number(in.priority); list += in.protocol; list += in.rel_addr.toString(); list += QString::number(in.rel_port); list += in.rem_addr.toString(); list += QString::number(in.rem_port); list += in.type; for(int n = 0; n < list.count(); ++n) list[n] = urlishEncode(list[n]); return list.join(","); } static XMPP::Ice176::Candidate line_to_candidate(const QString &in) { QStringList list = in.split(','); if(list.count() < 14) return XMPP::Ice176::Candidate(); for(int n = 0; n < list.count(); ++n) { bool ok; QString str = urlishDecode(list[n], &ok); if(!ok) return XMPP::Ice176::Candidate(); list[n] = str; } XMPP::Ice176::Candidate out; out.component = list[0].toInt(); out.foundation = list[1]; out.generation = list[2].toInt(); out.id = list[3]; out.ip = QHostAddress(list[4]); out.network = list[5].toInt(); out.port = list[6].toInt(); out.priority = list[7].toInt(); out.protocol = list[8]; out.rel_addr = QHostAddress(list[9]); out.rel_port = list[10].toInt(); out.rem_addr = QHostAddress(list[11]); out.rem_port = list[12].toInt(); out.type = list[13]; return out; } class IceOffer { public: QString user, pass; QList candidates; }; static QStringList line_wrap(const QString &in, int maxlen) { Q_ASSERT(maxlen >= 1); QStringList out; int at = 0; while(at < in.length()) { int takeAmount = qMin(maxlen, in.length() - at); out += in.mid(at, takeAmount); at += takeAmount; } return out; } static QString lines_unwrap(const QStringList &in) { return in.join(QString()); } static QStringList iceblock_create(const IceOffer &in) { QStringList out; out += "-----BEGIN ICE-----"; { QStringList body; QStringList userpass; userpass += urlishEncode(in.user); userpass += urlishEncode(in.pass); body += userpass.join(","); foreach(const XMPP::Ice176::Candidate &c, in.candidates) body += candidate_to_line(c); out += line_wrap(body.join(";"), 78); } out += "-----END ICE-----"; return out; } static IceOffer iceblock_parse(const QStringList &in) { IceOffer out; if(in.count() < 3 || in[0] != "-----BEGIN ICE-----" || in[in.count()-1] != "-----END ICE-----") return IceOffer(); QStringList body = lines_unwrap(in.mid(1, in.count() - 2)).split(';'); if(body.count() < 2) return IceOffer(); QStringList parts = body[0].split(','); if(parts.count() != 2) return IceOffer(); bool ok; out.user = urlishDecode(parts[0], &ok); if(!ok || out.user.isEmpty()) return IceOffer(); out.pass = urlishDecode(parts[1], &ok); if(!ok || out.pass.isEmpty()) return IceOffer(); for(int n = 1; n < body.count(); ++n) { XMPP::Ice176::Candidate c = line_to_candidate(body[n]); if(c.type.isEmpty()) return IceOffer(); out.candidates += c; } return out; } class IceBlockReader : public QObject { Q_OBJECT public: QCA::ConsoleReference *con; QByteArray in; IceBlockReader(QObject *parent = 0) : QObject(parent) { bool ok; con = new QCA::ConsoleReference(this); connect(con, SIGNAL(readyRead()), SLOT(con_readyRead())); connect(con, SIGNAL(inputClosed()), SLOT(con_inputClosed())); ok = con->start(QCA::Console::stdioInstance()); Q_ASSERT(ok); } signals: void finished(const QStringList &lines); void error(); private slots: void con_readyRead() { in += con->read(); if(in.contains("-----END ICE-----")) { delete con; con = 0; QStringList out; QBuffer buf(&in); buf.open(QIODevice::ReadOnly | QIODevice::Text); QTextStream ts(&buf); while(!ts.atEnd()) out += ts.readLine(); emit finished(out); } } void con_inputClosed() { delete con; con = 0; emit error(); } }; class EnterPrompt : public QObject { Q_OBJECT public: QCA::ConsoleReference *con; EnterPrompt(QObject *parent = 0) : QObject(parent) { bool ok; con = new QCA::ConsoleReference(this); connect(con, SIGNAL(readyRead()), SLOT(con_readyRead())); connect(con, SIGNAL(inputClosed()), SLOT(con_inputClosed())); ok = con->start(QCA::Console::stdioInstance()); Q_ASSERT(ok); } signals: void finished(); void error(); private slots: void con_readyRead() { if(con->read().contains('\n')) { delete con; con = 0; emit finished(); } } void con_inputClosed() { delete con; con = 0; emit error(); } }; /*static QStringList iceblock_read() { QStringList out; FILE *fp = stdin; while(1) { QByteArray line(1024, 0); if(fgets(line.data(), line.size(), fp) == NULL) return QStringList(); if(feof(fp)) break; // hack off newline line.resize(qstrlen(line.data()) - 1); QString str = QString::fromLocal8Bit(line); out += str; if(str == "-----END ICE-----") break; } return out; }*/ /*static void wait_for_enter() { QByteArray buf(1024, 0); if(fgets(buf.data(), buf.size(), stdin) == NULL) return; }*/ class App : public QObject { Q_OBJECT public: class Channel { public: QUdpSocket *sock6, *sock4; bool ready; }; enum StunServiceType { Auto, Basic, Relay }; int opt_mode; int opt_localBase; int opt_iceBase; int opt_channels; QString opt_stunHost; int opt_stunPort; StunServiceType opt_stunType; QString opt_user, opt_pass; bool opt_ipv6_only, opt_relay_udp_only, opt_relay_tcp_only; XMPP::NameResolver dns; QHostAddress stunAddr; XMPP::UdpPortReserver portReserver; XMPP::Ice176 *ice; QList localAddrs; QList channels; QCA::Console *console; IceBlockReader *reader; EnterPrompt *prompt; IceOffer inOffer; App() : portReserver(this), ice(0), console(0), reader(0), prompt(0) { } ~App() { delete prompt; delete reader; delete console; delete ice; for(int n = 0; n < channels.count(); ++n) { if(channels[n].sock6) { channels[n].sock6->disconnect(this); channels[n].sock6->setParent(0); channels[n].sock6->deleteLater(); channels[n].sock6 = 0; } if(channels[n].sock4) { channels[n].sock4->disconnect(this); channels[n].sock4->setParent(0); channels[n].sock4->deleteLater(); channels[n].sock4 = 0; } } } public slots: void start() { connect(XMPP::ProcessQuit::instance(), SIGNAL(quit()), SLOT(do_quit())); connect(&dns, SIGNAL(resultsReady(const QList &)), SLOT(dns_resultsReady(const QList &))); connect(&dns, SIGNAL(error(XMPP::NameResolver::Error)), SLOT(dns_error(XMPP::NameResolver::Error))); if(!opt_stunHost.isEmpty()) dns.start(opt_stunHost.toLatin1(), XMPP::NameRecord::A); else start_ice(); } public: void start_ice() { ice = new XMPP::Ice176(this); connect(ice, SIGNAL(started()), SLOT(ice_started())); connect(ice, SIGNAL(stopped()), SLOT(ice_stopped())); connect(ice, SIGNAL(error(XMPP::Ice176::Error)), SLOT(ice_error(XMPP::Ice176::Error))); connect(ice, SIGNAL(localCandidatesReady(const QList &)), SLOT(ice_localCandidatesReady(const QList &))); connect(ice, SIGNAL(componentReady(int)), SLOT(ice_componentReady(int))); connect(ice, SIGNAL(readyRead(int)), SLOT(ice_readyRead(int))); connect(ice, SIGNAL(datagramsWritten(int, int)), SLOT(ice_datagramsWritten(int, int))); // set up local ports for forwarding for(int n = 0; n < opt_channels; ++n) { Channel chan; int port = opt_localBase + 32 + n; chan.sock6 = setupSocket(QHostAddress::LocalHostIPv6, port); chan.sock4 = setupSocket(QHostAddress::LocalHost, port); if(!chan.sock6 && !chan.sock4) { printf("Unable to bind to port %d.\n", port); emit quit(); return; } chan.ready = false; channels += chan; } QList listenAddrs; if(!opt_relay_tcp_only) { /*XMPP::NetInterfaceManager netman; foreach(const QString &id, netman.interfaces()) { XMPP::NetInterface ni(id, &netman); QHostAddress v4addr; foreach(const QHostAddress &h, ni.addresses()) { if(h.protocol() == QAbstractSocket::IPv4Protocol) { v4addr = h; break; } } if(!v4addr.isNull()) { XMPP::Ice176::LocalAddress addr; addr.addr = v4addr; localAddrs += addr; strList += addr.addr.toString(); } }*/ foreach(const QNetworkInterface &ni, QNetworkInterface::allInterfaces()) { QList entries = ni.addressEntries(); foreach(const QNetworkAddressEntry &na, entries) { QHostAddress h = na.ip(); if(opt_ipv6_only && h.protocol() != QAbstractSocket::IPv6Protocol) continue; // skip localhost if(getAddressScope(h) == 0) continue; // don't put the same address in twice. // this also means that if there are // two link-local ipv6 interfaces // with the exact same address, we // only use the first one if(!listenAddrs.contains(h)) { if(h.protocol() == QAbstractSocket::IPv6Protocol && XMPP::Ice176::isIPv6LinkLocalAddress(h)) h.setScopeId(ni.name()); listenAddrs += h; } } } } /*{ XMPP::Ice176::LocalAddress addr; addr.addr = QHostAddress::LocalHost; localAddrs += addr; strList += addr.addr.toString(); }*/ listenAddrs = sortAddrs(listenAddrs); QStringList strList; foreach(const QHostAddress &h, listenAddrs) //QNetworkInterface::allAddresses()) { XMPP::Ice176::LocalAddress addr; addr.addr = h; localAddrs += addr; strList += h.toString(); } if(opt_iceBase > 0) { portReserver.setAddresses(listenAddrs); portReserver.setPorts(opt_iceBase, opt_channels); ice->setPortReserver(&portReserver); } ice->setLocalAddresses(localAddrs); if(!strList.isEmpty()) { printf("Host addresses:\n"); foreach(const QString &s, strList) printf(" %s\n", qPrintable(s)); } ice->setComponentCount(opt_channels); ice->setLocalCandidateTrickle(false); if(!stunAddr.isNull()) { if(opt_stunType == Basic || opt_stunType == Auto) ice->setStunBindService(stunAddr, opt_stunPort); if((opt_stunType == Relay || opt_stunType == Auto) && !opt_user.isEmpty()) { ice->setStunRelayUdpService(stunAddr, opt_stunPort, opt_user, opt_pass.toUtf8()); ice->setStunRelayTcpService(stunAddr, opt_stunPort, opt_user, opt_pass.toUtf8()); } printf("STUN service: %s\n", qPrintable(stunAddr.toString())); } if(opt_relay_udp_only) { ice->setUseLocal(false); ice->setUseStunBind(false); ice->setUseStunRelayTcp(false); } if(opt_mode == 0) ice->start(XMPP::Ice176::Initiator); else ice->start(XMPP::Ice176::Responder); } signals: void quit(); private: QUdpSocket *setupSocket(const QHostAddress &addr, int port) { QUdpSocket *sock = new QUdpSocket(this); connect(sock, SIGNAL(readyRead()), SLOT(sock_readyRead())); connect(sock, SIGNAL(bytesWritten(qint64)), SLOT(sock_bytesWritten(qint64))); if(!sock->bind(addr, port)) { delete sock; return 0; } return sock; } private slots: void do_quit() { // a good idea XMPP::ProcessQuit::cleanup(); if(ice) { printf("Stopping ICE.\n"); ice->stop(); } else emit quit(); } void dns_resultsReady(const QList &results) { stunAddr = results.first().address(); start_ice(); } void dns_error(XMPP::NameResolver::Error e) { Q_UNUSED(e); printf("Unable to resolve stun host.\n"); emit quit(); } void ice_started() { if(channels.count() > 1) { printf("Local ports: %d-%d\n", opt_localBase, opt_localBase + channels.count() - 1); printf("Tunnel ports: %d-%d\n", opt_localBase + 32, opt_localBase + 32 + channels.count() - 1); } else { printf("Local port: %d\n", opt_localBase); printf("Tunnel port: %d\n", opt_localBase + 32); } } void ice_stopped() { emit quit(); } void ice_error(XMPP::Ice176::Error e) { printf("ICE error: %d\n", e); emit quit(); } void ice_localCandidatesReady(const QList &list) { IceOffer out; out.user = ice->localUfrag(); out.pass = ice->localPassword(); out.candidates = list; QStringList block = iceblock_create(out); foreach(const QString &s, block) printf("%s\n", qPrintable(s)); printf("Give above ICE block to peer. Obtain peer ICE block and paste below...\n"); console = new QCA::Console(QCA::Console::Stdio, QCA::Console::Read, QCA::Console::Default, this); reader = new IceBlockReader(this); connect(reader, SIGNAL(finished(const QStringList &)), SLOT(reader_finished(const QStringList &))); connect(reader, SIGNAL(error()), SLOT(reader_error())); } void ice_componentReady(int index) { printf("Channel %d ready.\n", index); channels[index].ready = true; bool allReady = true; for(int n = 0; n < channels.count(); ++n) { if(!channels[n].ready) { allReady = false; break; } } if(allReady) { printf("Tunnel established!\n"); } } void ice_readyRead(int componentIndex) { while(ice->hasPendingDatagrams(componentIndex)) { QByteArray buf = ice->readDatagram(componentIndex); if(channels[componentIndex].sock6) channels[componentIndex].sock6->writeDatagram(buf, QHostAddress::LocalHostIPv6, opt_localBase + componentIndex); if(channels[componentIndex].sock4) channels[componentIndex].sock4->writeDatagram(buf, QHostAddress::LocalHost, opt_localBase + componentIndex); } } void ice_datagramsWritten(int componentIndex, int count) { Q_UNUSED(componentIndex); Q_UNUSED(count); // do nothing } void reader_finished(const QStringList &lines) { delete reader; reader = 0; inOffer = iceblock_parse(lines); if(inOffer.user.isEmpty()) { printf("Error parsing ICE block.\n"); emit quit(); return; } printf("Press enter to begin.\n"); prompt = new EnterPrompt(this); connect(prompt, SIGNAL(finished()), SLOT(prompt_finished())); connect(prompt, SIGNAL(finished()), SLOT(prompt_error())); } void reader_error() { delete reader; reader = 0; printf("Unable to read stdin.\n"); emit quit(); } void prompt_finished() { delete prompt; prompt = 0; ice->setPeerUfrag(inOffer.user); ice->setPeerPassword(inOffer.pass); ice->addRemoteCandidates(inOffer.candidates); } void prompt_error() { delete prompt; prompt = 0; printf("Unable to read stdin.\n"); emit quit(); } void sock_readyRead() { QUdpSocket *sock = (QUdpSocket *)sender(); int at = -1; for(int n = 0; n < channels.count(); ++n) { if(channels[n].sock6 == sock || channels[n].sock4 == sock) { at = n; break; } } Q_ASSERT(at != -1); while(sock->hasPendingDatagrams()) { QByteArray buf; buf.resize(sock->pendingDatagramSize()); // note: we don't care who sent it sock->readDatagram(buf.data(), buf.size()); if(channels[at].ready) ice->writeDatagram(at, buf); } } void sock_bytesWritten(qint64 bytes) { Q_UNUSED(bytes); // do nothing } }; void usage() { printf("icetunnel: create a peer-to-peer UDP tunnel based on ICE\n"); printf("usage: icetunnel initiator (options)\n"); printf(" icetunnel responder (options)\n"); printf("\n"); printf(" --localbase=[n] local base port (default=60000)\n"); printf(" --icebase=[n] ICE base port (default=0 (None))\n"); printf(" --channels=[n] number of channels to create (default=1)\n"); printf(" --stunhost=[host] STUN server to use\n"); printf(" --stunport=[n] STUN server port to use (default=3478)\n"); printf(" --stuntype=[type] auto, basic, or relay (default=auto)\n"); printf(" --user=[user] STUN server username\n"); printf(" --pass=[pass] STUN server password\n"); printf(" --ipv6-only only use IPv6 network interface addresses\n"); printf(" --relay-udp-only only offer UDP relay candidate\n"); printf(" --relay-tcp-only only offer TCP relay candidate\n"); printf("\n"); } int main(int argc, char **argv) { QCA::Initializer qcaInit; QCoreApplication qapp(argc, argv); QStringList args = qapp.arguments(); args.removeFirst(); int localBase = 60000; int iceBase = 0; int channels = 1; QString stunHost; int stunPort = 3478; App::StunServiceType stunType = App::Auto; QString user, pass; bool ipv6_only = false; bool relay_udp_only = false; bool relay_tcp_only = false; for(int n = 0; n < args.count(); ++n) { QString s = args[n]; if(!s.startsWith("--")) continue; QString var; QString val; int x = s.indexOf('='); if(x != -1) { var = s.mid(2, x - 2); val = s.mid(x + 1); } else { var = s.mid(2); } bool known = true; if(var == "localbase") localBase = val.toInt(); else if(var == "icebase") iceBase = val.toInt(); else if(var == "channels") { channels = val.toInt(); if(channels < 1 || channels > 32) { fprintf(stderr, "Number of channels must be between 1-32.\n"); return 1; } } else if(var == "stunhost") stunHost = val; else if(var == "stunport") stunPort = val.toInt(); else if(var == "stuntype") { if(val == "auto") stunType = App::Auto; else if(val == "basic") stunType = App::Basic; else if(val == "relay") stunType = App::Relay; else { usage(); return 1; } } else if(var == "user") user = val; else if(var == "pass") pass = val; else if(var == "ipv6-only") ipv6_only = true; else if(var == "relay-udp-only") relay_udp_only = true; else if(var == "relay-tcp-only") relay_tcp_only = true; else known = false; if(!known) { fprintf(stderr, "Unknown option '%s'.\n", qPrintable(var)); return 1; } args.removeAt(n); --n; // adjust position } if(args.isEmpty()) { usage(); return 1; } if(relay_udp_only && relay_tcp_only) { fprintf(stderr, "Cannot use both --relay-udp-only and --relay-tcp-only.\n"); return 1; } int mode = -1; if(args[0] == "initiator") mode = 0; else if(args[0] == "responder") mode = 1; if(mode == -1) { usage(); return 1; } if(!QCA::isSupported("hmac(sha1)")) { printf("Error: Need hmac(sha1) support.\n"); return 1; } App app; app.opt_mode = mode; app.opt_localBase = localBase; app.opt_iceBase = iceBase; app.opt_channels = channels; app.opt_stunHost = stunHost; app.opt_stunPort = stunPort; app.opt_stunType = stunType; app.opt_user = user; app.opt_pass = pass; app.opt_ipv6_only = ipv6_only; app.opt_relay_udp_only = relay_udp_only; app.opt_relay_tcp_only = relay_tcp_only; QObject::connect(&app, SIGNAL(quit()), &qapp, SLOT(quit())); QTimer::singleShot(0, &app, SLOT(start())); qapp.exec(); return 0; } #include "main.moc"