Wednesday, May 27, 2015

Veeam Backup и PowerShell. Бэкапы по расписанию.

Чудный продукт Veeam Backup and Replication, начиная с версии 8 (Patch 2) открыл небольшую лазейку для совершения бэкапов по расписанию, точнее, VeeamZIP.
Точнее - в бесплатной версии заработал cmdlet  Start-VBRZip.
Итак, чем это грозит и как это можно использовать. А можно просто напилить скрипт, который будет делать полные бэкапы виртуальных машин. Ну и запихнуть его в планировщик.

Естественно, надо установить Veeam PowerShellSDK. Ну и в самом Veeam настроить все сервера.

Собственно, сам скрипт:
 param (
    [string]$csvFile = 'C:\ToBackup.txt', 
    [int]$storeDays = 7
    )

$doVMBackup = {
    param ($VMHost, $VMName, $VMStorePath, $VMLimitStore)
    try {
        Add-PSSnapin VeeamPSSnapin
        $viServerObject = Get-VBRServer -Name $VMHost
        $viObject = Find-VBRViEntity -Server $viServerObject -Name $VMName
        Start-VBRZip -Folder $VMStorePath -Entity $viObject
        if ($VMLimitStore -ne -1) {

            $limit = (Get-Date).AddDays(-$VMLimitStore)
            # Delete files older than the $limit.
            Get-ChildItem -Path $VMStorePath -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $limit } | Remove-Item -Force

            # Delete any empty directories left behind after deleting the old files.
            Get-ChildItem -Path $VMStorePath -Recurse -Force | Where-Object { $_.PSIsContainer -and (Get-ChildItem -Path $_.FullName -Recurse -Force | Where-Object { !$_.PSIsContainer }) -eq $null } | Remove-Item -Force -Recurse
        }
    }
    catch {
        write-host "An error occured...."
    }
}

$VMTotal = Import-Csv $csvFile
foreach ($VM in $VMTotal) {
    Start-Job -ScriptBlock $doVMBackup -ArgumentList $VM.Host,$VM.Name,$VM.Path,$storeDays



Скрипт, как параметр принимает 2 параметра - CSV файл c машинами, которые надо бэкапить и срок хранения бэкапов. Естественно, этот механизм можно переделать под себя.
Благодаря использованию Job задачи идут параллельно :)

Формат CSV файла:
Host, Name, Path
"10.10.10.10","MyVM","C:\BKPTest"
"<Host name in Veeam>","<VM name>","<Path to Backup>"

Спасибо ресурсам
http://memos.nomblot.org/veeam
http://go.veeam.com/learn-powershell-basics-free-tutorial-course/
Хабру и StackOverflow :)

Wednesday, May 6, 2015

Сервис определения страны по IP

Итак, возникла необходимость вычислять, так сказать по IP. А точнее - определять страну для автоматической подстановки телефонного кода. Как оказалось, за эту услугу хотят брать денюжки, например тут. Но хочется то халявного. Ну и по общим правилам опенсорса, если что-то надо - напиши сам. Тем более, база, пусть и не такая точная есть у того же MaxMind
Есть также и такая штука, но я что-то не очень уверен в ее скорости. Но можно попробовать.
В любом случае, родился такой сервис.
Абсолютно бесплатно, поддерживает https.
Используется связка Nginx + uwsgi + flask + postgresql. Почему так? Потому что хотелось попробовать, что есть фласк и постгре. Написано крайне коряво, потому что только учусь. Сейчас получается каждый запрос - это отдельное подключение к базе. Т.к. это все-таки сервер есть мнение переписать это под вариант одно подключение - много запросов и подключение держать постоянно. Но как это сделать я пока не знаю, посмотрим на нагрузку.
Ну и по традиции - код. Да, я знаю, он ужасен, в связи с чем попрошу в комментах наставить на путь истинный.

--
upd:

  • Постарался сделать так, чтобы соединение к базе было одно, а не дергать каждый раз.
  • Добавил метод автоопределения места запроса https://geoip.webcall.today/getcountry?ip=auto



from flask import Flask,request,jsonify
import socket
import psycopg2

def ip_address_is_valid(address):
    try: socket.inet_aton(address)
    except socket.error: return False
    else: return address.count('.') == 3

application = Flask(__name__)
con  = psycopg2.connect(database='geoip', user='geoip', host='localhost', password = 'geoipuser')

@application.route('/')
def hello_world():
    return "<b>Geoserver welcomes you. Usage = https://geoip.webcall.today/getcountry?ip=X.X.X.X</b><br> This product includes GeoLite2 data created by MaxMind, available from<br> <a href=\"http://www.maxmind.com\">http://www.maxmind.com</a>."

@application.route('/getcountry')
def getcountry():
    global con
    ip = request.args.get('ip', '')
    if ip == 'auto':
        ip = '%s' % request.remote_addr
    if ip_address_is_valid(ip):
        try:
            cur = con.cursor()
            cur.execute("SELECT code,location,fullname FROM countrydata WHERE geodata = (SELECT geodata FROM ipdata WHERE subnet >>= '%s'::inet LIMIT 1) LIMIT 1" % ip)
            result = cur.fetchone()
            if result:
                result = jsonify(code = result[0],
                                   location = result[1],
                                   name = result[2]), 200
            else:
                result = jsonify(code = 'none',
                                 location = 'None',
                                 name = 'None'), 404
        except psycopg2.DatabaseError, e:
            if con:
                con.rollback()
            con  = psycopg2.connect(database='geoip', user='geoip', host='localhost', password = 'geoipuser')
            result = jsonify(code = 'Error %s' % e,
                            location = 'None',
                            name = 'None'), 500
        finally:
            pass
    else:
        result = jsonify(code = 'none',
                         location = 'None',
                         name = 'None'), 404
    return result

if __name__ == '__main__':
    application.run(host='127.0.0.1')


Ну и ip_address_is_valid можно переписать с использованием регулярок. Что-то типа

--import socket
++import re

def ip_address_is_valid(address):
    regex_ip = "^(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.(\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))$"
    if re.match(regex_ip, address):
        return True
    return False

Friday, August 29, 2014

Звонок с сайта. Таки доделали.

Вот примерно такой сервис сделали....
Сейчас звонит только на мою голосовую почту, не так много у меня денег, чтобы напрямую на мобильный мне звонило, а SIP я с собой не таскаю постоянно :)
Можете оставлять сообщения, они мне на почту будут приходить :)

позвонить

Monday, June 23, 2014

Примитивный L7 DPI на iptables

Оказывается, начиная с ядра 2.6.14 в Debian (Ubuntu) iptables умеет анализировать пакет текстово с помощью модуля string.
Поэтому можно сделать некий примитивный DPI. У меня есть задача не пропускать на сервер только пакеты SIP REGISTER и INVITE (INVITE допускается только с определенных адресов). Поэтому родилось что-то типа

iptables -A INPUT -m string --string "INVITE sip:" --algo bm -i eth0 -s X.X.X.X/255.255.255.255 -p udp --dport 5060 -j ACCEPT
iptables -A INPUT -m string --string "INVITE sip:" --algo bm -i eth0 -p udp --dport 5060 -j DROP
iptables -A INPUT -m string --string "INVITE sip:" --algo bm -i eth0 -p tcp --dport 5060 -j DROP
iptables -A INPUT -m string --string "REGISTER sip:" --algo bm -i eth0 -p udp --dport 5060 -j DROP
iptables -A INPUT -m string --string "REGISTER sip:" --algo bm -i eth0 -p tcp --dport 5060 -j DROP



По мотивам: http://www.linuxquestions.org/questions/linux-networking-3/iptables-rules-against-udp-flood-and-ddos-attack-789950/

Wednesday, April 2, 2014

Попытки сделать полноценный SIP SBC или OpenSIPS в руках....

Итак.... История не стоит на месте и я тоже. Выпала задачка сделать масштабируемый и (возможно в будущем) высоконагруженный SBC для всяческих сервисов.
Схема, к которой я пришел не отличается оптимизированностью, зато довольно проста в масштабировании (я надеюсь) и эксплуатации.
Схема:

WAN <->  OpenSIPS (1.8) - - Asterisk (any)
                          |                 |
WAN <-> FreeSwitch (1.2) - ---------

Роли:
OpenSIPS - регистрация пользователей, регистрация на внешних сервисах, балансировка между FreeSwitch и Asterisk
FreeSwitch - Meдиа-прокси, при необходимости - транскодинг, скрытие топологии. Количество ограничено внешними IP-адресами.
Asterisk - Собственно, сервер приложений типа голосовой почты и прочего. Количество - в разумных пределах :). Почему, собственно, Asterisk? Потому что человек, который пишет под него приложения, знает AGI.
Схема работы примерно такая:
Пользователь набирает какой-то номер -> OpenSIPS проверяет авторизации и прочее и перенаправляет звонок по WAN на один из FreeSwith'ей -> FreeSwitch просто делает "заворот" WAN - LAN и отправляет звонок обратно на OpenSIPS - > OpenSIPS, видя, что звонок пришел от одного из FreeSwith'ей, балансирует его на один из Астерисков.

Собственно, сигнализация при таком звонке ходит так:
Пользователь (NAT/WAN) - (WAN) OpenSIPS (WAN) - (WAN) FreeSwitch (LAN) - (LAN) OpenSIPS (LAN) - (LAN) Asterisk.
Медиа же ходит так:
Пользователь (NAT/WAN) - (WAN) FreeSwitch (LAN) - (LAN) Asterisk.
 Т.е. в минимальной комплектации нужно 2 внешних IP адреса.

Далее пойдет файлик opensips.cfg с некоторыми комментариями:

#
# $Id: opensips.cfg 8758 2012-02-29 11:59:26Z vladut-paiu $
#
# OpenSIPS residential configuration script
#     by OpenSIPS Solutions
#
# This script was generated via "make menuconfig", from
#   the "Residential" scenario.
# You can enable / disable more features / functionalities by
#   re-generating the scenario with different options.#
#
# Please refer to the Core CookBook at:
#      http://www.opensips.org/Resources/DocsCookbooks
# for a explanation of possible statements, functions and parameters.
#


####### Global Parameters #########

debug=3
log_stderror=no
log_facility=LOG_LOCAL7

fork=yes
children=4

auto_aliases=yes

listen=udp:172.16.3.8:5060
listen=udp:XXX:5060
listen=udp:XXX:5060

#port=5060
disable_tcp=yes

####### Modules Section ########

#set module path
mpath="/usr/lib/opensips/modules/"


#### SIGNALING module
loadmodule "signaling.so"

#### StateLess module
loadmodule "sl.so"

#### MySQL module
loadmodule "db_mysql.so"


loadmodule "regex.so"
loadmodule "textops.so"
loadmodule "auth.so"
loadmodule "auth_db.so"
loadmodule "domain.so"
loadmodule "alias_db.so"

#### Transaction Module
loadmodule "tm.so"
modparam("tm", "fr_timeout", 5)
modparam("tm", "fr_inv_timeout", 30)
modparam("tm", "restart_fr_on_each_reply", 0)
modparam("tm", "onreply_avp_mode", 1)


#### Record Route Module
loadmodule "rr.so"
modparam("rr", "append_fromtag", 1)

#### MAX ForWarD module
loadmodule "maxfwd.so"

#### SIP MSG OPerationS module
loadmodule "sipmsgops.so"

#### FIFO Management Interface
loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/tmp/opensips_fifo")
modparam("mi_fifo", "fifo_mode", 0666)

#### URI module
loadmodule "uri.so"
modparam("uri", "use_uri_table", 0)

#### USeR LOCation module
loadmodule "usrloc.so"
modparam("usrloc", "nat_bflag", "NAT_BFLAG")
modparam("usrloc", "db_mode",   2)
modparam("usrloc", "matching_mode", 1)
modparam("usrloc", "cseq_delay", 5)
modparam("usrloc", "accept_replicated_contacts", 0)
modparam("usrloc", "skip_replicated_db_ops", 1)

#### User Agent Client module
loadmodule "uac_auth.so"
loadmodule "uac.so"
loadmodule "uac_registrant.so"
modparam("uac","restore_mode","auto")
modparam("uac_registrant", "timer_interval", 120)
modparam("uac_registrant", "hash_size", 2)


#### REGISTRAR module
loadmodule "registrar.so"

modparam("registrar", "received_avp", "$avp(42)")


/* uncomment the next line not to allow more than 10 contacts per AOR */
#modparam("registrar", "max_contacts", 10)

# ----- auth_db params -----
modparam("auth_db", "calculate_ha1", yes)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "load_credentials", "")

# ----- domain params -----

#modparam("domain", "db_url",DBURL)
modparam("domain", "db_mode", 1)   # Use caching

#modparam("alias_db", "db_url", DBURL)

# ----- multi-module params -----
/* uncomment the following line if you want to enable multi-domain support
   in the modules (dafault off) */

# ----- Dialog params ----
loadmodule "dialog.so"
modparam("dialog", "enable_stats", 0)


# ------ NAT Traversal Module ----

loadmodule "nat_traversal.so"
modparam("nat_traversal", "keepalive_interval", 60)
modparam("nat_traversal", "keepalive_method", "OPTIONS")
modparam("nat_traversal", "keepalive_from", "sip:pinger@XXX")
modparam("nat_traversal", "keepalive_extra_headers", "X-KeepAliveHeader: Is there any out there?\r\n")
modparam("nat_traversal", "keepalive_state_file", "/tmp/opensips_keepalive_state")

# ----- Load-Balancer params ---- Switched to stateless dispatcher
#loadmodule "load_balancer.so"
#modparam("load_balancer", "db_url", DBURL)
#modparam("load_balancer", "probing_interval", 60)
#modparam("load_balancer", "probing_reply_codes", "404, 200, 481")
#modparam("load_balancer", "probing_method", "INFO")


# ----- DISPATCHER Module ------
loadmodule "dispatcher.so"
modparam("dispatcher", "flags", 3)
modparam("dispatcher", "attrs_avp", "$avp(attrs_avp)")
modparam("dispatcher", "ds_ping_method", "INFO")
modparam("dispatcher", "ds_ping_from", "sip:dispatcher@XXX")
modparam("dispatcher", "ds_ping_interval", 30)
modparam("dispatcher", "ds_probing_threshhold", 10)
modparam("dispatcher", "options_reply_codes", "404, 200, 481")


# ------AVPops Module params ------
loadmodule "avpops.so"


# ------Stun Module -------

loadmodule "stun.so"
modparam("stun", "primary_ip", "XXX")
modparam("stun", "alternate_ip", "172.16.3.8")

# ------Presence User Agent Module -----

loadmodule "xcap.so"
loadmodule "xcap_client.so"

loadmodule "pua.so"
loadmodule "pua_mi.so"
loadmodule "pua_dialoginfo.so"
loadmodule "presence.so"
loadmodule "presence_dialoginfo.so"
loadmodule "presence_xml.so"
loadmodule "presence_mwi.so"
loadmodule "presence_xcapdiff.so"
loadmodule "rls.so"
loadmodule "mi_xmlrpc.so"

modparam("xcap", "integrated_xcap_server", 1)
modparam("xcap_client", "periodical_query", 0)

modparam("mi_xmlrpc", "port", 8888)

modparam("presence", "server_address", "sip:precense@XXX:5060")
modparam("presence", "fallback2db", 1)
modparam("presence", "clean_period",  30)

modparam("presence_xml", "force_active", 0)
modparam("presence_xml", "pidf_manipulation", 1)

modparam("rls", "server_address", "sip:rls@XXX:5060")
modparam("rls", "to_presence_code", 5)

modparam("pua_dialoginfo", "presence_server", "sip:precense@XXX:5060")


# ---- DBURL for all modules ------

modparam("avpops|alias_db|domain|auth_db|uac_registrant|usrloc|dialog|pua|presence|xcap|dispatcher", "db_url", "mysql://opensips:XXX@db1.local/opensips")

####### Routing Logic ########
# main request routing logic

route{

# ### Here will be list of proxies. Yes, it's duplicating with MySQL base, but I've not figured what to be best.
# may be here to make db queries.... Ok, let's make some tests later;

# Group 1
        $var(external_media_proxy) = "XXX";
# Group 2
        $var(internal_media_proxy) = "172.16.3.2";
# Group 3
        $var(media_server) = "172.16.3.16";
# Group 4
    $var(webrtc_proxy) = "172.16.3.7";

# ### Var ends


Небольшая защита от сканеров




        if (search("friendly-scanner|sipvicious|sipcli*")) {
        xlog("L_INFO", "IS_FRIENDLYSCANNER_MESSAGE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        exit;
    }

        if (search_body("friendly-scanner|sipvicious|sipcli")) {
        xlog("L_INFO", "IS_FRIENDLYSCANNER_BODY: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        exit;
    }


   

    if (!mf_process_maxfwd_header("10")) {
                xlog("L_INFO", "TOO_MANY_HOPS: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                exit;
        }


Этот кусок работает как-то странно и часто отвечает 503. Особенно в случае doubango webrtc2sip, хотя он уже не используется. На период тестов он закомменчен.


#        if (msg:len >= 4096 ) {
#                xlog("L_INFO", "MESSAGE_TOO_BIG: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
#                sl_send_reply("513", "Message too big");
#                exit;
#        }

        force_rport();

        if (client_nat_test("3")) {
                xlog("L_INFO", "CLIENT_NAT_TEST: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        fix_contact();
                if (is_method("REGISTER|SUBSCRIBE")) {
                        xlog("L_INFO", "CLIENT_NAT_TEST_REGISTERSUBSCRIBEINVITE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        nat_keepalive();
                }
        }   


    #Precense and BLF part....

    if (uri==myself) {
        if( is_method("PUBLISH|SUBSCRIBE|NOTIFY")) {
            route(PRESENCE);
        }
    }

        #Client behind NAT
   
        route(TO_TAG);

        route(CANCEL);

    route(IS_FROM_MEDIA_INTERNAL);

    route(IS_FROM_MEDIA_EXTERNAL);

    route(IS_FROM_MEDIASERVER);
   
    route(IS_FROM_WEB);

    route(IS_FROM_PROVIDER);

  
        # authenticate if from local subscriber. Works for local users, but not for REGISTER
        if (!(method =="REGISTER") && is_from_local()) {
                xlog("L_INFO", "NOTREGISTRATIONANDLOCAL: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                if (!proxy_authorize("", "subscriber")) {
                        proxy_challenge("", "0");
                        xlog("L_INFO", "NOTREGISTRATIONANDLOCAL_NOTPROXYAUTHORIZE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        exit;
                } else {
                        xlog("L_INFO", "NOTREGISTRATIONANDLOCAL_PROXYAUTHORIZE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                }
                if (!db_check_from()) {
                        send_reply("403","Forbidden auth ID");
                        xlog("L_INFO", "NOTREGISTRATIONANDLOCAL_NOTDBCHECKFROM: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        exit;
                } else {
                        xlog("L_INFO", "NOTREGISTRATIONANDLOCAL_DBCHECKFROM: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                }
                xlog("L_INFO", "NOTREGISTRATIONANDLOCAL_CONSUMECREDENTIALS: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                consume_credentials();
                # caller authenticated
        }

        # preloaded route checking
        route(LOOSE_ROUTE);

        # record routing
        if (!is_method("REGISTER|MESSAGE")) {
                xlog("L_INFO", "NOTREGISTERORMESSAGE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                record_route();
        }

        # account only INVITEs
        #if (is_method("INVITE")) {
        #        xlog("L_INFO", "INVITEACCOUNTING: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        #        setflag(1); # do accounting
        #}

        # if not a targetting a local SIP domain, just send it out
        # based on DNS (calls to foreign SIP domains)
        if (!is_uri_host_local()) {
                xlog("L_INFO", "NOTLOCALURI: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                append_hf("P-hint: outbound\r\n");
                t_on_failure("1");
                resetflag(8);
                route(1);
        }


        # requests for my domain
        # Register users on OpenSIPS

        if (is_method("REGISTER")) {
                xlog("L_INFO", "REGISTER: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                # authenticate the REGISTER requests
                if (!www_authorize("", "subscriber")) {
                        xlog("L_INFO", "REGISTER_NOTWWWAUTHORIZE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        www_challenge("", "0");
                        exit;
                } else {
                        xlog("L_INFO", "REGISTER_WWWAUTHORIZE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                }
                if (!db_check_to()) {
                        xlog("L_INFO", "REGISTER_NOTDBCHECKTO: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        send_reply("403","Forbidden auth ID");
                        exit;
                } else {
                        xlog("L_INFO", "REGISTER_DBCHECKTO: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                }

                if (!save("location")) {
                        xlog("L_INFO", "REGISTER_NOTSAVELOCATION: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        sl_reply_error();
                } else {
                        xlog("L_INFO", "REGISTER_SAVELOCATION: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                }
                exit;
        }

        # Some error processing
        route(ERROR);
        #Change aliases
        if(alias_db_lookup("dbaliases")) {
                xlog("L_INFO", "ALIASDBLOOKUP: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        }


        # Nave we found our user?
        if (!lookup("location")) {
                xlog("L_INFO", "NOTLOOKUPLOCATION: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                # Let's try here to loadbalance it to mediaproxy :)
                if (is_method("INVITE")) {
                        xlog("L_INFO", "LOOKUPLOCATION_INVITE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        ds_select_dst("1", "4");
                } else {
                        xlog("L_INFO", "LOOKUPLOCATION_NOTINVITE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        if (is_method("OPTIONS")) {
                                #Answer OPTIONS anyway
                                xlog("L_INFO", "LOOKUPLOCATION_NOTINVITE_OPTIONS: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                                sl_send_reply("200", "ALIVE OK");
                        }
                        exit;
                }
        } else {
                xlog("L_INFO", "LOOKUPLOCATION: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        }

        # when routing via usrloc, log the missed calls also
        #setflag(2);

        # arm a failure route in order to catch failed calls
        # targeting local subscribers; if we fail to deliver
        # the call to the user, we send the call to voicemail
        t_on_failure("1");
        resetflag(8);

        route(1);
}

route[1] {
        # Just relay it forward....
        if (!t_relay()) {
                xlog("L_INFO", "NOTTRELAY: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                sl_reply_error();
        }
    if (is_method("OPTIONS")) {
        xlog("L_INFO", "OPTIONS_ANSWER: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        sl_send_reply("200", "ALIVE OK");
    }
        exit;
}


route[TO_TAG] {
        if (has_totag()) {
                xlog("L_INFO", "HAS_TO_TAG: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                # sequential request withing a dialog should
                # take the path determined by record-routing
                if (loose_route()) {
                        route(1);
                } else {
                        if ( is_method("ACK") ) {
                                xlog("L_INFO", "HAS_TO_TAG_NOTLR_ACK: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                                if ( t_check_trans() ) {
                                        # non loose-route, but stateful ACK; must be an ACK after
                                        # a 487 or e.g. 404 from upstream server
                                        xlog("L_INFO", "HAS_TO_TAG_NOTLR_ACK_TRANS: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                                        t_relay();
                                        exit;
                                } else {
                                        # ACK without matching transaction ->
                                        # ignore and discard
                                        xlog("L_INFO", "HAS_TO_TAG_NOTLR_ACK_NOTTRANS: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                                        exit;
                                }
                        }
                        xlog("L_INFO", "HAS_TO_TAG_NOTLR: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        send_reply("404","Not here");
                }
                exit;
        }
}


route[CANCEL] {
        # CANCEL processing
        if (is_method("CANCEL")) {
                xlog("L_INFO", "CANCEL: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                if (t_check_trans())
                        t_relay();
                exit;
        }

        if (t_check_trans()) {
                xlog("L_INFO", "TRANSACTION: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        }

}

route[LOOSE_ROUTE] {
        if (loose_route()) {
                xlog("L_INFO", "LOOSEROUTE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                if (!is_method("ACK"))
                        xlog("L_INFO", "LOOSEROUTE_ACK: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        send_reply("403","Preload Route denied");
                exit;
        }
}

route[ERROR] {
        if ($rU==NULL) {
                # request with no Username in RURI
                xlog("L_INFO", "RURIISNULL: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                if (is_method("OPTIONS")) {
                        xlog("L_INFO", "RURIISNULL_OPTIONS: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        send_reply("200","ALIVE OK");
                }
                else {
                        send_reply("484","Address Incomplete");
                }
                exit;
        }
}


failure_route[1] {
        # authentication reply received?
        if ( t_check_status("40[17]") ) {
                # have we already tried to authenticate?
                if (isflagset(8)) {
                        t_reply("503","Authentication failed");
                        exit;
                }

Кусок, который отвечает за регистрацию на внешних сервисах. Т.е. ту последовательность REGISTER - 401 - REGISTER (digest) - 200
                if (uac_auth()) {
                        # mark that auth was performed
                        setflag(8);
                        # trigger again the failure route
                        t_on_failure("1");
                        # repeat the request with auth response this time
                        append_branch();
                        t_relay();
                }
        }

        if (t_was_cancelled()) {
                xlog("L_INFO", "FAILUREROUTE_TWASCANCELLED: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                exit;
        }

        # if the failure code is "408 - timeout" or "486 - busy",
        if (t_check_status("486|408")) {
                xlog("L_INFO", "FAILUREROUTE_TCHECKSTATUS: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                exit;
        }
}


route[ROUTINE] {

        route(CANCEL);
        route(LOOSE_ROUTE);
        route(ERROR);
}


route[IS_FROM_MEDIA_INTERNAL] {
        if ($(var(internal_media_proxy){param.exist,$si})) {
                xlog("L_INFO", "IS_FROM_MEDIA_INTERNAL: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                if (is_method("REGISTER|MESSAGE")) {
                        sl_send_reply("500", "Method not allowed");
                }
                route(ROUTINE);

                if (is_method("INVITE")) {
                        ds_select_dst("3", "4");
                }
                route(1);
                exit;
        }
    xlog("L_INFO", "IS_NOTFROM_MEDIA_INTERNAL: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
}

route[IS_FROM_MEDIA_EXTERNAL] {
        if ($(var(external_media_proxy){param.exist,$si})) {
                xlog("L_INFO", "IS_FROM_MEDIA_EXTERNAL: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci] + DD = $dd / $ds");
                if (is_method("REGISTER|MESSAGE")) {
                        sl_send_reply("500", "Method not allowed");
            exit;
                }
                route(ROUTINE);

        if (is_method("INVITE")) {
            if (!registered("location") && is_uri_host_local()) {
                xlog("L_INFO", "IS_FROM_MEDIA_EXTERNAL_INVITE_USER_NOTREG: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                sl_send_reply("404", "User not found");
                exit;
            } else {
                xlog("L_INFO", "IS_FROM_MEDIA_EXTERNAL_INVITE_USERREG: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
            }
        }
   

                route(1);

                exit;
        }
    xlog("L_INFO", "IS_NOTFROM_MEDIA_EXTERNAL: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
}

route[IS_FROM_MEDIASERVER] {
        if ($(var(media_server){param.exist,$si})) {
                xlog("L_INFO", "IS_FROM_MEDIASERVER: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                if (is_method("REGISTER|MESSAGE")) {
                        sl_send_reply("500", "Method not allowed");
            exit;
                }

                route(ROUTINE);

Тут разделение. В качестве фронта для webrtc выступает FreeSwitch, и на нем регаются клиенты с префиксом web_

                if (is_method("INVITE")) {
            if (pcre_match("$oU","^web_")) {
                            ds_select_dst("4", "4");
            } else {
                            ds_select_dst("2", "4");
            }
                }
                route(1);

                exit;
        }
    xlog("L_INFO", "IS_NOTFROM_MEDIASERVER: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
}


Для провайдеров заведена отдельная табличка.

route[IS_FROM_PROVIDER] {
    avp_db_query("SELECT IF( EXISTS( SELECT name FROM providers WHERE ip_address = INET_ATON('$si')), 1, 0)","$avp(is_provider)");
    if ($avp(is_provider)) {
        xlog("L_INFO", "IS_FROM_PROVIDER: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
        # Here will be some ideas about providers not handling DID. I really don't know what to do so. Test will tell us
        if (is_method("REGISTER|MESSAGE")) {
            xlog("L_INFO", "IS_FROM_PROVIDER_REGISTERORMESSAGE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
            sl_send_reply("500", "Method not allowed");
        }
        route(ROUTINE);
        if (is_method("INVITE")) {
            xlog("L_INFO", "IS_FROM_PROVIDER_INVITE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
            ds_select_dst("1", "4");
        }
        route(1);
        exit;
    }
    xlog("L_INFO", "IS_NOTFROM_PROVIDER: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
}


route[IS_FROM_WEB] {
        if ($(var(webrtc_proxy){param.exist,$si})) {
                xlog("L_INFO", "IS_FROM_WEB: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                if (is_method("REGISTER|MESSAGE")) {
            xlog("L_INFO", "IS_FROM_WEB_REGISTERORMESSAGE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        sl_send_reply("500", "Method not allowed");
            exit;
                }

                route(ROUTINE);

                if (is_method("INVITE")) {
            xlog("L_INFO", "IS_FROM_WEB_INVITE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                        ds_select_dst("3", "4");
                }
                route(1);

                exit;
        }
    xlog("L_INFO", "IS_NOTFROM_WEB: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");

}

route[PRESENCE] {
        if (!t_newtran()) {
            xlog("L_INFO", "PRESENCE_NOTNEWTRAN: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
            sl_reply_error();
            exit;
        };

        if(is_method("PUBLISH")) {
            xlog("L_INFO", "PRESENCE_PUBLISH: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
            handle_publish();
        } else {
            if(is_method("SUBSCRIBE")) {
                xlog("L_INFO", "PRESENCE_SUBSCRIBE: [F=$fu R=$ru D=$du M=$rm IP=($si:$sp $Ri:$Rp) ID=$ci]");
                rls_handle_subscribe();
                handle_subscribe();
            }
        }
        exit;
}



Да, тут много лишнего и некрасивого, но работа идет. В перспективе - интеграция с OpenXCAP для пристутствия и поддержка SIP_MESSAGE.
Но топология скрывается и звук ходит, что меня не может не радовать.

https://code.google.com/p/opensips-freeswitch-sbc/source/browse/ - Тут мои конфиги для opensips и freeswitch

-- upd.
Залита новая версия конфига. Добавлена авторизация провайдера по IP, добавлена некая защита от сканеров. Пока нету теста при регистрациях на внешних серверах. Также добавлена поддержка BLF (не проверено)
-- upd 2
Залит новый конфиг. Миграция на 1.11,  load_balancer сменен на dispatcher (потому что stateless), nathelper -> nat_traversal (потому что rtpproxy не используется), выпилен модуль accounting. Ну и мелкие изменения по ходу дела.
-- upd 3
На Гуглокод залит только конфиг OpenSIPS 1.11, теперь он может работать как  proxy-auth, т.е. полностью на себя берет вопрос аутентификации при звонке на внешние сервисы. Для этого пришлось сделать еще одну табличку в mysql с параметрами username, password, realm.

Thursday, February 21, 2013

LiveStreaming для тех, кого там нет

Возникла немного нетипичная задачка - устроить людям вещание в интернете. Для этого можно использовать много средств, но мы будем использовать Flussonic - в бесплатной версии он умеет все, что нам надо. Запись - это дело десятое.
После долгих мучений и репозиториев был выбран дистрибутив Ubuntu Server 12.04.2 LTS.

Устанавливается крайне просто:

echo "deb http://binaries.erlang-solutions.com/debian precise contrib"  >> /etc/apt/sources.list
echo "deb http://debian.erlyvideo.org binary/" >> /etc/apt/sources.list
wget -O - http://binaries.erlang-solutions.com/debian/erlang_solutions.asc | sudo apt-key add -
wget -q -O - http://debian.erlyvideo.org/binary/gpg.key | apt-key add -
apt-get update
apt-get -y install esl-erlang flussonic

Итак, источников вещания у нас 2.

1. IP-камера Axis 210 (Такое себе старье без H.264).

 Будем вещать через RTSP. Ее надо перекодировать. Для этого будем использовать VLC

apt-get install vlc x264

VLC выкладывает http поток на свой порт 9090, оттуда  его забирает Flussonic и отдает JwPlayer'у RTSP поток.

файл startvlc.sh

#!/bin/sh

TR='transcode{venc=x264{cabac=yes,bframes=0,keyint=125,qpmin=20,qpmax=50,ref=5,merange=24,mixed-refs=yes,direct=auto,me=umh,subme=7,trellis=2,weightb=yes,partitions=all,vbv-maxrate=920,vbv-bufsize=9000,ratetol=100.0,scenecut=60},vb=820,,width=720,height=576,hurry-up,audio-sync,deinterlace=yes,acodec=aac,aenc=avcodec{strict=-2},ab=64,channels=2,samplerate=48000}'

OUT="std{access=http,mux=ts{use-key-frames=1},dst=:9090}"

cvlc -vvvv rtsp://root:passw@CAMERA-IP/mpeg4/media.amp --intf dummy --sout '#'${TR}:${OUT}


echo "{stream, "vlc.ts", "tshttp://VLC-IP:9090/"}."  >> /etc/flussonic/flussonic.conf
/etc/init.d/flussonic start

После этого в панели по адресу FLUSSONIC-IP:8080 убедимся, что поток виден и его можно посмотреть.

Примерный код для index.html

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js">
  </script>
  <script type="text/javascript">
  var flashvars = {
    file : "vlc.ts",
    streamer:'rtmp://FLUSSONIC-IP:1935',
    'rtmp.tunneling':false,
    autostart: true
  };
  var paramObj = {allowScriptAccess : "always", allowFullScreen : "true", allowNetworking : "all"};
  swfobject.embedSWF("http://FLUSSONIC-IP:8080/flu/jwplayer.swf", "video", 720, 576,
    "10.3", "/flu/expressInstall.swf",
    flashvars, paramObj, {name: "jwplayer"});
  </script>

 2. Adobe FLME.

Будем забирать по HDS (что бы это ни значило :) )
Тут немного проще, указываем FMLE в адресе сервера наш flussonic в виде
Адрес: rtmp://FLUSSONIC-IP/live
Поток: livestream

AFLME выкладывает RTMP поток на Flussonic, оттуда он рестримит по HDS плееру.

В панели по адресу FLUSSONIC-IP:8080 убедимся, что поток виден и его можно посмотреть.

 Примерный код для index.html

 <div id="video"></div>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js">
  </script>
  <script type="text/javascript">
  var flashvars = {
    src : "http://FLUSSONIC-IP:8080/live/livestream/manifest.f4m",
    autoPlay: true
  };
  var paramObj = {allowScriptAccess : "always", allowFullScreen : "true", allowNetworking : "all"};
  swfobject.embedSWF("http://FLUSSONIC-IP:8080/flu/StrobeMediaPlayback.swf", "video", 720, 576,
    "10.3", "/flu/expressInstall.swf",
    flashvars, paramObj, {name: "StrobeMediaPlayback"});
</script>


 Вообще, советую приобщиться к инструкциям на сервере проекта :) Для знающих людей они будут полезнее моих :)

Wednesday, February 20, 2013

Экзотика с кодеками или silk+amrnb в Asterisk

Возникла задачка гонять голос по 3G сетям. После долгих и не особенно продуктивных мучений были выбраны кодеки speex и g729, как кандидаты на релиз. amr-nb патч существовал для версии 1.8, у меня же по разным причинам используется 10ка.
Но выбравшись из консоли и взглянув на мир, выяснилось, что для 10ки можно использовать кодек silk, он же используется для Skype и с недавнего времени стал open-source. Плюс, добрый человек в Одессе сделал патч для поддержки amrnb в 10й версии Астериска.

Все собиралось и тестировалось на Asterisk 10.12.1, Debian 6.0 x64 (OpenVZ)

1. SILK
Собирался по мотивам https://github.com/mordak/codec_silk
К стандартному набору для компилирования необходимо добавить curl. Это для работы скрипта, но никто не мешает скачать все руками.

Из отличий - в install_silk.sh заменил 


ln -sf "SILK_SDK_SRC_FIX_v$SKYPE_SILK_VER" silk
на  ln -sf "SILK_SDK_SRC_FLP_v$SKYPE_SILK_VER" silk Почему-то версия с фиксированной точкой у меня собираться отказывалась.
Также в astdir/codecs/silk/Makefile заменить
CFLAGS  += -Wall -enable-threads -O3
на
CFLAGS  += -Wall -enable-threads -O3 -fPIC
После этого все как обычно собирается.
После инсталляции в файл /etc/asterisk/codecs.conf добавить

[silk8]
type=silk
samprate=8000
fec=true
packetloss_percentage=10
maxbitrate=20000
dtx=false

[silk12]
type=silk
samprate=12000
fec=true
packetloss_percentage=10
maxbitrate=25000
dtx=false

[silk16]
type=silk
samprate=16000
fec=true
packetloss_percentage=10
maxbitrate=30000
dtx=false

[silk24]
type=silk
samprate=24000
fec=true
packetloss_percentage=10
maxbitrate=40000
dtx=false

Ну и в консоли.: asterisk -rx "core restart now"
И да, я знаю o бинарниках от Digium, но это не путь истинного джедая. Плюс, тот же добрый человек из Одессы упрекает их в глючности и не верить ему у меня оснований нет.

2. AMR-NB
Качаем патч с этой странички. Единственное что, если silk не компилируется вручную, то падает 2й hunk при патче. Просто в /codecs/Makefile необходимо добавить строчку
$(if $(filter codec_amr,$(EMBEDDED_MODS)),modules.link,codec_amr.so): $(LIBAMR)
После этого разархивируем в папку /codecs/amr содержимое следующего архива.
Переходим в папку /codecs/amr

./configure CFLAGS = "-g -O2 -fPIC"
make

После этого все как обычно собирается.
После инсталляции в файл /etc/asterisk/codecs.conf добавить

[amr]
octet-aligned=0
;codec_amr: Must be one of MR475, MR515, MR59, MR67, MR74, MR795, MR102, MR122
mode = MR475
dtx=0
cng = 1
vad = 0

octet-aligned=0 - Это следствие того, что CSipSimple (точнее, pjsip) умеет  AMR только в Bandwidth-Efficient режиме. Что это такое, можно почитать в RFC.
Ну и в консоли.: asterisk -rx "core restart now"
Теперь по core show translation  можно увидеть, что у нас появился целый набор silk'ов и amr.